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: jobs:
compute-suffix: compute-suffix:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'mastodon/mastodon'
steps: steps:
- id: version_vars - id: version_vars
env: env:

View File

@ -7,7 +7,7 @@
* - Please do NOT modify this file. * - Please do NOT modify this file.
*/ */
const PACKAGE_VERSION = '2.10.2' const PACKAGE_VERSION = '2.10.4'
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af' const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set() 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.\ 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.\ 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: This adds the following REST API endpoints:
- `GET /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#get-policy - `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 - `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 - `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 - `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: 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 - `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)\ - **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 'ruby-progressbar', '~> 1.13'
gem 'sanitize', '~> 7.0' gem 'sanitize', '~> 7.0'
gem 'scenic', '~> 1.7' gem 'scenic', '~> 1.7'
gem 'sidekiq', '< 8' gem 'sidekiq', '< 9'
gem 'sidekiq-bulk', '~> 0.2.0' gem 'sidekiq-bulk', '~> 0.2.0'
gem 'sidekiq-scheduler', '~> 6.0' gem 'sidekiq-scheduler', '~> 6.0'
gem 'sidekiq-unique-jobs', '> 8' 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_job', '~> 0.8.0', require: false
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.22.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-concurrent_ruby', '~> 0.22.0', require: false
gem 'opentelemetry-instrumentation-excon', '~> 0.23.0', require: false gem 'opentelemetry-instrumentation-excon', '~> 0.24.0', require: false
gem 'opentelemetry-instrumentation-faraday', '~> 0.27.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', '~> 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-net_http', '~> 0.23.0', require: false
gem 'opentelemetry-instrumentation-pg', '~> 0.30.0', require: false gem 'opentelemetry-instrumentation-pg', '~> 0.30.0', require: false
gem 'opentelemetry-instrumentation-rack', '~> 0.26.0', require: false gem 'opentelemetry-instrumentation-rack', '~> 0.26.0', require: false

View File

@ -10,29 +10,29 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (8.0.2) actioncable (8.0.2.1)
actionpack (= 8.0.2) actionpack (= 8.0.2.1)
activesupport (= 8.0.2) activesupport (= 8.0.2.1)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (8.0.2) actionmailbox (8.0.2.1)
actionpack (= 8.0.2) actionpack (= 8.0.2.1)
activejob (= 8.0.2) activejob (= 8.0.2.1)
activerecord (= 8.0.2) activerecord (= 8.0.2.1)
activestorage (= 8.0.2) activestorage (= 8.0.2.1)
activesupport (= 8.0.2) activesupport (= 8.0.2.1)
mail (>= 2.8.0) mail (>= 2.8.0)
actionmailer (8.0.2) actionmailer (8.0.2.1)
actionpack (= 8.0.2) actionpack (= 8.0.2.1)
actionview (= 8.0.2) actionview (= 8.0.2.1)
activejob (= 8.0.2) activejob (= 8.0.2.1)
activesupport (= 8.0.2) activesupport (= 8.0.2.1)
mail (>= 2.8.0) mail (>= 2.8.0)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (8.0.2) actionpack (8.0.2.1)
actionview (= 8.0.2) actionview (= 8.0.2.1)
activesupport (= 8.0.2) activesupport (= 8.0.2.1)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
rack (>= 2.2.4) rack (>= 2.2.4)
rack-session (>= 1.0.1) rack-session (>= 1.0.1)
@ -40,15 +40,15 @@ GEM
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
useragent (~> 0.16) useragent (~> 0.16)
actiontext (8.0.2) actiontext (8.0.2.1)
actionpack (= 8.0.2) actionpack (= 8.0.2.1)
activerecord (= 8.0.2) activerecord (= 8.0.2.1)
activestorage (= 8.0.2) activestorage (= 8.0.2.1)
activesupport (= 8.0.2) activesupport (= 8.0.2.1)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (8.0.2) actionview (8.0.2.1)
activesupport (= 8.0.2) activesupport (= 8.0.2.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
@ -58,22 +58,22 @@ GEM
activemodel (>= 4.1) activemodel (>= 4.1)
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (8.0.2) activejob (8.0.2.1)
activesupport (= 8.0.2) activesupport (= 8.0.2.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (8.0.2) activemodel (8.0.2.1)
activesupport (= 8.0.2) activesupport (= 8.0.2.1)
activerecord (8.0.2) activerecord (8.0.2.1)
activemodel (= 8.0.2) activemodel (= 8.0.2.1)
activesupport (= 8.0.2) activesupport (= 8.0.2.1)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (8.0.2) activestorage (8.0.2.1)
actionpack (= 8.0.2) actionpack (= 8.0.2.1)
activejob (= 8.0.2) activejob (= 8.0.2.1)
activerecord (= 8.0.2) activerecord (= 8.0.2.1)
activesupport (= 8.0.2) activesupport (= 8.0.2.1)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (8.0.2) activesupport (8.0.2.1)
base64 base64
benchmark (>= 0.3) benchmark (>= 0.3)
bigdecimal bigdecimal
@ -547,19 +547,19 @@ GEM
opentelemetry-instrumentation-concurrent_ruby (0.22.0) opentelemetry-instrumentation-concurrent_ruby (0.22.0)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0) opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-excon (0.23.0) opentelemetry-instrumentation-excon (0.24.0)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0) opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-faraday (0.27.0) opentelemetry-instrumentation-faraday (0.28.0)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0) opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-http (0.25.1) opentelemetry-instrumentation-http (0.25.1)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.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-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.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-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0) opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-pg (0.30.1) opentelemetry-instrumentation-pg (0.30.1)
@ -667,20 +667,20 @@ GEM
rack (>= 1.3) rack (>= 1.3)
rackup (2.2.1) rackup (2.2.1)
rack (>= 3) rack (>= 3)
rails (8.0.2) rails (8.0.2.1)
actioncable (= 8.0.2) actioncable (= 8.0.2.1)
actionmailbox (= 8.0.2) actionmailbox (= 8.0.2.1)
actionmailer (= 8.0.2) actionmailer (= 8.0.2.1)
actionpack (= 8.0.2) actionpack (= 8.0.2.1)
actiontext (= 8.0.2) actiontext (= 8.0.2.1)
actionview (= 8.0.2) actionview (= 8.0.2.1)
activejob (= 8.0.2) activejob (= 8.0.2.1)
activemodel (= 8.0.2) activemodel (= 8.0.2.1)
activerecord (= 8.0.2) activerecord (= 8.0.2.1)
activestorage (= 8.0.2) activestorage (= 8.0.2.1)
activesupport (= 8.0.2) activesupport (= 8.0.2.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 8.0.2) railties (= 8.0.2.1)
rails-dom-testing (2.3.0) rails-dom-testing (2.3.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
@ -691,9 +691,9 @@ GEM
rails-i18n (8.0.1) rails-i18n (8.0.1)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9) railties (>= 8.0.0, < 9)
railties (8.0.2) railties (8.0.2.1)
actionpack (= 8.0.2) actionpack (= 8.0.2.1)
activesupport (= 8.0.2) activesupport (= 8.0.2.1)
irb (~> 1.13) irb (~> 1.13)
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
@ -719,7 +719,7 @@ GEM
redis (4.8.1) redis (4.8.1)
redis-client (0.25.2) redis-client (0.25.2)
connection_pool connection_pool
regexp_parser (2.11.1) regexp_parser (2.11.2)
reline (0.6.2) reline (0.6.2)
io-console (~> 0.5) io-console (~> 0.5)
request_store (1.7.0) request_store (1.7.0)
@ -787,7 +787,7 @@ GEM
lint_roller (~> 1.1) lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0) rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0) rubocop-ast (>= 1.38.0, < 2.0)
rubocop-rails (2.33.1) rubocop-rails (2.33.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rack (>= 1.1) rack (>= 1.1)
@ -823,12 +823,12 @@ GEM
securerandom (0.4.1) securerandom (0.4.1)
shoulda-matchers (6.5.0) shoulda-matchers (6.5.0)
activesupport (>= 5.2.0) activesupport (>= 5.2.0)
sidekiq (7.3.9) sidekiq (8.0.7)
base64 connection_pool (>= 2.5.0)
connection_pool (>= 2.3.0) json (>= 2.9.0)
logger logger (>= 1.6.2)
rack (>= 2.2.4) rack (>= 3.1.0)
redis-client (>= 0.22.2) redis-client (>= 0.23.2)
sidekiq-bulk (0.2.0) sidekiq-bulk (0.2.0)
sidekiq sidekiq
sidekiq-scheduler (6.0.1) sidekiq-scheduler (6.0.1)
@ -1030,10 +1030,10 @@ DEPENDENCIES
opentelemetry-instrumentation-active_job (~> 0.8.0) opentelemetry-instrumentation-active_job (~> 0.8.0)
opentelemetry-instrumentation-active_model_serializers (~> 0.22.0) opentelemetry-instrumentation-active_model_serializers (~> 0.22.0)
opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0) opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
opentelemetry-instrumentation-excon (~> 0.23.0) opentelemetry-instrumentation-excon (~> 0.24.0)
opentelemetry-instrumentation-faraday (~> 0.27.0) opentelemetry-instrumentation-faraday (~> 0.28.0)
opentelemetry-instrumentation-http (~> 0.25.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-net_http (~> 0.23.0)
opentelemetry-instrumentation-pg (~> 0.30.0) opentelemetry-instrumentation-pg (~> 0.30.0)
opentelemetry-instrumentation-rack (~> 0.26.0) opentelemetry-instrumentation-rack (~> 0.26.0)
@ -1077,7 +1077,7 @@ DEPENDENCIES
sanitize (~> 7.0) sanitize (~> 7.0)
scenic (~> 1.7) scenic (~> 1.7)
shoulda-matchers shoulda-matchers
sidekiq (< 8) sidekiq (< 9)
sidekiq-bulk (~> 0.2.0) sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 6.0) sidekiq-scheduler (~> 6.0)
sidekiq-unique-jobs (> 8) 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+ - **Ruby** 3.2+
- **PostgreSQL** 13+ - **PostgreSQL** 13+
- **Redis** 6.2+ - **Redis** 7.0+
- **Node.js** 20+ - **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. 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 \ pkg-config \
protobuf-compiler \ protobuf-compiler \
zlib1g-dev \ zlib1g-dev \
libvips42t64 \
-y -y
# Install rvm # Install rvm
@ -134,7 +135,7 @@ VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 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| config.vm.provider :virtualbox do |vb|
vb.name = "mastodon" 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 class Api::V1::StatusesController < Api::BaseController
include Authorization include Authorization
include AsyncRefreshesConcern include AsyncRefreshesConcern
include Api::InteractionPoliciesConcern
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy] before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [: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 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 def serializer_for_status
@status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
end 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) { return function (dispatch, getState) {
const status = getState().getIn(['compose', 'text'], ''); const status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']); const media = getState().getIn(['compose', 'media_attachments']);
@ -241,6 +241,9 @@ export function submitCompose() {
dispatch(insertIntoTagHistory(response.data.tags, status)); dispatch(insertIntoTagHistory(response.data.tags, status));
dispatch(submitComposeSuccess({ ...response.data })); dispatch(submitComposeSuccess({ ...response.data }));
if (typeof successCallback === 'function') {
successCallback(response.data);
}
// To make the app more responsive, immediately push the status // To make the app more responsive, immediately push the status
// into the columns // into the columns

View File

@ -1,8 +1,10 @@
import { createAction } from '@reduxjs/toolkit'; 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 { createDataLoadingThunk } from 'mastodon/store/typed_functions';
import type { ApiQuotePolicy } from '../api_types/quotes';
import { importFetchedStatuses } from './importer'; import { importFetchedStatuses } from './importer';
export const fetchContext = createDataLoadingThunk( export const fetchContext = createDataLoadingThunk(
@ -23,3 +25,10 @@ export const fetchContext = createDataLoadingThunk(
export const completeContextRefresh = createAction<{ statusId: string }>( export const completeContextRefresh = createAction<{ statusId: string }>(
'status/context/complete', '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 api, { apiRequestPut, getAsyncRefreshHeader } from 'mastodon/api';
import type { ApiContextJSON } from 'mastodon/api_types/statuses'; import type {
ApiContextJSON,
ApiStatusJSON,
} from 'mastodon/api_types/statuses';
import type { ApiQuotePolicy } from '../api_types/quotes';
export const apiGetContext = async (statusId: string) => { export const apiGetContext = async (statusId: string) => {
const response = await api().request<ApiContextJSON>({ const response = await api().request<ApiContextJSON>({
@ -12,3 +17,15 @@ export const apiGetContext = async (statusId: string) => {
refresh: getAsyncRefreshHeader(response), 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'; import type { ApiStatusJSON } from './statuses';
export type ApiQuoteState = 'accepted' | 'pending' | 'revoked' | 'unauthorized'; export type ApiQuoteState = 'accepted' | 'pending' | 'revoked' | 'unauthorized';
export type ApiQuotePolicy = 'public' | 'followers' | 'nobody'; export type ApiQuotePolicy = 'public' | 'followers' | 'nobody' | 'unknown';
interface ApiQuoteEmptyJSON { interface ApiQuoteEmptyJSON {
state: Exclude<ApiQuoteState, 'accepted'>; state: Exclude<ApiQuoteState, 'accepted'>;
@ -21,3 +21,13 @@ interface ApiQuoteAcceptedJSON {
} }
export type ApiQuoteJSON = ApiQuoteAcceptedJSON | ApiQuoteEmptyJSON; 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 { ApiCustomEmojiJSON } from './custom_emoji';
import type { ApiMediaAttachmentJSON } from './media_attachments'; import type { ApiMediaAttachmentJSON } from './media_attachments';
import type { ApiPollJSON } from './polls'; import type { ApiPollJSON } from './polls';
import type { ApiQuoteJSON } from './quotes'; import type { ApiQuoteJSON, ApiQuotePolicyJSON } from './quotes';
// See app/modals/status.rb // See app/modals/status.rb
export type StatusVisibility = export type StatusVisibility =
@ -120,9 +120,16 @@ export interface ApiStatusJSON {
card?: ApiPreviewCardJSON; card?: ApiPreviewCardJSON;
poll?: ApiPollJSON; poll?: ApiPollJSON;
quote?: ApiQuoteJSON; quote?: ApiQuoteJSON;
quote_approval?: ApiQuotePolicyJSON;
} }
export interface ApiContextJSON { export interface ApiContextJSON {
ancestors: ApiStatusJSON[]; ancestors: ApiStatusJSON[];
descendants: 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 } ? { passive: true, capture: true }
: true; : true;
export interface SelectItem { export interface SelectItem<Value extends string = string> {
value: string; value: Value;
icon?: string; icon?: string;
iconComponent?: IconProp; iconComponent?: IconProp;
text: string; text: string;
@ -24,7 +24,7 @@ export interface SelectItem {
interface Props { interface Props {
value: string; value: string;
classNamePrefix: string; classNamePrefix?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
items: SelectItem[]; items: SelectItem[];
onChange: (value: string) => void; onChange: (value: string) => void;

View File

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

View File

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

View File

@ -396,7 +396,7 @@ export const LanguageDropdown: React.FC = () => {
warning: guess !== '' && guess !== value, warning: guess !== '' && guess !== value,
})} })}
> >
<Icon id='' icon={TranslateIcon} /> <Icon id='translate' icon={TranslateIcon} />
<span className='dropdown-button__label'>{current[2] ?? value}</span> <span className='dropdown-button__label'>{current[2] ?? value}</span>
</button> </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 { DropdownSelector } from 'mastodon/components/dropdown_selector';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
const messages = defineMessages({ export const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' }, public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Quiet public' }, 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), maxChars: state.getIn(['server', 'server', 'configuration', 'statuses', 'max_characters'], 500),
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch, props) => ({
onChange (text) { onChange (text) {
dispatch(changeCompose(text)); dispatch(changeCompose(text));
@ -47,7 +47,11 @@ const mapDispatchToProps = (dispatch) => ({
modalProps: {}, modalProps: {},
})); }));
} else { } 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 = () => ( const Compose = () => (
<> <>
<ComposeFormContainer autoFocus withoutNavigation /> <ComposeFormContainer autoFocus withoutNavigation redirectOnSuccess />
<AlertsController /> <AlertsController />
<ModalContainer /> <ModalContainer />
<LoadingBarContainer className='loading-bar' /> <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 { IconButton } from '../../../components/icon_button';
import { Dropdown } from 'mastodon/components/dropdown_menu'; import { Dropdown } from 'mastodon/components/dropdown_menu';
import { me } from '../../../initial_state'; import { me } from '../../../initial_state';
import { isFeatureEnabled } from '@/mastodon/utils/environment';
const messages = defineMessages({ const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' }, delete: { id: 'status.delete', defaultMessage: 'Delete' },
@ -62,6 +63,7 @@ const messages = defineMessages({
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
revokeQuote: { id: 'status.revoke_quote', defaultMessage: 'Remove my post from @{name}s post' }, 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 }) => { const mapStateToProps = (state, { status }) => {
@ -84,6 +86,7 @@ class ActionBar extends PureComponent {
onBookmark: PropTypes.func.isRequired, onBookmark: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onRevokeQuote: PropTypes.func, onRevokeQuote: PropTypes.func,
onQuotePolicyChange: PropTypes.func,
onEdit: PropTypes.func.isRequired, onEdit: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired, onDirect: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired,
@ -122,7 +125,11 @@ class ActionBar extends PureComponent {
handleRevokeQuoteClick = () => { handleRevokeQuoteClick = () => {
this.props.onRevokeQuote(this.props.status); this.props.onRevokeQuote(this.props.status);
} };
handleQuotePolicyChange = () => {
this.props.onQuotePolicyChange(this.props.status);
};
handleRedraftClick = () => { handleRedraftClick = () => {
this.props.onDelete(this.props.status, true); 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 }); 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(null);
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true }); 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']) }})); 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) => { handleEditClick = (status) => {
const { dispatch, askReplyConfirmation } = this.props; const { dispatch, askReplyConfirmation } = this.props;
@ -642,6 +647,7 @@ class Status extends ImmutablePureComponent {
onBookmark={this.handleBookmarkClick} onBookmark={this.handleBookmarkClick}
onDelete={this.handleDeleteClick} onDelete={this.handleDeleteClick}
onRevokeQuote={this.handleRevokeQuoteClick} onRevokeQuote={this.handleRevokeQuoteClick}
onQuotePolicyChange={this.handleQuotePolicyChange}
onEdit={this.handleEditClick} onEdit={this.handleEditClick}
onDirect={this.handleDirectClick} onDirect={this.handleDirectClick}
onMention={this.handleMentionClick} onMention={this.handleMentionClick}

View File

@ -43,6 +43,7 @@ import { ImageModal } from './image_modal';
import MediaModal from './media_modal'; import MediaModal from './media_modal';
import { ModalPlaceholder } from './modal_placeholder'; import { ModalPlaceholder } from './modal_placeholder';
import VideoModal from './video_modal'; import VideoModal from './video_modal';
import { VisibilityModal } from './visibility_modal';
export const MODAL_COMPONENTS = { export const MODAL_COMPONENTS = {
'MEDIA': () => Promise.resolve({ default: MediaModal }), 'MEDIA': () => Promise.resolve({ default: MediaModal }),
@ -76,6 +77,7 @@ export const MODAL_COMPONENTS = {
'CLOSED_REGISTRATIONS': ClosedRegistrationsModal, 'CLOSED_REGISTRATIONS': ClosedRegistrationsModal,
'IGNORE_NOTIFICATIONS': IgnoreNotificationsModal, 'IGNORE_NOTIFICATIONS': IgnoreNotificationsModal,
'ANNUAL_REPORT': AnnualReportModal, 'ANNUAL_REPORT': AnnualReportModal,
'COMPOSE_PRIVACY': () => Promise.resolve({ default: VisibilityModal }),
}; };
export default class ModalRoot extends PureComponent { 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", "lightbox.zoom_out": "Přizpůsobit velikost",
"limited_account_hint.action": "Přesto profil zobrazit", "limited_account_hint.action": "Přesto profil zobrazit",
"limited_account_hint.title": "Tento profil byl skryt moderátory {domain}.", "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.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ů}}", "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", "lists.add_member": "Přidat",

View File

@ -292,6 +292,7 @@
"domain_pill.your_handle": "Your handle:", "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_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.", "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.instructions": "Embed this post on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
@ -884,6 +885,7 @@
"status.quote_error.pending_approval": "Post pending", "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.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_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.quote_post_author": "Quoted a post by @{name}",
"status.read_more": "Read more", "status.read_more": "Read more",
"status.reblog": "Boost", "status.reblog": "Boost",
@ -959,5 +961,17 @@
"video.skip_forward": "Skip forward", "video.skip_forward": "Skip forward",
"video.unmute": "Unmute", "video.unmute": "Unmute",
"video.volume_down": "Volume down", "video.volume_down": "Volume down",
"video.volume_up": "Volume up" "video.volume_up": "Volume up",
"visibility_modal.button_title": "Set visibility",
"visibility_modal.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.confirm": "Fjern fylgjar",
"confirmations.remove_from_followers.message": "{name} vil ikkje fylgja deg meir. Vil du halda fram?", "confirmations.remove_from_followers.message": "{name} vil ikkje fylgja deg meir. Vil du halda fram?",
"confirmations.remove_from_followers.title": "Fjern fylgjar?", "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.confirm": "Slutt å fylgja",
"confirmations.unfollow.message": "Er du sikker på at du vil slutta å fylgja {name}?", "confirmations.unfollow.message": "Er du sikker på at du vil slutta å fylgja {name}?",
"confirmations.unfollow.title": "Slutt å fylgja brukaren?", "confirmations.unfollow.title": "Slutt å fylgja brukaren?",
@ -498,6 +501,8 @@
"keyboard_shortcuts.translate": "å omsetje eit innlegg", "keyboard_shortcuts.translate": "å omsetje eit innlegg",
"keyboard_shortcuts.unfocus": "for å fokusere vekk skrive-/søkefeltet", "keyboard_shortcuts.unfocus": "for å fokusere vekk skrive-/søkefeltet",
"keyboard_shortcuts.up": "Flytt opp på lista", "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.close": "Lukk",
"lightbox.next": "Neste", "lightbox.next": "Neste",
"lightbox.previous": "Førre", "lightbox.previous": "Førre",
@ -598,6 +603,7 @@
"notification.label.mention": "Omtale", "notification.label.mention": "Omtale",
"notification.label.private_mention": "Privat omtale", "notification.label.private_mention": "Privat omtale",
"notification.label.private_reply": "Privat svar", "notification.label.private_reply": "Privat svar",
"notification.label.quote": "{name} siterte innlegget ditt",
"notification.label.reply": "Svar", "notification.label.reply": "Svar",
"notification.mention": "Omtale", "notification.mention": "Omtale",
"notification.mentioned_you": "{name} nemnde deg", "notification.mentioned_you": "{name} nemnde deg",
@ -655,6 +661,7 @@
"notifications.column_settings.mention": "Omtaler:", "notifications.column_settings.mention": "Omtaler:",
"notifications.column_settings.poll": "Røysteresultat:", "notifications.column_settings.poll": "Røysteresultat:",
"notifications.column_settings.push": "Pushvarsel", "notifications.column_settings.push": "Pushvarsel",
"notifications.column_settings.quote": "Sitat:",
"notifications.column_settings.reblog": "Framhevingar:", "notifications.column_settings.reblog": "Framhevingar:",
"notifications.column_settings.show": "Vis i kolonne", "notifications.column_settings.show": "Vis i kolonne",
"notifications.column_settings.sound": "Spel av lyd", "notifications.column_settings.sound": "Spel av lyd",
@ -845,6 +852,8 @@
"status.bookmark": "Set bokmerke", "status.bookmark": "Set bokmerke",
"status.cancel_reblog_private": "Opphev framheving", "status.cancel_reblog_private": "Opphev framheving",
"status.cannot_reblog": "Du kan ikkje framheva dette innlegget", "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.continued_thread": "Framhald til tråden",
"status.copy": "Kopier lenke til status", "status.copy": "Kopier lenke til status",
"status.delete": "Slett", "status.delete": "Slett",
@ -871,6 +880,11 @@
"status.open": "Utvid denne statusen", "status.open": "Utvid denne statusen",
"status.pin": "Fest på profil", "status.pin": "Fest på profil",
"status.quote_error.filtered": "Gøymt på grunn av eitt av filtra dine", "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.read_more": "Les meir",
"status.reblog": "Framhev", "status.reblog": "Framhev",
"status.reblog_private": "Framhev til dei originale mottakarane", "status.reblog_private": "Framhev til dei originale mottakarane",
@ -885,6 +899,7 @@
"status.reply": "Svar", "status.reply": "Svar",
"status.replyAll": "Svar til tråd", "status.replyAll": "Svar til tråd",
"status.report": "Rapporter @{name}", "status.report": "Rapporter @{name}",
"status.revoke_quote": "Fjern innlegget mitt frå @{name} sitt innlegg",
"status.sensitive_warning": "Ømtolig innhald", "status.sensitive_warning": "Ømtolig innhald",
"status.share": "Del", "status.share": "Del",
"status.show_less_all": "Vis mindre for alle", "status.show_less_all": "Vis mindre for alle",

View File

@ -29,6 +29,7 @@ import {
STATUS_FETCH_REQUEST, STATUS_FETCH_REQUEST,
STATUS_FETCH_FAIL, STATUS_FETCH_FAIL,
} from '../actions/statuses'; } from '../actions/statuses';
import { setStatusQuotePolicy } from '../actions/statuses_typed';
const importStatus = (state, status) => state.set(status.id, fromJS(status)); const importStatus = (state, status) => state.set(status.id, fromJS(status));
@ -70,6 +71,22 @@ const initialState = ImmutableMap();
/** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */ /** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */
export default function statuses(state = initialState, action) { 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) { switch(action.type) {
case STATUS_FETCH_REQUEST: case STATUS_FETCH_REQUEST:
return state.setIn([action.id, 'isLoading'], true); return state.setIn([action.id, 'isLoading'], true);

View File

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

View File

@ -12,7 +12,11 @@ export function isProduction() {
else return import.meta.env.PROD; 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) { export function isFeatureEnabled(feature: Features) {
return initialState?.features.includes(feature) ?? false; return initialState?.features.includes(feature) ?? false;

View File

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

View File

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

View File

@ -71,8 +71,16 @@ class Antispam
end end
def report_if_needed!(account) 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
end end

View File

@ -116,7 +116,7 @@ class Account < ApplicationRecord
# Local user validations # 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 :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 :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 :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? } 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), followed: (1 << 3),
}.freeze }.freeze
included do
before_validation :downgrade_quote_policy, if: -> { local? && !distributable? }
end
def quote_policy_as_keys(kind) def quote_policy_as_keys(kind)
case kind case kind
when :automatic when :automatic
@ -52,4 +56,8 @@ module Status::InteractionPolicyConcern
:denied :denied
end end
def downgrade_quote_policy
self.quote_approval_policy = 0
end
end end

View File

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

View File

@ -190,6 +190,7 @@ nn:
create_relay: Opprett eit relé create_relay: Opprett eit relé
create_unavailable_domain: Opprett utilgjengeleg domene create_unavailable_domain: Opprett utilgjengeleg domene
create_user_role: Opprett rolle create_user_role: Opprett rolle
create_username_block: Lag regel for brukarnamn
demote_user: Degrader brukar demote_user: Degrader brukar
destroy_announcement: Slett lysinga destroy_announcement: Slett lysinga
destroy_canonical_email_block: Fjern e-postblokkering destroy_canonical_email_block: Fjern e-postblokkering
@ -203,6 +204,7 @@ nn:
destroy_status: Slett status destroy_status: Slett status
destroy_unavailable_domain: Slett utilgjengeleg domene destroy_unavailable_domain: Slett utilgjengeleg domene
destroy_user_role: Øydelegg rolle destroy_user_role: Øydelegg rolle
destroy_username_block: Slett regel for brukarnamn
disable_2fa_user: Skruv av 2FA disable_2fa_user: Skruv av 2FA
disable_custom_emoji: Skruv av tilpassa emoji disable_custom_emoji: Skruv av tilpassa emoji
disable_relay: Skru av reléet disable_relay: Skru av reléet
@ -237,6 +239,7 @@ nn:
update_report: Oppdater rapport update_report: Oppdater rapport
update_status: Oppdater tut update_status: Oppdater tut
update_user_role: Oppdater rolla update_user_role: Oppdater rolla
update_username_block: Oppdater regel for brukarnamn
actions: actions:
approve_appeal_html: "%{name} godkjende klagen frå %{target} på modereringa" approve_appeal_html: "%{name} godkjende klagen frå %{target} på modereringa"
approve_user_html: "%{name} godkjende registreringa til %{target}" approve_user_html: "%{name} godkjende registreringa til %{target}"
@ -255,6 +258,7 @@ nn:
create_relay_html: "%{name} laga reléet %{target}" create_relay_html: "%{name} laga reléet %{target}"
create_unavailable_domain_html: "%{name} stogga levering til domenet %{target}" create_unavailable_domain_html: "%{name} stogga levering til domenet %{target}"
create_user_role_html: "%{name} oppretta rolla %{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}" demote_user_html: "%{name} degraderte brukaren %{target}"
destroy_announcement_html: "%{name} sletta kunngjeringa %{target}" destroy_announcement_html: "%{name} sletta kunngjeringa %{target}"
destroy_canonical_email_block_html: "%{name} avblokkerte e-post med hash %{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_status_html: "%{name} fjerna innlegget frå %{target}"
destroy_unavailable_domain_html: "%{name} tok opp att levering til domenet %{target}" destroy_unavailable_domain_html: "%{name} tok opp att levering til domenet %{target}"
destroy_user_role_html: "%{name} sletta rolla %{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_2fa_user_html: "%{name} tok vekk krav om tofaktorautentisering for brukaren %{target}"
disable_custom_emoji_html: "%{name} deaktiverte emojien %{target}" disable_custom_emoji_html: "%{name} deaktiverte emojien %{target}"
disable_relay_html: "%{name} skrudde av reléet %{target}" disable_relay_html: "%{name} skrudde av reléet %{target}"
@ -302,6 +307,7 @@ nn:
update_report_html: "%{name} oppdaterte rapporten %{target}" update_report_html: "%{name} oppdaterte rapporten %{target}"
update_status_html: "%{name} oppdaterte innlegg av %{target}" update_status_html: "%{name} oppdaterte innlegg av %{target}"
update_user_role_html: "%{name} endret %{target} -rolle" update_user_role_html: "%{name} endret %{target} -rolle"
update_username_block_html: "%{name} oppdaterte regelen for brukarnamn som inneheld %{target}"
deleted_account: sletta konto deleted_account: sletta konto
empty: Ingen loggar funne. empty: Ingen loggar funne.
filter_by_action: Sorter etter handling filter_by_action: Sorter etter handling
@ -1085,6 +1091,25 @@ nn:
other: Brukt av %{count} personer i løpet av den seneste uken other: Brukt av %{count} personer i løpet av den seneste uken
title: Anbefalingar og trendar title: Anbefalingar og trendar
trending: Trender 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: warning_presets:
add_new: Legg til ny add_new: Legg til ny
delete: Slett delete: Slett
@ -1349,6 +1374,10 @@ nn:
basic_information: Grunnleggande informasjon 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." 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 other: Anna
emoji_styles:
auto: Auto
native: Innebygd
twemoji: Twemoji
errors: errors:
'400': Søknaden du sende var ugyldig eller sett opp feil. '400': Søknaden du sende var ugyldig eller sett opp feil.
'403': Du har ikkje løyve til å sjå denne sida. '403': Du har ikkje løyve til å sjå denne sida.
@ -1658,6 +1687,10 @@ nn:
title: Ny omtale title: Ny omtale
poll: poll:
subject: Meiningsmålinga frå %{name} er avslutta subject: Meiningsmålinga frå %{name} er avslutta
quote:
body: 'Innlegget ditt vart sitert av %{name}:'
subject: "%{name} siterte innlegget ditt"
title: Nytt sitat
reblog: reblog:
body: 'Statusen din vart framheva av %{name}:' body: 'Statusen din vart framheva av %{name}:'
subject: "%{name} framheva statusen din" subject: "%{name} framheva statusen din"
@ -1868,6 +1901,7 @@ nn:
edited_at_html: Redigert %{date} edited_at_html: Redigert %{date}
errors: errors:
in_reply_not_found: Det ser ut til at tutet du freistar å svara ikkje finst. 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 over_character_limit: øvregrensa for teikn, %{max}, er nådd
pin_errors: pin_errors:
direct: Innlegg som bare er synlige for nevnte brukere kan ikke festes 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 ownership: Du kan ikkje festa andre sine tut
reblog: Ei framheving kan ikkje festast reblog: Ei framheving kan ikkje festast
quote_policies: quote_policies:
followers: Berre dei som fylgjer deg
nobody: Ingen
public: Alle public: Alle
title: "%{name}: «%{quote}»" title: "%{name}: «%{quote}»"
visibilities: 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. 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_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_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_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_default: Gøym media som er merka som sensitive
setting_display_media_hide_all: Alltid skjul alt media setting_display_media_hide_all: Alltid skjul alt media
setting_display_media_show_all: Vis alltid 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_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_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 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. min_age: Skal ikkje vere under minstealder som krevst av lover i jurisdiksjonen din.
user: user:
chosen_languages: Når merka vil berre tuta på dei valde språka synast på offentlege tidsliner 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. role: Rolla kontrollerer kva løyve brukaren har.
user_role: user_role:
color: Fargen som skal nyttast for denne rolla i heile brukargrensesnittet, som RGB i hex-format 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 name: Offentleg namn på rolla, dersom rolla skal visast som eit emblem
permissions_as_keys: Brukarar med denne rolla vil ha tilgang til... 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 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: webhook:
events: Vel hendingar å senda events: Vel hendingar å senda
template: Skriv di eiga JSON nyttelast ved å bruka variabel interpolering. La stå tom for standard JSON. 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_default: Standard
setting_display_media_hide_all: Gøym alle setting_display_media_hide_all: Gøym alle
setting_display_media_show_all: Vis 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_expand_spoilers: Vid alltid ut tut som er merka med innhaldsåtvaringar
setting_hide_network: Gøym nettverket ditt setting_hide_network: Gøym nettverket ditt
setting_missing_alt_text_modal: Vis stadfestingsdialog før du legg ut media utan alt-tekst 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 follow_request: Send e-post når nokon spør om å fylgja deg
mention: Send e-post når nokon nemner deg mention: Send e-post når nokon nemner deg
pending_account: Send e-post når ein ny konto treng gjennomgang 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 reblog: Send e-post når nokon framhevar statusen din
report: Ny rapport er sendt report: Ny rapport er sendt
software_updates: software_updates:
@ -365,6 +376,10 @@ nn:
name: Namn name: Namn
permissions_as_keys: Løyve permissions_as_keys: Løyve
position: Prioritet position: Prioritet
username_block:
allow_with_approval: Tillat registreringar med godkjenning
comparison: Samanlikningsmetode
username: Ord som skal passa
webhook: webhook:
events: Aktiverte hendingar events: Aktiverte hendingar
template: Nyttelastmal template: Nyttelastmal

View File

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

View File

@ -47,7 +47,7 @@
"@gamestdio/websocket": "^0.3.2", "@gamestdio/websocket": "^0.3.2",
"@github/webauthn-json": "^2.1.1", "@github/webauthn-json": "^2.1.1",
"@optimize-lodash/rollup-plugin": "^5.0.2", "@optimize-lodash/rollup-plugin": "^5.0.2",
"@rails/ujs": "7.1.501", "@rails/ujs": "7.1.502",
"@react-spring/web": "^9.7.5", "@react-spring/web": "^9.7.5",
"@reduxjs/toolkit": "^2.0.1", "@reduxjs/toolkit": "^2.0.1",
"@use-gesture/react": "^10.3.1", "@use-gesture/react": "^10.3.1",
@ -170,7 +170,7 @@
"eslint-import-resolver-typescript": "^4.2.5", "eslint-import-resolver-typescript": "^4.2.5",
"eslint-plugin-formatjs": "^5.3.1", "eslint-plugin-formatjs": "^5.3.1",
"eslint-plugin-import": "~2.32.0", "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-jsx-a11y": "~6.10.2",
"eslint-plugin-promise": "~7.2.1", "eslint-plugin-promise": "~7.2.1",
"eslint-plugin-react": "^7.37.4", "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 describe '#create' do
let(:action) { :create } let(:action) { :create }
let(:username) { 'tootctl_username' }
shared_examples 'a new user with given email address and username' do shared_examples 'a new user with given email address and username' do
it 'creates user and accounts from options and displays success message' do it 'creates user and accounts from options and displays success message' do
@ -48,18 +49,24 @@ RSpec.describe Mastodon::CLI::Accounts do
end end
def account_from_options def account_from_options
Account.find_local('tootctl_username') Account.find_local(username)
end end
end end
context 'when required USERNAME and --email are provided' do context 'when required USERNAME and --email are provided' do
let(:arguments) { ['tootctl_username'] } let(:arguments) { [username] }
context 'with USERNAME and --email only' do context 'with USERNAME and --email only' do
let(:options) { { email: 'tootctl@example.com' } } let(:options) { { email: 'tootctl@example.com' } }
it_behaves_like 'a new user with given email address and username' 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 context 'with invalid --email value' do
let(:options) { { email: 'invalid' } } 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 fill_in_form
expect(page) expect(page)
.to have_css('.notification-bar-message', text: frontend_translations('compose.published.body')) .to have_current_path(%r{/@bob/[0-9]+})
end end
def fill_in_form def fill_in_form

369
yarn.lock
View File

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