diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49da341ec1c4a2..ad084c8a562e95 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,26 @@
All notable changes to this project will be documented in this file.
+## [4.4.5] - 2025-09-23
+
+### Security
+
+- Update dependencies
+
+### Added
+
+- Add support for `has:quote` in search (#36217 by @ClearlyClaire)
+
+### Changed
+
+- Change quoted posts from silenced accounts to use a click-through rather than being hidden (#36166 and #36167 by @ClearlyClaire)
+
+### Fixed
+
+- Fix processing of out-of-order `Update` as implicit updates (#36190 by @ClearlyClaire)
+- Fix getting `Create` and `Update` out of order (#36176 by @ClearlyClaire)
+- Fix quotes with Content Warnings but no text being shown without Content Warnings (#36150 by @ClearlyClaire)
+
## [4.4.4] - 2025-09-16
### Security
@@ -18,7 +38,7 @@ All notable changes to this project will be documented in this file.
- Fix WebUI handling of deleted quoted posts (#35909 and #35918 by @ClearlyClaire and @diondiondion)
- Fix “Edit” and “Delete & Redraft” on a poll not inserting empty option (#35892 by @ClearlyClaire)
- Fix loading of some compatibility CSS on some configurations (#35876 by @shleeable)
-- Fix HttpLog not being enabled with `RAILS_LOGÈ_LEVEL=debug` (#35833 by @mjankowski)
+- Fix HttpLog not being enabled with `RAILS_LOG_LEVEL=debug` (#35833 by @mjankowski)
- Fix self-destruct scheduler behavior on some Redis setups (#35823 by @ClearlyClaire)
- Fix `tootctl admin create` not bypassing reserved username checks (#35779 by @ClearlyClaire)
- Fix interaction policy changes in implicit updates not being saved (#35751 by @ClearlyClaire)
diff --git a/Gemfile.lock b/Gemfile.lock
index 0718b102a8d7e3..2d144104b2f602 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -725,7 +725,7 @@ GEM
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
- rexml (3.4.1)
+ rexml (3.4.4)
rotp (6.3.0)
rouge (4.5.2)
rpam2 (4.0.2)
diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js
index 5bb221df305406..cdb91dc8a8e6a6 100644
--- a/app/javascript/mastodon/actions/importer/normalizer.js
+++ b/app/javascript/mastodon/actions/importer/normalizer.js
@@ -93,7 +93,7 @@ export function normalizeStatus(status, normalOldStatus, options = undefined) {
} else {
// If the status has a CW but no contents, treat the CW as if it were the
// status' contents, to avoid having a CW toggle with seemingly no effect.
- if (normalStatus.spoiler_text && !normalStatus.content) {
+ if (normalStatus.spoiler_text && !normalStatus.content && !normalStatus.quote) {
normalStatus.content = normalStatus.spoiler_text;
normalStatus.spoiler_text = '';
}
diff --git a/app/javascript/mastodon/components/hover_card_account.tsx b/app/javascript/mastodon/components/hover_card_account.tsx
index 87fbec0e25a85c..a0d2ad7cfb4745 100644
--- a/app/javascript/mastodon/components/hover_card_account.tsx
+++ b/app/javascript/mastodon/components/hover_card_account.tsx
@@ -19,7 +19,7 @@ import { FollowButton } from 'mastodon/components/follow_button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { ShortNumber } from 'mastodon/components/short_number';
import { useFetchFamiliarFollowers } from 'mastodon/features/account_timeline/hooks/familiar_followers';
-import { domain } from 'mastodon/initial_state';
+import { domain, isHideItem } from 'mastodon/initial_state';
import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
@@ -54,8 +54,10 @@ export const HoverCardAccount = forwardRef<
const relationship = useAppSelector((state) =>
accountId ? state.relationships.get(accountId) : undefined,
);
- const isMutual = relationship?.followed_by && relationship.following;
- const isFollower = relationship?.followed_by;
+ const isHideRelationships = isHideItem('relationships');
+ const isMutual =
+ !isHideRelationships && relationship?.followed_by && relationship.following;
+ const isFollower = !isHideRelationships && relationship?.followed_by;
const hasRelationshipLoaded = !!relationship;
const shouldDisplayFamiliarFollowers =
diff --git a/app/javascript/mastodon/components/status_quoted.tsx b/app/javascript/mastodon/components/status_quoted.tsx
index 3f7f51cf0643a3..d58760ac6e37c4 100644
--- a/app/javascript/mastodon/components/status_quoted.tsx
+++ b/app/javascript/mastodon/components/status_quoted.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo } from 'react';
+import { useCallback, useEffect, useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
@@ -11,13 +11,16 @@ import ArticleIcon from '@/material-icons/400-24px/article.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import { Icon } from 'mastodon/components/icon';
import StatusContainer from 'mastodon/containers/status_container';
+import { domain } from 'mastodon/initial_state';
import type { Status } from 'mastodon/models/status';
import type { RootState } from 'mastodon/store';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import QuoteIcon from '../../images/quote.svg?react';
+import { revealAccount } from '../actions/accounts_typed';
import { fetchStatus } from '../actions/statuses';
import { makeGetStatus } from '../selectors';
+import { getAccountHidden } from '../selectors/accounts';
const MAX_QUOTE_POSTS_NESTING_LEVEL = 1;
@@ -73,6 +76,29 @@ type GetStatusSelector = (
props: { id?: string | null; contextType?: string },
) => Status | null;
+const LimitedAccountHint: React.FC<{ accountId: string }> = ({ accountId }) => {
+ const dispatch = useAppDispatch();
+ const reveal = useCallback(() => {
+ dispatch(revealAccount({ id: accountId }));
+ }, [dispatch, accountId]);
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
export const QuotedStatus: React.FC<{
quote: QuoteMap;
contextType?: string;
@@ -100,6 +126,14 @@ export const QuotedStatus: React.FC<{
const shouldLoadQuote = !status?.get('isLoading') && quoteState !== 'deleted';
+ const accountId: string | null = status?.get('account', null) as
+ | string
+ | null;
+
+ const hiddenAccount = useAppSelector(
+ (state) => accountId && getAccountHidden(state, accountId),
+ );
+
useEffect(() => {
if (shouldLoadQuote && quotedStatusId) {
dispatch(
@@ -164,6 +198,8 @@ export const QuotedStatus: React.FC<{
defaultMessage='This post cannot be displayed.'
/>
);
+ } else if (hiddenAccount && accountId) {
+ quoteError = ;
}
if (quoteError) {
diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx
index d7f228129af6b7..5887ce114f90b2 100644
--- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx
+++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx
@@ -50,7 +50,12 @@ import { DomainPill } from 'mastodon/features/account/components/domain_pill';
import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container';
import { useLinks } from 'mastodon/hooks/useLinks';
import { useIdentity } from 'mastodon/identity_context';
-import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
+import {
+ autoPlayGif,
+ me,
+ domain as localDomain,
+ isHideItem,
+} from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account';
import type { MenuItem } from 'mastodon/models/dropdown_menu';
import {
@@ -220,6 +225,8 @@ export const AccountHeader: React.FC<{
const hidden = useAppSelector((state) => getAccountHidden(state, accountId));
const handleLinkClick = useLinks();
+ const isHideRelationships = isHideItem('relationships');
+
const handleBlock = useCallback(() => {
if (!account) {
return;
@@ -548,7 +555,7 @@ export const AccountHeader: React.FC<{
text: intl.formatMessage(messages.add_or_remove_from_exclude_antenna),
action: handleAddToExcludeAntenna,
});
- if (relationship?.followed_by) {
+ if (!isHideRelationships && relationship?.followed_by) {
arr.push({
text: intl.formatMessage(messages.add_or_remove_from_circle),
action: handleAddToCircle,
@@ -556,7 +563,7 @@ export const AccountHeader: React.FC<{
}
arr.push(null);
- if (relationship?.followed_by) {
+ if (!isHideRelationships && relationship?.followed_by) {
const handleRemoveFromFollowers = () => {
dispatch(
openModal({
@@ -709,6 +716,7 @@ export const AccountHeader: React.FC<{
handleReblogToggle,
handleReport,
handleUnblockDomain,
+ isHideRelationships,
]);
if (!account) {
@@ -724,6 +732,7 @@ export const AccountHeader: React.FC<{
if (me !== account.id && relationship) {
if (
+ !isHideRelationships &&
relationship.followed_by &&
(relationship.following || relationship.requested)
) {
@@ -735,7 +744,7 @@ export const AccountHeader: React.FC<{
/>
,
);
- } else if (relationship.followed_by) {
+ } else if (!isHideRelationships && relationship.followed_by) {
info.push(
,
);
- } else if (relationship.requested_by) {
+ } else if (!relationship.followed_by && relationship.requested_by) {
info.push(