diff --git a/CHANGELOG.md b/CHANGELOG.md index bcb5b213b89168..a7b706b534ca3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. -## [4.7.0] - UNRELEASED +## [4.7.0] - 2026-08-20 ### Added @@ -39,6 +39,10 @@ All notable changes to this project will be documented in this file. ### Fixed +- Fix autosuggestions overstaying their welcome in languages that don't use spaces (#40217 by @Gargron) +- Fix error when processing remote actors with `null` public keys (#40194 by @ClearlyClaire) +- Fix various off-by-one errors in statistics time ranges (#40193 by @ClearlyClaire) +- Fix `/deck` being removed from path when resizing window (#40180 by @ClearlyClaire) - Fix error in `tootctl media refresh` when cleaning some incompletely processed files (#40056 by @shleeable) - Fix plain-text formatter not treating `
` as newline (#40079 by @shleeable) - Fix performance of listing follow requests by adding appropriate index (#40033 by @ClearlyClaire) diff --git a/Dockerfile b/Dockerfile index 48ebee09c98247..667571829ece61 100644 --- a/Dockerfile +++ b/Dockerfile @@ -399,7 +399,7 @@ RUN \ RUN \ # Precompile bootsnap code for faster Rails startup - bundle exec bootsnap precompile --gemfile app/ lib/; + bundle exec bootsnap precompile --gemfile app/ lib/ config/; RUN \ # Pre-create and chown system volume to Mastodon user diff --git a/Gemfile.lock b/Gemfile.lock index 0e23223600f132..36c0638bb8d977 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -99,7 +99,7 @@ GEM ast (2.4.3) attr_required (1.0.2) aws-eventstream (1.4.0) - aws-partitions (1.1279.0) + aws-partitions (1.1281.0) aws-sdk-core (3.254.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -276,12 +276,12 @@ GEM raabro (~> 1.4) globalid (1.4.0) activesupport (>= 6.1) - google-protobuf (4.35.1) + google-protobuf (4.36.0) bigdecimal rake (~> 13.3) googleapis-common-protos-types (1.23.0) google-protobuf (~> 4.26) - haml (7.3.0) + haml (7.4.1) prism (>= 1.1.0) temple (>= 0.8.2) thor @@ -291,7 +291,7 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.77.0) + haml_lint (0.78.0) haml (>= 5.0) parallel (>= 1.10) rainbow @@ -341,7 +341,7 @@ GEM inline_svg (1.10.0) activesupport (>= 3.0) nokogiri (>= 1.6) - io-console (0.8.2) + io-console (0.9.2) ipaddr (1.2.9) irb (1.18.0) pp (>= 0.6.0) @@ -582,7 +582,7 @@ GEM opentelemetry-api (~> 1.0) orm_adapter (0.5.0) ostruct (0.6.3) - ox (2.14.28) + ox (2.14.29) bigdecimal (>= 3.0) parallel (2.1.0) parser (3.3.12.0) @@ -600,7 +600,7 @@ GEM mime-types (>= 3.0) pp (0.6.4) prettyprint - premailer (1.29.0) + premailer (1.30.0) addressable css_parser (>= 1.19.0) htmlentities (>= 4.0.0) @@ -684,7 +684,7 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.4.2) - rbs (4.1.3) + rbs (4.2.0) logger prism (>= 1.6.0) tsort @@ -754,8 +754,8 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 9) rspec-support (3.13.7) - rubocop (1.89.0) - json (~> 2.3) + rubocop (1.90.0) + json (>= 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) parallel (>= 1.10) @@ -802,7 +802,7 @@ GEM ruby-vips (2.3.0) ffi (~> 1.12) logger - rubyzip (3.4.1) + rubyzip (3.5.0) rufus-scheduler (3.9.2) fugit (~> 1.1, >= 1.11.1) safety_net_attestation (0.5.0) @@ -816,7 +816,7 @@ GEM securerandom (0.4.1) shoulda-matchers (8.0.1) activesupport (>= 7.2) - sidekiq (8.1.6) + sidekiq (8.1.7) connection_pool (>= 3.0.0) json (>= 2.16.0) logger (>= 1.7.0) @@ -844,7 +844,7 @@ GEM stackprof (0.2.28) starry (0.2.0) base64 - stoplight (5.8.2) + stoplight (5.8.3) concurrent-ruby zeitwerk strong_migrations (2.8.0) @@ -856,7 +856,7 @@ GEM faraday (~> 2.0) faraday-follow_redirects sysexits (1.2.0) - temple (0.10.6) + temple (0.10.7) terminal-table (4.0.0) unicode-display_width (>= 1.1.1, < 4) terrapin (1.1.1) @@ -864,9 +864,9 @@ GEM test-prof (1.6.3) logger thor (1.5.0) - tilt (2.8.0) + tilt (2.9.0) timeout (0.6.1) - tpm-key_attestation (0.14.1) + tpm-key_attestation (0.14.2) bindata (~> 2.4) openssl (> 2.0) openssl-signature_algorithm (~> 1.0) @@ -913,7 +913,7 @@ GEM activesupport faraday (~> 2.0) faraday-follow_redirects - webmock (3.26.2) + webmock (3.26.3) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -1091,4 +1091,4 @@ RUBY VERSION ruby 4.0.6 BUNDLED WITH - 4.0.18 + 4.0.19 diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index 42965696da1409..dcdc9afd7d16d2 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -5,6 +5,12 @@ class BaseController < ApplicationController include Authorization include AccountableConcern + content_security_policy do |p| + policy = ContentSecurityPolicy.new + p.img_src(*p.img_src, *policy.admin_media_hosts) + p.media_src(*p.media_src, *policy.admin_media_hosts) + end + layout 'admin' before_action :require_moderator_or_admin_permissions diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb index d248a04d61bc91..fd810d3f08e7b2 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -59,7 +59,7 @@ def create def filter_by_domain scope = EmailDomainBlock.parents.includes(:children).order(id: :desc) - scope.merge!(EmailDomainBlock.matches_domain(params[:domain])) if params[:domain].present? + scope.merge!(EmailDomainBlock.matches_domain(params[:by_domain])) if params[:by_domain].present? scope end diff --git a/app/controllers/admin/export_domain_allows_controller.rb b/app/controllers/admin/export_domain_allows_controller.rb index d1a2ea5bbf253a..5d7e92a1260185 100644 --- a/app/controllers/admin/export_domain_allows_controller.rb +++ b/app/controllers/admin/export_domain_allows_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'csv' - module Admin class ExportDomainAllowsController < BaseController include Admin::ExportControllerConcern diff --git a/app/controllers/admin/export_domain_blocks_controller.rb b/app/controllers/admin/export_domain_blocks_controller.rb index 06ce88f63d3a8a..50bbb489b5eb1f 100644 --- a/app/controllers/admin/export_domain_blocks_controller.rb +++ b/app/controllers/admin/export_domain_blocks_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'csv' - module Admin class ExportDomainBlocksController < BaseController include Admin::ExportControllerConcern diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 4c37d80422bd9e..0edfd8d3e26821 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -45,7 +45,7 @@ def create end def follow - follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, languages: params.key?(:languages) ? params[:languages] : nil, with_rate_limit: true) + follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, languages: params.key?(:languages) ? params[:languages] : nil, with_rate_limit: true, ref: params[:ref]) options = if @account.locked? || current_user.account.silenced? || (current_user.account.bot? && @account.user&.setting_lock_follow_from_bot) {} else diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 267107b6272c1e..a885333b936cf9 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -56,7 +56,7 @@ def reject_media? end def media_attachment_file_path - if @media_attachment.discarded? + if @media_attachment.on_hold? expiring_asset_url(media_attachment_file, 10.minutes) else full_asset_url(media_attachment_file.url(attachment_style)) @@ -76,6 +76,6 @@ def preview_requested? end def requires_file_streaming? - Paperclip::Attachment.default_options[:storage] == :filesystem && @media_attachment.discarded? + Paperclip::Attachment.default_options[:storage] == :filesystem && @media_attachment.on_hold? end end diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb index a8dc31b7c74e3b..6004012561d062 100644 --- a/app/controllers/settings/imports_controller.rb +++ b/app/controllers/settings/imports_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'csv' - class Settings::ImportsController < Settings::BaseController before_action :set_bulk_import, only: [:show, :confirm, :destroy] before_action :set_recent_imports, only: [:index] diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index b5c7ef9a48a3bb..aab1e0e227cbcb 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -241,9 +241,8 @@ def render_initial_state state_params[:owner] = Account.local.without_suspended.without_internal.first if single_user_mode? json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json - # rubocop:disable Rails/OutputSafety + # rubocop:disable-next Rails/OutputSafety content_tag(:script, json_escape(json).html_safe, id: 'initial-state', type: 'application/json') - # rubocop:enable Rails/OutputSafety end def grouped_scopes(scopes) diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index 3db127a99f7c3a..7da05d2e1bb11e 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -97,8 +97,7 @@ def render_seo_schema(status) adapter: SEO::Adapter ).to_json - # rubocop:disable Rails/OutputSafety + # rubocop:disable-next Rails/OutputSafety content_tag(:script, json_escape(json).html_safe, type: 'application/ld+json') - # rubocop:enable Rails/OutputSafety end end diff --git a/app/helpers/wrapstodon_helper.rb b/app/helpers/wrapstodon_helper.rb index 5a0075a0e58be8..22bfa77665e419 100644 --- a/app/helpers/wrapstodon_helper.rb +++ b/app/helpers/wrapstodon_helper.rb @@ -14,8 +14,7 @@ def render_wrapstodon_share_data(report) json_string = payload.to_json - # rubocop:disable Rails/OutputSafety + # rubocop:disable-next Rails/OutputSafety content_tag(:script, json_escape(json_string).html_safe, type: 'application/json', id: 'wrapstodon-data') - # rubocop:enable Rails/OutputSafety end end diff --git a/app/javascript/fonts/cal-sans-ui/CalSansUI[wght,GEOM].woff2 b/app/javascript/fonts/cal-sans-ui/CalSansUI[wght,GEOM].woff2 new file mode 100644 index 00000000000000..8501cb71929e04 Binary files /dev/null and b/app/javascript/fonts/cal-sans-ui/CalSansUI[wght,GEOM].woff2 differ diff --git a/app/javascript/fonts/syne/OFL.txt b/app/javascript/fonts/cal-sans-ui/OFL.txt similarity index 94% rename from app/javascript/fonts/syne/OFL.txt rename to app/javascript/fonts/cal-sans-ui/OFL.txt index f410e51ceb99dc..ec25d93bf0d550 100644 --- a/app/javascript/fonts/syne/OFL.txt +++ b/app/javascript/fonts/cal-sans-ui/OFL.txt @@ -1,8 +1,9 @@ -Copyright 2017 The Syne Project Authors (https://gitlab.com/bonjour-monde/fonderie/syne-typeface) +Copyright (c) 2025, Mark Davis , +with Font "Cal Sans UI" and “Cal Sans Text”. Commissioned by Peer Richelsen for Cal.com . This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: -https://scripts.sil.org/OFL +http://scripts.sil.org/OFL ----------------------------------------------------------- diff --git a/app/javascript/fonts/inter/inter-medium-italic.woff2 b/app/javascript/fonts/inter/inter-medium-italic.woff2 new file mode 100644 index 00000000000000..0dc5a3068f05f3 Binary files /dev/null and b/app/javascript/fonts/inter/inter-medium-italic.woff2 differ diff --git a/app/javascript/fonts/inter/inter-medium.woff2 b/app/javascript/fonts/inter/inter-medium.woff2 new file mode 100644 index 00000000000000..fdfdcc699fc1e1 Binary files /dev/null and b/app/javascript/fonts/inter/inter-medium.woff2 differ diff --git a/app/javascript/fonts/inter/inter-semibold-italic.woff2 b/app/javascript/fonts/inter/inter-semibold-italic.woff2 deleted file mode 100644 index d67d01c6c450a3..00000000000000 Binary files a/app/javascript/fonts/inter/inter-semibold-italic.woff2 and /dev/null differ diff --git a/app/javascript/fonts/inter/inter-semibold.woff2 b/app/javascript/fonts/inter/inter-semibold.woff2 deleted file mode 100644 index fbae113d2855e2..00000000000000 Binary files a/app/javascript/fonts/inter/inter-semibold.woff2 and /dev/null differ diff --git a/app/javascript/fonts/syne/syne-bold.woff2 b/app/javascript/fonts/syne/syne-bold.woff2 deleted file mode 100644 index e2c9861358c098..00000000000000 Binary files a/app/javascript/fonts/syne/syne-bold.woff2 and /dev/null differ diff --git a/app/javascript/images/composer_message.svg b/app/javascript/images/composer_message.svg index a9c86e6d7beff0..1d50ff7c2d93cc 100644 --- a/app/javascript/images/composer_message.svg +++ b/app/javascript/images/composer_message.svg @@ -1 +1,8 @@ - + + + + + + + + diff --git a/app/javascript/images/icons/icon_fediverse.svg b/app/javascript/images/icons/icon_fediverse.svg new file mode 100644 index 00000000000000..3ab7700bbbd32b --- /dev/null +++ b/app/javascript/images/icons/icon_fediverse.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index 5960c3dc2a463e..abedbc18251e00 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -149,6 +149,7 @@ export function fetchAccountFail(id, error) { * @param {Object} options * @param {boolean} [options.reblogs] * @param {boolean} [options.notify] + * @param {string} [options.ref] * @returns {function(): void} */ export function followAccount(id, options = { reblogs: true }) { diff --git a/app/javascript/mastodon/api/accounts.ts b/app/javascript/mastodon/api/accounts.ts index 52c5b017d96023..dfddd947030087 100644 --- a/app/javascript/mastodon/api/accounts.ts +++ b/app/javascript/mastodon/api/accounts.ts @@ -32,7 +32,8 @@ export const apiSubmitAccountNote = (id: string, value: string) => export const apiFollowAccount = ( id: string, params?: { - reblogs: boolean; + reblogs?: boolean; + ref?: string; }, ) => apiRequestPost(`v1/accounts/${id}/follow`, { diff --git a/app/javascript/mastodon/api_types/instance.ts b/app/javascript/mastodon/api_types/instance.ts index d709b833f28aad..a63d9361d25891 100644 --- a/app/javascript/mastodon/api_types/instance.ts +++ b/app/javascript/mastodon/api_types/instance.ts @@ -57,6 +57,7 @@ export interface ApiInstanceJSON { description: string; versions?: Record; }; + icon: { src: string; size: string }[]; contact: { email: string | null; account: ApiAccountJSON | null; diff --git a/app/javascript/mastodon/components/account/index.tsx b/app/javascript/mastodon/components/account/index.tsx index 1f6e6b7a8876a4..4b6de220a43e2f 100644 --- a/app/javascript/mastodon/components/account/index.tsx +++ b/app/javascript/mastodon/components/account/index.tsx @@ -93,6 +93,7 @@ interface AccountProps { extraAccountInfo?: React.ReactNode; className?: string; children?: React.ReactNode; + reference?: string; } export const Account: React.FC = ({ @@ -109,6 +110,7 @@ export const Account: React.FC = ({ extraAccountInfo, className, children, + reference, }) => { const intl = useIntl(); const { signedIn } = useIdentity(); @@ -187,7 +189,7 @@ export const Account: React.FC = ({ modalProps: { accountId: id, onConfirm: () => { - apiFollowAccount(id) + apiFollowAccount(id, { ref: reference }) .then((relationship) => { dispatch( followAccountSuccess({ @@ -294,6 +296,7 @@ export const Account: React.FC = ({ defaultAction, isRemote, signedIn, + reference, ]); if (hidden) { @@ -338,7 +341,7 @@ export const Account: React.FC = ({ /> ); } else { - button = ; + button = ; } if (hideButtons) { diff --git a/app/javascript/mastodon/components/account_header/buttons.tsx b/app/javascript/mastodon/components/account_header/buttons.tsx index af937d284da3ee..81e6b5041abd1e 100644 --- a/app/javascript/mastodon/components/account_header/buttons.tsx +++ b/app/javascript/mastodon/components/account_header/buttons.tsx @@ -96,6 +96,7 @@ const AccountButtonsOther: FC< accountId={accountId} className={classes.followButton} labelLength='long' + reference='profile' /> )} {isFollowing && ( diff --git a/app/javascript/mastodon/components/account_list_item/index.tsx b/app/javascript/mastodon/components/account_list_item/index.tsx index d6da056c8c952f..31f0fd8b9b3408 100644 --- a/app/javascript/mastodon/components/account_list_item/index.tsx +++ b/app/javascript/mastodon/components/account_list_item/index.tsx @@ -199,6 +199,12 @@ const defaultRenderButton = ({ accountId }: RenderButtonOptions) => ( export const AccountListItemFollowButton: React.FC<{ accountId: string | undefined; -}> = ({ accountId }) => ( - + reference?: string; +}> = ({ accountId, reference }) => ( + ); diff --git a/app/javascript/mastodon/components/autosuggest/autosuggest.stories.tsx b/app/javascript/mastodon/components/autosuggest/autosuggest.stories.tsx new file mode 100644 index 00000000000000..f2d47bfb0d06bb --- /dev/null +++ b/app/javascript/mastodon/components/autosuggest/autosuggest.stories.tsx @@ -0,0 +1,119 @@ +import { useRef, useState } from 'react'; + +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { fn } from 'storybook/test'; + +import { accountFactoryImmutable } from '@/testing/factories'; + +import { TextArea } from '../form_fields'; +import menuClasses from '../menu/styles.module.scss'; + +import { useAutosuggestMenu } from './hooks'; +import { AutosuggestItem } from './items'; +import { AutosuggestMenu } from './list'; +import type { + AccountSuggestion, + EmojiSuggestion, + HashtagSuggestion, + Suggestion, +} from './types'; + +type SuggestTypes = Suggestion['type']; + +const suggestionsMap = { + account: [{ type: 'account', id: '1' }] satisfies AccountSuggestion[], + emoji: [ + { type: 'emoji', id: '+1', native: '👍' }, + { type: 'emoji', id: '-1', native: '👎' }, + { type: 'emoji', id: 'smile', native: '🙂' }, + ] satisfies EmojiSuggestion[], + hashtag: [ + { type: 'hashtag', name: 'Testing', id: '1', totalUses: 0 }, + { type: 'hashtag', name: 'Test', id: '2', totalUses: 10 }, + ] satisfies HashtagSuggestion[], +} satisfies Record; + +const fetchCb = fn().mockName('fetching token'); +const selectCb = fn().mockName('selected item'); +const clearCb = fn().mockName('cleared suggestions'); + +const meta = { + title: 'Redesign/Autosuggest', + render() { + const [suggestions, setSuggestions] = useState([]); + const textareaRef = useRef(null); + + const { onTextChange, suggestProps, sourceProps } = useAutosuggestMenu({ + suggestions, + onSelect: selectCb, + onFetch(token) { + const newSuggestions = tokenToSuggestions(token); + + fetchCb(token, newSuggestions); + setSuggestions(newSuggestions); + }, + onClear() { + clearCb(); + setSuggestions([]); + }, + }); + + return ( +
+