Merge commit 'e91c7645905972d9663a0d944b133ff24670bce2' into glitch-soc/merge-4.5

This commit is contained in:
Claire
2025-11-05 10:02:33 +01:00
37 changed files with 786 additions and 79 deletions

View File

@@ -6,13 +6,14 @@ All notable changes to this project will be documented in this file.
### Added ### Added
- **Add support for allowing and authoring quotes** (#35355, #35578, #35614, #35618, #35624, #35626, #35652, #35629, #35665, #35653, #35670, #35677, #35690, #35697, #35689, #35699, #35700, #35701, #35709, #35714, #35713, #35715, #35725, #35749, #35769, #35780, #35762, #35804, #35808, #35805, #35819, #35824, #35828, #35822, #35835, #35865, #35860, #35832, #35891, #35894, #35895, #35820, #35917, #35924, #35925, #35914, #35930, #35941, #35939, #35948, #35955, #35967, #35990, #35991, #35975, #35971, #36002, #35986, #36031, #36034, #36038, #36054, #36052, #36055, #36065, #36068, #36083, #36087, #36080, #36091, #36090, #36118, #36119, #36128, #36094, #36129, #36138, #36132, #36151, #36158, #36171, #36194, #36220, #36169, #36130, #36249, #36153, #36299, #36291, #36301, #36315, #36317, #36364, #36383, #36381, #36459, #36464, #36461, #36516, #36528, #36549, #36550 and #36559 by @ChaosExAnima, @ClearlyClaire, @Lycolia, @diondiondion, and @tribela)\ - **Add support for allowing and authoring quotes** (#35355, #35578, #35614, #35618, #35624, #35626, #35652, #35629, #35665, #35653, #35670, #35677, #35690, #35697, #35689, #35699, #35700, #35701, #35709, #35714, #35713, #35715, #35725, #35749, #35769, #35780, #35762, #35804, #35808, #35805, #35819, #35824, #35828, #35822, #35835, #35865, #35860, #35832, #35891, #35894, #35895, #35820, #35917, #35924, #35925, #35914, #35930, #35941, #35939, #35948, #35955, #35967, #35990, #35991, #35975, #35971, #36002, #35986, #36031, #36034, #36038, #36054, #36052, #36055, #36065, #36068, #36083, #36087, #36080, #36091, #36090, #36118, #36119, #36128, #36094, #36129, #36138, #36132, #36151, #36158, #36171, #36194, #36220, #36169, #36130, #36249, #36153, #36299, #36291, #36301, #36315, #36317, #36364, #36383, #36381, #36459, #36464, #36461, #36516, #36528, #36549, #36550, #36559, #36693, #36704, #36690, #36689, #36696, #36721 and #36695 by @ChaosExAnima, @ClearlyClaire, @Lycolia, @diondiondion, and @tribela)\
This includes a revamp of the composer interface.\ This includes a revamp of the composer interface.\
See https://blog.joinmastodon.org/2025/09/introducing-quote-posts/ for a user-centric overview of the feature, and https://docs.joinmastodon.org/client/quotes/ for API documentation. See https://blog.joinmastodon.org/2025/09/introducing-quote-posts/ for a user-centric overview of the feature, and https://docs.joinmastodon.org/client/quotes/ for API documentation.
- **Add support for fetching and refreshing replies to the web UI** (#35210, #35496, #35575, #35500, #35577, #35602, #35603, #35654, #36141, #36237, #36172, #36256, #36271, #36334, #36382, #36239, #36484, #36481, #36583, #36627 and #36547 by @ClearlyClaire, @diondiondion, @Gargron and @renchap) - **Add support for fetching and refreshing replies to the web UI** (#35210, #35496, #35575, #35500, #35577, #35602, #35603, #35654, #36141, #36237, #36172, #36256, #36271, #36334, #36382, #36239, #36484, #36481, #36583, #36627 and #36547 by @ClearlyClaire, @diondiondion, @Gargron and @renchap)
- **Add ability to block words in usernames** (#35407, #35655, and #35806 by @ClearlyClaire and @Gargron) - **Add ability to block words in usernames** (#35407, #35655, and #35806 by @ClearlyClaire and @Gargron)
- Add ability to individually disable local or remote feeds for visitors or logged-in users `disabled` value to server setting for live and topic feeds, as well as user permission to bypass that (#36338, #36467, #36497, #36563, #36577, #36585, and #36607 by @ClearlyClaire)\ - Add ability to individually disable local or remote feeds for visitors or logged-in users `disabled` value to server setting for live and topic feeds, as well as user permission to bypass that (#36338, #36467, #36497, #36563, #36577, #36585, #36607 and #36703 by @ClearlyClaire)\
This splits the `timeline_preview` setting into four more granular settings controlling live feeds and topic (hashtag, trending link) feeds, with 3 values each: `public`, `authenticated`, `disabled`.\ This splits the `timeline_preview` setting into four more granular settings controlling live feeds and topic (hashtag, trending link) feeds.\
The setting for local topic feeds has 2 values: `public` and `authenticated`. Every other setting has 3 values: `public`, `authenticated`, `disabled`.\
When `disabled`, users with the “View live and topic feeds” will still be able to view them. When `disabled`, users with the “View live and topic feeds” will still be able to view them.
- Add support for displaying of quote posts in Moderator UI (#35964 by @ThisIsMissEm) - Add support for displaying of quote posts in Moderator UI (#35964 by @ThisIsMissEm)
- Add support for displaying link previews for Admin UI (#35958 by @ThisIsMissEm) - Add support for displaying link previews for Admin UI (#35958 by @ThisIsMissEm)
@@ -44,6 +45,8 @@ All notable changes to this project will be documented in this file.
- Change display of blocked and muted quoted users (#36619 by @ClearlyClaire)\ - Change display of blocked and muted quoted users (#36619 by @ClearlyClaire)\
This adds `blocked_account`, `blocked_domain` and `muted_account` values to the `state` attribute of `Quote` and `ShallowQuote` REST API entities. This adds `blocked_account`, `blocked_domain` and `muted_account` values to the `state` attribute of `Quote` and `ShallowQuote` REST API entities.
- Change submitting an empty post to show an error rather than failing silently (#36650 by @diondiondion) - Change submitting an empty post to show an error rather than failing silently (#36650 by @diondiondion)
- Change "Privacy and reach" settings from "Public profile" to their own top-level category (#27294 by @ChaelCodes)
- Change number of times quote verification is retried to better deal with temporary failures (#36698 by @ClearlyClaire)
- Change display of content warnings in Admin UI (#35935 by @ThisIsMissEm) - Change display of content warnings in Admin UI (#35935 by @ThisIsMissEm)
- Change styling of column banners (#36531 by @ClearlyClaire) - Change styling of column banners (#36531 by @ClearlyClaire)
- Change recommended Node version to 24 (LTS) (#36539 by @renchap) - Change recommended Node version to 24 (LTS) (#36539 by @renchap)
@@ -75,6 +78,7 @@ All notable changes to this project will be documented in this file.
- Fix URL comparison for mentions in case of empty path (#36613 and #36626 by @ClearlyClaire) - Fix URL comparison for mentions in case of empty path (#36613 and #36626 by @ClearlyClaire)
- Fix hashtags not being picked up when full-width hash sign is used (#36103 and #36625 by @ClearlyClaire and @Gargron) - Fix hashtags not being picked up when full-width hash sign is used (#36103 and #36625 by @ClearlyClaire and @Gargron)
- Fix layout of severed relationships when purged events are listed (#36593 by @mejofi) - Fix layout of severed relationships when purged events are listed (#36593 by @mejofi)
- Fix Skeleton placeholders being animated when setting to reduce animations is enabled (#36716 by @ClearlyClaire)
- Fix vacuum tasks being interrupted by a single batch failure (#36606 by @Gargron) - Fix vacuum tasks being interrupted by a single batch failure (#36606 by @Gargron)
- Fix handling of unreachable network error for search services (#36587 by @mjankowski) - Fix handling of unreachable network error for search services (#36587 by @mjankowski)
- Fix bookmarks export when a bookmarked status is soft-deleted (#36576 by @ClearlyClaire) - Fix bookmarks export when a bookmarked status is soft-deleted (#36576 by @ClearlyClaire)

View File

@@ -128,7 +128,7 @@ GEM
blurhash (0.1.8) blurhash (0.1.8)
bootsnap (1.18.6) bootsnap (1.18.6)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (7.0.2) brakeman (7.1.1)
racc racc
browser (6.2.0) browser (6.2.0)
builder (3.3.0) builder (3.3.0)
@@ -224,7 +224,7 @@ GEM
mail (~> 2.7) mail (~> 2.7)
email_validator (2.2.4) email_validator (2.2.4)
activemodel activemodel
erb (5.1.1) erb (5.1.3)
erubi (1.13.1) erubi (1.13.1)
et-orbi (1.4.0) et-orbi (1.4.0)
tzinfo tzinfo
@@ -337,7 +337,7 @@ GEM
activesupport (>= 3.0) activesupport (>= 3.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
io-console (0.8.1) io-console (0.8.1)
irb (1.15.2) irb (1.15.3)
pp (>= 0.6.0) pp (>= 0.6.0)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
@@ -621,7 +621,7 @@ GEM
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.8.1) racc (1.8.1)
rack (3.2.3) rack (3.2.4)
rack-attack (6.8.0) rack-attack (6.8.0)
rack (>= 1.0, < 4) rack (>= 1.0, < 4)
rack-cors (3.0.0) rack-cors (3.0.0)
@@ -691,7 +691,7 @@ GEM
readline (~> 0.0) readline (~> 0.0)
rdf-normalize (0.7.0) rdf-normalize (0.7.0)
rdf (~> 3.3) rdf (~> 3.3)
rdoc (6.15.0) rdoc (6.15.1)
erb erb
psych (>= 4.0.0) psych (>= 4.0.0)
tsort tsort
@@ -791,7 +791,7 @@ GEM
ruby-vips (2.2.5) ruby-vips (2.2.5)
ffi (~> 1.12) ffi (~> 1.12)
logger logger
rubyzip (3.2.1) rubyzip (3.2.2)
rufus-scheduler (3.9.2) rufus-scheduler (3.9.2)
fugit (~> 1.1, >= 1.11.1) fugit (~> 1.1, >= 1.11.1)
safety_net_attestation (0.5.0) safety_net_attestation (0.5.0)
@@ -805,7 +805,7 @@ GEM
securerandom (0.4.1) securerandom (0.4.1)
shoulda-matchers (6.5.0) shoulda-matchers (6.5.0)
activesupport (>= 5.2.0) activesupport (>= 5.2.0)
sidekiq (8.0.8) sidekiq (8.0.9)
connection_pool (>= 2.5.0) connection_pool (>= 2.5.0)
json (>= 2.9.0) json (>= 2.9.0)
logger (>= 1.6.2) logger (>= 1.6.2)

View File

@@ -9,7 +9,7 @@ class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController
before_action :set_quote_authorization before_action :set_quote_authorization
def show def show
expires_in 30.seconds, public: true if @quote.status.distributable? && public_fetch_mode? expires_in 30.seconds, public: true if @quote.quoted_status.distributable? && public_fetch_mode?
render json: @quote, serializer: ActivityPub::QuoteAuthorizationSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' render json: @quote, serializer: ActivityPub::QuoteAuthorizationSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end end
@@ -23,7 +23,7 @@ class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController
@quote = Quote.accepted.where(quoted_account: @account).find(params[:id]) @quote = Quote.accepted.where(quoted_account: @account).find(params[:id])
return not_found unless @quote.status.present? && @quote.quoted_status.present? return not_found unless @quote.status.present? && @quote.quoted_status.present?
authorize @quote.status, :show? authorize @quote.quoted_status, :show?
rescue Mastodon::NotPermittedError rescue Mastodon::NotPermittedError
not_found not_found
end end

View File

@@ -56,7 +56,6 @@ export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE'; export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE'; export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE'; export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE'; export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE'; export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE';
@@ -796,13 +795,6 @@ export function changeComposeSpoilerText(text) {
}; };
} }
export function changeComposeVisibility(value) {
return {
type: COMPOSE_VISIBILITY_CHANGE,
value,
};
}
export function insertEmojiCompose(position, emoji, needsSpace) { export function insertEmojiCompose(position, emoji, needsSpace) {
return { return {
type: COMPOSE_EMOJI_INSERT, type: COMPOSE_EMOJI_INSERT,

View File

@@ -13,10 +13,11 @@ import {
} from 'mastodon/store/typed_functions'; } from 'mastodon/store/typed_functions';
import type { ApiQuotePolicy } from '../api_types/quotes'; import type { ApiQuotePolicy } from '../api_types/quotes';
import type { Status } from '../models/status'; import type { Status, StatusVisibility } from '../models/status';
import type { RootState } from '../store';
import { showAlert } from './alerts'; import { showAlert } from './alerts';
import { focusCompose } from './compose'; import { changeCompose, focusCompose } from './compose';
import { importFetchedStatuses } from './importer'; import { importFetchedStatuses } from './importer';
import { openModal } from './modal'; import { openModal } from './modal';
@@ -41,6 +42,10 @@ const messages = defineMessages({
id: 'quote_error.unauthorized', id: 'quote_error.unauthorized',
defaultMessage: 'You are not authorized to quote this post.', defaultMessage: 'You are not authorized to quote this post.',
}, },
quoteErrorPrivateMention: {
id: 'quote_error.private_mentions',
defaultMessage: 'Quoting is not allowed with direct mentions.',
},
}); });
type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & { type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & {
@@ -67,6 +72,39 @@ const simulateModifiedApiResponse = (
return data; return data;
}; };
export const changeComposeVisibility = createAppThunk(
'compose/visibility_change',
(visibility: StatusVisibility, { dispatch, getState }) => {
if (visibility !== 'direct') {
return visibility;
}
const state = getState();
const quotedStatusId = state.compose.get('quoted_status_id') as
| string
| null;
if (!quotedStatusId) {
return visibility;
}
// Remove the quoted status
dispatch(quoteComposeCancel());
const quotedStatus = state.statuses.get(quotedStatusId) as Status | null;
if (!quotedStatus) {
return visibility;
}
// Append the quoted status URL to the compose text
const url = quotedStatus.get('url') as string;
const text = state.compose.get('text') as string;
if (!text.includes(url)) {
const newText = text.trim() ? `${text}\n\n${url}` : url;
dispatch(changeCompose(newText));
}
return visibility;
},
);
export const changeUploadCompose = createDataLoadingThunk( export const changeUploadCompose = createDataLoadingThunk(
'compose/changeUpload', 'compose/changeUpload',
async ( async (
@@ -130,6 +168,8 @@ export const quoteComposeByStatus = createAppThunk(
if (composeState.get('id')) { if (composeState.get('id')) {
dispatch(showAlert({ message: messages.quoteErrorEdit })); dispatch(showAlert({ message: messages.quoteErrorEdit }));
} else if (composeState.get('privacy') === 'direct') {
dispatch(showAlert({ message: messages.quoteErrorPrivateMention }));
} else if (composeState.get('poll')) { } else if (composeState.get('poll')) {
dispatch(showAlert({ message: messages.quoteErrorPoll })); dispatch(showAlert({ message: messages.quoteErrorPoll }));
} else if ( } else if (
@@ -173,6 +213,17 @@ export const quoteComposeById = createAppThunk(
}, },
); );
const composeStateForbidsLink = (composeState: RootState['compose']) => {
return (
composeState.get('quoted_status_id') ||
composeState.get('is_submitting') ||
composeState.get('poll') ||
composeState.get('is_uploading') ||
composeState.get('id') ||
composeState.get('privacy') === 'direct'
);
};
export const pasteLinkCompose = createDataLoadingThunk( export const pasteLinkCompose = createDataLoadingThunk(
'compose/pasteLink', 'compose/pasteLink',
async ({ url }: { url: string }) => { async ({ url }: { url: string }) => {
@@ -183,15 +234,12 @@ export const pasteLinkCompose = createDataLoadingThunk(
limit: 2, limit: 2,
}); });
}, },
(data, { dispatch, getState }) => { (data, { dispatch, getState, requestId }) => {
const composeState = getState().compose; const composeState = getState().compose;
if ( if (
composeState.get('quoted_status_id') || composeStateForbidsLink(composeState) ||
composeState.get('is_submitting') || composeState.get('fetching_link') !== requestId // Request has been cancelled
composeState.get('poll') ||
composeState.get('is_uploading') ||
composeState.get('id')
) )
return; return;
@@ -207,6 +255,17 @@ export const pasteLinkCompose = createDataLoadingThunk(
dispatch(quoteComposeById(data.statuses[0].id)); dispatch(quoteComposeById(data.statuses[0].id));
} }
}, },
{
useLoadingBar: false,
condition: (_, { getState }) =>
!getState().compose.get('fetching_link') &&
!composeStateForbidsLink(getState().compose),
},
);
// Ideally this would cancel the action and the HTTP request, but this is good enough
export const cancelPasteLinkCompose = createAction(
'compose/cancelPasteLinkCompose',
); );
export const quoteComposeCancel = createAction('compose/quoteComposeCancel'); export const quoteComposeCancel = createAction('compose/quoteComposeCancel');

View File

@@ -140,7 +140,10 @@ class ComposeForm extends ImmutablePureComponent {
return; return;
} }
this.props.onSubmit(missingAltTextModal && this.props.missingAltText && this.props.privacy !== 'direct'); this.props.onSubmit({
missingAltText: missingAltTextModal && this.props.missingAltText && this.props.privacy !== 'direct',
quoteToPrivate: this.props.quoteToPrivate,
});
if (e) { if (e) {
e.preventDefault(); e.preventDefault();

View File

@@ -0,0 +1,48 @@
import { useCallback } from 'react';
import type { FC } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { cancelPasteLinkCompose } from '@/mastodon/actions/compose_typed';
import { useAppDispatch } from '@/mastodon/store';
import CancelFillIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
import { DisplayName } from 'mastodon/components/display_name';
import { IconButton } from 'mastodon/components/icon_button';
import { Skeleton } from 'mastodon/components/skeleton';
const messages = defineMessages({
quote_cancel: { id: 'status.quote.cancel', defaultMessage: 'Cancel quote' },
});
export const QuotePlaceholder: FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const handleQuoteCancel = useCallback(() => {
dispatch(cancelPasteLinkCompose());
}, [dispatch]);
return (
<div className='status__quote'>
<div className='status'>
<div className='status__info'>
<div className='status__avatar'>
<Skeleton width='32px' height='32px' />
</div>
<div className='status__display-name'>
<DisplayName />
</div>
<IconButton
onClick={handleQuoteCancel}
className='status__quote-cancel'
title={intl.formatMessage(messages.quote_cancel)}
icon='cancel-fill'
iconComponent={CancelFillIcon}
/>
</div>
<div className='status__content'>
<Skeleton />
</div>
</div>
</div>
);
};

View File

@@ -7,11 +7,17 @@ import { quoteComposeCancel } from '@/mastodon/actions/compose_typed';
import { QuotedStatus } from '@/mastodon/components/status_quoted'; import { QuotedStatus } from '@/mastodon/components/status_quoted';
import { useAppDispatch, useAppSelector } from '@/mastodon/store'; import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { QuotePlaceholder } from './quote_placeholder';
export const ComposeQuotedStatus: FC = () => { export const ComposeQuotedStatus: FC = () => {
const quotedStatusId = useAppSelector( const quotedStatusId = useAppSelector(
(state) => state.compose.get('quoted_status_id') as string | null, (state) => state.compose.get('quoted_status_id') as string | null,
); );
const isFetchingLink = useAppSelector(
(state) => !!state.compose.get('fetching_link'),
);
const isEditing = useAppSelector((state) => !!state.compose.get('id')); const isEditing = useAppSelector((state) => !!state.compose.get('id'));
const quote = useMemo( const quote = useMemo(
@@ -30,7 +36,9 @@ export const ComposeQuotedStatus: FC = () => {
dispatch(quoteComposeCancel()); dispatch(quoteComposeCancel());
}, [dispatch]); }, [dispatch]);
if (!quote) { if (isFetchingLink && !quote) {
return <QuotePlaceholder />;
} else if (!quote) {
return null; return null;
} }

View File

@@ -5,8 +5,10 @@ import { defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { changeComposeVisibility } from '@/mastodon/actions/compose'; import {
import { setComposeQuotePolicy } from '@/mastodon/actions/compose_typed'; changeComposeVisibility,
setComposeQuotePolicy,
} from '@/mastodon/actions/compose_typed';
import { openModal } from '@/mastodon/actions/modal'; import { openModal } from '@/mastodon/actions/modal';
import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes'; import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
import type { StatusVisibility } from '@/mastodon/api_types/statuses'; import type { StatusVisibility } from '@/mastodon/api_types/statuses';

View File

@@ -12,6 +12,7 @@ import {
} from 'mastodon/actions/compose'; } from 'mastodon/actions/compose';
import { pasteLinkCompose } from 'mastodon/actions/compose_typed'; import { pasteLinkCompose } from 'mastodon/actions/compose_typed';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
import { PRIVATE_QUOTE_MODAL_ID } from 'mastodon/features/ui/components/confirmation_modals/private_quote_notify';
import ComposeForm from '../components/compose_form'; import ComposeForm from '../components/compose_form';
@@ -32,6 +33,10 @@ const mapStateToProps = state => ({
isUploading: state.getIn(['compose', 'is_uploading']), isUploading: state.getIn(['compose', 'is_uploading']),
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0, anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
missingAltText: state.getIn(['compose', 'media_attachments']).some(media => ['image', 'gifv'].includes(media.get('type')) && (media.get('description') ?? '').length === 0), missingAltText: state.getIn(['compose', 'media_attachments']).some(media => ['image', 'gifv'].includes(media.get('type')) && (media.get('description') ?? '').length === 0),
quoteToPrivate:
!!state.getIn(['compose', 'quoted_status_id'])
&& state.getIn(['compose', 'privacy']) === 'private'
&& !state.getIn(['settings', 'dismissed_banners', PRIVATE_QUOTE_MODAL_ID]),
isInReply: state.getIn(['compose', 'in_reply_to']) !== null, isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
lang: state.getIn(['compose', 'language']), lang: state.getIn(['compose', 'language']),
maxChars: state.getIn(['server', 'server', 'configuration', 'statuses', 'max_characters'], 500), maxChars: state.getIn(['server', 'server', 'configuration', 'statuses', 'max_characters'], 500),
@@ -43,12 +48,17 @@ const mapDispatchToProps = (dispatch, props) => ({
dispatch(changeCompose(text)); dispatch(changeCompose(text));
}, },
onSubmit (missingAltText) { onSubmit ({ missingAltText, quoteToPrivate }) {
if (missingAltText) { if (missingAltText) {
dispatch(openModal({ dispatch(openModal({
modalType: 'CONFIRM_MISSING_ALT_TEXT', modalType: 'CONFIRM_MISSING_ALT_TEXT',
modalProps: {}, modalProps: {},
})); }));
} else if (quoteToPrivate) {
dispatch(openModal({
modalType: 'CONFIRM_PRIVATE_QUOTE_NOTIFY',
modalProps: {},
}));
} else { } else {
dispatch(submitCompose((status) => { dispatch(submitCompose((status) => {
if (props.redirectOnSuccess) { if (props.redirectOnSuccess) {

View File

@@ -1,8 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { changeComposeVisibility } from '../../../actions/compose'; import { changeComposeVisibility } from '@/mastodon/actions/compose_typed';
import { openModal, closeModal } from '../../../actions/modal';
import { isUserTouching } from '../../../is_mobile';
import PrivacyDropdown from '../components/privacy_dropdown'; import PrivacyDropdown from '../components/privacy_dropdown';
const mapStateToProps = state => ({ const mapStateToProps = state => ({

View File

@@ -299,6 +299,12 @@ class Status extends ImmutablePureComponent {
dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId, onChange: handleChange } })); dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId, onChange: handleChange } }));
}; };
handleQuote = (status) => {
const { dispatch } = this.props;
dispatch(quoteComposeById(status.get('id')));
};
handleEditClick = (status) => { handleEditClick = (status) => {
const { dispatch, askReplyConfirmation } = this.props; const { dispatch, askReplyConfirmation } = this.props;
@@ -625,6 +631,7 @@ class Status extends ImmutablePureComponent {
onDelete={this.handleDeleteClick} onDelete={this.handleDeleteClick}
onRevokeQuote={this.handleRevokeQuoteClick} onRevokeQuote={this.handleRevokeQuoteClick}
onQuotePolicyChange={this.handleQuotePolicyChange} onQuotePolicyChange={this.handleQuotePolicyChange}
onQuote={this.handleQuote}
onEdit={this.handleEditClick} onEdit={this.handleEditClick}
onDirect={this.handleDirectClick} onDirect={this.handleDirectClick}
onMention={this.handleMentionClick} onMention={this.handleMentionClick}

View File

@@ -18,6 +18,7 @@ export const ConfirmationModal: React.FC<
onSecondary?: () => void; onSecondary?: () => void;
onConfirm: () => void; onConfirm: () => void;
closeWhenConfirm?: boolean; closeWhenConfirm?: boolean;
extraContent?: React.ReactNode;
} & BaseConfirmationModalProps } & BaseConfirmationModalProps
> = ({ > = ({
title, title,
@@ -29,6 +30,7 @@ export const ConfirmationModal: React.FC<
secondary, secondary,
onSecondary, onSecondary,
closeWhenConfirm = true, closeWhenConfirm = true,
extraContent,
}) => { }) => {
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
if (closeWhenConfirm) { if (closeWhenConfirm) {
@@ -49,6 +51,8 @@ export const ConfirmationModal: React.FC<
<div className='safety-action-modal__confirmation'> <div className='safety-action-modal__confirmation'>
<h1>{title}</h1> <h1>{title}</h1>
{message && <p>{message}</p>} {message && <p>{message}</p>}
{extraContent}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,88 @@
import { forwardRef, useCallback, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { submitCompose } from '@/mastodon/actions/compose';
import { changeSetting } from '@/mastodon/actions/settings';
import { CheckBox } from '@/mastodon/components/check_box';
import { useAppDispatch } from '@/mastodon/store';
import { ConfirmationModal } from './confirmation_modal';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import classes from './styles.module.css';
export const PRIVATE_QUOTE_MODAL_ID = 'quote/private_notify';
const messages = defineMessages({
title: {
id: 'confirmations.private_quote_notify.title',
defaultMessage: 'Share with followers and mentioned users?',
},
message: {
id: 'confirmations.private_quote_notify.message',
defaultMessage:
'The person you are quoting and other mentions ' +
"will be notified and will be able to view your post, even if they're not following you.",
},
confirm: {
id: 'confirmations.private_quote_notify.confirm',
defaultMessage: 'Publish post',
},
cancel: {
id: 'confirmations.private_quote_notify.cancel',
defaultMessage: 'Back to editing',
},
});
export const PrivateQuoteNotify = forwardRef<
HTMLDivElement,
BaseConfirmationModalProps
>(
(
{ onClose },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_ref,
) => {
const intl = useIntl();
const [dismiss, setDismissed] = useState(false);
const handleDismissToggle = useCallback(() => {
setDismissed((prev) => !prev);
}, []);
const dispatch = useAppDispatch();
const handleConfirm = useCallback(() => {
dispatch(submitCompose());
if (dismiss) {
dispatch(
changeSetting(['dismissed_banners', PRIVATE_QUOTE_MODAL_ID], true),
);
}
}, [dismiss, dispatch]);
return (
<ConfirmationModal
title={intl.formatMessage(messages.title)}
message={intl.formatMessage(messages.message)}
confirm={intl.formatMessage(messages.confirm)}
cancel={intl.formatMessage(messages.cancel)}
onConfirm={handleConfirm}
onClose={onClose}
extraContent={
<label className={classes.checkbox_wrapper}>
<CheckBox
value='hide'
checked={dismiss}
onChange={handleDismissToggle}
/>{' '}
<FormattedMessage
id='confirmations.private_quote_notify.do_not_show_again'
defaultMessage="Don't show me this message again"
/>
</label>
}
/>
);
},
);
PrivateQuoteNotify.displayName = 'PrivateQuoteNotify';

View File

@@ -0,0 +1,7 @@
.checkbox_wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 1rem 0;
cursor: pointer;
}

View File

@@ -47,6 +47,7 @@ import MediaModal from './media_modal';
import { ModalPlaceholder } from './modal_placeholder'; import { ModalPlaceholder } from './modal_placeholder';
import VideoModal from './video_modal'; import VideoModal from './video_modal';
import { VisibilityModal } from './visibility_modal'; import { VisibilityModal } from './visibility_modal';
import { PrivateQuoteNotify } from './confirmation_modals/private_quote_notify';
export const MODAL_COMPONENTS = { export const MODAL_COMPONENTS = {
'MEDIA': () => Promise.resolve({ default: MediaModal }), 'MEDIA': () => Promise.resolve({ default: MediaModal }),
@@ -66,6 +67,7 @@ export const MODAL_COMPONENTS = {
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }), 'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }), 'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),
'CONFIRM_MISSING_ALT_TEXT': () => Promise.resolve({ default: ConfirmMissingAltTextModal }), 'CONFIRM_MISSING_ALT_TEXT': () => Promise.resolve({ default: ConfirmMissingAltTextModal }),
'CONFIRM_PRIVATE_QUOTE_NOTIFY': () => Promise.resolve({ default: PrivateQuoteNotify }),
'CONFIRM_REVOKE_QUOTE': () => Promise.resolve({ default: ConfirmRevokeQuoteModal }), 'CONFIRM_REVOKE_QUOTE': () => Promise.resolve({ default: ConfirmRevokeQuoteModal }),
'CONFIRM_QUIET_QUOTE': () => Promise.resolve({ default: QuietPostQuoteInfoModal }), 'CONFIRM_QUIET_QUOTE': () => Promise.resolve({ default: QuietPostQuoteInfoModal }),
'MUTE': MuteModal, 'MUTE': MuteModal,

View File

@@ -128,9 +128,12 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
const disableVisibility = !!statusId; const disableVisibility = !!statusId;
const disableQuotePolicy = const disableQuotePolicy =
visibility === 'private' || visibility === 'direct'; visibility === 'private' || visibility === 'direct';
const disablePublicVisibilities: boolean = useAppSelector( const disablePublicVisibilities = useAppSelector(
selectDisablePublicVisibilities, selectDisablePublicVisibilities,
); );
const isQuotePost = useAppSelector(
(state) => state.compose.get('quoted_status_id') !== null,
);
const visibilityItems = useMemo<SelectItem<StatusVisibility>[]>(() => { const visibilityItems = useMemo<SelectItem<StatusVisibility>[]>(() => {
const items: SelectItem<StatusVisibility>[] = [ const items: SelectItem<StatusVisibility>[] = [
@@ -315,6 +318,21 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
id={quoteDescriptionId} id={quoteDescriptionId}
/> />
</div> </div>
{isQuotePost && visibility === 'direct' && (
<div className='visibility-modal__quote-warning'>
<FormattedMessage
id='visibility_modal.direct_quote_warning.title'
defaultMessage="Quotes can't be embedded in private mentions"
tagName='h3'
/>
<FormattedMessage
id='visibility_modal.direct_quote_warning.text'
defaultMessage='If you save the current settings, the embedded quote will be converted to a link.'
tagName='p'
/>
</div>
)}
</div> </div>
<div className='dialog-modal__content__actions'> <div className='dialog-modal__content__actions'>
<Button onClick={onClose} secondary> <Button onClick={onClose} secondary>

View File

@@ -35,7 +35,7 @@ interface InitialStateMeta {
streaming_api_base_url: string; streaming_api_base_url: string;
local_live_feed_access: 'public' | 'authenticated' | 'disabled'; local_live_feed_access: 'public' | 'authenticated' | 'disabled';
remote_live_feed_access: 'public' | 'authenticated' | 'disabled'; remote_live_feed_access: 'public' | 'authenticated' | 'disabled';
local_topic_feed_access: 'public' | 'authenticated' | 'disabled'; local_topic_feed_access: 'public' | 'authenticated';
remote_topic_feed_access: 'public' | 'authenticated' | 'disabled'; remote_topic_feed_access: 'public' | 'authenticated' | 'disabled';
title: string; title: string;
show_trends: boolean; show_trends: boolean;

View File

@@ -247,6 +247,11 @@
"confirmations.missing_alt_text.secondary": "Post anyway", "confirmations.missing_alt_text.secondary": "Post anyway",
"confirmations.missing_alt_text.title": "Add alt text?", "confirmations.missing_alt_text.title": "Add alt text?",
"confirmations.mute.confirm": "Mute", "confirmations.mute.confirm": "Mute",
"confirmations.private_quote_notify.cancel": "Back to editing",
"confirmations.private_quote_notify.confirm": "Publish post",
"confirmations.private_quote_notify.do_not_show_again": "Don't show me this message again",
"confirmations.private_quote_notify.message": "The person you are quoting and other mentions will be notified and will be able to view your post, even if they're not following you.",
"confirmations.private_quote_notify.title": "Share with followers and mentioned users?",
"confirmations.quiet_post_quote_info.dismiss": "Don't remind me again", "confirmations.quiet_post_quote_info.dismiss": "Don't remind me again",
"confirmations.quiet_post_quote_info.got_it": "Got it", "confirmations.quiet_post_quote_info.got_it": "Got it",
"confirmations.quiet_post_quote_info.message": "When quoting a quiet public post, your post will be hidden from trending timelines.", "confirmations.quiet_post_quote_info.message": "When quoting a quiet public post, your post will be hidden from trending timelines.",
@@ -759,6 +764,7 @@
"privacy_policy.title": "Privacy Policy", "privacy_policy.title": "Privacy Policy",
"quote_error.edit": "Quotes cannot be added when editing a post.", "quote_error.edit": "Quotes cannot be added when editing a post.",
"quote_error.poll": "Quoting is not allowed with polls.", "quote_error.poll": "Quoting is not allowed with polls.",
"quote_error.private_mentions": "Quoting is not allowed with direct mentions.",
"quote_error.quote": "Only one quote at a time is allowed.", "quote_error.quote": "Only one quote at a time is allowed.",
"quote_error.unauthorized": "You are not authorized to quote this post.", "quote_error.unauthorized": "You are not authorized to quote this post.",
"quote_error.upload": "Quoting is not allowed with media attachments.", "quote_error.upload": "Quoting is not allowed with media attachments.",
@@ -1012,6 +1018,8 @@
"video.volume_down": "Volume down", "video.volume_down": "Volume down",
"video.volume_up": "Volume up", "video.volume_up": "Volume up",
"visibility_modal.button_title": "Set visibility", "visibility_modal.button_title": "Set visibility",
"visibility_modal.direct_quote_warning.text": "If you save the current settings, the embedded quote will be converted to a link.",
"visibility_modal.direct_quote_warning.title": "Quotes can't be embedded in private mentions",
"visibility_modal.header": "Visibility and interaction", "visibility_modal.header": "Visibility and interaction",
"visibility_modal.helper.direct_quoting": "Private mentions authored on Mastodon can't be quoted by others.", "visibility_modal.helper.direct_quoting": "Private mentions authored on Mastodon can't be quoted by others.",
"visibility_modal.helper.privacy_editing": "Visibility can't be changed after a post is published.", "visibility_modal.helper.privacy_editing": "Visibility can't be changed after a post is published.",

View File

@@ -1,11 +1,14 @@
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import { import {
changeComposeVisibility,
changeUploadCompose, changeUploadCompose,
quoteCompose, quoteCompose,
quoteComposeCancel, quoteComposeCancel,
setComposeQuotePolicy, setComposeQuotePolicy,
} from 'mastodon/actions/compose_typed'; pasteLinkCompose,
cancelPasteLinkCompose,
} from '@/mastodon/actions/compose_typed';
import { timelineDelete } from 'mastodon/actions/timelines_typed'; import { timelineDelete } from 'mastodon/actions/timelines_typed';
import { import {
@@ -38,7 +41,6 @@ import {
COMPOSE_SENSITIVITY_CHANGE, COMPOSE_SENSITIVITY_CHANGE,
COMPOSE_SPOILERNESS_CHANGE, COMPOSE_SPOILERNESS_CHANGE,
COMPOSE_SPOILER_TEXT_CHANGE, COMPOSE_SPOILER_TEXT_CHANGE,
COMPOSE_VISIBILITY_CHANGE,
COMPOSE_LANGUAGE_CHANGE, COMPOSE_LANGUAGE_CHANGE,
COMPOSE_COMPOSING_CHANGE, COMPOSE_COMPOSING_CHANGE,
COMPOSE_EMOJI_INSERT, COMPOSE_EMOJI_INSERT,
@@ -93,6 +95,7 @@ const initialState = ImmutableMap({
quoted_status_id: null, quoted_status_id: null,
quote_policy: 'public', quote_policy: 'public',
default_quote_policy: 'public', // Set in hydration. default_quote_policy: 'public', // Set in hydration.
fetching_link: null,
}); });
const initialPoll = ImmutableMap({ const initialPoll = ImmutableMap({
@@ -315,7 +318,11 @@ const calculateProgress = (loaded, total) => Math.min(Math.round((loaded / total
/** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */ /** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */
export const composeReducer = (state = initialState, action) => { export const composeReducer = (state = initialState, action) => {
if (changeUploadCompose.fulfilled.match(action)) { if (changeComposeVisibility.match(action)) {
return state
.set('privacy', action.payload)
.set('idempotencyKey', uuid());
} else if (changeUploadCompose.fulfilled.match(action)) {
return state return state
.set('is_changing_upload', false) .set('is_changing_upload', false)
.update('media_attachments', list => list.map(item => { .update('media_attachments', list => list.map(item => {
@@ -331,15 +338,27 @@ export const composeReducer = (state = initialState, action) => {
return state.set('is_changing_upload', false); return state.set('is_changing_upload', false);
} else if (quoteCompose.match(action)) { } else if (quoteCompose.match(action)) {
const status = action.payload; const status = action.payload;
const isDirect = state.get('privacy') === 'direct';
return state return state
.set('quoted_status_id', status.get('id')) .set('quoted_status_id', isDirect ? null : status.get('id'))
.set('spoiler', status.get('sensitive')) .set('spoiler', status.get('sensitive'))
.set('spoiler_text', status.get('spoiler_text')) .set('spoiler_text', status.get('spoiler_text'))
.update('privacy', (visibility) => ['public', 'unlisted'].includes(visibility) && status.get('visibility') === 'private' ? 'private' : visibility); .update('privacy', (visibility) => {
if (['public', 'unlisted'].includes(visibility) && status.get('visibility') === 'private') {
return 'private';
}
return visibility;
});
} else if (quoteComposeCancel.match(action)) { } else if (quoteComposeCancel.match(action)) {
return state.set('quoted_status_id', null); return state.set('quoted_status_id', null);
} else if (setComposeQuotePolicy.match(action)) { } else if (setComposeQuotePolicy.match(action)) {
return state.set('quote_policy', action.payload); return state.set('quote_policy', action.payload);
} else if (pasteLinkCompose.pending.match(action)) {
return state.set('fetching_link', action.meta.requestId);
} else if (pasteLinkCompose.fulfilled.match(action) || pasteLinkCompose.rejected.match(action)) {
return action.meta.requestId === state.get('fetching_link') ? state.set('fetching_link', null) : state;
} else if (cancelPasteLinkCompose.match(action)) {
return state.set('fetching_link', null);
} }
switch(action.type) { switch(action.type) {
@@ -383,10 +402,6 @@ export const composeReducer = (state = initialState, action) => {
return state return state
.set('spoiler_text', action.text) .set('spoiler_text', action.text)
.set('idempotencyKey', uuid()); .set('idempotencyKey', uuid());
case COMPOSE_VISIBILITY_CHANGE:
return state
.set('privacy', action.value)
.set('idempotencyKey', uuid());
case COMPOSE_CHANGE: case COMPOSE_CHANGE:
return state return state
.set('text', action.text) .set('text', action.text)

View File

@@ -42,7 +42,7 @@ interface AppThunkConfig {
} }
export type AppThunkApi = Pick< export type AppThunkApi = Pick<
GetThunkAPI<AppThunkConfig>, GetThunkAPI<AppThunkConfig>,
'getState' | 'dispatch' 'getState' | 'dispatch' | 'requestId'
>; >;
interface AppThunkOptions<Arg> { interface AppThunkOptions<Arg> {
@@ -60,7 +60,7 @@ type AppThunk<Arg = void, Returned = void> = (
type AppThunkCreator<Arg = void, Returned = void, ExtraArg = unknown> = ( type AppThunkCreator<Arg = void, Returned = void, ExtraArg = unknown> = (
arg: Arg, arg: Arg,
api: AppThunkApi, api: Pick<AppThunkApi, 'getState' | 'dispatch'>,
extra?: ExtraArg, extra?: ExtraArg,
) => Returned; ) => Returned;
@@ -143,10 +143,10 @@ export function createAsyncThunk<Arg = void, Returned = void>(
name, name,
async ( async (
arg: Arg, arg: Arg,
{ getState, dispatch, fulfillWithValue, rejectWithValue }, { getState, dispatch, requestId, fulfillWithValue, rejectWithValue },
) => { ) => {
try { try {
const result = await creator(arg, { dispatch, getState }); const result = await creator(arg, { dispatch, getState, requestId });
return fulfillWithValue(result, { return fulfillWithValue(result, {
useLoadingBar: options.useLoadingBar, useLoadingBar: options.useLoadingBar,
@@ -280,10 +280,11 @@ export function createDataLoadingThunk<
return createAsyncThunk<Args, Returned>( return createAsyncThunk<Args, Returned>(
name, name,
async (arg, { getState, dispatch }) => { async (arg, { getState, dispatch, requestId }) => {
const data = await loadData(arg, { const data = await loadData(arg, {
dispatch, dispatch,
getState, getState,
requestId,
}); });
if (!onData) return data as Returned; if (!onData) return data as Returned;
@@ -291,6 +292,7 @@ export function createDataLoadingThunk<
const result = await onData(data, { const result = await onData(data, {
dispatch, dispatch,
getState, getState,
requestId,
discardLoadData: discardLoadDataInPayload, discardLoadData: discardLoadDataInPayload,
actionArg: arg, actionArg: arg,
}); });

View File

@@ -1325,6 +1325,10 @@ a.sparkline {
line-height: 1; line-height: 1;
width: 100%; width: 100%;
animation: skeleton 1.2s ease-in-out infinite; animation: skeleton 1.2s ease-in-out infinite;
.reduce-motion & {
animation: none;
}
} }
@keyframes skeleton { @keyframes skeleton {

View File

@@ -5743,6 +5743,34 @@ a.status-card {
} }
} }
.visibility-modal {
&__quote-warning {
color: var(--nested-card-text);
background:
/* This is a bit of a silly hack for layering two background colours
* since --nested-card-background is too transparent for a tooltip */
linear-gradient(
var(--nested-card-background),
var(--nested-card-background)
),
linear-gradient(var(--background-color), var(--background-color));
border: var(--nested-card-border);
padding: 16px;
border-radius: 4px;
h3 {
font-weight: 500;
margin-bottom: 4px;
color: $darker-text-color;
}
p {
font-size: 0.8em;
color: $dark-text-color;
}
}
}
.visibility-dropdown { .visibility-dropdown {
&__overlay[data-popper-placement] { &__overlay[data-popper-placement] {
z-index: 9999; z-index: 9999;

View File

@@ -102,6 +102,7 @@ class Form::AdminSettings
DOMAIN_BLOCK_AUDIENCES = %w(disabled users all).freeze DOMAIN_BLOCK_AUDIENCES = %w(disabled users all).freeze
REGISTRATION_MODES = %w(open approved none).freeze REGISTRATION_MODES = %w(open approved none).freeze
FEED_ACCESS_MODES = %w(public authenticated disabled).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 about local_feed).freeze
attr_accessor(*KEYS) attr_accessor(*KEYS)
@@ -114,7 +115,7 @@ class Form::AdminSettings
validates :show_domain_blocks_rationale, inclusion: { in: DOMAIN_BLOCK_AUDIENCES }, if: -> { defined?(@show_domain_blocks_rationale) } validates :show_domain_blocks_rationale, inclusion: { in: DOMAIN_BLOCK_AUDIENCES }, if: -> { defined?(@show_domain_blocks_rationale) }
validates :local_live_feed_access, inclusion: { in: FEED_ACCESS_MODES }, if: -> { defined?(@local_live_feed_access) } validates :local_live_feed_access, inclusion: { in: FEED_ACCESS_MODES }, if: -> { defined?(@local_live_feed_access) }
validates :remote_live_feed_access, inclusion: { in: FEED_ACCESS_MODES }, if: -> { defined?(@remote_live_feed_access) } validates :remote_live_feed_access, inclusion: { in: FEED_ACCESS_MODES }, if: -> { defined?(@remote_live_feed_access) }
validates :local_topic_feed_access, inclusion: { in: FEED_ACCESS_MODES }, if: -> { defined?(@local_topic_feed_access) } validates :local_topic_feed_access, inclusion: { in: ALTERNATE_FEED_ACCESS_MODES }, if: -> { defined?(@local_topic_feed_access) }
validates :remote_topic_feed_access, inclusion: { in: FEED_ACCESS_MODES }, if: -> { defined?(@remote_topic_feed_access) } validates :remote_topic_feed_access, inclusion: { in: FEED_ACCESS_MODES }, if: -> { defined?(@remote_topic_feed_access) }
validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) } validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) }
validates :min_age, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@min_age) } validates :min_age, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@min_age) }

View File

@@ -96,6 +96,7 @@ class PostStatusService < BaseService
@status = @account.statuses.new(status_attributes) @status = @account.statuses.new(status_attributes)
process_mentions_service.call(@status, save_records: false) process_mentions_service.call(@status, save_records: false)
safeguard_mentions!(@status) safeguard_mentions!(@status)
safeguard_private_mention_quote!(@status)
attach_quote!(@status) attach_quote!(@status)
antispam = Antispam.new(@status) antispam = Antispam.new(@status)
@@ -108,6 +109,16 @@ class PostStatusService < BaseService
end end
end end
def safeguard_private_mention_quote!(status)
return if @quoted_status.nil? || @visibility.to_sym != :direct
# The mentions array test here is awkward because the relationship is not persisted at this time
return if @quoted_status.account_id == @account.id || status.mentions.to_a.any? { |mention| mention.account_id == @quoted_status.account_id && !mention.silent }
status.errors.add(:base, I18n.t('statuses.errors.quoted_user_not_mentioned'))
raise ActiveRecord::RecordInvalid, status
end
def attach_quote!(status) def attach_quote!(status)
return if @quoted_status.nil? return if @quoted_status.nil?
@@ -130,6 +141,7 @@ class PostStatusService < BaseService
def schedule_status! def schedule_status!
status_for_validation = @account.statuses.build(status_attributes) status_for_validation = @account.statuses.build(status_attributes)
safeguard_private_mention_quote!(status_for_validation)
antispam = Antispam.new(status_for_validation) antispam = Antispam.new(status_for_validation)
antispam.local_preflight_check! antispam.local_preflight_check!

View File

@@ -46,7 +46,7 @@
.fields-row .fields-row
.fields-row__column.fields-row__column-6.fields-group .fields-row__column.fields-row__column-6.fields-group
= f.input :local_topic_feed_access, = f.input :local_topic_feed_access,
collection: f.object.class::FEED_ACCESS_MODES, collection: f.object.class::ALTERNATE_FEED_ACCESS_MODES,
include_blank: false, include_blank: false,
label_method: ->(mode) { I18n.t("admin.settings.feed_access.modes.#{mode}") }, label_method: ->(mode) { I18n.t("admin.settings.feed_access.modes.#{mode}") },
wrapper: :with_label wrapper: :with_label

View File

@@ -2,8 +2,7 @@
= t('privacy.title') = t('privacy.title')
- content_for :heading do - content_for :heading do
%h2= t('settings.profile') %h2= t('privacy.title')
= render partial: 'settings/shared/profile_navigation'
= simple_form_for @account, url: settings_privacy_path do |f| = simple_form_for @account, url: settings_privacy_path do |f|
= render 'shared/error_messages', object: @account = render 'shared/error_messages', object: @account

View File

@@ -2,6 +2,5 @@
= render_navigation renderer: :links do |primary| = render_navigation renderer: :links do |primary|
:ruby :ruby
primary.item :edit_profile, safe_join([material_symbol('person'), t('settings.edit_profile')]), settings_profile_path primary.item :edit_profile, safe_join([material_symbol('person'), t('settings.edit_profile')]), settings_profile_path
primary.item :privacy_reach, safe_join([material_symbol('lock'), t('privacy.title')]), settings_privacy_path
primary.item :verification, safe_join([material_symbol('check'), t('verification.verification')]), settings_verification_path primary.item :verification, safe_join([material_symbol('check'), t('verification.verification')]), settings_verification_path
primary.item :featured_tags, safe_join([material_symbol('tag'), t('settings.featured_tags')]), settings_featured_tags_path primary.item :featured_tags, safe_join([material_symbol('tag'), t('settings.featured_tags')]), settings_featured_tags_path

View File

@@ -5,7 +5,7 @@ class ActivityPub::RefetchAndVerifyQuoteWorker
include ExponentialBackoff include ExponentialBackoff
include JsonLdHelper include JsonLdHelper
sidekiq_options queue: 'pull', retry: 3 sidekiq_options queue: 'pull', retry: 5
def perform(quote_id, quoted_uri, options = {}) def perform(quote_id, quoted_uri, options = {})
quote = Quote.find(quote_id) quote = Quote.find(quote_id)

View File

@@ -1930,6 +1930,7 @@ en:
errors: errors:
in_reply_not_found: The post you are trying to reply to does not appear to exist. in_reply_not_found: The post you are trying to reply to does not appear to exist.
quoted_status_not_found: The post you are trying to quote does not appear to exist. quoted_status_not_found: The post you are trying to quote does not appear to exist.
quoted_user_not_mentioned: Cannot quote a non-mentioned user in a Private Mention post.
over_character_limit: character limit of %{max} exceeded over_character_limit: character limit of %{max} exceeded
pin_errors: pin_errors:
direct: Posts that are only visible to mentioned users cannot be pinned direct: Posts that are only visible to mentioned users cannot be pinned

View File

@@ -12,7 +12,8 @@ SimpleNavigation::Configuration.run do |navigation|
if: -> { Rails.configuration.x.mastodon.software_update_url.present? && current_user.can?(:view_devops) && SoftwareUpdate.urgent_pending? }, if: -> { Rails.configuration.x.mastodon.software_update_url.present? && current_user.can?(:view_devops) && SoftwareUpdate.urgent_pending? },
html: { class: 'warning' } html: { class: 'warning' }
n.item :profile, safe_join([material_symbol('person'), t('settings.profile')]), settings_profile_path, if: -> { current_user.functional? && !self_destruct }, highlights_on: %r{/settings/profile|/settings/featured_tags|/settings/verification|/settings/privacy} n.item :profile, safe_join([material_symbol('person'), t('settings.profile')]), settings_profile_path, if: -> { current_user.functional? && !self_destruct }, highlights_on: %r{/settings/profile|/settings/featured_tags|/settings/verification}
n.item :privacy, safe_join([material_symbol('globe'), t('privacy.title')]), settings_privacy_path, if: -> { current_user.functional? && !self_destruct }, highlights_on: %r{/settings/privacy}
n.item :preferences, safe_join([material_symbol('settings'), t('settings.preferences')]), settings_preferences_path, if: -> { current_user.functional? && !self_destruct } do |s| n.item :preferences, safe_join([material_symbol('settings'), t('settings.preferences')]), settings_preferences_path, if: -> { current_user.functional? && !self_destruct } do |s|
s.item :appearance, safe_join([material_symbol('computer'), t('settings.appearance')]), settings_preferences_appearance_path s.item :appearance, safe_join([material_symbol('computer'), t('settings.appearance')]), settings_preferences_appearance_path

View File

@@ -17,7 +17,7 @@ module Mastodon
end end
def default_prerelease def default_prerelease
'rc.2' 'rc.3'
end end
def prerelease def prerelease

View File

@@ -48,7 +48,7 @@
"@gamestdio/websocket": "^0.3.2", "@gamestdio/websocket": "^0.3.2",
"@github/webauthn-json": "^2.1.1", "@github/webauthn-json": "^2.1.1",
"@optimize-lodash/rollup-plugin": "^5.0.2", "@optimize-lodash/rollup-plugin": "^5.0.2",
"@rails/ujs": "7.1.502", "@rails/ujs": "7.1.600",
"@react-spring/web": "^9.7.5", "@react-spring/web": "^9.7.5",
"@reduxjs/toolkit": "^2.0.1", "@reduxjs/toolkit": "^2.0.1",
"@use-gesture/react": "^10.3.1", "@use-gesture/react": "^10.3.1",
@@ -194,6 +194,7 @@
"stylelint-config-standard-scss": "^16.0.0", "stylelint-config-standard-scss": "^16.0.0",
"typescript": "~5.9.0", "typescript": "~5.9.0",
"typescript-eslint": "^8.45.0", "typescript-eslint": "^8.45.0",
"typescript-plugin-css-modules": "^5.2.0",
"vitest": "^3.2.4" "vitest": "^3.2.4"
}, },
"resolutions": { "resolutions": {

View File

@@ -228,7 +228,7 @@ RSpec.describe '/api/v1/statuses' do
end end
context 'with a self-quote post' do context 'with a self-quote post' do
let(:quoted_status) { Fabricate(:status, account: user.account) } let!(:quoted_status) { Fabricate(:status, account: user.account) }
let(:params) do let(:params) do
{ {
status: 'Hello world, this is a self-quote', status: 'Hello world, this is a self-quote',
@@ -237,7 +237,48 @@ RSpec.describe '/api/v1/statuses' do
end end
it 'returns a quote post, as well as rate limit headers', :aggregate_failures do it 'returns a quote post, as well as rate limit headers', :aggregate_failures do
subject expect { subject }.to change(user.account.statuses, :count).by(1)
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
expect(response.parsed_body[:quote]).to be_present
expect(response.headers['X-RateLimit-Limit']).to eq RateLimiter::FAMILIES[:statuses][:limit].to_s
expect(response.headers['X-RateLimit-Remaining']).to eq (RateLimiter::FAMILIES[:statuses][:limit] - 1).to_s
end
end
context 'with a quote to a non-mentioned user in a Private Mention' do
let!(:quoted_status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) }
let(:params) do
{
status: 'Hello, this is a quote',
quoted_status_id: quoted_status.id,
visibility: :direct,
}
end
it 'returns an error and does not create a post', :aggregate_failures do
expect { subject }.to_not change(user.account.statuses, :count)
expect(response).to have_http_status(422)
expect(response.content_type)
.to start_with('application/json')
end
end
context 'with a quote to a mentioned user in a Private Mention' do
let!(:quoted_status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) }
let(:params) do
{
status: "Hello @#{quoted_status.account.acct}, this is a quote",
quoted_status_id: quoted_status.id,
visibility: :direct,
}
end
it 'returns a quote post, as well as rate limit headers', :aggregate_failures do
expect { subject }.to change(user.account.statuses, :count).by(1)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response.content_type) expect(response.content_type)

View File

@@ -26,7 +26,8 @@
"flavours/glitch/*": ["app/javascript/flavours/glitch/*"], "flavours/glitch/*": ["app/javascript/flavours/glitch/*"],
"images/*": ["app/javascript/images/*"], "images/*": ["app/javascript/images/*"],
"styles/*": ["app/javascript/styles/*"] "styles/*": ["app/javascript/styles/*"]
} },
"plugins": [{ "name": "typescript-plugin-css-modules" }]
}, },
"include": [ "include": [
"vite.config.mts", "vite.config.mts",

View File

@@ -27,6 +27,8 @@ import { MastodonAssetsManifest } from './config/vite/plugin-assets-manifest';
const jsRoot = path.resolve(__dirname, 'app/javascript'); const jsRoot = path.resolve(__dirname, 'app/javascript');
const cssAliasClasses: ReadonlyArray<string> = ['components', 'features'];
export const config: UserConfigFnPromise = async ({ mode, command }) => { export const config: UserConfigFnPromise = async ({ mode, command }) => {
const isProdBuild = mode === 'production' && command === 'build'; const isProdBuild = mode === 'production' && command === 'build';
@@ -49,6 +51,45 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => {
}, },
}, },
css: { css: {
modules: {
generateScopedName(name, filename) {
let prefix = '';
// Use the top two segments of the path as the prefix.
const [parentDirName, dirName] = path
.dirname(filename)
.split(path.sep)
.slice(-2)
.map((dir) => dir.toLowerCase());
// If the parent directory is in the cssAliasClasses list, use
// the first four letters of it as the prefix, otherwise use the full name.
if (parentDirName) {
if (cssAliasClasses.includes(parentDirName)) {
prefix = parentDirName.slice(0, 4);
} else {
prefix = parentDirName;
}
}
// If we have a directory name, append it to the prefix.
if (dirName) {
prefix = `${prefix}_${dirName}`;
}
// If the file is not styles.module.scss or style.module.scss,
// append the file base name to the prefix.
const baseName = path.basename(
filename,
`.module${path.extname(filename)}`,
);
if (baseName !== 'styles' && baseName !== 'style') {
prefix = `${prefix}_${baseName}`;
}
return `_${prefix}__${name}`;
},
},
postcss: { postcss: {
plugins: [ plugins: [
postcssPresetEnv({ postcssPresetEnv({

332
yarn.lock
View File

@@ -19,6 +19,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@adobe/css-tools@npm:~4.3.1":
version: 4.3.3
resolution: "@adobe/css-tools@npm:4.3.3"
checksum: 10c0/e76e712df713964b87cdf2aca1f0477f19bebd845484d5fcba726d3ec7782366e2f26ec8cb2dcfaf47081a5c891987d8a9f5c3f30d11e1eb3c1848adc27fcb24
languageName: node
linkType: hard
"@ampproject/remapping@npm:^2.3.0": "@ampproject/remapping@npm:^2.3.0":
version: 2.3.0 version: 2.3.0
resolution: "@ampproject/remapping@npm:2.3.0" resolution: "@ampproject/remapping@npm:2.3.0"
@@ -2795,7 +2802,7 @@ __metadata:
"@gamestdio/websocket": "npm:^0.3.2" "@gamestdio/websocket": "npm:^0.3.2"
"@github/webauthn-json": "npm:^2.1.1" "@github/webauthn-json": "npm:^2.1.1"
"@optimize-lodash/rollup-plugin": "npm:^5.0.2" "@optimize-lodash/rollup-plugin": "npm:^5.0.2"
"@rails/ujs": "npm:7.1.502" "@rails/ujs": "npm:7.1.600"
"@react-spring/web": "npm:^9.7.5" "@react-spring/web": "npm:^9.7.5"
"@reduxjs/toolkit": "npm:^2.0.1" "@reduxjs/toolkit": "npm:^2.0.1"
"@storybook/addon-a11y": "npm:^9.1.1" "@storybook/addon-a11y": "npm:^9.1.1"
@@ -2925,6 +2932,7 @@ __metadata:
twitter-text: "npm:3.1.0" twitter-text: "npm:3.1.0"
typescript: "npm:~5.9.0" typescript: "npm:~5.9.0"
typescript-eslint: "npm:^8.45.0" typescript-eslint: "npm:^8.45.0"
typescript-plugin-css-modules: "npm:^5.2.0"
use-debounce: "npm:^10.0.0" use-debounce: "npm:^10.0.0"
vite: "npm:^7.1.1" vite: "npm:^7.1.1"
vite-plugin-manifest-sri: "npm:^0.2.0" vite-plugin-manifest-sri: "npm:^0.2.0"
@@ -3294,10 +3302,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@rails/ujs@npm:7.1.502": "@rails/ujs@npm:7.1.600":
version: 7.1.502 version: 7.1.600
resolution: "@rails/ujs@npm:7.1.502" resolution: "@rails/ujs@npm:7.1.600"
checksum: 10c0/79b46e8abd03e3fc633d93cc4e4c23838c628b775802fb38c2ce68b0e609ce287a67b81db112a93cc78c07ec82ca3b4cf87e74eb556d35148ce5f64c8be9201f checksum: 10c0/0ccaa68a08fbc7b084ab89a1fe49520a5cba6d99f4b0feaf0cb3d00334c59d8d798932d7e49b84aa388875d039ea1e17eb115ed96a80ad157e408a13eceef53e
languageName: node languageName: node
linkType: hard linkType: hard
@@ -4346,6 +4354,24 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/postcss-modules-local-by-default@npm:^4.0.2":
version: 4.0.2
resolution: "@types/postcss-modules-local-by-default@npm:4.0.2"
dependencies:
postcss: "npm:^8.0.0"
checksum: 10c0/af13e40673abf96f1427c467bc9d96986fc0fb702f65ef702de05f70e51af2212bc0bdf177bfd817e418f2238bf210fdee3541dd2d939fde9a4df5f8972ad716
languageName: node
linkType: hard
"@types/postcss-modules-scope@npm:^3.0.4":
version: 3.0.4
resolution: "@types/postcss-modules-scope@npm:3.0.4"
dependencies:
postcss: "npm:^8.0.0"
checksum: 10c0/6364674e429143fd686e0238d071377cf9ae1780a77f99e99292a06adc93057609146aaf55c09310ae99526c37e56be5a8a843086c0ff95513bd34c6bc4c7480
languageName: node
linkType: hard
"@types/prop-types@npm:*, @types/prop-types@npm:^15.7.5": "@types/prop-types@npm:*, @types/prop-types@npm:^15.7.5":
version: 15.7.15 version: 15.7.15
resolution: "@types/prop-types@npm:15.7.15" resolution: "@types/prop-types@npm:15.7.15"
@@ -6116,6 +6142,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"copy-anything@npm:^2.0.1":
version: 2.0.6
resolution: "copy-anything@npm:2.0.6"
dependencies:
is-what: "npm:^3.14.1"
checksum: 10c0/2702998a8cc015f9917385b7f16b0d85f1f6e5e2fd34d99f14df584838f492f49aa0c390d973684c687e895c5c58d08b308a0400ac3e1e3d6fa1e5884a5402ad
languageName: node
linkType: hard
"core-js-compat@npm:^3.43.0": "core-js-compat@npm:^3.43.0":
version: 3.44.0 version: 3.44.0
resolution: "core-js-compat@npm:3.44.0" resolution: "core-js-compat@npm:3.44.0"
@@ -6590,6 +6625,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"dotenv@npm:^16.4.2":
version: 16.6.1
resolution: "dotenv@npm:16.6.1"
checksum: 10c0/15ce56608326ea0d1d9414a5c8ee6dcf0fffc79d2c16422b4ac2268e7e2d76ff5a572d37ffe747c377de12005f14b3cc22361e79fc7f1061cce81f77d2c973dc
languageName: node
linkType: hard
"dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1": "dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1":
version: 1.0.1 version: 1.0.1
resolution: "dunder-proto@npm:1.0.1" resolution: "dunder-proto@npm:1.0.1"
@@ -6757,6 +6799,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"errno@npm:^0.1.1":
version: 0.1.8
resolution: "errno@npm:0.1.8"
dependencies:
prr: "npm:~1.0.1"
bin:
errno: cli.js
checksum: 10c0/83758951967ec57bf00b5f5b7dc797e6d65a6171e57ea57adcf1bd1a0b477fd9b5b35fae5be1ff18f4090ed156bce1db749fe7e317aac19d485a5d150f6a4936
languageName: node
linkType: hard
"error-ex@npm:^1.3.1": "error-ex@npm:^1.3.1":
version: 1.3.2 version: 1.3.2
resolution: "error-ex@npm:1.3.2" resolution: "error-ex@npm:1.3.2"
@@ -8053,7 +8106,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6": "graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6":
version: 4.2.11 version: 4.2.11
resolution: "graceful-fs@npm:4.2.11" resolution: "graceful-fs@npm:4.2.11"
checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2
@@ -8270,7 +8323,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2": "iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3":
version: 0.6.3 version: 0.6.3
resolution: "iconv-lite@npm:0.6.3" resolution: "iconv-lite@npm:0.6.3"
dependencies: dependencies:
@@ -8279,6 +8332,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"icss-utils@npm:^5.0.0, icss-utils@npm:^5.1.0":
version: 5.1.0
resolution: "icss-utils@npm:5.1.0"
peerDependencies:
postcss: ^8.1.0
checksum: 10c0/39c92936fabd23169c8611d2b5cc39e39d10b19b0d223352f20a7579f75b39d5f786114a6b8fc62bee8c5fed59ba9e0d38f7219a4db383e324fb3061664b043d
languageName: node
linkType: hard
"idb-keyval@npm:^6.2.0": "idb-keyval@npm:^6.2.0":
version: 6.2.1 version: 6.2.1
resolution: "idb-keyval@npm:6.2.1" resolution: "idb-keyval@npm:6.2.1"
@@ -8314,6 +8376,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"image-size@npm:~0.5.0":
version: 0.5.5
resolution: "image-size@npm:0.5.5"
bin:
image-size: bin/image-size.js
checksum: 10c0/655204163af06732f483a9fe7cce9dff4a29b7b2e88f5c957a5852e8143fa750f5e54b1955a2ca83de99c5220dbd680002d0d4e09140b01433520f4d5a0b1f4c
languageName: node
linkType: hard
"immer@npm:^10.0.3": "immer@npm:^10.0.3":
version: 10.0.3 version: 10.0.3
resolution: "immer@npm:10.0.3" resolution: "immer@npm:10.0.3"
@@ -8791,6 +8862,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-what@npm:^3.14.1":
version: 3.14.1
resolution: "is-what@npm:3.14.1"
checksum: 10c0/4b770b85454c877b6929a84fd47c318e1f8c2ff70fd72fd625bc3fde8e0c18a6e57345b6e7aa1ee9fbd1c608d27cfe885df473036c5c2e40cd2187250804a2c7
languageName: node
linkType: hard
"is-wsl@npm:^2.2.0": "is-wsl@npm:^2.2.0":
version: 2.2.0 version: 2.2.0
resolution: "is-wsl@npm:2.2.0" resolution: "is-wsl@npm:2.2.0"
@@ -9181,6 +9259,41 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"less@npm:^4.2.0":
version: 4.4.2
resolution: "less@npm:4.4.2"
dependencies:
copy-anything: "npm:^2.0.1"
errno: "npm:^0.1.1"
graceful-fs: "npm:^4.1.2"
image-size: "npm:~0.5.0"
make-dir: "npm:^2.1.0"
mime: "npm:^1.4.1"
needle: "npm:^3.1.0"
parse-node-version: "npm:^1.0.1"
source-map: "npm:~0.6.0"
tslib: "npm:^2.3.0"
dependenciesMeta:
errno:
optional: true
graceful-fs:
optional: true
image-size:
optional: true
make-dir:
optional: true
mime:
optional: true
needle:
optional: true
source-map:
optional: true
bin:
lessc: bin/lessc
checksum: 10c0/f8b796e45ef171adc390b5250f3018922cd046c256181dd9d4cbcbbdc5d6de7cb88c8327741c10eff7ff76421cd826fd95a664ea1b88fbf6f31742428d4a2dab
languageName: node
linkType: hard
"leven@npm:^3.1.0": "leven@npm:^3.1.0":
version: 3.1.0 version: 3.1.0
resolution: "leven@npm:3.1.0" resolution: "leven@npm:3.1.0"
@@ -9198,6 +9311,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"lilconfig@npm:^2.0.5":
version: 2.1.0
resolution: "lilconfig@npm:2.1.0"
checksum: 10c0/64645641aa8d274c99338e130554abd6a0190533c0d9eb2ce7ebfaf2e05c7d9961f3ffe2bfa39efd3b60c521ba3dd24fa236fe2775fc38501bf82bf49d4678b8
languageName: node
linkType: hard
"lines-and-columns@npm:^1.1.6": "lines-and-columns@npm:^1.1.6":
version: 1.2.4 version: 1.2.4
resolution: "lines-and-columns@npm:1.2.4" resolution: "lines-and-columns@npm:1.2.4"
@@ -9254,6 +9374,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"lodash.camelcase@npm:^4.3.0":
version: 4.3.0
resolution: "lodash.camelcase@npm:4.3.0"
checksum: 10c0/fcba15d21a458076dd309fce6b1b4bf611d84a0ec252cb92447c948c533ac250b95d2e00955801ebc367e5af5ed288b996d75d37d2035260a937008e14eaf432
languageName: node
linkType: hard
"lodash.debounce@npm:^4.0.8": "lodash.debounce@npm:^4.0.8":
version: 4.0.8 version: 4.0.8
resolution: "lodash.debounce@npm:4.0.8" resolution: "lodash.debounce@npm:4.0.8"
@@ -9404,6 +9531,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"make-dir@npm:^2.1.0":
version: 2.1.0
resolution: "make-dir@npm:2.1.0"
dependencies:
pify: "npm:^4.0.1"
semver: "npm:^5.6.0"
checksum: 10c0/ada869944d866229819735bee5548944caef560d7a8536ecbc6536edca28c72add47cc4f6fc39c54fb25d06b58da1f8994cf7d9df7dadea047064749efc085d8
languageName: node
linkType: hard
"make-dir@npm:^4.0.0": "make-dir@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "make-dir@npm:4.0.0" resolution: "make-dir@npm:4.0.0"
@@ -9535,7 +9672,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"mime@npm:1.6.0": "mime@npm:1.6.0, mime@npm:^1.4.1":
version: 1.6.0 version: 1.6.0
resolution: "mime@npm:1.6.0" resolution: "mime@npm:1.6.0"
bin: bin:
@@ -9782,6 +9919,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"needle@npm:^3.1.0":
version: 3.3.1
resolution: "needle@npm:3.3.1"
dependencies:
iconv-lite: "npm:^0.6.3"
sax: "npm:^1.2.4"
bin:
needle: bin/needle
checksum: 10c0/233b9315d47b735867d03e7a018fb665ee6cacf3a83b991b19538019cf42b538a3e85ca745c840b4c5e9a0ffdca76472f941363bf7c166214ae8cbc650fd4d39
languageName: node
linkType: hard
"negotiator@npm:0.6.3": "negotiator@npm:0.6.3":
version: 0.6.3 version: 0.6.3
resolution: "negotiator@npm:0.6.3" resolution: "negotiator@npm:0.6.3"
@@ -10156,6 +10305,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"parse-node-version@npm:^1.0.1":
version: 1.0.1
resolution: "parse-node-version@npm:1.0.1"
checksum: 10c0/999cd3d7da1425c2e182dce82b226c6dc842562d3ed79ec47f5c719c32a7f6c1a5352495b894fc25df164be7f2ede4224758255da9902ddef81f2b77ba46bb2c
languageName: node
linkType: hard
"parse-statements@npm:1.0.11": "parse-statements@npm:1.0.11":
version: 1.0.11 version: 1.0.11
resolution: "parse-statements@npm:1.0.11" resolution: "parse-statements@npm:1.0.11"
@@ -10386,6 +10542,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"pify@npm:^4.0.1":
version: 4.0.1
resolution: "pify@npm:4.0.1"
checksum: 10c0/6f9d404b0d47a965437403c9b90eca8bb2536407f03de165940e62e72c8c8b75adda5516c6b9b23675a5877cc0bcac6bdfb0ef0e39414cd2476d5495da40e7cf
languageName: node
linkType: hard
"pino-abstract-transport@npm:^2.0.0": "pino-abstract-transport@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "pino-abstract-transport@npm:2.0.0" resolution: "pino-abstract-transport@npm:2.0.0"
@@ -10684,6 +10847,24 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"postcss-load-config@npm:^3.1.4":
version: 3.1.4
resolution: "postcss-load-config@npm:3.1.4"
dependencies:
lilconfig: "npm:^2.0.5"
yaml: "npm:^1.10.2"
peerDependencies:
postcss: ">=8.0.9"
ts-node: ">=9.0.0"
peerDependenciesMeta:
postcss:
optional: true
ts-node:
optional: true
checksum: 10c0/7d2cc6695c2fc063e4538316d651a687fdb55e48db453ff699de916a6ee55ab68eac2b120c28a6b8ca7aa746a588888351b810a215b5cd090eabea62c5762ede
languageName: node
linkType: hard
"postcss-logical@npm:^8.1.0": "postcss-logical@npm:^8.1.0":
version: 8.1.0 version: 8.1.0
resolution: "postcss-logical@npm:8.1.0" resolution: "postcss-logical@npm:8.1.0"
@@ -10702,6 +10883,39 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"postcss-modules-extract-imports@npm:^3.0.0":
version: 3.1.0
resolution: "postcss-modules-extract-imports@npm:3.1.0"
peerDependencies:
postcss: ^8.1.0
checksum: 10c0/402084bcab376083c4b1b5111b48ec92974ef86066f366f0b2d5b2ac2b647d561066705ade4db89875a13cb175b33dd6af40d16d32b2ea5eaf8bac63bd2bf219
languageName: node
linkType: hard
"postcss-modules-local-by-default@npm:^4.0.4":
version: 4.2.0
resolution: "postcss-modules-local-by-default@npm:4.2.0"
dependencies:
icss-utils: "npm:^5.0.0"
postcss-selector-parser: "npm:^7.0.0"
postcss-value-parser: "npm:^4.1.0"
peerDependencies:
postcss: ^8.1.0
checksum: 10c0/b0b83feb2a4b61f5383979d37f23116c99bc146eba1741ca3cf1acca0e4d0dbf293ac1810a6ab4eccbe1ee76440dd0a9eb2db5b3bba4f99fc1b3ded16baa6358
languageName: node
linkType: hard
"postcss-modules-scope@npm:^3.1.1":
version: 3.2.1
resolution: "postcss-modules-scope@npm:3.2.1"
dependencies:
postcss-selector-parser: "npm:^7.0.0"
peerDependencies:
postcss: ^8.1.0
checksum: 10c0/bd2d81f79e3da0ef6365b8e2c78cc91469d05b58046b4601592cdeef6c4050ed8fe1478ae000a1608042fc7e692cb51fecbd2d9bce3f4eace4d32e883ffca10b
languageName: node
linkType: hard
"postcss-nesting@npm:^13.0.2": "postcss-nesting@npm:^13.0.2":
version: 13.0.2 version: 13.0.2
resolution: "postcss-nesting@npm:13.0.2" resolution: "postcss-nesting@npm:13.0.2"
@@ -10898,14 +11112,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"postcss-value-parser@npm:^4.2.0": "postcss-value-parser@npm:^4.1.0, postcss-value-parser@npm:^4.2.0":
version: 4.2.0 version: 4.2.0
resolution: "postcss-value-parser@npm:4.2.0" resolution: "postcss-value-parser@npm:4.2.0"
checksum: 10c0/f4142a4f56565f77c1831168e04e3effd9ffcc5aebaf0f538eee4b2d465adfd4b85a44257bb48418202a63806a7da7fe9f56c330aebb3cac898e46b4cbf49161 checksum: 10c0/f4142a4f56565f77c1831168e04e3effd9ffcc5aebaf0f538eee4b2d465adfd4b85a44257bb48418202a63806a7da7fe9f56c330aebb3cac898e46b4cbf49161
languageName: node languageName: node
linkType: hard linkType: hard
"postcss@npm:^8.5.6": "postcss@npm:^8.0.0, postcss@npm:^8.4.35, postcss@npm:^8.5.6":
version: 8.5.6 version: 8.5.6
resolution: "postcss@npm:8.5.6" resolution: "postcss@npm:8.5.6"
dependencies: dependencies:
@@ -11059,6 +11273,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"prr@npm:~1.0.1":
version: 1.0.1
resolution: "prr@npm:1.0.1"
checksum: 10c0/5b9272c602e4f4472a215e58daff88f802923b84bc39c8860376bb1c0e42aaf18c25d69ad974bd06ec6db6f544b783edecd5502cd3d184748d99080d68e4be5f
languageName: node
linkType: hard
"pump@npm:^3.0.0": "pump@npm:^3.0.0":
version: 3.0.0 version: 3.0.0
resolution: "pump@npm:3.0.0" resolution: "pump@npm:3.0.0"
@@ -11775,6 +11996,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"reserved-words@npm:^0.1.2":
version: 0.1.2
resolution: "reserved-words@npm:0.1.2"
checksum: 10c0/88360388d88f4b36c1f5d47f8d596936dbf950bddd642c04ce940f1dab1fa58ef6fec23f5fab81a1bfe5cd0f223b2b635311496fcf0ef3db93ad4dfb6d7be186
languageName: node
linkType: hard
"resolve-from@npm:^4.0.0": "resolve-from@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "resolve-from@npm:4.0.0" resolution: "resolve-from@npm:4.0.0"
@@ -12084,7 +12312,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"sass@npm:^1.62.1": "sass@npm:^1.62.1, sass@npm:^1.70.0":
version: 1.93.2 version: 1.93.2
resolution: "sass@npm:1.93.2" resolution: "sass@npm:1.93.2"
dependencies: dependencies:
@@ -12101,6 +12329,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"sax@npm:^1.2.4":
version: 1.4.1
resolution: "sax@npm:1.4.1"
checksum: 10c0/6bf86318a254c5d898ede6bd3ded15daf68ae08a5495a2739564eb265cd13bcc64a07ab466fb204f67ce472bb534eb8612dac587435515169593f4fffa11de7c
languageName: node
linkType: hard
"sax@npm:~1.3.0":
version: 1.3.0
resolution: "sax@npm:1.3.0"
checksum: 10c0/599dbe0ba9d8bd55e92d920239b21d101823a6cedff71e542589303fa0fa8f3ece6cf608baca0c51be846a2e88365fac94a9101a9c341d94b98e30c4deea5bea
languageName: node
linkType: hard
"saxes@npm:^6.0.0": "saxes@npm:^6.0.0":
version: 6.0.0 version: 6.0.0
resolution: "saxes@npm:6.0.0" resolution: "saxes@npm:6.0.0"
@@ -12144,6 +12386,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"semver@npm:^5.6.0":
version: 5.7.2
resolution: "semver@npm:5.7.2"
bin:
semver: bin/semver
checksum: 10c0/e4cf10f86f168db772ae95d86ba65b3fd6c5967c94d97c708ccb463b778c2ee53b914cd7167620950fc07faf5a564e6efe903836639e512a1aa15fbc9667fa25
languageName: node
linkType: hard
"semver@npm:^6.3.1": "semver@npm:^6.3.1":
version: 6.3.1 version: 6.3.1
resolution: "semver@npm:6.3.1" resolution: "semver@npm:6.3.1"
@@ -12433,7 +12684,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.1, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": "source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.1, source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1":
version: 1.2.1 version: 1.2.1
resolution: "source-map-js@npm:1.2.1" resolution: "source-map-js@npm:1.2.1"
checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf
@@ -12464,13 +12715,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"source-map@npm:^0.6.0, source-map@npm:~0.6.1": "source-map@npm:^0.6.0, source-map@npm:~0.6.0, source-map@npm:~0.6.1":
version: 0.6.1 version: 0.6.1
resolution: "source-map@npm:0.6.1" resolution: "source-map@npm:0.6.1"
checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011
languageName: node languageName: node
linkType: hard linkType: hard
"source-map@npm:^0.7.3":
version: 0.7.6
resolution: "source-map@npm:0.7.6"
checksum: 10c0/59f6f05538539b274ba771d2e9e32f6c65451982510564438e048bc1352f019c6efcdc6dd07909b1968144941c14015c2c7d4369fb7c4d7d53ae769716dcc16c
languageName: node
linkType: hard
"source-map@npm:^0.7.4": "source-map@npm:^0.7.4":
version: 0.7.4 version: 0.7.4
resolution: "source-map@npm:0.7.4" resolution: "source-map@npm:0.7.4"
@@ -13018,6 +13276,21 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"stylus@npm:^0.62.0":
version: 0.62.0
resolution: "stylus@npm:0.62.0"
dependencies:
"@adobe/css-tools": "npm:~4.3.1"
debug: "npm:^4.3.2"
glob: "npm:^7.1.6"
sax: "npm:~1.3.0"
source-map: "npm:^0.7.3"
bin:
stylus: bin/stylus
checksum: 10c0/62afe3a6d781f66d7d283e8218dc1a15530d7d89fc2f09457a723975b2073e96e0d32c61d7f0dd1bd2686aae4ab6cc6933dc85e1b72eebab8aa30167bd16962b
languageName: node
linkType: hard
"substring-trie@npm:^1.0.2": "substring-trie@npm:^1.0.2":
version: 1.0.2 version: 1.0.2
resolution: "substring-trie@npm:1.0.2" resolution: "substring-trie@npm:1.0.2"
@@ -13393,7 +13666,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.4.0, tslib@npm:^2.8.0": "tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.8.0":
version: 2.8.1 version: 2.8.1
resolution: "tslib@npm:2.8.1" resolution: "tslib@npm:2.8.1"
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
@@ -13534,6 +13807,35 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"typescript-plugin-css-modules@npm:^5.2.0":
version: 5.2.0
resolution: "typescript-plugin-css-modules@npm:5.2.0"
dependencies:
"@types/postcss-modules-local-by-default": "npm:^4.0.2"
"@types/postcss-modules-scope": "npm:^3.0.4"
dotenv: "npm:^16.4.2"
icss-utils: "npm:^5.1.0"
less: "npm:^4.2.0"
lodash.camelcase: "npm:^4.3.0"
postcss: "npm:^8.4.35"
postcss-load-config: "npm:^3.1.4"
postcss-modules-extract-imports: "npm:^3.0.0"
postcss-modules-local-by-default: "npm:^4.0.4"
postcss-modules-scope: "npm:^3.1.1"
reserved-words: "npm:^0.1.2"
sass: "npm:^1.70.0"
source-map-js: "npm:^1.0.2"
stylus: "npm:^0.62.0"
tsconfig-paths: "npm:^4.2.0"
peerDependencies:
typescript: ">=4.0.0"
dependenciesMeta:
stylus:
optional: true
checksum: 10c0/7cd024f7145c0a29d9b78f2fb49c42cdf1747b50a43391f9993132ba42a727266f9b544fd868d905d5352e0a8676a19ae7a9aa56d516cc819c3ab39d66aa25e4
languageName: node
linkType: hard
"typescript@npm:^5.6.0, typescript@npm:~5.9.0": "typescript@npm:^5.6.0, typescript@npm:~5.9.0":
version: 5.9.2 version: 5.9.2
resolution: "typescript@npm:5.9.2" resolution: "typescript@npm:5.9.2"
@@ -14623,7 +14925,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"yaml@npm:^1.10.0": "yaml@npm:^1.10.0, yaml@npm:^1.10.2":
version: 1.10.2 version: 1.10.2
resolution: "yaml@npm:1.10.2" resolution: "yaml@npm:1.10.2"
checksum: 10c0/5c28b9eb7adc46544f28d9a8d20c5b3cb1215a886609a2fd41f51628d8aaa5878ccd628b755dbcd29f6bb4921bd04ffbc6dcc370689bb96e594e2f9813d2605f checksum: 10c0/5c28b9eb7adc46544f28d9a8d20c5b3cb1215a886609a2fd41f51628d8aaa5878ccd628b755dbcd29f6bb4921bd04ffbc6dcc370689bb96e594e2f9813d2605f