From 6b2616453ffe7430295bea3d3ccbf06ada67e033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sat, 23 May 2026 13:07:02 +0200 Subject: [PATCH 01/14] Make it possible to retrieve both resolved and unresolved reports by api (#38323) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../api/v1/admin/reports_controller.rb | 1 + app/models/report_filter.rb | 17 +++++++++++++---- spec/requests/api/v1/admin/reports_spec.rb | 11 +++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/v1/admin/reports_controller.rb b/app/controllers/api/v1/admin/reports_controller.rb index 9b5beeab67..765996eb3c 100644 --- a/app/controllers/api/v1/admin/reports_controller.rb +++ b/app/controllers/api/v1/admin/reports_controller.rb @@ -16,6 +16,7 @@ class Api::V1::Admin::ReportsController < Api::BaseController FILTER_PARAMS = %i( resolved + unresolved account_id target_account_id ).freeze diff --git a/app/models/report_filter.rb b/app/models/report_filter.rb index 9d2b0fb374..dab45007bc 100644 --- a/app/models/report_filter.rb +++ b/app/models/report_filter.rb @@ -3,6 +3,7 @@ class ReportFilter KEYS = %i( resolved + unresolved account_id target_account_id by_target_domain @@ -16,7 +17,7 @@ class ReportFilter end def results - scope = Report.unresolved + scope = status_scope relevant_params.each do |key, value| scope = scope.merge scope_for(key, value) @@ -28,7 +29,7 @@ class ReportFilter private def relevant_params - params.tap do |args| + params.except(:resolved, :unresolved).tap do |args| args.delete(:target_origin) if origin_is_remote_and_domain_present? end end @@ -37,12 +38,20 @@ class ReportFilter params[:target_origin] == 'remote' && params[:by_target_domain].present? end + def status_scope + resolved = params.key?(:resolved) + unresolved = params.key?(:unresolved) + + return Report.all if resolved && unresolved + return Report.resolved if resolved + + Report.unresolved + end + def scope_for(key, value) case key.to_sym when :by_target_domain Report.where(target_account: Account.where(domain: value)) - when :resolved - Report.resolved when :account_id Report.where(account_id: value) when :target_account_id diff --git a/spec/requests/api/v1/admin/reports_spec.rb b/spec/requests/api/v1/admin/reports_spec.rb index 54dd4c9c8c..432e7f47a2 100644 --- a/spec/requests/api/v1/admin/reports_spec.rb +++ b/spec/requests/api/v1/admin/reports_spec.rb @@ -78,6 +78,17 @@ RSpec.describe 'Reports' do end end + context 'with both resolved and unresolved params' do + let(:params) { { resolved: true, unresolved: true } } + let(:scope) { Report.all } + + it 'returns all reports' do + subject + + expect(response.parsed_body).to match_array(expected_response) + end + end + context 'with account_id param' do let(:params) { { account_id: reporter.id } } let(:scope) { Report.unresolved.where(account: reporter) } From 71da0c46483f2970befff84eb7f0fdf66b58c197 Mon Sep 17 00:00:00 2001 From: Coro Date: Tue, 26 May 2026 17:53:11 +0900 Subject: [PATCH 02/14] Fix bio text overflow (#39160) --- app/javascript/styles/mastodon/components.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 25265433fa..3848b76377 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -10778,6 +10778,7 @@ noscript { -webkit-box-orient: vertical; max-height: 2 * 20px; overflow: hidden; + overflow-wrap: anywhere; p { margin-bottom: 0; From 041100365f22ebaea3b87798dcb42867bec660b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 12:19:51 +0200 Subject: [PATCH 03/14] Update dependency chromatic to v17 (#39094) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 2cc867c007..3edde49a71 100644 --- a/package.json +++ b/package.json @@ -162,7 +162,7 @@ "@vitest/browser-playwright": "^4.1.0", "@vitest/coverage-v8": "^4.1.0", "@vitest/ui": "^4.1.0", - "chromatic": "^16.0.0", + "chromatic": "^17.0.0", "eslint": "^9.39.2", "eslint-import-resolver-typescript": "^4.2.5", "eslint-plugin-formatjs": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index e596521858..48f731f1eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2963,7 +2963,7 @@ __metadata: axios: "npm:^1.4.0" babel-plugin-transform-react-remove-prop-types: "npm:^0.4.24" blurhash: "npm:^2.0.5" - chromatic: "npm:^16.0.0" + chromatic: "npm:^17.0.0" classnames: "npm:^2.3.2" cocoon-js-vanilla: "npm:^1.5.1" color-blend: "npm:^4.0.0" @@ -6821,22 +6821,27 @@ __metadata: languageName: node linkType: hard -"chromatic@npm:^16.0.0": - version: 16.0.0 - resolution: "chromatic@npm:16.0.0" +"chromatic@npm:^17.0.0": + version: 17.0.0 + resolution: "chromatic@npm:17.0.0" + dependencies: + semver: "npm:^7.3.5" peerDependencies: "@chromatic-com/cypress": ^0.*.* || ^1.0.0 "@chromatic-com/playwright": ^0.*.* || ^1.0.0 + "@chromatic-com/vitest": ^0.*.* || ^1.0.0 peerDependenciesMeta: "@chromatic-com/cypress": optional: true "@chromatic-com/playwright": optional: true + "@chromatic-com/vitest": + optional: true bin: - chroma: dist/bin.js - chromatic: dist/bin.js - chromatic-cli: dist/bin.js - checksum: 10c0/ebebbf1c7d57e1ee9863997416c5125aab0a1886dce60fcb0358d34a51e0e1a45edc4635c8f8fb56d9facbcf21cd48014320c550f723b4791da51dde8552ee2b + chroma: dist/bin.cjs + chromatic: dist/bin.cjs + chromatic-cli: dist/bin.cjs + checksum: 10c0/962c86feec17b12757fa3327b7e98abff272a048c03227bb21ecb51c7f1d7ec3589386611ece8e2c413ac4049f26d71e9c9226a5fb7628fdcbb47e87905863a0 languageName: node linkType: hard From 9fe97e1ad6182ca62913e405a7e85746366e25a5 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Tue, 26 May 2026 12:22:15 +0200 Subject: [PATCH 04/14] Accessibility: Mark pseudo element content in link footer as decorative (#39164) --- .../mastodon/features/ui/components/link_footer.module.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/javascript/mastodon/features/ui/components/link_footer.module.scss b/app/javascript/mastodon/features/ui/components/link_footer.module.scss index f2b094744e..beb0173ffd 100644 --- a/app/javascript/mastodon/features/ui/components/link_footer.module.scss +++ b/app/javascript/mastodon/features/ui/components/link_footer.module.scss @@ -41,6 +41,10 @@ &:not(:last-child)::after { content: ' · '; + + @supports (content: 'x' / 'y') { + content: ' · ' / ''; + } } } From e1aa4e3a8c7401d9fd2ab0371e550dde0458c961 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Tue, 26 May 2026 12:22:22 +0200 Subject: [PATCH 05/14] Accessibility: Add landmark regions to Web UI (#39133) --- .../compose/components/compose_form.jsx | 10 +++++++++- .../features/compose/components/search.tsx | 8 ++++++-- .../mastodon/features/compose/index.tsx | 17 ++++++++++------- .../mastodon/features/getting_started/index.tsx | 6 ++++-- .../features/navigation_panel/index.tsx | 14 +++++++++++--- .../features/ui/components/columns_area.tsx | 8 ++++---- .../features/ui/components/navigation_bar.tsx | 4 ++++ app/javascript/mastodon/locales/en.json | 2 ++ app/javascript/styles/mastodon/admin.scss | 2 +- app/views/layouts/admin.html.haml | 5 +++-- spec/system/unlogged_spec.rb | 2 +- 11 files changed, 55 insertions(+), 23 deletions(-) diff --git a/app/javascript/mastodon/features/compose/components/compose_form.jsx b/app/javascript/mastodon/features/compose/components/compose_form.jsx index a646a7efae..2cc1f7915a 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.jsx +++ b/app/javascript/mastodon/features/compose/components/compose_form.jsx @@ -248,7 +248,15 @@ class ComposeForm extends ImmutablePureComponent { const { intl, onPaste, onDrop, autoFocus, withoutNavigation, maxChars, isSubmitting } = this.props; return ( -
+ {!withoutNavigation && } diff --git a/app/javascript/mastodon/features/compose/components/search.tsx b/app/javascript/mastodon/features/compose/components/search.tsx index c2855e8cb5..9c7baf5021 100644 --- a/app/javascript/mastodon/features/compose/components/search.tsx +++ b/app/javascript/mastodon/features/compose/components/search.tsx @@ -547,11 +547,15 @@ export const Search: React.FC<{ const searchOptionsHeading = useId(); return ( - + = ({ multiColumn }) => { if (multiColumn) { return ( -
-
-
+
{children}
-
+ @@ -121,7 +121,7 @@ export const ColumnsArea = forwardRef< } return ( -
-
+ ); }); diff --git a/app/javascript/mastodon/features/ui/components/navigation_bar.tsx b/app/javascript/mastodon/features/ui/components/navigation_bar.tsx index ec45e395b2..c3f9373966 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_bar.tsx +++ b/app/javascript/mastodon/features/ui/components/navigation_bar.tsx @@ -31,6 +31,10 @@ export const messages = defineMessages({ defaultMessage: 'Notifications', }, menu: { id: 'tabs_bar.menu', defaultMessage: 'Menu' }, + advancedUiQuickLinks: { + id: 'tabs_bar.quick_links', + defaultMessage: 'Quick links', + }, }); const IconLabelButton: React.FC<{ diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 057b235576..df12731eda 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -908,6 +908,7 @@ "navigation_bar.live_feed_local": "Live feed (local)", "navigation_bar.live_feed_public": "Live feed (public)", "navigation_bar.logout": "Logout", + "navigation_bar.main": "Main", "navigation_bar.moderation": "Moderation", "navigation_bar.more": "More", "navigation_bar.mutes": "Muted users", @@ -1305,6 +1306,7 @@ "tabs_bar.menu": "Menu", "tabs_bar.notifications": "Notifications", "tabs_bar.publish": "New Post", + "tabs_bar.quick_links": "Quick links", "tabs_bar.search": "Search", "tag.remove": "Remove", "terms_of_service.effective_as_of": "Effective as of {date}", diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 887f14360a..0abcefaa4b 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -411,7 +411,7 @@ $content-width: 840px; display: flex; } - & > ul { + & > nav > ul { display: none; &.visible { diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index 039ca88f7e..4bb05b892a 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -8,7 +8,7 @@ - content_for :content do %a.navigation-skip-link{ href: '#content' }= t('admin.skip_to_content') .admin-wrapper - %nav.sidebar-wrapper + %header.sidebar-wrapper .sidebar-wrapper__inner .sidebar = link_to root_path do @@ -23,7 +23,8 @@ = material_symbol 'menu' = material_symbol 'close' - = render_navigation + %nav + = render_navigation %main.content-wrapper#content .content diff --git a/spec/system/unlogged_spec.rb b/spec/system/unlogged_spec.rb index 26d1bd4542..90db5fb40e 100644 --- a/spec/system/unlogged_spec.rb +++ b/spec/system/unlogged_spec.rb @@ -12,6 +12,6 @@ RSpec.describe 'UnloggedBrowsing', :js, :streaming do it 'loads the home page' do expect(subject).to have_css('div.app-holder') - expect(subject).to have_css('div.columns-area__panels__main') + expect(subject).to have_css('main.columns-area__panels__main') end end From cc03e381efb0a264692d2a40193db56ad01cde58 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Tue, 26 May 2026 12:22:38 +0200 Subject: [PATCH 06/14] Accessibility: Fix broken aria label & description in `CollectionListItem` (#39165) --- .../collections/components/collection_list_item.tsx | 10 ++++++---- .../collections/components/collection_lockup.tsx | 6 ++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/javascript/mastodon/features/collections/components/collection_list_item.tsx b/app/javascript/mastodon/features/collections/components/collection_list_item.tsx index 8169544cb8..c1b32cac27 100644 --- a/app/javascript/mastodon/features/collections/components/collection_list_item.tsx +++ b/app/javascript/mastodon/features/collections/components/collection_list_item.tsx @@ -27,14 +27,14 @@ export const CollectionListItem: React.FC = ({ ...otherProps }) => { const uniqueId = useId(); - const linkId = `${uniqueId}-link`; - const infoId = `${uniqueId}-info`; + const titleId = `${uniqueId}-title`; + const subtitleId = `${uniqueId}-info`; return (
@@ -52,6 +52,8 @@ export const CollectionListItem: React.FC = ({ className={classes.menuButton} /> } + titleId={titleId} + subtitleId={subtitleId} {...otherProps} />
diff --git a/app/javascript/mastodon/features/collections/components/collection_lockup.tsx b/app/javascript/mastodon/features/collections/components/collection_lockup.tsx index e16d49e792..5250ea5504 100644 --- a/app/javascript/mastodon/features/collections/components/collection_lockup.tsx +++ b/app/javascript/mastodon/features/collections/components/collection_lockup.tsx @@ -48,6 +48,8 @@ export interface CollectionLockupProps { sideContent?: React.ReactNode; className?: string; headingLevel?: 'h2' | 'h3' | 'h4'; + titleId?: string; + subtitleId?: string; } export const CollectionLockup: React.FC = ({ @@ -56,6 +58,8 @@ export const CollectionLockup: React.FC = ({ withTimestamp, sideContent, headingLevel = 'h3', + titleId, + subtitleId, className, }) => { const { id, name } = collection; @@ -74,6 +78,7 @@ export const CollectionLockup: React.FC = ({ = ({ withTimestamp={withTimestamp} /> } + subtitleId={subtitleId} > {name} From 0275a978888516e68256acefd76f2238c99c9b01 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Tue, 26 May 2026 12:24:38 +0200 Subject: [PATCH 07/14] Don't open account hover card unless preceded by mouse movement (#39166) --- .../mastodon/components/hover_card_controller.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/components/hover_card_controller.tsx b/app/javascript/mastodon/components/hover_card_controller.tsx index a0c704a4e7..dd7ff3b3e7 100644 --- a/app/javascript/mastodon/components/hover_card_controller.tsx +++ b/app/javascript/mastodon/components/hover_card_controller.tsx @@ -144,11 +144,16 @@ export const HoverCardController: React.FC = () => { setScrollTimeout(handleScrollEnd, 100); }; - const handleMouseMove = () => { + const handleMouseMove = (e: MouseEvent) => { if (isUsingTouch) { isUsingTouch = false; } + const hasMoved = Math.max(e.movementX, e.movementY) > 0; + if (!hasMoved) { + return; + } + delayEnterTimeout(enterDelay); cancelMoveTimeout(); From f6d1795da5e42b9436b6f4bf49d25a3f29d34e25 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 26 May 2026 13:51:25 +0200 Subject: [PATCH 08/14] Fix some server-side limits not being respected in web UI (#39163) --- app/javascript/mastodon/actions/compose.js | 2 +- .../mastodon/features/compose/components/poll_form.jsx | 2 +- .../features/compose/containers/upload_button_container.js | 2 +- app/javascript/mastodon/features/ui/index.jsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 06335f9fbf..b4be55cbfe 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -327,7 +327,7 @@ export function uploadCompose(files) { dispatch(showAlert({ message: messages.uploadQuote })); return; } - const uploadLimit = getState().getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']); + const uploadLimit = getState().getIn(['server', 'server', 'item', 'configuration', 'statuses', 'max_media_attachments']); const media = getState().getIn(['compose', 'media_attachments']); const pending = getState().getIn(['compose', 'pending_media_attachments']); const progress = new Array(files.length).fill(0); diff --git a/app/javascript/mastodon/features/compose/components/poll_form.jsx b/app/javascript/mastodon/features/compose/components/poll_form.jsx index e8d5331196..a2036d0cc5 100644 --- a/app/javascript/mastodon/features/compose/components/poll_form.jsx +++ b/app/javascript/mastodon/features/compose/components/poll_form.jsx @@ -58,7 +58,7 @@ const Option = ({ multipleChoice, index, title, autoFocus }) => { const dispatch = useDispatch(); const suggestions = useSelector(state => state.getIn(['compose', 'suggestions'])); const lang = useSelector(state => state.getIn(['compose', 'language'])); - const maxOptions = useSelector(state => state.getIn(['server', 'server', 'configuration', 'polls', 'max_options'])); + const maxOptions = useSelector(state => state.getIn(['server', 'server', 'item', 'configuration', 'polls', 'max_options'])); const handleChange = useCallback(({ target: { value } }) => { dispatch(changePollOption(index, value, maxOptions)); diff --git a/app/javascript/mastodon/features/compose/containers/upload_button_container.js b/app/javascript/mastodon/features/compose/containers/upload_button_container.js index a5ae874b06..a332e99c08 100644 --- a/app/javascript/mastodon/features/compose/containers/upload_button_container.js +++ b/app/javascript/mastodon/features/compose/containers/upload_button_container.js @@ -9,7 +9,7 @@ const mapStateToProps = state => { const readyAttachmentsSize = state.getIn(['compose', 'media_attachments']).size ?? 0; const pendingAttachmentsSize = state.getIn(['compose', 'pending_media_attachments']).size ?? 0; const attachmentsSize = readyAttachmentsSize + pendingAttachmentsSize; - const isOverLimit = attachmentsSize > state.getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments'])-1; + const isOverLimit = attachmentsSize > state.getIn(['server', 'server', 'item', 'configuration', 'statuses', 'max_media_attachments'])-1; const hasVideoOrAudio = state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type'))); const hasQuote = !!state.compose.get('quoted_status_id'); diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index 4f398b510c..733a91d041 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -105,7 +105,7 @@ const mapStateToProps = state => ({ hasComposingContents: state.getIn(['compose', 'text']).trim().length !== 0 || state.getIn(['compose', 'media_attachments']).size > 0 || state.getIn(['compose', 'poll']) !== null || state.getIn(['compose', 'quoted_status_id']) !== null, canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) - && state.getIn(['compose', 'media_attachments']).size < state.getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']), + && state.getIn(['compose', 'media_attachments']).size < state.getIn(['server', 'server', 'item', 'configuration', 'statuses', 'max_media_attachments']), isUploadEnabled: state.getIn(['compose', 'isDragDisabled']) !== true, firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION, From 07d099cbf7b646b7dc715c3bfe3a8ea453e9eafb Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 26 May 2026 14:36:54 +0200 Subject: [PATCH 09/14] Add new overview landing page setting (#39074) --- app/javascript/mastodon/actions/server.ts | 14 ++ .../features/custom_homepage/about.tsx | 75 +++++++++ .../custom_homepage/components/footer.tsx | 39 +++++ .../custom_homepage/components/header.tsx | 25 +++ .../features/custom_homepage/index.tsx | 71 +++++++++ .../custom_homepage/latest_activity.tsx | 35 +++++ .../custom_homepage/styles.module.scss | 145 ++++++++++++++++++ .../features/ui/components/columns_area.tsx | 24 ++- .../ui/containers/status_list_container.js | 21 ++- app/javascript/mastodon/features/ui/index.jsx | 24 ++- app/javascript/mastodon/locales/en.json | 6 + app/models/form/admin_settings.rb | 2 +- .../admin/settings/branding/show.html.haml | 5 +- config/locales/en.yml | 12 +- config/routes/web_app.rb | 2 + 15 files changed, 483 insertions(+), 17 deletions(-) create mode 100644 app/javascript/mastodon/features/custom_homepage/about.tsx create mode 100644 app/javascript/mastodon/features/custom_homepage/components/footer.tsx create mode 100644 app/javascript/mastodon/features/custom_homepage/components/header.tsx create mode 100644 app/javascript/mastodon/features/custom_homepage/index.tsx create mode 100644 app/javascript/mastodon/features/custom_homepage/latest_activity.tsx create mode 100644 app/javascript/mastodon/features/custom_homepage/styles.module.scss diff --git a/app/javascript/mastodon/actions/server.ts b/app/javascript/mastodon/actions/server.ts index 4e4795c123..b301919b23 100644 --- a/app/javascript/mastodon/actions/server.ts +++ b/app/javascript/mastodon/actions/server.ts @@ -16,19 +16,33 @@ export const fetchServer = createDataLoadingThunk( dispatch(importFetchedAccount(instance.contact.account)); } }, + { + condition: (_, { getState }) => !getState().server.server.isLoading, + }, ); export const fetchExtendedDescription = createDataLoadingThunk( 'server/extended_description', () => apiGetExtendedDescription(), + { + condition: (_, { getState }) => + !getState().server.extendedDescription.isLoading, + }, ); export const fetchServerTranslationLanguages = createDataLoadingThunk( 'server/translation_languages', () => apiGetTranslationLanguages(), + { + condition: (_, { getState }) => + !getState().server.translationLanguages.isLoading, + }, ); export const fetchDomainBlocks = createDataLoadingThunk( 'server/domain_blocks', () => apiGetDomainBlocks(), + { + condition: (_, { getState }) => !getState().server.domainBlocks.isLoading, + }, ); diff --git a/app/javascript/mastodon/features/custom_homepage/about.tsx b/app/javascript/mastodon/features/custom_homepage/about.tsx new file mode 100644 index 0000000000..7ec3a8e442 --- /dev/null +++ b/app/javascript/mastodon/features/custom_homepage/about.tsx @@ -0,0 +1,75 @@ +import { useEffect } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { fetchExtendedDescription } from 'mastodon/actions/server'; +import { Account } from 'mastodon/components/account'; +import { Skeleton } from 'mastodon/components/skeleton'; +import { useAppSelector, useAppDispatch } from 'mastodon/store'; + +import classes from './styles.module.scss'; + +const Placeholder = () => ( +
+ + + +
+); + +export const About = () => { + const dispatch = useAppDispatch(); + const server = useAppSelector((state) => state.server.server); + const extendedDescription = useAppSelector( + (state) => state.server.extendedDescription, + ); + + const accountId = server.item?.contact.account?.id ?? ''; + const isLoading = extendedDescription.isLoading; + const hasContent = (extendedDescription.item?.content.length ?? 0) > 0; + const content = extendedDescription.item?.content ?? ''; + + useEffect(() => { + void dispatch(fetchExtendedDescription()); + }, [dispatch]); + + return ( + <> +
+

+ +

+ +
+ +
+

+ +

+ {isLoading ? ( + + ) : hasContent ? ( +
+ ) : ( +
+

+ +

+
+ )} +
+ + ); +}; diff --git a/app/javascript/mastodon/features/custom_homepage/components/footer.tsx b/app/javascript/mastodon/features/custom_homepage/components/footer.tsx new file mode 100644 index 0000000000..b1b69cfc76 --- /dev/null +++ b/app/javascript/mastodon/features/custom_homepage/components/footer.tsx @@ -0,0 +1,39 @@ +import { useEffect } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import { fetchServer } from 'mastodon/actions/server'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; + +import classes from '../styles.module.scss'; + +export const Footer = () => { + const dispatch = useAppDispatch(); + const server = useAppSelector((state) => state.server.server); + const email = server.item?.contact.email ?? ''; + + useEffect(() => { + void dispatch(fetchServer()); + }, [dispatch]); + + return ( + + ); +}; diff --git a/app/javascript/mastodon/features/custom_homepage/components/header.tsx b/app/javascript/mastodon/features/custom_homepage/components/header.tsx new file mode 100644 index 0000000000..dd4b7969c3 --- /dev/null +++ b/app/javascript/mastodon/features/custom_homepage/components/header.tsx @@ -0,0 +1,25 @@ +import { FormattedMessage } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import { domain, sso_redirect } from 'mastodon/initial_state'; + +import classes from '../styles.module.scss'; + +export const Header = () => ( +
+
+ {domain} +
+ +
+ + + +
+
+); diff --git a/app/javascript/mastodon/features/custom_homepage/index.tsx b/app/javascript/mastodon/features/custom_homepage/index.tsx new file mode 100644 index 0000000000..5fc75108f1 --- /dev/null +++ b/app/javascript/mastodon/features/custom_homepage/index.tsx @@ -0,0 +1,71 @@ +import { useEffect } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { Route, Switch, useRouteMatch } from 'react-router-dom'; + +import { Helmet } from '@unhead/react/helmet'; + +import { fetchServer } from 'mastodon/actions/server'; +import { ServerHeroImage } from 'mastodon/components/server_hero_image'; +import { TabLink, TabList } from 'mastodon/components/tab_list'; +import { useAppSelector, useAppDispatch } from 'mastodon/store'; + +import { About } from './about'; +import { LatestActivity } from './latest_activity'; +import classes from './styles.module.scss'; + +export const CustomHomepage: React.FC = () => { + const dispatch = useAppDispatch(); + const server = useAppSelector((state) => state.server.server); + const { path } = useRouteMatch(); + + useEffect(() => { + void dispatch(fetchServer()); + }, [dispatch]); + + return ( +
+ + `${server.item?.thumbnail.versions?.[key]} ${key.replace('@', '')}`, + ) + .join(', ')} + className={classes.header} + /> + +
+

{server.item?.domain}

+

{server.item?.description}

+
+ + + + + + + + + + + + + + + + + + {server.item?.domain} + + +
+ ); +}; diff --git a/app/javascript/mastodon/features/custom_homepage/latest_activity.tsx b/app/javascript/mastodon/features/custom_homepage/latest_activity.tsx new file mode 100644 index 0000000000..9deac2db4c --- /dev/null +++ b/app/javascript/mastodon/features/custom_homepage/latest_activity.tsx @@ -0,0 +1,35 @@ +import { useEffect } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { expandCommunityTimeline } from 'mastodon/actions/timelines'; +import { Callout } from 'mastodon/components/callout'; +import StatusListContainer from 'mastodon/features/ui/containers/status_list_container'; +import { useAppDispatch } from 'mastodon/store'; + +import classes from './styles.module.scss'; + +export const LatestActivity = () => { + const dispatch = useAppDispatch(); + + useEffect(() => { + void dispatch(expandCommunityTimeline()); + }, [dispatch]); + + return ( + + + + } + scrollKey='custom_homepage' + timelineId='community' + maxItems={40} + bindToDocument + /> + ); +}; diff --git a/app/javascript/mastodon/features/custom_homepage/styles.module.scss b/app/javascript/mastodon/features/custom_homepage/styles.module.scss new file mode 100644 index 0000000000..bbf834ffc9 --- /dev/null +++ b/app/javascript/mastodon/features/custom_homepage/styles.module.scss @@ -0,0 +1,145 @@ +.page { + border-radius: 16px; + border: 1px solid var(--color-border-primary); + background: var(--color-bg-primary); + min-height: 100%; + + :global(.item-list) article:last-child :global(.status) { + border-bottom: 0; + } + + @media screen and (width <= 1175px) { + border-radius: 0; + } +} + +.header { + aspect-ratio: 40/21; + border-radius: 16px 16px 0 0; + + @media screen and (width <= 1175px) { + border-radius: 0; + } +} + +.banner { + margin: 16px; + margin-bottom: 0; +} + +.topSection { + display: flex; + padding: 16px; + flex-direction: column; + gap: 8px; + color: var(--color-text-primary); + + h1 { + font-size: 24px; + font-weight: 500; + line-height: 30px; + letter-spacing: -0.12px; + } + + p { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + font-size: 16px; + line-height: 24px; + text-overflow: ellipsis; + } +} + +.block { + padding: 16px; + + h2 { + font-size: 16px; + font-weight: 500; + line-height: 22.4px; + margin-bottom: 8px; + } + + :global(.account) { + border: 1px solid var(--color-border-primary); + padding: 18px 16px; + border-radius: 12px; + + --avatar-border-radius: 50%; + } +} + +.placeholder { + padding: 4px 0; + display: flex; + flex-direction: column; + gap: 12px; + + :global(.skeleton) { + height: 40px; + border-radius: 12px; + background: var(--color-bg-overlay-highlight); + } +} + +.minimalHeader { + padding-top: 24px; + padding-bottom: 12px; + display: flex; + align-items: center; + + @media screen and (width <= 1175px) { + padding-inline-start: 12px; + padding-inline-end: 12px; + } +} + +.leftSide { + font-size: 15px; + font-weight: 600; + max-width: 300px; + + a { + text-decoration: none; + color: inherit; + } +} + +.rightSide { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 4px; + flex: 1 0 0; +} + +.minimalFooter { + display: flex; + justify-content: space-between; + align-items: center; + color: var(--color-text-secondary); + font-size: 16px; + font-weight: 500; + line-height: 22.4px; + padding-top: 28px; + padding-bottom: 54px; + gap: 8px; + flex-wrap: wrap; + + a { + color: inherit; + text-decoration: underline; + } + + @media screen and (width <= 1175px) { + justify-content: center; + padding-inline-start: 12px; + padding-inline-end: 12px; + } +} + +.contact { + display: flex; + gap: 8px; +} diff --git a/app/javascript/mastodon/features/ui/components/columns_area.tsx b/app/javascript/mastodon/features/ui/components/columns_area.tsx index ae735e196f..f5ab3de849 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.tsx +++ b/app/javascript/mastodon/features/ui/components/columns_area.tsx @@ -12,6 +12,8 @@ import classNames from 'classnames'; import type { List, Record } from 'immutable'; import { useAppSelector } from '@/mastodon/store'; +import { Footer } from 'mastodon/features/custom_homepage/components/footer'; +import { Header } from 'mastodon/features/custom_homepage/components/header'; import { CollapsibleNavigationPanel } from 'mastodon/features/navigation_panel'; import { useBreakpoint } from '../hooks/useBreakpoint'; @@ -85,9 +87,10 @@ export const ColumnsArea = forwardRef< HTMLDivElement, { singleColumn?: boolean; + minimalShell?: boolean; children: React.ReactElement | React.ReactElement[]; } ->(({ children, singleColumn }, ref) => { +>(({ children, minimalShell, singleColumn }, ref) => { const renderComposePanel = !useBreakpoint('full'); const columns = useAppSelector((state) => (state.settings as Record<{ columns: List> }>).get( @@ -98,6 +101,24 @@ export const ColumnsArea = forwardRef< (state) => !state.modal.get('stack').isEmpty(), ); + if (minimalShell) { + return ( +
+
+
+ +
+ +
+ +
{children}
+ +
+
+
+ ); + } + if (singleColumn) { return (
@@ -112,6 +133,7 @@ export const ColumnsArea = forwardRef<
+
{children}
diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js index 1e21730a00..8abbbefe14 100644 --- a/app/javascript/mastodon/features/ui/containers/status_list_container.js +++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js @@ -11,7 +11,15 @@ import { me } from '@/mastodon/initial_state'; const makeGetStatusIds = (pending = false) => createSelector([ (state, { type }) => state.getIn(['settings', type], ImmutableMap()), - (state, { type }) => state.getIn(['timelines', type, pending ? 'pendingItems' : 'items'], ImmutableList()), + (state, { type, maxItems }) => { + const items = state.getIn(['timelines', type, pending ? 'pendingItems' : 'items'], ImmutableList()); + + if (maxItems) { + return items.take(maxItems); + } + + return items; + }, (state) => state.get('statuses'), ], (columnSettings, statusIds, statuses) => { return statusIds.filter(id => { @@ -41,8 +49,15 @@ const makeMapStateToProps = () => { const getStatusIds = makeGetStatusIds(); const getPendingStatusIds = makeGetStatusIds(true); - const mapStateToProps = (state, { timelineId, initialLoadingState = true }) => ({ - statusIds: getStatusIds(state, { type: timelineId }), + /** + * @param {import('mastodon/store').RootState} state + * @param {Object} props + * @param {string} props.timelineId + * @param {boolean} [props.initialLoadingState] + * @param {number} [props.maxItems] + */ + const mapStateToProps = (state, { timelineId, initialLoadingState = true, maxItems }) => ({ + statusIds: getStatusIds(state, { type: timelineId, maxItems }), lastId: state.getIn(['timelines', timelineId, 'items'])?.last(), isLoading: state.getIn(['timelines', timelineId, 'isLoading'], initialLoadingState), isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false), diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index 733a91d041..1bf0842cb8 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -29,7 +29,7 @@ import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../act import { clearHeight } from '../../actions/height_cache'; import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server'; import { expandHomeTimeline } from '../../actions/timelines'; -import { initialState, me, owner, singleUserMode, trendsEnabled, landingPage, localLiveFeedAccess, disableHoverCards } from '../../initial_state'; +import { initialState, me, owner, singleUserMode, trendsEnabled, landingPage, localLiveFeedAccess, disableHoverCards, domain } from '../../initial_state'; import BundleColumnError from './components/bundle_column_error'; import { NavigationBar } from './components/navigation_bar'; @@ -88,6 +88,7 @@ import { import { ColumnsContextProvider } from './util/columns_context'; import { focusColumn, getFocusedItemIndex, focusItemSibling, focusFirstItem } from './util/focusUtils'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; +import { CustomHomepage } from 'mastodon/features/custom_homepage'; // Dummy import, to make sure that ends up in the application bundle. // Without this it ends up in ~8 very commonly used bundles. @@ -177,13 +178,15 @@ class SwitchingColumnsArea extends PureComponent { rootRedirect = '/explore'; } else if (localLiveFeedAccess === 'public' && landingPage === 'local_feed') { rootRedirect = '/public/local'; + } else if (landingPage === 'overview') { + rootRedirect = '/overview'; } else { rootRedirect = '/about'; } return ( - + @@ -262,6 +265,8 @@ class SwitchingColumnsArea extends PureComponent { + + @@ -633,13 +638,18 @@ class UI extends PureComponent { cheat: this.handleDonate, }; + const minimalShell = !this.props.identity.signedIn && landingPage === 'overview'; + return (
- + {!minimalShell && ( + + )} + - + {!minimalShell && } {layout !== 'mobile' && } {!disableHoverCards && } diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index df12731eda..b97260173b 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -584,6 +584,12 @@ "copy_icon_button.copy_this_text": "Copy link to clipboard", "copypaste.copied": "Copied", "copypaste.copy_to_clipboard": "Copy to clipboard", + "custom_homepage.about": "About", + "custom_homepage.about_this_server": "About this server", + "custom_homepage.administered_by": "Administered by", + "custom_homepage.contact": "Contact:", + "custom_homepage.latest_activity": "Latest activity", + "custom_homepage.these_are_the_latest_posts": "These are the latest 40 posts from accounts on this server.", "directory.federated": "From known fediverse", "directory.local": "From {domain} only", "directory.new_arrivals": "New arrivals", diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 0ad5335419..5e97151438 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -94,7 +94,7 @@ class Form::AdminSettings REGISTRATION_MODES = %w(open approved none).freeze FEED_ACCESS_MODES = %w(public authenticated disabled).freeze ALTERNATE_FEED_ACCESS_MODES = %w(public authenticated).freeze - LANDING_PAGE = %w(trends about local_feed).freeze + LANDING_PAGE = %w(trends overview local_feed about).freeze attr_accessor(*KEYS) diff --git a/app/views/admin/settings/branding/show.html.haml b/app/views/admin/settings/branding/show.html.haml index 65aae77d5f..9e9653abf1 100644 --- a/app/views/admin/settings/branding/show.html.haml +++ b/app/views/admin/settings/branding/show.html.haml @@ -75,10 +75,11 @@ .fields-row = f.input :landing_page, + as: :radio_buttons, collection: f.object.class::LANDING_PAGE, include_blank: false, - label_method: ->(page) { I18n.t("admin.settings.landing_page.values.#{page}") }, - wrapper: :with_label + label_method: ->(page) { safe_join([I18n.t("admin.settings.landing_page.values.#{page}"), content_tag(:span, I18n.t("admin.settings.landing_page.hints.#{page}_html"), class: 'hint')]) }, + wrapper: :with_block_label .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/config/locales/en.yml b/config/locales/en.yml index 2ed8dd1c1a..f47eac1fe1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -956,10 +956,16 @@ en: disabled: Require specific user role public: Everyone landing_page: + hints: + about_html: A page with the description, contact information, rules and other information regarding this server. + local_feed_html: A live feed featuring most recent posts by users on this server. + overview_html: A page showcasing the description of your server alongside the most recent local posts by users on this server. + trends_html: A page featuring what's popular on this server right now. values: - about: About - local_feed: Local feed - trends: Trends + about: About page + local_feed: Local live feed + overview: Overview + trends: Trending registrations: moderation_recommandation: Please make sure you have an adequate and reactive moderation team before you open registrations to everyone! preamble: Control who can create an account on your server. diff --git a/config/routes/web_app.rb b/config/routes/web_app.rb index cb85dc8753..22814f294c 100644 --- a/config/routes/web_app.rb +++ b/config/routes/web_app.rb @@ -33,4 +33,6 @@ /search /start/(*any) /statuses/(*any) + /overview + /overview/about ).each { |path| get path, to: 'home#index' } From 3559efe526c144e128607cb60b556c366adba0c3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 26 May 2026 15:42:55 +0200 Subject: [PATCH 10/14] Fix missing padding on email subscription form (#39162) --- .../mastodon/components/account_header/styles.module.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/mastodon/components/account_header/styles.module.scss b/app/javascript/mastodon/components/account_header/styles.module.scss index 2daf386734..ab751ce170 100644 --- a/app/javascript/mastodon/components/account_header/styles.module.scss +++ b/app/javascript/mastodon/components/account_header/styles.module.scss @@ -480,6 +480,7 @@ $button-fallback-breakpoint: $button-breakpoint + 55px; justify-content: center; align-items: flex-start; margin: 16px 0; + padding: 16px; } .bannerBaseCentered { From c39072ad9d2b531a04c0192f06018fbd8d737f69 Mon Sep 17 00:00:00 2001 From: Echo Date: Tue, 26 May 2026 16:00:23 +0200 Subject: [PATCH 11/14] Emojis: Fix bug with search + improve custom tokenization (#39167) --- .../mastodon/actions/importer/emoji.ts | 9 ++++++ .../mastodon/features/emoji/database.ts | 31 +++++++++++++++---- .../mastodon/features/emoji/index.ts | 2 ++ .../mastodon/features/emoji/normalize.ts | 14 ++++++--- .../mastodon/features/emoji/picker.ts | 11 ++++++- .../mastodon/hooks/useCustomEmojis.ts | 2 +- 6 files changed, 56 insertions(+), 13 deletions(-) diff --git a/app/javascript/mastodon/actions/importer/emoji.ts b/app/javascript/mastodon/actions/importer/emoji.ts index 9e06c88f66..e9356ab621 100644 --- a/app/javascript/mastodon/actions/importer/emoji.ts +++ b/app/javascript/mastodon/actions/importer/emoji.ts @@ -1,5 +1,8 @@ import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; import { loadCustomEmoji } from '@/mastodon/features/emoji'; +import { emojiLogger } from '@/mastodon/features/emoji/utils'; + +const log = emojiLogger('actions'); export async function importCustomEmoji(emojis: ApiCustomEmojiJSON[]) { if (emojis.length === 0) { @@ -18,5 +21,11 @@ export async function importCustomEmoji(emojis: ApiCustomEmojiJSON[]) { if (existingEmojis.length < emojis.length) { await clearCache('custom'); await loadCustomEmoji(); + + const { reloadCustomEmojis } = + await import('@/mastodon/features/emoji/picker'); + await reloadCustomEmojis(); + + log('Custom emojis updated, reloaded cache and picker data.'); } } diff --git a/app/javascript/mastodon/features/emoji/database.ts b/app/javascript/mastodon/features/emoji/database.ts index b74e21fe78..3dd2f95858 100644 --- a/app/javascript/mastodon/features/emoji/database.ts +++ b/app/javascript/mastodon/features/emoji/database.ts @@ -85,13 +85,16 @@ export async function search({ // Only query the range for the last token to allow partial matches. const range = i === queryTokens.length - 1 - ? IDBKeyRange.bound(token, token + '\uffff') + ? IDBKeyRange.lowerBound(token) : IDBKeyRange.only(token); - const [unicodeResults, customResults] = await Promise.all([ - db.getAllFromIndex(locale, 'tokens', range), - db.getAllFromIndex('custom', 'tokens', range), - ]); + const [unicodeResults, customResults, shortcodeResults] = await Promise.all( + [ + db.getAllFromIndex(locale, 'tokens', range), + db.getAllFromIndex('custom', 'tokens', range), + db.getAllFromIndex('shortcodes', 'shortcodes', range), + ], + ); const resultMap: ScoreMap = new Map(); for (const emoji of unicodeResults) { const score = getScoreForEmoji(emoji, token); @@ -107,6 +110,22 @@ export async function search({ } resultMap.set(emoji.shortcode, { ...emoji, score }); } + + for (const shortcodeResult of shortcodeResults) { + if (resultMap.has(shortcodeResult.hexcode)) { + continue; + } + const emoji = await db.get(locale, shortcodeResult.hexcode); + if (!emoji) { + continue; + } + const score = getScoreForEmoji(emoji, token); + if (score === null) { + continue; + } + resultMap.set(emoji.hexcode, { ...emoji, score }); + } + log('found %d results for token "%s"', resultMap.size, token); resultArrays.push(resultMap); } @@ -147,7 +166,7 @@ function getScoreForEmoji(emoji: AnyEmojiData, query: string) { } let index = 1; - for (const token of [id, emoji.tokens]) { + for (const token of [id, ...emoji.tokens]) { const tokenIndex = token.indexOf(query); if (tokenIndex !== -1) { return index + tokenIndex / token.length; diff --git a/app/javascript/mastodon/features/emoji/index.ts b/app/javascript/mastodon/features/emoji/index.ts index b134d88441..a3ef9ffb25 100644 --- a/app/javascript/mastodon/features/emoji/index.ts +++ b/app/javascript/mastodon/features/emoji/index.ts @@ -2,6 +2,7 @@ import { initialState } from '@/mastodon/initial_state'; import type { EMOJI_DB_NAME_SHORTCODES } from './constants'; import { toSupportedLocale } from './locale'; +import { reloadCustomEmojis } from './picker'; import type { LocaleOrCustom } from './types'; import { emojiLogger } from './utils'; @@ -90,6 +91,7 @@ export async function loadCustomEmoji() { const emojis = await importCustomEmojiData(); if (emojis && emojis.length > 0) { log('loaded %d custom emojis', emojis.length); + await reloadCustomEmojis(); } } } diff --git a/app/javascript/mastodon/features/emoji/normalize.ts b/app/javascript/mastodon/features/emoji/normalize.ts index fd4f61c66d..257cddfcb6 100644 --- a/app/javascript/mastodon/features/emoji/normalize.ts +++ b/app/javascript/mastodon/features/emoji/normalize.ts @@ -14,6 +14,7 @@ import { EMOJIS_REQUIRING_INVERSION_IN_DARK_MODE, EMOJI_MIN_TOKEN_LENGTH, } from './constants'; +import { localeToSegmenter } from './locale'; import type { CustomEmojiData, CustomEmojiMapArg, @@ -92,10 +93,11 @@ export function transformEmojiData( export function transformCustomEmojiData( emoji: ApiCustomEmojiJSON, ): CustomEmojiData { - const tokens = emoji.shortcode - .split('_') - .filter((word) => word.length >= EMOJI_MIN_TOKEN_LENGTH) - .map((word) => word.toLowerCase()); + const tokens = extractTokens(emoji.shortcode, localeToSegmenter('en')); + if (!tokens.includes(emoji.shortcode)) { + tokens.unshift(emoji.shortcode); + } + return { ...emoji, tokens, @@ -215,7 +217,9 @@ export function extractTokens( // Prefer to use Intl.Segmenter if available for better locale support. if (segmenter) { for (const { isWordLike, segment } of segmenter.segment( - input.replaceAll('_', ' '), // Handle underscores from shortcodes. + input + .replaceAll(/[_-]+/g, ' ') // Handle underscores from shortcodes. + .replaceAll(/([a-z])([A-Z])/g, '$1 $2'), // Handle camelCase. )) { if (isWordLike && segment.length >= EMOJI_MIN_TOKEN_LENGTH) { tokens.push(segment.toLowerCase()); diff --git a/app/javascript/mastodon/features/emoji/picker.ts b/app/javascript/mastodon/features/emoji/picker.ts index 1bc9bb2f12..fccf6769c3 100644 --- a/app/javascript/mastodon/features/emoji/picker.ts +++ b/app/javascript/mastodon/features/emoji/picker.ts @@ -82,13 +82,22 @@ type LegacyEmoji = custom: true; }; +export async function reloadCustomEmojis() { + customEmojis = null; + + const { loadEmojisIntoCache } = + await import('@/mastodon/hooks/useCustomEmojis'); + + await Promise.all([fetchCustomEmojiData(), loadEmojisIntoCache()]); +} + // Replicates the old legacy search function. export async function emojiMartSearch( token: string, locale: string, limit = 5, ): Promise { - const query = token.replace(':', '').toLowerCase().trim(); + const query = token.replace(':', '').trim(); if (!query.length) { return []; } diff --git a/app/javascript/mastodon/hooks/useCustomEmojis.ts b/app/javascript/mastodon/hooks/useCustomEmojis.ts index aeb77620dd..6eb3c17fbf 100644 --- a/app/javascript/mastodon/hooks/useCustomEmojis.ts +++ b/app/javascript/mastodon/hooks/useCustomEmojis.ts @@ -20,7 +20,7 @@ export function useCustomEmojis() { return emojis; } -async function loadEmojisIntoCache() { +export async function loadEmojisIntoCache() { const { loadAllCustomEmoji } = await import('../features/emoji/database'); const emojisRaw = await loadAllCustomEmoji(); if (emojisRaw === null) { From ceab04a1fddaf1389bd88994af913838a2c04652 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Tue, 26 May 2026 16:33:37 +0200 Subject: [PATCH 12/14] Fix missing Translate button (#39170) --- app/javascript/mastodon/components/status_content.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index 4d9ae4fa3f..7e5cccf198 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -69,7 +69,7 @@ class TranslateButton extends PureComponent { } const mapStateToProps = state => ({ - languages: state.server.translationLanguages.items, + languages: state.server.translationLanguages.item, }); class StatusContent extends PureComponent { @@ -187,7 +187,7 @@ class StatusContent extends PureComponent { const renderReadMore = this.props.onClick && status.get('collapsed'); const contentLocale = intl.locale.replace(/[_-].*/, ''); - const targetLanguages = this.props.languages?.get(status.get('language') || 'und'); + const targetLanguages = this.props.languages?.[status.get('language') || 'und']; const renderTranslate = this.props.onTranslate && this.props.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale); const content = statusContent ?? getStatusContent(status); From 1962e4743c3786b3694a802547271a6046da4c9a Mon Sep 17 00:00:00 2001 From: diondiondion Date: Tue, 26 May 2026 16:59:07 +0200 Subject: [PATCH 13/14] Fix advanced UI column crashing in development (#39171) --- app/javascript/mastodon/components/scrollable_list/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/components/scrollable_list/index.jsx b/app/javascript/mastodon/components/scrollable_list/index.jsx index a80fe5581a..e44a827a2f 100644 --- a/app/javascript/mastodon/components/scrollable_list/index.jsx +++ b/app/javascript/mastodon/components/scrollable_list/index.jsx @@ -285,7 +285,7 @@ class ScrollableList extends PureComponent { if (this.props.bindToDocument) { document.removeEventListener('scroll', this.handleScroll); document.removeEventListener('wheel', this.handleWheel, listenerOptions); - } else { + } else if (this.node) { this.node.removeEventListener('scroll', this.handleScroll); this.node.removeEventListener('wheel', this.handleWheel, listenerOptions); } From d20d04922672ed016029c40ed64798f341ebca91 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Tue, 26 May 2026 19:42:49 +0200 Subject: [PATCH 14/14] Accessibility: Ensure focus order of post elements matches visual reading order (#39169) --- .../mastodon/components/status/header.tsx | 22 +++++++++------ .../mastodon/components/status_quoted.tsx | 25 +++++++++-------- .../components/pinned_statuses.tsx | 28 +++++++++++-------- .../account_timeline/styles.module.scss | 3 ++ .../styles/mastodon/components.scss | 2 -- 5 files changed, 46 insertions(+), 34 deletions(-) diff --git a/app/javascript/mastodon/components/status/header.tsx b/app/javascript/mastodon/components/status/header.tsx index 65790bb493..1ce5c4a36c 100644 --- a/app/javascript/mastodon/components/status/header.tsx +++ b/app/javascript/mastodon/components/status/header.tsx @@ -20,7 +20,8 @@ export interface StatusHeaderProps { status: Status; account?: Account; avatarSize?: number; - children?: ReactNode; + contentBeforeDate?: ReactNode; + contentAfterDate?: ReactNode; wrapperProps?: HTMLAttributes; displayNameProps?: DisplayNameProps; onHeaderClick?: MouseEventHandler; @@ -33,10 +34,11 @@ export type StatusHeaderRenderFn = (args: StatusHeaderProps) => ReactNode; export const StatusHeader: FC = ({ status, account, - children, className, avatarSize = 48, wrapperProps, + contentBeforeDate, + contentAfterDate, onHeaderClick, }) => { const statusAccount = status.get('account') as Account | undefined; @@ -51,6 +53,14 @@ export const StatusHeader: FC = ({ className={classNames('status__info', className)} /* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */ > + + + {contentBeforeDate} + = ({ {editedAt && } - - - {children} + {contentAfterDate}
); }; diff --git a/app/javascript/mastodon/components/status_quoted.tsx b/app/javascript/mastodon/components/status_quoted.tsx index 5c9804fb40..b269792a5e 100644 --- a/app/javascript/mastodon/components/status_quoted.tsx +++ b/app/javascript/mastodon/components/status_quoted.tsx @@ -225,17 +225,20 @@ export const QuotedStatus: React.FC = ({ const intl = useIntl(); const headerRenderFn: StatusHeaderRenderFn = useCallback( (props) => ( - - {onQuoteCancel && ( - - )} - + + ) + } + /> ), [intl, onQuoteCancel], ); diff --git a/app/javascript/mastodon/features/account_timeline/components/pinned_statuses.tsx b/app/javascript/mastodon/features/account_timeline/components/pinned_statuses.tsx index 11de336002..c751af83ca 100644 --- a/app/javascript/mastodon/features/account_timeline/components/pinned_statuses.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/pinned_statuses.tsx @@ -22,18 +22,22 @@ export const renderPinnedStatusHeader: StatusHeaderRenderFn = ({ return ; } return ( - - } - label={ - - } - /> - + } + label={ + + } + /> + } + /> ); }; diff --git a/app/javascript/mastodon/features/account_timeline/styles.module.scss b/app/javascript/mastodon/features/account_timeline/styles.module.scss index cd5cef3f29..9773861d22 100644 --- a/app/javascript/mastodon/features/account_timeline/styles.module.scss +++ b/app/javascript/mastodon/features/account_timeline/styles.module.scss @@ -126,4 +126,7 @@ .pinnedBadge { justify-self: end; + + // Allow "click to open post" event to pass through + pointer-events: none; } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 3848b76377..809634e8f5 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1613,7 +1613,6 @@ body > [data-popper-placement] { font-size: 15px; line-height: 22px; height: 40px; - order: 2; flex: 0 0 auto; color: var(--color-text-secondary); } @@ -1666,7 +1665,6 @@ body > [data-popper-placement] { .status__quote-cancel { align-self: self-start; - order: 5; } .status__info {