Merge branch 'main' into feature/require-mfa-by-admin

This commit is contained in:
FredysFonseca 2025-08-14 20:56:31 -04:00 committed by GitHub
commit d114645e60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 1234 additions and 373 deletions

View File

@ -9,7 +9,6 @@ permissions:
jobs:
compute-suffix:
runs-on: ubuntu-latest
if: github.repository == 'mastodon/mastodon'
steps:
- id: version_vars
env:

View File

@ -7,7 +7,7 @@
* - Please do NOT modify this file.
*/
const PACKAGE_VERSION = '2.10.2'
const PACKAGE_VERSION = '2.10.4'
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()

View File

@ -583,7 +583,6 @@ The following changelog entries focus on changes visible to users, administrator
You can now separately filter or drop notifications from people you don't follow, people who don't follow you, accounts created within the past 30 days, as well as unsolicited private mentions, and accounts limited by the moderation.\
Instead of being outright dropped, notifications that you chose to filter are put in a separate “Filtered notifications” box that you can review separately without it clogging your main notifications.\
This adds the following REST API endpoints:
- `GET /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#get-policy
- `PATCH /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#update-the-filtering-policy-for-notifications
- `GET /api/v1/notifications/requests`: https://docs.joinmastodon.org/methods/notifications/#get-requests
@ -595,7 +594,6 @@ The following changelog entries focus on changes visible to users, administrator
- `GET /api/v1/notifications/requests/merged`: https://docs.joinmastodon.org/methods/notifications/#requests-merged
In addition, accepting one or more notification requests generates a new streaming event:
- `notifications_merged`: an event of this type indicates accepted notification requests have finished merging, and the notifications list should be refreshed
- **Add notifications of severed relationships** (#27511, #29665, #29668, #29670, #29700, #29714, #29712, and #29731 by @ClearlyClaire and @Gargron)\

View File

@ -82,7 +82,7 @@ gem 'rqrcode', '~> 3.0'
gem 'ruby-progressbar', '~> 1.13'
gem 'sanitize', '~> 7.0'
gem 'scenic', '~> 1.7'
gem 'sidekiq', '< 8'
gem 'sidekiq', '< 9'
gem 'sidekiq-bulk', '~> 0.2.0'
gem 'sidekiq-scheduler', '~> 6.0'
gem 'sidekiq-unique-jobs', '> 8'
@ -109,10 +109,10 @@ group :opentelemetry do
gem 'opentelemetry-instrumentation-active_job', '~> 0.8.0', require: false
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.22.0', require: false
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.22.0', require: false
gem 'opentelemetry-instrumentation-excon', '~> 0.23.0', require: false
gem 'opentelemetry-instrumentation-faraday', '~> 0.27.0', require: false
gem 'opentelemetry-instrumentation-excon', '~> 0.24.0', require: false
gem 'opentelemetry-instrumentation-faraday', '~> 0.28.0', require: false
gem 'opentelemetry-instrumentation-http', '~> 0.25.0', require: false
gem 'opentelemetry-instrumentation-http_client', '~> 0.23.0', require: false
gem 'opentelemetry-instrumentation-http_client', '~> 0.24.0', require: false
gem 'opentelemetry-instrumentation-net_http', '~> 0.23.0', require: false
gem 'opentelemetry-instrumentation-pg', '~> 0.30.0', require: false
gem 'opentelemetry-instrumentation-rack', '~> 0.26.0', require: false

View File

@ -10,29 +10,29 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (8.0.2)
actionpack (= 8.0.2)
activesupport (= 8.0.2)
actioncable (8.0.2.1)
actionpack (= 8.0.2.1)
activesupport (= 8.0.2.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.0.2)
actionpack (= 8.0.2)
activejob (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
actionmailbox (8.0.2.1)
actionpack (= 8.0.2.1)
activejob (= 8.0.2.1)
activerecord (= 8.0.2.1)
activestorage (= 8.0.2.1)
activesupport (= 8.0.2.1)
mail (>= 2.8.0)
actionmailer (8.0.2)
actionpack (= 8.0.2)
actionview (= 8.0.2)
activejob (= 8.0.2)
activesupport (= 8.0.2)
actionmailer (8.0.2.1)
actionpack (= 8.0.2.1)
actionview (= 8.0.2.1)
activejob (= 8.0.2.1)
activesupport (= 8.0.2.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.0.2)
actionview (= 8.0.2)
activesupport (= 8.0.2)
actionpack (8.0.2.1)
actionview (= 8.0.2.1)
activesupport (= 8.0.2.1)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
@ -40,15 +40,15 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.0.2)
actionpack (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
actiontext (8.0.2.1)
actionpack (= 8.0.2.1)
activerecord (= 8.0.2.1)
activestorage (= 8.0.2.1)
activesupport (= 8.0.2.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.2)
activesupport (= 8.0.2)
actionview (8.0.2.1)
activesupport (= 8.0.2.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
@ -58,22 +58,22 @@ GEM
activemodel (>= 4.1)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (8.0.2)
activesupport (= 8.0.2)
activejob (8.0.2.1)
activesupport (= 8.0.2.1)
globalid (>= 0.3.6)
activemodel (8.0.2)
activesupport (= 8.0.2)
activerecord (8.0.2)
activemodel (= 8.0.2)
activesupport (= 8.0.2)
activemodel (8.0.2.1)
activesupport (= 8.0.2.1)
activerecord (8.0.2.1)
activemodel (= 8.0.2.1)
activesupport (= 8.0.2.1)
timeout (>= 0.4.0)
activestorage (8.0.2)
actionpack (= 8.0.2)
activejob (= 8.0.2)
activerecord (= 8.0.2)
activesupport (= 8.0.2)
activestorage (8.0.2.1)
actionpack (= 8.0.2.1)
activejob (= 8.0.2.1)
activerecord (= 8.0.2.1)
activesupport (= 8.0.2.1)
marcel (~> 1.0)
activesupport (8.0.2)
activesupport (8.0.2.1)
base64
benchmark (>= 0.3)
bigdecimal
@ -547,19 +547,19 @@ GEM
opentelemetry-instrumentation-concurrent_ruby (0.22.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-excon (0.23.0)
opentelemetry-instrumentation-excon (0.24.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-faraday (0.27.0)
opentelemetry-instrumentation-faraday (0.28.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-http (0.25.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-http_client (0.23.0)
opentelemetry-instrumentation-http_client (0.24.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-net_http (0.23.0)
opentelemetry-instrumentation-net_http (0.23.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-pg (0.30.1)
@ -667,20 +667,20 @@ GEM
rack (>= 1.3)
rackup (2.2.1)
rack (>= 3)
rails (8.0.2)
actioncable (= 8.0.2)
actionmailbox (= 8.0.2)
actionmailer (= 8.0.2)
actionpack (= 8.0.2)
actiontext (= 8.0.2)
actionview (= 8.0.2)
activejob (= 8.0.2)
activemodel (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
rails (8.0.2.1)
actioncable (= 8.0.2.1)
actionmailbox (= 8.0.2.1)
actionmailer (= 8.0.2.1)
actionpack (= 8.0.2.1)
actiontext (= 8.0.2.1)
actionview (= 8.0.2.1)
activejob (= 8.0.2.1)
activemodel (= 8.0.2.1)
activerecord (= 8.0.2.1)
activestorage (= 8.0.2.1)
activesupport (= 8.0.2.1)
bundler (>= 1.15.0)
railties (= 8.0.2)
railties (= 8.0.2.1)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
@ -691,9 +691,9 @@ GEM
rails-i18n (8.0.1)
i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9)
railties (8.0.2)
actionpack (= 8.0.2)
activesupport (= 8.0.2)
railties (8.0.2.1)
actionpack (= 8.0.2.1)
activesupport (= 8.0.2.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@ -719,7 +719,7 @@ GEM
redis (4.8.1)
redis-client (0.25.2)
connection_pool
regexp_parser (2.11.1)
regexp_parser (2.11.2)
reline (0.6.2)
io-console (~> 0.5)
request_store (1.7.0)
@ -787,7 +787,7 @@ GEM
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
rubocop-rails (2.33.1)
rubocop-rails (2.33.3)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
@ -823,12 +823,12 @@ GEM
securerandom (0.4.1)
shoulda-matchers (6.5.0)
activesupport (>= 5.2.0)
sidekiq (7.3.9)
base64
connection_pool (>= 2.3.0)
logger
rack (>= 2.2.4)
redis-client (>= 0.22.2)
sidekiq (8.0.7)
connection_pool (>= 2.5.0)
json (>= 2.9.0)
logger (>= 1.6.2)
rack (>= 3.1.0)
redis-client (>= 0.23.2)
sidekiq-bulk (0.2.0)
sidekiq
sidekiq-scheduler (6.0.1)
@ -1030,10 +1030,10 @@ DEPENDENCIES
opentelemetry-instrumentation-active_job (~> 0.8.0)
opentelemetry-instrumentation-active_model_serializers (~> 0.22.0)
opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
opentelemetry-instrumentation-excon (~> 0.23.0)
opentelemetry-instrumentation-faraday (~> 0.27.0)
opentelemetry-instrumentation-excon (~> 0.24.0)
opentelemetry-instrumentation-faraday (~> 0.28.0)
opentelemetry-instrumentation-http (~> 0.25.0)
opentelemetry-instrumentation-http_client (~> 0.23.0)
opentelemetry-instrumentation-http_client (~> 0.24.0)
opentelemetry-instrumentation-net_http (~> 0.23.0)
opentelemetry-instrumentation-pg (~> 0.30.0)
opentelemetry-instrumentation-rack (~> 0.26.0)
@ -1077,7 +1077,7 @@ DEPENDENCIES
sanitize (~> 7.0)
scenic (~> 1.7)
shoulda-matchers
sidekiq (< 8)
sidekiq (< 9)
sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 6.0)
sidekiq-unique-jobs (> 8)

View File

@ -58,7 +58,7 @@ Mastodon is a **free, open-source social network server** based on [ActivityPub]
- **Ruby** 3.2+
- **PostgreSQL** 13+
- **Redis** 6.2+
- **Redis** 7.0+
- **Node.js** 20+
This repository includes deployment configurations for **Docker and docker-compose**, as well as for other environments like Heroku and Scalingo. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). A [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the main documentation.

3
Vagrantfile vendored
View File

@ -54,6 +54,7 @@ sudo apt-get install \
pkg-config \
protobuf-compiler \
zlib1g-dev \
libvips42t64 \
-y
# Install rvm
@ -134,7 +135,7 @@ VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "ubuntu/focal64"
config.vm.box = "bento/ubuntu-24.04"
config.vm.provider :virtualbox do |vb|
vb.name = "mastodon"

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
class Api::V1::Statuses::InteractionPoliciesController < Api::V1::Statuses::BaseController
include Api::InteractionPoliciesConcern
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
before_action -> { check_feature_enabled }
def update
authorize @status, :update?
@status.update!(quote_approval_policy: quote_approval_policy)
broadcast_updates! if @status.quote_approval_policy_previously_changed?
render json: @status, serializer: REST::StatusSerializer
end
private
def status_params
params.permit(:quote_approval_policy)
end
def check_feature_enabled
raise ActionController::RoutingError unless Mastodon::Feature.outgoing_quotes_enabled?
end
def broadcast_updates!
DistributionWorker.perform_async(@status.id, { 'update' => true })
ActivityPub::StatusUpdateDistributionWorker.perform_async(@status.id)
end
end

View File

@ -3,6 +3,7 @@
class Api::V1::StatusesController < Api::BaseController
include Authorization
include AsyncRefreshesConcern
include Api::InteractionPoliciesConcern
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
@ -205,23 +206,6 @@ class Api::V1::StatusesController < Api::BaseController
)
end
def quote_approval_policy
# TODO: handle `nil` separately
return nil unless Mastodon::Feature.outgoing_quotes_enabled? && status_params[:quote_approval_policy].present?
case status_params[:quote_approval_policy]
when 'public'
Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16
when 'followers'
Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16
when 'nobody'
0
else
# TODO: raise more useful message
raise ActiveRecord::RecordInvalid
end
end
def serializer_for_status
@status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
module Api::InteractionPoliciesConcern
extend ActiveSupport::Concern
def quote_approval_policy
# TODO: handle `nil` separately
return nil unless Mastodon::Feature.outgoing_quotes_enabled? && status_params[:quote_approval_policy].present?
case status_params[:quote_approval_policy]
when 'public'
Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16
when 'followers'
Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16
when 'nobody'
0
else
# TODO: raise more useful message
raise ActiveRecord::RecordInvalid
end
end
end

View File

@ -183,7 +183,7 @@ export function directCompose(account) {
};
}
export function submitCompose() {
export function submitCompose(successCallback) {
return function (dispatch, getState) {
const status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']);
@ -241,6 +241,9 @@ export function submitCompose() {
dispatch(insertIntoTagHistory(response.data.tags, status));
dispatch(submitComposeSuccess({ ...response.data }));
if (typeof successCallback === 'function') {
successCallback(response.data);
}
// To make the app more responsive, immediately push the status
// into the columns

View File

@ -1,8 +1,10 @@
import { createAction } from '@reduxjs/toolkit';
import { apiGetContext } from 'mastodon/api/statuses';
import { apiGetContext, apiSetQuotePolicy } from 'mastodon/api/statuses';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
import type { ApiQuotePolicy } from '../api_types/quotes';
import { importFetchedStatuses } from './importer';
export const fetchContext = createDataLoadingThunk(
@ -23,3 +25,10 @@ export const fetchContext = createDataLoadingThunk(
export const completeContextRefresh = createAction<{ statusId: string }>(
'status/context/complete',
);
export const setStatusQuotePolicy = createDataLoadingThunk(
'status/setQuotePolicy',
({ statusId, policy }: { statusId: string; policy: ApiQuotePolicy }) => {
return apiSetQuotePolicy(statusId, policy);
},
);

View File

@ -1,5 +1,10 @@
import api, { getAsyncRefreshHeader } from 'mastodon/api';
import type { ApiContextJSON } from 'mastodon/api_types/statuses';
import api, { apiRequestPut, getAsyncRefreshHeader } from 'mastodon/api';
import type {
ApiContextJSON,
ApiStatusJSON,
} from 'mastodon/api_types/statuses';
import type { ApiQuotePolicy } from '../api_types/quotes';
export const apiGetContext = async (statusId: string) => {
const response = await api().request<ApiContextJSON>({
@ -12,3 +17,15 @@ export const apiGetContext = async (statusId: string) => {
refresh: getAsyncRefreshHeader(response),
};
};
export const apiSetQuotePolicy = async (
statusId: string,
policy: ApiQuotePolicy,
) => {
return apiRequestPut<ApiStatusJSON>(
`v1/statuses/${statusId}/interaction_policy`,
{
quote_approval_policy: policy,
},
);
};

View File

@ -1,7 +1,7 @@
import type { ApiStatusJSON } from './statuses';
export type ApiQuoteState = 'accepted' | 'pending' | 'revoked' | 'unauthorized';
export type ApiQuotePolicy = 'public' | 'followers' | 'nobody';
export type ApiQuotePolicy = 'public' | 'followers' | 'nobody' | 'unknown';
interface ApiQuoteEmptyJSON {
state: Exclude<ApiQuoteState, 'accepted'>;
@ -21,3 +21,13 @@ interface ApiQuoteAcceptedJSON {
}
export type ApiQuoteJSON = ApiQuoteAcceptedJSON | ApiQuoteEmptyJSON;
export interface ApiQuotePolicyJSON {
automatic: ApiQuotePolicy[];
manual: ApiQuotePolicy[];
current_user: ApiQuotePolicy;
}
export function isQuotePolicy(policy: string): policy is ApiQuotePolicy {
return ['public', 'followers', 'nobody'].includes(policy);
}

View File

@ -4,7 +4,7 @@ import type { ApiAccountJSON } from './accounts';
import type { ApiCustomEmojiJSON } from './custom_emoji';
import type { ApiMediaAttachmentJSON } from './media_attachments';
import type { ApiPollJSON } from './polls';
import type { ApiQuoteJSON } from './quotes';
import type { ApiQuoteJSON, ApiQuotePolicyJSON } from './quotes';
// See app/modals/status.rb
export type StatusVisibility =
@ -120,9 +120,16 @@ export interface ApiStatusJSON {
card?: ApiPreviewCardJSON;
poll?: ApiPollJSON;
quote?: ApiQuoteJSON;
quote_approval?: ApiQuotePolicyJSON;
}
export interface ApiContextJSON {
ancestors: ApiStatusJSON[];
descendants: ApiStatusJSON[];
}
export interface ApiStatusSourceJSON {
id: string;
text: string;
spoiler_text: string;
}

View File

@ -0,0 +1,114 @@
import { useCallback, useId, useMemo, useRef, useState } from 'react';
import type { ComponentPropsWithoutRef, FC } from 'react';
import { FormattedMessage } from 'react-intl';
import type { MessageDescriptor } from 'react-intl';
import classNames from 'classnames';
import Overlay from 'react-overlays/Overlay';
import type { SelectItem } from '../dropdown_selector';
import { DropdownSelector } from '../dropdown_selector';
interface DropdownProps {
title: string;
disabled?: boolean;
items: SelectItem[];
onChange: (value: string) => void;
current: string;
emptyText?: MessageDescriptor;
classPrefix: string;
}
export const Dropdown: FC<
DropdownProps & Omit<ComponentPropsWithoutRef<'button'>, keyof DropdownProps>
> = ({
title,
disabled,
items,
current,
onChange,
classPrefix,
className,
...buttonProps
}) => {
const buttonRef = useRef<HTMLButtonElement>(null);
const accessibilityId = useId();
const [open, setOpen] = useState(false);
const handleToggle = useCallback(() => {
if (!disabled) {
setOpen((prevOpen) => !prevOpen);
}
}, [disabled]);
const handleClose = useCallback(() => {
setOpen(false);
}, []);
const currentText = useMemo(
() => items.find((i) => i.value === current)?.text,
[current, items],
);
return (
<>
<button
type='button'
{...buttonProps}
title={title}
aria-expanded={open}
aria-controls={accessibilityId}
onClick={handleToggle}
disabled={disabled}
className={classNames(
`${classPrefix}__button`,
{
active: open,
disabled,
},
className,
)}
ref={buttonRef}
>
{currentText ?? (
<FormattedMessage
id='dropdown.empty'
defaultMessage='Select an option'
/>
)}
</button>
<Overlay
show={open}
offset={[0, 4]}
placement='bottom-start'
onHide={handleClose}
flip
target={buttonRef.current}
popperConfig={{
strategy: 'fixed',
}}
>
{({ props, placement }) => (
<div {...props} className={`${classPrefix}__overlay`}>
<div
className={classNames(
'dropdown-animation',
`${classPrefix}__dropdown`,
placement,
)}
id={accessibilityId}
>
<DropdownSelector
items={items}
value={current}
onClose={handleClose}
onChange={onChange}
classNamePrefix={classPrefix}
/>
</div>
</div>
)}
</Overlay>
</>
);
};

View File

@ -13,8 +13,8 @@ const listenerOptions = supportsPassiveEvents
? { passive: true, capture: true }
: true;
export interface SelectItem {
value: string;
export interface SelectItem<Value extends string = string> {
value: Value;
icon?: string;
iconComponent?: IconProp;
text: string;
@ -24,7 +24,7 @@ export interface SelectItem {
interface Props {
value: string;
classNamePrefix: string;
classNamePrefix?: string;
style?: React.CSSProperties;
items: SelectItem[];
onChange: (value: string) => void;

View File

@ -29,6 +29,7 @@ import { Dropdown } from 'mastodon/components/dropdown_menu';
import { me } from '../initial_state';
import { IconButton } from './icon_button';
import { isFeatureEnabled } from '../utils/environment';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
@ -68,6 +69,7 @@ const messages = defineMessages({
filter: { id: 'status.filter', defaultMessage: 'Filter this post' },
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
revokeQuote: { id: 'status.revoke_quote', defaultMessage: 'Remove my post from @{name}s post' },
quotePolicyChange: { id: 'status.quote_policy_change', defaultMessage: 'Change who can quote' },
});
const mapStateToProps = (state, { status }) => {
@ -89,6 +91,7 @@ class StatusActionBar extends ImmutablePureComponent {
onReblog: PropTypes.func,
onDelete: PropTypes.func,
onRevokeQuote: PropTypes.func,
onQuotePolicyChange: PropTypes.func,
onDirect: PropTypes.func,
onMention: PropTypes.func,
onMute: PropTypes.func,
@ -200,7 +203,11 @@ class StatusActionBar extends ImmutablePureComponent {
handleRevokeQuoteClick = () => {
this.props.onRevokeQuote(this.props.status);
}
};
handleQuotePolicyChange = () => {
this.props.onQuotePolicyChange(this.props.status);
};
handleBlockClick = () => {
const { status, relationship, onBlock, onUnblock } = this.props;
@ -291,6 +298,9 @@ class StatusActionBar extends ImmutablePureComponent {
if (writtenByMe || withDismiss) {
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
if (writtenByMe && isFeatureEnabled('outgoing_quotes') && !['private', 'direct'].includes(status.get('visibility'))) {
menu.push({ text: intl.formatMessage(messages.quotePolicyChange), action: this.handleQuotePolicyChange });
}
menu.push(null);
}

View File

@ -115,6 +115,10 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({
dispatch(openModal({ modalType: 'CONFIRM_REVOKE_QUOTE', modalProps: { statusId: status.get('id'), quotedStatusId: status.getIn(['quote', 'quoted_status']) }}));
},
onQuotePolicyChange(status) {
dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId: status.get('id') } }));
},
onEdit (status) {
dispatch((_, getState) => {
let state = getState();

View File

@ -73,6 +73,7 @@ class ComposeForm extends ImmutablePureComponent {
singleColumn: PropTypes.bool,
lang: PropTypes.string,
maxChars: PropTypes.number,
redirectOnSuccess: PropTypes.bool,
};
static defaultProps = {
@ -255,62 +256,60 @@ class ComposeForm extends ImmutablePureComponent {
<Warning />
<div className={classNames('compose-form__highlightable', { active: highlighted })} ref={this.setRef}>
<div className='compose-form__scrollable'>
<EditIndicator />
<EditIndicator />
{this.props.spoiler && (
<div className='spoiler-input'>
<div className='spoiler-input__border' />
{this.props.spoiler && (
<div className='spoiler-input'>
<div className='spoiler-input__border' />
<AutosuggestInput
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
value={this.props.spoilerText}
disabled={isSubmitting}
onChange={this.handleChangeSpoilerText}
onKeyDown={this.handleKeyDownSpoiler}
ref={this.setSpoilerText}
suggestions={this.props.suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSpoilerSuggestionSelected}
searchTokens={[':']}
id='cw-spoiler-input'
className='spoiler-input__input'
lang={this.props.lang}
spellCheck
/>
<AutosuggestInput
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
value={this.props.spoilerText}
disabled={isSubmitting}
onChange={this.handleChangeSpoilerText}
onKeyDown={this.handleKeyDownSpoiler}
ref={this.setSpoilerText}
suggestions={this.props.suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSpoilerSuggestionSelected}
searchTokens={[':']}
id='cw-spoiler-input'
className='spoiler-input__input'
lang={this.props.lang}
spellCheck
/>
<div className='spoiler-input__border' />
</div>
)}
<div className='spoiler-input__border' />
</div>
)}
<AutosuggestTextarea
ref={this.textareaRef}
placeholder={intl.formatMessage(messages.placeholder)}
disabled={isSubmitting}
value={this.props.text}
onChange={this.handleChange}
suggestions={this.props.suggestions}
onFocus={this.handleFocus}
onKeyDown={this.handleKeyDownPost}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
onPaste={onPaste}
autoFocus={autoFocus}
lang={this.props.lang}
/>
<div className='compose-form__dropdowns'>
<PrivacyDropdownContainer disabled={this.props.isEditing} />
<LanguageDropdown />
</div>
<AutosuggestTextarea
ref={this.textareaRef}
placeholder={intl.formatMessage(messages.placeholder)}
disabled={isSubmitting}
value={this.props.text}
onChange={this.handleChange}
suggestions={this.props.suggestions}
onFocus={this.handleFocus}
onKeyDown={this.handleKeyDownPost}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
onPaste={onPaste}
autoFocus={autoFocus}
lang={this.props.lang}
/>
<UploadForm />
<PollForm />
<div className='compose-form__footer'>
<div className='compose-form__dropdowns'>
<PrivacyDropdownContainer disabled={this.props.isEditing} />
<LanguageDropdown />
</div>
<div className='compose-form__actions'>
<div className='compose-form__buttons'>
<UploadButtonContainer />
@ -329,7 +328,7 @@ class ComposeForm extends ImmutablePureComponent {
>
{intl.formatMessage(
this.props.isEditing ?
messages.saveChanges :
messages.saveChanges :
(this.props.isInReply ? messages.reply : messages.publish)
)}
</Button>

View File

@ -396,7 +396,7 @@ export const LanguageDropdown: React.FC = () => {
warning: guess !== '' && guess !== value,
})}
>
<Icon id='' icon={TranslateIcon} />
<Icon id='translate' icon={TranslateIcon} />
<span className='dropdown-button__label'>{current[2] ?? value}</span>
</button>

View File

@ -14,7 +14,7 @@ import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
import { DropdownSelector } from 'mastodon/components/dropdown_selector';
import { Icon } from 'mastodon/components/icon';
const messages = defineMessages({
export const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Quiet public' },

View File

@ -34,7 +34,7 @@ const mapStateToProps = state => ({
maxChars: state.getIn(['server', 'server', 'configuration', 'statuses', 'max_characters'], 500),
});
const mapDispatchToProps = (dispatch) => ({
const mapDispatchToProps = (dispatch, props) => ({
onChange (text) {
dispatch(changeCompose(text));
@ -47,7 +47,11 @@ const mapDispatchToProps = (dispatch) => ({
modalProps: {},
}));
} else {
dispatch(submitCompose());
dispatch(submitCompose((status) => {
if (props.redirectOnSuccess) {
window.location.assign(status.url);
}
}));
}
},

View File

@ -5,7 +5,7 @@ import ModalContainer from 'mastodon/features/ui/containers/modal_container';
const Compose = () => (
<>
<ComposeFormContainer autoFocus withoutNavigation />
<ComposeFormContainer autoFocus withoutNavigation redirectOnSuccess />
<AlertsController />
<ModalContainer />
<LoadingBarContainer className='loading-bar' />

View File

@ -26,6 +26,7 @@ import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/
import { IconButton } from '../../../components/icon_button';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { me } from '../../../initial_state';
import { isFeatureEnabled } from '@/mastodon/utils/environment';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
@ -62,6 +63,7 @@ const messages = defineMessages({
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
revokeQuote: { id: 'status.revoke_quote', defaultMessage: 'Remove my post from @{name}s post' },
quotePolicyChange: { id: 'status.quote_policy_change', defaultMessage: 'Change who can quote' },
});
const mapStateToProps = (state, { status }) => {
@ -84,6 +86,7 @@ class ActionBar extends PureComponent {
onBookmark: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onRevokeQuote: PropTypes.func,
onQuotePolicyChange: PropTypes.func,
onEdit: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
@ -122,7 +125,11 @@ class ActionBar extends PureComponent {
handleRevokeQuoteClick = () => {
this.props.onRevokeQuote(this.props.status);
}
};
handleQuotePolicyChange = () => {
this.props.onQuotePolicyChange(this.props.status);
};
handleRedraftClick = () => {
this.props.onDelete(this.props.status, true);
@ -240,6 +247,9 @@ class ActionBar extends PureComponent {
}
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
if (isFeatureEnabled('outgoing_quotes') && !['private', 'direct'].includes(status.get('visibility'))) {
menu.push({ text: intl.formatMessage(messages.quotePolicyChange), action: this.handleQuotePolicyChange });
}
menu.push(null);
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });

View File

@ -265,6 +265,11 @@ class Status extends ImmutablePureComponent {
dispatch(openModal({ modalType: 'CONFIRM_REVOKE_QUOTE', modalProps: { statusId: status.get('id'), quotedStatusId: status.getIn(['quote', 'quoted_status']) }}));
};
handleQuotePolicyChange = (status) => {
const { dispatch } = this.props;
dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId: status.get('id') } }));
};
handleEditClick = (status) => {
const { dispatch, askReplyConfirmation } = this.props;
@ -642,6 +647,7 @@ class Status extends ImmutablePureComponent {
onBookmark={this.handleBookmarkClick}
onDelete={this.handleDeleteClick}
onRevokeQuote={this.handleRevokeQuoteClick}
onQuotePolicyChange={this.handleQuotePolicyChange}
onEdit={this.handleEditClick}
onDirect={this.handleDirectClick}
onMention={this.handleMentionClick}

View File

@ -43,6 +43,7 @@ import { ImageModal } from './image_modal';
import MediaModal from './media_modal';
import { ModalPlaceholder } from './modal_placeholder';
import VideoModal from './video_modal';
import { VisibilityModal } from './visibility_modal';
export const MODAL_COMPONENTS = {
'MEDIA': () => Promise.resolve({ default: MediaModal }),
@ -76,6 +77,7 @@ export const MODAL_COMPONENTS = {
'CLOSED_REGISTRATIONS': ClosedRegistrationsModal,
'IGNORE_NOTIFICATIONS': IgnoreNotificationsModal,
'ANNUAL_REPORT': AnnualReportModal,
'COMPOSE_PRIVACY': () => Promise.resolve({ default: VisibilityModal }),
};
export default class ModalRoot extends PureComponent {

View File

@ -0,0 +1,293 @@
import { forwardRef, useCallback, useId, useMemo } from 'react';
import type { FC } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames';
import { changeComposeVisibility } from '@/mastodon/actions/compose';
import { setStatusQuotePolicy } from '@/mastodon/actions/statuses_typed';
import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
import { isQuotePolicy } from '@/mastodon/api_types/quotes';
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
import { Dropdown } from '@/mastodon/components/dropdown';
import type { SelectItem } from '@/mastodon/components/dropdown_selector';
import { IconButton } from '@/mastodon/components/icon_button';
import { messages as privacyMessages } from '@/mastodon/features/compose/components/privacy_dropdown';
import {
createAppSelector,
useAppDispatch,
useAppSelector,
} from '@/mastodon/store';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import type { BaseConfirmationModalProps } from './confirmation_modals/confirmation_modal';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
buttonTitle: {
id: 'visibility_modal.button_title',
defaultMessage: 'Set visibility',
},
quotePublic: {
id: 'visibility_modal.quote_public',
defaultMessage: 'Anyone',
},
quoteFollowers: {
id: 'visibility_modal.quote_followers',
defaultMessage: 'Followers only',
},
quoteNobody: {
id: 'visibility_modal.quote_nobody',
defaultMessage: 'No one',
},
});
interface VisibilityModalProps extends BaseConfirmationModalProps {
statusId: string;
}
const selectStatusPolicy = createAppSelector(
[(state) => state.statuses, (_state, statusId: string) => statusId],
(statuses, statusId) => {
const status = statuses.get(statusId);
if (!status) {
return 'public';
}
const policy =
(status.getIn(['quote_approval', 'automatic', 0]) as string) || 'nobody';
const visibility = status.get('visibility') as StatusVisibility;
// If the status is private or direct, it cannot be quoted by anyone.
if (visibility === 'private' || visibility === 'direct') {
return 'nobody';
}
// If the status has a specific quote policy, return it.
if (isQuotePolicy(policy)) {
return policy;
}
// Otherwise, return the default based on visibility.
if (visibility === 'unlisted') {
return 'followers';
}
return 'public';
},
);
export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
({ onClose, statusId }, ref) => {
const intl = useIntl();
const currentVisibility = useAppSelector(
(state) =>
(state.statuses.getIn([statusId, 'visibility'], 'public') as
| StatusVisibility
| undefined) ?? 'public',
);
const currentQuotePolicy = useAppSelector((state) =>
selectStatusPolicy(state, statusId),
);
const disableQuotePolicy =
currentVisibility === 'private' || currentVisibility === 'direct';
const isSaving = useAppSelector(
(state) =>
state.statuses.getIn([statusId, 'isSavingQuotePolicy']) === true,
);
const visibilityItems = useMemo<SelectItem<StatusVisibility>[]>(
() => [
{
value: 'public',
text: intl.formatMessage(privacyMessages.public_short),
meta: intl.formatMessage(privacyMessages.public_long),
},
{
value: 'unlisted',
text: intl.formatMessage(privacyMessages.unlisted_short),
meta: intl.formatMessage(privacyMessages.unlisted_long),
},
{
value: 'private',
text: intl.formatMessage(privacyMessages.private_short),
meta: intl.formatMessage(privacyMessages.private_long),
},
{
value: 'direct',
text: intl.formatMessage(privacyMessages.direct_short),
meta: intl.formatMessage(privacyMessages.direct_long),
},
],
[intl],
);
const quoteItems = useMemo<SelectItem<ApiQuotePolicy>[]>(
() => [
{ value: 'public', text: intl.formatMessage(messages.quotePublic) },
{
value: 'followers',
text: intl.formatMessage(messages.quoteFollowers),
},
{ value: 'nobody', text: intl.formatMessage(messages.quoteNobody) },
],
[intl],
);
const dispatch = useAppDispatch();
const handleVisibilityChange = useCallback(
(value: string) => {
// Published statuses cannot change visibility.
if (statusId) {
return;
}
dispatch(changeComposeVisibility(value));
},
[dispatch, statusId],
);
const handleQuotePolicyChange = useCallback(
(value: string) => {
if (isQuotePolicy(value) && !disableQuotePolicy) {
void dispatch(setStatusQuotePolicy({ policy: value, statusId }));
}
},
[disableQuotePolicy, dispatch, statusId],
);
const privacyDropdownId = useId();
const quoteDropdownId = useId();
return (
<div className='modal-root__modal dialog-modal visibility-modal'>
<div className='dialog-modal__header'>
<IconButton
className='dialog-modal__header__close'
title={intl.formatMessage(messages.close)}
icon='times'
iconComponent={CloseIcon}
onClick={onClose}
/>
<FormattedMessage
id='visibility_modal.header'
defaultMessage='Visibility and interaction'
>
{(chunks) => (
<span className='dialog-modal__header__title'>{chunks}</span>
)}
</FormattedMessage>
</div>
<div className='dialog-modal__content'>
<div className='dialog-modal__content__description'>
<FormattedMessage
id='visibility_modal.instructions'
defaultMessage='Control who can interact with this post. Global settings can be found under <link>Preferences > Other</link>.'
values={{
link: (chunks) => (
<a href='/settings/preferences/other'>{chunks}</a>
),
}}
tagName='p'
/>
</div>
<div className='dialog-modal__content__form'>
<label
htmlFor={privacyDropdownId}
className={classNames('visibility-dropdown__label', {
disabled: isSaving || !!statusId,
})}
>
<FormattedMessage
id='visibility_modal.privacy_label'
defaultMessage='Privacy'
/>
<Dropdown
items={visibilityItems}
classPrefix='visibility-dropdown'
current={currentVisibility}
onChange={handleVisibilityChange}
title={intl.formatMessage(privacyMessages.change_privacy)}
disabled={isSaving || !!statusId}
id={privacyDropdownId}
/>
{!!statusId && (
<p className='visibility-dropdown__helper'>
<FormattedMessage
id='visibility_modal.helper.privacy_editing'
defaultMessage="Visibility can't be changed after a post is published."
/>
</p>
)}
</label>
<label
htmlFor={quoteDropdownId}
className={classNames('visibility-dropdown__label', {
disabled: disableQuotePolicy || isSaving,
})}
>
<FormattedMessage
id='visibility_modal.quote_label'
defaultMessage='Change who can quote'
/>
<Dropdown
items={quoteItems}
onChange={handleQuotePolicyChange}
classPrefix='visibility-dropdown'
current={currentQuotePolicy}
title={intl.formatMessage(messages.buttonTitle)}
disabled={disableQuotePolicy || isSaving}
id={quoteDropdownId}
/>
<QuotePolicyHelper
policy={currentQuotePolicy}
visibility={currentVisibility}
/>
</label>
</div>
</div>
</div>
);
},
);
VisibilityModal.displayName = 'VisibilityModal';
const QuotePolicyHelper: FC<{
policy: ApiQuotePolicy;
visibility: StatusVisibility;
}> = ({ policy, visibility }) => {
if (visibility === 'unlisted' && policy !== 'nobody') {
return (
<p className='visibility-dropdown__helper'>
<FormattedMessage
id='visibility_modal.helper.unlisted_quoting'
defaultMessage='When people quote you, their post will also be hidden from trending timelines.'
/>
</p>
);
}
if (visibility === 'private') {
return (
<p className='visibility-dropdown__helper'>
<FormattedMessage
id='visibility_modal.helper.private_quoting'
defaultMessage="Follower-only posts can't be quoted."
/>
</p>
);
}
if (visibility === 'direct') {
return (
<p className='visibility-dropdown__helper'>
<FormattedMessage
id='visibility_modal.helper.direct_quoting'
defaultMessage="Private mentions can't be quoted."
/>
</p>
);
}
return null;
};

View File

@ -510,7 +510,7 @@
"lightbox.zoom_out": "Přizpůsobit velikost",
"limited_account_hint.action": "Přesto profil zobrazit",
"limited_account_hint.title": "Tento profil byl skryt moderátory {domain}.",
"link_preview.author": "Podle {name}",
"link_preview.author": "Od {name}",
"link_preview.more_from_author": "Více od {name}",
"link_preview.shares": "{count, plural, one {{counter} příspěvek} few {{counter} příspěvky} many {{counter} příspěvků} other {{counter} příspěvků}}",
"lists.add_member": "Přidat",

View File

@ -292,6 +292,7 @@
"domain_pill.your_handle": "Your handle:",
"domain_pill.your_server": "Your digital home, where all of your posts live. Dont like this one? Transfer servers at any time and bring your followers, too.",
"domain_pill.your_username": "Your unique identifier on this server. Its possible to find users with the same username on different servers.",
"dropdown.empty": "Select an option",
"embed.instructions": "Embed this post on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity",
@ -884,6 +885,7 @@
"status.quote_error.pending_approval": "Post pending",
"status.quote_error.pending_approval_popout.body": "Quotes shared across the Fediverse may take time to display, as different servers have different protocols.",
"status.quote_error.pending_approval_popout.title": "Pending quote? Remain calm",
"status.quote_policy_change": "Change who can quote",
"status.quote_post_author": "Quoted a post by @{name}",
"status.read_more": "Read more",
"status.reblog": "Boost",
@ -959,5 +961,17 @@
"video.skip_forward": "Skip forward",
"video.unmute": "Unmute",
"video.volume_down": "Volume down",
"video.volume_up": "Volume up"
"video.volume_up": "Volume up",
"visibility_modal.button_title": "Set visibility",
"visibility_modal.header": "Visibility and interaction",
"visibility_modal.helper.direct_quoting": "Private mentions can't be quoted.",
"visibility_modal.helper.privacy_editing": "Published posts cannot change their visibility.",
"visibility_modal.helper.private_quoting": "Follower-only posts can't be quoted.",
"visibility_modal.helper.unlisted_quoting": "When people quote you, their post will also be hidden from trending timelines.",
"visibility_modal.instructions": "Control who can interact with this post. Global settings can be found under <link>Preferences > Other</link>.",
"visibility_modal.privacy_label": "Privacy",
"visibility_modal.quote_followers": "Followers only",
"visibility_modal.quote_label": "Change who can quote",
"visibility_modal.quote_nobody": "No one",
"visibility_modal.quote_public": "Anyone"
}

View File

@ -245,6 +245,9 @@
"confirmations.remove_from_followers.confirm": "Fjern fylgjar",
"confirmations.remove_from_followers.message": "{name} vil ikkje fylgja deg meir. Vil du halda fram?",
"confirmations.remove_from_followers.title": "Fjern fylgjar?",
"confirmations.revoke_quote.confirm": "Fjern innlegget",
"confirmations.revoke_quote.message": "Du kan ikkje angra denne handlinga.",
"confirmations.revoke_quote.title": "Fjern innlegget?",
"confirmations.unfollow.confirm": "Slutt å fylgja",
"confirmations.unfollow.message": "Er du sikker på at du vil slutta å fylgja {name}?",
"confirmations.unfollow.title": "Slutt å fylgja brukaren?",
@ -498,6 +501,8 @@
"keyboard_shortcuts.translate": "å omsetje eit innlegg",
"keyboard_shortcuts.unfocus": "for å fokusere vekk skrive-/søkefeltet",
"keyboard_shortcuts.up": "Flytt opp på lista",
"learn_more_link.got_it": "Forstått",
"learn_more_link.learn_more": "Lær meir",
"lightbox.close": "Lukk",
"lightbox.next": "Neste",
"lightbox.previous": "Førre",
@ -598,6 +603,7 @@
"notification.label.mention": "Omtale",
"notification.label.private_mention": "Privat omtale",
"notification.label.private_reply": "Privat svar",
"notification.label.quote": "{name} siterte innlegget ditt",
"notification.label.reply": "Svar",
"notification.mention": "Omtale",
"notification.mentioned_you": "{name} nemnde deg",
@ -655,6 +661,7 @@
"notifications.column_settings.mention": "Omtaler:",
"notifications.column_settings.poll": "Røysteresultat:",
"notifications.column_settings.push": "Pushvarsel",
"notifications.column_settings.quote": "Sitat:",
"notifications.column_settings.reblog": "Framhevingar:",
"notifications.column_settings.show": "Vis i kolonne",
"notifications.column_settings.sound": "Spel av lyd",
@ -845,6 +852,8 @@
"status.bookmark": "Set bokmerke",
"status.cancel_reblog_private": "Opphev framheving",
"status.cannot_reblog": "Du kan ikkje framheva dette innlegget",
"status.context.load_new_replies": "Nye svar finst",
"status.context.loading": "Ser etter fleire svar",
"status.continued_thread": "Framhald til tråden",
"status.copy": "Kopier lenke til status",
"status.delete": "Slett",
@ -871,6 +880,11 @@
"status.open": "Utvid denne statusen",
"status.pin": "Fest på profil",
"status.quote_error.filtered": "Gøymt på grunn av eitt av filtra dine",
"status.quote_error.not_available": "Innlegget er ikkje tilgjengeleg",
"status.quote_error.pending_approval": "Innlegget ventar",
"status.quote_error.pending_approval_popout.body": "Sitat frå rundt om i allheimen kan ta tid å visa, fordi ulike tenarar har ulike protokollar.",
"status.quote_error.pending_approval_popout.title": "Ventande sitat? Ikkje stress",
"status.quote_post_author": "Siterte eit innlegg av @{name}",
"status.read_more": "Les meir",
"status.reblog": "Framhev",
"status.reblog_private": "Framhev til dei originale mottakarane",
@ -885,6 +899,7 @@
"status.reply": "Svar",
"status.replyAll": "Svar til tråd",
"status.report": "Rapporter @{name}",
"status.revoke_quote": "Fjern innlegget mitt frå @{name} sitt innlegg",
"status.sensitive_warning": "Ømtolig innhald",
"status.share": "Del",
"status.show_less_all": "Vis mindre for alle",

View File

@ -29,6 +29,7 @@ import {
STATUS_FETCH_REQUEST,
STATUS_FETCH_FAIL,
} from '../actions/statuses';
import { setStatusQuotePolicy } from '../actions/statuses_typed';
const importStatus = (state, status) => state.set(status.id, fromJS(status));
@ -70,6 +71,22 @@ const initialState = ImmutableMap();
/** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */
export default function statuses(state = initialState, action) {
if (setStatusQuotePolicy.pending.match(action)) {
const status = state.get(action.meta.arg.statusId);
if (status) {
return state.setIn([action.meta.arg.statusId, 'isSavingQuotePolicy'], true);
}
} else if (setStatusQuotePolicy.fulfilled.match(action)) {
const status = state.get(action.payload.id);
if (status) {
return state
.setIn([action.payload.id, 'quote_approval'], action.payload.quote_approval)
.deleteIn([action.payload.id, 'isSavingQuotePolicy']);
}
} else if (setStatusQuotePolicy.rejected.match(action)) {
return state.deleteIn([action.meta.arg.statusId, 'isSavingQuotePolicy']);
}
switch(action.type) {
case STATUS_FETCH_REQUEST:
return state.setIn([action.id, 'isLoading'], true);

View File

@ -40,7 +40,10 @@ interface AppThunkConfig {
fulfilledMeta: AppMeta;
rejectedMeta: AppMeta;
}
type AppThunkApi = Pick<GetThunkAPI<AppThunkConfig>, 'getState' | 'dispatch'>;
export type AppThunkApi = Pick<
GetThunkAPI<AppThunkConfig>,
'getState' | 'dispatch'
>;
interface AppThunkOptions<Arg> {
useLoadingBar?: boolean;

View File

@ -12,7 +12,11 @@ export function isProduction() {
else return import.meta.env.PROD;
}
export type Features = 'modern_emojis';
export type Features =
| 'modern_emojis'
| 'outgoing_quotes'
| 'fasp'
| 'http_message_signatures';
export function isFeatureEnabled(feature: Features) {
return initialState?.features.includes(feature) ?? false;

View File

@ -602,15 +602,12 @@ body > [data-popper-placement] {
&__highlightable {
display: flex;
flex-direction: column;
gap: 16px;
flex: 0 1 auto;
border-radius: 4px;
border: 1px solid var(--background-border-color);
transition: border-color 300ms linear;
min-height: 0;
position: relative;
background: var(--input-background-color);
overflow-y: auto;
&.active {
transition: none;
@ -705,6 +702,8 @@ body > [data-popper-placement] {
display: flex;
align-items: center;
gap: 8px;
margin: 8px;
flex-wrap: wrap;
& > div {
overflow: hidden;
@ -715,6 +714,7 @@ body > [data-popper-placement] {
&__uploads {
padding: 0 12px;
aspect-ratio: 3/2;
flex-shrink: 0;
}
.media-gallery {
@ -813,7 +813,6 @@ body > [data-popper-placement] {
flex-direction: column;
gap: 12px;
padding: 12px;
padding-top: 0;
}
&__submit {
@ -874,6 +873,7 @@ body > [data-popper-placement] {
}
&__poll {
margin-top: 8px;
display: flex;
flex-direction: column;
align-self: stretch;
@ -3518,11 +3518,10 @@ a.account__display-name {
display: flex;
flex-direction: column;
height: calc(100% - 10px);
overflow-y: hidden;
overflow-y: auto;
.compose-form {
flex: 1 1 auto;
min-height: 0;
}
}
@ -5403,7 +5402,8 @@ a.status-card {
}
.privacy-dropdown__dropdown,
.language-dropdown__dropdown {
.language-dropdown__dropdown,
.visibility-dropdown__dropdown {
box-shadow: var(--dropdown-shadow);
background: var(--dropdown-background-color);
backdrop-filter: $backdrop-blur-filter;
@ -5432,7 +5432,8 @@ a.status-card {
z-index: 9999;
}
.privacy-dropdown__option {
.privacy-dropdown__option,
.visibility-dropdown__option {
font-size: 14px;
line-height: 20px;
letter-spacing: 0.25px;
@ -5578,6 +5579,39 @@ a.status-card {
}
}
.visibility-dropdown {
&__overlay[data-popper-placement] {
z-index: 9999;
}
&__label.disabled {
cursor: default;
opacity: 0.5;
}
&__button {
color: $primary-text-color;
background: var(--dropdown-background-color);
border: 1px solid var(--dropdown-border-color);
padding: 8px 12px;
width: 100%;
text-align: left;
border-radius: 4px;
font-size: 14px;
height: 40px;
&:disabled {
cursor: default;
}
}
&__helper {
margin-top: 4px;
font-size: 0.8em;
color: $dark-text-color;
}
}
.search {
margin-bottom: 32px;
position: relative;
@ -5870,6 +5904,17 @@ a.status-card {
}
}
.modal-root label {
cursor: pointer;
display: block;
> span {
display: block;
font-weight: 500;
margin-bottom: 8px;
}
}
.video-modal .video-player {
max-height: 80vh;
max-width: 100vw;
@ -6376,6 +6421,15 @@ a.status-card {
letter-spacing: 0.25px;
overflow-y: auto;
&__description {
margin: 24px 24px 0;
color: $darker-text-color;
a {
color: inherit;
}
}
&__form {
display: flex;
flex-direction: column;

View File

@ -5,7 +5,8 @@
:root {
--dropdown-border-color: #{lighten($ui-base-color, 4%)};
--dropdown-background-color: #{rgba(darken($ui-base-color, 8%), 0.9)};
--dropdown-shadow: 0 20px 25px -5px #{rgba($base-shadow-color, 0.25)},
--dropdown-shadow:
0 20px 25px -5px #{rgba($base-shadow-color, 0.25)},
0 8px 10px -6px #{rgba($base-shadow-color, 0.25)};
--modal-background-color: #{rgba(darken($ui-base-color, 8%), 0.7)};
--modal-background-variant-color: #{rgba($ui-base-color, 0.7)};

View File

@ -71,8 +71,16 @@ class Antispam
end
def report_if_needed!(account)
return if Report.unresolved.exists?(account: Account.representative, target_account: account)
return if system_reports.unresolved.exists?(target_account: account)
Report.create!(account: Account.representative, target_account: account, category: :spam, comment: 'Account automatically reported for posting a banned URL')
system_reports.create!(
category: :spam,
comment: 'Account automatically reported for posting a banned URL',
target_account: account
)
end
def system_reports
Account.representative.reports
end
end

View File

@ -116,7 +116,7 @@ class Account < ApplicationRecord
# Local user validations
validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: USERNAME_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_username? && !actor_type_application? }
validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? && !actor_type_application? }
validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? && !actor_type_application? && !user&.bypass_registration_checks }
validates :display_name, length: { maximum: DISPLAY_NAME_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_display_name? }
validates :note, note_length: { maximum: NOTE_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_note? }
validates :fields, length: { maximum: DEFAULT_FIELDS_SIZE }, if: -> { local? && will_save_change_to_fields? }

View File

@ -10,6 +10,10 @@ module Status::InteractionPolicyConcern
followed: (1 << 3),
}.freeze
included do
before_validation :downgrade_quote_policy, if: -> { local? && !distributable? }
end
def quote_policy_as_keys(kind)
case kind
when :automatic
@ -52,4 +56,8 @@ module Status::InteractionPolicyConcern
:denied
end
def downgrade_quote_policy
self.quote_approval_policy = 0
end
end

View File

@ -7,7 +7,7 @@ module BulkMailingConcern
job_class = ActionMailer::MailDeliveryJob
Sidekiq::Client.push_bulk({
'class' => ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper,
'class' => Sidekiq::ActiveJob::Wrapper,
'wrapped' => job_class,
'queue' => mailer_class.deliver_later_queue_name,
'args' => args_array.map do |args|

View File

@ -190,6 +190,7 @@ nn:
create_relay: Opprett eit relé
create_unavailable_domain: Opprett utilgjengeleg domene
create_user_role: Opprett rolle
create_username_block: Lag regel for brukarnamn
demote_user: Degrader brukar
destroy_announcement: Slett lysinga
destroy_canonical_email_block: Fjern e-postblokkering
@ -203,6 +204,7 @@ nn:
destroy_status: Slett status
destroy_unavailable_domain: Slett utilgjengeleg domene
destroy_user_role: Øydelegg rolle
destroy_username_block: Slett regel for brukarnamn
disable_2fa_user: Skruv av 2FA
disable_custom_emoji: Skruv av tilpassa emoji
disable_relay: Skru av reléet
@ -237,6 +239,7 @@ nn:
update_report: Oppdater rapport
update_status: Oppdater tut
update_user_role: Oppdater rolla
update_username_block: Oppdater regel for brukarnamn
actions:
approve_appeal_html: "%{name} godkjende klagen frå %{target} på modereringa"
approve_user_html: "%{name} godkjende registreringa til %{target}"
@ -255,6 +258,7 @@ nn:
create_relay_html: "%{name} laga reléet %{target}"
create_unavailable_domain_html: "%{name} stogga levering til domenet %{target}"
create_user_role_html: "%{name} oppretta rolla %{target}"
create_username_block_html: "%{name} laga ein regel for brukarnamn som inneheld %{target}"
demote_user_html: "%{name} degraderte brukaren %{target}"
destroy_announcement_html: "%{name} sletta kunngjeringa %{target}"
destroy_canonical_email_block_html: "%{name} avblokkerte e-post med hash %{target}"
@ -268,6 +272,7 @@ nn:
destroy_status_html: "%{name} fjerna innlegget frå %{target}"
destroy_unavailable_domain_html: "%{name} tok opp att levering til domenet %{target}"
destroy_user_role_html: "%{name} sletta rolla %{target}"
destroy_username_block_html: "%{name} fjerna regelen for brukarnamn som inneheld %{target}"
disable_2fa_user_html: "%{name} tok vekk krav om tofaktorautentisering for brukaren %{target}"
disable_custom_emoji_html: "%{name} deaktiverte emojien %{target}"
disable_relay_html: "%{name} skrudde av reléet %{target}"
@ -302,6 +307,7 @@ nn:
update_report_html: "%{name} oppdaterte rapporten %{target}"
update_status_html: "%{name} oppdaterte innlegg av %{target}"
update_user_role_html: "%{name} endret %{target} -rolle"
update_username_block_html: "%{name} oppdaterte regelen for brukarnamn som inneheld %{target}"
deleted_account: sletta konto
empty: Ingen loggar funne.
filter_by_action: Sorter etter handling
@ -1085,6 +1091,25 @@ nn:
other: Brukt av %{count} personer i løpet av den seneste uken
title: Anbefalingar og trendar
trending: Trender
username_blocks:
add_new: Lag ny
block_registrations: Blokker registreringar
comparison:
contains: Inneheld
equals: Er lik
contains_html: Inneheld %{string}
created_msg: Laga regelen for brukarnamn
delete: Slett
edit:
title: Rediger regelen for brukarnamn
matches_exactly_html: Er lik %{string}
new:
create: Lag regel
title: Lag ein ny regel for brukarnamn
no_username_block_selected: Ingen brukarnamnreglar vart endra fordi du ikkje valde nokon
not_permitted: Ikkje tillate
title: Reglar for brukarnamn
updated_msg: Oppdaterte regelen for brukarnamn
warning_presets:
add_new: Legg til ny
delete: Slett
@ -1349,6 +1374,10 @@ nn:
basic_information: Grunnleggande informasjon
hint_html: "<strong>Tilpass kva folk ser på den offentlege profilen din og ved sida av innlegga dine.</strong> Andre vil i større grad fylgja og samhandla med deg når du har eit profilbilete og har fyllt ut profilen din."
other: Anna
emoji_styles:
auto: Auto
native: Innebygd
twemoji: Twemoji
errors:
'400': Søknaden du sende var ugyldig eller sett opp feil.
'403': Du har ikkje løyve til å sjå denne sida.
@ -1658,6 +1687,10 @@ nn:
title: Ny omtale
poll:
subject: Meiningsmålinga frå %{name} er avslutta
quote:
body: 'Innlegget ditt vart sitert av %{name}:'
subject: "%{name} siterte innlegget ditt"
title: Nytt sitat
reblog:
body: 'Statusen din vart framheva av %{name}:'
subject: "%{name} framheva statusen din"
@ -1868,6 +1901,7 @@ nn:
edited_at_html: Redigert %{date}
errors:
in_reply_not_found: Det ser ut til at tutet du freistar å svara ikkje finst.
quoted_status_not_found: Innlegget du prøver å sitera ser ikkje ut til å finnast.
over_character_limit: øvregrensa for teikn, %{max}, er nådd
pin_errors:
direct: Innlegg som bare er synlige for nevnte brukere kan ikke festes
@ -1875,6 +1909,8 @@ nn:
ownership: Du kan ikkje festa andre sine tut
reblog: Ei framheving kan ikkje festast
quote_policies:
followers: Berre dei som fylgjer deg
nobody: Ingen
public: Alle
title: "%{name}: «%{quote}»"
visibilities:

View File

@ -56,10 +56,12 @@ nn:
scopes: API-ane som programmet vil få tilgjenge til. Ettersom du vel eit toppnivåomfang tarv du ikkje velja einskilde API-ar.
setting_aggregate_reblogs: Ikkje vis nye framhevingar for tut som nyleg har vorte heva fram (Påverkar berre nylege framhevingar)
setting_always_send_emails: Vanlegvis vil ikkje e-postvarsel bli sendt når du brukar Mastodon aktivt
setting_default_quote_policy: Denne innstillinga har berre verknad for innlegg som er laga med den neste utgåva av Mastodon, men du kan velja kva du vil ha i førebuingane.
setting_default_sensitive: Sensitive media vert gøymde som standard, og du syner dei ved å klikka på dei
setting_display_media_default: Gøym media som er merka som sensitive
setting_display_media_hide_all: Alltid skjul alt media
setting_display_media_show_all: Vis alltid media
setting_emoji_style: Korleis du skal visa smilefjes. «Auto» prøver å visa innebygde smilefjes, men bruker Twemoji som reserveløysing for eldre nettlesarar.
setting_system_scrollbars_ui: Gjeld berre skrivebordsnettlesarar som er bygde på Safari og Chrome
setting_use_blurhash: Overgangar er basert på fargane til skjulte grafikkelement, men gjer detaljar utydelege
setting_use_pending_items: Gøym tidslineoppdateringar bak eit klikk, i staden for å rulla ned automatisk
@ -148,6 +150,9 @@ nn:
min_age: Skal ikkje vere under minstealder som krevst av lover i jurisdiksjonen din.
user:
chosen_languages: Når merka vil berre tuta på dei valde språka synast på offentlege tidsliner
date_of_birth:
one: Me må sikra oss at du er minst %{count} for å bruka %{domain}. Me lagrar ikkje dette.
other: Me må sikra oss at du er minst %{count} for å bruka %{domain}. Me lagrar ikkje dette.
role: Rolla kontrollerer kva løyve brukaren har.
user_role:
color: Fargen som skal nyttast for denne rolla i heile brukargrensesnittet, som RGB i hex-format
@ -155,6 +160,10 @@ nn:
name: Offentleg namn på rolla, dersom rolla skal visast som eit emblem
permissions_as_keys: Brukarar med denne rolla vil ha tilgang til...
position: Høgare rolle avgjer konfliktløysing i visse situasjonar. Visse handlingar kan berre utførast på roller med lågare prioritet
username_block:
allow_with_approval: I staden for å hindra registreringar i det heile, må du godkjenna registreringar som passar
comparison: Ver merksam på Scunthorpe-problemet når du blokkerer delvise treff
username: Vil passa uansett store og små bokstavar og vanlege homoglyfar som «4» for «a» eller «3» for «e»
webhook:
events: Vel hendingar å senda
template: Skriv di eiga JSON nyttelast ved å bruka variabel interpolering. La stå tom for standard JSON.
@ -237,6 +246,7 @@ nn:
setting_display_media_default: Standard
setting_display_media_hide_all: Gøym alle
setting_display_media_show_all: Vis alle
setting_emoji_style: Stil for smilefjes
setting_expand_spoilers: Vid alltid ut tut som er merka med innhaldsåtvaringar
setting_hide_network: Gøym nettverket ditt
setting_missing_alt_text_modal: Vis stadfestingsdialog før du legg ut media utan alt-tekst
@ -319,6 +329,7 @@ nn:
follow_request: Send e-post når nokon spør om å fylgja deg
mention: Send e-post når nokon nemner deg
pending_account: Send e-post når ein ny konto treng gjennomgang
quote: Nokon siterte deg
reblog: Send e-post når nokon framhevar statusen din
report: Ny rapport er sendt
software_updates:
@ -365,6 +376,10 @@ nn:
name: Namn
permissions_as_keys: Løyve
position: Prioritet
username_block:
allow_with_approval: Tillat registreringar med godkjenning
comparison: Samanlikningsmetode
username: Ord som skal passa
webhook:
events: Aktiverte hendingar
template: Nyttelastmal

View File

@ -39,6 +39,8 @@ namespace :api, format: false do
resource :history, only: :show
resource :source, only: :show
resource :interaction_policy, only: :update
post :translate, to: 'translations#create'
end

View File

@ -47,7 +47,7 @@
"@gamestdio/websocket": "^0.3.2",
"@github/webauthn-json": "^2.1.1",
"@optimize-lodash/rollup-plugin": "^5.0.2",
"@rails/ujs": "7.1.501",
"@rails/ujs": "7.1.502",
"@react-spring/web": "^9.7.5",
"@reduxjs/toolkit": "^2.0.1",
"@use-gesture/react": "^10.3.1",
@ -170,7 +170,7 @@
"eslint-import-resolver-typescript": "^4.2.5",
"eslint-plugin-formatjs": "^5.3.1",
"eslint-plugin-import": "~2.32.0",
"eslint-plugin-jsdoc": "^53.0.0",
"eslint-plugin-jsdoc": "^54.0.0",
"eslint-plugin-jsx-a11y": "~6.10.2",
"eslint-plugin-promise": "~7.2.1",
"eslint-plugin-react": "^7.37.4",

47
spec/lib/antispam_spec.rb Normal file
View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Antispam do
describe '#local_preflight_check!' do
subject { described_class.new.local_preflight_check!(status) }
let(:status) { Fabricate :status }
context 'when there is no spammy text registered' do
it { is_expected.to be_nil }
end
context 'with spammy text' do
before { redis.sadd 'antispam:spammy_texts', 'https://banned.example' }
context 'when status matches' do
let(:status) { Fabricate :status, text: 'I use https://banned.example urls in my text' }
it 'raises error and reports' do
expect { subject }
.to raise_error(described_class::SilentlyDrop)
.and change(spam_reports, :count).by(1)
end
context 'when report already exists' do
before { Fabricate :report, account: Account.representative, target_account: status.account }
it 'raises error and does not report' do
expect { subject }
.to raise_error(described_class::SilentlyDrop)
.and not_change(spam_reports, :count)
end
end
def spam_reports
Account.representative.reports.where(target_account: status.account).spam
end
end
context 'when status does not match' do
it { is_expected.to be_nil }
end
end
end
end

View File

@ -32,6 +32,7 @@ RSpec.describe Mastodon::CLI::Accounts do
describe '#create' do
let(:action) { :create }
let(:username) { 'tootctl_username' }
shared_examples 'a new user with given email address and username' do
it 'creates user and accounts from options and displays success message' do
@ -48,18 +49,24 @@ RSpec.describe Mastodon::CLI::Accounts do
end
def account_from_options
Account.find_local('tootctl_username')
Account.find_local(username)
end
end
context 'when required USERNAME and --email are provided' do
let(:arguments) { ['tootctl_username'] }
let(:arguments) { [username] }
context 'with USERNAME and --email only' do
let(:options) { { email: 'tootctl@example.com' } }
it_behaves_like 'a new user with given email address and username'
context 'with a reserved username' do
let(:username) { 'security' }
it_behaves_like 'a new user with given email address and username'
end
context 'with invalid --email value' do
let(:options) { { email: 'invalid' } }

View File

@ -0,0 +1,120 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Interaction policies', feature: :outgoing_quotes do
let(:user) { Fabricate(:user) }
let(:scopes) { 'write:statuses' }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
let(:status) { Fabricate(:status, account: user.account) }
let(:params) { { quote_approval_policy: 'followers' } }
describe 'PUT /api/v1/statuses/:status_id/interaction_policy' do
subject do
put "/api/v1/statuses/#{status.id}/interaction_policy", headers: headers, params: params
end
it_behaves_like 'forbidden for wrong scope', 'read read:statuses'
context 'without an authorization header' do
let(:headers) { {} }
it 'returns http unauthorized' do
expect { subject }
.to_not(change { status.reload.quote_approval_policy })
expect(response).to have_http_status(401)
expect(response.content_type)
.to start_with('application/json')
end
end
context 'with a status from a different user' do
let(:status) { Fabricate(:status) }
it 'returns http unauthorized' do
expect { subject }
.to_not(change { status.reload.quote_approval_policy })
expect(response).to have_http_status(403)
expect(response.content_type)
.to start_with('application/json')
end
end
context 'when changing the interaction policy' do
it 'changes the interaction policy, returns the updated status, and schedules distribution jobs' do
expect { subject }
.to change { status.reload.quote_approval_policy }.to(Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16)
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
expect(response.parsed_body).to include(
'quote_approval' => match(
'automatic' => ['followers'],
'manual' => [],
'current_user' => 'automatic'
)
)
expect(DistributionWorker)
.to have_enqueued_sidekiq_job(status.id, { 'update' => true })
expect(ActivityPub::StatusUpdateDistributionWorker)
.to have_enqueued_sidekiq_job(status.id)
end
end
context 'when not changing the interaction policy' do
let(:params) { { quote_approval_policy: 'nobody' } }
it 'keeps the interaction policy, returns the status, and does not schedule distribution jobs' do
expect { subject }
.to_not(change { status.reload.quote_approval_policy }.from(0))
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
expect(response.parsed_body).to include(
'quote_approval' => match(
'automatic' => [],
'manual' => [],
'current_user' => 'automatic'
)
)
expect(DistributionWorker)
.to_not have_enqueued_sidekiq_job
expect(ActivityPub::StatusUpdateDistributionWorker)
.to_not have_enqueued_sidekiq_job
end
end
context 'when trying to change the interaction policy of a private post' do
let(:status) { Fabricate(:status, account: user.account, visibility: :private) }
let(:params) { { quote_approval_policy: 'public' } }
it 'keeps the interaction policy, returns the status, and does not schedule distribution jobs' do
expect { subject }
.to_not(change { status.reload.quote_approval_policy }.from(0))
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
expect(response.parsed_body).to include(
'quote_approval' => match(
'automatic' => [],
'manual' => [],
'current_user' => 'automatic'
)
)
expect(DistributionWorker)
.to_not have_enqueued_sidekiq_job
expect(ActivityPub::StatusUpdateDistributionWorker)
.to_not have_enqueued_sidekiq_job
end
end
end
end

View File

@ -23,7 +23,7 @@ RSpec.describe 'Share page', :js, :streaming do
fill_in_form
expect(page)
.to have_css('.notification-bar-message', text: frontend_translations('compose.published.body'))
.to have_current_path(%r{/@bob/[0-9]+})
end
def fill_in_form

369
yarn.lock
View File

@ -1295,7 +1295,7 @@ __metadata:
languageName: node
linkType: hard
"@csstools/media-query-list-parser@npm:^4.0.2, @csstools/media-query-list-parser@npm:^4.0.3":
"@csstools/media-query-list-parser@npm:^4.0.3":
version: 4.0.3
resolution: "@csstools/media-query-list-parser@npm:4.0.3"
peerDependencies:
@ -2262,17 +2262,14 @@ __metadata:
linkType: hard
"@formatjs/cli@npm:^6.1.1":
version: 6.3.14
resolution: "@formatjs/cli@npm:6.3.14"
version: 6.7.2
resolution: "@formatjs/cli@npm:6.7.2"
peerDependencies:
"@glimmer/env": ^0.1.7
"@glimmer/reference": ^0.91.1 || ^0.92.0 || ^0.93.0
"@glimmer/syntax": ^0.92.0 || ^0.93.0
"@glimmer/validator": ^0.92.0 || ^0.93.0
"@vue/compiler-core": ^3.4.0
content-tag: ^2.0.1 || ^3.0.0
ember-template-recast: ^6.1.4
vue: ^3.4.0
"@glimmer/syntax": ^0.94.9
"@vue/compiler-core": ^3.5.12
content-tag: ^3.0.0
ember-template-recast: ^6.1.5
vue: ^3.5.12
peerDependenciesMeta:
"@glimmer/env":
optional: true
@ -2292,7 +2289,7 @@ __metadata:
optional: true
bin:
formatjs: bin/formatjs
checksum: 10c0/b4c83ed7fdc8dcd48b2f48fa9cca65b52472fb096eb028517a872f8a71ed3964f4b0a6bbc607f821a9504f396fe7341ef4d9ad44a381a37f280ed7547de66f41
checksum: 10c0/fbcb1d35915a5b1542e4fa3f3f79e23fa5988681e6c238d7e8a3d4d32e18df3e5c36cebe01b04e011bb5c91f96f4d08d36af750259aa799d3e81943084e2f0c2
languageName: node
linkType: hard
@ -2595,12 +2592,10 @@ __metadata:
languageName: node
linkType: hard
"@keyv/serialize@npm:^1.0.3":
version: 1.0.3
resolution: "@keyv/serialize@npm:1.0.3"
dependencies:
buffer: "npm:^6.0.3"
checksum: 10c0/24a257870b0548cfe430680c2ae1641751e6a6ec90c573eaf51bfe956839b6cfa462b4d2827157363b6d620872d32d69fa2f37210a864ba488f8ec7158436398
"@keyv/serialize@npm:^1.1.0":
version: 1.1.0
resolution: "@keyv/serialize@npm:1.1.0"
checksum: 10c0/30e34adf4fff52374c2c531e3ff215eed6414350ee56eebcb98c422feaff171b4900c73082a72399a6bfbc5ce60fbb6f968594110c960521923499146bc68c20
languageName: node
linkType: hard
@ -2618,7 +2613,7 @@ __metadata:
"@gamestdio/websocket": "npm:^0.3.2"
"@github/webauthn-json": "npm:^2.1.1"
"@optimize-lodash/rollup-plugin": "npm:^5.0.2"
"@rails/ujs": "npm:7.1.501"
"@rails/ujs": "npm:7.1.502"
"@react-spring/web": "npm:^9.7.5"
"@reduxjs/toolkit": "npm:^2.0.1"
"@storybook/addon-a11y": "npm:^9.1.1"
@ -2680,7 +2675,7 @@ __metadata:
eslint-import-resolver-typescript: "npm:^4.2.5"
eslint-plugin-formatjs: "npm:^5.3.1"
eslint-plugin-import: "npm:~2.32.0"
eslint-plugin-jsdoc: "npm:^53.0.0"
eslint-plugin-jsdoc: "npm:^54.0.0"
eslint-plugin-jsx-a11y: "npm:~6.10.2"
eslint-plugin-promise: "npm:~7.2.1"
eslint-plugin-react: "npm:^7.37.4"
@ -3109,10 +3104,10 @@ __metadata:
languageName: node
linkType: hard
"@rails/ujs@npm:7.1.501":
version: 7.1.501
resolution: "@rails/ujs@npm:7.1.501"
checksum: 10c0/b75a30f36ff219264e0926da1ffcd14c2a5d6aee5be29da4dc81f9a45843875da79ac19cf7ed9a3f11a39084398d0ae4a75a8edb28ba94907db3081572af62b0
"@rails/ujs@npm:7.1.502":
version: 7.1.502
resolution: "@rails/ujs@npm:7.1.502"
checksum: 10c0/79b46e8abd03e3fc633d93cc4e4c23838c628b775802fb38c2ce68b0e609ce287a67b81db112a93cc78c07ec82ca3b4cf87e74eb556d35148ce5f64c8be9201f
languageName: node
linkType: hard
@ -3476,37 +3471,37 @@ __metadata:
linkType: hard
"@storybook/addon-a11y@npm:^9.1.1":
version: 9.1.1
resolution: "@storybook/addon-a11y@npm:9.1.1"
version: 9.1.2
resolution: "@storybook/addon-a11y@npm:9.1.2"
dependencies:
"@storybook/global": "npm:^5.0.0"
axe-core: "npm:^4.2.0"
peerDependencies:
storybook: ^9.1.1
checksum: 10c0/bf5eba0a51ffec20c8c4432985494295115bcf48e0807e4ca21314845d4aaaaaae9122d4be4f78a2fc4c15caa5e1207c01e118724c2cbecbd80aa8a5f6826924
storybook: ^9.1.2
checksum: 10c0/36fc399db0af0acff6542c7e2aa54ef715dcff0e8a7f12fec3468dfdee2d83651c1d02c7226a420269d18f522dbaa96fa6faacb9c647c2a65518cece9d38582b
languageName: node
linkType: hard
"@storybook/addon-docs@npm:^9.1.1":
version: 9.1.1
resolution: "@storybook/addon-docs@npm:9.1.1"
version: 9.1.2
resolution: "@storybook/addon-docs@npm:9.1.2"
dependencies:
"@mdx-js/react": "npm:^3.0.0"
"@storybook/csf-plugin": "npm:9.1.1"
"@storybook/csf-plugin": "npm:9.1.2"
"@storybook/icons": "npm:^1.4.0"
"@storybook/react-dom-shim": "npm:9.1.1"
"@storybook/react-dom-shim": "npm:9.1.2"
react: "npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
react-dom: "npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
ts-dedent: "npm:^2.0.0"
peerDependencies:
storybook: ^9.1.1
checksum: 10c0/92b3ac089a38b892319de5ec02ca4ae477e38d88aef8561f6cbb1f15dd69ad856c194fca4514c783983baba063f428994a7d7f2aed98204b931f8e684e681194
storybook: ^9.1.2
checksum: 10c0/b17a3a8d3b9ad70f7cd8f8295f8cf7a10a6c39ab69e752f3acfb2260809055f85088a6382a2fc729b48860854b94a67faca239ff00bbe0e7e9553113cb2542fb
languageName: node
linkType: hard
"@storybook/addon-vitest@npm:^9.1.1":
version: 9.1.1
resolution: "@storybook/addon-vitest@npm:9.1.1"
version: 9.1.2
resolution: "@storybook/addon-vitest@npm:9.1.2"
dependencies:
"@storybook/global": "npm:^5.0.0"
"@storybook/icons": "npm:^1.4.0"
@ -3515,7 +3510,7 @@ __metadata:
peerDependencies:
"@vitest/browser": ^3.0.0
"@vitest/runner": ^3.0.0
storybook: ^9.1.1
storybook: ^9.1.2
vitest: ^3.0.0
peerDependenciesMeta:
"@vitest/browser":
@ -3524,31 +3519,31 @@ __metadata:
optional: true
vitest:
optional: true
checksum: 10c0/a4770aad2f3e4ae10e3d7ae7083354e98287d095644aae87af62c59c9a97ec7e57cf25620c32e2e5f9261a3686a6efabea0a830061500f63e78a49a2bac6f130
checksum: 10c0/75eacf6757d9ab6d0ad8c496d55a1548ab67f098a7ceb431900e8b6eb98ac8ac2235382a44a26765607be07e1b09c0e2a34ee9b846c234da6073d38aabc0ea4d
languageName: node
linkType: hard
"@storybook/builder-vite@npm:9.1.1":
version: 9.1.1
resolution: "@storybook/builder-vite@npm:9.1.1"
"@storybook/builder-vite@npm:9.1.2":
version: 9.1.2
resolution: "@storybook/builder-vite@npm:9.1.2"
dependencies:
"@storybook/csf-plugin": "npm:9.1.1"
"@storybook/csf-plugin": "npm:9.1.2"
ts-dedent: "npm:^2.0.0"
peerDependencies:
storybook: ^9.1.1
storybook: ^9.1.2
vite: ^5.0.0 || ^6.0.0 || ^7.0.0
checksum: 10c0/334235a64e05d6fb1e1cdf23f41ac211d1e55429e425e1aea33d9b4469503aa328eb5d9cad24e6328b392098afb2c884943d2c304c1eb3cf55ce25dcc3ad4414
checksum: 10c0/2411e593903bc61336f2a2c6f48e7314dcc8c776346eff0f6fec28e9fc8e3a90d3f8d6561f30d1caf490349d34c7690f8addf4c56fa1fd778f0dfda49cf3aa97
languageName: node
linkType: hard
"@storybook/csf-plugin@npm:9.1.1":
version: 9.1.1
resolution: "@storybook/csf-plugin@npm:9.1.1"
"@storybook/csf-plugin@npm:9.1.2":
version: 9.1.2
resolution: "@storybook/csf-plugin@npm:9.1.2"
dependencies:
unplugin: "npm:^1.3.1"
peerDependencies:
storybook: ^9.1.1
checksum: 10c0/d29b5685ef79eacbcd891977f95a58238f104004b014f88ee59eab6e5995df31010435aa222a27cdf54056accc43239c44f7e8e461c263c60b09d7d2383be8b8
storybook: ^9.1.2
checksum: 10c0/a145da545844b9b2af345d43d8f2c035dd801bd6414b4a9a2037dfa950250d08133a956226c49c36a79ffda171ad9388a0f1621c04cfed77e5c342817f4a275e
languageName: node
linkType: hard
@ -3569,25 +3564,25 @@ __metadata:
languageName: node
linkType: hard
"@storybook/react-dom-shim@npm:9.1.1":
version: 9.1.1
resolution: "@storybook/react-dom-shim@npm:9.1.1"
"@storybook/react-dom-shim@npm:9.1.2":
version: 9.1.2
resolution: "@storybook/react-dom-shim@npm:9.1.2"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
storybook: ^9.1.1
checksum: 10c0/ea2725719e04871b56c0a3755a7791034abd1d8ebb51392ed5f1eb2d21aee873444080509efe69c919e43121c299e06e2266d603478d059efe38762b0ae96ae5
storybook: ^9.1.2
checksum: 10c0/7547cb0fdcf8098c00017cbfb501f11a34ae73b9e13984520b8143e709b4b8ec1acf7fed9ce51dbb5b5af5dcd657396da17ef1f262f60efdd4956f3e26b3c704
languageName: node
linkType: hard
"@storybook/react-vite@npm:^9.1.1":
version: 9.1.1
resolution: "@storybook/react-vite@npm:9.1.1"
version: 9.1.2
resolution: "@storybook/react-vite@npm:9.1.2"
dependencies:
"@joshwooding/vite-plugin-react-docgen-typescript": "npm:0.6.1"
"@rollup/pluginutils": "npm:^5.0.2"
"@storybook/builder-vite": "npm:9.1.1"
"@storybook/react": "npm:9.1.1"
"@storybook/builder-vite": "npm:9.1.2"
"@storybook/react": "npm:9.1.2"
find-up: "npm:^7.0.0"
magic-string: "npm:^0.30.0"
react-docgen: "npm:^8.0.0"
@ -3596,27 +3591,27 @@ __metadata:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
storybook: ^9.1.1
storybook: ^9.1.2
vite: ^5.0.0 || ^6.0.0 || ^7.0.0
checksum: 10c0/fa79e5a9be38f229a8cc0db85452e651081740cd3f9e6951c0fc3e195437124f8a792494b90a613c673ef9410d565d5f459276e157f073367ea28f0293d10167
checksum: 10c0/afed36a0219599577b255042a9c9ac1af0106003ac37e2e9b5846a42b4e8729ff0e8b7ae6018d3ac85b69e918c2a20d554cd484de7345e5fb4974df92914e059
languageName: node
linkType: hard
"@storybook/react@npm:9.1.1":
version: 9.1.1
resolution: "@storybook/react@npm:9.1.1"
"@storybook/react@npm:9.1.2":
version: 9.1.2
resolution: "@storybook/react@npm:9.1.2"
dependencies:
"@storybook/global": "npm:^5.0.0"
"@storybook/react-dom-shim": "npm:9.1.1"
"@storybook/react-dom-shim": "npm:9.1.2"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
storybook: ^9.1.1
storybook: ^9.1.2
typescript: ">= 4.9.x"
peerDependenciesMeta:
typescript:
optional: true
checksum: 10c0/343598e9c1bb2e53b7d6eaaa50b5c024c109fb825f6f4869a82da0ccd428400e5a93eba494e810f6bfbf6bff9b73810595ab8ffdd1530df8b4f2b20e617623e3
checksum: 10c0/ea3d9fa25825fde5022942579db9a57154e57cb37244b0d54bb189679a37f20c20906041898f5fcfd4867043ea789384c2d968f334f9d0c55958add0b18fb6ea
languageName: node
linkType: hard
@ -5402,13 +5397,6 @@ __metadata:
languageName: node
linkType: hard
"base64-js@npm:^1.3.1":
version: 1.5.1
resolution: "base64-js@npm:1.5.1"
checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf
languageName: node
linkType: hard
"better-opn@npm:^3.0.2":
version: 3.0.2
resolution: "better-opn@npm:3.0.2"
@ -5528,16 +5516,6 @@ __metadata:
languageName: node
linkType: hard
"buffer@npm:^6.0.3":
version: 6.0.3
resolution: "buffer@npm:6.0.3"
dependencies:
base64-js: "npm:^1.3.1"
ieee754: "npm:^1.2.1"
checksum: 10c0/2a905fbbcde73cc5d8bd18d1caa23715d5f83a5935867c2329f0ac06104204ba7947be098fe1317fbd8830e26090ff8e764f08cd14fefc977bb248c3487bcbd0
languageName: node
linkType: hard
"bufferutil@npm:^4.0.7":
version: 4.0.9
resolution: "bufferutil@npm:4.0.9"
@ -5582,13 +5560,13 @@ __metadata:
languageName: node
linkType: hard
"cacheable@npm:^1.9.0":
version: 1.9.0
resolution: "cacheable@npm:1.9.0"
"cacheable@npm:^1.10.3":
version: 1.10.3
resolution: "cacheable@npm:1.10.3"
dependencies:
hookified: "npm:^1.8.2"
keyv: "npm:^5.3.3"
checksum: 10c0/1ca171dd2f7c3a929d84b3f94e712cb8fbbfb7c636f19ba01657cf89c6ea4316f21f2beb6c696fa5c87d42f52620f4a7c2dfecb41bf71ee3b249d539895256c4
hookified: "npm:^1.10.0"
keyv: "npm:^5.4.0"
checksum: 10c0/eaa483140133b58dbd5c9811688137016c263a874886ce98f9590d252fb92859633929b36aa4c05ec67aee70cc1c9ba9aa1be02e53365604dd0202a88e44fef8
languageName: node
linkType: hard
@ -5678,10 +5656,10 @@ __metadata:
languageName: node
linkType: hard
"chalk@npm:^5.4.1":
version: 5.4.1
resolution: "chalk@npm:5.4.1"
checksum: 10c0/b23e88132c702f4855ca6d25cb5538b1114343e41472d5263ee8a37cccfccd9c4216d111e1097c6a27830407a1dc81fecdf2a56f2c63033d4dbbd88c10b0dcef
"chalk@npm:^5.5.0":
version: 5.5.0
resolution: "chalk@npm:5.5.0"
checksum: 10c0/23063b544f7c2fe57d25ff814807de561f8adfff72e4f0051051eaa606f772586470507ccd38d89166300eeaadb0164acde8bb8a0716a0f2d56ccdf3761d5e4f
languageName: node
linkType: hard
@ -5864,10 +5842,10 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:^13.1.0":
version: 13.1.0
resolution: "commander@npm:13.1.0"
checksum: 10c0/7b8c5544bba704fbe84b7cab2e043df8586d5c114a4c5b607f83ae5060708940ed0b5bd5838cf8ce27539cde265c1cbd59ce3c8c6b017ed3eec8943e3a415164
"commander@npm:^14.0.0":
version: 14.0.0
resolution: "commander@npm:14.0.0"
checksum: 10c0/73c4babfa558077868d84522b11ef56834165d472b9e86a634cd4c3ae7fc72d59af6377d8878e06bd570fe8f3161eced3cbe383c38f7093272bb65bd242b595b
languageName: node
linkType: hard
@ -6217,7 +6195,7 @@ __metadata:
languageName: node
linkType: hard
"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0, debug@npm:^4.4.1":
"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.6, debug@npm:^4.4.1":
version: 4.4.1
resolution: "debug@npm:4.4.1"
dependencies:
@ -6986,9 +6964,9 @@ __metadata:
languageName: node
linkType: hard
"eslint-plugin-jsdoc@npm:^53.0.0":
version: 53.0.1
resolution: "eslint-plugin-jsdoc@npm:53.0.1"
"eslint-plugin-jsdoc@npm:^54.0.0":
version: 54.0.0
resolution: "eslint-plugin-jsdoc@npm:54.0.0"
dependencies:
"@es-joy/jsdoccomment": "npm:~0.52.0"
are-docs-informative: "npm:^0.0.2"
@ -7002,7 +6980,7 @@ __metadata:
spdx-expression-parse: "npm:^4.0.0"
peerDependencies:
eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
checksum: 10c0/d629863faa98026edc09535e8095669d97be4e77173bfe6cbe7ea0fcd641cd2537bec58e25db433cc6f4103d9a2a13c1bbb3916a8ed4ff05b18c566971dec472
checksum: 10c0/cf0a388fc670ababe26f9584c467bc8c1592aa83affcf16118d8181c186a6d8f02a8ea65250766b45168fca5cb879a6af66e8457cdb98f0f923bd927572e2de5
languageName: node
linkType: hard
@ -7309,9 +7287,9 @@ __metadata:
linkType: hard
"fake-indexeddb@npm:^6.0.1":
version: 6.0.1
resolution: "fake-indexeddb@npm:6.0.1"
checksum: 10c0/60f4ccdfd5ecb37bb98019056c688366847840cce7146e0005c5ca54823238455403b0a8803b898a11cf80f6147b1bb553457c6af427a644a6e64566cdbe42ec
version: 6.1.0
resolution: "fake-indexeddb@npm:6.1.0"
checksum: 10c0/f2bae6cf3ed38619ccc536ee1c0d72a1ba721da24d840e9c0993800c031a5c1d8e61adc00ea31f0c2a2380447c57bc953bd4128b08dabf559c7cdf03c8893239
languageName: node
linkType: hard
@ -7412,12 +7390,12 @@ __metadata:
languageName: node
linkType: hard
"file-entry-cache@npm:^10.0.8":
version: 10.1.0
resolution: "file-entry-cache@npm:10.1.0"
"file-entry-cache@npm:^10.1.3":
version: 10.1.3
resolution: "file-entry-cache@npm:10.1.3"
dependencies:
flat-cache: "npm:^6.1.9"
checksum: 10c0/9464848577f68809237ffdf11c09a285d930ed4cda8cf392ee44ac8fa70658e41bbe60d08d883285d6af798a26f008dd8dfa94a31d42ecf1ba5a7bcd6dd8b07d
flat-cache: "npm:^6.1.12"
checksum: 10c0/7365c3358698f5ccf085c164989ad48f1d9341157895577d7c34bf4f9c258d2410f4d2c749c73232111aab9e2fdd632ef6941f2c2d3acdd3a7f3daf2c840bd54
languageName: node
linkType: hard
@ -7501,14 +7479,14 @@ __metadata:
languageName: node
linkType: hard
"flat-cache@npm:^6.1.9":
version: 6.1.9
resolution: "flat-cache@npm:6.1.9"
"flat-cache@npm:^6.1.12":
version: 6.1.12
resolution: "flat-cache@npm:6.1.12"
dependencies:
cacheable: "npm:^1.9.0"
cacheable: "npm:^1.10.3"
flatted: "npm:^3.3.3"
hookified: "npm:^1.8.2"
checksum: 10c0/ca9241fab68154e9a4efe8875beff43cb7b2de65628d15dcf8488dc69bca3f8e98085a707c3395d03b1022f586364b0f37aa5dd5cc085a8cf7481516757ac864
hookified: "npm:^1.10.0"
checksum: 10c0/9c7e22ebc68edef373170a2171fe4d7d68eecd18953fbd16f5f3e9c32c36491b61ab0468e07242a5bbed58b36d139a41d3c33b23fc013fc535a41b00546c14f0
languageName: node
linkType: hard
@ -8021,10 +7999,10 @@ __metadata:
languageName: node
linkType: hard
"hookified@npm:^1.8.2":
version: 1.9.0
resolution: "hookified@npm:1.9.0"
checksum: 10c0/28145882504e40ef58f328554966520c56dc2aaca92457996a5cd68fda6f92e38d3ca283e7ebbf3d71f36cda887234ce580abfa6532ade906be7810a812f15fa
"hookified@npm:^1.10.0":
version: 1.11.0
resolution: "hookified@npm:1.11.0"
checksum: 10c0/c74d28e90c55247ffc036a5cabd0681e715f50db8c6b1f47e10253b577e355f3dcd71bb96565a23467f72a8695ec2d482e5801e2d9d99ac24bdc179fef635ba0
languageName: node
linkType: hard
@ -8146,13 +8124,6 @@ __metadata:
languageName: node
linkType: hard
"ieee754@npm:^1.2.1":
version: 1.2.1
resolution: "ieee754@npm:1.2.1"
checksum: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb
languageName: node
linkType: hard
"ignore@npm:^5.2.0":
version: 5.3.2
resolution: "ignore@npm:5.3.2"
@ -8160,7 +8131,7 @@ __metadata:
languageName: node
linkType: hard
"ignore@npm:^7.0.0, ignore@npm:^7.0.3":
"ignore@npm:^7.0.0, ignore@npm:^7.0.5":
version: 7.0.5
resolution: "ignore@npm:7.0.5"
checksum: 10c0/ae00db89fe873064a093b8999fe4cc284b13ef2a178636211842cceb650b9c3e390d3339191acb145d81ed5379d2074840cf0c33a20bdbd6f32821f79eb4ad5d
@ -8986,12 +8957,12 @@ __metadata:
languageName: node
linkType: hard
"keyv@npm:^5.3.3":
version: 5.3.3
resolution: "keyv@npm:5.3.3"
"keyv@npm:^5.4.0":
version: 5.5.0
resolution: "keyv@npm:5.5.0"
dependencies:
"@keyv/serialize": "npm:^1.0.3"
checksum: 10c0/6b9064d061784e80a5dc500453b03cacb099f06fddd0346063519371d563a66771237e04467f3387b60d8a33a3c99864288991274921fb1338c6adf638574924
"@keyv/serialize": "npm:^1.1.0"
checksum: 10c0/2db63fd2abcdf71929f032569673b6edd0de111edb012411658e2589dc5f49793a98aecd56c67fafda3f90a31f32e35555a97f8621040728260c66ad8daeea48
languageName: node
linkType: hard
@ -9016,6 +8987,13 @@ __metadata:
languageName: node
linkType: hard
"known-css-properties@npm:^0.37.0":
version: 0.37.0
resolution: "known-css-properties@npm:0.37.0"
checksum: 10c0/e0ec08cae580e8833254b690971f73ec6f78ac461820a3c755b4a0b62c5b871501753b4aa60b30576a0f621ba44b231235cf9f35ab89e2e7de5448dfe0600241
languageName: node
linkType: hard
"lande@npm:^1.0.10":
version: 1.0.10
resolution: "lande@npm:1.0.10"
@ -9073,28 +9051,28 @@ __metadata:
linkType: hard
"lint-staged@npm:^16.0.0":
version: 16.0.0
resolution: "lint-staged@npm:16.0.0"
version: 16.1.5
resolution: "lint-staged@npm:16.1.5"
dependencies:
chalk: "npm:^5.4.1"
commander: "npm:^13.1.0"
debug: "npm:^4.4.0"
chalk: "npm:^5.5.0"
commander: "npm:^14.0.0"
debug: "npm:^4.4.1"
lilconfig: "npm:^3.1.3"
listr2: "npm:^8.3.3"
listr2: "npm:^9.0.1"
micromatch: "npm:^4.0.8"
nano-spawn: "npm:^1.0.0"
nano-spawn: "npm:^1.0.2"
pidtree: "npm:^0.6.0"
string-argv: "npm:^0.3.2"
yaml: "npm:^2.7.1"
yaml: "npm:^2.8.1"
bin:
lint-staged: bin/lint-staged.js
checksum: 10c0/8778dbe7892bbf14e378d612d1649c1e3df38a8ddf14cf35962b6e8a962be72efb1ebb48a697e38366be97d25b8d2599cad3c26ac5afc0d0460452484e27924d
checksum: 10c0/771e7be871f1d74ed09ef4e4eae5f835ed962965db7709be26cccf71bef8fed34f8d5d92f193b2a6fad32c12d955850aa74008e6180fabea8a7a6666cba2ac39
languageName: node
linkType: hard
"listr2@npm:^8.3.3":
version: 8.3.3
resolution: "listr2@npm:8.3.3"
"listr2@npm:^9.0.1":
version: 9.0.1
resolution: "listr2@npm:9.0.1"
dependencies:
cli-truncate: "npm:^4.0.0"
colorette: "npm:^2.0.20"
@ -9102,7 +9080,7 @@ __metadata:
log-update: "npm:^6.1.0"
rfdc: "npm:^1.4.1"
wrap-ansi: "npm:^9.0.0"
checksum: 10c0/0792f8a7fd482fa516e21689e012e07081cab3653172ca606090622cfa0024c784a1eba8095a17948a0e9a4aa98a80f7c9c90f78a0dd35173d6802f9cc123a82
checksum: 10c0/73462e84a3c4f05de5a3cdea5eaa0209c6ab04a2fdb4046545049806e9ba17b6ee84a097ebf7ffc0e903b0f2a9094c0c480cd2f2bb21d7d21e20969e17a3c32b
languageName: node
linkType: hard
@ -9588,8 +9566,8 @@ __metadata:
linkType: hard
"msw@npm:^2.10.2":
version: 2.10.2
resolution: "msw@npm:2.10.2"
version: 2.10.4
resolution: "msw@npm:2.10.4"
dependencies:
"@bundled-es-modules/cookie": "npm:^2.0.1"
"@bundled-es-modules/statuses": "npm:^1.0.1"
@ -9616,7 +9594,7 @@ __metadata:
optional: true
bin:
msw: cli/index.js
checksum: 10c0/fb44961e17e12864b4764b4c015f6ce7c907081f8dcd237ecd635eab00b787847406fbd36a2bcf2ef4c21114a3610ac03c7f93f3080f509a69b0c1c5285fd683
checksum: 10c0/48dff36c7cf8ad504bb8f8a2ff6946cf5727752c140681eb68da00991d9fe56224bace970476771a9fffae136256c389c591d71368a6967d053dbad6b6df3346
languageName: node
linkType: hard
@ -9627,10 +9605,10 @@ __metadata:
languageName: node
linkType: hard
"nano-spawn@npm:^1.0.0":
version: 1.0.1
resolution: "nano-spawn@npm:1.0.1"
checksum: 10c0/e03edc6971f653bc4651f2413b2011772a7c18797c0a4e986ff8eaea3adf4f017697d4d494ffb4ba6bce907b42abbeb0f7f681dbf336c84a324c940fb64c1dec
"nano-spawn@npm:^1.0.2":
version: 1.0.2
resolution: "nano-spawn@npm:1.0.2"
checksum: 10c0/d8cec78f127a44aa5e38be01746b3d963a8dcf8b00b4a05bf259b5369af2225b8c7dc9d12517050b90234e5c3eeea4ece5d18a5f9c6c3462b56f9f595f07e632
languageName: node
linkType: hard
@ -10278,8 +10256,8 @@ __metadata:
linkType: hard
"pino-pretty@npm:^13.0.0":
version: 13.0.0
resolution: "pino-pretty@npm:13.0.0"
version: 13.1.1
resolution: "pino-pretty@npm:13.1.1"
dependencies:
colorette: "npm:^2.0.7"
dateformat: "npm:^4.6.3"
@ -10291,12 +10269,12 @@ __metadata:
on-exit-leak-free: "npm:^2.1.0"
pino-abstract-transport: "npm:^2.0.0"
pump: "npm:^3.0.0"
secure-json-parse: "npm:^2.4.0"
secure-json-parse: "npm:^4.0.0"
sonic-boom: "npm:^4.0.1"
strip-json-comments: "npm:^3.1.1"
strip-json-comments: "npm:^5.0.2"
bin:
pino-pretty: bin.js
checksum: 10c0/015dac25006c1b9820b9e01fccb8a392a019e12b30e6bfc3f3f61ecca8dbabcd000a8f3f64410b620b7f5d08579ba85e6ef137f7fbeaad70d46397a97a5f75ea
checksum: 10c0/845c07afd3d73cb96ad2049cfa7fca12b8280a51e30d6db8b490857690637556bb8e7f05b2fa640b3e4a7edd9b1369110042d670fda743ef98fe3be29876c8c7
languageName: node
linkType: hard
@ -10328,27 +10306,27 @@ __metadata:
languageName: node
linkType: hard
"playwright-core@npm:1.54.1":
version: 1.54.1
resolution: "playwright-core@npm:1.54.1"
"playwright-core@npm:1.54.2":
version: 1.54.2
resolution: "playwright-core@npm:1.54.2"
bin:
playwright-core: cli.js
checksum: 10c0/b821262b024d7753b1bfa71eb2bc99f2dda12a869d175b2e1bc6ac2764bd661baf36d9d42f45caf622854ad7e4a6077b9b57014c74bb5a78fe339c9edf1c9019
checksum: 10c0/44850e20bf35237c8c3dedf1096c655f8af939dde53c5469f72cae3dd744966858a302419b909a73d7a2093323123e7ebcc0fdd55151b4193afb7812c1fd2c88
languageName: node
linkType: hard
"playwright@npm:^1.54.1":
version: 1.54.1
resolution: "playwright@npm:1.54.1"
version: 1.54.2
resolution: "playwright@npm:1.54.2"
dependencies:
fsevents: "npm:2.3.2"
playwright-core: "npm:1.54.1"
playwright-core: "npm:1.54.2"
dependenciesMeta:
fsevents:
optional: true
bin:
playwright: cli.js
checksum: 10c0/c5fedae31a03a1f4c4846569aef3ffb98da23000a4d255abfc8c2ede15b43cc7cd87b80f6fa078666c030373de8103787cf77ef7653ae9458aabbbd4320c2599
checksum: 10c0/6f642fa70179eee5d5bf8a90df2a6147c9638ff926f4f3ad0a0517396b8a3fe00ccebf13377e032a75b3f0b2610ec1562293e0cfc3bde234181c7a50af8af80a
languageName: node
linkType: hard
@ -10772,7 +10750,7 @@ __metadata:
languageName: node
linkType: hard
"postcss@npm:^8.5.3, postcss@npm:^8.5.6":
"postcss@npm:^8.5.6":
version: 8.5.6
resolution: "postcss@npm:8.5.6"
dependencies:
@ -10821,11 +10799,11 @@ __metadata:
linkType: hard
"prettier@npm:^3.3.3":
version: 3.4.2
resolution: "prettier@npm:3.4.2"
version: 3.6.2
resolution: "prettier@npm:3.6.2"
bin:
prettier: bin/prettier.cjs
checksum: 10c0/99e076a26ed0aba4ebc043880d0f08bbb8c59a4c6641cdee6cdadf2205bdd87aa1d7823f50c3aea41e015e99878d37c58d7b5f0e663bba0ef047f94e36b96446
checksum: 10c0/488cb2f2b99ec13da1e50074912870217c11edaddedeadc649b1244c749d15ba94e846423d062e2c4c9ae683e2d65f754de28889ba06e697ac4f988d44f45812
languageName: node
linkType: hard
@ -12030,10 +12008,10 @@ __metadata:
languageName: node
linkType: hard
"secure-json-parse@npm:^2.4.0":
version: 2.7.0
resolution: "secure-json-parse@npm:2.7.0"
checksum: 10c0/f57eb6a44a38a3eeaf3548228585d769d788f59007454214fab9ed7f01fbf2e0f1929111da6db28cf0bcc1a2e89db5219a59e83eeaec3a54e413a0197ce879e4
"secure-json-parse@npm:^4.0.0":
version: 4.0.0
resolution: "secure-json-parse@npm:4.0.0"
checksum: 10c0/1a298cf00e1de91e833cee5eb406d6e77fb2f7eca9bef3902047d49e7f5d3e6c21b5de61ff73466c831e716430bfe87d732a6e645a7dabb5f1e8a8e4d3e15eb4
languageName: node
linkType: hard
@ -12527,8 +12505,8 @@ __metadata:
linkType: hard
"storybook@npm:^9.1.1":
version: 9.1.1
resolution: "storybook@npm:9.1.1"
version: 9.1.2
resolution: "storybook@npm:9.1.2"
dependencies:
"@storybook/global": "npm:^5.0.0"
"@testing-library/jest-dom": "npm:^6.6.3"
@ -12549,7 +12527,7 @@ __metadata:
optional: true
bin:
storybook: ./bin/index.cjs
checksum: 10c0/efd2665547dc6bfd4cabd00ee1a6368c7cec27a3e4c16bc875ae06bb5d9ee74120c11102699165530f31fe8becb47212d80632b1fefcafa60fef94b3cd0bf50f
checksum: 10c0/3a575f94913f9000a3591e5c685f4eabf75fa78ce306f8b0d48e9c72e46028df31f6d15955b8a338be2bf48dadca6550b65782783d8b3cb4b737ba9f3887d007
languageName: node
linkType: hard
@ -12757,6 +12735,13 @@ __metadata:
languageName: node
linkType: hard
"strip-json-comments@npm:^5.0.2":
version: 5.0.3
resolution: "strip-json-comments@npm:5.0.3"
checksum: 10c0/daaf20b29f69fb51112698f4a9a662490dbb78d5baf6127c75a0a83c2ac6c078a8c0f74b389ad5e0519d6fc359c4a57cb9971b1ae201aef62ce45a13247791e0
languageName: node
linkType: hard
"strip-literal@npm:^3.0.0":
version: 3.0.0
resolution: "strip-literal@npm:3.0.0"
@ -12850,12 +12835,12 @@ __metadata:
linkType: hard
"stylelint@npm:^16.19.1":
version: 16.19.1
resolution: "stylelint@npm:16.19.1"
version: 16.23.1
resolution: "stylelint@npm:16.23.1"
dependencies:
"@csstools/css-parser-algorithms": "npm:^3.0.4"
"@csstools/css-tokenizer": "npm:^3.0.3"
"@csstools/media-query-list-parser": "npm:^4.0.2"
"@csstools/css-parser-algorithms": "npm:^3.0.5"
"@csstools/css-tokenizer": "npm:^3.0.4"
"@csstools/media-query-list-parser": "npm:^4.0.3"
"@csstools/selector-specificity": "npm:^5.0.0"
"@dual-bundle/import-meta-resolve": "npm:^4.1.0"
balanced-match: "npm:^2.0.0"
@ -12863,24 +12848,24 @@ __metadata:
cosmiconfig: "npm:^9.0.0"
css-functions-list: "npm:^3.2.3"
css-tree: "npm:^3.1.0"
debug: "npm:^4.3.7"
debug: "npm:^4.4.1"
fast-glob: "npm:^3.3.3"
fastest-levenshtein: "npm:^1.0.16"
file-entry-cache: "npm:^10.0.8"
file-entry-cache: "npm:^10.1.3"
global-modules: "npm:^2.0.0"
globby: "npm:^11.1.0"
globjoin: "npm:^0.1.4"
html-tags: "npm:^3.3.1"
ignore: "npm:^7.0.3"
ignore: "npm:^7.0.5"
imurmurhash: "npm:^0.1.4"
is-plain-object: "npm:^5.0.0"
known-css-properties: "npm:^0.36.0"
known-css-properties: "npm:^0.37.0"
mathml-tag-names: "npm:^2.1.3"
meow: "npm:^13.2.0"
micromatch: "npm:^4.0.8"
normalize-path: "npm:^3.0.0"
picocolors: "npm:^1.1.1"
postcss: "npm:^8.5.3"
postcss: "npm:^8.5.6"
postcss-resolve-nested-selector: "npm:^0.1.6"
postcss-safe-parser: "npm:^7.0.1"
postcss-selector-parser: "npm:^7.1.0"
@ -12893,7 +12878,7 @@ __metadata:
write-file-atomic: "npm:^5.0.1"
bin:
stylelint: bin/stylelint.mjs
checksum: 10c0/e633f323ff730e8f2ac982067e4caa9a6c98b81a519e7adff96fa7a7d047f68a24c0dd2d81f3511b0943d99c915f20f19da911d16d47336705ea70d46e960c89
checksum: 10c0/18d01587396cce68b59e4a89a7c89d5eb7e76ee7cc27dd109b0f8f241625eb0ffe87763f67b2d20df0f23a243443591fa2514300311a48a945bd6a3bc14db36b
languageName: node
linkType: hard
@ -14545,12 +14530,12 @@ __metadata:
languageName: node
linkType: hard
"yaml@npm:^2.7.1":
version: 2.8.0
resolution: "yaml@npm:2.8.0"
"yaml@npm:^2.8.1":
version: 2.8.1
resolution: "yaml@npm:2.8.1"
bin:
yaml: bin.mjs
checksum: 10c0/f6f7310cf7264a8107e72c1376f4de37389945d2fb4656f8060eca83f01d2d703f9d1b925dd8f39852a57034fafefde6225409ddd9f22aebfda16c6141b71858
checksum: 10c0/7c587be00d9303d2ae1566e03bc5bc7fe978ba0d9bf39cc418c3139d37929dfcb93a230d9749f2cb578b6aa5d9ebebc322415e4b653cb83acd8bc0bc321707f3
languageName: node
linkType: hard