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
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,54 @@

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

- Update dependencies

### Fixed

- Fix missing memoization in `Web::PushNotificationWorker` (#36085 by @ClearlyClaire)
- Fix unresponsive areas around GIFV modals in some cases (#36059 by @ClearlyClaire)
- Fix missing `beforeUnload` confirmation when a poll is being authored (#36030 by @ClearlyClaire)
- Fix processing of remote edited statuses with new media and no text (#35970 by @unfokus)
- Fix polls not being displayed in moderation interface (#35644 and #35933 by @ThisIsMissEm)
- 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 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)
- Fix quote revocation not being streamed (#35710 by @ClearlyClaire)
- Fix export of large user archives by enabling Zip64 (#35850 by @ClearlyClaire)

### Changed

- Change labels for quote policy settings (#35893 by @ClearlyClaire)
- Change standalone “Share” page to redirect to web interface after posting (#35763 by @ChaosExAnima)

## [4.4.3] - 2025-08-05

### Security
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -735,7 +735,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.6.0)
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
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
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,17 @@ const searchabilityOptions = {
const PrivacyModalButton: FC<PrivacyDropdownProps> = ({ disabled = false }) => {
const intl = useIntl();

const { visibility, searchability, quotePolicy, circleId } = useAppSelector(
(state) => ({
visibility: state.compose.get('privacy') as StatusVisibility,
searchability: state.compose.get('searchability') as StatusSearchability,
quotePolicy: state.compose.get('quote_policy') as ApiQuotePolicy,
circleId: state.compose.get('circle_id') as string,
}),
const quotePolicy = useAppSelector(
(state) => state.compose.get('quote_policy') as ApiQuotePolicy,
);
const visibility = useAppSelector(
(state) => state.compose.get('privacy') as StatusVisibility,
);
const searchability = useAppSelector(
(state) => state.compose.get('searchability') as StatusSearchability,
);
const circleId = useAppSelector(
(state) => state.compose.get('circle_id') as string,
);

const { icon, iconComponent } = useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,9 @@ const DEFAULTS = [
const RECENT_SIZE = DEFAULTS.length;

const getFrequentlyUsedEmojis = createSelector([
state => { return {
emojiCounters: state.getIn(['settings', 'frequentlyUsedEmojis'], ImmutableMap()),
reactionDeck: state.get('reaction_deck', ImmutableList()),
}; },
], data => {
const { emojiCounters, reactionDeck } = data;

state => state.getIn(['settings', 'frequentlyUsedEmojis'], ImmutableMap()),
state => state.get('reaction_deck', ImmutableList()),
], (emojiCounters, reactionDeck) => {
let deckEmojis = reactionDeck
.toArray()
.map((e) => e.get('name'))
Expand Down
15 changes: 14 additions & 1 deletion app/javascript/mastodon/features/emoji/emoji_html.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { ComponentPropsWithoutRef, ElementType } from 'react';

import { isModernEmojiEnabled } from '@/mastodon/utils/environment';

import { useEmojify } from './hooks';
import type { CustomEmojiMapArg } from './types';

Expand All @@ -13,7 +15,7 @@ type EmojiHTMLProps<Element extends ElementType = 'div'> = Omit<
shallow?: boolean;
};

export const EmojiHTML = ({
export const ModernEmojiHTML = ({
extraEmojis,
htmlString,
as: Wrapper = 'div', // Rename for syntax highlighting
Expand All @@ -34,3 +36,14 @@ export const EmojiHTML = ({
<Wrapper {...props} dangerouslySetInnerHTML={{ __html: emojifiedHtml }} />
);
};

export const EmojiHTML = <Element extends ElementType>(
props: EmojiHTMLProps<Element>,
) => {
if (isModernEmojiEnabled()) {
return <ModernEmojiHTML {...props} />;
}
const { as: asElement, htmlString, extraEmojis, ...rest } = props;
const Wrapper = asElement ?? 'div';
return <Wrapper {...rest} dangerouslySetInnerHTML={{ __html: htmlString }} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useIntl, defineMessages } from 'react-intl';
import { fetchAntennas } from '@/mastodon/actions/antennas_typed';
import AntennaIcon from '@/material-icons/400-24px/wifi.svg?react';
import { ColumnLink } from 'mastodon/features/ui/components/column_link';
import { getOrderedAntennas } from 'mastodon/selectors/antennas';
import { getFavouritedAntennas } from 'mastodon/selectors/antennas';
import { useAppDispatch, useAppSelector } from 'mastodon/store';

import { CollapsiblePanel } from './collapsible_panel';
Expand All @@ -25,7 +25,7 @@ const messages = defineMessages({
export const AntennaPanel: React.FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const antennas = useAppSelector((state) => getOrderedAntennas(state));
const antennas = useAppSelector((state) => getFavouritedAntennas(state));
const [loading, setLoading] = useState(false);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import ListAltActiveIcon from '@/material-icons/400-24px/list_alt-fill.svg?react
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { fetchLists } from 'mastodon/actions/lists';
import { ColumnLink } from 'mastodon/features/ui/components/column_link';
import { getOrderedLists } from 'mastodon/selectors/lists';
import { getFavouritedLists } from 'mastodon/selectors/lists';
import { useAppDispatch, useAppSelector } from 'mastodon/store';

import { CollapsiblePanel } from './collapsible_panel';
Expand All @@ -26,7 +26,7 @@ const messages = defineMessages({
export const ListPanel: React.FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const lists = useAppSelector((state) => getOrderedLists(state));
const lists = useAppSelector((state) => getFavouritedLists(state));
const [loading, setLoading] = useState(false);

useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/mastodon/features/status/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const makeMapStateToProps = () => {
if (status) {
ancestorsIds = getAncestorsIds(state, status.get('in_reply_to_id'));
descendantsIds = getDescendantsIds(state, status.get('id'));
referencesIds = getReferencesIds(state, status.get('id'));
referencesIds = getReferencesIds(state, status.get('id'), status.getIn(['quote', 'quoted_status']));
}

return {
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 @@ -55,6 +55,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
9 changes: 9 additions & 0 deletions app/javascript/mastodon/selectors/antennas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,12 @@ export const getOrderedAntennas = createAppSelector(
.sort((a: Antenna, b: Antenna) => a.title.localeCompare(b.title))
.toArray(),
);

export const getFavouritedAntennas = createAppSelector(
[(state) => getAntennas(state)],
(antennas) =>
antennas
.sort((a: Antenna, b: Antenna) => a.title.localeCompare(b.title))
.filter((antenna) => antenna.favourite)
.toArray(),
);
10 changes: 7 additions & 3 deletions app/javascript/mastodon/selectors/contexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,12 @@ export const getDescendantsIds = createAppSelector(
);

export const getReferencesIds = createAppSelector(
[(_, id: string) => id, (state) => state.contexts.references],
(statusId, references) => {
return references[statusId] ?? [];
[
(_, id: string) => id,
(state) => state.contexts.references,
(_, __, exceptId: string) => exceptId,
],
(statusId, references, exceptId) => {
return references[statusId]?.filter((id) => exceptId !== id) ?? [];
},
);
9 changes: 9 additions & 0 deletions app/javascript/mastodon/selectors/lists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,12 @@ export const getOrderedLists = createAppSelector(
(lists) =>
lists.sort((a: List, b: List) => a.title.localeCompare(b.title)).toArray(),
);

export const getFavouritedLists = createAppSelector(
[(state) => getLists(state)],
(lists) =>
lists
.sort((a: List, b: List) => a.title.localeCompare(b.title))
.filter((list) => list.favourite)
.toArray(),
);
2 changes: 1 addition & 1 deletion app/lib/status_cache_hydrator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,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
Loading