Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/mastodon/actions/importer/normalizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
}
Expand Down
8 changes: 5 additions & 3 deletions app/javascript/mastodon/components/hover_card_account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 =
Expand Down
38 changes: 37 additions & 1 deletion app/javascript/mastodon/components/status_quoted.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo } from 'react';

import { FormattedMessage } from 'react-intl';

Expand All @@ -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;

Expand Down Expand Up @@ -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 (
<>
<FormattedMessage
id='status.quote_error.limited_account_hint.title'
defaultMessage='This account has been hidden by the moderators of {domain}.'
values={{ domain }}
/>
<button onClick={reveal} className='link-button'>
<FormattedMessage
id='status.quote_error.limited_account_hint.action'
defaultMessage='Show anyway'
/>
</button>
</>
);
};

export const QuotedStatus: React.FC<{
quote: QuoteMap;
contextType?: string;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -164,6 +198,8 @@ export const QuotedStatus: React.FC<{
defaultMessage='This post cannot be displayed.'
/>
);
} else if (hiddenAccount && accountId) {
quoteError = <LimitedAccountHint accountId={accountId} />;
}

if (quoteError) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -548,15 +555,15 @@ 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,
});
}
arr.push(null);

if (relationship?.followed_by) {
if (!isHideRelationships && relationship?.followed_by) {
const handleRemoveFromFollowers = () => {
dispatch(
openModal({
Expand Down Expand Up @@ -709,6 +716,7 @@ export const AccountHeader: React.FC<{
handleReblogToggle,
handleReport,
handleUnblockDomain,
isHideRelationships,
]);

if (!account) {
Expand All @@ -724,6 +732,7 @@ export const AccountHeader: React.FC<{

if (me !== account.id && relationship) {
if (
!isHideRelationships &&
relationship.followed_by &&
(relationship.following || relationship.requested)
) {
Expand All @@ -735,7 +744,7 @@ export const AccountHeader: React.FC<{
/>
</span>,
);
} else if (relationship.followed_by) {
} else if (!isHideRelationships && relationship.followed_by) {
info.push(
<span key='followed_by' className='relationship-tag'>
<FormattedMessage
Expand All @@ -744,7 +753,7 @@ export const AccountHeader: React.FC<{
/>
</span>,
);
} else if (relationship.requested_by) {
} else if (!relationship.followed_by && relationship.requested_by) {
info.push(
<span key='requested_by' className='relationship-tag'>
<FormattedMessage
Expand Down
2 changes: 2 additions & 0 deletions app/javascript/mastodon/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1082,6 +1082,8 @@
"status.open": "Expand this post",
"status.pin": "Pin on profile",
"status.quote_error.filtered": "Hidden due to one of your filters",
"status.quote_error.limited_account_hint.action": "Show anyway",
"status.quote_error.limited_account_hint.title": "This account has been hidden by the moderators of {domain}.",
"status.quote_error.not_found": "This post cannot be displayed.",
"status.quote_error.pending_approval": "This post is pending approval from the original author.",
"status.quote_error.rejected": "This post cannot be displayed as the original author does not allow it to be quoted.",
Expand Down
1 change: 1 addition & 0 deletions app/javascript/mastodon/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"account.followers": "フォロワー",
"account.followers.empty": "まだ誰もフォローしていません。",
"account.followers_counter": "{count, plural, other {{counter} フォロワー}}",
"account.followers.hidden_from_me": "この情報はあなた自身の設定によって隠されています。",
"account.followers_you_know_counter": "あなたと知り合いの{counter}人",
"account.following": "フォロー中",
"account.following_counter": "{count, plural, other {{counter} フォロー}}",
Expand Down
3 changes: 3 additions & 0 deletions app/lib/activitypub/activity/update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ def update_status

@status = Status.find_by(uri: object_uri, account_id: @account.id)

# We may be getting `Create` and `Update` out of order
@status ||= ActivityPub::Activity::Create.new(@json, @account, **@options).perform

return if @status.nil?

ActivityPub::ProcessStatusUpdateService.new.call(@status, @json, @object, request_id: @options[:request_id])
Expand Down
2 changes: 1 addition & 1 deletion app/lib/status_cache_hydrator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def hydrate_quote_payload(empty_payload, quote, account_id, nested: false)
if quote.quoted_status.nil?
payload[nested ? :quoted_status_id : :quoted_status] = nil
payload[:state] = 'deleted'
elsif StatusFilter.new(quote.quoted_status, Account.find_by(id: account_id)).filtered?
elsif StatusFilter.new(quote.quoted_status, Account.find_by(id: account_id)).filtered_for_quote?
payload[nested ? :quoted_status_id : :quoted_status] = nil
payload[:state] = 'unauthorized'
else
Expand Down
6 changes: 6 additions & 0 deletions app/lib/status_filter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ def filtered?
blocked_by_policy? || (account_present? && filtered_status?) || silenced_account?
end

def filtered_for_quote?
return false if !account.nil? && account.id == status.account_id

blocked_by_policy? || (account_present? && filtered_status?)
end

private

def account_present?
Expand Down
1 change: 1 addition & 0 deletions app/models/concerns/status/search_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def searchable_properties
properties << 'sensitive' if sensitive?
properties << 'reply' if reply?
properties << 'reference' if with_status_reference?
properties << 'quote' if with_quote?
end
end
end
4 changes: 2 additions & 2 deletions app/serializers/rest/base_quote_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ def state

# Extra states when a status is unavailable
return 'deleted' if object.quoted_status.nil?
return 'unauthorized' if status_filter.filtered?
return 'unauthorized' if status_filter.filtered_for_quote?

object.state
end

def quoted_status
object.quoted_status if object.accepted? && object.quoted_status.present? && !status_filter.filtered?
object.quoted_status if object.accepted? && object.quoted_status.present? && !status_filter.filtered_for_quote?
end

private
Expand Down
3 changes: 3 additions & 0 deletions app/services/activitypub/process_status_update_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ def call(status, activity_json, object_json, request_id: nil)
return @status unless valid_status?

handle_explicit_update!
elsif @status.edited_at.present? && (@status_parser.edited_at.nil? || @status_parser.edited_at < @status.edited_at)
# This is an older update, reject it
return @status
else
handle_implicit_update!
end
Expand Down
6 changes: 3 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ services:
web:
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
build: .
image: kmyblue:19.5-lts
image: kmyblue:19.6-lts
restart: always
env_file: .env.production
command: bundle exec puma -C config/puma.rb
Expand All @@ -83,7 +83,7 @@ services:
build:
dockerfile: ./streaming/Dockerfile
context: .
image: kmyblue-streaming:19.5-lts
image: kmyblue-streaming:19.6-lts
restart: always
env_file: .env.production
command: node ./streaming/index.js
Expand All @@ -101,7 +101,7 @@ services:

sidekiq:
build: .
image: kmyblue:19.5-lts
image: kmyblue:19.6-lts
restart: always
env_file: .env.production
command: bundle exec sidekiq
Expand Down
4 changes: 2 additions & 2 deletions lib/mastodon/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def kmyblue_major
end

def kmyblue_minor
5
6
end

def kmyblue_flag
Expand All @@ -31,7 +31,7 @@ def minor
end

def patch
4
5
end

def default_prerelease
Expand Down
5 changes: 3 additions & 2 deletions spec/lib/activitypub/activity/update_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,10 @@
subject.perform
end

it 'does not create a new status', :inline_jobs do
it 'creates a new status', :inline_jobs do
status = Status.find_by(uri: 'https://example.com/note')
expect(status).to be_nil
expect(status).to_not be_nil
expect(status.text).to eq 'Ohagi is tsubuan'
end
end

Expand Down
Loading