diff --git a/.devcontainer/compose.yaml b/.devcontainer/compose.yaml index 4d5ed0f25f..5da1ec3a24 100644 --- a/.devcontainer/compose.yaml +++ b/.devcontainer/compose.yaml @@ -21,12 +21,13 @@ services: ES_HOST: es ES_PORT: '9200' LIBRE_TRANSLATE_ENDPOINT: http://libretranslate:5000 + LOCAL_DOMAIN: ${LOCAL_DOMAIN:-localhost:3000} # Overrides default command so things don't shut down after the process ends. command: sleep infinity ports: - - '127.0.0.1:3000:3000' - - '127.0.0.1:3035:3035' - - '127.0.0.1:4000:4000' + - '3000:3000' + - '3035:3035' + - '4000:4000' networks: - external_network - internal_network diff --git a/.env.production.sample b/.env.production.sample index 12ab2b6dcb..4afaf8d756 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -79,6 +79,9 @@ AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= S3_ALIAS_HOST=files.example.com +# Optional list of hosts that are allowed to serve media for your instance +# EXTRA_MEDIA_HOSTS=https://data.example1.com,https://data.example2.com + # IP and session retention # ----------------------- # Make sure to modify the scheduling of ip_cleanup_scheduler in config/sidekiq.yml diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 120b7df202..f589f5f03d 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.73.2. +# using RuboCop version 1.75.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -49,7 +49,7 @@ Style/FetchEnvVar: - 'lib/tasks/repo.rake' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns. +# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, Mode, AllowedMethods, AllowedPatterns. # SupportedStyles: annotated, template, unannotated # AllowedMethods: redirect Style/FormatStringToken: @@ -77,7 +77,6 @@ Style/MapToHash: # AllowedMethods: respond_to_missing? Style/OptionalBooleanParameter: Exclude: - - 'app/helpers/json_ld_helper.rb' - 'app/lib/admin/system_check/message.rb' - 'app/lib/request.rb' - 'app/lib/webfinger.rb' diff --git a/Gemfile b/Gemfile index fba7371b64..b64a1dbe91 100644 --- a/Gemfile +++ b/Gemfile @@ -14,6 +14,7 @@ gem 'haml-rails', '~>2.0' gem 'pg', '~> 1.5' gem 'pghero' +gem 'aws-sdk-core', '< 3.216.0', require: false # TODO: https://github.com/mastodon/mastodon/pull/34173#issuecomment-2733378873 gem 'aws-sdk-s3', '~> 1.123', require: false gem 'blurhash', '~> 0.1' gem 'fog-core', '<= 2.6.0' diff --git a/Gemfile.lock b/Gemfile.lock index 66a597e99e..1ad5429d4b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -91,11 +91,11 @@ GEM aes_key_wrap (1.1.0) android_key_attestation (0.3.0) annotaterb (4.14.0) - ast (2.4.2) + ast (2.4.3) attr_required (1.0.2) - aws-eventstream (1.3.0) - aws-partitions (1.1032.0) - aws-sdk-core (3.214.1) + aws-eventstream (1.3.2) + aws-partitions (1.1066.0) + aws-sdk-core (3.215.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -107,9 +107,9 @@ GEM aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.10.1) + aws-sigv4 (1.11.0) aws-eventstream (~> 1, >= 1.0.2) - azure-blob (0.5.4) + azure-blob (0.5.7) rexml base64 (0.2.0) bcp47_spec (0.2.1) @@ -168,9 +168,9 @@ GEM bigdecimal rexml crass (1.0.6) - css_parser (1.21.0) + css_parser (1.21.1) addressable - csv (3.3.2) + csv (3.3.3) database_cleaner-active_record (2.2.0) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) @@ -222,7 +222,8 @@ GEM erubi (1.13.1) et-orbi (1.2.11) tzinfo - excon (1.2.3) + excon (1.2.5) + logger fabrication (2.31.0) faker (3.5.1) i18n (>= 1.8.11, < 2) @@ -256,7 +257,7 @@ GEM fog-json (1.2.0) fog-core multi_json (~> 1.10) - fog-openstack (1.1.4) + fog-openstack (1.1.5) fog-core (~> 2.1) fog-json (>= 1.0) formatador (1.1.0) @@ -265,7 +266,9 @@ GEM raabro (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) - google-protobuf (3.25.6) + google-protobuf (4.30.1) + bigdecimal + rake (>= 13) googleapis-common-protos-types (1.18.0) google-protobuf (>= 3.18, < 5.a) haml (6.3.0) @@ -277,7 +280,7 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.61.0) + haml_lint (0.61.1) haml (>= 5.0) parallel (~> 1.10) rainbow @@ -302,7 +305,8 @@ GEM domain_name (~> 0.5) http-form_data (2.3.0) http_accept_language (2.1.1) - httpclient (2.8.3) + httpclient (2.9.0) + mutex_m httplog (1.7.0) rack (>= 2.0) rainbow (>= 2.0.0) @@ -378,7 +382,7 @@ GEM mime-types terrapin (>= 0.6.0, < 2.0) language_server-protocol (3.17.0.4) - launchy (3.1.0) + launchy (3.1.1) addressable (~> 2.8) childprocess (~> 5.0) logger (~> 1.6) @@ -391,7 +395,7 @@ GEM rexml link_header (0.0.8) lint_roller (1.1.0) - llhttp-ffi (0.5.0) + llhttp-ffi (0.5.1) ffi-compiler (~> 1.0) rake (~> 13.0) logger (1.6.6) @@ -413,13 +417,13 @@ GEM redis (>= 3.0.5) matrix (0.4.2) memory_profiler (1.1.0) - mime-types (3.6.0) + mime-types (3.6.2) logger mime-types-data (~> 3.2015) - mime-types-data (3.2025.0220) + mime-types-data (3.2025.0318) mini_mime (1.1.5) mini_portile2 (2.8.8) - minitest (5.25.4) + minitest (5.25.5) msgpack (1.8.0) multi_json (1.15.0) mutex_m (0.3.0) @@ -436,7 +440,7 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.4) - nokogiri (1.18.3) + nokogiri (1.18.6) mini_portile2 (~> 2.8.2) racc (~> 1.4) oj (3.16.10) @@ -576,14 +580,14 @@ GEM ox (2.14.22) bigdecimal (>= 3.0) parallel (1.26.3) - parser (3.3.7.1) + parser (3.3.7.3) ast (~> 2.4.1) racc parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) pg (1.5.9) - pghero (3.6.1) + pghero (3.6.2) activerecord (>= 6.1) pp (0.6.2) prettyprint @@ -596,6 +600,7 @@ GEM net-smtp premailer (~> 1.7, >= 1.7.9) prettyprint (0.2.0) + prism (1.4.0) prometheus_exporter (2.2.0) webrick propshaft (1.1.0) @@ -729,7 +734,7 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 9) rspec-support (3.13.2) - rubocop (1.74.0) + rubocop (1.75.1) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -737,11 +742,12 @@ GEM parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.38.0, < 2.0) + rubocop-ast (>= 1.43.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.38.1) - parser (>= 3.3.1.0) + rubocop-ast (1.43.0) + parser (>= 3.3.7.2) + prism (~> 1.4) rubocop-capybara (2.22.1) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) @@ -785,7 +791,7 @@ GEM activerecord (>= 4.0.0) railties (>= 4.0.0) securerandom (0.4.1) - selenium-webdriver (4.29.1) + selenium-webdriver (4.30.1) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) @@ -826,7 +832,7 @@ GEM stoplight (4.1.1) redlock (~> 1.0) stringio (3.1.5) - strong_migrations (2.2.0) + strong_migrations (2.2.1) activerecord (>= 7) swd (2.0.3) activesupport (>= 3) @@ -862,7 +868,7 @@ GEM unf (~> 0.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - tzinfo-data (1.2025.1) + tzinfo-data (1.2025.2) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext @@ -917,6 +923,7 @@ DEPENDENCIES active_model_serializers (~> 0.10) addressable (~> 2.8) annotaterb (~> 4.13) + aws-sdk-core (< 3.216.0) aws-sdk-s3 (~> 1.123) better_errors (~> 2.9) binding_of_caller (~> 1.0) @@ -1069,4 +1076,4 @@ RUBY VERSION ruby 3.4.1p0 BUNDLED WITH - 2.6.5 + 2.6.6 diff --git a/app/controllers/api/v1/accounts/identity_proofs_controller.rb b/app/controllers/api/v1/accounts/identity_proofs_controller.rb index 48f293f47a..02a45e8758 100644 --- a/app/controllers/api/v1/accounts/identity_proofs_controller.rb +++ b/app/controllers/api/v1/accounts/identity_proofs_controller.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true class Api::V1::Accounts::IdentityProofsController < Api::BaseController + include DeprecationConcern + + deprecate_api '2022-03-30' + before_action :require_user! before_action :set_account diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 6bef6a3768..46838aeb66 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -124,7 +124,7 @@ class Api::V1::AccountsController < Api::BaseController end def account_params - params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code) + params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code, :date_of_birth) end def invite diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb index c97e9720ad..f8d91c5f7f 100644 --- a/app/controllers/api/v1/filters_controller.rb +++ b/app/controllers/api/v1/filters_controller.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true class Api::V1::FiltersController < Api::BaseController + include DeprecationConcern + + deprecate_api '2022-11-14' + before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show] before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show] before_action :require_user! diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index 49da75ed28..e01267c000 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -1,15 +1,9 @@ # frozen_string_literal: true -class Api::V1::InstancesController < Api::BaseController - skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? - skip_around_action :set_locale +class Api::V1::InstancesController < Api::V2::InstancesController + include DeprecationConcern - vary_by '' - - # Override `current_user` to avoid reading session cookies unless in limited federation mode - def current_user - super if limited_federation_mode? - end + deprecate_api '2022-11-14' def show cache_even_if_authenticated! diff --git a/app/controllers/api/v1/suggestions_controller.rb b/app/controllers/api/v1/suggestions_controller.rb index 9ba1cef63c..918ec45beb 100644 --- a/app/controllers/api/v1/suggestions_controller.rb +++ b/app/controllers/api/v1/suggestions_controller.rb @@ -2,6 +2,9 @@ class Api::V1::SuggestionsController < Api::BaseController include Authorization + include DeprecationConcern + + deprecate_api '2021-05-16' before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb index 10a3442344..f84f1c0252 100644 --- a/app/controllers/api/v1/trends/tags_controller.rb +++ b/app/controllers/api/v1/trends/tags_controller.rb @@ -1,12 +1,16 @@ # frozen_string_literal: true class Api::V1::Trends::TagsController < Api::BaseController + include DeprecationConcern + before_action :set_tags after_action :insert_pagination_headers DEFAULT_TAGS_LIMIT = 10 + deprecate_api '2022-03-30', only: :index, if: -> { request.path == '/api/v1/trends' } + def index cache_if_unauthenticated! render json: @tags, each_serializer: REST::TagSerializer, relationships: TagRelationshipsPresenter.new(@tags, current_user&.account_id) diff --git a/app/controllers/api/v2/instances_controller.rb b/app/controllers/api/v2/instances_controller.rb index 8346e28830..62adf95260 100644 --- a/app/controllers/api/v2/instances_controller.rb +++ b/app/controllers/api/v2/instances_controller.rb @@ -1,6 +1,16 @@ # frozen_string_literal: true -class Api::V2::InstancesController < Api::V1::InstancesController +class Api::V2::InstancesController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? + skip_around_action :set_locale + + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in limited federation mode + def current_user + super if limited_federation_mode? + end + def show cache_even_if_authenticated! render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance' diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 6e34b6b627..0b6f5b3af4 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -62,7 +62,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController def configure_sign_up_params devise_parameter_sanitizer.permit(:sign_up) do |user_params| - user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password) + user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password, :date_of_birth) end end diff --git a/app/controllers/backups_controller.rb b/app/controllers/backups_controller.rb index 5df1af5f2f..076d19874b 100644 --- a/app/controllers/backups_controller.rb +++ b/app/controllers/backups_controller.rb @@ -9,13 +9,15 @@ class BackupsController < ApplicationController before_action :authenticate_user! before_action :set_backup + BACKUP_LINK_TIMEOUT = 1.hour.freeze + def download case Paperclip::Attachment.default_options[:storage] when :s3, :azure - redirect_to @backup.dump.expiring_url(10), allow_other_host: true + redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.to_i), allow_other_host: true when :fog if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present? - redirect_to @backup.dump.expiring_url(Time.now.utc + 10), allow_other_host: true + redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.from_now), allow_other_host: true else redirect_to full_asset_url(@backup.dump.url), allow_other_host: true end diff --git a/app/controllers/concerns/deprecation_concern.rb b/app/controllers/concerns/deprecation_concern.rb new file mode 100644 index 0000000000..ad8de724a1 --- /dev/null +++ b/app/controllers/concerns/deprecation_concern.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module DeprecationConcern + extend ActiveSupport::Concern + + class_methods do + def deprecate_api(date, sunset: nil, **kwargs) + deprecation_timestamp = "@#{date.to_datetime.to_i}" + sunset = sunset&.to_date&.httpdate + + before_action(**kwargs) do + response.headers['Deprecation'] = deprecation_timestamp + response.headers['Sunset'] = sunset if sunset + end + end + end +end diff --git a/app/helpers/admin/trends/statuses_helper.rb b/app/helpers/admin/trends/statuses_helper.rb index c7a59660cf..33da1f7216 100644 --- a/app/helpers/admin/trends/statuses_helper.rb +++ b/app/helpers/admin/trends/statuses_helper.rb @@ -2,11 +2,18 @@ module Admin::Trends::StatusesHelper def one_line_preview(status) - text = if status.local? - status.text.split("\n").first - else - Nokogiri::HTML5(status.text).css('html > body > *').first&.text - end + text = begin + if status.local? + status.text.split("\n").first + else + Nokogiri::HTML5(status.text).css('html > body > *').first&.text + end + rescue ArgumentError + # This can happen if one of the Nokogumbo limits is encountered + # Unfortunately, it does not use a more precise error class + # nor allows more graceful handling + '' + end return '' if text.blank? diff --git a/app/inputs/date_of_birth_input.rb b/app/inputs/date_of_birth_input.rb new file mode 100644 index 0000000000..131234b02e --- /dev/null +++ b/app/inputs/date_of_birth_input.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class DateOfBirthInput < SimpleForm::Inputs::Base + OPTIONS = [ + { autocomplete: 'bday-day', maxlength: 2, pattern: '[0-9]+', placeholder: 'DD' }.freeze, + { autocomplete: 'bday-month', maxlength: 2, pattern: '[0-9]+', placeholder: 'MM' }.freeze, + { autocomplete: 'bday-year', maxlength: 4, pattern: '[0-9]+', placeholder: 'YYYY' }.freeze, + ].freeze + + def input(wrapper_options = nil) + merged_input_options = merge_wrapper_options(input_html_options, wrapper_options) + merged_input_options[:inputmode] = 'numeric' + + values = (object.public_send(attribute_name) || '').split('.') + + safe_join(Array.new(3) do |index| + options = merged_input_options.merge(OPTIONS[index]).merge id: generate_id(index), 'aria-label': I18n.t("simple_form.labels.user.date_of_birth_#{index + 1}i"), value: values[index] + @builder.text_field("#{attribute_name}(#{index + 1}i)", options) + end) + end + + def label_target + "#{attribute_name}_1i" + end + + private + + def generate_id(index) + "#{object_name}_#{attribute_name}_#{index + 1}i" + end +end diff --git a/app/javascript/mastodon/actions/alerts.ts b/app/javascript/mastodon/actions/alerts.ts index a521f3ef35..4fd293e252 100644 --- a/app/javascript/mastodon/actions/alerts.ts +++ b/app/javascript/mastodon/actions/alerts.ts @@ -1,14 +1,11 @@ import { defineMessages } from 'react-intl'; -import type { MessageDescriptor } from 'react-intl'; + +import { createAction } from '@reduxjs/toolkit'; import { AxiosError } from 'axios'; import type { AxiosResponse } from 'axios'; -interface Alert { - title: string | MessageDescriptor; - message: string | MessageDescriptor; - values?: Record<string, string | number | Date>; -} +import type { Alert } from 'mastodon/models/alert'; interface ApiErrorResponse { error?: string; @@ -30,24 +27,13 @@ const messages = defineMessages({ }, }); -export const ALERT_SHOW = 'ALERT_SHOW'; -export const ALERT_DISMISS = 'ALERT_DISMISS'; -export const ALERT_CLEAR = 'ALERT_CLEAR'; -export const ALERT_NOOP = 'ALERT_NOOP'; +export const dismissAlert = createAction<{ key: number }>('alerts/dismiss'); -export const dismissAlert = (alert: Alert) => ({ - type: ALERT_DISMISS, - alert, -}); +export const clearAlerts = createAction('alerts/clear'); -export const clearAlert = () => ({ - type: ALERT_CLEAR, -}); +export const showAlert = createAction<Omit<Alert, 'key'>>('alerts/show'); -export const showAlert = (alert: Alert) => ({ - type: ALERT_SHOW, - alert, -}); +const ignoreAlert = createAction('alerts/ignore'); export const showAlertForError = (error: unknown, skipNotFound = false) => { if (error instanceof AxiosError && error.response) { @@ -56,7 +42,7 @@ export const showAlertForError = (error: unknown, skipNotFound = false) => { // Skip these errors as they are reflected in the UI if (skipNotFound && (status === 404 || status === 410)) { - return { type: ALERT_NOOP }; + return ignoreAlert(); } // Rate limit errors @@ -76,9 +62,9 @@ export const showAlertForError = (error: unknown, skipNotFound = false) => { }); } - // An aborted request, e.g. due to reloading the browser window, it not really error + // An aborted request, e.g. due to reloading the browser window, is not really an error if (error instanceof AxiosError && error.code === AxiosError.ECONNABORTED) { - return { type: ALERT_NOOP }; + return ignoreAlert(); } console.error(error); diff --git a/app/javascript/mastodon/api.ts b/app/javascript/mastodon/api.ts index f0663ded40..a41b058d2c 100644 --- a/app/javascript/mastodon/api.ts +++ b/app/javascript/mastodon/api.ts @@ -1,4 +1,9 @@ -import type { AxiosResponse, Method, RawAxiosRequestHeaders } from 'axios'; +import type { + AxiosError, + AxiosResponse, + Method, + RawAxiosRequestHeaders, +} from 'axios'; import axios from 'axios'; import LinkHeader from 'http-link-header'; @@ -41,7 +46,7 @@ const authorizationTokenFromInitialState = (): RawAxiosRequestHeaders => { // eslint-disable-next-line import/no-default-export export default function api(withAuthorization = true) { - return axios.create({ + const instance = axios.create({ transitional: { clarifyTimeoutError: true, }, @@ -60,6 +65,22 @@ export default function api(withAuthorization = true) { }, ], }); + + instance.interceptors.response.use( + (response: AxiosResponse) => { + if (response.headers.deprecation) { + console.warn( + `Deprecated request: ${response.config.method} ${response.config.url}`, + ); + } + return response; + }, + (error: AxiosError) => { + return Promise.reject(error); + }, + ); + + return instance; } type RequestParamsOrData = Record<string, unknown>; diff --git a/app/javascript/mastodon/components/alerts_controller.tsx b/app/javascript/mastodon/components/alerts_controller.tsx new file mode 100644 index 0000000000..26749fa103 --- /dev/null +++ b/app/javascript/mastodon/components/alerts_controller.tsx @@ -0,0 +1,105 @@ +import { useState, useEffect } from 'react'; + +import { useIntl } from 'react-intl'; +import type { IntlShape } from 'react-intl'; + +import classNames from 'classnames'; + +import { dismissAlert } from 'mastodon/actions/alerts'; +import type { + Alert, + TranslatableString, + TranslatableValues, +} from 'mastodon/models/alert'; +import { useAppSelector, useAppDispatch } from 'mastodon/store'; + +const formatIfNeeded = ( + intl: IntlShape, + message: TranslatableString, + values?: TranslatableValues, +) => { + if (typeof message === 'object') { + return intl.formatMessage(message, values); + } + + return message; +}; + +const Alert: React.FC<{ + alert: Alert; + dismissAfter: number; +}> = ({ + alert: { key, title, message, values, action, onClick }, + dismissAfter, +}) => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + const [active, setActive] = useState(false); + + useEffect(() => { + const setActiveTimeout = setTimeout(() => { + setActive(true); + }, 1); + + return () => { + clearTimeout(setActiveTimeout); + }; + }, []); + + useEffect(() => { + const dismissTimeout = setTimeout(() => { + setActive(false); + + // Allow CSS transition to finish before removing from the DOM + setTimeout(() => { + dispatch(dismissAlert({ key })); + }, 500); + }, dismissAfter); + + return () => { + clearTimeout(dismissTimeout); + }; + }, [dispatch, setActive, key, dismissAfter]); + + return ( + <div + className={classNames('notification-bar', { + 'notification-bar-active': active, + })} + > + <div className='notification-bar-wrapper'> + {title && ( + <span className='notification-bar-title'> + {formatIfNeeded(intl, title, values)} + </span> + )} + + <span className='notification-bar-message'> + {formatIfNeeded(intl, message, values)} + </span> + + {action && ( + <button className='notification-bar-action' onClick={onClick}> + {formatIfNeeded(intl, action, values)} + </button> + )} + </div> + </div> + ); +}; + +export const AlertsController: React.FC = () => { + const alerts = useAppSelector((state) => state.alerts); + + if (alerts.length === 0) { + return null; + } + + return ( + <div className='notification-list'> + {alerts.map((alert, idx) => ( + <Alert key={alert.key} alert={alert} dismissAfter={5000 + idx * 1000} /> + ))} + </div> + ); +}; diff --git a/app/javascript/mastodon/components/edited_timestamp/index.jsx b/app/javascript/mastodon/components/edited_timestamp/index.jsx index fbf14ec4bd..f8fa043259 100644 --- a/app/javascript/mastodon/components/edited_timestamp/index.jsx +++ b/app/javascript/mastodon/components/edited_timestamp/index.jsx @@ -6,6 +6,7 @@ import { FormattedMessage, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { openModal } from 'mastodon/actions/modal'; +import { FormattedDateWrapper } from 'mastodon/components/formatted_date'; import InlineAccount from 'mastodon/components/inline_account'; import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; @@ -60,12 +61,12 @@ class EditedTimestamp extends PureComponent { }; render () { - const { timestamp, intl, statusId } = this.props; + const { timestamp, statusId } = this.props; return ( <DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}> <button className='dropdown-menu__text-button'> - <FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: <span className='animated-number'>{intl.formatDate(timestamp, { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</span> }} /> + <FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: <FormattedDateWrapper className='animated-number' value={timestamp} month='short' day='2-digit' hour='2-digit' minute='2-digit' /> }} /> </button> </DropdownMenu> ); diff --git a/app/javascript/mastodon/components/formatted_date.tsx b/app/javascript/mastodon/components/formatted_date.tsx new file mode 100644 index 0000000000..cc927b0873 --- /dev/null +++ b/app/javascript/mastodon/components/formatted_date.tsx @@ -0,0 +1,26 @@ +import type { ComponentProps } from 'react'; + +import { FormattedDate } from 'react-intl'; + +export const FormattedDateWrapper = ( + props: ComponentProps<typeof FormattedDate> & { className?: string }, +) => ( + <FormattedDate {...props}> + {(date) => ( + <time dateTime={tryIsoString(props.value)} className={props.className}> + {date} + </time> + )} + </FormattedDate> +); + +const tryIsoString = (date?: string | number | Date): string => { + if (!date) { + return ''; + } + try { + return new Date(date).toISOString(); + } catch { + return date.toString(); + } +}; diff --git a/app/javascript/mastodon/components/media_gallery.jsx b/app/javascript/mastodon/components/media_gallery.jsx index fd8aa59b01..12cf381e5e 100644 --- a/app/javascript/mastodon/components/media_gallery.jsx +++ b/app/javascript/mastodon/components/media_gallery.jsx @@ -12,6 +12,7 @@ import { debounce } from 'lodash'; import { AltTextBadge } from 'mastodon/components/alt_text_badge'; import { Blurhash } from 'mastodon/components/blurhash'; +import { SpoilerButton } from 'mastodon/components/spoiler_button'; import { formatTime } from 'mastodon/features/video'; import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state'; @@ -38,6 +39,7 @@ class Item extends PureComponent { state = { loaded: false, + error: false, }; handleMouseEnter = (e) => { @@ -81,6 +83,10 @@ class Item extends PureComponent { this.setState({ loaded: true }); }; + handleImageError = () => { + this.setState({ error: true }); + }; + render () { const { attachment, lang, index, size, standalone, displayWidth, visible } = this.props; @@ -164,6 +170,7 @@ class Item extends PureComponent { lang={lang} style={{ objectPosition: `${x}% ${y}%` }} onLoad={this.handleImageLoad} + onError={this.handleImageError} /> </a> ); @@ -199,7 +206,7 @@ class Item extends PureComponent { } return ( - <div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}> + <div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--error': this.state.error, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}> <Blurhash hash={attachment.get('blurhash')} dummy={!useBlurhash} @@ -236,6 +243,7 @@ class MediaGallery extends PureComponent { autoplay: PropTypes.bool, onToggleVisibility: PropTypes.func, compact: PropTypes.bool, + matchedFilters: PropTypes.arrayOf(PropTypes.string), }; state = { @@ -306,11 +314,11 @@ class MediaGallery extends PureComponent { } render () { - const { media, lang, sensitive, defaultWidth, autoplay, compact } = this.props; + const { media, lang, sensitive, defaultWidth, autoplay, compact, matchedFilters } = this.props; const { visible } = this.state; const width = this.state.width || defaultWidth; - let children, spoilerButton; + let children; const style = {}; @@ -329,26 +337,6 @@ class MediaGallery extends PureComponent { children = media.map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} lang={lang} size={size} displayWidth={width} visible={visible || uncached} />); } - if (uncached) { - spoilerButton = ( - <button type='button' disabled className='spoiler-button__overlay'> - <span className='spoiler-button__overlay__label'> - <FormattedMessage id='status.uncached_media_warning' defaultMessage='Preview not available' /> - <span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.open' defaultMessage='Click to open' /></span> - </span> - </button> - ); - } else if (!visible) { - spoilerButton = ( - <button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'> - <span className='spoiler-button__overlay__label'> - {sensitive ? <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /> : <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />} - <span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span> - </span> - </button> - ); - } - const rowClass = (size === 5 || size === 6 || size === 9 || size === 10 || size === 11 || size === 12) ? 'media-gallery--row3' : (size === 7 || size === 8 || size === 13 || size === 14 || size === 15 || size === 16) ? 'media-gallery--row4' : 'media-gallery--row2'; @@ -366,11 +354,7 @@ class MediaGallery extends PureComponent { <div className={classNames(classList)} style={style} ref={this.handleRef}> {children} - {(!visible || uncached) && ( - <div className={classNames('spoiler-button', { 'spoiler-button--click-thru': uncached })}> - {spoilerButton} - </div> - )} + {(!visible || uncached) && <SpoilerButton uncached={uncached} sensitive={sensitive} onClick={this.handleOpen} matchedFilters={matchedFilters} />} {(visible && !uncached) && ( <div className='media-gallery__actions'> diff --git a/app/javascript/mastodon/components/spoiler_button.tsx b/app/javascript/mastodon/components/spoiler_button.tsx new file mode 100644 index 0000000000..bf84ffd04d --- /dev/null +++ b/app/javascript/mastodon/components/spoiler_button.tsx @@ -0,0 +1,89 @@ +import { FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +interface Props { + hidden?: boolean; + sensitive: boolean; + uncached?: boolean; + matchedFilters?: string[]; + onClick: React.MouseEventHandler<HTMLButtonElement>; +} + +export const SpoilerButton: React.FC<Props> = ({ + hidden = false, + sensitive, + uncached = false, + matchedFilters, + onClick, +}) => { + let warning; + let action; + + if (uncached) { + warning = ( + <FormattedMessage + id='status.uncached_media_warning' + defaultMessage='Preview not available' + /> + ); + action = ( + <FormattedMessage id='status.media.open' defaultMessage='Click to open' /> + ); + } else if (matchedFilters) { + warning = ( + <FormattedMessage + id='filter_warning.matches_filter' + defaultMessage='Matches filter “<span>{title}</span>”' + values={{ + title: matchedFilters.join(', '), + span: (chunks) => <span className='filter-name'>{chunks}</span>, + }} + /> + ); + action = ( + <FormattedMessage id='status.media.show' defaultMessage='Click to show' /> + ); + } else if (sensitive) { + warning = ( + <FormattedMessage + id='status.sensitive_warning' + defaultMessage='Sensitive content' + /> + ); + action = ( + <FormattedMessage id='status.media.show' defaultMessage='Click to show' /> + ); + } else { + warning = ( + <FormattedMessage + id='status.media_hidden' + defaultMessage='Media hidden' + /> + ); + action = ( + <FormattedMessage id='status.media.show' defaultMessage='Click to show' /> + ); + } + + return ( + <div + className={classNames('spoiler-button', { + 'spoiler-button--hidden': hidden, + 'spoiler-button--click-thru': uncached, + })} + > + <button + type='button' + className='spoiler-button__overlay' + onClick={onClick} + disabled={uncached} + > + <span className='spoiler-button__overlay__label'> + {warning} + <span className='spoiler-button__overlay__action'>{action}</span> + </span> + </button> + </div> + ); +}; diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 5c95cf46e1..0efea48f87 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -77,7 +77,7 @@ export const defaultMediaVisibility = (status) => { status = status.get('reblog'); } - return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all'); + return !status.get('matched_media_filters') && (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all'); }; const messages = defineMessages({ @@ -496,6 +496,7 @@ class Status extends ImmutablePureComponent { defaultWidth={this.props.cachedMediaWidth} visible={this.state.showMedia} onToggleVisibility={this.handleToggleMediaVisibility} + matchedFilters={status.get('matched_media_filters')} /> )} </Bundle> @@ -524,6 +525,7 @@ class Status extends ImmutablePureComponent { blurhash={attachment.get('blurhash')} visible={this.state.showMedia} onToggleVisibility={this.handleToggleMediaVisibility} + matchedFilters={status.get('matched_media_filters')} /> )} </Bundle> @@ -548,6 +550,7 @@ class Status extends ImmutablePureComponent { deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined} visible={this.state.showMedia} onToggleVisibility={this.handleToggleMediaVisibility} + matchedFilters={status.get('matched_media_filters')} /> )} </Bundle> diff --git a/app/javascript/mastodon/features/account/components/account_note.jsx b/app/javascript/mastodon/features/account/components/account_note.jsx index e736e7ad64..df7312eafc 100644 --- a/app/javascript/mastodon/features/account/components/account_note.jsx +++ b/app/javascript/mastodon/features/account/components/account_note.jsx @@ -4,7 +4,6 @@ import { PureComponent } from 'react'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { is } from 'immutable'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Textarea from 'react-textarea-autosize'; @@ -49,7 +48,7 @@ class InlineAlert extends PureComponent { class AccountNote extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.record.isRequired, + accountId: PropTypes.string.isRequired, value: PropTypes.string, onSave: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -66,7 +65,7 @@ class AccountNote extends ImmutablePureComponent { } UNSAFE_componentWillReceiveProps (nextProps) { - const accountWillChange = !is(this.props.account, nextProps.account); + const accountWillChange = !is(this.props.accountId, nextProps.accountId); const newState = {}; if (accountWillChange && this._isDirty()) { @@ -102,10 +101,10 @@ class AccountNote extends ImmutablePureComponent { if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { e.preventDefault(); - this._save(); - if (this.textarea) { this.textarea.blur(); + } else { + this._save(); } } else if (e.keyCode === 27) { e.preventDefault(); @@ -141,21 +140,21 @@ class AccountNote extends ImmutablePureComponent { } render () { - const { account, intl } = this.props; + const { accountId, intl } = this.props; const { value, saved } = this.state; - if (!account) { + if (!accountId) { return null; } return ( <div className='account__header__account-note'> - <label htmlFor={`account-note-${account.get('id')}`}> + <label htmlFor={`account-note-${accountId}`}> <FormattedMessage id='account.account_note_header' defaultMessage='Personal note' /> <InlineAlert show={saved} /> </label> <Textarea - id={`account-note-${account.get('id')}`} + id={`account-note-${accountId}`} className='account__header__account-note__content' disabled={this.props.value === null || value === null} placeholder={intl.formatMessage(messages.placeholder)} diff --git a/app/javascript/mastodon/features/account/containers/account_note_container.js b/app/javascript/mastodon/features/account/containers/account_note_container.js index 1530242d69..77de964039 100644 --- a/app/javascript/mastodon/features/account/containers/account_note_container.js +++ b/app/javascript/mastodon/features/account/containers/account_note_container.js @@ -4,14 +4,14 @@ import { submitAccountNote } from 'mastodon/actions/account_notes'; import AccountNote from '../components/account_note'; -const mapStateToProps = (state, { account }) => ({ - value: account.getIn(['relationship', 'note']), +const mapStateToProps = (state, { accountId }) => ({ + value: state.relationships.getIn([accountId, 'note']), }); -const mapDispatchToProps = (dispatch, { account }) => ({ +const mapDispatchToProps = (dispatch, { accountId }) => ({ onSave (value) { - dispatch(submitAccountNote({ accountId: account.get('id'), note: value })); + dispatch(submitAccountNote({ accountId: accountId, note: value })); }, }); diff --git a/app/javascript/mastodon/features/account_gallery/components/media_item.tsx b/app/javascript/mastodon/features/account_gallery/components/media_item.tsx index 3de2a29b18..0d251ff99f 100644 --- a/app/javascript/mastodon/features/account_gallery/components/media_item.tsx +++ b/app/javascript/mastodon/features/account_gallery/components/media_item.tsx @@ -26,11 +26,16 @@ export const MediaItem: React.FC<{ displayMedia === 'show_all', ); const [loaded, setLoaded] = useState(false); + const [error, setError] = useState(false); const handleImageLoad = useCallback(() => { setLoaded(true); }, [setLoaded]); + const handleImageError = useCallback(() => { + setError(true); + }, [setError]); + const handleMouseEnter = useCallback( (e: React.MouseEvent<HTMLVideoElement>) => { if (e.target instanceof HTMLVideoElement) { @@ -98,6 +103,7 @@ export const MediaItem: React.FC<{ alt={description} lang={lang} onLoad={handleImageLoad} + onError={handleImageError} /> <div className='media-gallery__item__overlay media-gallery__item__overlay--corner'> @@ -118,6 +124,7 @@ export const MediaItem: React.FC<{ lang={lang} style={{ objectPosition: `${x}% ${y}%` }} onLoad={handleImageLoad} + onError={handleImageError} /> ); } else if (['video', 'gifv'].includes(type)) { @@ -173,7 +180,11 @@ export const MediaItem: React.FC<{ } return ( - <div className='media-gallery__item media-gallery__item--square'> + <div + className={classNames('media-gallery__item media-gallery__item--square', { + 'media-gallery__item--error': error, + })} + > <Blurhash hash={blurhash} className={classNames('media-gallery__preview', { diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx index 723160c349..0d4f20795c 100644 --- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx @@ -44,6 +44,7 @@ import { FollowingCounter, StatusesCounter, } from 'mastodon/components/counters'; +import { FormattedDateWrapper } from 'mastodon/components/formatted_date'; import { getFeaturedHashtagBar } from 'mastodon/components/hashtag_bar'; import { Icon } from 'mastodon/components/icon'; import { IconButton } from 'mastodon/components/icon_button'; @@ -1020,7 +1021,7 @@ export const AccountHeader: React.FC<{ onClickCapture={handleLinkClick} > {account.id !== me && signedIn && ( - <AccountNoteContainer account={account} /> + <AccountNoteContainer accountId={accountId} /> )} {account.note.length > 0 && account.note !== '<p></p>' && ( @@ -1045,11 +1046,12 @@ export const AccountHeader: React.FC<{ /> </dt> <dd> - {intl.formatDate(account.created_at, { - year: 'numeric', - month: 'short', - day: '2-digit', - })} + <FormattedDateWrapper + value={account.created_at} + year='numeric' + month='short' + day='2-digit' + /> </dd> </dl> diff --git a/app/javascript/mastodon/features/audio/index.jsx b/app/javascript/mastodon/features/audio/index.jsx index 56c0e524ad..dc48756906 100644 --- a/app/javascript/mastodon/features/audio/index.jsx +++ b/app/javascript/mastodon/features/audio/index.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; import classNames from 'classnames'; @@ -16,6 +16,7 @@ import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?reac import VolumeOffIcon from '@/material-icons/400-24px/volume_off-fill.svg?react'; import VolumeUpIcon from '@/material-icons/400-24px/volume_up-fill.svg?react'; import { Icon } from 'mastodon/components/icon'; +import { SpoilerButton } from 'mastodon/components/spoiler_button'; import { formatTime, getPointerPosition, fileNameFromURL } from 'mastodon/features/video'; import { Blurhash } from '../../components/blurhash'; @@ -61,6 +62,7 @@ class Audio extends PureComponent { volume: PropTypes.number, muted: PropTypes.bool, deployPictureInPicture: PropTypes.func, + matchedFilters: PropTypes.arrayOf(PropTypes.string), }; state = { @@ -471,19 +473,11 @@ class Audio extends PureComponent { }; render () { - const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash } = this.props; + const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash, matchedFilters } = this.props; const { paused, volume, currentTime, duration, buffer, dragging, revealed } = this.state; const progress = Math.min((currentTime / duration) * 100, 100); const muted = this.state.muted || volume === 0; - let warning; - - if (sensitive) { - warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />; - } else { - warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />; - } - return ( <div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), aspectRatio: '16 / 9' }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}> @@ -521,14 +515,7 @@ class Audio extends PureComponent { lang={lang} /> - <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}> - <button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}> - <span className='spoiler-button__overlay__label'> - {warning} - <span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span> - </span> - </button> - </div> + <SpoilerButton hidden={revealed || editable} sensitive={sensitive} onClick={this.toggleReveal} matchedFilters={matchedFilters} /> {(revealed || editable) && <img src={this.props.poster} diff --git a/app/javascript/mastodon/features/bookmarked_statuses/index.jsx b/app/javascript/mastodon/features/bookmarked_statuses/index.jsx index 93be1c6b2e..284342d4ee 100644 --- a/app/javascript/mastodon/features/bookmarked_statuses/index.jsx +++ b/app/javascript/mastodon/features/bookmarked_statuses/index.jsx @@ -101,6 +101,7 @@ class Bookmarks extends ImmutablePureComponent { onLoadMore={this.handleLoadMore} emptyMessage={emptyMessage} bindToDocument={!multiColumn} + timelineId='bookmarks' /> <Helmet> diff --git a/app/javascript/mastodon/features/compose/components/language_dropdown.tsx b/app/javascript/mastodon/features/compose/components/language_dropdown.tsx index 21c4359981..d11891308f 100644 --- a/app/javascript/mastodon/features/compose/components/language_dropdown.tsx +++ b/app/javascript/mastodon/features/compose/components/language_dropdown.tsx @@ -378,6 +378,7 @@ export const LanguageDropdown: React.FC = () => { if (text.length > 20) { debouncedGuess(text, setGuess); } else { + debouncedGuess.cancel(); setGuess(''); } }, [text, setGuess]); diff --git a/app/javascript/mastodon/features/favourited_statuses/index.jsx b/app/javascript/mastodon/features/favourited_statuses/index.jsx index f7d6d14178..9049a20f05 100644 --- a/app/javascript/mastodon/features/favourited_statuses/index.jsx +++ b/app/javascript/mastodon/features/favourited_statuses/index.jsx @@ -101,6 +101,7 @@ class Favourites extends ImmutablePureComponent { onLoadMore={this.handleLoadMore} emptyMessage={emptyMessage} bindToDocument={!multiColumn} + timelineId='favourites' /> <Helmet> diff --git a/app/javascript/mastodon/features/privacy_policy/index.tsx b/app/javascript/mastodon/features/privacy_policy/index.tsx index f0309c2712..cd6f9f3b2b 100644 --- a/app/javascript/mastodon/features/privacy_policy/index.tsx +++ b/app/javascript/mastodon/features/privacy_policy/index.tsx @@ -1,17 +1,13 @@ import { useState, useEffect } from 'react'; -import { - FormattedMessage, - FormattedDate, - useIntl, - defineMessages, -} from 'react-intl'; +import { FormattedMessage, useIntl, defineMessages } from 'react-intl'; import { Helmet } from 'react-helmet'; import { apiGetPrivacyPolicy } from 'mastodon/api/instance'; import type { ApiPrivacyPolicyJSON } from 'mastodon/api_types/instance'; import { Column } from 'mastodon/components/column'; +import { FormattedDateWrapper } from 'mastodon/components/formatted_date'; import { Skeleton } from 'mastodon/components/skeleton'; const messages = defineMessages({ @@ -58,7 +54,7 @@ const PrivacyPolicy: React.FC<{ date: loading ? ( <Skeleton width='10ch' /> ) : ( - <FormattedDate + <FormattedDateWrapper value={response?.updated_at} year='numeric' month='short' diff --git a/app/javascript/mastodon/features/standalone/compose/index.jsx b/app/javascript/mastodon/features/standalone/compose/index.jsx index 241d9aadfd..3aff78ffee 100644 --- a/app/javascript/mastodon/features/standalone/compose/index.jsx +++ b/app/javascript/mastodon/features/standalone/compose/index.jsx @@ -1,12 +1,12 @@ +import { AlertsController } from 'mastodon/components/alerts_controller'; import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container'; import LoadingBarContainer from 'mastodon/features/ui/containers/loading_bar_container'; import ModalContainer from 'mastodon/features/ui/containers/modal_container'; -import NotificationsContainer from 'mastodon/features/ui/containers/notifications_container'; const Compose = () => ( <> <ComposeFormContainer autoFocus withoutNavigation /> - <NotificationsContainer /> + <AlertsController /> <ModalContainer /> <LoadingBarContainer className='loading-bar' /> </> diff --git a/app/javascript/mastodon/features/status/components/detailed_status.tsx b/app/javascript/mastodon/features/status/components/detailed_status.tsx index c8f082156e..a113d71d99 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.tsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.tsx @@ -6,7 +6,7 @@ import type { CSSProperties } from 'react'; import { useState, useRef, useCallback } from 'react'; -import { FormattedDate, FormattedMessage } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import { Link } from 'react-router-dom'; @@ -15,6 +15,8 @@ import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?re import { AnimatedNumber } from 'mastodon/components/animated_number'; import { ContentWarning } from 'mastodon/components/content_warning'; import EditedTimestamp from 'mastodon/components/edited_timestamp'; +import { FilterWarning } from 'mastodon/components/filter_warning'; +import { FormattedDateWrapper } from 'mastodon/components/formatted_date'; import type { StatusLike } from 'mastodon/components/hashtag_bar'; import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar'; import { Icon } from 'mastodon/components/icon'; @@ -80,6 +82,7 @@ export const DetailedStatus: React.FC<{ }) => { const properStatus = status?.get('reblog') ?? status; const [height, setHeight] = useState(0); + const [showDespiteFilter, setShowDespiteFilter] = useState(false); const nodeRef = useRef<HTMLDivElement>(); const handleOpenVideo = useCallback( @@ -92,6 +95,10 @@ export const DetailedStatus: React.FC<{ [onOpenVideo, status], ); + const handleFilterToggle = useCallback(() => { + setShowDespiteFilter(!showDespiteFilter); + }, [showDespiteFilter, setShowDespiteFilter]); + const handleExpandedToggle = useCallback(() => { if (onToggleHidden) onToggleHidden(status); }, [onToggleHidden, status]); @@ -180,6 +187,7 @@ export const DetailedStatus: React.FC<{ onOpenMedia={onOpenMedia} visible={showMedia} onToggleVisibility={onToggleMediaVisibility} + matchedFilters={status.get('matched_media_filters')} /> ); } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { @@ -206,6 +214,7 @@ export const DetailedStatus: React.FC<{ blurhash={attachment.get('blurhash')} height={150} onToggleVisibility={onToggleMediaVisibility} + matchedFilters={status.get('matched_media_filters')} /> ); } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { @@ -229,6 +238,7 @@ export const DetailedStatus: React.FC<{ sensitive={status.get('sensitive')} visible={showMedia} onToggleVisibility={onToggleMediaVisibility} + matchedFilters={status.get('matched_media_filters')} /> ); } @@ -369,8 +379,12 @@ export const DetailedStatus: React.FC<{ const { statusContentProps, hashtagBar } = getHashtagBarForStatus( status as StatusLike, ); + + const matchedFilters = status.get('matched_filters'); + const expanded = - !status.get('hidden') || status.get('spoiler_text').length === 0; + (!matchedFilters || showDespiteFilter) && + (!status.get('hidden') || status.get('spoiler_text').length === 0); const quote = !muted && status.get('quote_id') && ( <> @@ -418,17 +432,26 @@ export const DetailedStatus: React.FC<{ )} </Link> - {status.get('spoiler_text').length > 0 && ( - <ContentWarning - text={ - status.getIn(['translation', 'spoilerHtml']) || - status.get('spoilerHtml') - } - expanded={expanded} - onClick={handleExpandedToggle} + {matchedFilters && ( + <FilterWarning + title={matchedFilters.join(', ')} + expanded={showDespiteFilter} + onClick={handleFilterToggle} /> )} + {status.get('spoiler_text').length > 0 && + (!matchedFilters || showDespiteFilter) && ( + <ContentWarning + text={ + status.getIn(['translation', 'spoilerHtml']) || + status.get('spoilerHtml') + } + expanded={expanded} + onClick={handleExpandedToggle} + /> + )} + {expanded && ( <> <StatusContent @@ -452,7 +475,7 @@ export const DetailedStatus: React.FC<{ target='_blank' rel='noopener noreferrer' > - <FormattedDate + <FormattedDateWrapper value={new Date(status.get('created_at') as string)} year='numeric' month='short' diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 08a2a74e88..e9d0c9f8d6 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -147,7 +147,7 @@ const makeMapStateToProps = () => { }); const mapStateToProps = (state, props) => { - const status = getStatus(state, { id: props.params.statusId }); + const status = getStatus(state, { id: props.params.statusId, contextType: 'detailed' }); let ancestorsIds = ImmutableList(); let descendantsIds = ImmutableList(); diff --git a/app/javascript/mastodon/features/ui/components/image_loader.jsx b/app/javascript/mastodon/features/ui/components/image_loader.jsx deleted file mode 100644 index b1417deda7..0000000000 --- a/app/javascript/mastodon/features/ui/components/image_loader.jsx +++ /dev/null @@ -1,175 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import classNames from 'classnames'; - -import { LoadingBar } from 'react-redux-loading-bar'; - -import ZoomableImage from './zoomable_image'; - -export default class ImageLoader extends PureComponent { - - static propTypes = { - alt: PropTypes.string, - lang: PropTypes.string, - src: PropTypes.string.isRequired, - previewSrc: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - onClick: PropTypes.func, - zoomedIn: PropTypes.bool, - }; - - static defaultProps = { - alt: '', - lang: '', - width: null, - height: null, - }; - - state = { - loading: true, - error: false, - width: null, - }; - - removers = []; - canvas = null; - - get canvasContext() { - if (!this.canvas) { - return null; - } - this._canvasContext = this._canvasContext || this.canvas.getContext('2d'); - return this._canvasContext; - } - - componentDidMount () { - this.loadImage(this.props); - } - - UNSAFE_componentWillReceiveProps (nextProps) { - if (this.props.src !== nextProps.src) { - this.loadImage(nextProps); - } - } - - componentWillUnmount () { - this.removeEventListeners(); - } - - loadImage (props) { - this.removeEventListeners(); - this.setState({ loading: true, error: false }); - Promise.all([ - props.previewSrc && this.loadPreviewCanvas(props), - this.hasSize() && this.loadOriginalImage(props), - ].filter(Boolean)) - .then(() => { - this.setState({ loading: false, error: false }); - this.clearPreviewCanvas(); - }) - .catch(() => this.setState({ loading: false, error: true })); - } - - loadPreviewCanvas = ({ previewSrc, width, height }) => new Promise((resolve, reject) => { - const image = new Image(); - const removeEventListeners = () => { - image.removeEventListener('error', handleError); - image.removeEventListener('load', handleLoad); - }; - const handleError = () => { - removeEventListeners(); - reject(); - }; - const handleLoad = () => { - removeEventListeners(); - this.canvasContext.drawImage(image, 0, 0, width, height); - resolve(); - }; - image.addEventListener('error', handleError); - image.addEventListener('load', handleLoad); - image.src = previewSrc; - this.removers.push(removeEventListeners); - }); - - clearPreviewCanvas () { - const { width, height } = this.canvas; - this.canvasContext.clearRect(0, 0, width, height); - } - - loadOriginalImage = ({ src }) => new Promise((resolve, reject) => { - const image = new Image(); - const removeEventListeners = () => { - image.removeEventListener('error', handleError); - image.removeEventListener('load', handleLoad); - }; - const handleError = () => { - removeEventListeners(); - reject(); - }; - const handleLoad = () => { - removeEventListeners(); - resolve(); - }; - image.addEventListener('error', handleError); - image.addEventListener('load', handleLoad); - image.src = src; - this.removers.push(removeEventListeners); - }); - - removeEventListeners () { - this.removers.forEach(listeners => listeners()); - this.removers = []; - } - - hasSize () { - const { width, height } = this.props; - return typeof width === 'number' && typeof height === 'number'; - } - - setCanvasRef = c => { - this.canvas = c; - if (c) this.setState({ width: c.offsetWidth }); - }; - - render () { - const { alt, lang, src, width, height, onClick, zoomedIn } = this.props; - const { loading } = this.state; - - const className = classNames('image-loader', { - 'image-loader--loading': loading, - 'image-loader--amorphous': !this.hasSize(), - }); - - return ( - <div className={className}> - {loading ? ( - <> - <div className='loading-bar__container' style={{ width: this.state.width || width }}> - <LoadingBar className='loading-bar' loading={1} /> - </div> - - <canvas - className='image-loader__preview-canvas' - ref={this.setCanvasRef} - width={width} - height={height} - /> - </> - ) : ( - <ZoomableImage - alt={alt} - lang={lang} - src={src} - onClick={onClick} - width={width} - height={height} - zoomedIn={zoomedIn} - /> - )} - </div> - ); - } - -} diff --git a/app/javascript/mastodon/features/ui/components/image_modal.jsx b/app/javascript/mastodon/features/ui/components/image_modal.jsx deleted file mode 100644 index f08ce15342..0000000000 --- a/app/javascript/mastodon/features/ui/components/image_modal.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { defineMessages, injectIntl } from 'react-intl'; - -import classNames from 'classnames'; - -import CloseIcon from '@/material-icons/400-24px/close.svg?react'; -import { IconButton } from 'mastodon/components/icon_button'; - -import ImageLoader from './image_loader'; - -const messages = defineMessages({ - close: { id: 'lightbox.close', defaultMessage: 'Close' }, -}); - -class ImageModal extends PureComponent { - - static propTypes = { - src: PropTypes.string.isRequired, - alt: PropTypes.string.isRequired, - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - state = { - navigationHidden: false, - }; - - toggleNavigation = () => { - this.setState(prevState => ({ - navigationHidden: !prevState.navigationHidden, - })); - }; - - render () { - const { intl, src, alt, onClose } = this.props; - const { navigationHidden } = this.state; - - const navigationClassName = classNames('media-modal__navigation', { - 'media-modal__navigation--hidden': navigationHidden, - }); - - return ( - <div className='modal-root__modal media-modal'> - <div className='media-modal__closer' role='presentation' onClick={onClose} > - <ImageLoader - src={src} - width={400} - height={400} - alt={alt} - onClick={this.toggleNavigation} - /> - </div> - - <div className={navigationClassName}> - <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' iconComponent={CloseIcon} onClick={onClose} size={40} /> - </div> - </div> - ); - } - -} - -export default injectIntl(ImageModal); diff --git a/app/javascript/mastodon/features/ui/components/image_modal.tsx b/app/javascript/mastodon/features/ui/components/image_modal.tsx new file mode 100644 index 0000000000..fa94cfcc3c --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/image_modal.tsx @@ -0,0 +1,61 @@ +import { useCallback, useState } from 'react'; + +import { defineMessages, useIntl } from 'react-intl'; + +import classNames from 'classnames'; + +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; +import { IconButton } from 'mastodon/components/icon_button'; + +import { ZoomableImage } from './zoomable_image'; + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' }, +}); + +export const ImageModal: React.FC<{ + src: string; + alt: string; + onClose: () => void; +}> = ({ src, alt, onClose }) => { + const intl = useIntl(); + const [navigationHidden, setNavigationHidden] = useState(false); + + const toggleNavigation = useCallback(() => { + setNavigationHidden((prevState) => !prevState); + }, [setNavigationHidden]); + + const navigationClassName = classNames('media-modal__navigation', { + 'media-modal__navigation--hidden': navigationHidden, + }); + + return ( + <div className='modal-root__modal media-modal'> + <div + className='media-modal__closer' + role='presentation' + onClick={onClose} + > + <ZoomableImage + src={src} + width={400} + height={400} + alt={alt} + onClick={toggleNavigation} + /> + </div> + + <div className={navigationClassName}> + <div className='media-modal__buttons'> + <IconButton + className='media-modal__close' + title={intl.formatMessage(messages.close)} + icon='times' + iconComponent={CloseIcon} + onClick={onClose} + /> + </div> + </div> + </div> + ); +}; diff --git a/app/javascript/mastodon/features/ui/components/media_modal.jsx b/app/javascript/mastodon/features/ui/components/media_modal.jsx index d69ceba539..9312805b5c 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/media_modal.jsx @@ -22,7 +22,7 @@ import Footer from 'mastodon/features/picture_in_picture/components/footer'; import Video from 'mastodon/features/video'; import { disableSwiping } from 'mastodon/initial_state'; -import ImageLoader from './image_loader'; +import { ZoomableImage } from './zoomable_image'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -59,6 +59,12 @@ class MediaModal extends ImmutablePureComponent { })); }; + handleZoomChange = (zoomedIn) => { + this.setState({ + zoomedIn, + }); + }; + handleSwipe = (index) => { this.setState({ index: index % this.props.media.size, @@ -165,23 +171,26 @@ class MediaModal extends ImmutablePureComponent { const leftNav = media.size > 1 && <button tabIndex={0} className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><Icon id='chevron-left' icon={ChevronLeftIcon} /></button>; const rightNav = media.size > 1 && <button tabIndex={0} className='media-modal__nav media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><Icon id='chevron-right' icon={ChevronRightIcon} /></button>; - const content = media.map((image) => { + const content = media.map((image, idx) => { const width = image.getIn(['meta', 'original', 'width']) || null; const height = image.getIn(['meta', 'original', 'height']) || null; const description = image.getIn(['translation', 'description']) || image.get('description'); if (image.get('type') === 'image') { return ( - <ImageLoader - previewSrc={image.get('preview_url')} + <ZoomableImage src={image.get('url')} + blurhash={image.get('blurhash')} width={width} height={height} alt={description} lang={lang} key={image.get('url')} onClick={this.handleToggleNavigation} - zoomedIn={zoomedIn} + onDoubleClick={this.handleZoomClick} + onClose={onClose} + onZoomChange={this.handleZoomChange} + zoomedIn={zoomedIn && idx === index} /> ); } else if (image.get('type') === 'video') { @@ -262,7 +271,7 @@ class MediaModal extends ImmutablePureComponent { onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleTransitionEnd} index={index} - disabled={disableSwiping} + disabled={disableSwiping || zoomedIn} > {content} </ReactSwipeableViews> diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx index 5b2c2fc8d7..4dac46681d 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.jsx +++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx @@ -45,7 +45,7 @@ import { ConfirmFollowToListModal, ConfirmMissingAltTextModal, } from './confirmation_modals'; -import ImageModal from './image_modal'; +import { ImageModal } from './image_modal'; import MediaModal from './media_modal'; import { ModalPlaceholder } from './modal_placeholder'; import VideoModal from './video_modal'; diff --git a/app/javascript/mastodon/features/ui/components/zoomable_image.jsx b/app/javascript/mastodon/features/ui/components/zoomable_image.jsx deleted file mode 100644 index c4129bf260..0000000000 --- a/app/javascript/mastodon/features/ui/components/zoomable_image.jsx +++ /dev/null @@ -1,402 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -const MIN_SCALE = 1; -const MAX_SCALE = 4; -const NAV_BAR_HEIGHT = 66; - -const getMidpoint = (p1, p2) => ({ - x: (p1.clientX + p2.clientX) / 2, - y: (p1.clientY + p2.clientY) / 2, -}); - -const getDistance = (p1, p2) => - Math.sqrt(Math.pow(p1.clientX - p2.clientX, 2) + Math.pow(p1.clientY - p2.clientY, 2)); - -const clamp = (min, max, value) => Math.min(max, Math.max(min, value)); - -// Normalizing mousewheel speed across browsers -// copy from: https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js -const normalizeWheel = event => { - // Reasonable defaults - const PIXEL_STEP = 10; - const LINE_HEIGHT = 40; - const PAGE_HEIGHT = 800; - - let sX = 0, - sY = 0, // spinX, spinY - pX = 0, - pY = 0; // pixelX, pixelY - - // Legacy - if ('detail' in event) { - sY = event.detail; - } - if ('wheelDelta' in event) { - sY = -event.wheelDelta / 120; - } - if ('wheelDeltaY' in event) { - sY = -event.wheelDeltaY / 120; - } - if ('wheelDeltaX' in event) { - sX = -event.wheelDeltaX / 120; - } - - // side scrolling on FF with DOMMouseScroll - if ('axis' in event && event.axis === event.HORIZONTAL_AXIS) { - sX = sY; - sY = 0; - } - - pX = sX * PIXEL_STEP; - pY = sY * PIXEL_STEP; - - if ('deltaY' in event) { - pY = event.deltaY; - } - if ('deltaX' in event) { - pX = event.deltaX; - } - - if ((pX || pY) && event.deltaMode) { - if (event.deltaMode === 1) { // delta in LINE units - pX *= LINE_HEIGHT; - pY *= LINE_HEIGHT; - } else { // delta in PAGE units - pX *= PAGE_HEIGHT; - pY *= PAGE_HEIGHT; - } - } - - // Fall-back if spin cannot be determined - if (pX && !sX) { - sX = (pX < 1) ? -1 : 1; - } - if (pY && !sY) { - sY = (pY < 1) ? -1 : 1; - } - - return { - spinX: sX, - spinY: sY, - pixelX: pX, - pixelY: pY, - }; -}; - -class ZoomableImage extends PureComponent { - - static propTypes = { - alt: PropTypes.string, - lang: PropTypes.string, - src: PropTypes.string.isRequired, - width: PropTypes.number, - height: PropTypes.number, - onClick: PropTypes.func, - zoomedIn: PropTypes.bool, - }; - - static defaultProps = { - alt: '', - lang: '', - width: null, - height: null, - }; - - state = { - scale: MIN_SCALE, - zoomMatrix: { - type: null, // 'width' 'height' - fullScreen: null, // bool - rate: null, // full screen scale rate - clientWidth: null, - clientHeight: null, - offsetWidth: null, - offsetHeight: null, - clientHeightFixed: null, - scrollTop: null, - scrollLeft: null, - translateX: null, - translateY: null, - }, - dragPosition: { top: 0, left: 0, x: 0, y: 0 }, - dragged: false, - lockScroll: { x: 0, y: 0 }, - lockTranslate: { x: 0, y: 0 }, - }; - - removers = []; - container = null; - image = null; - lastTouchEndTime = 0; - lastDistance = 0; - - componentDidMount () { - let handler = this.handleTouchStart; - this.container.addEventListener('touchstart', handler); - this.removers.push(() => this.container.removeEventListener('touchstart', handler)); - handler = this.handleTouchMove; - // on Chrome 56+, touch event listeners will default to passive - // https://www.chromestatus.com/features/5093566007214080 - this.container.addEventListener('touchmove', handler, { passive: false }); - this.removers.push(() => this.container.removeEventListener('touchend', handler)); - - handler = this.mouseDownHandler; - this.container.addEventListener('mousedown', handler); - this.removers.push(() => this.container.removeEventListener('mousedown', handler)); - - handler = this.mouseWheelHandler; - this.container.addEventListener('wheel', handler); - this.removers.push(() => this.container.removeEventListener('wheel', handler)); - // Old Chrome - this.container.addEventListener('mousewheel', handler); - this.removers.push(() => this.container.removeEventListener('mousewheel', handler)); - // Old Firefox - this.container.addEventListener('DOMMouseScroll', handler); - this.removers.push(() => this.container.removeEventListener('DOMMouseScroll', handler)); - - this._initZoomMatrix(); - } - - componentWillUnmount () { - this._removeEventListeners(); - } - - componentDidUpdate (prevProps) { - if (prevProps.zoomedIn !== this.props.zoomedIn) { - this._toggleZoom(); - } - } - - _removeEventListeners () { - this.removers.forEach(listeners => listeners()); - this.removers = []; - } - - mouseWheelHandler = e => { - e.preventDefault(); - - const event = normalizeWheel(e); - - if (this.state.zoomMatrix.type === 'width') { - // full width, scroll vertical - this.container.scrollTop = Math.max(this.container.scrollTop + event.pixelY, this.state.lockScroll.y); - } else { - // full height, scroll horizontal - this.container.scrollLeft = Math.max(this.container.scrollLeft + event.pixelY, this.state.lockScroll.x); - } - - // lock horizontal scroll - this.container.scrollLeft = Math.max(this.container.scrollLeft + event.pixelX, this.state.lockScroll.x); - }; - - mouseDownHandler = e => { - this.setState({ dragPosition: { - left: this.container.scrollLeft, - top: this.container.scrollTop, - // Get the current mouse position - x: e.clientX, - y: e.clientY, - } }); - - this.image.addEventListener('mousemove', this.mouseMoveHandler); - this.image.addEventListener('mouseup', this.mouseUpHandler); - }; - - mouseMoveHandler = e => { - const dx = e.clientX - this.state.dragPosition.x; - const dy = e.clientY - this.state.dragPosition.y; - - this.container.scrollLeft = Math.max(this.state.dragPosition.left - dx, this.state.lockScroll.x); - this.container.scrollTop = Math.max(this.state.dragPosition.top - dy, this.state.lockScroll.y); - - this.setState({ dragged: true }); - }; - - mouseUpHandler = () => { - this.image.removeEventListener('mousemove', this.mouseMoveHandler); - this.image.removeEventListener('mouseup', this.mouseUpHandler); - }; - - handleTouchStart = e => { - if (e.touches.length !== 2) return; - - this.lastDistance = getDistance(...e.touches); - }; - - handleTouchMove = e => { - const { scrollTop, scrollHeight, clientHeight } = this.container; - if (e.touches.length === 1 && scrollTop !== scrollHeight - clientHeight) { - // prevent propagating event to MediaModal - e.stopPropagation(); - return; - } - if (e.touches.length !== 2) return; - - e.preventDefault(); - e.stopPropagation(); - - const distance = getDistance(...e.touches); - const midpoint = getMidpoint(...e.touches); - const _MAX_SCALE = Math.max(MAX_SCALE, this.state.zoomMatrix.rate); - const scale = clamp(MIN_SCALE, _MAX_SCALE, this.state.scale * distance / this.lastDistance); - - this._zoom(scale, midpoint); - - this.lastMidpoint = midpoint; - this.lastDistance = distance; - }; - - _zoom(nextScale, midpoint) { - const { scale, zoomMatrix } = this.state; - const { scrollLeft, scrollTop } = this.container; - - // math memo: - // x = (scrollLeft + midpoint.x) / scrollWidth - // x' = (nextScrollLeft + midpoint.x) / nextScrollWidth - // scrollWidth = clientWidth * scale - // scrollWidth' = clientWidth * nextScale - // Solve x = x' for nextScrollLeft - const nextScrollLeft = (scrollLeft + midpoint.x) * nextScale / scale - midpoint.x; - const nextScrollTop = (scrollTop + midpoint.y) * nextScale / scale - midpoint.y; - - this.setState({ scale: nextScale }, () => { - this.container.scrollLeft = nextScrollLeft; - this.container.scrollTop = nextScrollTop; - // reset the translateX/Y constantly - if (nextScale < zoomMatrix.rate) { - this.setState({ - lockTranslate: { - x: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateX * ((nextScale - MIN_SCALE) / (zoomMatrix.rate - MIN_SCALE)), - y: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateY * ((nextScale - MIN_SCALE) / (zoomMatrix.rate - MIN_SCALE)), - }, - }); - } - }); - } - - handleClick = e => { - // don't propagate event to MediaModal - e.stopPropagation(); - const dragged = this.state.dragged; - this.setState({ dragged: false }); - if (dragged) return; - const handler = this.props.onClick; - if (handler) handler(); - }; - - handleMouseDown = e => { - e.preventDefault(); - }; - - _initZoomMatrix = () => { - const { width, height } = this.props; - const { clientWidth, clientHeight } = this.container; - const { offsetWidth, offsetHeight } = this.image; - const clientHeightFixed = clientHeight - NAV_BAR_HEIGHT; - - const type = width / height < clientWidth / clientHeightFixed ? 'width' : 'height'; - const fullScreen = type === 'width' ? width > clientWidth : height > clientHeightFixed; - const rate = type === 'width' ? Math.min(clientWidth, width) / offsetWidth : Math.min(clientHeightFixed, height) / offsetHeight; - const scrollTop = type === 'width' ? (clientHeight - offsetHeight) / 2 - NAV_BAR_HEIGHT : (clientHeightFixed - offsetHeight) / 2; - const scrollLeft = (clientWidth - offsetWidth) / 2; - const translateX = type === 'width' ? (width - offsetWidth) / (2 * rate) : 0; - const translateY = type === 'height' ? (height - offsetHeight) / (2 * rate) : 0; - - this.setState({ - zoomMatrix: { - type: type, - fullScreen: fullScreen, - rate: rate, - clientWidth: clientWidth, - clientHeight: clientHeight, - offsetWidth: offsetWidth, - offsetHeight: offsetHeight, - clientHeightFixed: clientHeightFixed, - scrollTop: scrollTop, - scrollLeft: scrollLeft, - translateX: translateX, - translateY: translateY, - }, - }); - }; - - _toggleZoom () { - const { scale, zoomMatrix } = this.state; - - if ( scale >= zoomMatrix.rate ) { - this.setState({ - scale: MIN_SCALE, - lockScroll: { - x: 0, - y: 0, - }, - lockTranslate: { - x: 0, - y: 0, - }, - }, () => { - this.container.scrollLeft = 0; - this.container.scrollTop = 0; - }); - } else { - this.setState({ - scale: zoomMatrix.rate, - lockScroll: { - x: zoomMatrix.scrollLeft, - y: zoomMatrix.scrollTop, - }, - lockTranslate: { - x: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateX, - y: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateY, - }, - }, () => { - this.container.scrollLeft = zoomMatrix.scrollLeft; - this.container.scrollTop = zoomMatrix.scrollTop; - }); - } - } - - setContainerRef = c => { - this.container = c; - }; - - setImageRef = c => { - this.image = c; - }; - - render () { - const { alt, lang, src, width, height } = this.props; - const { scale, lockTranslate, dragged } = this.state; - const overflow = scale === MIN_SCALE ? 'hidden' : 'scroll'; - const cursor = scale === MIN_SCALE ? null : (dragged ? 'grabbing' : 'grab'); - - return ( - <div - className='zoomable-image' - ref={this.setContainerRef} - style={{ overflow, cursor, userSelect: 'none' }} - > - <img - role='presentation' - ref={this.setImageRef} - alt={alt} - title={alt} - lang={lang} - src={src} - width={width} - height={height} - style={{ - transform: `scale(${scale}) translate(-${lockTranslate.x}px, -${lockTranslate.y}px)`, - transformOrigin: '0 0', - }} - draggable={false} - onClick={this.handleClick} - onMouseDown={this.handleMouseDown} - /> - </div> - ); - } -} - -export default ZoomableImage; diff --git a/app/javascript/mastodon/features/ui/components/zoomable_image.tsx b/app/javascript/mastodon/features/ui/components/zoomable_image.tsx new file mode 100644 index 0000000000..85e29e6aea --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/zoomable_image.tsx @@ -0,0 +1,319 @@ +import { useState, useCallback, useRef, useEffect } from 'react'; + +import classNames from 'classnames'; + +import { useSpring, animated, config } from '@react-spring/web'; +import { createUseGesture, dragAction, pinchAction } from '@use-gesture/react'; + +import { Blurhash } from 'mastodon/components/blurhash'; +import { LoadingIndicator } from 'mastodon/components/loading_indicator'; + +const MIN_SCALE = 1; +const MAX_SCALE = 4; +const DOUBLE_CLICK_THRESHOLD = 250; + +interface ZoomMatrix { + containerWidth: number; + containerHeight: number; + imageWidth: number; + imageHeight: number; + initialScale: number; +} + +const createZoomMatrix = ( + container: HTMLElement, + image: HTMLImageElement, + fullWidth: number, + fullHeight: number, +): ZoomMatrix => { + const { clientWidth, clientHeight } = container; + const { offsetWidth, offsetHeight } = image; + + const type = + fullWidth / fullHeight < clientWidth / clientHeight ? 'width' : 'height'; + + const initialScale = + type === 'width' + ? Math.min(clientWidth, fullWidth) / offsetWidth + : Math.min(clientHeight, fullHeight) / offsetHeight; + + return { + containerWidth: clientWidth, + containerHeight: clientHeight, + imageWidth: offsetWidth, + imageHeight: offsetHeight, + initialScale, + }; +}; + +const useGesture = createUseGesture([dragAction, pinchAction]); + +const getBounds = (zoomMatrix: ZoomMatrix | null, scale: number) => { + if (!zoomMatrix || scale === MIN_SCALE) { + return { + left: -Infinity, + right: Infinity, + top: -Infinity, + bottom: Infinity, + }; + } + + const { containerWidth, containerHeight, imageWidth, imageHeight } = + zoomMatrix; + + const bounds = { + left: -Math.max(imageWidth * scale - containerWidth, 0) / 2, + right: Math.max(imageWidth * scale - containerWidth, 0) / 2, + top: -Math.max(imageHeight * scale - containerHeight, 0) / 2, + bottom: Math.max(imageHeight * scale - containerHeight, 0) / 2, + }; + + return bounds; +}; + +interface ZoomableImageProps { + alt?: string; + lang?: string; + src: string; + width: number; + height: number; + onClick?: () => void; + onDoubleClick?: () => void; + onClose?: () => void; + onZoomChange?: (zoomedIn: boolean) => void; + zoomedIn?: boolean; + blurhash?: string; +} + +export const ZoomableImage: React.FC<ZoomableImageProps> = ({ + alt = '', + lang = '', + src, + width, + height, + onClick, + onDoubleClick, + onClose, + onZoomChange, + zoomedIn, + blurhash, +}) => { + useEffect(() => { + const handler = (e: Event) => { + e.preventDefault(); + }; + + document.addEventListener('gesturestart', handler); + document.addEventListener('gesturechange', handler); + document.addEventListener('gestureend', handler); + + return () => { + document.removeEventListener('gesturestart', handler); + document.removeEventListener('gesturechange', handler); + document.removeEventListener('gestureend', handler); + }; + }, []); + + const [dragging, setDragging] = useState(false); + const [loaded, setLoaded] = useState(false); + const [error, setError] = useState(false); + + const containerRef = useRef<HTMLDivElement>(null); + const imageRef = useRef<HTMLImageElement>(null); + const doubleClickTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(); + const zoomMatrixRef = useRef<ZoomMatrix | null>(null); + + const [style, api] = useSpring(() => ({ + x: 0, + y: 0, + scale: 1, + onRest: { + scale({ value }) { + if (!onZoomChange) { + return; + } + if (value === MIN_SCALE) { + onZoomChange(false); + } else { + onZoomChange(true); + } + }, + }, + })); + + useGesture( + { + onDrag({ + pinching, + cancel, + active, + last, + offset: [x, y], + velocity: [, vy], + direction: [, dy], + tap, + }) { + if (tap) { + if (!doubleClickTimeoutRef.current) { + doubleClickTimeoutRef.current = setTimeout(() => { + onClick?.(); + doubleClickTimeoutRef.current = null; + }, DOUBLE_CLICK_THRESHOLD); + } else { + clearTimeout(doubleClickTimeoutRef.current); + doubleClickTimeoutRef.current = null; + onDoubleClick?.(); + } + + return; + } + + if (!zoomedIn) { + // Swipe up/down to dismiss parent + if (last) { + if ((vy > 0.5 && dy !== 0) || Math.abs(y) > 150) { + onClose?.(); + } + + void api.start({ y: 0, config: config.wobbly }); + return; + } else if (dy !== 0) { + void api.start({ y, immediate: true }); + return; + } + + cancel(); + return; + } + + if (pinching) { + cancel(); + return; + } + + if (active) { + setDragging(true); + } else { + setDragging(false); + } + + void api.start({ x, y }); + }, + + onPinch({ origin: [ox, oy], first, movement: [ms], offset: [s], memo }) { + if (!imageRef.current) { + return; + } + + if (first) { + const { width, height, x, y } = + imageRef.current.getBoundingClientRect(); + const tx = ox - (x + width / 2); + const ty = oy - (y + height / 2); + + memo = [style.x.get(), style.y.get(), tx, ty]; + } + + const x = memo[0] - (ms - 1) * memo[2]; // eslint-disable-line @typescript-eslint/no-unsafe-member-access + const y = memo[1] - (ms - 1) * memo[3]; // eslint-disable-line @typescript-eslint/no-unsafe-member-access + + void api.start({ scale: s, x, y }); + + return memo as [number, number, number, number]; + }, + }, + { + target: imageRef, + drag: { + from: () => [style.x.get(), style.y.get()], + filterTaps: true, + bounds: () => getBounds(zoomMatrixRef.current, style.scale.get()), + rubberband: true, + }, + pinch: { + scaleBounds: { + min: MIN_SCALE, + max: MAX_SCALE, + }, + rubberband: true, + }, + }, + ); + + useEffect(() => { + if (!loaded || !containerRef.current || !imageRef.current) { + return; + } + + zoomMatrixRef.current = createZoomMatrix( + containerRef.current, + imageRef.current, + width, + height, + ); + + if (!zoomedIn) { + void api.start({ scale: MIN_SCALE, x: 0, y: 0 }); + } else if (style.scale.get() === MIN_SCALE) { + void api.start({ scale: zoomMatrixRef.current.initialScale, x: 0, y: 0 }); + } + }, [api, style.scale, zoomedIn, width, height, loaded]); + + const handleClick = useCallback((e: React.MouseEvent) => { + // This handler exists to cancel the onClick handler on the media modal which would + // otherwise close the modal. It cannot be used for actual click handling because + // we don't know if the user is about to pan the image or not. + + e.preventDefault(); + e.stopPropagation(); + }, []); + + const handleLoad = useCallback(() => { + setLoaded(true); + }, [setLoaded]); + + const handleError = useCallback(() => { + setError(true); + }, [setError]); + + return ( + <div + className={classNames('zoomable-image', { + 'zoomable-image--zoomed-in': zoomedIn, + 'zoomable-image--error': error, + 'zoomable-image--dragging': dragging, + })} + ref={containerRef} + > + {!loaded && blurhash && ( + <div + className='zoomable-image__preview' + style={{ + aspectRatio: `${width}/${height}`, + height: `min(${height}px, 100%)`, + }} + > + <Blurhash hash={blurhash} /> + </div> + )} + + <animated.img + style={style} + role='presentation' + ref={imageRef} + alt={alt} + title={alt} + lang={lang} + src={src} + width={width} + height={height} + draggable={false} + onLoad={handleLoad} + onError={handleError} + onClickCapture={handleClick} + /> + + {!loaded && !error && <LoadingIndicator />} + </div> + ); +}; diff --git a/app/javascript/mastodon/features/ui/containers/notifications_container.js b/app/javascript/mastodon/features/ui/containers/notifications_container.js deleted file mode 100644 index b8aa9bc461..0000000000 --- a/app/javascript/mastodon/features/ui/containers/notifications_container.js +++ /dev/null @@ -1,20 +0,0 @@ -import { injectIntl } from 'react-intl'; - -import { connect } from 'react-redux'; - -import { NotificationStack } from 'react-notification'; - -import { dismissAlert } from 'mastodon/actions/alerts'; -import { getAlerts } from 'mastodon/selectors'; - -const mapStateToProps = (state, { intl }) => ({ - notifications: getAlerts(state, { intl }), -}); - -const mapDispatchToProps = (dispatch) => ({ - onDismiss (alert) { - dispatch(dismissAlert(alert)); - }, -}); - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack)); diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index d43b6d7b01..8538817b78 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -15,6 +15,7 @@ import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app'; import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers'; import { fetchNotifications } from 'mastodon/actions/notification_groups'; import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding'; +import { AlertsController } from 'mastodon/components/alerts_controller'; import { HoverCardController } from 'mastodon/components/hover_card_controller'; import { PictureInPicture } from 'mastodon/features/picture_in_picture'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; @@ -33,7 +34,6 @@ import UploadArea from './components/upload_area'; import ColumnsAreaContainer from './containers/columns_area_container'; import LoadingBarContainer from './containers/loading_bar_container'; import ModalContainer from './containers/modal_container'; -import NotificationsContainer from './containers/notifications_container'; import { Compose, Status, @@ -656,7 +656,7 @@ class UI extends PureComponent { </SwitchingColumnsArea> {layout !== 'mobile' && <PictureInPicture />} - <NotificationsContainer /> + <AlertsController /> {!disableHoverCards && <HoverCardController />} <LoadingBarContainer className='loading-bar' /> <ModalContainer /> diff --git a/app/javascript/mastodon/features/video/index.jsx b/app/javascript/mastodon/features/video/index.jsx index 89a8ba560a..7459b94a92 100644 --- a/app/javascript/mastodon/features/video/index.jsx +++ b/app/javascript/mastodon/features/video/index.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; import classNames from 'classnames'; @@ -19,6 +19,7 @@ import VolumeOffIcon from '@/material-icons/400-24px/volume_off-fill.svg?react'; import VolumeUpIcon from '@/material-icons/400-24px/volume_up-fill.svg?react'; import { Blurhash } from 'mastodon/components/blurhash'; import { Icon } from 'mastodon/components/icon'; +import { SpoilerButton } from 'mastodon/components/spoiler_button'; import { playerSettings } from 'mastodon/settings'; import { displayMedia, useBlurhash } from '../../initial_state'; @@ -135,6 +136,7 @@ class Video extends PureComponent { muted: PropTypes.bool, componentIndex: PropTypes.number, autoFocus: PropTypes.bool, + matchedFilters: PropTypes.arrayOf(PropTypes.string), }; static defaultProps = { @@ -534,7 +536,7 @@ class Video extends PureComponent { } render () { - const { preview, src, aspectRatio, onOpenVideo, onCloseVideo, intl, alt, lang, detailed, sensitive, editable, blurhash, autoFocus } = this.props; + const { preview, src, aspectRatio, onOpenVideo, onCloseVideo, intl, alt, lang, detailed, sensitive, editable, blurhash, autoFocus, matchedFilters } = this.props; const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, revealed } = this.state; const progress = Math.min((currentTime / duration) * 100, 100); const muted = this.state.muted || volume === 0; @@ -549,14 +551,6 @@ class Video extends PureComponent { preload = 'none'; } - let warning; - - if (sensitive) { - warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />; - } else { - warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />; - } - // The outer wrapper is necessary to avoid reflowing the layout when going into full screen return ( <div style={{ aspectRatio }}> @@ -599,14 +593,7 @@ class Video extends PureComponent { style={{ width: '100%' }} />} - <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}> - <button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}> - <span className='spoiler-button__overlay__label'> - {warning} - <span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span> - </span> - </button> - </div> + <SpoilerButton hidden={revealed || editable} sensitive={sensitive} onClick={this.toggleReveal} matchedFilters={matchedFilters} /> <div className={classNames('video-player__controls', { active: paused || hovered })}> <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}> diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json index 9829986174..dde569bf5e 100644 --- a/app/javascript/mastodon/locales/br.json +++ b/app/javascript/mastodon/locales/br.json @@ -81,9 +81,14 @@ "alert.rate_limited.title": "Feur bevennet", "alert.unexpected.message": "Ur fazi dic'hortozet zo degouezhet.", "alert.unexpected.title": "Hopala !", + "alt_text_modal.cancel": "Nullañ", + "alt_text_modal.change_thumbnail": "Kemmañ ar velvenn", + "alt_text_modal.done": "Graet", "announcement.announcement": "Kemennad", "annual_report.summary.followers.followers": "heulier", "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.summary.most_used_hashtag.none": "Hini ebet", + "annual_report.summary.new_posts.new_posts": "toudoù nevez", "attachments_list.unprocessed": "(ket meret)", "audio.hide": "Kuzhat ar c'hleved", "block_modal.show_less": "Diskouez nebeutoc'h", @@ -109,9 +114,11 @@ "column.blocks": "Implijer·ezed·ien berzet", "column.bookmarks": "Sinedoù", "column.community": "Red-amzer lec'hel", + "column.create_list": "Krouiñ ul listenn", "column.direct": "Menegoù prevez", "column.directory": "Mont a-dreuz ar profiloù", "column.domain_blocks": "Domani berzet", + "column.edit_list": "Kemmañ al listenn", "column.favourites": "Muiañ-karet", "column.firehose": "Redoù war-eeun", "column.follow_requests": "Rekedoù heuliañ", @@ -162,9 +169,12 @@ "confirmations.delete.message": "Ha sur oc'h e fell deoc'h dilemel an toud-mañ ?", "confirmations.delete_list.confirm": "Dilemel", "confirmations.delete_list.message": "Ha sur eo hoc'h eus c'hoant da zilemel ar roll-mañ da vat ?", + "confirmations.delete_list.title": "Dilemel al listenn?", "confirmations.discard_edit_media.confirm": "Nac'hañ", "confirmations.discard_edit_media.message": "Bez ez eus kemmoù n'int ket enrollet e deskrivadur ar media pe ar rakwel, nullañ anezho evelato?", "confirmations.edit.confirm": "Kemmañ", + "confirmations.edit.message": "Kemmañ bremañ a zilamo ar gemennadenn emaoc'h o skrivañ. Sur e oc'h e fell deoc'h kenderc'hel ganti?", + "confirmations.follow_to_list.title": "Heuliañ an implijer·ez?", "confirmations.logout.confirm": "Digevreañ", "confirmations.logout.message": "Ha sur oc'h e fell deoc'h digevreañ ?", "confirmations.mute.confirm": "Kuzhat", @@ -266,8 +276,10 @@ "footer.privacy_policy": "Reolennoù prevezded", "footer.source_code": "Gwelet ar c'hod mammenn", "footer.status": "Statud", + "footer.terms_of_service": "Divizoù implijout hollek", "generic.saved": "Enrollet", "getting_started.heading": "Loc'hañ", + "hashtag.admin_moderation": "Digeriñ an etrefas evezhiañ evit #{name}", "hashtag.column_header.tag_mode.all": "ha(g) {additional}", "hashtag.column_header.tag_mode.any": "pe {additional}", "hashtag.column_header.tag_mode.none": "hep {additional}", @@ -337,8 +349,14 @@ "limited_account_hint.action": "Diskouez an aelad memes tra", "limited_account_hint.title": "Kuzhet eo bet ar profil-mañ gant an evezhierien eus {domain}.", "link_preview.author": "Gant {name}", + "lists.add_member": "Ouzhpennañ", + "lists.add_to_list": "Ouzhpennañ d'al listenn", + "lists.create": "Krouiñ", + "lists.create_list": "Krouiñ ul listenn", "lists.delete": "Dilemel al listenn", + "lists.done": "Graet", "lists.edit": "Kemmañ al listenn", + "lists.list_name": "Anv al listenn", "lists.replies_policy.followed": "Pep implijer.ez heuliet", "lists.replies_policy.list": "Izili ar roll", "lists.replies_policy.none": "Den ebet", @@ -373,11 +391,17 @@ "notification.follow": "heuliañ a ra {name} ac'hanoc'h", "notification.follow.name_and_others": "{name} <a>{count, plural, one {hag # den all} two {ha # zen all} few {ha # den all} many {ha # den all} other {ha # den all}}</a> zo o heuliañ ac'hanoc'h", "notification.follow_request": "Gant {name} eo bet goulennet ho heuliañ", + "notification.label.reply": "Respont", "notification.moderation-warning.learn_more": "Gouzout hiroc'h", "notification.own_poll": "Echu eo ho sontadeg", "notification.reblog": "Gant {name} eo bet skignet ho toud", + "notification.relationships_severance_event.learn_more": "Gouzout hiroc'h", "notification.status": "Emañ {name} o paouez toudañ", "notification.update": "Gant {name} ez eus bet kemmet un toud", + "notification_requests.accept": "Asantiñ", + "notification_requests.dismiss": "Diverkañ", + "notification_requests.edit_selection": "Kemmañ", + "notification_requests.exit_selection": "Graet", "notifications.clear": "Skarzhañ ar c'hemennoù", "notifications.clear_confirmation": "Ha sur oc'h e fell deoc'h skarzhañ ho holl kemennoù ?", "notifications.column_settings.admin.report": "Disklêriadurioù nevez :", @@ -410,6 +434,10 @@ "notifications.permission_denied": "Kemennoù war ar burev n'int ket hegerz rak pedadenn aotren ar merdeer a zo bet nullet araok", "notifications.permission_denied_alert": "Kemennoù wa ar burev na c'hellont ket bezañ lezelet, rak aotre ar merdeer a zo bet nac'het a-raok", "notifications.permission_required": "Kemennoù war ar burev n'int ket hegerz abalamour d'an aotre rekis n'eo ket bet roet.", + "notifications.policy.accept": "Asantiñ", + "notifications.policy.accept_hint": "Diskouez er c’hemennoù", + "notifications.policy.drop": "Tremen e-bioù", + "notifications.policy.filter": "Silañ", "notifications.policy.filter_new_accounts_title": "Kontoù nevez", "notifications_permission_banner.enable": "Lezel kemennoù war ar burev", "notifications_permission_banner.how_to_control": "Evit reseviñ kemennoù pa ne vez ket digoret Mastodon, lezelit kemennoù war ar burev. Gallout a rit kontrollañ peseurt eskemmoù a c'henel kemennoù war ar burev gant ar {icon} nozelenn a-us kentre ma'z int lezelet.", @@ -515,6 +543,7 @@ "search_results.accounts": "Profiloù", "search_results.all": "Pep tra", "search_results.hashtags": "Hashtagoù", + "search_results.no_results": "Disoc'h ebet.", "search_results.see_all": "Gwelet pep tra", "search_results.statuses": "Toudoù", "server_banner.active_users": "implijerien·ezed oberiant", @@ -579,6 +608,7 @@ "subscribed_languages.target": "Cheñch ar yezhoù koumanantet evit {target}", "tabs_bar.home": "Degemer", "tabs_bar.notifications": "Kemennoù", + "terms_of_service.title": "Divizoù implijout", "time_remaining.days": "{number, plural,one {# devezh} other {# a zevezh}} a chom", "time_remaining.hours": "{number, plural, one {# eurvezh} other{# eurvezh}} a chom", "time_remaining.minutes": "{number, plural, one {# munut} other{# a vunut}} a chom", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index 501283833d..aff66c97f8 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -110,7 +110,7 @@ "annual_report.summary.most_used_hashtag.most_used_hashtag": "nejpoužívanější hashtag", "annual_report.summary.most_used_hashtag.none": "Žádné", "annual_report.summary.new_posts.new_posts": "nové příspěvky", - "annual_report.summary.percentile.text": "<topLabel>To vás umisťuje do vrcholu</topLabel><percentage></percentage><bottomLabel>{domain} uživatelů.</bottomLabel>", + "annual_report.summary.percentile.text": "<topLabel>To vás umisťuje do horních</topLabel><percentage></percentage><bottomLabel> uživatelů domény {domain}.</bottomLabel>", "annual_report.summary.percentile.we_wont_tell_bernie": "To, že jste zdejší smetánka, zůstane mezi námi ;).", "annual_report.summary.thanks": "Děkujeme, že jste součástí Mastodonu!", "attachments_list.unprocessed": "(nezpracováno)", @@ -513,9 +513,9 @@ "loading_indicator.label": "Načítání…", "media_gallery.hide": "Skrýt", "moved_to_account_banner.text": "Váš účet {disabledAccount} je momentálně deaktivován, protože jste se přesunul/a na {movedToAccount}.", - "mute_modal.hide_from_notifications": "Skrýt z notifikací", + "mute_modal.hide_from_notifications": "Skrýt z oznámení", "mute_modal.hide_options": "Skrýt možnosti", - "mute_modal.indefinite": "Dokud je neodkryju", + "mute_modal.indefinite": "Dokud je neodeberu ze ztišených", "mute_modal.show_options": "Zobrazit možnosti", "mute_modal.they_can_mention_and_follow": "Mohou vás zmínit a sledovat, ale neuvidíte je.", "mute_modal.they_wont_know": "Nebudou vědět, že byli skryti.", @@ -524,7 +524,7 @@ "mute_modal.you_wont_see_posts": "Stále budou moci vidět vaše příspěvky, ale vy jejich neuvidíte.", "navigation_bar.about": "O aplikaci", "navigation_bar.administration": "Administrace", - "navigation_bar.advanced_interface": "Otevřít pokročilé webové rozhraní", + "navigation_bar.advanced_interface": "Otevřít v pokročilém webovém rozhraní", "navigation_bar.blocks": "Blokovaní uživatelé", "navigation_bar.bookmarks": "Záložky", "navigation_bar.community_timeline": "Místní časová osa", @@ -553,13 +553,13 @@ "notification.admin.report": "Uživatel {name} nahlásil {target}", "notification.admin.report_account": "{name} nahlásil {count, plural, one {jeden příspěvek} few {# příspěvky} many {# příspěvků} other {# příspěvků}} od {target} za {category}", "notification.admin.report_account_other": "{name} nahlásil {count, plural, one {jeden příspěvek} few {# příspěvky} many {# příspěvků} other {# příspěvků}} od {target}", - "notification.admin.report_statuses": "{name} nahlásil {target} za {category}", - "notification.admin.report_statuses_other": "{name} nahlásil {target}", + "notification.admin.report_statuses": "{name} nahlásili {target} za {category}", + "notification.admin.report_statuses_other": "{name} nahlásili {target}", "notification.admin.sign_up": "Uživatel {name} se zaregistroval", "notification.admin.sign_up.name_and_others": "{name} a {count, plural, one {# další} few {# další} many {# dalších} other {# dalších}} se zaregistrovali", "notification.annual_report.message": "Váš #Wrapstodon {year} na Vás čeká! Podívejte se, jak vypadal tento Váš rok na Mastodonu!", "notification.annual_report.view": "Zobrazit #Wrapstodon", - "notification.favourite": "Uživatel {name} si oblíbil váš příspěvek", + "notification.favourite": "{name} si oblíbil*a váš příspěvek", "notification.favourite.name_and_others_with_link": "{name} a {count, plural, one {<a># další</a> si oblíbil} few {<a># další</a> si oblíbili} other {<a># dalších</a> si oblíbilo}} Váš příspěvek", "notification.favourite_pm": "{name} si oblíbil vaši soukromou zmínku", "notification.favourite_pm.name_and_others_with_link": "{name} a {count, plural, one {<a># další</a> si oblíbil} few {<a># další</a> si oblíbili} other {<a># dalších</a> si oblíbilo}} Vaši soukromou zmínku", @@ -578,7 +578,7 @@ "notification.moderation_warning.action_delete_statuses": "Některé z vašich příspěvků byly odstraněny.", "notification.moderation_warning.action_disable": "Váš účet je zablokován.", "notification.moderation_warning.action_mark_statuses_as_sensitive": "Některé z vašich příspěvků byly označeny jako citlivé.", - "notification.moderation_warning.action_none": "Váš účet obdržel moderační varování.", + "notification.moderation_warning.action_none": "Váš účet obdržel varování od moderátorů.", "notification.moderation_warning.action_sensitive": "Vaše příspěvky budou od nynějška označeny jako citlivé.", "notification.moderation_warning.action_silence": "Váš účet byl omezen.", "notification.moderation_warning.action_suspend": "Váš účet byl pozastaven.", @@ -610,7 +610,7 @@ "notification_requests.maximize": "Maximalizovat", "notification_requests.minimize_banner": "Minimalizovat banner filtrovaných oznámení", "notification_requests.notifications_from": "Oznámení od {name}", - "notification_requests.title": "Vyfiltrovaná oznámení", + "notification_requests.title": "Filtrovaná oznámení", "notification_requests.view": "Zobrazit oznámení", "notifications.clear": "Vyčistit oznámení", "notifications.clear_confirmation": "Opravdu chcete trvale smazat všechna vaše oznámení?", @@ -773,7 +773,7 @@ "report_notification.categories.spam": "Spam", "report_notification.categories.spam_sentence": "spam", "report_notification.categories.violation": "Porušení pravidla", - "report_notification.categories.violation_sentence": "porušení pravidla", + "report_notification.categories.violation_sentence": "porušení pravidel", "report_notification.open": "Otevřít hlášení", "search.no_recent_searches": "Žádná nedávná vyhledávání", "search.placeholder": "Hledat", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index a3749a0e42..2aa662960e 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -16,7 +16,7 @@ "account.badges.bot": "Awtomataidd", "account.badges.group": "Grŵp", "account.block": "Blocio @{name}", - "account.block_domain": "Blocio parth {domain}", + "account.block_domain": "Blocio'r parth {domain}", "account.block_short": "Blocio", "account.blocked": "Blociwyd", "account.cancel_follow_request": "Tynnu cais i ddilyn", @@ -41,7 +41,7 @@ "account.go_to_profile": "Mynd i'r proffil", "account.hide_reblogs": "Cuddio hybiau gan @{name}", "account.in_memoriam": "Er Cof.", - "account.joined_short": "Wedi Ymuno", + "account.joined_short": "Ymunodd", "account.languages": "Newid ieithoedd wedi tanysgrifio iddyn nhw", "account.link_verified_on": "Gwiriwyd perchnogaeth y ddolen yma ar {date}", "account.locked_info": "Mae'r statws preifatrwydd cyfrif hwn wedi'i osod i fod ar glo. Mae'r perchennog yn adolygu'r sawl sy'n gallu eu dilyn.", @@ -56,7 +56,7 @@ "account.no_bio": "Dim disgrifiad wedi'i gynnig.", "account.open_original_page": "Agor y dudalen wreiddiol", "account.posts": "Postiadau", - "account.posts_with_replies": "Postiadau ac atebion", + "account.posts_with_replies": "Postiadau ac ymatebion", "account.report": "Adrodd @{name}", "account.requested": "Aros am gymeradwyaeth. Cliciwch er mwyn canslo cais dilyn", "account.requested_follow": "Mae {name} wedi gwneud cais i'ch dilyn", @@ -85,7 +85,7 @@ "alert.rate_limited.title": "Cyfradd gyfyngedig", "alert.unexpected.message": "Digwyddodd gwall annisgwyl.", "alert.unexpected.title": "Wps!", - "alt_text_badge.title": "Testun Amgen", + "alt_text_badge.title": "Testun amgen", "alt_text_modal.add_alt_text": "Ychwanegu testun amgen", "alt_text_modal.add_text_from_image": "Ychwanegu testun o'r ddelwedd", "alt_text_modal.cancel": "Diddymu", @@ -110,7 +110,7 @@ "annual_report.summary.most_used_hashtag.most_used_hashtag": "hashnod a ddefnyddiwyd fwyaf", "annual_report.summary.most_used_hashtag.none": "Dim", "annual_report.summary.new_posts.new_posts": "postiadau newydd", - "annual_report.summary.percentile.text": "<topLabel>Mae hynny'n eich rhoi chi ar y brig</topLabel><percentage></percentage><bottomLabel> o ddefnyddiwr {domain}.</bottomLabel>", + "annual_report.summary.percentile.text": "<topLabel>Mae hynny'n eich rhoi chi ymysg y</topLabel><percentage></percentage><bottomLabel>uchaf o ddefnyddwyr {domain}.</bottomLabel>", "annual_report.summary.percentile.we_wont_tell_bernie": "Ni fyddwn yn dweud wrth Bernie.", "annual_report.summary.thanks": "Diolch am fod yn rhan o Mastodon!", "attachments_list.unprocessed": "(heb eu prosesu)", @@ -142,7 +142,7 @@ "closed_registrations_modal.description": "Ar hyn o bryd nid yw'n bosib creu cyfrif ar {domain}, ond cadwch mewn cof nad oes raid i chi gael cyfrif yn benodol ar {domain} i ddefnyddio Mastodon.", "closed_registrations_modal.find_another_server": "Dod o hyd i weinydd arall", "closed_registrations_modal.preamble": "Mae Mastodon wedi'i ddatganoli, felly does dim gwahaniaeth ble rydych chi'n creu eich cyfrif, byddwch chi'n gallu dilyn a rhyngweithio ag unrhyw un ar y gweinydd hwn. Gallwch hyd yn oed ei gynnal un eich hun!", - "closed_registrations_modal.title": "Ymgofrestru ar Mastodon", + "closed_registrations_modal.title": "Cofrestru ar Mastodon", "column.about": "Ynghylch", "column.blocks": "Defnyddwyr a flociwyd", "column.bookmarks": "Llyfrnodau", @@ -192,9 +192,9 @@ "compose_form.poll.switch_to_multiple": "Newid pleidlais i adael mwy nag un dewis", "compose_form.poll.switch_to_single": "Newid pleidlais i gyfyngu i un dewis", "compose_form.poll.type": "Arddull", - "compose_form.publish": "Postiad", + "compose_form.publish": "Postio", "compose_form.publish_form": "Postiad newydd", - "compose_form.reply": "Ateb", + "compose_form.reply": "Ymateb", "compose_form.save_changes": "Diweddaru", "compose_form.spoiler.marked": "Dileu rhybudd cynnwys", "compose_form.spoiler.unmarked": "Ychwanegu rhybudd cynnwys", @@ -226,7 +226,7 @@ "confirmations.redraft.confirm": "Dileu ac ailddrafftio", "confirmations.redraft.message": "Ydych chi wir eisiau'r dileu'r postiad hwn a'i ailddrafftio? Bydd ffefrynnau a hybiau'n cael eu colli, a bydd atebion i'r post gwreiddiol yn mynd yn amddifad.", "confirmations.redraft.title": "Dileu & ailddraftio postiad?", - "confirmations.reply.confirm": "Ateb", + "confirmations.reply.confirm": "Ymateb", "confirmations.reply.message": "Bydd ateb nawr yn cymryd lle y neges yr ydych yn cyfansoddi ar hyn o bryd. Ydych chi'n siŵr eich bod am barhau?", "confirmations.reply.title": "Trosysgrifo'r postiad?", "confirmations.unfollow.confirm": "Dad-ddilyn", @@ -248,8 +248,8 @@ "directory.recently_active": "Ar-lein yn ddiweddar", "disabled_account_banner.account_settings": "Gosodiadau'r cyfrif", "disabled_account_banner.text": "Mae eich cyfrif {disabledAccount} wedi ei analluogi ar hyn o bryd.", - "dismissable_banner.community_timeline": "Dyma'r postiadau cyhoeddus diweddaraf gan bobl sydd â chyfrifon ar {domain}.", - "dismissable_banner.dismiss": "Cau", + "dismissable_banner.community_timeline": "Dyma'r postiadau cyhoeddus diweddaraf gan bobl y caiff eu cyfrifon eu cynnal ar {domain}.", + "dismissable_banner.dismiss": "Diystyru", "dismissable_banner.explore_links": "Y straeon newyddion hyn yw'r rhai sy'n cael eu rhannu fwyaf ar y ffederasiwn heddiw. Mae straeon newyddion mwy diweddar sy'n cael eu postio gan fwy o amrywiaeth o bobl yn cael eu graddio'n uwch.", "dismissable_banner.explore_statuses": "Mae'r postiadau hyn o bob rhan o'r ffedysawd yn cael mwy o sylw heddiw. Mae postiadau mwy diweddar sydd â mwy o hybu a ffefrynnu'n cael eu graddio'n uwch.", "dismissable_banner.explore_tags": "Mae'r hashnodau hyn ar gynnydd y ffedysawd heddiw. Mae hashnodau sy'n cael eu defnyddio gan fwy o bobl amrywiol yn cael eu graddio'n uwch.", @@ -398,10 +398,10 @@ "hints.profiles.see_more_followers": "Gweld mwy o ddilynwyr ar {domain}", "hints.profiles.see_more_follows": "Gweld mwy o 'yn dilyn' ar {domain}", "hints.profiles.see_more_posts": "Gweld mwy o bostiadau ar {domain}", - "hints.threads.replies_may_be_missing": "Mae'n bosibl y bydd atebion gan weinyddion eraill ar goll.", - "hints.threads.see_more": "Gweld mwy o atebion ar {domain}", + "hints.threads.replies_may_be_missing": "Mae'n bosibl y bydd ymatebion gan weinyddion eraill ar goll.", + "hints.threads.see_more": "Gweld mwy o ymatebion ar {domain}", "home.column_settings.show_reblogs": "Dangos hybiau", - "home.column_settings.show_replies": "Dangos atebion", + "home.column_settings.show_replies": "Dangos ymatebion", "home.hide_announcements": "Cuddio cyhoeddiadau", "home.pending_critical_update.body": "Diweddarwch eich gweinydd Mastodon cyn gynted â phosibl!", "home.pending_critical_update.link": "Gweld diweddariadau", @@ -423,7 +423,7 @@ "interaction_modal.action.favourite": "I barhau, mae angen i chi hoffi o'ch cyfrif.", "interaction_modal.action.follow": "I barhau, mae angen i chi ddilyn o'ch cyfrif.", "interaction_modal.action.reblog": "I barhau, mae angen i chi ail-flogio o'ch cyfrif.", - "interaction_modal.action.reply": "I barhau, mae angen i chi ateb o'ch cyfrif.", + "interaction_modal.action.reply": "I barhau, mae angen i chi ymateb o'ch cyfrif.", "interaction_modal.action.vote": "I barhau, mae angen i chi bleidleisio o'ch cyfrif.", "interaction_modal.go": "Mynd", "interaction_modal.no_account_yet": "Dim cyfrif eto?", @@ -462,7 +462,7 @@ "keyboard_shortcuts.open_media": "Agor cyfryngau", "keyboard_shortcuts.pinned": "Agor rhestr postiadau wedi'u pinio", "keyboard_shortcuts.profile": "Agor proffil yr awdur", - "keyboard_shortcuts.reply": "Ateb i bostiad", + "keyboard_shortcuts.reply": "Ymateb i bostiad", "keyboard_shortcuts.requests": "Agor rhestr ceisiadau dilyn", "keyboard_shortcuts.search": "Ffocysu ar y bar chwilio", "keyboard_shortcuts.spoilers": "Dangos/cuddio'r maes CW", @@ -489,9 +489,9 @@ "lists.create": "Creu", "lists.create_a_list_to_organize": "Creu rhestr newydd i drefnu eich llif Cartref", "lists.create_list": "Creu rhestr", - "lists.delete": "Dileu rhestr", + "lists.delete": "Dileu'r rhestr", "lists.done": "Wedi gorffen", - "lists.edit": "Golygu rhestr", + "lists.edit": "Golygu'r rhestr", "lists.exclusive": "Cuddio aelodau yn y Cartref", "lists.exclusive_hint": "Os oes rhywun ar y rhestr hon, cuddiwch nhw yn eich llif Cartref i osgoi gweld eu postiadau ddwywaith.", "lists.find_users_to_add": "Canfod defnyddwyr i'w hychwanegu", @@ -508,11 +508,11 @@ "lists.replies_policy.none": "Neb", "lists.save": "Cadw", "lists.search": "Chwilio", - "lists.show_replies_to": "Cynhwyswch atebion gan aelodau'r rhestr i", + "lists.show_replies_to": "Cynnwys ymatebion gan aelodau'r rhestr i", "load_pending": "{count, plural, one {# eitem newydd} other {# eitem newydd}}", "loading_indicator.label": "Yn llwytho…", "media_gallery.hide": "Cuddio", - "moved_to_account_banner.text": "Ar hyn y bryd, mae eich cyfrif {disabledAccount} wedi ei analluogi am i chi symud i {movedToAccount}.", + "moved_to_account_banner.text": "Mae eich cyfrif {disabledAccount} wedi ei analluogi ar hyn o bryd am i chi symud i {movedToAccount}.", "mute_modal.hide_from_notifications": "Cuddio rhag hysbysiadau", "mute_modal.hide_options": "Cuddio'r dewis", "mute_modal.indefinite": "Nes i mi eu dad-dewi", @@ -550,7 +550,7 @@ "navigation_bar.search": "Chwilio", "navigation_bar.security": "Diogelwch", "not_signed_in_indicator.not_signed_in": "Rhaid i chi fewngofnodi i weld yr adnodd hwn.", - "notification.admin.report": "Adroddwyd ar {name} {target}", + "notification.admin.report": "Adroddodd {name} {target}", "notification.admin.report_account": "Adroddodd {name} {count, plural, one {un postiad} other {# postiad}} gan {target} oherwydd {category}", "notification.admin.report_account_other": "Adroddodd {name} {count, plural, one {un postiad} two {# bostiad} few {# postiad} other {# postiad}} gan {target}", "notification.admin.report_statuses": "Adroddodd {name} {target} ar gyfer {category}", @@ -569,8 +569,8 @@ "notification.follow_request.name_and_others": "Mae {name} a{count, plural, one {# arall} other {# arall}} wedi gofyn i'ch dilyn chi", "notification.label.mention": "Crybwyll", "notification.label.private_mention": "Crybwyll preifat", - "notification.label.private_reply": "Ateb preifat", - "notification.label.reply": "Ateb", + "notification.label.private_reply": "Ymateb preifat", + "notification.label.reply": "Ymateb", "notification.mention": "Crybwyll", "notification.mentioned_you": "Rydych wedi'ch crybwyll gan {name}", "notification.moderation-warning.learn_more": "Dysgu mwy", @@ -731,7 +731,7 @@ "report.categories.other": "Arall", "report.categories.spam": "Sbam", "report.categories.violation": "Mae cynnwys yn torri un neu fwy o reolau'r gweinydd", - "report.category.subtitle": "Dewiswch y gyfatebiaeth gorau", + "report.category.subtitle": "Dewiswch yr ateb gorau", "report.category.title": "Beth sy'n digwydd gyda'r {type} yma?", "report.category.title_account": "proffil", "report.category.title_status": "post", @@ -853,7 +853,7 @@ "status.remove_favourite": "Tynnu o'r ffefrynnau", "status.replied_in_thread": "Atebodd mewn edefyn", "status.replied_to": "Wedi ateb {name}", - "status.reply": "Ateb", + "status.reply": "Ymateb", "status.replyAll": "Ateb i edefyn", "status.report": "Adrodd ar @{name}", "status.sensitive_warning": "Cynnwys sensitif", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index 43983e502f..8a7edc1ccb 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -562,6 +562,7 @@ "notification.favourite": "{name} märkis su postituse lemmikuks", "notification.favourite.name_and_others_with_link": "{name} ja <a>{count, plural, one {# veel} other {# teist}}</a> märkis su postituse lemmikuks", "notification.favourite_pm": "{name} märkis sinu privaatse mainimise lemmikuks", + "notification.favourite_pm.name_and_others_with_link": "{name} ja <a>{count, plural, one {# veel} other {# veel}}</a> märkisid su privaatse mainimise lemmikuks", "notification.follow": "{name} alustas su jälgimist", "notification.follow.name_and_others": "{name} ja veel {count, plural, one {# kasutaja} other {# kasutajat}} hakkas sind jälgima", "notification.follow_request": "{name} soovib sind jälgida", @@ -696,6 +697,7 @@ "poll_button.remove_poll": "Eemalda küsitlus", "privacy.change": "Muuda postituse nähtavust", "privacy.direct.long": "Kõik postituses mainitud", + "privacy.direct.short": "Privaatne mainimine", "privacy.private.long": "Ainult jälgijad", "privacy.private.short": "Jälgijad", "privacy.public.long": "Nii kasutajad kui mittekasutajad", diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index 55d5480157..d0bfec8229 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -31,7 +31,7 @@ "account.featured_tags.last_status_never": "Aucune publication", "account.featured_tags.title": "Hashtags inclus de {name}", "account.follow": "Suivre", - "account.follow_back": "S'abonner en retour", + "account.follow_back": "Suivre en retour", "account.followers": "abonné·e·s", "account.followers.empty": "Personne ne suit ce compte pour l'instant.", "account.followers_counter": "{count, plural, one {{counter} abonné·e} other {{counter} abonné·e·s}}", @@ -119,7 +119,7 @@ "block_modal.show_less": "Afficher moins", "block_modal.show_more": "Afficher plus", "block_modal.they_cant_mention": "Il ne peut pas vous mentionner ou vous suivre.", - "block_modal.they_cant_see_posts": "Il peut toujours voir vos messages, mais vous ne verrez pas les siens.", + "block_modal.they_cant_see_posts": "Il ne peut plus voir vos messages et vous ne verrez plus les siens.", "block_modal.they_will_know": "Il peut voir qu'il est bloqué.", "block_modal.title": "Bloquer le compte ?", "block_modal.you_wont_see_mentions": "Vous ne verrez pas les messages qui le mentionne.", @@ -872,7 +872,9 @@ "subscribed_languages.target": "Changer les langues abonnées pour {target}", "tabs_bar.home": "Accueil", "tabs_bar.notifications": "Notifications", + "terms_of_service.effective_as_of": "En vigueur à compter du {date}", "terms_of_service.title": "Conditions d'utilisation", + "terms_of_service.upcoming_changes_on": "Modifications à venir le {date}", "time_remaining.days": "{number, plural, one {# jour restant} other {# jours restants}}", "time_remaining.hours": "{number, plural, one {# heure restante} other {# heures restantes}}", "time_remaining.minutes": "{number, plural, one {# minute restante} other {# minutes restantes}}", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 8034c1696e..101aeba211 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -3,7 +3,7 @@ "about.contact": "Contact :", "about.disclaimer": "Mastodon est un logiciel libre, open-source et une marque déposée de Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "Raison non disponible", - "about.domain_blocks.preamble": "Mastodon vous permet généralement de visualiser le contenu et d'interagir avec les utilisateur⋅rice⋅s de n'importe quel autre serveur dans le fédiverse. Voici les exceptions qui ont été faites sur ce serveur en particulier.", + "about.domain_blocks.preamble": "Mastodon vous permet généralement de visualiser le contenu et d'interagir avec les utilisateur⋅rices de n'importe quel autre serveur dans le fédivers. Voici les exceptions qui ont été faites sur ce serveur-là.", "about.domain_blocks.silenced.explanation": "Vous ne verrez généralement pas les profils et le contenu de ce serveur, à moins que vous ne les recherchiez explicitement ou que vous ne choisissiez de les suivre.", "about.domain_blocks.silenced.title": "Limité", "about.domain_blocks.suspended.explanation": "Aucune donnée de ce serveur ne sera traitée, enregistrée ou échangée, rendant impossible toute interaction ou communication avec les comptes de ce serveur.", @@ -19,7 +19,7 @@ "account.block_domain": "Bloquer le domaine {domain}", "account.block_short": "Bloquer", "account.blocked": "Bloqué·e", - "account.cancel_follow_request": "Annuler le suivi", + "account.cancel_follow_request": "Annuler l'abonnement", "account.copy": "Copier le lien vers le profil", "account.direct": "Mention privée @{name}", "account.disable_notifications": "Ne plus me notifier quand @{name} publie quelque chose", @@ -31,18 +31,18 @@ "account.featured_tags.last_status_never": "Aucun message", "account.featured_tags.title": "Les hashtags en vedette de {name}", "account.follow": "Suivre", - "account.follow_back": "S'abonner en retour", + "account.follow_back": "Suivre en retour", "account.followers": "Abonné·e·s", "account.followers.empty": "Personne ne suit cet·te utilisateur·rice pour l’instant.", "account.followers_counter": "{count, plural, one {{counter} abonné·e} other {{counter} abonné·e·s}}", "account.following": "Abonnements", "account.following_counter": "{count, plural, one {{counter} abonnement} other {{counter} abonnements}}", "account.follows.empty": "Cet·te utilisateur·rice ne suit personne pour l’instant.", - "account.go_to_profile": "Aller au profil", + "account.go_to_profile": "Voir le profil", "account.hide_reblogs": "Masquer les partages de @{name}", "account.in_memoriam": "En mémoire de.", "account.joined_short": "Ici depuis", - "account.languages": "Changer les langues abonnées", + "account.languages": "Modifier les langues d'abonnements", "account.link_verified_on": "La propriété de ce lien a été vérifiée le {date}", "account.locked_info": "Ce compte est privé. Son ou sa propriétaire approuve manuellement qui peut le suivre.", "account.media": "Médias", @@ -119,7 +119,7 @@ "block_modal.show_less": "Afficher moins", "block_modal.show_more": "Afficher plus", "block_modal.they_cant_mention": "Il ne peut pas vous mentionner ou vous suivre.", - "block_modal.they_cant_see_posts": "Il peut toujours voir vos messages, mais vous ne verrez pas les siens.", + "block_modal.they_cant_see_posts": "Il ne peut plus voir vos messages et vous ne verrez plus les siens.", "block_modal.they_will_know": "Il peut voir qu'il est bloqué.", "block_modal.title": "Bloquer le compte ?", "block_modal.you_wont_see_mentions": "Vous ne verrez pas les messages qui le mentionne.", @@ -872,7 +872,9 @@ "subscribed_languages.target": "Changer les langues abonnées pour {target}", "tabs_bar.home": "Accueil", "tabs_bar.notifications": "Notifications", + "terms_of_service.effective_as_of": "En vigueur à compter du {date}", "terms_of_service.title": "Conditions d'utilisation", + "terms_of_service.upcoming_changes_on": "Modifications à venir le {date}", "time_remaining.days": "{number, plural, one {# jour restant} other {# jours restants}}", "time_remaining.hours": "{number, plural, one {# heure restante} other {# heures restantes}}", "time_remaining.minutes": "{number, plural, one {# minute restante} other {# minutes restantes}}", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 13505d77a9..22a2209fc5 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -36,7 +36,7 @@ "account.followers.empty": "Ancora nessuno segue questo utente.", "account.followers_counter": "{count, plural, one {{counter} seguace} other {{counter} seguaci}}", "account.following": "Seguiti", - "account.following_counter": "{count, plural, one {{counter} segui} other {{counter} segui}}", + "account.following_counter": "{count, plural, one {{counter} segui} other {{counter} seguiti}}", "account.follows.empty": "Questo utente non segue ancora nessuno.", "account.go_to_profile": "Vai al profilo", "account.hide_reblogs": "Nascondi condivisioni da @{name}", diff --git a/app/javascript/mastodon/locales/ka.json b/app/javascript/mastodon/locales/ka.json index d1587e1c2c..4dec178650 100644 --- a/app/javascript/mastodon/locales/ka.json +++ b/app/javascript/mastodon/locales/ka.json @@ -6,20 +6,20 @@ "account.badges.group": "ჯგუფი", "account.block": "დაბლოკე @{name}", "account.block_domain": "დაიმალოს ყველაფერი დომენიდან {domain}", - "account.blocked": "დაიბლოკა", + "account.blocked": "დაბლოკილია", "account.cancel_follow_request": "Withdraw follow request", "account.domain_blocked": "დომენი დამალულია", "account.edit_profile": "პროფილის ცვლილება", "account.endorse": "გამორჩევა პროფილზე", - "account.featured_tags.last_status_never": "პოსტები არ არის", + "account.featured_tags.last_status_never": "პოსტების გარეშე", "account.follow": "გაყოლა", "account.followers": "მიმდევრები", "account.hide_reblogs": "დაიმალოს ბუსტები @{name}-სგან", "account.media": "მედია", "account.mention": "ასახელეთ @{name}", "account.mute": "გააჩუმე @{name}", - "account.muted": "გაჩუმებული", - "account.posts": "ტუტები", + "account.muted": "დადუმებულია", + "account.posts": "პოსტები", "account.posts_with_replies": "ტუტები და პასუხები", "account.report": "დაარეპორტე @{name}", "account.requested": "დამტკიცების მოლოდინში. დააწკაპუნეთ რომ უარყოთ დადევნების მოთხონვა", @@ -42,7 +42,7 @@ "column.community": "ლოკალური თაიმლაინი", "column.domain_blocks": "დამალული დომენები", "column.follow_requests": "დადევნების მოთხოვნები", - "column.home": "სახლი", + "column.home": "საწყისი", "column.lists": "სიები", "column.mutes": "გაჩუმებული მომხმარებლები", "column.notifications": "შეტყობინებები", @@ -52,9 +52,9 @@ "column_header.hide_settings": "პარამეტრების დამალვა", "column_header.moveLeft_settings": "სვეტის მარცხნივ გადატანა", "column_header.moveRight_settings": "სვეტის მარჯვნივ გადატანა", - "column_header.pin": "აპინვა", + "column_header.pin": "მიმაგრება", "column_header.show_settings": "პარამეტრების ჩვენება", - "column_header.unpin": "პინის მოხსნა", + "column_header.unpin": "მოხსნა", "column_subheading.settings": "პარამეტრები", "community.column_settings.media_only": "მხოლოდ მედია", "compose_form.direct_message_warning_learn_more": "გაიგე მეტი", @@ -66,21 +66,21 @@ "compose_form.publish_form": "Publish", "compose_form.spoiler.marked": "გაფრთხილების უკან ტექსტი დამალულია", "compose_form.spoiler.unmarked": "ტექსტი არაა დამალული", - "confirmation_modal.cancel": "უარყოფა", + "confirmation_modal.cancel": "გაუქმება", "confirmations.block.confirm": "ბლოკი", - "confirmations.delete.confirm": "გაუქმება", + "confirmations.delete.confirm": "წაშლა", "confirmations.delete.message": "დარწმუნებული ხართ, გსურთ გააუქმოთ ეს სტატუსი?", - "confirmations.delete_list.confirm": "გაუქმება", + "confirmations.delete_list.confirm": "წაშლა", "confirmations.delete_list.message": "დარწმუნებული ხართ, გსურთ სამუდამოდ გააუქმოთ ეს სია?", - "confirmations.mute.confirm": "გაჩუმება", + "confirmations.mute.confirm": "დადუმება", "confirmations.redraft.confirm": "გაუქმება და გადანაწილება", "confirmations.unfollow.confirm": "ნუღარ მიჰყვები", "confirmations.unfollow.message": "დარწმუნებული ხართ, აღარ გსურთ მიჰყვებოდეთ {name}-ს?", "embed.instructions": "ეს სტატუსი ჩასვით თქვენს ვებ-საიტზე შემდეგი კოდის კოპირებით.", "embed.preview": "ესაა თუ როგორც გამოჩნდება:", "emoji_button.activity": "აქტივობა", - "emoji_button.custom": "პერსონალიზირებული", - "emoji_button.flags": "დროშები", + "emoji_button.custom": "მომხმარებლის", + "emoji_button.flags": "ალმები", "emoji_button.food": "საჭმელი და სასლმელი", "emoji_button.label": "ემოჯის ჩასმა", "emoji_button.nature": "ბუმება", @@ -119,7 +119,7 @@ "keyboard_shortcuts.federated": "to open federated timeline", "keyboard_shortcuts.heading": "კლავიატურის სწრაფი ბმულები", "keyboard_shortcuts.home": "to open home timeline", - "keyboard_shortcuts.hotkey": "ცხელი კლავიში", + "keyboard_shortcuts.hotkey": "მალსახმობი ღილაკი", "keyboard_shortcuts.legend": "ამ ლეგენდის გამოსაჩენად", "keyboard_shortcuts.local": "to open local timeline", "keyboard_shortcuts.mention": "ავტორის დასახელებლად", @@ -180,20 +180,20 @@ "relative_time.just_now": "ახლა", "relative_time.minutes": "{number}წთ", "relative_time.seconds": "{number}წმ", - "reply_indicator.cancel": "უარყოფა", + "reply_indicator.cancel": "გაუქმება", "report.forward": "ფორვარდი {target}-ს", "report.forward_hint": "ანგარიში სხვა სერვერიდანაა. გავაგზავნოთ რეპორტის ანონიმური ასლიც?", "report.placeholder": "დამატებითი კომენტარები", - "report.submit": "დასრულება", + "report.submit": "გადაცემა", "report.target": "არეპორტებთ {target}", "report_notification.attached_statuses": "{count, plural, one {# post} other {# posts}} attached", "search.placeholder": "ძებნა", "search_results.hashtags": "ჰეშტეგები", - "search_results.statuses": "ტუტები", - "sign_in_banner.sign_in": "Sign in", + "search_results.statuses": "პოსტები", + "sign_in_banner.sign_in": "შესვლა", "status.admin_status": "Open this status in the moderation interface", "status.block": "დაბლოკე @{name}", - "status.cancel_reblog_private": "ბუსტის მოშორება", + "status.cancel_reblog_private": "ბუსტის მოხსნა", "status.cannot_reblog": "ეს პოსტი ვერ დაიბუსტება", "status.copy": "Copy link to status", "status.delete": "წაშლა", @@ -222,7 +222,7 @@ "status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}", "status.unmute_conversation": "საუბარზე გაჩუმების მოშორება", "status.unpin": "პროფილიდან პინის მოშორება", - "tabs_bar.home": "სახლი", + "tabs_bar.home": "საწყისი", "tabs_bar.notifications": "შეტყობინებები", "trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}", "ui.beforeunload": "თქვენი დრაფტი გაუქმდება თუ დატოვებთ მასტოდონს.", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 5b93084c13..e9c5ef2133 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -874,6 +874,7 @@ "tabs_bar.notifications": "알림", "terms_of_service.effective_as_of": "{date}부터 적용됨", "terms_of_service.title": "이용 약관", + "terms_of_service.upcoming_changes_on": "{date}에 예정된 변경사항", "time_remaining.days": "{number} 일 남음", "time_remaining.hours": "{number} 시간 남음", "time_remaining.minutes": "{number} 분 남음", diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json index 08e52077e9..6cf1558198 100644 --- a/app/javascript/mastodon/locales/lv.json +++ b/app/javascript/mastodon/locales/lv.json @@ -173,7 +173,7 @@ "compose.published.open": "Atvērt", "compose.saved.body": "Ziņa saglabāta.", "compose_form.direct_message_warning_learn_more": "Uzzināt vairāk", - "compose_form.encryption_warning": "Mastodon ieraksti nav pilnībā šifrēti. Nedalies ar jebkādu jutīgu informāciju caur Mastodon!", + "compose_form.encryption_warning": "Mastodon ieraksti nav pilnībā šifrēti. Nedalies ar jebkādu jūtīgu informāciju caur Mastodon!", "compose_form.hashtag_warning": "Šis ieraksts netiks uzrādīts nevienā tēmturī, jo tas nav redzams visiem. Tikai visiem redzamos ierakstus var meklēt pēc tēmtura.", "compose_form.lock_disclaimer": "Tavs konts nav {locked}. Ikviens var Tev sekot, lai redzētu tikai sekotājiem paredzētos ierakstus.", "compose_form.lock_disclaimer.lock": "slēgts", @@ -474,9 +474,9 @@ "notification.moderation_warning": "Ir saņemts satura pārraudzības brīdinājums", "notification.moderation_warning.action_delete_statuses": "Daži no Taviem ierakstiem tika noņemti.", "notification.moderation_warning.action_disable": "Tavs konts tika atspējots.", - "notification.moderation_warning.action_mark_statuses_as_sensitive": "Daži no Taviem ierakstiem tika atzīmēti kā jutīgi.", + "notification.moderation_warning.action_mark_statuses_as_sensitive": "Daži no Taviem ierakstiem tika atzīmēti kā jūtīgi.", "notification.moderation_warning.action_none": "Konts ir saņēmis satura pārraudzības brīdinājumu.", - "notification.moderation_warning.action_sensitive": "Tavi ieraksti turpmāk tiks atzīmēti kā jutīgi.", + "notification.moderation_warning.action_sensitive": "Tavi ieraksti turpmāk tiks atzīmēti kā jūtīgi.", "notification.moderation_warning.action_silence": "Tavs konts tika ierobežots.", "notification.moderation_warning.action_suspend": "Tava konta darbība tika apturēta.", "notification.own_poll": "Tava aptauja ir noslēgusies", @@ -702,7 +702,7 @@ "status.reply": "Atbildēt", "status.replyAll": "Atbildēt uz tematu", "status.report": "Ziņot par @{name}", - "status.sensitive_warning": "Sensitīvs saturs", + "status.sensitive_warning": "Jūtīgs saturs", "status.share": "Kopīgot", "status.show_less_all": "Rādīt mazāk visiem", "status.show_more_all": "Rādīt vairāk visiem", diff --git a/app/javascript/mastodon/locales/nan.json b/app/javascript/mastodon/locales/nan.json index 0daba1d8ae..57eef0a874 100644 --- a/app/javascript/mastodon/locales/nan.json +++ b/app/javascript/mastodon/locales/nan.json @@ -309,6 +309,15 @@ "empty_column.followed_tags": "Lí iáu buē收著任何ê hashtag。Nā是lí收著,ē佇tsia顯示。", "empty_column.hashtag": "Tsit ê hashtag內底無物件。", "empty_column.home": "Lí tshù ê時間線是空ê!跟tuè別lâng來kā充滿。", + "empty_column.list": "Tsit張列單內底iáu bô物件。若是列單內底ê成員貼新ê PO文,in ē tī tsia顯示。", + "empty_column.mutes": "Lí iáu無消音任何用者。", + "empty_column.notification_requests": "清hōo空ah!內底無物件。若是lí收著新ê通知,ē根據lí ê設定,佇tsia出現。", + "empty_column.notifications": "Lí iáu無收著任何通知。Nā別lâng kap lí互動,lí ē佇tsia看著。", + "empty_column.public": "內底無物件!寫beh公開ê PO文,á是主動跟tuè別ê服侍器ê用者,來加添內容。", + "error.unexpected_crash.explanation": "因為原始碼內底有錯誤,á是瀏覽器相容出tshê,tsit頁bē當正確顯示。", + "error.unexpected_crash.explanation_addons": "Tsit頁bē當正確顯示,可能是瀏覽器附ê功能,á是自動翻譯工具所致。", + "error.unexpected_crash.next_steps": "請試更新tsit頁。若是bē當改善,lí iáu是ē當改使用無kâng ê瀏覽器,á是app,來用Mastodon。", + "error.unexpected_crash.next_steps_addons": "請試kā in停止使用,suà落來更新tsit頁。若是bē當改善,lí iáu是ē當改使用無kâng ê瀏覽器,á是app,來用Mastodon。", "errors.unexpected_crash.copy_stacktrace": "Khóo-pih stacktrace kàu剪貼pang-á", "errors.unexpected_crash.report_issue": "報告問題", "explore.suggested_follows": "用者", @@ -345,6 +354,11 @@ "follow_suggestions.dismiss": "Mài koh顯示。", "follow_suggestions.featured_longer": "{domain} 團隊所揀ê", "follow_suggestions.friends_of_friends_longer": "時行佇lí所tuè ê lâng", + "follow_suggestions.hints.featured": "Tsit ê個人資料是 {domain} 團隊特別揀ê。", + "follow_suggestions.hints.friends_of_friends": "Tsit ê個人資料tī lí跟tuè ê lâng之間真流行。", + "follow_suggestions.hints.most_followed": "Tsit ê個人資料是 {domain} 內,有足tsē跟tuè者ê其中tsit ê。", + "follow_suggestions.hints.most_interactions": "Tsit ê個人資料tsi̍t-tsām-á佇 {domain} 有得著真tsē關注。", + "follow_suggestions.hints.similar_to_recently_followed": "Tsit ê個人資料kap lí最近跟tuè ê口座相siâng。", "follow_suggestions.personalized_suggestion": "個人化ê推薦", "follow_suggestions.popular_suggestion": "流行ê推薦", "follow_suggestions.popular_suggestion_longer": "佇{domain} 足有lâng緣", @@ -378,6 +392,9 @@ "hashtag.follow": "跟tuè hashtag", "hashtag.unfollow": "取消跟tuè hashtag", "hashtags.and_other": "……kap 其他 {count, plural, other {# ê}}", + "hints.profiles.followers_may_be_missing": "Tsit ê個人資料ê跟tuè者資訊可能有落勾ê。", + "hints.profiles.follows_may_be_missing": "Tsit ê口座所跟tuè ê ê資訊可能有落勾ê。", + "hints.profiles.posts_may_be_missing": "Tsit ê口座ê tsi̍t kuá PO文可能有落勾ê。", "hints.profiles.see_more_followers": "佇 {domain} 看koh khah tsē跟tuè lí ê", "hints.profiles.see_more_follows": "佇 {domain} 看koh khah tsē lí跟tuè ê", "hints.profiles.see_more_posts": "佇 {domain} 看koh khah tsē ê PO文", @@ -390,6 +407,67 @@ "home.pending_critical_update.link": "看更新內容", "home.pending_critical_update.title": "有重要ê安全更新!", "home.show_announcements": "顯示公告", + "ignore_notifications_modal.disclaimer": "Lí所忽略in ê通知ê用者,Mastodonbē當kā lí通知。忽略通知bē當阻擋訊息ê寄送。", + "ignore_notifications_modal.filter_instead": "改做過濾", + "ignore_notifications_modal.filter_to_act_users": "Lí猶原ē當接受、拒絕猶是檢舉用者", + "ignore_notifications_modal.filter_to_avoid_confusion": "過濾ē當避免可能ê bē分明。", + "ignore_notifications_modal.filter_to_review_separately": "Lí ē當個別檢視所過濾ê通知", + "ignore_notifications_modal.ignore": "Kā通知忽略", + "ignore_notifications_modal.limited_accounts_title": "Kám beh忽略受限制ê口座送來ê通知?", + "ignore_notifications_modal.new_accounts_title": "Kám beh忽略新口座送來ê通知?", + "ignore_notifications_modal.not_followers_title": "Kám beh忽略無跟tuè lí ê口座送來ê通知?", + "ignore_notifications_modal.not_following_title": "Kám beh忽略lí 無跟tuè ê口座送來ê通知?", + "ignore_notifications_modal.private_mentions_title": "忽略ka-kī主動送ê私人提起ê通知?", + "info_button.label": "幫tsān", + "info_button.what_is_alt_text": "<h1>Siánn物是替代文字?</h1> <p>替代文字kā視覺有障礙、網路速度khah慢,á是beh tshuē頂下文ê lâng,提供圖ê敘述。</p> <p>Lí ē當通過寫明白、簡單kap客觀ê替代文字,替逐家改善容易使用性kap幫tsān理解。</p> <ul> <li>掌握重要ê因素</li> <li>替圖寫摘要ê文字</li> <li>用規則ê語句結構</li> <li>避免重複ê資訊</li> <li>專注佇趨勢kap佇複雜視覺(比如圖表á是地圖)內底tshuē關鍵</li> </ul>", + "interaction_modal.action.favourite": "Nā beh繼續,lí tio̍h用你ê口座收藏。", + "interaction_modal.action.follow": "Nā beh繼續,lí tio̍h用你ê口座跟tuè。", + "interaction_modal.action.reblog": "Nā beh繼續,lí tio̍h用你ê口座轉送。", + "interaction_modal.action.reply": "Nā beh繼續,lí tio̍h用你ê口座回應。", + "interaction_modal.action.vote": "Nā beh繼續,lí tio̍h用你ê口座投票。", + "interaction_modal.go": "行", + "interaction_modal.no_account_yet": "Tsit-má iáu bô口座?", + "interaction_modal.on_another_server": "佇無kâng ê服侍器", + "interaction_modal.on_this_server": "Tī tsit ê服侍器", + "interaction_modal.title.favourite": "收藏 {name} ê PO文", + "interaction_modal.title.follow": "跟tuè {name}", + "interaction_modal.title.reblog": "轉送 {name} ê PO文", + "interaction_modal.title.reply": "回應 {name} ê PO文", + "interaction_modal.title.vote": "參加 {name} ê投票", + "interaction_modal.username_prompt": "比如:{example}", + "intervals.full.days": "{number, plural, other {# kang}}", + "intervals.full.hours": "{number, plural, other {# 點鐘}}", + "intervals.full.minutes": "{number, plural, other {# 分鐘}}", + "keyboard_shortcuts.back": "Tńg去", + "keyboard_shortcuts.blocked": "開封鎖ê用者ê列單", + "keyboard_shortcuts.boost": "轉送PO文", + "keyboard_shortcuts.column": "揀tsit ê欄", + "keyboard_shortcuts.compose": "揀寫文字ê框仔", + "keyboard_shortcuts.description": "說明", + "keyboard_shortcuts.direct": "phah開私人提起ê欄", + "keyboard_shortcuts.down": "佇列單內kā suá khah 下kha", + "keyboard_shortcuts.enter": "Phah開PO文", + "keyboard_shortcuts.favourite": "收藏PO文", + "keyboard_shortcuts.favourites": "Phah開收藏ê列單", + "keyboard_shortcuts.federated": "Phah開聯邦ê時間線", + "keyboard_shortcuts.heading": "鍵盤ê快速key", + "keyboard_shortcuts.home": "Phah開tshù ê時間線", + "keyboard_shortcuts.hotkey": "快速key", + "keyboard_shortcuts.legend": "顯示tsit篇說明", + "keyboard_shortcuts.local": "Phah開本站ê時間線", + "keyboard_shortcuts.mention": "提起作者", + "keyboard_shortcuts.muted": "Phah開消音ê用者列單", + "keyboard_shortcuts.my_profile": "Phah開lí ê個人資料", + "keyboard_shortcuts.notifications": "Phah開通知欄", + "keyboard_shortcuts.open_media": "Phah開媒體", + "keyboard_shortcuts.pinned": "Phah開釘起來ê PO文列單", + "keyboard_shortcuts.profile": "Phah開作者ê個人資料", + "keyboard_shortcuts.reply": "回應PO文", + "keyboard_shortcuts.requests": "Phah開跟tuè請求ê列單", + "keyboard_shortcuts.search": "揀tshiau-tshuē條á", + "keyboard_shortcuts.spoilers": "顯示/隱藏內容警告", + "keyboard_shortcuts.start": "Phah開「開始用」欄", + "keyboard_shortcuts.toggle_hidden": "顯示/隱藏內容警告後壁ê PO文", "notification.favourite_pm": "{name} kah意lí ê私人提起", "notification.favourite_pm.name_and_others_with_link": "{name} kap<a>{count, plural, other {另外 # ê lâng}}</a>kah意lí ê私人提起", "search_popout.language_code": "ISO語言代碼", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index 7d4369642c..ed3f741197 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -872,7 +872,9 @@ "subscribed_languages.target": "Изменить языки подписки для {target}", "tabs_bar.home": "Главная", "tabs_bar.notifications": "Уведомления", + "terms_of_service.effective_as_of": "Действует с {date}", "terms_of_service.title": "Пользовательское соглашение", + "terms_of_service.upcoming_changes_on": "Предстоящие изменения {date}", "time_remaining.days": "{number, plural, one {остался # день} few {осталось # дня} many {осталось # дней} other {осталось # дней}}", "time_remaining.hours": "{number, plural, one {остался # час} few {осталось # часа} many {осталось # часов} other {осталось # часов}}", "time_remaining.minutes": "{number, plural, one {осталась # минута} few {осталось # минуты} many {осталось # минут} other {осталось # минут}}", diff --git a/app/javascript/mastodon/locales/tok.json b/app/javascript/mastodon/locales/tok.json index 7291588a38..a5612aa5be 100644 --- a/app/javascript/mastodon/locales/tok.json +++ b/app/javascript/mastodon/locales/tok.json @@ -65,7 +65,7 @@ "account.statuses_counter": "{count, plural, other {toki {counter}}}", "account.unblock": "o weka ala e jan {name}", "account.unblock_domain": "o weka ala e ma {domain}", - "account.unblock_short": "o weka ala", + "account.unblock_short": "o pini weka", "account.unendorse": "lipu jan la o suli ala e ni", "account.unfollow": "o kute ala", "account.unmute": "o len ala e @{name}", @@ -114,8 +114,8 @@ "attachments_list.unprocessed": "(nasin open)", "audio.hide": "o len e kalama", "block_modal.remote_users_caveat": "mi pana e wile sina tawa ma {domain}. taso, o sona: ma li ken kepeken nasin len ante la pakala li ken lon. toki pi lukin ale la jan pi ma ala li ken lukin.", - "block_modal.show_less": "o lili e lukin", - "block_modal.show_more": "o suli e lukin", + "block_modal.show_less": "o pana e lili", + "block_modal.show_more": "o pana e mute", "block_modal.they_cant_mention": "ona li ken ala toki tawa sina li ken ala kute e sina.", "block_modal.they_cant_see_posts": "ona li ken ala lukin e toki sina. sina ken ala lukin e toki ona.", "block_modal.they_will_know": "ona li sona e ni: sina weka e lukin ona.", @@ -215,6 +215,9 @@ "confirmations.logout.confirm": "o weka", "confirmations.logout.message": "sina wile ala wile weka", "confirmations.logout.title": "o weka?", + "confirmations.missing_alt_text.confirm": "pana e toki pi sona lukin", + "confirmations.missing_alt_text.message": "toki ni la sitelen li lon. taso toki pi sona lukin li lon ala. toki pi sona lukin li pona tan ni: jan ale li ken sona e toki.", + "confirmations.missing_alt_text.title": "o pana e toki pi sona lukin", "confirmations.mute.confirm": "o len", "confirmations.redraft.confirm": "o weka o pali sin e toki", "confirmations.redraft.message": "pali sin e toki ni la sina wile ala wile weka e ona? sina ni la suli pi toki ni en wawa pi toki ni li weka. kin la toki lon toki ni li jo e mama ala.", @@ -235,6 +238,7 @@ "copy_icon_button.copied": "toki li awen lon ilo sina", "copypaste.copied": "sina jo e toki", "copypaste.copy_to_clipboard": "o awen lon ilo sina", + "directory.federated": "tan lipu ante sona", "directory.local": "tan {domain} taso", "directory.new_arrivals": "jan pi kama sin", "directory.recently_active": "jan lon tenpo poka", @@ -243,6 +247,7 @@ "dismissable_banner.community_timeline": "ni li toki pi tenpo poka tawa ale tan jan lon ma lawa pi nimi {domain}.", "dismissable_banner.dismiss": "o weka", "dismissable_banner.explore_links": "tenpo suno ni la jan pi kulupu ale li toki e ijo sin ni. ijo sin pi jan ante mute li sewi lon lipu ni.", + "dismissable_banner.explore_statuses": "jan mute li lukin e toki ni tan ma ilo weka. toki sin en toki pi wawa mute li lon sewi.", "domain_block_modal.block": "o weka e ma", "domain_block_modal.they_wont_know": "ona li sona ala e ni: sina weka e ona.", "domain_block_modal.title": "sina wile weka ala weka e ma?", @@ -276,6 +281,7 @@ "emoji_button.symbols": "sitelen", "emoji_button.travel": "ma en tawa", "empty_column.account_hides_collections": "jan ni li wile len e sona ni", + "empty_column.account_suspended": "lipu ni li weka", "empty_column.account_timeline": "toki ala li lon!", "empty_column.account_unavailable": "ken ala lukin e lipu jan", "empty_column.blocks": "jan ala li weka tawa sina.", @@ -328,6 +334,7 @@ "hashtag.counter_by_uses": "{count, plural, other {toki {counter}}}", "hashtag.follow": "o kute e kulupu lipu", "hashtag.unfollow": "o kute ala e kulupu lipu", + "home.column_settings.show_reblogs": "lukin e wawa", "home.pending_critical_update.link": "o lukin e ijo ilo sin", "info_button.label": "sona", "interaction_modal.go": "o tawa ma ni", @@ -376,6 +383,7 @@ "navigation_bar.about": "sona", "navigation_bar.blocks": "jan weka", "navigation_bar.compose": "o pali e toki sin", + "navigation_bar.domain_blocks": "kulupu pi ma weka", "navigation_bar.favourites": "ijo pona", "navigation_bar.filters": "nimi len", "navigation_bar.lists": "kulupu lipu", @@ -472,6 +480,8 @@ "status.pin": "o sewi lon lipu sina", "status.pinned": "toki sewi", "status.reblog": "o wawa", + "status.reblogged_by": "jan {name} li wawa", + "status.reblogs.empty": "jan ala li wawa e toki ni. jan li wawa la, nimi ona li sitelen lon ni.", "status.share": "o pana tawa ante", "status.show_less_all": "o lili e ale", "status.show_more_all": "o suli e ale", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index bb7ee3c086..b116008fac 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -872,7 +872,9 @@ "subscribed_languages.target": "更改 {target} 的订阅语言", "tabs_bar.home": "主页", "tabs_bar.notifications": "通知", + "terms_of_service.effective_as_of": "自 {date} 起生效", "terms_of_service.title": "服务条款", + "terms_of_service.upcoming_changes_on": "将于 {date} 进行变更", "time_remaining.days": "剩余 {number, plural, other {# 天}}", "time_remaining.hours": "剩余 {number, plural, other {# 小时}}", "time_remaining.minutes": "剩余 {number, plural, other {# 分钟}}", diff --git a/app/javascript/mastodon/models/alert.ts b/app/javascript/mastodon/models/alert.ts new file mode 100644 index 0000000000..bc492eff3c --- /dev/null +++ b/app/javascript/mastodon/models/alert.ts @@ -0,0 +1,14 @@ +import type { MessageDescriptor } from 'react-intl'; + +export type TranslatableString = string | MessageDescriptor; + +export type TranslatableValues = Record<string, string | number | Date>; + +export interface Alert { + key: number; + title?: TranslatableString; + message: TranslatableString; + action?: TranslatableString; + values?: TranslatableValues; + onClick?: () => void; +} diff --git a/app/javascript/mastodon/reducers/alerts.js b/app/javascript/mastodon/reducers/alerts.js deleted file mode 100644 index 1ca9b62a02..0000000000 --- a/app/javascript/mastodon/reducers/alerts.js +++ /dev/null @@ -1,30 +0,0 @@ -import { List as ImmutableList } from 'immutable'; - -import { - ALERT_SHOW, - ALERT_DISMISS, - ALERT_CLEAR, -} from '../actions/alerts'; - -const initialState = ImmutableList([]); - -let id = 0; - -const addAlert = (state, alert) => - state.push({ - key: id++, - ...alert, - }); - -export default function alerts(state = initialState, action) { - switch(action.type) { - case ALERT_SHOW: - return addAlert(state, action.alert); - case ALERT_DISMISS: - return state.filterNot(item => item.key === action.alert.key); - case ALERT_CLEAR: - return state.clear(); - default: - return state; - } -} diff --git a/app/javascript/mastodon/reducers/alerts.ts b/app/javascript/mastodon/reducers/alerts.ts new file mode 100644 index 0000000000..30108744ae --- /dev/null +++ b/app/javascript/mastodon/reducers/alerts.ts @@ -0,0 +1,24 @@ +import { createReducer } from '@reduxjs/toolkit'; + +import { showAlert, dismissAlert, clearAlerts } from 'mastodon/actions/alerts'; +import type { Alert } from 'mastodon/models/alert'; + +const initialState: Alert[] = []; + +let id = 0; + +export const alertsReducer = createReducer(initialState, (builder) => { + builder + .addCase(showAlert, (state, { payload }) => { + state.push({ + key: id++, + ...payload, + }); + }) + .addCase(dismissAlert, (state, { payload: { key } }) => { + return state.filter((item) => item.key !== key); + }) + .addCase(clearAlerts, () => { + return []; + }); +}); diff --git a/app/javascript/mastodon/reducers/index.ts b/app/javascript/mastodon/reducers/index.ts index fd83682e12..719ccf86b9 100644 --- a/app/javascript/mastodon/reducers/index.ts +++ b/app/javascript/mastodon/reducers/index.ts @@ -5,7 +5,7 @@ import { combineReducers } from 'redux-immutable'; import { accountsReducer } from './accounts'; import accounts_map from './accounts_map'; -import alerts from './alerts'; +import { alertsReducer } from './alerts'; import announcements from './announcements'; import { antennasReducer } from './antennas'; import { bookmarkCategoriesReducer } from './bookmark_categories'; @@ -49,7 +49,7 @@ const reducers = { dropdownMenu: dropdownMenuReducer, timelines, meta, - alerts, + alerts: alertsReducer, loadingBar: loadingBarReducer, modal: modalReducer, user_lists, diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index 9ae91dac3a..91ff05df4d 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -17,9 +17,10 @@ export const makeGetStatus = () => { (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]), getFilters, + (_, { contextType }) => ['detailed', 'bookmarks', 'favourites'].includes(contextType), ], - (statusBase, statusReblog, statusQuote, statusReblogQuote, accountBase, accountReblog, filters) => { + (statusBase, statusReblog, statusQuote, statusReblogQuote, accountBase, accountReblog, filters, warnInsteadOfHide) => { if (!statusBase || statusBase.get('isLoading')) { return null; } @@ -36,6 +37,7 @@ export const makeGetStatus = () => { } let filtered = false; + let mediaFiltered = false; if ((accountReblog || accountBase).get('id') !== me && filters) { let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList(); const quoteFilterResults = statusQuote?.get('filtered'); @@ -46,10 +48,16 @@ export const makeGetStatus = () => { } } - if (filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) { + if (!warnInsteadOfHide && filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) { return null; } - filterResults = filterResults.filter(result => filters.has(result.get('filter'))); + + let mediaFilters = filterResults.filter(result => filters.getIn([result.get('filter'), 'filter_action']) === 'blur'); + if (!mediaFilters.isEmpty()) { + mediaFiltered = mediaFilters.map(result => filters.getIn([result.get('filter'), 'title'])); + } + + filterResults = filterResults.filter(result => filters.has(result.get('filter')) && filters.getIn([result.get('filter'), 'filter_action']) !== 'blur'); if (!filterResults.isEmpty()) { filtered = filterResults.map(result => filters.getIn([result.get('filter'), 'title'])); } @@ -60,6 +68,7 @@ export const makeGetStatus = () => { map.set('quote', statusQuote); map.set('account', accountBase); map.set('matched_filters', filtered); + map.set('matched_media_filters', mediaFiltered); }); }, ); @@ -75,28 +84,6 @@ export const makeGetPictureInPicture = () => { })); }; -const ALERT_DEFAULTS = { - dismissAfter: 5000, - style: false, -}; - -const formatIfNeeded = (intl, message, values) => { - if (typeof message === 'object') { - return intl.formatMessage(message, values); - } - - return message; -}; - -export const getAlerts = createSelector([state => state.get('alerts'), (_, { intl }) => intl], (alerts, intl) => - alerts.map(item => ({ - ...ALERT_DEFAULTS, - ...item, - action: formatIfNeeded(intl, item.action, item.values), - title: formatIfNeeded(intl, item.title, item.values), - message: formatIfNeeded(intl, item.message, item.values), - })).toArray()); - export const makeGetNotification = () => createSelector([ (_, base) => base, (state, _, accountId) => state.getIn(['accounts', accountId]), diff --git a/app/javascript/mastodon/store/middlewares/errors.ts b/app/javascript/mastodon/store/middlewares/errors.ts index 3ad3844d5b..b9efe9f2b4 100644 --- a/app/javascript/mastodon/store/middlewares/errors.ts +++ b/app/javascript/mastodon/store/middlewares/errors.ts @@ -12,19 +12,21 @@ import type { AsyncThunkRejectValue } from '../typed_functions'; const defaultFailSuffix = 'FAIL'; const isFailedAction = new RegExp(`${defaultFailSuffix}$`, 'g'); -interface ActionWithMaybeAlertParams extends Action, AsyncThunkRejectValue {} - interface RejectedAction extends Action { payload: AsyncThunkRejectValue; } +interface ActionWithMaybeAlertParams extends Action, AsyncThunkRejectValue { + payload?: AsyncThunkRejectValue; +} + function isRejectedActionWithPayload( action: unknown, ): action is RejectedAction { return isAsyncThunkAction(action) && isRejectedWithValue(action); } -function isActionWithmaybeAlertParams( +function isActionWithMaybeAlertParams( action: unknown, ): action is ActionWithMaybeAlertParams { return isAction(action); @@ -40,11 +42,12 @@ export const errorsMiddleware: Middleware<{}, RootState> = showAlertForError(action.payload.error, action.payload.skipNotFound), ); } else if ( - isActionWithmaybeAlertParams(action) && - !action.skipAlert && + isActionWithMaybeAlertParams(action) && + !(action.payload?.skipAlert || action.skipAlert) && action.type.match(isFailedAction) ) { - dispatch(showAlertForError(action.error, action.skipNotFound)); + const { error, skipNotFound } = action.payload ?? action; + dispatch(showAlertForError(error, skipNotFound)); } return next(action); diff --git a/app/javascript/mastodon/utils/filters.ts b/app/javascript/mastodon/utils/filters.ts index 5d334fe509..479e1f44ab 100644 --- a/app/javascript/mastodon/utils/filters.ts +++ b/app/javascript/mastodon/utils/filters.ts @@ -7,6 +7,11 @@ export const toServerSideType = (columnType: string) => { case 'account': case 'explore': return columnType; + case 'detailed': + return 'thread'; + case 'bookmarks': + case 'favourites': + return 'home'; default: if (columnType.includes('list:') || columnType.includes('antenna:')) { return 'home'; diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index 109b69bca5..b328d8ee34 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -1,25 +1,27 @@ -@import 'mastodon/mixins'; -@import 'mastodon/variables'; -@import 'fonts/roboto'; -@import 'fonts/roboto-mono'; +@use 'mastodon/functions'; +@use 'mastodon/mixins'; +@use 'mastodon/variables'; +@use 'mastodon/css_variables'; +@use 'fonts/roboto'; +@use 'fonts/roboto-mono'; -@import 'mastodon/reset'; -@import 'mastodon/basics'; -@import 'mastodon/branding'; -@import 'mastodon/containers'; -@import 'mastodon/lists'; -@import 'mastodon/widgets'; -@import 'mastodon/forms'; -@import 'mastodon/accounts'; -@import 'mastodon/components'; -@import 'mastodon/polls'; -@import 'mastodon/modal'; -@import 'mastodon/emoji_picker'; -@import 'mastodon/annual_reports'; -@import 'mastodon/about'; -@import 'mastodon/tables'; -@import 'mastodon/admin'; -@import 'mastodon/dashboard'; -@import 'mastodon/rtl'; -@import 'mastodon/accessibility'; -@import 'mastodon/rich_text'; +@use 'mastodon/reset'; +@use 'mastodon/basics'; +@use 'mastodon/branding'; +@use 'mastodon/containers'; +@use 'mastodon/lists'; +@use 'mastodon/widgets'; +@use 'mastodon/forms'; +@use 'mastodon/accounts'; +@use 'mastodon/components'; +@use 'mastodon/polls'; +@use 'mastodon/modal'; +@use 'mastodon/emoji_picker'; +@use 'mastodon/annual_reports'; +@use 'mastodon/about'; +@use 'mastodon/tables'; +@use 'mastodon/admin'; +@use 'mastodon/dashboard'; +@use 'mastodon/rtl'; +@use 'mastodon/accessibility'; +@use 'mastodon/rich_text'; diff --git a/app/javascript/styles/contrast.scss b/app/javascript/styles/contrast.scss index 5b43aecbe7..367be051f1 100644 --- a/app/javascript/styles/contrast.scss +++ b/app/javascript/styles/contrast.scss @@ -1,3 +1,3 @@ -@import 'contrast/variables'; -@import 'application'; -@import 'contrast/diff'; +@use 'contrast/variables'; +@use 'application'; +@use 'contrast/diff'; diff --git a/app/javascript/styles/contrast/diff.scss b/app/javascript/styles/contrast/diff.scss index ae607f484a..8aa05dd8ef 100644 --- a/app/javascript/styles/contrast/diff.scss +++ b/app/javascript/styles/contrast/diff.scss @@ -1,3 +1,5 @@ +@use '../mastodon/variables' as *; + .status__content a, .reply-indicator__content a, .edit-indicator__content a, diff --git a/app/javascript/styles/contrast/variables.scss b/app/javascript/styles/contrast/variables.scss index 2bee5eca74..d63512ce43 100644 --- a/app/javascript/styles/contrast/variables.scss +++ b/app/javascript/styles/contrast/variables.scss @@ -1,3 +1,5 @@ +@use '../mastodon/functions' as *; + // Dependent colors $black: #000000; @@ -14,12 +16,13 @@ $ui-primary-color: $classic-primary-color !default; $ui-secondary-color: $classic-secondary-color !default; $ui-highlight-color: $classic-highlight-color !default; -$darker-text-color: lighten($ui-primary-color, 20%) !default; -$dark-text-color: lighten($ui-primary-color, 12%) !default; -$secondary-text-color: lighten($ui-secondary-color, 6%) !default; -$highlight-text-color: lighten($ui-highlight-color, 10%) !default; -$action-button-color: lighten($ui-base-color, 50%); - -$inverted-text-color: $black !default; -$lighter-text-color: darken($ui-base-color, 6%) !default; -$light-text-color: darken($ui-primary-color, 40%) !default; +@use '../mastodon/variables' with ( + $darker-text-color: lighten($ui-primary-color, 20%), + $dark-text-color: lighten($ui-primary-color, 12%), + $secondary-text-color: lighten($ui-secondary-color, 6%), + $highlight-text-color: lighten($ui-highlight-color, 10%), + $action-button-color: lighten($ui-base-color, 50%), + $inverted-text-color: $black, + $lighter-text-color: darken($ui-base-color, 6%), + $light-text-color: darken($ui-primary-color, 40%) +); diff --git a/app/javascript/styles/full-dark.scss b/app/javascript/styles/full-dark.scss index 105964ba6f..33195d5c6e 100644 --- a/app/javascript/styles/full-dark.scss +++ b/app/javascript/styles/full-dark.scss @@ -1,3 +1,4 @@ -@import 'full-dark/variables'; -@import 'application'; -@import 'full-dark/diff'; +@use 'full-dark/variables'; +@use 'full-dark/css_variables'; +@use 'application'; +@use 'full-dark/diff'; diff --git a/app/javascript/styles/full-dark/css_variables.scss b/app/javascript/styles/full-dark/css_variables.scss new file mode 100644 index 0000000000..56e63dd23b --- /dev/null +++ b/app/javascript/styles/full-dark/css_variables.scss @@ -0,0 +1,4 @@ +@use 'sass:color'; +@use '../mastodon/variables' as *; +@use 'variables' as *; +@use '../mastodon/functions' as *; diff --git a/app/javascript/styles/full-dark/diff.scss b/app/javascript/styles/full-dark/diff.scss index 9483e7ecb6..727072dc39 100644 --- a/app/javascript/styles/full-dark/diff.scss +++ b/app/javascript/styles/full-dark/diff.scss @@ -1,3 +1,7 @@ +@use 'sass:color'; +@use '../mastodon/functions' as *; +@use '../mastodon/variables' as *; + input[type='text']:not(#cw-spoiler-input), input[type='search'], input[type='number'], diff --git a/app/javascript/styles/mailer.scss b/app/javascript/styles/mailer.scss index 1f3310877a..1e339b4313 100644 --- a/app/javascript/styles/mailer.scss +++ b/app/javascript/styles/mailer.scss @@ -1,4 +1,4 @@ -@import 'fonts/inter'; +@use 'fonts/inter'; body { accent-color: #6364ff; diff --git a/app/javascript/styles/mastodon-light.scss b/app/javascript/styles/mastodon-light.scss index 756a12d868..b530616a3c 100644 --- a/app/javascript/styles/mastodon-light.scss +++ b/app/javascript/styles/mastodon-light.scss @@ -1,3 +1,4 @@ -@import 'mastodon-light/variables'; -@import 'application'; -@import 'mastodon-light/diff'; +@use 'mastodon-light/variables'; +@use 'mastodon-light/css_variables'; +@use 'application'; +@use 'mastodon-light/diff'; diff --git a/app/javascript/styles/mastodon-light/css_variables.scss b/app/javascript/styles/mastodon-light/css_variables.scss new file mode 100644 index 0000000000..d9311da1b9 --- /dev/null +++ b/app/javascript/styles/mastodon-light/css_variables.scss @@ -0,0 +1,21 @@ +@use 'sass:color'; +@use '../mastodon/variables' as *; +@use 'variables' as *; +@use '../mastodon/functions' as *; + +body { + --dropdown-border-color: hsl(240deg, 25%, 88%); + --dropdown-background-color: #fff; + --modal-border-color: hsl(240deg, 25%, 88%); + --modal-background-color: var(--background-color-tint); + --background-border-color: hsl(240deg, 25%, 88%); + --background-color: #fff; + --background-color-tint: rgba(255, 255, 255, 80%); + --background-filter: blur(10px); + --on-surface-color: #{color.adjust($ui-base-color, $alpha: -0.65)}; + --rich-text-container-color: rgba(255, 216, 231, 100%); + --rich-text-text-color: rgba(114, 47, 83, 100%); + --rich-text-decorations-color: rgba(255, 175, 212, 100%); + --input-placeholder-color: #{color.adjust($dark-text-color, $alpha: -0.5)}; + --input-background-color: #{darken($ui-base-color, 10%)}; +} diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss index d23d789e46..8ca860a86d 100644 --- a/app/javascript/styles/mastodon-light/diff.scss +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -1,5 +1,8 @@ // Notes! // Sass color functions, "darken" and "lighten" are automatically replaced. +@use 'sass:color'; +@use '../mastodon/functions' as *; +@use '../mastodon/variables' as *; .simple_form .button.button-tertiary { color: $highlight-text-color; @@ -152,8 +155,12 @@ } .reactions-bar__item.active { - background-color: mix($white, $ui-highlight-color, 80%); - border-color: mix(lighten($ui-base-color, 8%), $ui-highlight-color, 80%); + background-color: color.mix($white, $ui-highlight-color, 80%); + border-color: color.mix( + lighten($ui-base-color, 8%), + $ui-highlight-color, + 80% + ); } .media-modal__overlay .picture-in-picture__footer { @@ -242,7 +249,7 @@ // Change the default colors used on some parts of the profile pages .activity-stream-tabs { - background: $account-background-color; + background: $white; border-bottom-color: lighten($ui-base-color, 8%); } @@ -284,7 +291,7 @@ } .entry { - background: $account-background-color; + background: $white; .detailed-status.light, .more.light, diff --git a/app/javascript/styles/mastodon-light/variables.scss b/app/javascript/styles/mastodon-light/variables.scss index c47ff2792c..43cd463868 100644 --- a/app/javascript/styles/mastodon-light/variables.scss +++ b/app/javascript/styles/mastodon-light/variables.scss @@ -1,87 +1,49 @@ @use 'sass:color'; -// Dependent colors -$black: #000000; -$white: #ffffff; +@use '../mastodon/functions' with ( + $darken-multiplier: 1, + $lighten-multiplier: -1 +); -$classic-base-color: hsl(240deg, 16%, 19%); -$classic-primary-color: hsl(240deg, 29%, 70%); -$classic-secondary-color: hsl(255deg, 25%, 88%); -$classic-highlight-color: hsl(240deg, 100%, 69%); - -$blurple-600: hsl(252deg, 59%, 51%); // Iris -$blurple-500: hsl(240deg, 100%, 69%); // Brand purple -$blurple-300: hsl(237deg, 92%, 75%); // Faded Blue +$black: #000000; // Black +$white: #ffffff; // White +$blurple-500: #6364ff; // Brand purple $grey-600: hsl(240deg, 8%, 33%); // Trout $grey-100: hsl(240deg, 51%, 90%); // Topaz $emoji-reaction-color: #dfe5f5 !default; $emoji-reaction-selected-color: #9ac1f2 !default; -// Differences -$success-green: lighten(hsl(138deg, 32%, 35%), 8%); +$classic-base-color: hsl(240deg, 16%, 19%); +$classic-secondary-color: hsl(255deg, 25%, 88%); +$classic-highlight-color: $blurple-500; -$base-overlay-background: $white !default; -$valid-value-color: $success-green !default; +@use '../mastodon/variables' with ( + $success-green: color.adjust( + hsl(138deg, 32%, 35%), + $lightness: 8%, + $space: hsl + ), + $base-overlay-background: $white, -$ui-base-color: $classic-secondary-color !default; -$ui-base-lighter-color: hsl(250deg, 24%, 75%); -$ui-primary-color: $classic-primary-color !default; -$ui-secondary-color: $classic-base-color !default; -$ui-highlight-color: $classic-highlight-color !default; + $ui-base-color: $classic-secondary-color, + $ui-base-lighter-color: hsl(250deg, 24%, 75%), + $ui-secondary-color: $classic-base-color, -$ui-button-secondary-color: $grey-600 !default; -$ui-button-secondary-border-color: $grey-600 !default; -$ui-button-secondary-focus-color: $white !default; + $ui-button-secondary-color: $grey-600, + $ui-button-secondary-border-color: $grey-600, + $ui-button-secondary-focus-color: $white, + $ui-button-tertiary-color: $blurple-500, + $ui-button-tertiary-border-color: $blurple-500, -$ui-button-tertiary-color: $blurple-500 !default; -$ui-button-tertiary-border-color: $blurple-500 !default; + $primary-text-color: $black, + $darker-text-color: $classic-base-color, + $lighter-text-color: $classic-base-color, + $highlight-text-color: $classic-highlight-color, + $dark-text-color: hsl(240deg, 16%, 32%), + $light-text-color: hsl(240deg, 16%, 32%), + $inverted-text-color: $black, -$primary-text-color: $black !default; -$darker-text-color: $classic-base-color !default; -$highlight-text-color: $ui-highlight-color !default; -$dark-text-color: hsl(240deg, 16%, 32%); -$action-button-color: hsl(240deg, 16%, 45%); - -$inverted-text-color: $black !default; -$lighter-text-color: $classic-base-color !default; -$light-text-color: hsl(240deg, 16%, 32%); - -// Newly added colors -$account-background-color: $white !default; - -// Invert darkened and lightened colors -@function darken($color, $amount) { - @return hsl( - hue($color), - color.channel($color, 'saturation', $space: hsl), - color.channel($color, 'lightness', $space: hsl) + $amount - ); -} - -@function lighten($color, $amount) { - @return hsl( - hue($color), - color.channel($color, 'saturation', $space: hsl), - color.channel($color, 'lightness', $space: hsl) - $amount - ); -} - -$emojis-requiring-inversion: 'chains'; - -body { - --dropdown-border-color: hsl(240deg, 25%, 88%); - --dropdown-background-color: #fff; - --modal-border-color: hsl(240deg, 25%, 88%); - --modal-background-color: var(--background-color-tint); - --background-border-color: hsl(240deg, 25%, 88%); - --background-color: #fff; - --background-color-tint: rgba(255, 255, 255, 80%); - --background-filter: blur(10px); - --on-surface-color: #{transparentize($ui-base-color, 0.65)}; - --rich-text-container-color: rgba(255, 216, 231, 100%); - --rich-text-text-color: rgba(114, 47, 83, 100%); - --rich-text-decorations-color: rgba(255, 175, 212, 100%); - --input-placeholder-color: #{transparentize($dark-text-color, 0.5)}; - --input-background-color: #{darken($ui-base-color, 10%)}; -} + $action-button-color: hsl(240deg, 16%, 45%), + $emojis-requiring-inversion: 'chains' +); diff --git a/app/javascript/styles/mastodon/_functions.scss b/app/javascript/styles/mastodon/_functions.scss new file mode 100644 index 0000000000..7190a6233e --- /dev/null +++ b/app/javascript/styles/mastodon/_functions.scss @@ -0,0 +1,21 @@ +@use 'sass:color'; + +$darken-multiplier: -1 !default; +$lighten-multiplier: 1 !default; + +// Invert darkened and lightened colors +@function darken($color, $amount) { + @return color.adjust( + $color, + $lightness: $amount * $darken-multiplier, + $space: hsl + ); +} + +@function lighten($color, $amount) { + @return color.adjust( + $color, + $lightness: $amount * $lighten-multiplier, + $space: hsl + ); +} diff --git a/app/javascript/styles/mastodon/_mixins.scss b/app/javascript/styles/mastodon/_mixins.scss index 2599cb0e05..b7d9203e3f 100644 --- a/app/javascript/styles/mastodon/_mixins.scss +++ b/app/javascript/styles/mastodon/_mixins.scss @@ -1,3 +1,5 @@ +@use 'variables' as *; + @mixin search-input { outline: 0; box-sizing: border-box; diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/_variables.scss similarity index 68% rename from app/javascript/styles/mastodon/variables.scss rename to app/javascript/styles/mastodon/_variables.scss index 6b1057605d..ea2d216441 100644 --- a/app/javascript/styles/mastodon/variables.scss +++ b/app/javascript/styles/mastodon/_variables.scss @@ -1,4 +1,5 @@ @use 'sass:color'; +@use 'functions' as *; // Commonly used web colors $black: #000000; // Black @@ -101,43 +102,13 @@ $media-modal-media-max-height: 80%; $no-gap-breakpoint: 1175px; $mobile-breakpoint: 630px; +$no-columns-breakpoint: 600px; $font-sans-serif: 'mastodon-font-sans-serif' !default; $font-display: 'mastodon-font-display' !default; $font-monospace: 'mastodon-font-monospace' !default; -:root { - --dropdown-border-color: #{lighten($ui-base-color, 4%)}; - --dropdown-background-color: #{rgba(darken($ui-base-color, 8%), 0.9)}; - --dropdown-shadow: 0 20px 25px -5px #{rgba($base-shadow-color, 0.25)}, - 0 8px 10px -6px #{rgba($base-shadow-color, 0.25)}; - --modal-background-color: #{rgba(darken($ui-base-color, 8%), 0.7)}; - --modal-background-variant-color: #{rgba($ui-base-color, 0.7)}; - --modal-border-color: #{lighten($ui-base-color, 4%)}; - --background-border-color: #{lighten($ui-base-color, 4%)}; - --background-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%); - --background-color: #{darken($ui-base-color, 8%)}; - --background-color-tint: #{rgba(darken($ui-base-color, 8%), 0.9)}; - --surface-background-color: #{darken($ui-base-color, 4%)}; - --surface-variant-background-color: #{$ui-base-color}; - --surface-variant-active-background-color: #{lighten($ui-base-color, 4%)}; - --on-surface-color: #{transparentize($ui-base-color, 0.5)}; - --avatar-border-radius: 8px; - --content-font-size: 15px; - --content-emoji-size: 20px; - --content-line-height: 22px; - --detail-content-font-size: 19px; - --detail-content-emoji-size: 24px; - --detail-content-line-height: 24px; - --media-outline-color: #{rgba(#fcf8ff, 0.15)}; - --overlay-icon-shadow: drop-shadow(0 0 8px #{rgba($base-shadow-color, 0.25)}); - --error-background-color: #{darken($error-red, 16%)}; - --error-active-background-color: #{darken($error-red, 12%)}; - --on-error-color: #fff; - --rich-text-container-color: rgba(87, 24, 60, 100%); - --rich-text-text-color: rgba(255, 175, 212, 100%); - --rich-text-decorations-color: rgba(128, 58, 95, 100%); - --input-placeholder-color: #{$dark-text-color}; - --input-background-color: var(--surface-variant-background-color); - --on-input-color: #{$secondary-text-color}; -} +$emojis-requiring-inversion: 'back' 'copyright' 'curly_loop' 'currency_exchange' + 'end' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign' + 'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'on' + 'registered' 'soon' 'spider' 'telephone_receiver' 'tm' 'top' 'wavy_dash' !default; diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss index 03e3ccd643..a310e2ffe7 100644 --- a/app/javascript/styles/mastodon/about.scss +++ b/app/javascript/styles/mastodon/about.scss @@ -1,3 +1,5 @@ +@use 'variables' as *; + $maximum-width: 1235px; $fluid-breakpoint: $maximum-width + 20px; diff --git a/app/javascript/styles/mastodon/accessibility.scss b/app/javascript/styles/mastodon/accessibility.scss index deaa0afdac..7cd2d4eae3 100644 --- a/app/javascript/styles/mastodon/accessibility.scss +++ b/app/javascript/styles/mastodon/accessibility.scss @@ -1,7 +1,4 @@ -$emojis-requiring-inversion: 'back' 'copyright' 'curly_loop' 'currency_exchange' - 'end' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign' - 'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'on' - 'registered' 'soon' 'spider' 'telephone_receiver' 'tm' 'top' 'wavy_dash' !default; +@use 'variables' as *; %emoji-color-inversion { filter: invert(1); diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index 89778b9f3b..34d4e840ef 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -1,3 +1,6 @@ +@use 'variables' as *; +@use 'functions' as *; + .card { & > a { display: block; diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 9c566719ef..c3035f946f 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -1,4 +1,6 @@ @use 'sass:math'; +@use 'functions' as *; +@use 'variables' as *; $no-columns-breakpoint: 890px; $sidebar-width: 300px; diff --git a/app/javascript/styles/mastodon/annual_reports.scss b/app/javascript/styles/mastodon/annual_reports.scss index dff1c76eca..96500a18bb 100644 --- a/app/javascript/styles/mastodon/annual_reports.scss +++ b/app/javascript/styles/mastodon/annual_reports.scss @@ -1,3 +1,5 @@ +@use 'variables' as *; + :root { --indigo-1: #17063b; --indigo-2: #2f0c7a; diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss index b7140fa6d6..69b88c8645 100644 --- a/app/javascript/styles/mastodon/basics.scss +++ b/app/javascript/styles/mastodon/basics.scss @@ -1,3 +1,6 @@ +@use 'variables' as *; +@use 'functions' as *; + @function hex-color($color) { @if type-of($color) == 'color' { $color: str-slice(ie-hex-str($color), 4); @@ -92,6 +95,7 @@ body { &.with-modals--active { overflow-y: hidden; + overscroll-behavior: none; } } diff --git a/app/javascript/styles/mastodon/branding.scss b/app/javascript/styles/mastodon/branding.scss index d1bddc68b0..8e8dd3530b 100644 --- a/app/javascript/styles/mastodon/branding.scss +++ b/app/javascript/styles/mastodon/branding.scss @@ -1,3 +1,5 @@ +@use 'variables' as *; + .logo { color: $primary-text-color; } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 8bfb1b518d..e5bc76a46f 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1,3 +1,8 @@ +@use 'sass:color'; +@use 'variables' as *; +@use 'functions' as *; +@use 'mixins' as *; + .app-body { -webkit-overflow-scrolling: touch; -ms-overflow-style: -ms-autohiding-scrollbar; @@ -1911,18 +1916,22 @@ body > [data-popper-placement] { .detailed-status__wrapper-direct { .detailed-status, .detailed-status__action-bar { - background: mix($ui-base-color, $ui-highlight-color, 95%); + background: color.mix($ui-base-color, $ui-highlight-color, 95%); } &:focus { .detailed-status, .detailed-status__action-bar { - background: mix(lighten($ui-base-color, 4%), $ui-highlight-color, 95%); + background: color.mix( + lighten($ui-base-color, 4%), + $ui-highlight-color, + 95% + ); } } .detailed-status__action-bar { - border-top-color: mix( + border-top-color: color.mix( lighten($ui-base-color, 8%), $ui-highlight-color, 95% @@ -2528,49 +2537,6 @@ a.account__display-name { } } -.image-loader { - position: relative; - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* IE 10+ */ - - * { - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* IE 10+ */ - } - - &::-webkit-scrollbar, - *::-webkit-scrollbar { - width: 0; - height: 0; - background: transparent; /* Chrome/Safari/Webkit */ - } - - .image-loader__preview-canvas { - max-width: $media-modal-media-max-width; - max-height: $media-modal-media-max-height; - background: url('../images/void.png') repeat; - object-fit: contain; - } - - .loading-bar__container { - position: relative; - } - - .loading-bar { - position: absolute; - } - - &.image-loader--amorphous .image-loader__preview-canvas { - display: none; - } -} - .zoomable-image { position: relative; width: 100%; @@ -2578,13 +2544,61 @@ a.account__display-name { display: flex; align-items: center; justify-content: center; + scrollbar-width: none; + overflow: hidden; + user-select: none; img { max-width: $media-modal-media-max-width; max-height: $media-modal-media-max-height; width: auto; height: auto; - object-fit: contain; + outline: 1px solid var(--media-outline-color); + outline-offset: -1px; + border-radius: 8px; + touch-action: none; + } + + &--zoomed-in { + z-index: 9999; + cursor: grab; + + img { + outline: none; + border-radius: 0; + } + } + + &--dragging { + cursor: grabbing; + } + + &--error img { + visibility: hidden; + } + + &__preview { + max-width: $media-modal-media-max-width; + max-height: $media-modal-media-max-height; + position: absolute; + z-index: 1; + outline: 1px solid var(--media-outline-color); + outline-offset: -1px; + border-radius: 8px; + overflow: hidden; + + canvas { + position: absolute; + width: 100%; + height: 100%; + object-fit: cover; + z-index: -1; + } + } + + .loading-indicator { + z-index: 2; + mix-blend-mode: luminosity; } } @@ -5887,6 +5901,7 @@ a.status-card { z-index: 9999; pointer-events: none; user-select: none; + overscroll-behavior: none; } .modal-root__modal { @@ -6020,7 +6035,7 @@ a.status-card { .picture-in-picture__footer { border-radius: 0; background: transparent; - padding: 20px 0; + padding: 16px; .icon-button { color: $white; @@ -7235,6 +7250,10 @@ a.status-card { filter: var(--overlay-icon-shadow); } } + + &--error img { + visibility: hidden; + } } .media-gallery__item-thumbnail { @@ -8858,7 +8877,7 @@ noscript { &.active { transition: all 100ms ease-in; transition-property: background-color, color; - background-color: mix( + background-color: color.mix( lighten($ui-base-color, 12%), $ui-highlight-color, 80% @@ -9759,6 +9778,7 @@ noscript { border: 1px solid $highlight-text-color; background: rgba($highlight-text-color, 0.15); overflow: hidden; + flex-shrink: 0; &__background-image { width: 125%; @@ -10186,6 +10206,9 @@ noscript { } .notification-bar-action { + display: inline-block; + border: 0; + background: transparent; text-transform: uppercase; margin-inline-start: 10px; cursor: pointer; diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index ac1f862a09..7db9ca409d 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -1,3 +1,5 @@ +@use 'variables' as *; + .container-alt { width: 700px; margin: 0 auto; diff --git a/app/javascript/styles/mastodon/css_variables.scss b/app/javascript/styles/mastodon/css_variables.scss new file mode 100644 index 0000000000..d1a357f730 --- /dev/null +++ b/app/javascript/styles/mastodon/css_variables.scss @@ -0,0 +1,39 @@ +@use 'sass:color'; +@use 'functions' as *; +@use 'variables' as *; + +:root { + --dropdown-border-color: #{lighten($ui-base-color, 4%)}; + --dropdown-background-color: #{rgba(darken($ui-base-color, 8%), 0.9)}; + --dropdown-shadow: 0 20px 25px -5px #{rgba($base-shadow-color, 0.25)}, + 0 8px 10px -6px #{rgba($base-shadow-color, 0.25)}; + --modal-background-color: #{rgba(darken($ui-base-color, 8%), 0.7)}; + --modal-background-variant-color: #{rgba($ui-base-color, 0.7)}; + --modal-border-color: #{lighten($ui-base-color, 4%)}; + --background-border-color: #{lighten($ui-base-color, 4%)}; + --background-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%); + --background-color: #{darken($ui-base-color, 8%)}; + --background-color-tint: #{rgba(darken($ui-base-color, 8%), 0.9)}; + --surface-background-color: #{darken($ui-base-color, 4%)}; + --surface-variant-background-color: #{$ui-base-color}; + --surface-variant-active-background-color: #{lighten($ui-base-color, 4%)}; + --on-surface-color: #{color.adjust($ui-base-color, $alpha: -0.5)}; + --avatar-border-radius: 8px; + --media-outline-color: #{rgba(#fcf8ff, 0.15)}; + --overlay-icon-shadow: drop-shadow(0 0 8px #{rgba($base-shadow-color, 0.25)}); + --error-background-color: #{darken($error-red, 16%)}; + --error-active-background-color: #{darken($error-red, 12%)}; + --on-error-color: #fff; + --rich-text-container-color: rgba(87, 24, 60, 100%); + --rich-text-text-color: rgba(255, 175, 212, 100%); + --rich-text-decorations-color: rgba(128, 58, 95, 100%); + --input-placeholder-color: #{$dark-text-color}; + --input-background-color: var(--surface-variant-background-color); + --on-input-color: #{$secondary-text-color}; + --content-font-size: 15px; + --content-emoji-size: 20px; + --content-line-height: 22px; + --detail-content-font-size: 19px; + --detail-content-emoji-size: 24px; + --detail-content-line-height: 24px; +} diff --git a/app/javascript/styles/mastodon/dashboard.scss b/app/javascript/styles/mastodon/dashboard.scss index d049b2456c..c99cdc357a 100644 --- a/app/javascript/styles/mastodon/dashboard.scss +++ b/app/javascript/styles/mastodon/dashboard.scss @@ -1,3 +1,6 @@ +@use 'functions' as *; +@use 'variables' as *; + .dashboard__counters { display: flex; flex-wrap: wrap; diff --git a/app/javascript/styles/mastodon/emoji_picker.scss b/app/javascript/styles/mastodon/emoji_picker.scss index e883bb4ab5..68f4c87ecd 100644 --- a/app/javascript/styles/mastodon/emoji_picker.scss +++ b/app/javascript/styles/mastodon/emoji_picker.scss @@ -1,3 +1,6 @@ +@use 'variables' as *; +@use 'functions' as *; + .emoji-mart { font-size: 13px; display: inline-block; diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index e7fdb35cb9..f8eaa43a20 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -1,4 +1,5 @@ -$no-columns-breakpoint: 600px; +@use 'variables' as *; +@use 'functions' as *; code { font-family: $font-monospace, monospace; @@ -365,6 +366,22 @@ code { } } + .input.date_of_birth .label_input { + display: flex; + gap: 8px; + align-items: center; + + input { + box-sizing: content-box; + width: 32px; + flex: 0; + + &:last-child { + width: 64px; + } + } + } + .input.select.select--languages { min-width: 32ch; } diff --git a/app/javascript/styles/mastodon/modal.scss b/app/javascript/styles/mastodon/modal.scss index 60e7d62245..7d060a2681 100644 --- a/app/javascript/styles/mastodon/modal.scss +++ b/app/javascript/styles/mastodon/modal.scss @@ -1,3 +1,6 @@ +@use 'variables' as *; +@use 'functions' as *; + .modal-layout { background: darken($ui-base-color, 4%) url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-lighter-color)}33"/></svg>') diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss index ced4c60c44..f49ce3c413 100644 --- a/app/javascript/styles/mastodon/polls.scss +++ b/app/javascript/styles/mastodon/polls.scss @@ -1,3 +1,6 @@ +@use 'variables' as *; +@use 'functions' as *; + .poll { margin-top: 16px; font-size: 14px; diff --git a/app/javascript/styles/mastodon/reset.scss b/app/javascript/styles/mastodon/reset.scss index d1ca4a1837..2dce637a06 100644 --- a/app/javascript/styles/mastodon/reset.scss +++ b/app/javascript/styles/mastodon/reset.scss @@ -1,3 +1,5 @@ +@use 'variables' as *; + /* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 License: none (public domain) diff --git a/app/javascript/styles/mastodon/rtl.scss b/app/javascript/styles/mastodon/rtl.scss index 0a05ce7c62..6aa94a97bc 100644 --- a/app/javascript/styles/mastodon/rtl.scss +++ b/app/javascript/styles/mastodon/rtl.scss @@ -1,3 +1,6 @@ +@use 'functions' as *; +@use 'variables' as *; + body.rtl { direction: rtl; diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index 310d3def07..620518ebf8 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -1,3 +1,6 @@ +@use 'variables' as *; +@use 'functions' as *; + .table { width: 100%; max-width: 100%; diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss index f467069052..8d09c7d583 100644 --- a/app/javascript/styles/mastodon/widgets.scss +++ b/app/javascript/styles/mastodon/widgets.scss @@ -1,3 +1,6 @@ +@use 'variables' as *; +@use 'functions' as *; + .directory { &__tag { box-sizing: border-box; diff --git a/app/lib/account_reach_finder.rb b/app/lib/account_reach_finder.rb index 19464024a6..f5e229c6a3 100644 --- a/app/lib/account_reach_finder.rb +++ b/app/lib/account_reach_finder.rb @@ -37,7 +37,11 @@ class AccountReachFinder def oldest_status_id Mastodon::Snowflake - .id_at(STATUS_SINCE.ago, with_random: false) + .id_at(oldest_status_date, with_random: false) + end + + def oldest_status_date + @account.suspended? && @account.suspension_origin_local? ? @account.suspended_at - STATUS_SINCE : STATUS_SINCE.ago end def recent_statuses diff --git a/app/lib/content_security_policy.rb b/app/lib/content_security_policy.rb index c764d1856d..fc42e2d48b 100644 --- a/app/lib/content_security_policy.rb +++ b/app/lib/content_security_policy.rb @@ -10,7 +10,7 @@ class ContentSecurityPolicy end def media_hosts - [assets_host, cdn_host_value, paperclip_root_url].compact + [assets_host, cdn_host_value, paperclip_root_url].concat(extra_media_hosts).compact end def sso_host @@ -31,6 +31,10 @@ class ContentSecurityPolicy private + def extra_media_hosts + ENV.fetch('EXTRA_MEDIA_HOSTS', '').split(/(?:\s*,\s*|\s+)/) + end + def url_from_configured_asset_host Rails.configuration.action_controller.asset_host end diff --git a/app/lib/emoji_formatter.rb b/app/lib/emoji_formatter.rb index c0302767ef..1574d4588d 100644 --- a/app/lib/emoji_formatter.rb +++ b/app/lib/emoji_formatter.rb @@ -24,7 +24,15 @@ class EmojiFormatter def to_s return html if custom_emojis.empty? || html.blank? - tree = Nokogiri::HTML5.fragment(html) + begin + tree = Nokogiri::HTML5.fragment(html) + rescue ArgumentError + # This can happen if one of the Nokogumbo limits is encountered + # Unfortunately, it does not use a more precise error class + # nor allows more graceful handling + return '' + end + tree.xpath('./text()|.//text()[not(ancestor[@class="invisible"])]').to_a.each do |node| i = -1 inside_shortname = false diff --git a/app/lib/plain_text_formatter.rb b/app/lib/plain_text_formatter.rb index f960ba7acc..e8ff79806f 100644 --- a/app/lib/plain_text_formatter.rb +++ b/app/lib/plain_text_formatter.rb @@ -16,7 +16,15 @@ class PlainTextFormatter if local? text else - node = Nokogiri::HTML5.fragment(insert_newlines) + begin + node = Nokogiri::HTML5.fragment(insert_newlines) + rescue ArgumentError + # This can happen if one of the Nokogumbo limits is encountered + # Unfortunately, it does not use a more precise error class + # nor allows more graceful handling + return '' + end + # Elements that are entirely removed with our Sanitize config node.xpath('.//iframe|.//math|.//noembed|.//noframes|.//noscript|.//plaintext|.//script|.//style|.//svg|.//xmp').remove node.text.chomp diff --git a/app/lib/request.rb b/app/lib/request.rb index 8fda86f0e8..ad39f928db 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -260,7 +260,7 @@ class Request outer_e = nil port = args.first - addresses = [] # rubocop:disable Lint/UselessAssignment -- TODO: https://github.com/rubocop/rubocop/issues/13395 + addresses = [] begin addresses = [IPAddr.new(host)] rescue IPAddr::InvalidAddressError diff --git a/app/models/account.rb b/app/models/account.rb index 0612e63fd5..f3f591d006 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -160,7 +160,7 @@ class Account < ApplicationRecord scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) } scope :not_domain_blocked_by_account, ->(account) { where(arel_table[:domain].eq(nil).or(arel_table[:domain].not_in(account.excluded_from_timeline_domains))) } scope :dormant, -> { joins(:account_stat).merge(AccountStat.without_recent_activity) } - scope :with_username, ->(value) { where arel_table[:username].lower.eq(value.to_s.downcase) } + scope :with_username, ->(value) { value.is_a?(Array) ? where(arel_table[:username].lower.in(value.map { |x| x.to_s.downcase })) : where(arel_table[:username].lower.eq(value.to_s.downcase)) } scope :with_domain, ->(value) { where arel_table[:domain].lower.eq(value&.to_s&.downcase) } scope :without_memorial, -> { where(memorial: false) } scope :duplicate_uris, -> { select(:uri, Arel.star.count).group(:uri).having(Arel.star.count.gt(1)) } diff --git a/app/models/account/field.rb b/app/models/account/field.rb index bcd89015de..4b3ccea9c4 100644 --- a/app/models/account/field.rb +++ b/app/models/account/field.rb @@ -73,7 +73,14 @@ class Account::Field < ActiveModelSerializers::Model end def extract_url_from_html - doc = Nokogiri::HTML5.fragment(value) + begin + doc = Nokogiri::HTML5.fragment(value) + rescue ArgumentError + # This can happen if one of the Nokogumbo limits is encountered + # Unfortunately, it does not use a more precise error class + # nor allows more graceful handling + return + end return if doc.nil? return if doc.children.size != 1 diff --git a/app/models/custom_filter.rb b/app/models/custom_filter.rb index 9e0784be28..386b49a2df 100644 --- a/app/models/custom_filter.rb +++ b/app/models/custom_filter.rb @@ -38,7 +38,7 @@ class CustomFilter < ApplicationRecord include Expireable include Redisable - enum :action, { warn: 0, hide: 1 }, suffix: :action + enum :action, { warn: 0, hide: 1, blur: 2 }, suffix: :action belongs_to :account has_many :keywords, class_name: 'CustomFilterKeyword', inverse_of: :custom_filter, dependent: :destroy diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index f9c5b80262..31ac04b754 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -65,6 +65,7 @@ class Form::AdminSettings stop_link_preview_domains app_icon favicon + min_age ).freeze INTEGER_KEYS = %i( @@ -80,6 +81,7 @@ class Form::AdminSettings registrations_end_hour registrations_secondary_start_hour registrations_secondary_end_hour + min_age ).freeze BOOLEAN_KEYS = %i( @@ -140,6 +142,7 @@ class Form::AdminSettings validates :show_domain_blocks, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks) } validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks_rationale) } validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) } + validates :min_age, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@min_age) } validates :site_short_description, length: { maximum: DESCRIPTION_LIMIT }, if: -> { defined?(@site_short_description) } validates :status_page_url, url: true, allow_blank: true validate :validate_site_uploads diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 49ff740884..89c74d5a41 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -116,7 +116,7 @@ class MediaAttachment < ApplicationRecord VIDEO_PASSTHROUGH_OPTIONS = { video_codecs: ['h264'].freeze, audio_codecs: ['aac', nil].freeze, - colorspaces: ['yuv420p'].freeze, + colorspaces: ['yuv420p', 'yuvj420p'].freeze, options: { format: 'mp4', convert_options: { @@ -425,8 +425,10 @@ class MediaAttachment < ApplicationRecord @paths_to_cache_bust = MediaAttachment.attachment_definitions.keys.flat_map do |attachment_name| attachment = public_send(attachment_name) + next if attachment.blank? + styles = DEFAULT_STYLES | attachment.styles.keys - styles.map { |style| attachment.path(style) } + styles.map { |style| attachment.url(style) } end.compact rescue => e # We really don't want any error here preventing media deletion diff --git a/app/models/trends/links.rb b/app/models/trends/links.rb index 35ccf7744c..c24e069b51 100644 --- a/app/models/trends/links.rb +++ b/app/models/trends/links.rb @@ -33,7 +33,8 @@ class Trends::Links < Trends::Base def register(status, at_time = Time.now.utc) original_status = status.proper - return unless (original_status.public_visibility? && status.public_visibility?) && + return unless original_status.public_visibility? && + status.public_visibility? && !(original_status.account.silenced? || status.account.silenced?) && !(original_status.spoiler_text? || original_status.sensitive?) diff --git a/app/models/user.rb b/app/models/user.rb index 70e8ed2e87..7858ab906d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,41 +5,42 @@ # Table name: users # # id :bigint(8) not null, primary key -# email :string default(""), not null -# created_at :datetime not null -# updated_at :datetime not null -# encrypted_password :string default(""), not null -# reset_password_token :string -# reset_password_sent_at :datetime -# sign_in_count :integer default(0), not null -# current_sign_in_at :datetime -# last_sign_in_at :datetime +# age_verified_at :datetime +# approved :boolean default(TRUE), not null +# chosen_languages :string is an Array +# confirmation_sent_at :datetime # confirmation_token :string # confirmed_at :datetime -# confirmation_sent_at :datetime -# unconfirmed_email :string -# locale :string +# consumed_timestep :integer +# current_sign_in_at :datetime +# disabled :boolean default(FALSE), not null +# email :string default(""), not null # encrypted_otp_secret :string # encrypted_otp_secret_iv :string # encrypted_otp_secret_salt :string -# consumed_timestep :integer -# otp_required_for_login :boolean default(FALSE), not null +# encrypted_password :string default(""), not null # last_emailed_at :datetime +# last_sign_in_at :datetime +# locale :string # otp_backup_codes :string is an Array -# account_id :bigint(8) not null -# disabled :boolean default(FALSE), not null -# invite_id :bigint(8) -# chosen_languages :string is an Array -# created_by_application_id :bigint(8) -# approved :boolean default(TRUE), not null +# otp_required_for_login :boolean default(FALSE), not null +# otp_secret :string +# reset_password_sent_at :datetime +# reset_password_token :string +# settings :text +# sign_in_count :integer default(0), not null # sign_in_token :string # sign_in_token_sent_at :datetime -# webauthn_id :string # sign_up_ip :inet -# role_id :bigint(8) -# settings :text # time_zone :string -# otp_secret :string +# unconfirmed_email :string +# created_at :datetime not null +# updated_at :datetime not null +# account_id :bigint(8) not null +# created_by_application_id :bigint(8) +# invite_id :bigint(8) +# role_id :bigint(8) +# webauthn_id :string # class User < ApplicationRecord @@ -116,6 +117,7 @@ class User < ApplicationRecord validates_with RegistrationFormTimeValidator, on: :create validates :website, absence: true, on: :create validates :confirm_password, absence: true, on: :create + validates :date_of_birth, presence: true, date_of_birth: true, on: :create, if: -> { Setting.min_age.present? } validate :validate_role_elevation scope :account_not_suspended, -> { joins(:account).merge(Account.without_suspended) } @@ -134,6 +136,7 @@ class User < ApplicationRecord before_validation :sanitize_role before_create :set_approved + before_create :set_age_verified_at after_commit :send_pending_devise_notifications after_create_commit :trigger_webhooks @@ -145,7 +148,7 @@ class User < ApplicationRecord delegate :can?, to: :role - attr_reader :invite_code + attr_reader :invite_code, :date_of_birth attr_writer :external, :bypass_invite_request_check, :current_account def self.those_who_can(*any_of_privileges) @@ -162,6 +165,17 @@ class User < ApplicationRecord Rails.env.local? end + def date_of_birth=(hash_or_string) + @date_of_birth = begin + if hash_or_string.is_a?(Hash) + day, month, year = hash_or_string.values_at(1, 2, 3) + "#{day}.#{month}.#{year}" + else + hash_or_string + end + end + end + def role if role_id.nil? UserRole.everyone @@ -455,6 +469,10 @@ class User < ApplicationRecord end end + def set_age_verified_at + self.age_verified_at = Time.now.utc if Setting.min_age.present? + end + def grant_approval_on_confirmation? # Re-check approval on confirmation if the server has switched to open registrations open_registrations? && !sign_up_from_ip_requires_approval? && !sign_up_email_requires_approval? diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index 57ceef9bd5..b786baedfa 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -131,7 +131,9 @@ class REST::InstanceSerializer < ActiveModel::Serializer enabled: registrations_enabled?, approval_required: Setting.registrations_mode == 'approved' || (Setting.registrations_mode == 'open' && !registrations_in_time?), limit_reached: Setting.registrations_mode != 'none' && reach_registrations_limit?, + reason_required: Setting.registrations_mode == 'approved' && Setting.require_invite_text, message: registrations_enabled? ? nil : registrations_message, + min_age: Setting.min_age.presence, url: ENV.fetch('SSO_ACCOUNT_SIGN_UP', nil), } end diff --git a/app/serializers/rest/terms_of_service_serializer.rb b/app/serializers/rest/terms_of_service_serializer.rb index 7f48788693..373cb8b56f 100644 --- a/app/serializers/rest/terms_of_service_serializer.rb +++ b/app/serializers/rest/terms_of_service_serializer.rb @@ -4,7 +4,7 @@ class REST::TermsOfServiceSerializer < ActiveModel::Serializer attributes :effective_date, :effective, :content, :succeeded_by def effective_date - object.effective_date.iso8601 + (object.effective_date || object.published_at).iso8601 end def effective diff --git a/app/services/activitypub/synchronize_followers_service.rb b/app/services/activitypub/synchronize_followers_service.rb index b01974dcc6..5b58a025cb 100644 --- a/app/services/activitypub/synchronize_followers_service.rb +++ b/app/services/activitypub/synchronize_followers_service.rb @@ -4,32 +4,46 @@ class ActivityPub::SynchronizeFollowersService < BaseService include JsonLdHelper include Payloadable + MAX_COLLECTION_PAGES = 10 + def call(account, partial_collection_url) @account = account + @expected_followers_ids = [] - items = collection_items(partial_collection_url) - return if items.nil? - - # There could be unresolved accounts (hence the call to .compact) but this - # should never happen in practice, since in almost all cases we keep an - # Account record, and should we not do that, we should have sent a Delete. - # In any case there is not much we can do if that occurs. - @expected_followers = items.filter_map { |uri| ActivityPub::TagManager.instance.uri_to_resource(uri, Account) } + return unless process_collection!(partial_collection_url) remove_unexpected_local_followers! - handle_unexpected_outgoing_follows! end private + def process_page!(items) + page_expected_followers = extract_local_followers(items) + @expected_followers_ids.concat(page_expected_followers.pluck(:id)) + + handle_unexpected_outgoing_follows!(page_expected_followers) + end + + def extract_local_followers(items) + # There could be unresolved accounts (hence the call to .filter_map) but this + # should never happen in practice, since in almost all cases we keep an + # Account record, and should we not do that, we should have sent a Delete. + # In any case there is not much we can do if that occurs. + + # TODO: this will need changes when switching to numeric IDs + + usernames = items.filter_map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username)&.downcase } + Account.local.with_username(usernames) + end + def remove_unexpected_local_followers! - @account.followers.local.where.not(id: @expected_followers.map(&:id)).reorder(nil).find_each do |unexpected_follower| + @account.followers.local.where.not(id: @expected_followers_ids).reorder(nil).find_each do |unexpected_follower| UnfollowService.new.call(unexpected_follower, @account) end end - def handle_unexpected_outgoing_follows! - @expected_followers.each do |expected_follower| + def handle_unexpected_outgoing_follows!(expected_followers) + expected_followers.each do |expected_follower| next if expected_follower.following?(@account) if expected_follower.requested?(@account) @@ -50,18 +64,33 @@ class ActivityPub::SynchronizeFollowersService < BaseService Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer)) end - def collection_items(collection_or_uri) - collection = fetch_collection(collection_or_uri) - return unless collection.is_a?(Hash) + # Only returns true if the whole collection has been processed + def process_collection!(collection_uri, max_pages: MAX_COLLECTION_PAGES) + collection = fetch_collection(collection_uri) + return false unless collection.is_a?(Hash) collection = fetch_collection(collection['first']) if collection['first'].present? - return unless collection.is_a?(Hash) + while collection.is_a?(Hash) + process_page!(as_array(collection_page_items(collection))) + + max_pages -= 1 + + return true if collection['next'].blank? # We reached the end of the collection + return false if max_pages <= 0 # We reached our pages limit + + collection = fetch_collection(collection['next']) + end + + false + end + + def collection_page_items(collection) case collection['type'] when 'Collection', 'CollectionPage' - as_array(collection['items']) + collection['items'] when 'OrderedCollection', 'OrderedCollectionPage' - as_array(collection['orderedItems']) + collection['orderedItems'] end end diff --git a/app/services/app_sign_up_service.rb b/app/services/app_sign_up_service.rb index 7665880115..a4399efd65 100644 --- a/app/services/app_sign_up_service.rb +++ b/app/services/app_sign_up_service.rb @@ -41,7 +41,7 @@ class AppSignUpService < BaseService end def user_params - @params.slice(:email, :password, :agreement, :locale, :time_zone, :invite_code) + @params.slice(:email, :password, :agreement, :locale, :time_zone, :invite_code, :date_of_birth) end def account_params diff --git a/app/services/concerns/payloadable.rb b/app/services/concerns/payloadable.rb index 4fa534c6d1..42ed994a7f 100644 --- a/app/services/concerns/payloadable.rb +++ b/app/services/concerns/payloadable.rb @@ -19,7 +19,7 @@ module Payloadable object = record.respond_to?(:virtual_object) ? record.virtual_object : record bearcap = object.is_a?(String) && record.respond_to?(:type) && ['Create', 'Update'].include?(record.type) - if ((object.respond_to?(:sign?) && object.sign?) && signer && (always_sign || signing_enabled?)) || bearcap || (signer && always_sign_unsafe) + if (object.respond_to?(:sign?) && object.sign? && signer && (always_sign || signing_enabled?)) || bearcap || (signer && always_sign_unsafe) ActivityPub::LinkedDataSignature.new(payload).sign!(signer, sign_with: sign_with) else payload diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index 44210799f9..3934a738f7 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -95,7 +95,7 @@ class SuspendAccountService < BaseService end end - CacheBusterWorker.perform_async(attachment.path(style)) if Rails.configuration.x.cache_buster_enabled + CacheBusterWorker.perform_async(attachment.url(style)) if Rails.configuration.x.cache_buster_enabled end end end diff --git a/app/services/unsuspend_account_service.rb b/app/services/unsuspend_account_service.rb index 652dd6a845..7d3bb806a6 100644 --- a/app/services/unsuspend_account_service.rb +++ b/app/services/unsuspend_account_service.rb @@ -91,7 +91,7 @@ class UnsuspendAccountService < BaseService end end - CacheBusterWorker.perform_async(attachment.path(style)) if Rails.configuration.x.cache_buster_enabled + CacheBusterWorker.perform_async(attachment.url(style)) if Rails.configuration.x.cache_buster_enabled end end end diff --git a/app/validators/date_of_birth_validator.rb b/app/validators/date_of_birth_validator.rb new file mode 100644 index 0000000000..79119d2c4c --- /dev/null +++ b/app/validators/date_of_birth_validator.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class DateOfBirthValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + record.errors.add(attribute, :below_limit) if value.present? && value.to_date > min_age.ago + rescue Date::Error + record.errors.add(attribute, :invalid) + end + + private + + def min_age + Setting.min_age.to_i.years + end +end diff --git a/app/views/admin/settings/registrations/show.html.haml b/app/views/admin/settings/registrations/show.html.haml index fe6a71a4ca..4bd384f8b5 100644 --- a/app/views/admin/settings/registrations/show.html.haml +++ b/app/views/admin/settings/registrations/show.html.haml @@ -12,6 +12,9 @@ .flash-message= t('admin.settings.registrations.moderation_recommandation') + .fields-group + = f.input :min_age, as: :string, wrapper: :with_block_label, input_html: { inputmode: 'numeric' } + .fields-row .fields-row__column.fields-row__column-6.fields-group = f.input :registrations_mode, diff --git a/app/views/admin/terms_of_service/index.html.haml b/app/views/admin/terms_of_service/index.html.haml index 457ef42670..636851b449 100644 --- a/app/views/admin/terms_of_service/index.html.haml +++ b/app/views/admin/terms_of_service/index.html.haml @@ -11,7 +11,7 @@ .dot-indicator.success .dot-indicator__indicator %span - - if @terms_of_service.effective? + - if @terms_of_service.effective? || @terms_of_service.effective_date.nil? = t('admin.terms_of_service.live') - else = t('admin.terms_of_service.going_live_on_html', date: tag.time(l(@terms_of_service.effective_date), class: 'formatted', date: @terms_of_service.effective_date.iso8601)) diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml index 74dffe83ee..ebbef00ff3 100644 --- a/app/views/auth/registrations/new.html.haml +++ b/app/views/auth/registrations/new.html.haml @@ -21,20 +21,19 @@ = f.simple_fields_for :account do |ff| = ff.input :username, append: "@#{site_hostname}", - input_html: { 'aria-label': t('simple_form.labels.defaults.username'), autocomplete: 'off', placeholder: t('simple_form.labels.defaults.username'), pattern: '[a-zA-Z0-9_]+', maxlength: Account::USERNAME_LENGTH_LIMIT }, - label: false, + input_html: { autocomplete: 'off', pattern: '[a-zA-Z0-9_]+', maxlength: Account::USERNAME_LENGTH_LIMIT, placeholder: ' ' }, required: true, wrapper: :with_label = f.input :email, hint: false, - input_html: { 'aria-label': t('simple_form.labels.defaults.email'), autocomplete: 'username' }, - placeholder: t('simple_form.labels.defaults.email'), - required: true + input_html: { autocomplete: 'username', placeholder: ' ' }, + required: true, + wrapper: :with_label = f.input :password, hint: false, - input_html: { 'aria-label': t('simple_form.labels.defaults.password'), autocomplete: 'new-password', minlength: User.password_length.first, maxlength: User.password_length.last }, - placeholder: t('simple_form.labels.defaults.password'), - required: true + input_html: { autocomplete: 'new-password', minlength: User.password_length.first, maxlength: User.password_length.last, placeholder: ' ' }, + required: true, + wrapper: :with_label = f.input :password_confirmation, hint: false, input_html: { 'aria-label': t('simple_form.labels.defaults.confirm_password'), autocomplete: 'new-password', maxlength: User.password_length.last }, @@ -53,6 +52,14 @@ required: false, wrapper: :with_label + - if Setting.min_age.present? + .fields-group + = f.input :date_of_birth, + as: :date_of_birth, + hint: t('simple_form.hints.user.date_of_birth', age: Setting.min_age.to_i), + required: true, + wrapper: :with_block_label + - if approved_registrations? && @invite.blank? %p.lead= t('auth.sign_up.manual_review', domain: site_hostname) diff --git a/app/views/filters/_filter_fields.html.haml b/app/views/filters/_filter_fields.html.haml index 911b10467a..18e9a6580e 100644 --- a/app/views/filters/_filter_fields.html.haml +++ b/app/views/filters/_filter_fields.html.haml @@ -26,7 +26,7 @@ .fields-group = f.input :filter_action, as: :radio_buttons, - collection: %i(warn hide), + collection: %i(warn blur hide), hint: t('simple_form.hints.filters.action'), include_blank: false, label_method: ->(action) { filter_action_label(action) }, diff --git a/app/workers/poll_expiration_notify_worker.rb b/app/workers/poll_expiration_notify_worker.rb index b7a60fab84..fe7647024e 100644 --- a/app/workers/poll_expiration_notify_worker.rb +++ b/app/workers/poll_expiration_notify_worker.rb @@ -8,7 +8,7 @@ class PollExpirationNotifyWorker def perform(poll_id) @poll = Poll.find(poll_id) - return if does_not_expire? + return if missing_expiration? requeue! && return if not_due_yet? notify_remote_voters_and_owner! if @poll.local? @@ -24,7 +24,7 @@ class PollExpirationNotifyWorker private - def does_not_expire? + def missing_expiration? @poll.expires_at.nil? end diff --git a/config/initializers/prometheus_exporter.rb b/config/initializers/prometheus_exporter.rb index fab095658f..fdfee59dc8 100644 --- a/config/initializers/prometheus_exporter.rb +++ b/config/initializers/prometheus_exporter.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true if ENV['MASTODON_PROMETHEUS_EXPORTER_ENABLED'] == 'true' + require 'prometheus_exporter' + require 'prometheus_exporter/middleware' + if ENV['MASTODON_PROMETHEUS_EXPORTER_LOCAL'] == 'true' - require 'prometheus_exporter' require 'prometheus_exporter/server' require 'prometheus_exporter/client' @@ -17,9 +19,11 @@ if ENV['MASTODON_PROMETHEUS_EXPORTER_ENABLED'] == 'true' if ENV['MASTODON_PROMETHEUS_EXPORTER_WEB_DETAILED_METRICS'] == 'true' # Optional, as those metrics might generate extra overhead and be redundant with what OTEL provides - require 'prometheus_exporter/middleware' - # Per-action/controller request stats like HTTP status and timings Rails.application.middleware.unshift PrometheusExporter::Middleware + else + # Include stripped down version of PrometheusExporter::Middleware that only collects queue time + require 'mastodon/middleware/prometheus_queue_time' + Rails.application.middleware.unshift Mastodon::Middleware::PrometheusQueueTime, instrument: false end end diff --git a/config/locales/activerecord.bg.yml b/config/locales/activerecord.bg.yml index 55436e59ad..0b33e953ae 100644 --- a/config/locales/activerecord.bg.yml +++ b/config/locales/activerecord.bg.yml @@ -55,6 +55,8 @@ bg: too_soon: е твърде скоро и трябва да е по-късно от %{date} user: attributes: + date_of_birth: + below_limit: е под възрастовата граница email: blocked: използва се забранен доставчик на услуга за е-поща unreachable: изглежда не съществува diff --git a/config/locales/activerecord.ca.yml b/config/locales/activerecord.ca.yml index dd98774c35..f53f7f364a 100644 --- a/config/locales/activerecord.ca.yml +++ b/config/locales/activerecord.ca.yml @@ -55,6 +55,8 @@ ca: too_soon: és massa aviat, ha de ser després de %{date} user: attributes: + date_of_birth: + below_limit: és inferior a l'edat mínima email: blocked: utilitza un proveïdor de correu-e no autoritzat unreachable: sembla que no existeix diff --git a/config/locales/activerecord.cs.yml b/config/locales/activerecord.cs.yml index 6b89af6004..38708713d2 100644 --- a/config/locales/activerecord.cs.yml +++ b/config/locales/activerecord.cs.yml @@ -55,6 +55,8 @@ cs: too_soon: je příliš brzy, musí být později než %{date} user: attributes: + date_of_birth: + below_limit: je pod věkovou hranicí email: blocked: používá zakázanou e-mailovou službu unreachable: pravděpodobně neexistuje diff --git a/config/locales/activerecord.cy.yml b/config/locales/activerecord.cy.yml index 7f75449ab7..c201016be6 100644 --- a/config/locales/activerecord.cy.yml +++ b/config/locales/activerecord.cy.yml @@ -3,7 +3,7 @@ cy: activerecord: attributes: poll: - expires_at: Terfyn amser + expires_at: Dyddiad cau options: Dewisiadau user: agreement: Cytundeb gwasanaeth @@ -55,6 +55,8 @@ cy: too_soon: yn rhy fuan, rhaid iddo fod yn hwyrach na %{date} user: attributes: + date_of_birth: + below_limit: yn iau na'r terfyn oedran email: blocked: yn defnyddio darparwr e-bost nad yw'n cael ei ganiatáu unreachable: nid yw i weld yn bodoli @@ -63,7 +65,7 @@ cy: user_role: attributes: permissions_as_keys: - dangerous: yn cynnwys caniatâd nad ydynt yn ddiogel ar gyfer rôl sail + dangerous: yn cynnwys caniatâd nad ydyn nhw'n ddiogel ar gyfer rôl sail elevated: yn methu a chynnwys caniatâd nad yw eich rôl cyfredol yn ei gynnwys own_role: nid oes modd ei newid gyda'ch rôl cyfredol position: @@ -72,4 +74,4 @@ cy: webhook: attributes: events: - invalid_permissions: ni ellir cynnwys digwyddiadau nad oes gennych yr hawl iddynt + invalid_permissions: nid oes modd cynnwys digwyddiadau nad oes gennych yr hawl iddyn nhw diff --git a/config/locales/activerecord.da.yml b/config/locales/activerecord.da.yml index 8e98b5f8d2..7b49c18ca3 100644 --- a/config/locales/activerecord.da.yml +++ b/config/locales/activerecord.da.yml @@ -55,6 +55,8 @@ da: too_soon: er for tidligt, skal være efter %{date} user: attributes: + date_of_birth: + below_limit: er under alderskravet email: blocked: bruger en ikke-tilladt e-mailudbyder unreachable: ser ikke ud til at eksistere diff --git a/config/locales/activerecord.de.yml b/config/locales/activerecord.de.yml index 1a4626b0b2..4ae7aec5dd 100644 --- a/config/locales/activerecord.de.yml +++ b/config/locales/activerecord.de.yml @@ -55,6 +55,8 @@ de: too_soon: Datum muss später als %{date} sein user: attributes: + date_of_birth: + below_limit: liegt unterhalb der Altersgrenze email: blocked: verwendet einen unerlaubten E-Mail-Anbieter unreachable: scheint nicht zu existieren diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml index ed389c1323..6940d589ca 100644 --- a/config/locales/activerecord.en.yml +++ b/config/locales/activerecord.en.yml @@ -55,6 +55,8 @@ en: too_soon: is too soon, must be later than %{date} user: attributes: + date_of_birth: + below_limit: is below the age limit email: blocked: uses a disallowed e-mail provider unreachable: does not seem to exist diff --git a/config/locales/activerecord.es-AR.yml b/config/locales/activerecord.es-AR.yml index 86080aebb2..62a409a353 100644 --- a/config/locales/activerecord.es-AR.yml +++ b/config/locales/activerecord.es-AR.yml @@ -55,6 +55,8 @@ es-AR: too_soon: es demasiado pronto, debe ser posterior a %{date} user: attributes: + date_of_birth: + below_limit: está por debajo de la edad mínima email: blocked: usa un proveedor de correo electrónico no permitido unreachable: no parece existir diff --git a/config/locales/activerecord.es-MX.yml b/config/locales/activerecord.es-MX.yml index 3af9eb598a..c3b0562c32 100644 --- a/config/locales/activerecord.es-MX.yml +++ b/config/locales/activerecord.es-MX.yml @@ -55,6 +55,8 @@ es-MX: too_soon: es demasiado pronto, debe ser posterior al %{date} user: attributes: + date_of_birth: + below_limit: está por debajo de la edad mínima email: blocked: utiliza un proveedor de correo no autorizado unreachable: no parece existir diff --git a/config/locales/activerecord.es.yml b/config/locales/activerecord.es.yml index d5facd3477..94f29365e9 100644 --- a/config/locales/activerecord.es.yml +++ b/config/locales/activerecord.es.yml @@ -55,6 +55,8 @@ es: too_soon: es demasiado pronto, debe ser posterior a %{date} user: attributes: + date_of_birth: + below_limit: está por debajo de la edad mínima email: blocked: utiliza un proveedor de correo no autorizado unreachable: no parece existir diff --git a/config/locales/activerecord.fi.yml b/config/locales/activerecord.fi.yml index 3bb580a113..c731688a1f 100644 --- a/config/locales/activerecord.fi.yml +++ b/config/locales/activerecord.fi.yml @@ -55,6 +55,8 @@ fi: too_soon: on liian pian, täytyy olla myöhemmin kuin %{date} user: attributes: + date_of_birth: + below_limit: alittaa alaikärajan email: blocked: käyttää kiellettyä sähköpostipalveluntarjoajaa unreachable: ei näytä olevan olemassa diff --git a/config/locales/activerecord.fo.yml b/config/locales/activerecord.fo.yml index 9c2715d68b..ce84a1fffd 100644 --- a/config/locales/activerecord.fo.yml +++ b/config/locales/activerecord.fo.yml @@ -55,6 +55,8 @@ fo: too_soon: tað er ov tíðliga, má vera eftir %{date} user: attributes: + date_of_birth: + below_limit: er niðanfyri aldursmarkið email: blocked: brúkar ein ikki loyvdan teldopostveitara unreachable: tykist ikki at vera til diff --git a/config/locales/activerecord.fr-CA.yml b/config/locales/activerecord.fr-CA.yml index 97aee2304c..a966bb5a8a 100644 --- a/config/locales/activerecord.fr-CA.yml +++ b/config/locales/activerecord.fr-CA.yml @@ -49,8 +49,14 @@ fr-CA: attributes: reblog: taken: de la publication existe déjà + terms_of_service: + attributes: + effective_date: + too_soon: est trop tôt, doit être plus tard que %{date} user: attributes: + date_of_birth: + below_limit: est en dessous de la limite d'âge email: blocked: utilise un fournisseur de courriel interdit unreachable: ne semble pas exister diff --git a/config/locales/activerecord.fr.yml b/config/locales/activerecord.fr.yml index 85dbaf293b..ae3ce7f9cb 100644 --- a/config/locales/activerecord.fr.yml +++ b/config/locales/activerecord.fr.yml @@ -49,8 +49,14 @@ fr: attributes: reblog: taken: du message existe déjà + terms_of_service: + attributes: + effective_date: + too_soon: est trop tôt, doit être plus tard que %{date} user: attributes: + date_of_birth: + below_limit: est en dessous de la limite d'âge email: blocked: utilise un fournisseur de courriel interdit unreachable: ne semble pas exister diff --git a/config/locales/activerecord.gl.yml b/config/locales/activerecord.gl.yml index 5a2393ced5..f4e6725565 100644 --- a/config/locales/activerecord.gl.yml +++ b/config/locales/activerecord.gl.yml @@ -55,6 +55,8 @@ gl: too_soon: é demasiado axiña, debería ser posterior a %{date} user: attributes: + date_of_birth: + below_limit: é inferior ao límite de idade email: blocked: utiliza un provedor de email non autorizado unreachable: semella que non existe diff --git a/config/locales/activerecord.he.yml b/config/locales/activerecord.he.yml index 4649bc00ad..7dff17493b 100644 --- a/config/locales/activerecord.he.yml +++ b/config/locales/activerecord.he.yml @@ -55,6 +55,8 @@ he: too_soon: מוקדם מדי, חייב להיות אחרי %{date} user: attributes: + date_of_birth: + below_limit: מתחת למגבלת הגיל email: blocked: עושה שימוש בספק דוא"ל אסור unreachable: נראה שלא קיים diff --git a/config/locales/activerecord.hu.yml b/config/locales/activerecord.hu.yml index ee5c79d252..cf2f50a9f9 100644 --- a/config/locales/activerecord.hu.yml +++ b/config/locales/activerecord.hu.yml @@ -55,6 +55,8 @@ hu: too_soon: túl korán van, később kellene lennie, mint %{date} user: attributes: + date_of_birth: + below_limit: a korhatár alatt van email: blocked: egy letiltott email szolgáltatót használ unreachable: úgy tűnik, hogy nem létezik diff --git a/config/locales/activerecord.is.yml b/config/locales/activerecord.is.yml index 559a49880f..cff90a3476 100644 --- a/config/locales/activerecord.is.yml +++ b/config/locales/activerecord.is.yml @@ -55,6 +55,8 @@ is: too_soon: er of snemmt, verður að vera síðar en %{date} user: attributes: + date_of_birth: + below_limit: er undir aldurstakmörkum email: blocked: notar óleyfilega tölvupóstþjónustu unreachable: virðist ekki vera til diff --git a/config/locales/activerecord.it.yml b/config/locales/activerecord.it.yml index 89e038615d..9ff385f26e 100644 --- a/config/locales/activerecord.it.yml +++ b/config/locales/activerecord.it.yml @@ -55,6 +55,8 @@ it: too_soon: è troppo presto, deve essere successivo alla data %{date} user: attributes: + date_of_birth: + below_limit: è inferiore al limite di età email: blocked: utilizza un provider di posta elettronica non autorizzato unreachable: non sembra esistere diff --git a/config/locales/activerecord.ko.yml b/config/locales/activerecord.ko.yml index 5b1542496f..3aa991734b 100644 --- a/config/locales/activerecord.ko.yml +++ b/config/locales/activerecord.ko.yml @@ -55,6 +55,8 @@ ko: too_soon: 너무 이릅니다. %{date} 이후로 지정해야 합니다 user: attributes: + date_of_birth: + below_limit: 나이 제한보다 아래입니다 email: blocked: 허용되지 않은 이메일 제공자입니다 unreachable: 존재하지 않는 것 같습니다 diff --git a/config/locales/activerecord.lt.yml b/config/locales/activerecord.lt.yml index 778cd98271..1eec2782f4 100644 --- a/config/locales/activerecord.lt.yml +++ b/config/locales/activerecord.lt.yml @@ -55,6 +55,8 @@ lt: too_soon: yra per anksti, turi būti vėliau nei %{date}. user: attributes: + date_of_birth: + below_limit: yra žemiau amžiaus ribos. email: blocked: naudoja neleidžiamą el. laiško paslaugų teikėją. unreachable: neatrodo, kad egzistuoja. diff --git a/config/locales/activerecord.lv.yml b/config/locales/activerecord.lv.yml index 6d912bd628..c7030221a7 100644 --- a/config/locales/activerecord.lv.yml +++ b/config/locales/activerecord.lv.yml @@ -49,8 +49,14 @@ lv: attributes: reblog: taken: ziņai jau pastāv + terms_of_service: + attributes: + effective_date: + too_soon: ir pārāk agri, jābūt vēlāk kā %{date} user: attributes: + date_of_birth: + below_limit: ir mazāks par minimālo vecuma ierobežojumu email: blocked: lieto neatļautu e-pasta pakalpojuma sniedzēju unreachable: šķietami neeksistē diff --git a/config/locales/activerecord.nl.yml b/config/locales/activerecord.nl.yml index 5e5424902e..b05d6680e7 100644 --- a/config/locales/activerecord.nl.yml +++ b/config/locales/activerecord.nl.yml @@ -55,6 +55,8 @@ nl: too_soon: is te vroeg, moet na %{date} zijn user: attributes: + date_of_birth: + below_limit: is onder de leeftijdsgrens email: blocked: gebruikt een niet toegestane e-mailprovider unreachable: schijnt niet te bestaan diff --git a/config/locales/activerecord.pt-PT.yml b/config/locales/activerecord.pt-PT.yml index a3bf125e58..397ed492e5 100644 --- a/config/locales/activerecord.pt-PT.yml +++ b/config/locales/activerecord.pt-PT.yml @@ -55,6 +55,8 @@ pt-PT: too_soon: é muito cedo, deve ser após %{date} user: attributes: + date_of_birth: + below_limit: está abaixo da idade mínima email: blocked: usa um fornecedor de e-mail que não é permitido unreachable: não parece existir diff --git a/config/locales/activerecord.ru.yml b/config/locales/activerecord.ru.yml index 79d39a5cdc..08e91e459f 100644 --- a/config/locales/activerecord.ru.yml +++ b/config/locales/activerecord.ru.yml @@ -3,7 +3,7 @@ ru: activerecord: attributes: poll: - expires_at: Крайний срок + expires_at: Срок окончания голосования options: Варианты user: agreement: Соглашение с условиями сервиса @@ -49,8 +49,14 @@ ru: attributes: reblog: taken: пост уже существует + terms_of_service: + attributes: + effective_date: + too_soon: должна быть не ранее %{date} user: attributes: + date_of_birth: + below_limit: ниже возрастного ограничения email: blocked: использует запрещённого провайдера эл. почты unreachable: не существует diff --git a/config/locales/activerecord.sl.yml b/config/locales/activerecord.sl.yml index 8b05d5d2cd..e4c4fe598f 100644 --- a/config/locales/activerecord.sl.yml +++ b/config/locales/activerecord.sl.yml @@ -55,6 +55,8 @@ sl: too_soon: je prekmalu, naj bo kasneje od %{date} user: attributes: + date_of_birth: + below_limit: ne dosega starostne meje email: blocked: uporablja nedovoljenega ponudnika e-poštnih storitev unreachable: kot kaže ne obstaja diff --git a/config/locales/activerecord.sq.yml b/config/locales/activerecord.sq.yml index 7fae00035b..2683dd014b 100644 --- a/config/locales/activerecord.sq.yml +++ b/config/locales/activerecord.sq.yml @@ -55,6 +55,8 @@ sq: too_soon: është shumë herët, duhet të jetë më vonë se %{date} user: attributes: + date_of_birth: + below_limit: është nën kufirin e moshave email: blocked: përdor një shërbim email të palejuar unreachable: s’duket se ekziston diff --git a/config/locales/activerecord.sv.yml b/config/locales/activerecord.sv.yml index db488200df..74b939fda4 100644 --- a/config/locales/activerecord.sv.yml +++ b/config/locales/activerecord.sv.yml @@ -49,8 +49,14 @@ sv: attributes: reblog: taken: av status finns redan + terms_of_service: + attributes: + effective_date: + too_soon: är för tidigt, måste vara senare än %{date} user: attributes: + date_of_birth: + below_limit: är under åldersgränsen email: blocked: använder en icke tillåten e-postleverantör unreachable: verkar inte existera diff --git a/config/locales/activerecord.tr.yml b/config/locales/activerecord.tr.yml index 65d353eda2..db9317afa2 100644 --- a/config/locales/activerecord.tr.yml +++ b/config/locales/activerecord.tr.yml @@ -55,6 +55,8 @@ tr: too_soon: çok erken, %{date} tarihinden sonra olmalıdır user: attributes: + date_of_birth: + below_limit: yaş sınırının altında email: blocked: izin verilmeyen bir e-posta sağlayıcı kullanıyor unreachable: mevcut gözükmüyor diff --git a/config/locales/activerecord.uk.yml b/config/locales/activerecord.uk.yml index 1ee426ae45..8b4ba1f671 100644 --- a/config/locales/activerecord.uk.yml +++ b/config/locales/activerecord.uk.yml @@ -51,6 +51,8 @@ uk: taken: цього допису вже існує user: attributes: + date_of_birth: + below_limit: менше вікової межі email: blocked: використовує не дозволенного постачальника електронної пошти unreachable: не існує diff --git a/config/locales/activerecord.vi.yml b/config/locales/activerecord.vi.yml index 1483b32cc9..fe810d94e5 100644 --- a/config/locales/activerecord.vi.yml +++ b/config/locales/activerecord.vi.yml @@ -55,6 +55,8 @@ vi: too_soon: là quá sớm, cần phải sau %{date} user: attributes: + date_of_birth: + below_limit: dưới độ tuổi tối thiểu email: blocked: sử dụng dịch vụ email bị cấm unreachable: không tồn tại diff --git a/config/locales/activerecord.zh-CN.yml b/config/locales/activerecord.zh-CN.yml index 0513c82a7c..af19014cfd 100644 --- a/config/locales/activerecord.zh-CN.yml +++ b/config/locales/activerecord.zh-CN.yml @@ -55,6 +55,8 @@ zh-CN: too_soon: 日期太近,必须晚于 %{date} user: attributes: + date_of_birth: + below_limit: 低于年龄限制 email: blocked: 使用了被封禁的电子邮件提供商 unreachable: 似乎不存在 diff --git a/config/locales/activerecord.zh-TW.yml b/config/locales/activerecord.zh-TW.yml index 2e4841f6c9..f8f630ba3c 100644 --- a/config/locales/activerecord.zh-TW.yml +++ b/config/locales/activerecord.zh-TW.yml @@ -55,6 +55,8 @@ zh-TW: too_soon: 太快了,必須晚於 %{date} user: attributes: + date_of_birth: + below_limit: 低於年齡要求 email: blocked: 使用不被允許的電子郵件提供商 unreachable: 該電子郵件地址似乎無法使用 diff --git a/config/locales/br.yml b/config/locales/br.yml index fbe91fcbd7..f12269eba3 100644 --- a/config/locales/br.yml +++ b/config/locales/br.yml @@ -47,6 +47,7 @@ br: demote: Argilañ disable: Skornañ disabled: Skornet + display_name: Anv diskouezet domain: Domani edit: Kemmañ email: Postel @@ -66,6 +67,7 @@ br: moderation: active: Oberiant all: Pep tra + disabled: Diweredekaet pending: War ober silenced: Bevennet suspended: Astalet @@ -98,6 +100,7 @@ br: action_logs: action_types: destroy_status: Dilemel ar c'hannad + reset_password_user: Adderaouekaat ar ger-tremen update_status: Hizivaat ar c'hannad actions: destroy_status_html: Dilamet eo bet toud %{target} gant %{name} diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 5328ec94ca..55d87f995d 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -99,7 +99,7 @@ cs: active: Aktivní all: Vše disabled: Deaktivován - pending: Čekající + pending: Nevyřízeno silenced: Omezeno suspended: Pozastavené title: Moderování diff --git a/config/locales/cy.yml b/config/locales/cy.yml index 3b31a68ed0..9349176842 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -1417,7 +1417,7 @@ cy: filters: contexts: account: Proffilau - home: Cartref a rhestrau + home: Ffrwd gartref notifications: Hysbysiadau public: Ffrydiau cyhoeddus thread: Sgyrsiau diff --git a/config/locales/devise.lv.yml b/config/locales/devise.lv.yml index b5b8f22296..e60f8fa62e 100644 --- a/config/locales/devise.lv.yml +++ b/config/locales/devise.lv.yml @@ -2,7 +2,7 @@ lv: devise: confirmations: - confirmed: Tava e-pasta adrese ir veiksmīgi apstiprināta. + confirmed: Tava e-pasta adrese tika sekmīgi apstiprināta. send_instructions: Pēc dažām minūtēm saņemsi e-pasta ziņojum ar norādēm, kā apstiprināt savu e-pasta adresi. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu. send_paranoid_instructions: Ja Tava e-pasta adrese ir mūsu datubāzē, pēc dažām minūtēm saņemsi e-pasta ziņojumu ar norādēm, kā apstiprināt savu e-pasta adresi. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu. failure: @@ -85,25 +85,25 @@ lv: title: Drošības atslēgas iespējotas omniauth_callbacks: failure: Nevarēja autentificēt tevi no %{kind}, jo "%{reason}". - success: Veiksmīgi autentificēts no %{kind} konta. + success: Sekmīgi autentificēts no %{kind} konta. passwords: no_token: Tu nevari piekļūt šai lapai, ja neesi saņēmis paroles atiestatīšanas e-pasta ziņojumu. Ja ienāci no paroles atiestatīšanas e-pasta, lūdzu, pārliecinies, vai izmanto visu norādīto URL. send_instructions: Ja Tava e-pasta adrese ir mūsu datubāzē, pēc dažām minūtēm savā e-pasta adresē saņemsi paroles atkopes saiti. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu. send_paranoid_instructions: Ja Tava e-pasta adrese ir mūsu datubāzē, pēc dažām minūtēm savā e-pasta adresē saņemsi paroles atkopes saiti. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu. - updated: Tava parole tika veiksmīgi nomainīta. Tagad esi pieteicies. - updated_not_active: Tava parole ir veiksmīgi nomainīta. + updated: Tava parole tika sekmīgi nomainīta. Tagad esi pieteicies. + updated_not_active: Tava parole tika sekmīgi nomainīta. registrations: - destroyed: Visu labu! Tavs konts ir veiksmīgi atcelts. Mēs ceram tevi drīz atkal redzēt. - update_needs_confirmation: Tu veiksmīgi atjaunināji savu kontu, taču mums ir jāapliecina Tava jaunā e-pasta adrese. Lūgums pārbaudīt savu e-pastu un sekot apstiprinājuma saitei, lai apstiprinātu savu jauno e-pasta adresi. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu. - updated: Tavs konts ir veiksmīgi atjaunināts. + destroyed: Visu labu! Tavs konts ir sekmīgi atcelts. Mēs ceram Tevi drīz atkal redzēt. + update_needs_confirmation: Tu sekmīgi atjaunināji savu kontu, taču mums ir jāapliecina Tava jaunā e-pasta adrese. Lūgums pārbaudīt savu e-pastu un sekot apstiprinājuma saitei, lai apstiprinātu savu jauno e-pasta adresi. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu. + updated: Tavs konts tika sekmīgi atjaunināts. sessions: - already_signed_out: Veiksmīgi izrakstījies. - signed_in: Veiksmīgi pieteicies. - signed_out: Veiksmīgi izrakstījies. + already_signed_out: Sekmīgi izrakstījies. + signed_in: Sekmīgi pierakstījies. + signed_out: Sekmīgi izrakstījies. unlocks: send_instructions: Pēc dažām minūtēm Tu saņemsi e-pasta ziņojumu ar norādēm, kā atslēgt savu kontu. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu. send_paranoid_instructions: Ja Tavs konts pastāv, dažu minūšu laikā saņemsi e-pasta ziņojumu ar norādēm, kā to atslēgt. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu. - unlocked: Konts tika veiksmīgi atbloķēts. Lūgums pieteikties, lai turpinātu. + unlocked: Konts tika sekmīgi atslēgts. Lūgums pieteikties, lai turpinātu. errors: messages: already_confirmed: jau tika apstiprināts, lūgums mēģināt pieteikties diff --git a/config/locales/doorkeeper.lv.yml b/config/locales/doorkeeper.lv.yml index af892d79fa..15c1e7692a 100644 --- a/config/locales/doorkeeper.lv.yml +++ b/config/locales/doorkeeper.lv.yml @@ -150,13 +150,13 @@ lv: title: OAuth nepieciešama autorizācija scopes: admin:read: lasīt visus datus uz servera - admin:read:accounts: lasīt sensitīvu informāciju no visiem kontiem - admin:read:canonical_email_blocks: lasīt sensitīvu informāciju par visiem kanoniskajiem e-pasta blokiem - admin:read:domain_allows: lasīt visu domēnu sensitīvo informāciju, ko atļauj - admin:read:domain_blocks: lasīt sensitīvu informāciju par visiem domēna blokiem - admin:read:email_domain_blocks: lasīt sensitīvu informāciju par visiem e-pasta domēna blokiem - admin:read:ip_blocks: lasīt sensitīvu informāciju par visiem IP blokiem - admin:read:reports: lasīt sensitīvu informāciju no visiem pārskatiem un kontiem, par kuriem ziņots + admin:read:accounts: lasīt jūtīgu informāciju no visiem kontiem + admin:read:canonical_email_blocks: lasīt jūtīgu informāciju par visiem kanoniskajiem e-pasta blokiem + admin:read:domain_allows: lasīt jūtīgu informāciju par visiem atļautajiem domēniem + admin:read:domain_blocks: lasīt jūtīgu informāciju par visiem domēna blokiem + admin:read:email_domain_blocks: lasīt jūtīgu informāciju par visiem e-pasta domēna blokiem + admin:read:ip_blocks: lasīt jūtīgu informāciju par visiem IP blokiem + admin:read:reports: lasīt jūtīgu informāciju no visiem pārskatiem un kontiem, par kuriem ziņots admin:write: modificēt visus datus uz servera admin:write:accounts: veikt satura pārraudzības darbības kontos admin:write:canonical_email_blocks: veikt satura pārraudzības darbības kanoniskajos e-pasta blokos diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 2ac853c2bc..614b0f1412 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -319,7 +319,7 @@ fi: create: Luo tiedote title: Uusi tiedote preview: - explanation_html: 'Sähköposti lähetetään <strong>%{display_count} käyttäjälle</strong>. Seuraava teksti sisällytetään sähköpostiviestiin:' + explanation_html: "<strong>%{display_count} käyttäjälle</strong> lähetetään sähköpostia. Sähköpostiviestiin sisällytetään seuraava teksti:" title: Esikatsele tiedoteilmoitus publish: Julkaise published_msg: Tiedotteen julkaisu onnistui! @@ -1945,8 +1945,8 @@ fi: terms_of_service_changed: agreement: Jatkamalla palvelun %{domain} käyttöä hyväksyt nämä ehdot. Jos et hyväksy päivitettyjä ehtoja, voit milloin tahansa päättää sopimuksesi palvelun %{domain} kanssa poistamalla tilisi. changelog: 'Lyhyesti, mitä tämä päivitys tarkoittaa sinulle:' - description: 'Sait tämän sähköpostiviestin, koska teemme muutoksia palvelun %{domain} käyttöehtoihin. Muutokset tulevat voimaan %{date}. Kehotamme sinua tutustumaan päivitettyihin ehtoihin kokonaisuudessaan täällä:' - description_html: Sait tämän sähköpostiviestin, koska teemme muutoksia palvelun %{domain} käyttöehtoihin. Muutokset tulevat voimaan <strong>%{date}</strong>. Kehotamme sinua tutustumaan <a href="%{path}" target="_blank">päivitettyihin ehtoihin kokonaisuudessaan täällä</a>. + description: 'Sait tämän sähköpostiviestin, koska muutamme palvelun %{domain} käyttöehtoja. Muutokset tulevat voimaan %{date}. Kehotamme tutustumaan päivitettyihin ehtoihin kokonaisuudessaan täällä:' + description_html: Sait tämän sähköpostiviestin, koska muutamme palvelun %{domain} käyttöehtoja. Muutokset tulevat voimaan <strong>%{date}</strong>. Kehotamme tutustumaan <a href="%{path}" target="_blank">päivitettyihin ehtoihin kokonaisuudessaan täällä</a>. sign_off: Palvelimen %{domain} tiimi subject: Käyttöehtojemme päivitykset subtitle: Palvelimen %{domain} käyttöehdot muuttuvat diff --git a/config/locales/fr-CA.yml b/config/locales/fr-CA.yml index 228b837324..dd751c0678 100644 --- a/config/locales/fr-CA.yml +++ b/config/locales/fr-CA.yml @@ -309,6 +309,7 @@ fr-CA: title: Journal d’audit unavailable_instance: "(nom de domaine indisponible)" announcements: + back: Retour aux annonces destroyed_msg: Annonce supprimée avec succès ! edit: title: Modifier l’annonce @@ -317,6 +318,9 @@ fr-CA: new: create: Créer une annonce title: Nouvelle annonce + preview: + explanation_html: 'L''e-mail sera envoyé à <strong>%{display_count} utilisateurs</strong>. Le texte suivant sera inclus :' + title: Aperçu de la notification d'annonce publish: Publier published_msg: Annonce publiée avec succès ! scheduled_for: Planifiée pour %{time} @@ -942,6 +946,7 @@ fr-CA: chance_to_review_html: "<strong>Les conditions d'utilisation générées ne seront pas publiées automatiquement.</strong> Vous aurez la possibilité de vérifier les résultats. Veuillez remplir les informations nécessaires pour continuer." explanation_html: Le modèle de conditions d'utilisation fourni l'est uniquement à titre informatif et ne doit pas être interprété comme un conseil juridique sur quelque sujet que ce soit. Veuillez consulter votre propre conseiller juridique sur votre situation et les questions juridiques spécifiques que vous vous posez. title: Configuration des Conditions d'Utilisation + going_live_on_html: En direct, à compter du %{date} history: Historique live: En cours d'utilisation no_history: Il n'y a pas encore de modifications enregistrées des conditions d'utilisation. @@ -1907,6 +1912,10 @@ fr-CA: recovery_instructions_html: Si vous perdez l’accès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour retrouver l’accès à votre compte. <strong>Conservez les codes de récupération en sécurité</strong>. Par exemple, en les imprimant et en les stockant avec vos autres documents importants. webauthn: Clés de sécurité user_mailer: + announcement_published: + description: 'Les administrateurs de %{domain} font une annonce :' + subject: Annonce de service + title: Annonce de service de %{domain} appeal_approved: action: Paramètres du compte explanation: L'appel de la sanction contre votre compte mise en place le %{strike_date} que vous avez soumis le %{appeal_date} a été approuvé. Votre compte est de nouveau en règle. @@ -1939,6 +1948,8 @@ fr-CA: terms_of_service_changed: agreement: En continuant d'utiliser %{domain}, vous acceptez ces conditions. Si vous n'êtes pas d'accord avec les conditions mises à jour, vous pouvez résilier votre accord avec %{domain} à tout moment en supprimant votre compte. changelog: 'En un coup d''œil, voici ce que cette mise à jour signifie pour vous :' + description: 'Vous recevez cet e-mail parce que nous apportons des modifications à nos conditions de service à %{domain}. Ces modifications entreront en vigueur le %{date}. Nous vous encourageons à consulter l''intégralité des conditions mises à jour ici :' + description_html: Vous recevez cet e-mail parce que nous apportons des modifications à nos conditions de service à %{domain}. Ces mises à jour entreront en vigueur le <strong>%{date}</strong>. Nous vous encourageons à consulter l'intégralité des <a href="%{path}" target="_blank">conditions mises à jour ici</a>. sign_off: L'équipe %{domain} subject: Mises à jour de nos conditions d'utilisation subtitle: Les conditions d'utilisation de `%{domain}` changent diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 93ba3cbab8..6ad2733f3e 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -309,6 +309,7 @@ fr: title: Journal d’audit unavailable_instance: "(nom de domaine indisponible)" announcements: + back: Retour aux annonces destroyed_msg: Annonce supprimée avec succès ! edit: title: Modifier l’annonce @@ -317,6 +318,9 @@ fr: new: create: Créer une annonce title: Nouvelle annonce + preview: + explanation_html: 'L''e-mail sera envoyé à <strong>%{display_count} utilisateurs</strong>. Le texte suivant sera inclus :' + title: Aperçu de la notification d'annonce publish: Publier published_msg: Annonce publiée avec succès ! scheduled_for: Planifiée pour %{time} @@ -942,6 +946,7 @@ fr: chance_to_review_html: "<strong>Les conditions d'utilisation générées ne seront pas publiées automatiquement.</strong> Vous aurez la possibilité de vérifier les résultats. Veuillez remplir les informations nécessaires pour continuer." explanation_html: Le modèle de conditions d'utilisation fourni l'est uniquement à titre informatif et ne doit pas être interprété comme un conseil juridique sur quelque sujet que ce soit. Veuillez consulter votre propre conseiller juridique sur votre situation et les questions juridiques spécifiques que vous vous posez. title: Configuration des Conditions d'Utilisation + going_live_on_html: En direct, à compter du %{date} history: Historique live: En cours d'utilisation no_history: Il n'y a pas encore de modifications enregistrées des conditions d'utilisation. @@ -1907,6 +1912,10 @@ fr: recovery_instructions_html: Si vous perdez l’accès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour retrouver l’accès à votre compte. <strong>Conservez les codes de récupération en sécurité</strong>. Par exemple, en les imprimant et en les stockant avec vos autres documents importants. webauthn: Clés de sécurité user_mailer: + announcement_published: + description: 'Les administrateurs de %{domain} font une annonce :' + subject: Annonce de service + title: Annonce de service de %{domain} appeal_approved: action: Paramètres du compte explanation: L'appel de la sanction contre votre compte mise en place le %{strike_date} que vous avez soumis le %{appeal_date} a été approuvé. Votre compte est de nouveau en règle. @@ -1939,6 +1948,8 @@ fr: terms_of_service_changed: agreement: En continuant d'utiliser %{domain}, vous acceptez ces conditions. Si vous n'êtes pas d'accord avec les conditions mises à jour, vous pouvez résilier votre accord avec %{domain} à tout moment en supprimant votre compte. changelog: 'En un coup d''œil, voici ce que cette mise à jour signifie pour vous :' + description: 'Vous recevez cet e-mail parce que nous apportons des modifications à nos conditions de service à %{domain}. Ces modifications entreront en vigueur le %{date}. Nous vous encourageons à consulter l''intégralité des conditions mises à jour ici :' + description_html: Vous recevez cet e-mail parce que nous apportons des modifications à nos conditions de service à %{domain}. Ces mises à jour entreront en vigueur le <strong>%{date}</strong>. Nous vous encourageons à consulter l'intégralité des <a href="%{path}" target="_blank">conditions mises à jour ici</a>. sign_off: L'équipe %{domain} subject: Mises à jour de nos conditions d'utilisation subtitle: Les conditions d'utilisation de `%{domain}` changent diff --git a/config/locales/ko.yml b/config/locales/ko.yml index f96f9a91c0..ca40a2abcd 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -931,6 +931,7 @@ ko: chance_to_review_html: "<strong>생성된 이용 약관은 자동으로 게시되지 않을 것입니다.</strong> 결과를 확인할 기회가 있습니다. 진행하려면 필요한 정보들을 입력하세요." explanation_html: 제공되는 이용약관 틀은 정보 제공만을 목적으로 하며 법률 조언으로 해석하면 안 됩니다. 귀하의 상황에 맞는 법률 자문을 받아주시기 바랍니다. title: 이용 약관 설정 + going_live_on_html: "%{date} 시행중" history: 역사 live: 활성 no_history: 기록된 이용약관 변경이 아직 없습니다. @@ -1903,6 +1904,8 @@ ko: terms_of_service_changed: agreement: "%{domain}을 계속 사용하는 것으로 약관에 동의하는 것으로 간주합니다. 약관에 동의하지 않는 경우 계정을 삭제함으로써 언제든 동의를 철회할 수 있습니다." changelog: '이번 변경사항의 주요 내용입니다:' + description: "%{domain}의 이용 약관이 변경되었기 때문에 발송된 이메일입니다. 이 변경사항은 %{date}부터 효력을 발휘합니다. 변경된 전체 약관을 확인하시길 권합니다:" + description_html: '%{domain}의 이용 약관이 변경되었기 때문에 발송된 이메일입니다. 이 변경사항은 <strong>%{date}</strong>부터 효력을 발휘합니다. <a href="%{path}" target="_blank">변경된 전체 약관</a>을 확인하시길 권합니다.' sign_off: "%{domain} 팀" subject: 변경된 이용 약관 subtitle: "%{domain}의 이용 약관이 변경됩니다" diff --git a/config/locales/lt.yml b/config/locales/lt.yml index 8a85daff7d..c5862bcf5d 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -294,6 +294,7 @@ lt: title: Audito žurnalas unavailable_instance: "(domeno pavadinimas neprieinamas)" announcements: + back: Atgal į skelbimus destroyed_msg: Skelbimas sėkmingai ištrintas. edit: title: Redaguoti skelbimą @@ -302,6 +303,9 @@ lt: new: create: Sukurti skelbimą title: Naujas skelbimas + preview: + explanation_html: 'El. laiškas bus išsiųstas <strong>%{display_count} naudotojams</strong>. Į el. laišką bus įtrauktas toliau nurodytas tekstas:' + title: Peržiūrėti skelbimo pranešimą publish: Skelbti published_msg: Skelbimas sėkmingai paskelbtas. scheduled_for: Suplanuota %{time} @@ -1219,6 +1223,10 @@ lt: recovery_instructions_html: Jeigu prarandate prieiga prie telefono, jūs galite naudoti atkūrimo kodus esančius žemiau, kad atgautumėte priega prie savo paskyros.<strong>Laikykite atkūrimo kodus saugiai</strong> Pavyzdžiui, galite norėti juos išspausdinti, ir laikyti kartu su kitais svarbiais dokumentais. webauthn: Saugumo raktai user_mailer: + announcement_published: + description: "%{domain} administratoriai daro skelbimą:" + subject: Paslaugos skelbimas + title: "%{domain} paslaugos skelbimas" appeal_approved: action: Paskyros nustatymai subtitle: Tavo paskyros būklė vėl yra gera. diff --git a/config/locales/lv.yml b/config/locales/lv.yml index 66c5821bd1..3dbfb0e2b2 100644 --- a/config/locales/lv.yml +++ b/config/locales/lv.yml @@ -144,8 +144,8 @@ lv: security_measures: only_password: Tikai parole password_and_2fa: Parole un 2FA - sensitive: Sensitīvs - sensitized: Atzīmēts kā sensitīvs + sensitive: Uzspiest atzīmēšanu kā jūtīgu + sensitized: Atzīmēts kā jūtīgs shared_inbox_url: Koplietotās iesūtnes URL show: created_reports: Sastādītie ziņojumi @@ -163,7 +163,7 @@ lv: unblock_email: Atbloķēt e-pasta adresi unblocked_email_msg: Veiksmīgi atbloķēta %{username} e-pasta adrese unconfirmed_email: Neapstiprināts e-pasts - undo_sensitized: Atcelt sensitivizēšanu + undo_sensitized: Atcelt uzspiestu atzīmēšanu kā jūtīgu undo_silenced: Atsaukt ierobežojumu undo_suspension: Atsaukt apturēšanu unsilenced_msg: Veiksmīgi atsaukts %{username} konta ierobežojums @@ -225,12 +225,12 @@ lv: resend_user: Atkārtoti nosūtīt Apstiprinājuma Pastu reset_password_user: Atiestatīt Paroli resolve_report: Atrisināt Ziņojumu - sensitive_account: Piespiedu sensitīvizēt kontu + sensitive_account: Uzspiesti atzimēt kontu kā jūtīgu silence_account: Ierobežot Kontu suspend_account: Apturēt Kontu unassigned_report: Atcelt Pārskata Piešķiršanu unblock_email_account: Atbloķēt e-pasta adresi - unsensitive_account: Atsaukt Konta Piespiedu Sensitivizēšanu + unsensitive_account: Atsaukt uzspiestu konta atzīmēšanu kā jūtīgu unsilence_account: Atcelt Konta Ierobežošanu unsuspend_account: Atcelt konta apturēšanu update_announcement: Atjaunināt Paziņojumu @@ -282,12 +282,12 @@ lv: resend_user_html: "%{name} atkārtoti nosūtīja %{target} apstiprinājuma e-pasta ziņojumu" reset_password_user_html: "%{name} atiestatīja paroli lietotājam %{target}" resolve_report_html: "%{name} atrisināja ziņojumu %{target}" - sensitive_account_html: "%{name} atzīmēja %{target} multividi kā sensitīvu" + sensitive_account_html: "%{name} atzīmēja %{target} informācijas nesēju kā jūtīgu" silence_account_html: "%{name} ierobežoja %{target} kontu" suspend_account_html: "%{name} apturēja %{target} kontu" unassigned_report_html: "%{name} nepiešķīra ziņojumu %{target}" unblock_email_account_html: "%{name} atbloķēja %{target} e-pasta adresi" - unsensitive_account_html: "%{name} atmarķēja %{target} multividi kā sensitīvu" + unsensitive_account_html: "%{name} atcēla %{target} informācijas nesēja atzīmēšanu kā jūtīgu" unsilence_account_html: "%{name} atcēla ierobežojumu %{target} kontam" unsuspend_account_html: "%{name} neapturēja %{target} kontu" update_announcement_html: "%{name} atjaunināja paziņojumu %{target}" @@ -610,7 +610,7 @@ lv: action_taken_by: Veiktā darbība actions: delete_description_html: Raksti, par kurām ziņots, tiks dzēsti, un tiks reģistrēts brīdinājums, lai palīdzētu tev izvērst turpmākos pārkāpumus saistībā ar to pašu kontu. - mark_as_sensitive_description_html: Multividesu faili ziņojumos, par kuriem ziņots, tiks atzīmēti kā sensitīvi, un tiks reģistrēts brīdinājums, lai palīdzētu tev izvērst turpmākus pārkāpumus saistībā ar to pašu kontu. + mark_as_sensitive_description_html: Informācijas nesēji ierakstos, par kuriem ziņots, tiks atzīmēti kā jūtīgi, un tiks iegrāmatots brīdinājums, lai palīdzētu ziņot par turpmākiem tā paša konta pārkāpumiem. other_description_html: Skatīt vairāk iespēju kontrolēt konta uzvedību un pielāgot saziņu ar paziņoto kontu. resolve_description_html: Pret norādīto kontu netiks veiktas nekādas darbības, netiks reģistrēts brīdinājums, un ziņojums tiks slēgts. silence_description_html: Konts būs redzams tikai tiem, kas tam jau seko vai meklē to manuāli, ievērojami ierobežojot tā sasniedzamību. To vienmēr var atgriezt. Tiek aizvērti visi šī konta pārskati. @@ -621,7 +621,7 @@ lv: add_to_report: Pievienot varāk paziņošanai are_you_sure: Vai esi pārliecināts? assign_to_self: Piešķirt man - assigned: Piešķirtais moderators + assigned: Piešķirtais satura pārraudzītājs by_target_domain: Ziņotā konta domēns cancel: Atcelt category: Kategorija @@ -637,7 +637,7 @@ lv: forwarded_replies_explanation: Šis ziņojums ir no attāla lietotāja un par attālu saturu. Tas tika pārvirzīts šeit, jo saturs, par kuru tika ziņots, ir atbilde vienam no šī servera lietotājiem. forwarded_to: Pārsūtīti %{domain} mark_as_resolved: Atzīmēt kā atrisinātu - mark_as_sensitive: Atzīmēt kā sensitīvu + mark_as_sensitive: Atzīmēt kā jūtīgu mark_as_unresolved: Atzīmēt kā neatrisinātu no_one_assigned: Neviena notes: @@ -665,12 +665,12 @@ lv: summary: action_preambles: delete_html: 'Jūs gatavojaties <strong>noņemt</strong> dažas no lietotāja <strong>@%{acct}</strong> ziņām. Tas:' - mark_as_sensitive_html: 'Jūs gatavojaties <strong>atzīmēt</strong> dažas no lietotāja <strong>@%{acct}</strong> ziņām kā <strong>sensitīvas</strong>. Tas:' + mark_as_sensitive_html: 'Tu gatavojies <strong>atzīmēt</strong> dažus no lietotāja <strong>@%{acct}</strong> ierakstiem kā <strong>jūtīgus</strong>. Tas:' silence_html: 'Jūs gatavojaties <strong>ierobežot</strong> <strong>@%{acct}</strong> kontu. Tas:' suspend_html: 'Jūs gatavojaties <strong>apturēt</strong> <strong>@%{acct}</strong> kontu. Tas:' actions: delete_html: Noņemt aizskarošās ziņas - mark_as_sensitive_html: Atzīmēt aizskarošo ziņu multivides saturu kā sensitīvu + mark_as_sensitive_html: Atzīmēt aizskarošo ierakstu informācijas nesējus kā jūtīgus silence_html: Ievērojami ierobežo <strong>@%{acct}</strong> sasniedzamību, padarot viņa profilu un saturu redzamu tikai cilvēkiem, kas jau seko tam vai pašrocīgi uzmeklē profilu suspend_html: Apturēt <strong>@%{acct}</strong>, padarot viņu profilu un saturu nepieejamu un neiespējamu mijiedarbību ar close_report: 'Atzīmēt ziņojumu #%{id} kā atrisinātu' @@ -854,9 +854,9 @@ lv: actions: delete_statuses: "%{name} izdzēsa %{target} publikācijas" disable: "%{name} iesaldēja %{target} kontu" - mark_statuses_as_sensitive: "%{name} atzīmēja %{target} ziņu kā sensitīvu" + mark_statuses_as_sensitive: "%{name} atzīmēja %{target} ierakstu kā jūtīgu" none: "%{name} nosūtīja brīdinājumu %{target}" - sensitive: "%{name} atzīmēja %{target} kontu kā sensitīvu" + sensitive: "%{name} atzīmēja %{target} kontu kā jūtīgu" silence: "%{name} ierobežoja %{target} kontu" suspend: "%{name} apturēja %{target} kontu" appeal_approved: Pārsūdzēts @@ -1052,9 +1052,9 @@ lv: actions: delete_statuses: lai izdzēstu viņu ierakstus disable: lai iesaldētu viņu kontu - mark_statuses_as_sensitive: lai atzīmētu viņu ziņas kā sensitīvas + mark_statuses_as_sensitive: lai atzīmētu viņu ierakstus kā jūtīgus none: brīdinājums - sensitive: lai atzīmētu viņu kontu kā sensitīvu + sensitive: lai atzīmētu viņu kontu kā jūtīgu silence: lai ierobežotu viņu kontu suspend: lai apturētu viņu kontu body: "%{target} iebilst %{action_taken_by} satura pārraudzības lēmumam no %{date}, kas bija %{type}. Viņi rakstīja:" @@ -1099,7 +1099,7 @@ lv: body: Mastodon ir tulkojuši brīvprātīgie. guide_link: https://crowdin.com/project/mastodon guide_link_text: Ikviens var piedalīties. - sensitive_content: Sensitīvs saturs + sensitive_content: Jūtīgs saturs application_mailer: notification_preferences: Mainīt e-pasta uztādījumus salutation: "%{name}," @@ -1263,9 +1263,9 @@ lv: title_actions: delete_statuses: Ziņas noņemšana disable: Konta iesaldēšana - mark_statuses_as_sensitive: Ziņu atzīmēšana kā sensitīvas + mark_statuses_as_sensitive: Ierakstu atzīmēšana kā jūtīgus none: Brīdinājums - sensitive: Konta atzīmēšana kā sensitīvs + sensitive: Konta atzīmēšana kā jūtīgu silence: Konta ierobežošana suspend: Konta apturēšana your_appeal_approved: Jūsu apelācija ir apstiprināta @@ -1807,7 +1807,7 @@ lv: min_reblogs: Saglabāt ziņas izceltas vismaz min_reblogs_hint: Neizdzēš nevienu no tavām ziņām, kas ir izceltas vismaz tik reižu. Atstāj tukšu, lai dzēstu ziņas neatkarīgi no to izcēlumu skaita stream_entries: - sensitive_content: Sensitīvs saturs + sensitive_content: Jūtīgs saturs strikes: errors: too_late: Brīdinājuma apstrīdēšanas laiks ir nokavēts @@ -1880,8 +1880,8 @@ lv: explanation: delete_statuses: Tika konstatēts, ka dažas no tavām ziņām pārkāpj vienu vai vairākas kopienas vadlīnijas, un rezultātā %{instance} moderatori tās noņēma. disable: Tu vairs nevari izmantot savu kontu, taču tavs profils un citi dati paliek neskarti. Tu vari pieprasīt savu datu dublējumu, mainīt konta iestatījumus vai dzēst kontu. - mark_statuses_as_sensitive: "%{instance} moderatori dažus no Taviem ierakstiem ir atzīmējuši kā jutīgus. Tas nozīmē, ka cilvēkiem būs jāpiesit ierakstos esošajiem informāijas nesējiem, pirms tiek attēlots priekšskatījums. Tu arī pats vari atzīmēt informācijas nesēju kā jutīgu, kad nākotnē tādu ievietosi." - sensitive: No šī brīža visi augšupielādētie multivides faili tiks atzīmēti kā sensitīvi un paslēpti aiz klikšķa brīdinājuma. + mark_statuses_as_sensitive: "%{instance} satura pārraudzītāji dažus no Taviem ierakstiem ir atzīmējuši kā jūtīgus. Tas nozīmē, ka cilvēkiem būs jāpiesit ierakstos esošajiem informāijas nesējiem, pirms tiek attēlots priekšskatījums. Tu pats vari atzīmēt informācijas nesēju kā jūtīgu, kad nākotnē tādu ievietosi." + sensitive: Turpmāk visi augšupielādētās informācijas nesēju datnes tiks atzīmētas kā jūtīgas un paslēptas aiz klikšķināma brīdinājuma. silence: Tu joprojām vari izmantot savu kontu, taču tikai tie cilvēki, kuri jau tev seko, redzēs tavas ziņas šajā serverī, un tev var tikt liegtas dažādas atklāšanas funkcijas. Tomēr citi joprojām var tev manuāli sekot. suspend: Tu vairs nevari izmantot savu kontu, un tavs profils un citi dati vairs nav pieejami. Tu joprojām vari pieteikties, lai pieprasītu savu datu dublēšanu, līdz dati tiks pilnībā noņemti aptuveni 30 dienu laikā, taču mēs saglabāsim dažus pamata datus, lai neļautu tev izvairīties no apturēšanas. reason: 'Iemesls:' @@ -1889,17 +1889,17 @@ lv: subject: delete_statuses: Tavas ziņas %{acct} tika noņemtas disable: Tavs konts %{acct} tika iesaldēts - mark_statuses_as_sensitive: Tavas ziņas vietnē %{acct} ir atzīmētas kā sensitīvas + mark_statuses_as_sensitive: Tavi ieraksti %{acct} ir atzīmēti kā jūtīgi none: Brīdinājums par %{acct} - sensitive: Tavas ziņas vietnē %{acct} turpmāk tiks atzīmētas kā sensitīvas + sensitive: Tavi ieraksti %{acct} turpmāk tiks atzīmēti kā jūtīgi silence: Tavs konts %{acct} tika ierobežots suspend: Tava konta %{acct} darbība ir apturēta title: delete_statuses: Izdzēstās ziņas disable: Konts iesaldēts - mark_statuses_as_sensitive: Ziņas ir atzīmēts kā sensitīvas + mark_statuses_as_sensitive: Ieraksti atzīmēti kā jūtīgi none: Brīdinājums - sensitive: Konts ir atzīmēts kā sensitīvs + sensitive: Konts ir atzīmēts kā jūtīgs silence: Konts ierobežots suspend: Konts apturēts welcome: diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 37b8dd5a09..75c3757934 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -315,6 +315,7 @@ ru: title: Журнал аудита unavailable_instance: "(доменное имя недоступно)" announcements: + back: Вернуться к объявлениям destroyed_msg: Объявление удалено. edit: title: Редактировать объявление @@ -323,6 +324,9 @@ ru: new: create: Создать объявление title: Новое объявление + preview: + explanation_html: 'Сообщение будет отравлено <strong>%{display_count} пользователям</strong>. В теле письма будет указан следующий текст:' + title: Предпросмотр объявления по электронной почте publish: Опубликовать published_msg: Объявление опубликовано. scheduled_for: Запланировано на %{time} @@ -967,6 +971,7 @@ ru: chance_to_review_html: "<strong>Сгенерированное пользовательское соглашение не будет опубликовано автоматически.</strong> У вас будет возможность просмотреть результат. Введите все необходимые сведения, чтобы продолжить." explanation_html: Шаблон пользовательского соглашения приводится исключительно в ознакомительных целях, и не может рассматриваться как юридическая консультация по тому или иному вопросу. Обратитесь к своему юрисконсульту насчёт вашей ситуации и имеющихся правовых вопросов. title: Создание пользовательского соглашения + going_live_on_html: Вступило в силу с %{date} history: История live: Действует no_history: Нет зафиксированных изменений пользовательского соглашения. @@ -1990,6 +1995,10 @@ ru: recovery_instructions_html: 'Пожалуйста, сохраните коды ниже в надёжном месте: они понадобятся, чтобы войти в учётную запись, если вы потеряете доступ к своему смартфону. Вы можете вручную переписать их, распечатать и спрятать среди важных документов или, например, в любимой книжке. <strong>Каждый код действителен только один раз</strong>.' webauthn: Ключи безопасности user_mailer: + announcement_published: + description: 'Администраторы %{domain} опубликовали новое объявление:' + subject: Сервисное объявление + title: Сервисное объявление %{domain} appeal_approved: action: Настройки аккаунта explanation: Апелляция на разблокировку против вашей учетной записи %{strike_date}, которую вы подали на %{appeal_date}, была одобрена. Ваша учетная запись снова на хорошем счету. @@ -2022,6 +2031,8 @@ ru: terms_of_service_changed: agreement: Продолжая использовать %{domain}, вы соглашаетесь с этими условиями. Если вы не согласны с новыми условиями, вы в любой момент можете удалить вашу учётную запись на %{domain}. changelog: 'Вот что обновление условий будет значит для вас в общих чертах:' + description: 'Вы получили это сообщение, потому что мы внесли некоторые изменения в пользовательское соглашение %{domain}. Эти изменения вступят в силу %{date}. Рекомендуем вам ознакомиться с обновлёнными условиями по ссылке:' + description_html: Вы получили это сообщение, потому что мы внесли некоторые изменения в пользовательское соглашение %{domain}. Эти изменения вступят в силу <strong>%{date}</strong>. Рекомендуем вам ознакомиться с <a href="%{path}" target="_blank">обновлёнными условиями</a>. sign_off: Ваш %{domain} subject: Обновления наших условий использования subtitle: На %{domain} изменилось пользовательское соглашение diff --git a/config/locales/simple_form.bg.yml b/config/locales/simple_form.bg.yml index 7a4fea91b2..b3a5882562 100644 --- a/config/locales/simple_form.bg.yml +++ b/config/locales/simple_form.bg.yml @@ -75,6 +75,7 @@ bg: filters: action: Изберете кое действие да се извърши, прецеждайки съвпаденията на публикацията actions: + blur: Скриване на мултимедия зад предупреждение, но без скриване на самия текст hide: Напълно скриване на филтрираното съдържание, сякаш не съществува warn: Скриване на филтрираното съдържание зад предупреждение, споменавайки заглавието на филтъра form_admin_settings: @@ -88,6 +89,7 @@ bg: favicon: WEBP, PNG, GIF или JPG. Заменя стандартната сайтоикона на Mastodon с произволна икона. mascot: Замества илюстрацията в разширения уеб интерфейс. media_cache_retention_period: Мултимедийни файлове от публикации, направени от отдалечени потребители, се сринаха в сървъра ви. Задавайки положителна стойност, мултимедията ще се изтрие след посочения брой дни. Ако се искат мултимедийни данни след изтриването, то ще се изтегли пак, ако още е наличен източникът на съдържание. Поради ограниченията за това колко често картите за предварващ преглед на връзките анкетират сайтове на трети страни, се препоръчва да зададете тази стойност на поне 14 дни или картите за предварващ преглед на връзките няма да се обновяват при поискване преди този момент. + min_age: Потребителите ще се питат да потвърдят рождената си дата по време на регириране peers_api_enabled: Списък от имена на домейни, с които сървърът се е свързал във федивселената. Тук не се включват данни за това дали федерирате с даден сървър, а само за това дали сървърът ви знае за него. Това се ползва от услуги, събиращи статистика за федерацията в общия смисъл. profile_directory: Указателят на профили вписва всички потребители, избрали да бъдат откриваеми. require_invite_text: Когато регистрацията изисква ръчно одобрение, то направете текстовото поле за това "Защо желаете да се присъедините?" по-скоро задължително, отколкото по желание @@ -136,12 +138,15 @@ bg: text: Може да се структурира със синтаксиса на Markdown. terms_of_service_generator: admin_email: Правните бележки включват насрещни известия, постановления на съда, заявки за сваляне и заявки от правоохранителните органи. + arbitration_address: Може да е същото като физическия адрес горе или "неприложимо", ако се употребява имейл. choice_of_law: Град, регион, територия, щат или държава, чиито вътрешни материални права ще уреждат всички искове. + dmca_email: Може да е същият имейл, използван за "Имейл адрес за правни известия" по-горе. domain: Неповторимо идентифициране на онлайн услугата, която предоставяте. jurisdiction: Впишете държавата, където живее всеки, който плаща сметките. Ако е дружество или друго образувание, то впишете държавата, в която е регистрирано, и градът, регионът, територията или щатът според случая. min_age: Не трябва да е под изискваната минимална възраст от закона на юрисдикцията ви. user: chosen_languages: Само публикации на отметнатите езици ще се показват в публичните часови оси + date_of_birth: Трябва да се уверим, че сте поне на %{age}, за да употребявате Mastodon. Няма да съхраняваме това. role: Ролята управлява какви позволения има потребителят. user_role: color: Цветът, използван за ролите в потребителския интерфейс, като RGB в шестнадесетичен формат @@ -254,6 +259,7 @@ bg: name: Хаштаг filters: actions: + blur: Скриване на мултимедията с предупреждение hide: Напълно скриване warn: Скриване зад предупреждение form_admin_settings: @@ -267,6 +273,7 @@ bg: favicon: Сайтоикона mascot: Плашило талисман по избор (остаряло) media_cache_retention_period: Период на запазване на мултимедийния кеш + min_age: Минимално възрастово изискване peers_api_enabled: Публикуване на списъка с открити сървъри в API profile_directory: Показване на директорията от профили registrations_mode: Кой може да се регистрира @@ -345,6 +352,9 @@ bg: jurisdiction: Законова юрисдикция min_age: Минимална възраст user: + date_of_birth_1i: Ден + date_of_birth_2i: Месец + date_of_birth_3i: Година role: Роля time_zone: Часова зона user_role: diff --git a/config/locales/simple_form.ca.yml b/config/locales/simple_form.ca.yml index 2f4c96edab..2f85738c32 100644 --- a/config/locales/simple_form.ca.yml +++ b/config/locales/simple_form.ca.yml @@ -75,6 +75,7 @@ ca: filters: action: Tria quina acció cal executar quan un apunt coincideixi amb el filtre actions: + blur: Amaga el contingut multimèdia rere un avís, sense amagar el text en si hide: Ocultar completament el contingut filtrat, comportant-se com si no existís warn: Oculta el contingut filtrat darrere d'un avís mencionant el títol del filtre form_admin_settings: @@ -88,6 +89,7 @@ ca: favicon: WEBP, PNG, GIF o JPG. Canvia la icona per defecte de Mastodon a la pestanya del navegador per una de personalitzada. mascot: Anul·la la il·lustració en la interfície web avançada. media_cache_retention_period: El vostre servidor conserva una còpia dels fitxers multimèdia de les publicacions dels usuaris remots. Si s'indica un valor positiu, s'esborraran passats els dies indicats. Si el fitxer es torna a demanar un cop esborrat, es tornarà a baixar si el contingut origen segueix disponible. Per causa de les restriccions en la freqüència amb què es poden demanar les targetes de previsualització d'altres servidors, es recomana definir aquest valor com a mínim a 14 dies, o les targetes de previsualització no s'actualizaran a demanda abans d'aquest termini. + min_age: Es demanarà als usuaris la data de naixement durant la inscripció peers_api_enabled: Una llista de noms de domini que aquest servidor ha trobat al fedivers. No inclou cap dada sobre si estàs federat amb un servidor determinat, només si el teu en sap res. La fan servir, en un sentit general, serveis que recol·lecten estadístiques sobre la federació. profile_directory: El directori de perfils llista tots els usuaris que tenen activat ser descoberts. require_invite_text: Quan el registre requereixi aprovació manual, fes que sigui obligatori en lloc d'opcional d'escriure el text de la sol·licitud d'invitació "Per què vols unir-te?" @@ -143,6 +145,7 @@ ca: min_age: No hauria de ser inferior a l'edat mínima exigida per la llei de la vostra jurisdicció. user: chosen_languages: Quan estigui marcat, només es mostraran els tuts de les llengües seleccionades en les línies de temps públiques + date_of_birth: Ens hem d'assegurar que teniu %{age} anys com a mínim. No desarem aquesta dada. role: El rol controla quins permisos té l'usuari. user_role: color: Color que s'usarà per al rol a tota la interfície d'usuari, com a RGB en format hexadecimal @@ -255,6 +258,7 @@ ca: name: Etiqueta filters: actions: + blur: Amaga el contingut multimèdia amb un avís hide: Oculta completament warn: Oculta amb un avís form_admin_settings: @@ -268,6 +272,7 @@ ca: favicon: Icona de preferits mascot: Mascota personalitzada (llegat) media_cache_retention_period: Període de retenció del cau multimèdia + min_age: Edat mínima requerida peers_api_enabled: Publica a l'API la llista de servidors descoberts profile_directory: Habilita el directori de perfils registrations_mode: Qui es pot registrar @@ -346,6 +351,9 @@ ca: jurisdiction: Jurisdicció min_age: Edat mínima user: + date_of_birth_1i: Dia + date_of_birth_2i: Mes + date_of_birth_3i: Any role: Rol time_zone: Zona horària user_role: diff --git a/config/locales/simple_form.cs.yml b/config/locales/simple_form.cs.yml index f384de36b6..00abf91d3e 100644 --- a/config/locales/simple_form.cs.yml +++ b/config/locales/simple_form.cs.yml @@ -3,7 +3,7 @@ cs: simple_form: hints: account: - attribution_domains: Jeden na řádek. Chrání před falešným připisování autorství. + attribution_domains: Jeden na řádek. Chrání před falešným připisováním autorství. discoverable: Vaše veřejné příspěvky a profil mohou být zobrazeny nebo doporučeny v různých oblastech Mastodonu a váš profil může být navrhován ostatním uživatelům. display_name: Vaše celé jméno nebo přezdívka. fields: Vaše domovská stránka, zájmena, věk, cokoliv chcete. @@ -75,6 +75,7 @@ cs: filters: action: Vyberte, jakou akci provést, když příspěvek odpovídá filtru actions: + blur: Skrýt média za varováním, bez skrytí samotného textu hide: Úplně schovat filtrovaný obsah tak, jako by neexistoval warn: Schovat filtrovaný obsah za varováním zmiňujicím název filtru form_admin_settings: @@ -88,6 +89,7 @@ cs: favicon: WEBP, PNG, GIF nebo JPG. Nahradí výchozí favicon Mastodonu vlastní ikonou. mascot: Přepíše ilustraci v pokročilém webovém rozhraní. media_cache_retention_period: Mediální soubory z příspěvků vzdálených uživatelů se ukládají do mezipaměti na vašem serveru. Pokud je nastaveno na kladnou hodnotu, budou média po zadaném počtu dní odstraněna. Pokud jsou mediální data vyžádána po jejich odstranění, budou znovu stažena, pokud je zdrojový obsah stále k dispozici. Vzhledem k omezením týkajícím se četnosti dotazů karet náhledů odkazů na weby třetích stran se doporučuje nastavit tuto hodnotu alespoň na 14 dní, jinak nebudou karty náhledů odkazů na vyžádání aktualizovány dříve. + min_age: Uživatelé budou požádáni, aby při registraci potvrdili datum svého narození peers_api_enabled: Seznam názvů domén se kterými se tento server setkal ve fediversu. Neobsahuje žádná data o tom, zda jste federovali s daným serverem, pouze že o něm váš server ví. Toto je využíváno službami, které sbírají o federování statistiku v obecném smyslu. profile_directory: Adresář profilu obsahuje seznam všech uživatelů, kteří se přihlásili, aby mohli být nalezeni. require_invite_text: Pokud přihlášení vyžaduje ruční schválení, měl by být textový vstup „Proč se chcete připojit?“ povinný spíše než volitelný @@ -146,6 +148,7 @@ cs: min_age: Neměla by být pod minimálním věkem požadovaným zákony vaší jurisdikce. user: chosen_languages: Po zaškrtnutí budou ve veřejných časových osách zobrazeny pouze příspěvky ve zvolených jazycích + date_of_birth: Musíme se ujistit, že je Vám alespoň %{age}, abyste mohli používat Mastodon. Nebudeme to ukládat. role: Role určuje, která oprávnění uživatel má. user_role: color: Barva, která má být použita pro roli v celém UI, jako RGB v hex formátu @@ -258,6 +261,7 @@ cs: name: Hashtag filters: actions: + blur: Skrýt média za varováním hide: Zcela skrýt warn: Skrýt s varováním form_admin_settings: @@ -271,6 +275,7 @@ cs: favicon: Favicon mascot: Vlastní maskot (zastaralé) media_cache_retention_period: Doba uchovávání mezipaměti médií + min_age: Minimální věková hranice peers_api_enabled: Zveřejnit seznam nalezených serverů v API profile_directory: Povolit adresář profilů registrations_mode: Kdo se může přihlásit @@ -349,6 +354,9 @@ cs: jurisdiction: Právní příslušnost min_age: Věková hranice user: + date_of_birth_1i: Den + date_of_birth_2i: Měsíc + date_of_birth_3i: Rok role: Role time_zone: Časové pásmo user_role: diff --git a/config/locales/simple_form.cy.yml b/config/locales/simple_form.cy.yml index be60431e51..c9c7862a91 100644 --- a/config/locales/simple_form.cy.yml +++ b/config/locales/simple_form.cy.yml @@ -41,7 +41,7 @@ cy: defaults: autofollow: Bydd pobl sy'n cofrestru drwy'r gwahoddiad yn eich dilyn yn awtomatig avatar: WEBP, PNG, GIF neu JPG. %{size} ar y mwyaf. Bydd yn cael ei leihau i %{dimensions}px - bot: Mae'r cyfrif hwn yn perfformio gweithredoedd awtomatig yn bennaf ac mae'n bosib nad yw'n cael ei fonitro + bot: Rhoi gwybod i eraill bod y cyfrif hwn yn perfformio gweithredoedd awtomatig yn bennaf ac mae'n bosib nad yw'n cael ei fonitro context: Un neu fwy cyd-destun lle dylai'r hidlydd weithio current_password: At ddibenion diogelwch, nodwch gyfrinair y cyfrif cyfredol current_username: I gadarnhau, nodwch enw defnyddiwr y cyfrif cyfredol @@ -75,6 +75,7 @@ cy: filters: action: Dewiswch pa weithred i'w chyflawni pan fydd postiad yn cyfateb i'r hidlydd actions: + blur: Cuddio cyfryngau tu ôl i rybudd, heb guddio'r testun ei hun hide: Cuddiwch y cynnwys wedi'i hidlo'n llwyr, gan ymddwyn fel pe na bai'n bodoli warn: Cuddiwch y cynnwys wedi'i hidlo y tu ôl i rybudd sy'n sôn am deitl yr hidlydd form_admin_settings: @@ -88,6 +89,7 @@ cy: favicon: WEBP, PNG, GIF neu JPG. Yn diystyru'r favicon Mastodon rhagosodedig gydag eicon cyfaddas. mascot: Yn diystyru'r darlun yn y rhyngwyneb gwe uwch. media_cache_retention_period: Mae ffeiliau cyfryngau o bostiadau a wneir gan ddefnyddwyr o bell yn cael eu storio ar eich gweinydd. Pan gaiff ei osod i werth positif, bydd y cyfryngau yn cael eu dileu ar ôl y nifer penodedig o ddyddiau. Os gofynnir am y data cyfryngau ar ôl iddo gael ei ddileu, caiff ei ail-lwytho i lawr, os yw'r cynnwys ffynhonnell yn dal i fod ar gael. Oherwydd cyfyngiadau ar ba mor aml y mae cardiau rhagolwg cyswllt yn pleidleisio i wefannau trydydd parti, argymhellir gosod y gwerth hwn i o leiaf 14 diwrnod, neu ni fydd cardiau rhagolwg cyswllt yn cael eu diweddaru ar alw cyn yr amser hwnnw. + min_age: Mae gofyn i ddefnyddwyr gadarnhau eu dyddiad geni wrth gofrestru peers_api_enabled: Rhestr o enwau parth y mae'r gweinydd hwn wedi dod ar eu traws yn y ffediws. Nid oes unrhyw ddata wedi'i gynnwys yma ynghylch a ydych chi'n ffedereiddio â gweinydd penodol, dim ond bod eich gweinydd yn gwybod amdano. Defnyddir hwn gan wasanaethau sy'n casglu ystadegau ar ffedereiddio mewn ystyr cyffredinol. profile_directory: Mae'r cyfeiriadur proffil yn rhestru'r holl ddefnyddwyr sydd wedi dewis i fod yn ddarganfyddiadwy. require_invite_text: Pan fydd angen cymeradwyaeth â llaw ar gyfer cofrestriadau, gwnewch y “Pam ydych chi am ymuno?” mewnbwn testun yn orfodol yn hytrach na dewisol @@ -146,6 +148,7 @@ cy: min_age: Ni ddylai fod yn is na'r isafswm oedran sy'n ofynnol gan gyfreithiau eich awdurdodaeth. user: chosen_languages: Wedi eu dewis, dim ond tŵtiau yn yr ieithoedd hyn bydd yn cael eu harddangos mewn ffrydiau cyhoeddus + date_of_birth: Mae'n rhaid i ni sicrhau eich bod o leiaf %{age} i ddefnyddio Mastodon. Fyddwn ni ddim yn storio hwn. role: Mae'r rôl yn rheoli pa ganiatâd sydd gan y defnyddiwr. user_role: color: Lliw i'w ddefnyddio ar gyfer y rôl drwy'r UI, fel RGB mewn fformat hecs @@ -258,6 +261,7 @@ cy: name: Hashnod filters: actions: + blur: Cuddio cyfryngau gyda rhybudd hide: Cuddio'n llwyr warn: Cuddio â rhybudd form_admin_settings: @@ -271,6 +275,7 @@ cy: favicon: Favicon mascot: Mascot cyfaddas (hen) media_cache_retention_period: Cyfnod cadw storfa cyfryngau + min_age: Gofyniad oed ieuengaf peers_api_enabled: Cyhoeddi rhestr o weinyddion a ddarganfuwyd yn yr API profile_directory: Galluogi cyfeiriadur proffil registrations_mode: Pwy all gofrestru @@ -336,7 +341,7 @@ cy: usable: Caniatáu i bostiadau ddefnyddio'r hashnod hwn yn lleol terms_of_service: changelog: Beth sydd wedi newid? - effective_date: Dyddiad effeithiol + effective_date: Dyddiad dod i rym text: Telerau Gwasanaeth terms_of_service_generator: admin_email: Cyfeiriad e-bost ar gyfer hysbysiadau cyfreithiol @@ -349,6 +354,9 @@ cy: jurisdiction: Awdurdodaeth gyfreithiol min_age: Isafswm oedran user: + date_of_birth_1i: Dydd + date_of_birth_2i: Mis + date_of_birth_3i: Blwyddyn role: Rôl time_zone: Cylchfa amser user_role: diff --git a/config/locales/simple_form.da.yml b/config/locales/simple_form.da.yml index 6cc86a6d3e..91582e60c5 100644 --- a/config/locales/simple_form.da.yml +++ b/config/locales/simple_form.da.yml @@ -75,6 +75,7 @@ da: filters: action: Vælg handlingen til eksekvering, når et indlæg matcher filteret actions: + blur: Skjul medier bag en advarsel, uden at skjule selve teksten hide: Skjul filtreret indhold helt (adfærd som om, det ikke fandtes) warn: Skjul filtreret indhold bag en advarsel, der nævner filterets titel form_admin_settings: @@ -88,6 +89,7 @@ da: favicon: WEBP, PNG, GIF eller JPG. Tilsidesætter standard Mastodon favikonet på mobilenheder med et tilpasset ikon. mascot: Tilsidesætter illustrationen i den avancerede webgrænseflade. media_cache_retention_period: Mediefiler fra indlæg oprettet af eksterne brugere er cachet på din server. Når sat til positiv værdi, slettes medier efter det angivne antal dage. Anmodes om mediedata efter de er slettet, gendownloades de, hvis kildeindholdet stadig er tilgængeligt. Grundet begrænsninger på, hvor ofte linkforhåndsvisningskort forespørger tredjeparts websteder, anbefales det at sætte denne værdi til mindst 14 dage, ellers opdateres linkforhåndsvisningskort ikke efter behov før det tidspunkt. + min_age: Brugere anmodes om at bekræfte deres fødselsdato under tilmelding peers_api_enabled: En liste med domænenavne, som denne server har stødt på i fediverset. Ingen data inkluderes her om, hvorvidt der fødereres med en given server, blot at din server kender til det. Dette bruges af tjenester, som indsamler generelle føderationsstatistikker. profile_directory: Profilmappen oplister alle brugere, som har valgt at kunne opdages. require_invite_text: Når tilmelding kræver manuel godkendelse, så gør “Hvorfor ønsker du at deltage?” tekstinput obligatorisk i stedet for valgfrit @@ -146,6 +148,7 @@ da: min_age: Bør ikke være under den iht. lovgivningen i det aktuelle retsområde krævede minimumsalder. user: chosen_languages: Når markeret, vil kun indlæg på de valgte sprog fremgå på offentlige tidslinjer + date_of_birth: Vi er nødt til at sikre, at man er fyldt %{age} for at bruge Mastodon. Vi gemmer ikke denne information. role: Rollen styrer, hvilke tilladelser brugeren er tildelt. user_role: color: Farven, i RGB hex-format, der skal bruges til rollen i hele UI'en @@ -258,6 +261,7 @@ da: name: Hashtag filters: actions: + blur: Skjul medier med en advarsel hide: Skjul helt warn: Skjul bag en advarsel form_admin_settings: @@ -271,6 +275,7 @@ da: favicon: Favikon mascot: Tilpasset maskot (ældre funktion) media_cache_retention_period: Media-cache opbevaringsperiode + min_age: Minimums alderskrav peers_api_enabled: Udgiv liste over fundne server i API'en profile_directory: Aktivér profiloversigt registrations_mode: Hvem, der kan tilmelde sig @@ -349,6 +354,9 @@ da: jurisdiction: Juridisk jurisdiktion min_age: Minimumsalder user: + date_of_birth_1i: Dag + date_of_birth_2i: Måned + date_of_birth_3i: År role: Rolle time_zone: Tidszone user_role: diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml index 13cfe4e324..016ed4b25a 100644 --- a/config/locales/simple_form.de.yml +++ b/config/locales/simple_form.de.yml @@ -75,6 +75,7 @@ de: filters: action: Gib an, welche Aktion ausgeführt werden soll, wenn ein Beitrag dem Filter entspricht actions: + blur: Medien mit einer Warnung ausblenden, ohne den Text selbst auszublenden hide: Den gefilterten Beitrag vollständig ausblenden, als hätte er nie existiert warn: Den gefilterten Beitrag hinter einer Warnung, die den Filtertitel beinhaltet, ausblenden form_admin_settings: @@ -88,6 +89,7 @@ de: favicon: WEBP, PNG, GIF oder JPG. Überschreibt das Standard-Mastodon-Favicon mit einem eigenen Symbol. mascot: Überschreibt die Abbildung in der erweiterten Weboberfläche. media_cache_retention_period: Mediendateien aus Beiträgen von externen Nutzer*innen werden auf deinem Server zwischengespeichert. Wenn ein positiver Wert gesetzt ist, werden die Medien nach der festgelegten Anzahl von Tagen gelöscht. Sollten die Medien nach dem Löschvorgang wieder angefragt werden, werden sie erneut heruntergeladen, sofern der ursprüngliche Inhalt noch vorhanden ist. Es wird empfohlen, diesen Wert auf mindestens 14 Tage festzulegen, da die Häufigkeit der Abfrage von Linkvorschaukarten für Websites von Dritten begrenzt ist und die Linkvorschaukarten sonst nicht vor Ablauf dieser Zeit aktualisiert werden. + min_age: Nutzer*innen werden bei der Registrierung aufgefordert, ihr Geburtsdatum zu bestätigen peers_api_enabled: Eine Liste von Domains, die diesem Server im Fediverse begegnet sind. Hierbei werden keine Angaben darüber gemacht, ob du mit einem bestimmten Server föderierst, sondern nur, dass dein Server davon weiß. Dies wird von Diensten verwendet, die allgemein Statistiken übers Ferdiverse sammeln. profile_directory: Dieses Verzeichnis zeigt alle Profile an, die sich dafür entschieden haben, entdeckt zu werden. require_invite_text: Wenn Registrierungen eine manuelle Genehmigung erfordern, dann werden Nutzer einen Grund für ihre Registrierung angeben müssen @@ -146,6 +148,7 @@ de: min_age: Sollte nicht unter dem gesetzlich vorgeschriebenen Mindestalter liegen. user: chosen_languages: Wenn du hier eine oder mehrere Sprachen auswählst, werden ausschließlich Beiträge in diesen Sprachen in deinen öffentlichen Timelines angezeigt + date_of_birth: Wir müssen sicherstellen, dass du mindestens %{age} Jahre alt bist, um Mastodon verwenden zu können. Das Alter wird nicht gespeichert. role: Die Rolle bestimmt, welche Berechtigungen das Konto hat. user_role: color: Farbe, die für diese Rolle in der gesamten Benutzerschnittstelle verwendet wird, als RGB im Hexadezimalsystem @@ -258,8 +261,9 @@ de: name: Hashtag filters: actions: + blur: Medien mit einer Warnung ausblenden hide: Vollständig ausblenden - warn: Mit einer Inhaltswarnung ausblenden + warn: Mit einer Warnung ausblenden form_admin_settings: activity_api_enabled: Aggregierte Nutzungsdaten über die API veröffentlichen app_icon: App-Symbol @@ -271,6 +275,7 @@ de: favicon: Favicon mascot: Benutzerdefiniertes Maskottchen (Legacy) media_cache_retention_period: Aufbewahrungsfrist für Medien im Cache + min_age: Erforderliches Mindestalter peers_api_enabled: Die entdeckten Server im Fediverse über die API veröffentlichen profile_directory: Profilverzeichnis aktivieren registrations_mode: Wer darf ein neues Konto registrieren? @@ -349,6 +354,9 @@ de: jurisdiction: Gerichtsstand min_age: Mindestalter user: + date_of_birth_1i: Tag + date_of_birth_2i: Monat + date_of_birth_3i: Jahr role: Rolle time_zone: Zeitzone user_role: diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 805705a0d0..9264216121 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -99,6 +99,7 @@ en: filters: action: Chose which action to perform when a post matches the filter actions: + blur: Hide media behind a warning, without hiding the text itself hide: Completely hide the filtered content, behaving as if it did not exist warn: Hide the filtered content behind a warning mentioning the filter's title form_admin_settings: @@ -116,6 +117,7 @@ en: favicon: WEBP, PNG, GIF or JPG. Overrides the default Mastodon favicon with a custom icon. mascot: Overrides the illustration in the advanced web interface. media_cache_retention_period: Media files from posts made by remote users are cached on your server. When set to a positive value, media will be deleted after the specified number of days. If the media data is requested after it is deleted, it will be re-downloaded, if the source content is still available. Due to restrictions on how often link preview cards poll third-party sites, it is recommended to set this value to at least 14 days, or link preview cards will not be updated on demand before that time. + min_age: Users will be asked to confirm their date of birth during sign-up peers_api_enabled: A list of domain names this server has encountered in the fediverse. No data is included here about whether you federate with a given server, just that your server knows about it. This is used by services that collect statistics on federation in a general sense. profile_directory: The profile directory lists all users who have opted-in to be discoverable. receive_other_servers_emoji_reaction: It can cause load. It is recommended to enable it only when there are few people. @@ -180,6 +182,7 @@ en: min_age: Should not be below the minimum age required by the laws of your jurisdiction. user: chosen_languages: When checked, only posts in selected languages will be displayed in public timelines + date_of_birth: We have to make sure you're at least %{age} to use Mastodon. We won't store this. role: The role controls which permissions the user has. user_role: color: Color to be used for the role throughout the UI, as RGB in hex format @@ -376,6 +379,7 @@ en: name: Hashtag filters: actions: + blur: Hide media with a warning hide: Hide completely warn: Hide with a warning options: @@ -400,6 +404,7 @@ en: favicon: Favicon mascot: Custom mascot (legacy) media_cache_retention_period: Media cache retention period + min_age: Minimum age requirement peers_api_enabled: Publish list of discovered servers in the API profile_directory: Enable profile directory receive_other_servers_emoji_reaction: Receive emoji reaction between other server users @@ -491,6 +496,9 @@ en: jurisdiction: Legal jurisdiction min_age: Minimum age user: + date_of_birth_1i: Day + date_of_birth_2i: Month + date_of_birth_3i: Year role: Role time_zone: Time zone user_role: diff --git a/config/locales/simple_form.eo.yml b/config/locales/simple_form.eo.yml index 06da13b199..1331ea7333 100644 --- a/config/locales/simple_form.eo.yml +++ b/config/locales/simple_form.eo.yml @@ -75,6 +75,7 @@ eo: filters: action: Elekti ago kiam mesaĝo kongruas la filtrilon actions: + blur: Kaŝi amaskomunikilaron malantaŭ averto, sen kaŝi la tekston mem hide: Tute kaŝigi la filtritajn enhavojn, kvazau ĝi ne ekzistis warn: Kaŝi la enhavon filtritan malantaŭ averto mencianta la nomon de la filtro form_admin_settings: @@ -142,6 +143,7 @@ eo: jurisdiction: Enlistigu la landon, kie loĝas kiu pagas la fakturojn. Se ĝi estas kompanio aŭ alia ento, listigu la landon, kie ĝi estas enkorpigita, kaj la urbon, regionon, teritorion aŭ ŝtaton laŭeble. user: chosen_languages: Kun tio markita nur mesaĝoj en elektitaj lingvoj aperos en publikaj tempolinioj + date_of_birth: Ni devas certigi, ke vi estas almenaŭ %{age} por uzi Mastodon. Ni ne konservos ĉi tion. role: La rolo kontrolas kiujn permesojn la uzanto havas. user_role: color: Koloro uzita por la rolo sur la UI, kun RGB-formato @@ -254,6 +256,7 @@ eo: name: Kradvorto filters: actions: + blur: Kaŝi amaskomunikilaron kun averto hide: Kaŝi komplete warn: Kaŝi malantaŭ averto form_admin_settings: @@ -343,7 +346,11 @@ eo: dmca_email: Retpoŝtadreso por DMCA/kopirajto-avizoj domain: Domajno jurisdiction: Laŭleĝa jurisdikcio + min_age: Minimuma aĝo user: + date_of_birth_1i: Tago + date_of_birth_2i: Monato + date_of_birth_3i: Jaro role: Rolo time_zone: Horzono user_role: diff --git a/config/locales/simple_form.es-AR.yml b/config/locales/simple_form.es-AR.yml index 81dbef0723..4333db9fed 100644 --- a/config/locales/simple_form.es-AR.yml +++ b/config/locales/simple_form.es-AR.yml @@ -75,6 +75,7 @@ es-AR: filters: action: Elegir qué acción realizar cuando un mensaje coincide con el filtro actions: + blur: Ocultar medios detrás de una advertencia, sin ocultar el texto en sí mismo hide: Ocultar completamente el contenido filtrado, comportándose como si no existiera warn: Ocultar el contenido filtrado detrás de una advertencia mencionando el título del filtro form_admin_settings: @@ -88,6 +89,7 @@ es-AR: favicon: WEBP, PNG, GIF o JPG. Reemplaza el favicón predeterminado de Mastodon con uno personalizado. mascot: Reemplaza la ilustración en la interface web avanzada. media_cache_retention_period: Los archivos de medios de mensajes publicados por usuarios remotos se almacenan en la memoria caché en tu servidor. Cuando se establece un valor positivo, los medios se eliminarán después del número especificado de días. Si los datos multimedia se solicitan después de eliminarse, se volverán a descargar, si es que el contenido fuente todavía está disponible. Debido a restricciones en la frecuencia con la que las tarjetas de previsualización de enlace consultan a sitios web de terceros, se recomienda establecer este valor a, al menos, 14 días, o las tarjetas de previsualización de enlaces no se actualizarán a pedido antes de ese momento. + min_age: Se pedirá a los usuarios que confirmen su fecha de nacimiento durante el registro peers_api_enabled: Una lista de nombres de dominio que este servidor ha encontrado en el Fediverso. Acá no se incluye ningún dato sobre si federás con un servidor determinado, sólo que tu servidor lo conoce. Esto es usado por los servicios que recopilan estadísticas sobre la federación en un sentido general. profile_directory: El directorio de perfiles lista a todos los usuarios que han optado a que su cuenta pueda ser descubierta. require_invite_text: Cuando registros aprobación manual, hacé que la solicitud de invitación "¿Por qué querés unirte?" sea obligatoria, en vez de opcional @@ -146,6 +148,7 @@ es-AR: min_age: No debería estar por debajo de la edad mínima requerida por las leyes de su jurisdicción. user: chosen_languages: Cuando estén marcados, sólo se mostrarán los mensajes en los idiomas seleccionados en las líneas temporales públicas + date_of_birth: Tenemos que asegurarnos de que al menos tenés %{age} años de edad para usar Mastodon. No almacenaremos esta información. role: El rol controla qué permisos tiene el usuario. user_role: color: Color que se utilizará para el rol a lo largo de la interface de usuario, como RGB en formato hexadecimal @@ -258,6 +261,7 @@ es-AR: name: Etiqueta filters: actions: + blur: Ocultar medios con una advertencia hide: Ocultar completamente warn: Ocultar con una advertencia form_admin_settings: @@ -271,6 +275,7 @@ es-AR: favicon: Favicón mascot: Mascota personalizada (legado) media_cache_retention_period: Período de retención de la caché de medios + min_age: Edad mínima requerida peers_api_enabled: Publicar lista de servidores descubiertos en la API profile_directory: Habilitar directorio de perfiles registrations_mode: Quién puede registrarse @@ -349,6 +354,9 @@ es-AR: jurisdiction: Jurisdicción legal min_age: Edad mínima user: + date_of_birth_1i: Día + date_of_birth_2i: Mes + date_of_birth_3i: Año role: Rol time_zone: Zona horaria user_role: diff --git a/config/locales/simple_form.es-MX.yml b/config/locales/simple_form.es-MX.yml index 4b47f33c2e..63287a3989 100644 --- a/config/locales/simple_form.es-MX.yml +++ b/config/locales/simple_form.es-MX.yml @@ -75,6 +75,7 @@ es-MX: filters: action: Elegir qué acción realizar cuando una publicación coincide con el filtro actions: + blur: Ocultar contenido multimedia detrás de una advertencia, sin ocultar el texto en sí hide: Ocultar completamente el contenido filtrado, comportándose como si no existiera warn: Ocultar el contenido filtrado detrás de una advertencia mencionando el título del filtro form_admin_settings: @@ -88,6 +89,7 @@ es-MX: favicon: WEBP, PNG, GIF o JPG. Reemplaza el icono predeterminado de Mastodon con un icono personalizado. mascot: Reemplaza la ilustración en la interfaz web avanzada. media_cache_retention_period: Los archivos multimedia de las publicaciones realizadas por usuarios remotos se almacenan en caché en su servidor. Si se establece en un valor positivo, los archivos multimedia se eliminarán tras el número de días especificado. Si los datos multimedia se solicitan después de haber sido eliminados, se volverán a descargar, si el contenido de origen sigue estando disponible. Debido a las restricciones sobre la frecuencia con la que las tarjetas de previsualización de enlaces sondean sitios de terceros, se recomienda establecer este valor en al menos 14 días, o las tarjetas de previsualización de enlaces no se actualizarán bajo demanda antes de ese tiempo. + min_age: Se pedirá a los usuarios que confirmen su fecha de nacimiento durante el registro peers_api_enabled: Una lista de nombres de dominio que este servidor ha encontrado en el fediverso. Aquí no se incluye ningún dato sobre si usted federa con un servidor determinado, sólo que su servidor lo sabe. Esto es utilizado por los servicios que recopilan estadísticas sobre la federación en un sentido general. profile_directory: El directorio de perfiles lista a todos los usuarios que han optado por que su cuenta pueda ser descubierta. require_invite_text: Cuando los registros requieren aprobación manual, hace obligatoria la entrada de texto "¿Por qué quieres unirte?" en lugar de opcional @@ -146,6 +148,7 @@ es-MX: min_age: No debería estar por debajo de la edad mínima requerida por las leyes de su jurisdicción. user: chosen_languages: Cuando se marca, solo se mostrarán las publicaciones en los idiomas seleccionados en las líneas de tiempo públicas + date_of_birth: Tenemos que asegurarnos de que al menos tienes %{age} años para usar Mastodon. No almacenaremos esta información. role: El rol controla qué permisos tiene el usuario. user_role: color: Color que se usará para el rol en toda la interfaz de usuario, como RGB en formato hexadecimal @@ -258,6 +261,7 @@ es-MX: name: Etiqueta filters: actions: + blur: Ocultar contenido multimedia con una advertencia hide: Ocultar completamente warn: Ocultar con una advertencia form_admin_settings: @@ -271,6 +275,7 @@ es-MX: favicon: Favicon mascot: Mascota personalizada (legado) media_cache_retention_period: Período de retención de caché multimedia + min_age: Edad mínima requerida peers_api_enabled: Publicar lista de servidores descubiertos en la API profile_directory: Habilitar directorio de perfiles registrations_mode: Quién puede registrarse @@ -349,6 +354,9 @@ es-MX: jurisdiction: Jurisdicción legal min_age: Edad mínima user: + date_of_birth_1i: Día + date_of_birth_2i: Mes + date_of_birth_3i: Año role: Rol time_zone: Zona horaria user_role: diff --git a/config/locales/simple_form.es.yml b/config/locales/simple_form.es.yml index a0f115f577..d469c6ec3a 100644 --- a/config/locales/simple_form.es.yml +++ b/config/locales/simple_form.es.yml @@ -75,6 +75,7 @@ es: filters: action: Elegir qué acción realizar cuando una publicación coincide con el filtro actions: + blur: Ocultar contenido multimedia detrás de una advertencia, sin ocultar el texto en sí hide: Ocultar completamente el contenido filtrado, comportándose como si no existiera warn: Ocultar el contenido filtrado detrás de una advertencia mencionando el título del filtro form_admin_settings: @@ -88,6 +89,7 @@ es: favicon: WEBP, PNG, GIF o JPG. Reemplaza el favicon predeterminado de Mastodon con un icono personalizado. mascot: Reemplaza la ilustración en la interfaz web avanzada. media_cache_retention_period: Los archivos multimedia de las publicaciones creadas por usuarios remotos se almacenan en caché en tu servidor. Cuando se establece un valor positivo, estos archivos se eliminarán después del número especificado de días. Si los datos multimedia se solicitan después de eliminarse, se volverán a descargar, si el contenido fuente todavía está disponible. Debido a restricciones en la frecuencia con la que las tarjetas de previsualización de enlaces realizan peticiones a sitios de terceros, se recomienda establecer este valor a al menos 14 días, o las tarjetas de previsualización de enlaces no se actualizarán bajo demanda antes de ese momento. + min_age: Se pedirá a los usuarios que confirmen su fecha de nacimiento durante el registro peers_api_enabled: Una lista de nombres de dominio que este servidor ha encontrado en el Fediverso. Aquí no se incluye ningún dato sobre si federas con un servidor determinado, solo que tu servidor lo conoce. Esto es utilizado por los servicios que recopilan estadísticas sobre la federación en un sentido general. profile_directory: El directorio de perfiles lista a todos los usuarios que han optado por que su cuenta pueda ser descubierta. require_invite_text: Cuando los registros requieren aprobación manual, hace obligatoria la entrada de texto "¿Por qué quieres unirte?" en lugar de opcional @@ -146,6 +148,7 @@ es: min_age: No debería estar por debajo de la edad mínima requerida por las leyes de su jurisdicción. user: chosen_languages: Cuando se marca, solo se mostrarán las publicaciones en los idiomas seleccionados en las líneas de tiempo públicas + date_of_birth: Tenemos que asegurarnos de que al menos tienes %{age} años para usar Mastodon. No almacenaremos esta información. role: El rol controla qué permisos tiene el usuario. user_role: color: Color que se utilizará para el rol a lo largo de la interfaz de usuario, como RGB en formato hexadecimal @@ -258,6 +261,7 @@ es: name: Etiqueta filters: actions: + blur: Ocultar contenido multimedia con una advertencia hide: Ocultar completamente warn: Ocultar con una advertencia form_admin_settings: @@ -271,6 +275,7 @@ es: favicon: Favicon mascot: Mascota personalizada (legado) media_cache_retention_period: Período de retención de caché multimedia + min_age: Edad mínima requerida peers_api_enabled: Publicar lista de servidores descubiertos en la API profile_directory: Habilitar directorio de perfiles registrations_mode: Quién puede registrarse @@ -349,6 +354,9 @@ es: jurisdiction: Jurisdicción legal min_age: Edad mínima user: + date_of_birth_1i: Día + date_of_birth_2i: Mes + date_of_birth_3i: Año role: Rol time_zone: Zona horaria user_role: diff --git a/config/locales/simple_form.fi.yml b/config/locales/simple_form.fi.yml index 374b61f247..5489780ec7 100644 --- a/config/locales/simple_form.fi.yml +++ b/config/locales/simple_form.fi.yml @@ -75,6 +75,7 @@ fi: filters: action: Valitse, mikä toiminto suoritetaan, kun julkaisu vastaa suodatinta actions: + blur: Piilota media varoituksen taakse piilottamatta itse tekstiä hide: Piilota suodatettu sisältö kokonaan, ikään kuin sitä ei olisi olemassa warn: Piilota suodatettu sisältö varoituksen taakse, jossa mainitaan suodattimen nimi form_admin_settings: @@ -88,6 +89,7 @@ fi: favicon: WEBP, PNG, GIF tai JPG. Korvaa oletusarvoisen Mastodonin sivustokuvakkeen haluamallasi kuvakkeella. mascot: Korvaa kuvituksen edistyneessä selainkäyttöliittymässä. media_cache_retention_period: Etäkäyttäjien tekemien julkaisujen mediatiedostot ovat välimuistissa palvelimellasi. Kun kentän arvo on positiivinen, media poistuu, kun määritetty määrä päiviä on kulunut. Jos mediaa pyydetään sen poistamisen jälkeen, se ladataan uudelleen, jos lähdesisältö on vielä saatavilla. Koska linkkien esikatselun kyselyitä kolmansien osapuolien sivustoille on rajoitettu, on suositeltavaa asettaa tämä arvo vähintään 14 päivään, tai linkkien kortteja ei päivitetä pyynnöstä ennen tätä ajankohtaa. + min_age: Käyttäjiä pyydetään rekisteröitymisen aikana vahvistamaan syntymäpäivänsä peers_api_enabled: Luettelo verkkotunnuksista, jotka tämä palvelin on kohdannut fediversumissa. Se ei kerro, federoitko tietyn palvelimen kanssa, vaan että palvelimesi on ylipäätään tietoinen siitä. Tätä tietoa käytetään palveluissa, jotka keräävät tilastoja federoinnista yleisellä tasolla. profile_directory: Profiilihakemisto luetteloi kaikki käyttäjät, jotka ovat valinneet olla löydettävissä. require_invite_text: Kun rekisteröityminen vaatii manuaalisen hyväksynnän, tee ”Miksi haluat liittyä?” -tekstikentästä pakollinen vapaaehtoisen sijaan @@ -132,16 +134,20 @@ fi: name: Voit esimerkiksi vaihtaa suur- ja pienaakkosten kesken helppolukuistaaksesi tekstiäsi terms_of_service: changelog: Voidaan jäsentää Markdown-syntaksilla. + effective_date: Sopiva aika on 10–30 päivää siitä, kun olet ilmoittanut ehdoista käyttäjillesi. text: Voidaan jäsentää Markdown-syntaksilla. terms_of_service_generator: admin_email: Oikeudellisiin ilmoituksiin kuuluvat vastailmoitukset, oikeuden määräykset, poistopyynnöt ja lainvalvontaviranomaisten pyynnöt. arbitration_address: Voi olla sama kuin edellä mainittu Fyysinen osoite tai ”N/A”, jos käytät sähköpostia. arbitration_website: Voi olla verkkolomake tai ”N/A”, jos käytät sähköpostia. dmca_address: Yhdysvaltalaisten operaattoreiden on käytettävä DMCA Designated Agent Directory -luetteloon rekisteröityä osoitetta. Postilokeroluettelo on saatavissa suoralla pyynnöllä, joten käytä DMCA Designated Agent Post Office Box Waiver Request -lomaketta lähettääksesi sähköpostia tekijänoikeusvirastolle ja kuvaile, että olet kotona toimiva sisältömoderaattori, joka pelkää kostoa tai rangaistusta toimistaan ja tarvitsee postilokeroa pitääkseen kotiosoitteensa poissa julkisuudesta. + dmca_email: Voi olla sama kuin edellä mainittu ”Sähköpostiosoite oikeudellisille ilmoituksille”. domain: Tarjoamasi verkkopalvelun yksilöllinen tunniste. jurisdiction: Mainitse valtio, jossa laskujen maksaja asuu. Jos kyseessä on yritys tai muu yhteisö, mainitse valtio, johon se on rekisteröity, ja tarvittaessa kaupunki, alue, territorio tai osavaltio. + min_age: Ei pidä alittaa lainkäyttöalueesi lakien vaatimaa vähimmäisikää. user: chosen_languages: Jos valitset kieliä oheisesta luettelosta, vain niidenkieliset julkaisut näkyvät sinulle julkisilla aikajanoilla + date_of_birth: Meidän on varmistettava, että olet vähintään %{age} vuotta vanha, jotta voit käyttää Mastodonia. Emme tallenna tätä. role: Rooli määrää, millaiset käyttöoikeudet käyttäjällä on. user_role: color: Väri, jota käytetään roolille kaikkialla käyttöliittymässä, RGB-heksadesimaalimuodossa @@ -254,6 +260,7 @@ fi: name: Aihetunniste filters: actions: + blur: Piilota media varoittaen hide: Piilota kokonaan warn: Piilota varoittaen form_admin_settings: @@ -267,6 +274,7 @@ fi: favicon: Sivustokuvake mascot: Mukautettu maskotti (vanhentunut) media_cache_retention_period: Mediasisällön välimuistin säilytysaika + min_age: Vähimmäisikävaatimus peers_api_enabled: Julkaise löydettyjen palvelinten luettelo ohjelmointirajapinnassa profile_directory: Ota profiilihakemisto käyttöön registrations_mode: Kuka voi rekisteröityä @@ -344,6 +352,9 @@ fi: jurisdiction: Lainkäyttöalue min_age: Vähimmäisikä user: + date_of_birth_1i: Päivä + date_of_birth_2i: Kuukausi + date_of_birth_3i: Vuosi role: Rooli time_zone: Aikavyöhyke user_role: diff --git a/config/locales/simple_form.fo.yml b/config/locales/simple_form.fo.yml index 201de307b0..32d066ed18 100644 --- a/config/locales/simple_form.fo.yml +++ b/config/locales/simple_form.fo.yml @@ -75,6 +75,7 @@ fo: filters: action: Vel, hvat skal henda, tá eitt uppslag svarar til filtrið actions: + blur: Fjal miðlar aftanfyri eina ávaring, uttan at fjala sjálvan tekstin hide: Fjal filtreraða innihaldið fullkomiliga, ber seg at sum at tað ikki fanst warn: Fjal filtreraða innihaldið aftan fyri eina ávaring, sum nevnir heitið á filtrinum form_admin_settings: @@ -88,6 +89,7 @@ fo: favicon: WEBP, PNG, GIF ella JPG. Býtir vanligu Mastodon fav-ikonina um við eina ser-ikon. mascot: Skúgvar til viks myndprýðingina í framkomna vev-markamótinum. media_cache_retention_period: Miðlafílur frá postum, sum fjarbrúkarar hava gjørt, verða goymdir á tínum ambætara. Tá hetta er sett til eitt virði størri enn 0, so verða miðlafílurnar strikaðar eftir ásetta talið av døgum. Um miðladátur verða umbidnar eftir at tær eru strikaðar, verða tær tiknar innaftur á ambætaran, um keldutilfarið enn er tøkt. Vegna avmarkingar á hvussu ofta undanvísingarkort til leinki spyrja triðjapartsstøð, so verður mælt til at seta hetta virðið til í minsta lagi 14 dagar. Annars verða umbønir um dagføringar av undanvísingarkortum til leinki ikki gjørdar áðrenn hetta. + min_age: Brúkarar verða spurdir um at vátta teirra føðingardag, tá tey skráseta seg peers_api_enabled: Ein listi við navnaøkjum, sum hesin ambætarin er komin framat í fediversinum. Ongar dátur eru tiknar við her um tú er sameind/ur við ein givnan ambætara, einans at tín ambætari veit um hann. Hetta verður brúkt av tænastum, sum gera hagtøl um sameining yvirhøvur. profile_directory: Vangaskráin listar allar brúkarar, sum hava valt at kunna uppdagast. require_invite_text: Tá tilmeldingar krevja serskilda góðkenning, set so "Hví vil tú vera við?" tekstateigin til at vera kravdan heldur enn valfrían @@ -146,6 +148,7 @@ fo: min_age: Eigur ikki at vera undir lægsta aldri, sum lógirnar í tínum rættarøki krevja. user: chosen_languages: Tá hetta er valt, verða einans postar í valdum málum vístir á almennum tíðarlinjum + date_of_birth: Vit mugu tryggja okkum, at tú er í minsta lagi %{age} ár fyri at brúka Mastodon. Vit goyma ikki hesar upplýsingar. role: Leikluturin stýrir hvørji rættindi, brúkarin hevur. user_role: color: Litur, sum leikluturin hevur í øllum brúkaramarkamótinum, sum RGB og upplýst sum sekstandatal @@ -258,6 +261,7 @@ fo: name: Tvíkrossur filters: actions: + blur: Fjal miðlar við eini ávaring hide: Fjal fullkomiliga warn: Fjal við eini ávaring form_admin_settings: @@ -271,6 +275,7 @@ fo: favicon: Favikon mascot: Serskildur maskottur (arvur) media_cache_retention_period: Tíðarskeið, har miðlagoymslur verða varðveittar + min_age: Aldursmark peers_api_enabled: Kunnger lista við uppdagaðum ambætarum í API'num profile_directory: Ger vangaskrá virkna registrations_mode: Hvør kann tilmelda seg @@ -349,6 +354,9 @@ fo: jurisdiction: Løgdømi min_age: Lægsti aldur user: + date_of_birth_1i: Dagur + date_of_birth_2i: Mánaði + date_of_birth_3i: Ár role: Leiklutur time_zone: Tíðarsona user_role: diff --git a/config/locales/simple_form.fr-CA.yml b/config/locales/simple_form.fr-CA.yml index 5e9fe7baa5..905ff30675 100644 --- a/config/locales/simple_form.fr-CA.yml +++ b/config/locales/simple_form.fr-CA.yml @@ -88,6 +88,7 @@ fr-CA: favicon: WEBP, PNG, GIF ou JPG. Remplace la favicon Mastodon par défaut avec une icône personnalisée. mascot: Remplace l'illustration dans l'interface Web avancée. media_cache_retention_period: Les fichiers médias des messages publiés par des utilisateurs distants sont mis en cache sur votre serveur. Lorsque cette valeur est positive, les médias sont supprimés au terme du nombre de jours spécifié. Si les données des médias sont demandées après leur suppression, elles seront téléchargées à nouveau, dans la mesure où le contenu source est toujours disponible. En raison des restrictions concernant la fréquence à laquelle les cartes de prévisualisation des liens interrogent des sites tiers, il est recommandé de fixer cette valeur à au moins 14 jours, faute de quoi les cartes de prévisualisation des liens ne seront pas mises à jour à la demande avant cette échéance. + min_age: Les utilisateurs seront invités à confirmer leur date de naissance lors de l'inscription peers_api_enabled: Une liste de noms de domaine que ce serveur a rencontrés dans le fédiverse. Aucune donnée indiquant si vous vous fédérez ou non avec un serveur particulier n'est incluse ici, seulement l'information que votre serveur connaît un autre serveur. Cette option est utilisée par les services qui collectent des statistiques sur la fédération en général. profile_directory: L'annuaire des profils répertorie tous les utilisateurs qui ont opté pour être découverts. require_invite_text: Lorsque les inscriptions nécessitent une approbation manuelle, rendre le texte de l’invitation "Pourquoi voulez-vous vous inscrire ?" obligatoire plutôt que facultatif @@ -132,14 +133,21 @@ fr-CA: name: Vous ne pouvez modifier que la casse des lettres, par exemple, pour le rendre plus lisible terms_of_service: changelog: Peut être structuré avec la syntaxe Markdown. + effective_date: Un délai raisonnable peut varier entre 10 et 30 jours à compter de la date à laquelle vous informez vos utilisateurs. text: Peut être structuré avec la syntaxe Markdown. terms_of_service_generator: admin_email: Les avis juridiques comprennent les contre-avis, les ordonnances judiciaires, les demandes de retrait et les demandes des forces de l'ordre. + arbitration_address: Il peut s'agir de la même que l'adresse physique ci-dessus, ou « N/A » si vous utilisez une adresse e-mail. + arbitration_website: Il peut s'agir d'un formulaire web ou de « N/A » s'il s'agit d'un courrier électronique. + choice_of_law: Ville, région, territoire ou État dont le droit matériel interne régit toute réclamation. dmca_address: Pour les opérateurs américains, utilisez l'adresse enregistrée dans le répertoire des agents désignés du DMCA Designated Agent Directory. Une boîte postale est disponible sur demande directe. Utilisez le formulaire de demande de dérogation pour l'utilisation d'une boîte postale par un agent désigné du Designated Agent Post Office Box Waiver Request pour envoyer un e-mail au Bureau du droit d'auteur (Copyright Office) et expliquer que vous êtes un modérateur de contenu à domicile qui craint des représailles ou une vengeance pour ses actions et que vous avez besoin d'utiliser une boîte postale afin de masquer votre adresse personnelle au public. + dmca_email: Il peut s'agir du même courriel que celui utilisé pour l'« Adresse électronique pour les avis juridiques » ci-dessus. domain: Identification unique du service en ligne que vous offrez. jurisdiction: Indiquez le pays dans lequel réside la personne qui paie les factures. S'il s'agit d'une entreprise ou d'une autre entité, indiquez le pays dans lequel elle est enregistrée, ainsi que la ville, la région, le territoire ou l'État, le cas échéant. + min_age: Ne doit pas être en dessous de l’âge minimum requis par les lois de votre juridiction. user: chosen_languages: Lorsque coché, seuls les messages dans les langues sélectionnées seront affichés sur les fils publics + date_of_birth: Nous devons nous assurer que vous avez au moins %{age} pour utiliser Mastodon. Nous ne conserverons pas ces informations. role: Le rôle définit quelles autorisations a l'utilisateur⋅rice. user_role: color: Couleur à attribuer au rôle dans l'interface, au format hexadécimal RVB @@ -265,6 +273,7 @@ fr-CA: favicon: Favicon mascot: Mascotte personnalisée (héritée) media_cache_retention_period: Durée de rétention des médias dans le cache + min_age: Âge minimum requis peers_api_enabled: Publie la liste des serveurs découverts dans l'API profile_directory: Activer l’annuaire des profils registrations_mode: Qui peut s’inscrire @@ -330,16 +339,22 @@ fr-CA: usable: Autoriser les messages à utiliser ce hashtag localement terms_of_service: changelog: Nouveautés? + effective_date: Date effective text: Conditions d'utilisation terms_of_service_generator: admin_email: Adresse électronique pour les notifications légales arbitration_address: Adresse physique pour les notifications d'arbitrage arbitration_website: Site Web pour soumettre les notifications d'arbitrage + choice_of_law: Choix de la loi dmca_address: Adresse physique pour les avis DMCA/copyright dmca_email: Adresse e-mail pour les avis DMCA/copyright domain: Domaine jurisdiction: Juridiction + min_age: Âge minimum user: + date_of_birth_1i: Jour + date_of_birth_2i: Mois + date_of_birth_3i: Année role: Rôle time_zone: Fuseau horaire user_role: diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml index 921fa8eda4..3802a7f32f 100644 --- a/config/locales/simple_form.fr.yml +++ b/config/locales/simple_form.fr.yml @@ -88,6 +88,7 @@ fr: favicon: WEBP, PNG, GIF ou JPG. Remplace la favicon Mastodon par défaut avec une icône personnalisée. mascot: Remplace l'illustration dans l'interface Web avancée. media_cache_retention_period: Les fichiers médias des messages publiés par des utilisateurs distants sont mis en cache sur votre serveur. Lorsque cette valeur est positive, les médias sont supprimés au terme du nombre de jours spécifié. Si les données des médias sont demandées après leur suppression, elles seront téléchargées à nouveau, dans la mesure où le contenu source est toujours disponible. En raison des restrictions concernant la fréquence à laquelle les cartes de prévisualisation des liens interrogent des sites tiers, il est recommandé de fixer cette valeur à au moins 14 jours, faute de quoi les cartes de prévisualisation des liens ne seront pas mises à jour à la demande avant cette échéance. + min_age: Les utilisateurs seront invités à confirmer leur date de naissance lors de l'inscription peers_api_enabled: Une liste de noms de domaine que ce serveur a rencontrés dans le fédiverse. Aucune donnée indiquant si vous vous fédérez ou non avec un serveur particulier n'est incluse ici, seulement l'information que votre serveur connaît un autre serveur. Cette option est utilisée par les services qui collectent des statistiques sur la fédération en général. profile_directory: L'annuaire des profils répertorie tous les comptes qui choisi d'être découvrables. require_invite_text: Lorsque les inscriptions nécessitent une approbation manuelle, rendre le texte de l’invitation "Pourquoi voulez-vous vous inscrire ?" obligatoire plutôt que facultatif @@ -132,14 +133,21 @@ fr: name: Vous ne pouvez modifier que la casse des lettres, par exemple, pour le rendre plus lisible terms_of_service: changelog: Peut être structuré avec la syntaxe Markdown. + effective_date: Un délai raisonnable peut varier entre 10 et 30 jours à compter de la date à laquelle vous informez vos utilisateurs. text: Peut être structuré avec la syntaxe Markdown. terms_of_service_generator: admin_email: Les avis juridiques comprennent les contre-avis, les ordonnances judiciaires, les demandes de retrait et les demandes des forces de l'ordre. + arbitration_address: Il peut s'agir de la même que l'adresse physique ci-dessus, ou « N/A » si vous utilisez une adresse e-mail. + arbitration_website: Il peut s'agir d'un formulaire web ou de « N/A » s'il s'agit d'un courrier électronique. + choice_of_law: Ville, région, territoire ou État dont le droit matériel interne régit toute réclamation. dmca_address: Pour les opérateurs américains, utilisez l'adresse enregistrée dans le répertoire des agents désignés du DMCA Designated Agent Directory. Une boîte postale est disponible sur demande directe. Utilisez le formulaire de demande de dérogation pour l'utilisation d'une boîte postale par un agent désigné du Designated Agent Post Office Box Waiver Request pour envoyer un e-mail au Bureau du droit d'auteur (Copyright Office) et expliquer que vous êtes un modérateur de contenu à domicile qui craint des représailles ou une vengeance pour ses actions et que vous avez besoin d'utiliser une boîte postale afin de masquer votre adresse personnelle au public. + dmca_email: Il peut s'agir du même courriel que celui utilisé pour l'« Adresse électronique pour les avis juridiques » ci-dessus. domain: Identification unique du service en ligne que vous offrez. jurisdiction: Indiquez le pays dans lequel réside la personne qui paie les factures. S'il s'agit d'une entreprise ou d'une autre entité, indiquez le pays dans lequel elle est enregistrée, ainsi que la ville, la région, le territoire ou l'État, le cas échéant. + min_age: Ne doit pas être en dessous de l’âge minimum requis par les lois de votre juridiction. user: chosen_languages: Lorsque coché, seuls les messages dans les langues sélectionnées seront affichés sur les fils publics + date_of_birth: Nous devons nous assurer que vous avez au moins %{age} pour utiliser Mastodon. Nous ne conserverons pas ces informations. role: Le rôle définit quelles autorisations a l'utilisateur⋅rice. user_role: color: Couleur à attribuer au rôle dans l'interface, au format hexadécimal RVB @@ -265,6 +273,7 @@ fr: favicon: Favicon mascot: Mascotte personnalisée (héritée) media_cache_retention_period: Durée de rétention des médias dans le cache + min_age: Âge minimum requis peers_api_enabled: Publie la liste des serveurs découverts dans l'API profile_directory: Activer l’annuaire des profils registrations_mode: Qui peut s’inscrire @@ -330,16 +339,22 @@ fr: usable: Autoriser les messages à utiliser ce hashtag localement terms_of_service: changelog: Nouveautés? + effective_date: Date effective text: Conditions d'utilisation terms_of_service_generator: admin_email: Adresse électronique pour les notifications légales arbitration_address: Adresse physique pour les notifications d'arbitrage arbitration_website: Site Web pour soumettre les notifications d'arbitrage + choice_of_law: Choix de la loi dmca_address: Adresse physique pour les avis DMCA/copyright dmca_email: Adresse e-mail pour les avis DMCA/copyright domain: Domaine jurisdiction: Juridiction + min_age: Âge minimum user: + date_of_birth_1i: Jour + date_of_birth_2i: Mois + date_of_birth_3i: Année role: Rôle time_zone: Fuseau horaire user_role: diff --git a/config/locales/simple_form.gl.yml b/config/locales/simple_form.gl.yml index a23173a6d1..ccf3ebcd4f 100644 --- a/config/locales/simple_form.gl.yml +++ b/config/locales/simple_form.gl.yml @@ -88,6 +88,7 @@ gl: favicon: WEBP, PNG, GIF ou JPG. Sobrescribe a icona de favoritos de Mastodon por defecto cunha icona personalizada. mascot: Sobrescribe a ilustración na interface web avanzada. media_cache_retention_period: Os ficheiros multimedia de publicacións de usuarias remotas están almacenados no teu servidor. Ao establecer un valor positivo, o multimedia vaise eliminar despois do número de días establecido. Se o multimedia fose requerido após ser eliminado entón descargaríase outra vez, se aínda está dispoñible na orixe. Debido a restricións sobre a frecuencia en que o servizo de vista previa trae recursos de terceiras partes, é recomendable establecer este valor polo menos en 14 días, ou as tarxetas de vista previa non se actualizarán baixo demanda para casos anteriores a ese prazo. + min_age: Váiselle pedir ás usuarias que confirmen a súa data de nacemento cando creen a conta peers_api_enabled: Unha lista dos nomes de dominio que este servidor atopou no fediverso. Non se inclúen aquí datos acerca de se estás a federar con eles ou non, só que o teu servidor os recoñeceu. Ten utilidade para servizos que recollen estatísticas acerca da federación nun amplo senso. profile_directory: O directorio de perfís inclúe a tódalas usuarias que optaron por ser descubribles. require_invite_text: Cando os rexistros requiren aprobación manual, facer que o texto "Por que te queres rexistrar?" do convite sexa obrigatorio en lugar de optativo @@ -146,6 +147,7 @@ gl: min_age: Non debería ser inferior á idade mínima requerida polas leis da túa xurisdición. user: chosen_languages: Se ten marca, só as publicacións nos idiomas seleccionados serán mostrados en cronoloxías públicas + date_of_birth: Temos que ter certeza de que tes %{age} como mínimo para usar Mastodon. Non gardamos este dato. role: Os roles establecen os permisos que ten a usuaria. user_role: color: Cor que se usará para o rol a través da IU, como RGB en formato hex @@ -271,6 +273,7 @@ gl: favicon: Favicon mascot: Mascota propia (herdado) media_cache_retention_period: Período de retención da caché multimedia + min_age: Idade mínima requerida peers_api_enabled: Publicar na API unha lista dos servidores descubertos profile_directory: Activar o directorio de perfís registrations_mode: Quen se pode rexistrar @@ -349,6 +352,9 @@ gl: jurisdiction: Xurisdición legal min_age: Idade mínima user: + date_of_birth_1i: Día + date_of_birth_2i: Mes + date_of_birth_3i: Ano role: Rol time_zone: Fuso horario user_role: diff --git a/config/locales/simple_form.he.yml b/config/locales/simple_form.he.yml index bab104025f..9c01b91638 100644 --- a/config/locales/simple_form.he.yml +++ b/config/locales/simple_form.he.yml @@ -75,6 +75,7 @@ he: filters: action: בחרו איזו פעולה לבצע כאשר הודעה מתאימה למסנן actions: + blur: החבאת וידאו ותמונות מאחורי אזהרה, ללא החבאת המלל עצמו hide: הסתרת התוכן המסונן, כאילו לא היה קיים warn: הסתרת התוכן המסונן מאחורי אזהרה עם כותרת המסנן form_admin_settings: @@ -88,6 +89,7 @@ he: favicon: WEBP, PNG, GIF או JPG. גובר על "פאבאייקון" ברירת המחדל ומחליף אותו באייקון נבחר בדפדפן. mascot: בחירת ציור למנשק הווב המתקדם. media_cache_retention_period: קבצי מדיה מהודעות שהגיעו משרתים רחוקים נשמרות על השרת שלך. כאשר יבחר פה מספר חיובי, המדיה תמחק לאחר מספר ימים כמצוין. אם המידע יבוקש שוב לאחר שנמחק, הוא יורד מחדש, אם המידע עדיין זמין בצד הרחוק. עקב מגבלות על תכיפות שליפת כרטיסי קדימון מאתרים מרוחקים, מומלץ לכוון את הערך ל־14 יום לפחות, או שכרטיסי קדימונים לא יעודכנו לפי דרישה לפני חלוף חלון הזמן הזה. + min_age: משתמשיםות יתבקשו לאשר את תאריך הלידה בתהליך ההרשמה peers_api_enabled: רשימת השרתים ששרת זה פגש בפדיוורס. לא כולל מידע לגבי קשר ישיר עם שרת נתון, אלא רק שידוע לשרת זה על קיומו. מידע זה משמש שירותים האוספים סטטיסטיקות כלליות על הפדרציה. profile_directory: ספריית הפרופילים מציגה ברשימה את כל המשתמשים שביקשו להיות ניתנים לגילוי. require_invite_text: כאשר הרשמות דורשות אישור ידני, הפיכת טקסט ה"מדוע את/ה רוצה להצטרף" להכרחי במקום אופציונלי @@ -146,6 +148,7 @@ he: min_age: על הערך להיות לפחות בגיל המינימלי הדרוש בחוק באיזור השיפוט שלך. user: chosen_languages: אם פעיל, רק הודעות בשפות הנבחרות יוצגו לפידים הפומביים + date_of_birth: עלינו לוודא שגילך לפחות %{age} כדי להשתמש במסטודון. המידע לא ישמר בשרת שלנו. role: התפקיד שולט על אילו הרשאות יש למשתמש. user_role: color: צבע לתפקיד בממשק המשתמש, כ RGB בפורמט הקסדצימלי @@ -258,6 +261,7 @@ he: name: תגית filters: actions: + blur: הסתרת מדיה עם אזהרה hide: הסתרה כוללת warn: הסתרה עם אזהרה form_admin_settings: @@ -271,6 +275,7 @@ he: favicon: סמל מועדפים (Favicon) mascot: סמל השרת (ישן) media_cache_retention_period: תקופת שמירת מטמון מדיה + min_age: דרישת גיל מינימלי peers_api_enabled: פרסם רשימה של שרתים שנתגלו באמצעות ה-API profile_directory: הפעלת ספריית פרופילים registrations_mode: מי יכולים לפתוח חשבון @@ -349,6 +354,9 @@ he: jurisdiction: איזור השיפוט min_age: גיל מינימלי user: + date_of_birth_1i: יום + date_of_birth_2i: חודש + date_of_birth_3i: שנה role: תפקיד time_zone: אזור זמן user_role: diff --git a/config/locales/simple_form.hu.yml b/config/locales/simple_form.hu.yml index 977ef0a279..adcc3d5789 100644 --- a/config/locales/simple_form.hu.yml +++ b/config/locales/simple_form.hu.yml @@ -75,6 +75,7 @@ hu: filters: action: A végrehajtandó műveletet, ha a bejegyzés megfelel a szűrőnek actions: + blur: Média elrejtése figyelmeztetéssel, a szöveg elrejtése nélkül hide: A szűrt tartalom teljes elrejtése, mintha nem is létezne warn: A szűrt tartalom a szűrő címét említő figyelmeztetés mögé rejtése form_admin_settings: @@ -88,6 +89,7 @@ hu: favicon: WEBP, PNG, GIF vagy JPG. Az alapértelmezett Mastodon favicont felülírja egy egyéni ikonnal. mascot: Felülbírálja a speciális webes felületen található illusztrációt. media_cache_retention_period: A távoli felhasználók bejegyzéseinek médiatartalmait a kiszolgálód gyorsítótárazza. Ha pozitív értékre állítják, ezek a médiatartalmak a megadott számú nap után törölve lesznek. Ha a médiát újra lekérik, miután törlődött, újra le fogjuk tölteni, ha az eredeti még elérhető. A hivatkozások előnézeti kártyáinak harmadik fél weboldalai felé történő hivatkozásaira alkalmazott megkötései miatt javasolt, hogy ezt az értéket legalább 14 napra állítsuk, ellenkező esetben a hivatkozások előnézeti kártyái szükség esetén nem fognak tudni frissülni ezen idő előtt. + min_age: A felhasználók a regisztráció során arra lesznek kérve, hogy erősítsek meg a születési dátumukat peers_api_enabled: Azon domainek listája, melyekkel ez a kiszolgáló találkozott a fediverzumban. Nem csatolunk adatot arról, hogy föderált kapcsolatban vagy-e az adott kiszolgálóval, csak arról, hogy a kiszolgálód tud a másikról. Ezt olyan szolgáltatások használják, melyek általában a föderációról készítenek statisztikákat. profile_directory: A profilok jegyzéke minden olyan felhasználót felsorol, akik engedélyezték a felfedezhetőségüket. require_invite_text: Ha a regisztrációhoz manuális jóváhagyásra van szükség, akkor a „Miért akarsz csatlakozni?” válasz kitöltése legyen kötelező, és ne opcionális @@ -146,6 +148,7 @@ hu: min_age: Nem lehet a joghatóság által meghatározott minimális kor alatt. user: chosen_languages: Ha aktív, csak a kiválasztott nyelvű bejegyzések jelennek majd meg a nyilvános idővonalon + date_of_birth: Legalább %{age} évesnek kell lenniük, hogy használhassák a Mastodont. Ezt nem tároljuk. role: A szerep szabályozza, hogy a felhasználó milyen jogosultságokkal rendelkezik. user_role: color: A szerephez használandó szín mindenhol a felhasználói felületen, hexa RGB formátumban @@ -258,6 +261,7 @@ hu: name: Hashtag filters: actions: + blur: Média elrejtése figyelmeztetéssel hide: Teljes elrejtés warn: Elrejtés figyelmeztetéssel form_admin_settings: @@ -271,6 +275,7 @@ hu: favicon: Könyvjelzőikon mascot: Egyéni kabala (örökölt) media_cache_retention_period: Média-gyorsítótár megtartási időszaka + min_age: Minimális korhatár peers_api_enabled: Felfedezett kiszolgálók listájának közzététele az API-ban profile_directory: Profiladatbázis engedélyezése registrations_mode: Ki regisztrálhat @@ -349,6 +354,9 @@ hu: jurisdiction: Joghatóság min_age: Minimális életkor user: + date_of_birth_1i: Nap + date_of_birth_2i: Hónap + date_of_birth_3i: Év role: Szerep time_zone: Időzóna user_role: diff --git a/config/locales/simple_form.is.yml b/config/locales/simple_form.is.yml index e190198f89..bc68d14f2a 100644 --- a/config/locales/simple_form.is.yml +++ b/config/locales/simple_form.is.yml @@ -75,6 +75,7 @@ is: filters: action: Veldu hvaða aðgerð á að framkvæma þegar færsla samsvarar síunni actions: + blur: Fela myndefni á bakvið aðvörun, án þess að fela sjálfann textann hide: Fela síað efni algerlega, rétt eins og það sé ekki til staðar warn: Fela síað efni á bakvið aðvörun sem tekur fram titil síunnar form_admin_settings: @@ -88,6 +89,7 @@ is: favicon: WEBP, PNG, GIF eða JPG. Tekur yfir sjálfgefna Mastodon favicon-táknmynd með sérsniðinni táknmynd. mascot: Þetta tekyr yfir myndskreytinguna í ítarlega vefviðmótinu. media_cache_retention_period: Myndefnisskrár úr færslum sem gerðar eru af fjartengdum notendum eru geymdar á netþjóninum þínum. Þegar þetta er stillt á jákvætt gildi, verður þessum skrám eytt sjáfkrafa eftir þeim tiltekna fjölda daga. Ef beðið er um myndefnið eftir að því er eytt, mun það verða sótt aftur ef frumgögnin eru ennþá aðgengileg. Vegna takmarkana á hversu oft forskoðunarspjöld tengla eru sótt á utanaðkomandi netþjóna, þá er mælt með því að setja þetta gildi á að minnsta kosti 14 daga, annars gæti mistekist að uppfæra forskoðunarspjöld tengla eftir þörfum fyrir þann tíma. + min_age: Notendur verða beðnir um að staðfesta fæðingardag sinn við nýskráningu peers_api_enabled: Listi yfir þau lénaheiti sem þessi netþjónn hefur rekist á í skýjasambandinu. Engin gögn eru hér sem gefa til kynna hvort þú sért í sambandi við tiltekinn netþjón, bara að netþjónninn þinn viti um hann. Þetta er notað af þjónustum sem safna tölfræði um skýjasambönd á almennan hátt. profile_directory: Notendamappan telur upp alla þá notendur sem hafa valið að vera uppgötvanlegir. require_invite_text: Þegar nýskráningar krefjast handvirks samþykkis, þá skal gera textann í “Hvers vegna viltu taka þátt?” að kröfu en ekki valkvæðan @@ -146,6 +148,7 @@ is: min_age: Ætti ekki að vera lægri en sá lágmarksaldur sek kveðið er á um í lögum þíns lögsagnarumdæmis. user: chosen_languages: Þegar merkt er við þetta, birtast einungis færslur á völdum tungumálum á opinberum tímalínum + date_of_birth: Við verðum að ganga úr skugga um að þú hafir náð %{age} aldri til að nota Mastodon. Við munum ekki geyma þessar upplýsingar. role: Hlutverk stýrir hvaða heimildir notandinn hefur. user_role: color: Litur sem notaður er fyrir hlutverkið allsstaðar í viðmótinu, sem RGB-gildi á hex-sniði @@ -258,6 +261,7 @@ is: name: Myllumerki filters: actions: + blur: Fela myndefni með aðvörun hide: Fela alveg warn: Fela með aðvörun form_admin_settings: @@ -271,6 +275,7 @@ is: favicon: Auðkennismynd mascot: Sérsniðið gæludýr (eldra) media_cache_retention_period: Tímalengd sem myndefni haldið + min_age: Kröfur um lágmarksaldur peers_api_enabled: Birta lista yfir uppgötvaða netþjóna í API-kerfisviðmótinu profile_directory: Virkja notendamöppu registrations_mode: Hverjir geta nýskráð sig @@ -349,6 +354,9 @@ is: jurisdiction: Lögsagnarumdæmi min_age: Lágmarksaldur user: + date_of_birth_1i: Dagur + date_of_birth_2i: Mánuður + date_of_birth_3i: Ár role: Hlutverk time_zone: Tímabelti user_role: diff --git a/config/locales/simple_form.it.yml b/config/locales/simple_form.it.yml index 57f497c98f..da203270fa 100644 --- a/config/locales/simple_form.it.yml +++ b/config/locales/simple_form.it.yml @@ -75,6 +75,7 @@ it: filters: action: Scegli quale azione eseguire quando un post corrisponde al filtro actions: + blur: Nascondi i contenuti multimediali dietro un avviso, senza nascondere il testo stesso hide: Nascondi completamente il contenuto filtrato, come se non esistesse warn: Nascondi il contenuto filtrato e mostra invece un avviso, citando il titolo del filtro form_admin_settings: @@ -88,6 +89,7 @@ it: favicon: WEBP, PNG, GIF o JPG. Sostituisce la favicon predefinita di Mastodon con un'icona personalizzata. mascot: Sostituisce l'illustrazione nell'interfaccia web avanzata. media_cache_retention_period: I file multimediali da post fatti da utenti remoti sono memorizzati nella cache sul tuo server. Quando impostato a un valore positivo, i media verranno eliminati dopo il numero specificato di giorni. Se i dati multimediali sono richiesti dopo che sono stati eliminati, saranno nuovamente scaricati, se il contenuto sorgente è ancora disponibile. A causa di restrizioni su quanto spesso link anteprima carte sondaggio siti di terze parti, si consiglia di impostare questo valore ad almeno 14 giorni, o le schede di anteprima link non saranno aggiornate su richiesta prima di quel tempo. + min_age: Gli utenti saranno invitati a confermare la loro data di nascita durante la registrazione peers_api_enabled: Un elenco di nomi di dominio che questo server ha incontrato nel fediverse. Qui non sono inclusi dati sul fatto se si federano con un dato server, solo che il server ne è a conoscenza. Questo viene utilizzato dai servizi che raccolgono statistiche sulla federazione in senso generale. profile_directory: La directory del profilo elenca tutti gli utenti che hanno acconsentito ad essere individuabili. require_invite_text: 'Quando le iscrizioni richiedono l''approvazione manuale, rendi la domanda: "Perché vuoi unirti?" obbligatoria anziché facoltativa' @@ -146,6 +148,7 @@ it: min_age: Non si dovrebbe avere un'età inferiore a quella minima richiesta, dalle leggi della tua giurisdizione. user: chosen_languages: Quando una o più lingue sono contrassegnate, nelle timeline pubbliche vengono mostrati solo i toot nelle lingue selezionate + date_of_birth: Dobbiamo verificare che tu abbia almeno %{age} anni per usare Mastodon. Non archivieremo questa informazione. role: Il ruolo controlla quali permessi ha l'utente. user_role: color: Colore da usare per il ruolo in tutta l'UI, come RGB in formato esadecimale @@ -258,6 +261,7 @@ it: name: Etichetta filters: actions: + blur: Nascondi i contenuti multimediali con un avviso hide: Nascondi completamente warn: Nascondi con avviso form_admin_settings: @@ -271,6 +275,7 @@ it: favicon: Favicon mascot: Personalizza mascotte (legacy) media_cache_retention_period: Periodo di conservazione della cache multimediale + min_age: Età minima richiesta peers_api_enabled: Pubblica l'elenco dei server scoperti nell'API profile_directory: Abilita directory del profilo registrations_mode: Chi può iscriversi @@ -349,6 +354,9 @@ it: jurisdiction: Giurisdizione legale min_age: Età minima user: + date_of_birth_1i: Giorno + date_of_birth_2i: Mese + date_of_birth_3i: Anno role: Ruolo time_zone: Fuso orario user_role: diff --git a/config/locales/simple_form.ko.yml b/config/locales/simple_form.ko.yml index ef3764bed8..a823604584 100644 --- a/config/locales/simple_form.ko.yml +++ b/config/locales/simple_form.ko.yml @@ -75,6 +75,7 @@ ko: filters: action: 게시물이 필터에 걸러질 때 어떤 동작을 수행할 지 고르세요 actions: + blur: 텍스트는 숨기지 않고 그대로 둔 채 경고 뒤에 미디어를 숨김니다 hide: 필터에 걸러진 글을 처음부터 없었던 것처럼 완전히 가리기 warn: 필터 제목을 언급하는 경고 뒤에 걸러진 내용을 숨기기 form_admin_settings: @@ -88,6 +89,7 @@ ko: favicon: WEBP, PNG, GIF 또는 JPG. 기본 파비콘을 대체합니다. mascot: 고급 웹 인터페이스의 그림을 대체합니다. media_cache_retention_period: 원격 사용자가 작성한 글의 미디어 파일은 이 서버에 캐시됩니다. 양수로 설정하면 지정된 일수 후에 미디어가 삭제됩니다. 삭제된 후에 미디어 데이터를 요청하면 원본 콘텐츠를 사용할 수 있는 경우 다시 다운로드됩니다. 링크 미리 보기 카드가 타사 사이트를 폴링하는 빈도에 제한이 있으므로 이 값을 최소 14일로 설정하는 것이 좋으며, 그렇지 않으면 그 이전에는 링크 미리 보기 카드가 제때 업데이트되지 않을 것입니다. + min_age: 사용자들은 가입할 때 생일을 확인받게 됩니다 peers_api_enabled: 이 서버가 연합우주에서 만났던 서버들에 대한 도메인 네임의 목록입니다. 해당 서버와 어떤 연합을 했는지에 대한 정보는 전혀 포함되지 않고, 단순히 그 서버를 알고 있는지에 대한 것입니다. 이것은 일반적으로 연합에 대한 통계를 수집할 때 사용됩니다. profile_directory: 프로필 책자는 발견되기를 희망하는 모든 사람들의 목록을 나열합니다. require_invite_text: 가입이 수동 승인을 필요로 할 때, "왜 가입하려고 하나요?" 항목을 선택사항으로 두는 것보다는 필수로 두는 것이 낫습니다 @@ -153,7 +155,7 @@ ko: position: 특정 상황에서 충돌이 발생할 경우 더 높은 역할이 충돌을 해결합니다. 특정 작업은 우선순위가 낮은 역할에 대해서만 수행될 수 있습니다 webhook: events: 전송할 이벤트를 선택하세요 - template: 원하는 JSON 페이로드를 변수와 함께 작성하거나, 그냥 냅둬서 기본 JSON을 사용할 수 있습니다. + template: 원하는 JSON 페이로드를 변수와 함께 작성하거나, 그대로 두어 기본 JSON을 사용할 수 있습니다. url: 이벤트가 어디로 전송될 지 labels: account: @@ -256,6 +258,7 @@ ko: name: 해시태그 filters: actions: + blur: 경고와 함께 미디어 숨기기 hide: 완전히 숨기기 warn: 경고와 함께 숨기기 form_admin_settings: @@ -269,6 +272,7 @@ ko: favicon: 파비콘 mascot: 사용자 정의 마스코트 (legacy) media_cache_retention_period: 미디어 캐시 유지 기한 + min_age: 최소 연령 제한 peers_api_enabled: API에 발견 된 서버들의 목록 발행 profile_directory: 프로필 책자 활성화 registrations_mode: 누가 가입할 수 있는지 @@ -347,6 +351,9 @@ ko: jurisdiction: 법적 관할권 min_age: 최소 연령 user: + date_of_birth_1i: 일 + date_of_birth_2i: 월 + date_of_birth_3i: 년 role: 역할 time_zone: 시간대 user_role: diff --git a/config/locales/simple_form.lt.yml b/config/locales/simple_form.lt.yml index 1db9e27359..d3febceea9 100644 --- a/config/locales/simple_form.lt.yml +++ b/config/locales/simple_form.lt.yml @@ -72,6 +72,7 @@ lt: filters: action: Pasirink, kokį veiksmą atlikti, kai įrašas atitinka filtrą actions: + blur: Slėpti mediją po įspėjimu, neslepiant paties teksto hide: Visiškai paslėpti filtruotą turinį ir elgtis taip, tarsi jo neegzistuotų warn: Slėpti filtruojamą turinį po įspėjimu, paminint filtro pavadinimą form_admin_settings: @@ -82,6 +83,7 @@ lt: favicon: WEBP, PNG, GIF arba JPG. Pakeičia numatytąją Mastodon svetaines piktogramą pasirinktine piktograma. mascot: Pakeičia išplėstinės žiniatinklio sąsajos iliustraciją. media_cache_retention_period: Nuotolinių naudotojų įrašytų įrašų medijos failai talpinami tavo serveryje. Nustačius teigiamą reikšmę, medijos bus ištrinamos po nurodyto dienų skaičiaus. Jei medijos duomenų bus paprašyta po to, kai jie bus ištrinti, jie bus atsiųsti iš naujo, jei šaltinio turinys vis dar prieinamas. Dėl apribojimų, susijusių su nuorodų peržiūros kortelių apklausos dažnumu trečiųjų šalių svetainėse, rekomenduojama nustatyti šią reikšmę ne trumpesnę kaip 14 dienų, kitaip nuorodų peržiūros kortelės nebus atnaujinamos pagal pareikalavimą iki to laiko. + min_age: Registracijos metu naudotojai bus paprašyti patvirtinti savo gimimo datą. peers_api_enabled: Domenų pavadinimų sąrašas, su kuriais šis serveris susidūrė fediverse. Čia nėra duomenų apie tai, ar tu bendrauji su tam tikru serveriu, tik apie tai, kad tavo serveris apie jį žino. Tai naudojama tarnybose, kurios renka federacijos statistiką bendrąja prasme. require_invite_text: Kai registraciją reikia patvirtinti rankiniu būdu, teksto įvesties laukelį „Kodėl nori prisijungti?“ padaryk privalomą, o ne pasirenkamą site_contact_email: Kaip žmonės gali su tavimi susisiekti teisiniais ar pagalbos užklausimais. @@ -108,12 +110,17 @@ lt: text: Gali būti struktūrizuota su ženklinimo sintakse. terms_of_service_generator: admin_email: Teisiniai pranešimai įtraukia priešpriešinius pranešimus, teismo įsakymus, pašalinimo prašymus ir teisėsaugos institucijų prašymus. + arbitration_address: Gali būti toks pat kaip aukščiau nurodytas fizinis adresas arba „N/A“ (netaikoma), jei naudojamas el. paštas. + arbitration_website: Gali būti interneto forma arba „N/A“ (netaikoma), jei naudojamas el. paštas. choice_of_law: Miestas, regionas, teritorija ar valstija, kurių vidaus materialinė teisė reglamentuoja visus reikalavimus. dmca_address: JAV operatoriams naudokite DMCA paskirtojo agento kataloge užregistruotą adresą. Pašto dėžutės sąrašą galima sudaryti pateikus tiesioginį prašymą, naudokite DMCA paskirtojo agento pašto dėžutės atsisakymo prašymą, kad parašytumėte el. laišką Autorinių teisių tarnybai ir aprašytumėte, kad esate namuose įsikūręs turinio moderatorius, kuris baiminasi keršto ar bausmės už savo veiksmus ir kuriam reikia naudoti pašto dėžutę, kad jo namų adresas nebūtų viešai matomas. + dmca_email: Gali būti tas pats aukščiau nurodytas el. pašto adresas, naudojamas „El. pašto adresas, skirtas teisiniams pranešimams“. domain: Unikalus jūsų teikiamos internetinės paslaugos identifikavimas. jurisdiction: Nurodykite šalį, kurioje gyvena tas, kas apmoka sąskaitas. Jei tai bendrovė ar kita esybė, nurodykite šalį, kurioje jis įregistruotas, ir atitinkamai miestą, regioną, teritoriją ar valstiją. + min_age: Neturėtų būti žemiau mažiausio amžiaus, reikalaujamo pagal jūsų jurisdikcijos įstatymus. user: chosen_languages: Kai pažymėta, viešose laiko skalėse bus rodomi tik įrašai pasirinktomis kalbomis. + date_of_birth: Turime įsitikinti, kad esate bent %{age}, kad naudotumėte „Mastodon“. Mes to neišsaugosime. role: Vaidmuo valdo, kokius leidimus naudotojas turi. labels: account: @@ -177,6 +184,7 @@ lt: name: Saitažodis filters: actions: + blur: Slėpti mediją su įspėjimu hide: Slėpti visiškai warn: Slėpti su įspėjimu form_admin_settings: @@ -187,6 +195,7 @@ lt: custom_css: Pasirinktinis CSS favicon: Svetainės piktograma mascot: Pasirinktinis talismanas (pasenęs) + min_age: Mažiausias amžiaus reikalavimas registrations_mode: Kas gali užsiregistruoti require_invite_text: Reikalauti priežasties prisijungti show_domain_blocks_rationale: Rodyti, kodėl domenai buvo užblokuoti @@ -237,7 +246,11 @@ lt: dmca_email: El. pašto adresas, skirtas DMCA / autorinių teisių pranešimams domain: Domenas jurisdiction: Teisinis teismingumas + min_age: Mažiausias amžius user: + date_of_birth_1i: Diena + date_of_birth_2i: Mėnuo + date_of_birth_3i: Metai role: Vaidmuo time_zone: Laiko juosta user_role: diff --git a/config/locales/simple_form.lv.yml b/config/locales/simple_form.lv.yml index d9fe043952..19e517340d 100644 --- a/config/locales/simple_form.lv.yml +++ b/config/locales/simple_form.lv.yml @@ -75,6 +75,7 @@ lv: filters: action: Izvēlies, kuru darbību veikt, ja ziņa atbilst filtram actions: + blur: Paslēpt informācijas nesējus aiz brīdinājuma, nepaslēpjot tekstu hide: Paslēp filtrēto saturu pilnībā, izturoties tā, it kā tas neeksistētu warn: Paslēp filtrēto saturu aiz brīdinājuma, kurā minēts filtra nosaukums form_admin_settings: @@ -88,6 +89,7 @@ lv: favicon: WEBP, PNG, GIF vai JPG. Aizstāj noklusējuma Mastodon favikonu ar pielāgotu. mascot: Ignorē ilustrāciju uzlabotajā tīmekļa saskarnē. media_cache_retention_period: Informācijas nesēju datnes no ierakstiem, kurus ir veikuši attālie lietotāji, tiek kešoti šajā serverī. Kad ir iestatīta apstiprinoša vērtība, informācijas nesēji tiks izdzēsti pēc norādītā dienu skaita. Ja informācijas nesēju dati tiks pieprasīti pēc tam, kad tie tika izdzēsti, tie tiks atkārtoti lejupielādēti, ja avota saturs joprojām būs pieejams. Saišu priekšskatījuma karšu vaicājumu biežuma ierobežojumu dēļ ir ieteicams iestatīt šo vērtību vismaz 14 dienas vai saišu priekšskatījuma kartes netiks atjauninātas pēc pieprasījuma pirms tā laika. + min_age: Lietotājiem tiks lūgts apstiprināt viņu dzimšanas datumu reģistrācijas laikā peers_api_enabled: Domēna vārdu saraksts, ar kuriem šis serveris ir saskāries fediversā. Šeit nav iekļauti dati par to, vai tu veic federāciju ar noteiktu serveri, tikai tavs serveris par to zina. To izmanto dienesti, kas apkopo statistiku par federāciju vispārīgā nozīmē. profile_directory: Profilu direktorijā ir uzskaitīti visi lietotāji, kuri ir izvēlējušies būt atklājami. require_invite_text: Ja nepieciešama pašrocīga apstiprināšana, lai pierakstītos, teksta “Kāpēc vēlies pievienoties?” ievade jāpadara par nepieciešamu, nevis izvēles @@ -140,6 +142,7 @@ lv: domain: Sniegtā tiešsaistas pakalpojuma neatkārtojama identifikācija. user: chosen_languages: Ja ieķeksēts, publiskos laika grafikos tiks parādītas tikai ziņas noteiktajās valodās + date_of_birth: Mums jāpārliecinās, ka jums ir vismaz %{age} gadi, lai varētu izmantot Mastodonu. Mēs neuzglabāsim šo informāciju. role: Loma nosaka, kādas lietotājam ir atļaujas. user_role: color: Krāsa, kas jāizmanto lomai visā lietotāja saskarnē, kā RGB hex formātā @@ -265,6 +268,7 @@ lv: favicon: Favikona mascot: Pielāgots talismans (mantots) media_cache_retention_period: Multivides kešatmiņas saglabāšanas periods + min_age: Nepieciešamais minimālais vecums peers_api_enabled: Publicēt API atklāto serveru sarakstu profile_directory: Iespējot profila direktoriju registrations_mode: Kurš drīkst pieteikties @@ -338,6 +342,9 @@ lv: domain: Domēna vārds min_age: Mazākais pieļaujamais vecums user: + date_of_birth_1i: Diena + date_of_birth_2i: Mēnesis + date_of_birth_3i: Gads role: Loma time_zone: Laika josla user_role: diff --git a/config/locales/simple_form.nl.yml b/config/locales/simple_form.nl.yml index b31ae4b5ea..6029698bd7 100644 --- a/config/locales/simple_form.nl.yml +++ b/config/locales/simple_form.nl.yml @@ -75,6 +75,7 @@ nl: filters: action: Kies welke acties uitgevoerd moeten wanneer een bericht overeenkomt met het filter actions: + blur: Media verbergen achter een waarschuwing, zonder de tekst zelf te verbergen hide: Verberg de gefilterde inhoud volledig, alsof het niet bestaat warn: Verberg de gefilterde inhoud achter een waarschuwing, met de titel van het filter als waarschuwingstekst form_admin_settings: @@ -88,6 +89,7 @@ nl: favicon: WEBP, PNG, GIF of JPG. Vervangt de standaard Mastodon favicon met een aangepast pictogram. mascot: Overschrijft de illustratie in de geavanceerde webomgeving. media_cache_retention_period: Mediabestanden van berichten van externe gebruikers worden op jouw server in de cache opgeslagen. Indien ingesteld op een positieve waarde, worden media verwijderd na het opgegeven aantal dagen. Als de mediagegevens worden opgevraagd nadat ze zijn verwijderd, worden ze opnieuw gedownload wanneer de originele inhoud nog steeds beschikbaar is. Vanwege beperkingen op hoe vaak linkvoorbeelden sites van derden raadplegen, wordt aanbevolen om deze waarde in te stellen op ten minste 14 dagen. Anders worden linkvoorbeelden niet op aanvraag bijgewerkt. + min_age: Gebruikers krijgen tijdens hun inschrijving de vraag om hun geboortedatum te bevestigen peers_api_enabled: Een lijst met domeinnamen die deze server heeft aangetroffen in de fediverse. Er zijn hier geen gegevens inbegrepen over de vraag of je verbonden bent met een bepaalde server, alleen dat je server er van weet. Dit wordt gebruikt door diensten die statistieken over de federatie in algemene zin verzamelen. profile_directory: De gebruikersgids bevat een lijst van alle gebruikers die ervoor gekozen hebben om ontdekt te kunnen worden. require_invite_text: Maak het invullen van "Waarom wil je je hier registreren?" verplicht in plaats van optioneel, wanneer registraties handmatig moeten worden goedgekeurd @@ -146,6 +148,7 @@ nl: min_age: Mag niet lager zijn dan de minimale vereiste leeftijd volgens de wetten van jouw jurisdictie. user: chosen_languages: Alleen berichten in de aangevinkte talen worden op de openbare tijdlijnen getoond + date_of_birth: We moeten ervoor zorgen dat je tenminste %{age} bent om Mastodon te gebruiken. Dit wordt niet opgeslagen. role: De rol bepaalt welke rechten de gebruiker heeft. user_role: color: Kleur die gebruikt wordt voor de rol in de UI, als RGB in hexadecimale formaat @@ -258,6 +261,7 @@ nl: name: Hashtag filters: actions: + blur: Media met een waarschuwing verbergen hide: Volledig verbergen warn: Met een waarschuwing verbergen form_admin_settings: @@ -271,6 +275,7 @@ nl: favicon: Favicon mascot: Aangepaste mascotte (legacy) media_cache_retention_period: Bewaartermijn mediacache + min_age: Vereiste minimumleeftijd peers_api_enabled: Lijst van bekende servers via de API publiceren profile_directory: Gebruikersgids inschakelen registrations_mode: Wie kan zich registreren @@ -349,6 +354,9 @@ nl: jurisdiction: Jurisdictie min_age: Minimumleeftijd user: + date_of_birth_1i: Dag + date_of_birth_2i: Maand + date_of_birth_3i: Jaar role: Rol time_zone: Tijdzone user_role: diff --git a/config/locales/simple_form.nn.yml b/config/locales/simple_form.nn.yml index 1463d8582d..1a33a4b91d 100644 --- a/config/locales/simple_form.nn.yml +++ b/config/locales/simple_form.nn.yml @@ -75,6 +75,7 @@ nn: filters: action: Velg kva som skal gjerast når eit innlegg samsvarar med filteret actions: + blur: Gøym media bak ei åtvaring utan å gøyme sjølve teksten hide: Skjul filtrert innhald fullstendig og lat som om det ikkje finst warn: Skjul det filtrerte innhaldet bak ei åtvaring som nemner tittelen på filteret form_admin_settings: @@ -256,6 +257,7 @@ nn: name: Emneknagg filters: actions: + blur: Gøym media med ei åtvaring hide: Gøym heilt warn: Gøym med ei åtvaring form_admin_settings: diff --git a/config/locales/simple_form.pt-PT.yml b/config/locales/simple_form.pt-PT.yml index ac8fd8689b..48817a60d5 100644 --- a/config/locales/simple_form.pt-PT.yml +++ b/config/locales/simple_form.pt-PT.yml @@ -88,6 +88,7 @@ pt-PT: favicon: WEBP, PNG, GIF ou JPG. Substitui o ícone de favorito padrão do Mastodon por um ícone personalizado. mascot: Sobrepõe-se à ilustração na interface web avançada. media_cache_retention_period: Os ficheiros multimédia de publicações feitas por utilizadores remotos são armazenados em cache no seu servidor. Quando definido para um valor positivo, os ficheiros multimédia serão eliminados após o número de dias especificado. Se os ficheiros multimédia forem solicitados depois de terem sido eliminados, serão transferidos novamente, se o conteúdo de origem ainda estiver disponível. Devido a restrições sobre a frequência com que os cartões de pré-visualização de links pesquisam sites de terceiros, recomenda-se que este valor seja definido para, pelo menos, 14 dias, ou os cartões de pré-visualização de links não serão atualizados a pedido antes desse período. + min_age: Os utilizadores serão convidados a confirmar a sua data de nascimento durante o processo de inscrição peers_api_enabled: Uma lista de nomes de domínio que este servidor encontrou no fediverso. Nenhum dado é incluído aqui sobre se você federa com um determinado servidor, apenas que o seu servidor o conhece. Este serviço é utilizado por serviços que recolhem estatísticas na federação, em termos gerais. profile_directory: O diretório de perfis lista todos os utilizadores que optaram por ter a sua conta a ser sugerida a outros. require_invite_text: Quando as incrições exigirem aprovação manual, faça o texto "Por que se quer juntar a nós?" da solicitação de convite ser obrigatório, em vez de opcional @@ -146,6 +147,7 @@ pt-PT: min_age: Não deve ter menos do que a idade mínima exigida pela legislação da sua jurisdição. user: chosen_languages: Quando selecionado, só serão mostradas nas cronologias públicas as publicações nos idiomas escolhidos + date_of_birth: Temos de ter a certeza de que tens pelo menos %{age} para usar o Mastodon. Não vamos guardar esta informação. role: A função controla as permissões que o utilizador tem. user_role: color: Cor a ser utilizada para a função em toda a interface de utilizador, como RGB no formato hexadecimal @@ -271,6 +273,7 @@ pt-PT: favicon: Ícone de favoritos mascot: Mascote personalizada (legado) media_cache_retention_period: Período de retenção de ficheiros multimédia em cache + min_age: Idade mínima requerida peers_api_enabled: Publicar lista de servidores descobertos na API profile_directory: Ativar o diretório de perfis registrations_mode: Quem se pode inscrever @@ -349,6 +352,9 @@ pt-PT: jurisdiction: Jurisdição legal min_age: Idade mínima user: + date_of_birth_1i: Dia + date_of_birth_2i: Mês + date_of_birth_3i: Ano role: Função time_zone: Fuso horário user_role: diff --git a/config/locales/simple_form.ru.yml b/config/locales/simple_form.ru.yml index 366335208e..0b8cbe0561 100644 --- a/config/locales/simple_form.ru.yml +++ b/config/locales/simple_form.ru.yml @@ -88,6 +88,7 @@ ru: favicon: WEBP, PNG, GIF или JPG. Заменяет стандартный фавикон Mastodon на собственный значок. mascot: Заменяет иллюстрацию в расширенном веб-интерфейсе. media_cache_retention_period: Медиафайлы из сообщений, сделанных удаленными пользователями, кэшируются на вашем сервере. При положительном значении медиафайлы будут удалены через указанное количество дней. Если медиаданные будут запрошены после удаления, они будут загружены повторно, если исходный контент все еще доступен. В связи с ограничениями на частоту опроса карточек предварительного просмотра ссылок на сторонних сайтах рекомендуется устанавливать значение не менее 14 дней, иначе карточки предварительного просмотра ссылок не будут обновляться по запросу до этого времени. + min_age: Пользователям при регистрации будет предложено ввести свою дату рождения peers_api_enabled: Список доменных имен, с которыми сервер столкнулся в fediverse. Здесь нет данных о том, федерировались ли вы с данным сервером, только что ваш сервер знает об этом. Это используется службами, которые собирают статистику по федерации в общем смысле. profile_directory: В каталоге профилей перечислены все пользователи, которые согласились быть доступными для обнаружения. require_invite_text: Когда регистрация требует ручного одобрения, сделайте текстовый ввод "Почему вы хотите присоединиться?" обязательным, а не опциональным @@ -132,15 +133,21 @@ ru: name: Вы можете изменить только регистр букв чтобы, например, сделать тег более читаемым terms_of_service: changelog: Можно использовать синтаксис языка разметки Markdown. + effective_date: Разумные временные рамки могут варьироваться в диапазоне от 10 до 30 дней после уведомления пользователей. text: Можно использовать синтаксис языка разметки Markdown. terms_of_service_generator: admin_email: Юридические уведомления включают в себя встречные уведомления, постановления суда, запросы на удаление и запросы правоохранительных органов. - choice_of_law: Город, регион, территория или государственное материальное право, которое регулирует любые претензии и все их требования. + arbitration_address: Может совпадать с почтовым адресом, указанным выше, либо «N/A» в случае электронной почты. + arbitration_website: Веб-форма или «N/A» в случае электронной почты. + choice_of_law: Город, регион, территория или государство, внутреннее материальное право которого регулирует любые претензии. dmca_address: Находящиеся в США операторы должны использовать адрес, зарегистрированный в DMCA Designated Agent Directory. Использовать абонентский ящик возможно при обращении в соответствующей просьбой, для чего нужно с помощью DMCA Designated Agent Post Office Box Waiver Request написать сообщение в Copyright Office и объяснить, что вы занимаетесь модерацией контента из дома и опасаетесь мести за свои действия, поэтому должны использовать абонентский ящик, чтобы убрать ваш домашний адрес из общего доступа. + dmca_email: Может совпадать с адресом электронной почты для юридических уведомлений, указанным выше. domain: Имя, позволяющее уникально идентифицировать ваш онлайн-ресурс. jurisdiction: Впишите страну, где находится лицо, оплачивающее счета. Если это компания либо организация, впишите страну инкорпорации, включая город, регион, территорию или штат, если это необходимо. + min_age: Не меньше минимального возраста, требуемого по закону в вашей юрисдикции. user: chosen_languages: Если выбрано, то в публичных лентах будут показаны только посты на выбранных языках. + date_of_birth: Нужно убедиться, что вам не меньше %{age} лет. Мы не храним введённые здесь данные. role: Роль определяет, какими правами обладает пользователь. user_role: color: Цвет, который будет использоваться для роли в интерфейсе (UI), как RGB в формате HEX @@ -266,6 +273,7 @@ ru: favicon: Favicon mascot: Пользовательский маскот (устаревшее) media_cache_retention_period: Период хранения кэша медиафайлов + min_age: Требование минимального возраста peers_api_enabled: Публикация списка обнаруженных узлов в API profile_directory: Включить каталог профилей registrations_mode: Кто может зарегистрироваться @@ -331,17 +339,22 @@ ru: usable: Позволить этот хэштег в локальных сообщениях terms_of_service: changelog: Что изменилось? + effective_date: Дата вступления в силу text: Пользовательское соглашение terms_of_service_generator: admin_email: Адрес электронной почты для юридических уведомлений arbitration_address: Почтовый адрес для уведомлений об арбитраже arbitration_website: Вебсайт для подачи уведомления об арбитраже - choice_of_law: Выбор закана. + choice_of_law: Юрисдикция dmca_address: Почтовый адрес для обращений правообладателей dmca_email: Адрес электронной почты для обращений правообладателей domain: Доменное имя jurisdiction: Юрисдикция + min_age: Минимальный возраст user: + date_of_birth_1i: День + date_of_birth_2i: Месяц + date_of_birth_3i: Год role: Роль time_zone: Часовой пояс user_role: diff --git a/config/locales/simple_form.sl.yml b/config/locales/simple_form.sl.yml index 4562b7004b..5d55844aa9 100644 --- a/config/locales/simple_form.sl.yml +++ b/config/locales/simple_form.sl.yml @@ -88,6 +88,7 @@ sl: favicon: WEBP, PNG, GIF ali JPG. Zamenja privzeto ikono spletne strani Mastodon z ikono po meri. mascot: Preglasi ilustracijo v naprednem spletnem vmesniku. media_cache_retention_period: Predstavnostne datoteke iz objav uporabnikov na ostalih strežnikih se začasno hranijo na tem strežniku. Ko je nastavljeno na pozitivno vrednost, bodo predstavnostne datoteke izbrisane po nastavljenem številu dni. Če bo predstavnostna datoteka zahtevana po izbrisu, bo ponovno prenešena, če bo vir še vedno na voljo. Zaradi omejitev pogostosti prejemanja predogledov povezav z drugih strani je priporočljivo to vrednost nastaviti na vsaj 14 dni. V nasprotnem predogledi povezav pred tem časom ne bodo osveženi na zahtevo. + min_age: Med registracijo bodo morali uporabniki potrditi svoj datum rojstva peers_api_enabled: Seznam imen domen, na katere je ta strežnik naletel v fediverzumu. Sem niso vključeni podatki o tem, ali ste v federaciji z danim strežnikom, zgolj to, ali vaš strežnik ve zanj. To uporabljajo storitve, ki zbirajo statistične podatke o federaciji v splošnem smislu. profile_directory: Imenik profilov izpiše vse uporabnike, ki so dovolili, da so v njem navedeni. require_invite_text: Če registracije zahtevajo ročno potrditev, nastavite vnos besedila pod »Zakaj se želite pridružiti?« za obveznega. @@ -145,6 +146,7 @@ sl: min_age: Ne smete biti mlajši od starostne omejitve, ki jo postavljajo zakoni vašega pravosodnega sistema. user: chosen_languages: Ko je označeno, bodo v javnih časovnicah prikazane samo objave v izbranih jezikih + date_of_birth: Prepričati se moramo, da so uporabniki Mastodona stari vsaj %{age} let. Tega podatka ne bomo shranili. role: Vloga določa, katera dovoljenja ima uporabnik. user_role: color: Barva, uporabljena za vlogo po celem up. vmesniku, podana v šestnajstiškem zapisu RGB @@ -270,6 +272,7 @@ sl: favicon: Ikona spletne strani mascot: Maskota po meri (opuščeno) media_cache_retention_period: Obdobje hrambe predpomnilnika predstavnosti + min_age: Spodnja starostna meja peers_api_enabled: Objavi seznam odkritih strežnikov v API-ju profile_directory: Omogoči imenik profilov registrations_mode: Kdo se lahko registrira @@ -341,12 +344,16 @@ sl: admin_email: E-poštni naslov za pravna obvestila arbitration_address: Fizični naslov za arbitražna obvestila arbitration_website: Spletišče za vložitev arbitražnih obvestil + choice_of_law: Izbira prava dmca_address: Fizični naslov za obvestila DMCA ali o avtorskih pravicah dmca_email: E-poštni naslov za obvestila DMCA ali o avtorskih pravicah domain: Domena jurisdiction: Pravna pristojnost min_age: Najmanjša starost user: + date_of_birth_1i: Dan + date_of_birth_2i: Mesec + date_of_birth_3i: Leto role: Vloga time_zone: Časovni pas user_role: diff --git a/config/locales/simple_form.sq.yml b/config/locales/simple_form.sq.yml index 47c0e18c5c..9f5ada184c 100644 --- a/config/locales/simple_form.sq.yml +++ b/config/locales/simple_form.sq.yml @@ -75,6 +75,7 @@ sq: filters: action: Zgjidhni cili veprim të kryhet, kur një postim ka përputhje me një filtër actions: + blur: Fshihe median pas një sinjalizimi, pa fshehur vetë tekstin hide: Fshihe plotësisht lëndën e filtruar, duke u sjellë sikur të mos ekzistonte warn: Fshihe lëndën e filtruar pas një sinjalizimi që përmend titullin e filtrit form_admin_settings: @@ -88,6 +89,7 @@ sq: favicon: WEBP, PNG, GIF, ose JPG. Anashkalon favikonën parazgjedhje Mastodon me një ikonë vetjake. mascot: Anashkalon ilustrimin te ndërfaqja web e thelluar. media_cache_retention_period: Kartela media nga postime të bëra nga përdorues të largët ruhen në një fshehtinë në shërbyesin tuaj. Kur i jepet një vlerë pozitive, media do të fshihet pas numrit të dhënë të ditëve. Nëse të dhënat e medias duhen pas fshirjes, do të rishkarkohen, nëse lënda burim mund të kihet ende. Për shkak kufizimesh mbi sa shpesh skeda paraparjesh lidhjesh ndërveprojnë me sajte palësh të treta, rekomandohet të vihet kjo vlerë të paktën 14 ditë, ose skedat e paraparjes së lidhje s’do të përditësohen duke e kërkuar para asaj kohe. + min_age: Përdoruesve do t’ju kërkohet gjatë regjistrimit të ripohojnë datën e lindjes peers_api_enabled: Një listë emrash përkatësish që ky shërbyes ka hasur në fedivers. Këtu s’jepen të dhëna nëse jeni i federuar me shërbyesin e dhënë, thjesht tregohet se shërbyesi juaj e njeh. Kjo përdoret nga shërbime që mbledhin statistika mbi federimin në kuptimin e përgjithshëm. profile_directory: Drejtoria e profileve paraqet krejt përdoruesit që kanë zgjedhur të jenë të zbulueshëm. require_invite_text: Kur regjistrimet lypin miratim dorazi, bëje tekstin “Përse doni të bëheni pjesë?” të detyrueshëm, në vend se opsional @@ -145,6 +147,7 @@ sq: min_age: S’duhet të jetë nën moshën minimum të domosdoshme nga ligjet në juridiksionin tuaj. user: chosen_languages: Në iu vëntë shenjë, te rrjedha kohore publike do të shfaqen vetëm mesazhe në gjuhët e përzgjedhura + date_of_birth: Na duhet të sigurohemi se jeni të paktën %{age}, që të përdorni Mastodon-in. Këtë s’e depozitojmë. role: Roli kontrollon cilat leje ka përdoruesi. user_role: color: Ngjyrë për t’u përdorur për rolin nëpër UI, si RGB në format gjashtëmbëdhjetësh @@ -257,6 +260,7 @@ sq: name: Hashtag filters: actions: + blur: Fshihe median me një sinjalizim hide: Fshihe plotësisht warn: Fshihe me një sinjalizim form_admin_settings: @@ -270,6 +274,7 @@ sq: favicon: Favikonë mascot: Simbol vetjak (e dikurshme) media_cache_retention_period: Periudhë mbajtjeje lënde media + min_age: Domosdosmëri moshe minimum peers_api_enabled: Publiko te API listë shërbyesish të zbuluar profile_directory: Aktivizo drejtori profilesh registrations_mode: Kush mund të regjistrohet @@ -348,6 +353,9 @@ sq: jurisdiction: Juridiksion ligjor min_age: Mosha minimale user: + date_of_birth_1i: Ditë + date_of_birth_2i: Muaj + date_of_birth_3i: Vit role: Rol time_zone: Zonë kohore user_role: diff --git a/config/locales/simple_form.sv.yml b/config/locales/simple_form.sv.yml index e5503a5d12..41ac513f39 100644 --- a/config/locales/simple_form.sv.yml +++ b/config/locales/simple_form.sv.yml @@ -331,7 +331,11 @@ sv: dmca_address: Fysisk adress för meddelanden om DMCA/upphovsrätt dmca_email: Fysisk adress för meddelanden om DMCA/upphovsrätt domain: Domän + min_age: Minimiålder user: + date_of_birth_1i: Dag + date_of_birth_2i: Månad + date_of_birth_3i: År role: Roll time_zone: Tidszon user_role: diff --git a/config/locales/simple_form.tr.yml b/config/locales/simple_form.tr.yml index 9667b2b658..22300ccec3 100644 --- a/config/locales/simple_form.tr.yml +++ b/config/locales/simple_form.tr.yml @@ -75,6 +75,7 @@ tr: filters: action: Bir gönderi filtreyle eşleştiğinde hangi eylemin yapılacağını seçin actions: + blur: Medyayı, metnin kendisini gizlemeden bir uyarı arkasında gizle hide: Filtrelenmiş içeriği tamamen gizle, sanki varolmamış gibi warn: Süzgeçlenmiş içeriği, süzgecinin başlığından söz eden bir uyarının arkasında gizle form_admin_settings: @@ -88,6 +89,7 @@ tr: favicon: WEBP, PNG, GIF veya JPG. Varsayılan Mastodon simgesini isteğe bağlı bir simgeyle değiştirir. mascot: Gelişmiş web arayüzündeki illüstrasyonu geçersiz kılar. media_cache_retention_period: Uzak kullanıcıların gönderilerindeki ortam dosyaları sunucunuzda önbelleklenir. Pozitif bir değer verildiğinde, ortam dosyaları belirlenen gün sonunda silinecektir. Eğer ortam dosyaları silindikten sonra istenirse, kaynak içerik hala mevcutsa, tekrar indirilecektir. Bağlantı önizleme kartlarının üçüncü parti siteleri yoklamasına ilişkin kısıtlamalar nedeniyle, bu değeri en azından 14 gün olarak ayarlamanız önerilir, yoksa bağlantı önizleme kartları bu süreden önce isteğe bağlı olarak güncellenmeyecektir. + min_age: Kullanıcılardan kayıt olurken doğum tarihlerini doğrulamaları istenecektir peers_api_enabled: Bu sunucunun fediverse'te karşılaştığı alan adlarının bir listesi. İlgili sunucuyla birleştirme mi yapıyorsunuz yoksa sunucunuz sadece onu biliyor mu hakkında bir bilgi burada yok. Bu blgi genel olarak federasyın hakkında istatistik toplamak isteyen hizmetler tarafından kullanılıyor. profile_directory: Profil dizini keşfedilebilir olmayı kabul eden tüm kullanıcıları listeler. require_invite_text: Kayıt olmak elle doğrulama gerektiriyorsa, "Neden katılmak istiyorsunuz?" metin girdisini isteğe bağlı yerine zorunlu yapın @@ -146,6 +148,7 @@ tr: min_age: Tabi olduğunuz yasaların gerektirdiği yaştan düşük olmamalıdır. user: chosen_languages: İşaretlendiğinde, yalnızca seçilen dillerdeki gönderiler genel zaman çizelgelerinde görüntülenir + date_of_birth: Mastodon kullanmak için en az %{age} yaşında olduğunuzdan emin olmalıyız. Bu bilgiyi saklamıyoruz. role: Rol, kullanıcıların sahip olduğu izinleri denetler. user_role: color: Arayüz boyunca rol için kullanılacak olan renk, hex biçiminde RGB @@ -258,6 +261,7 @@ tr: name: Etiket filters: actions: + blur: Medyayı bir uyarıyla gizle hide: Tamamen gizle warn: Uyarıyla gizle form_admin_settings: @@ -271,6 +275,7 @@ tr: favicon: Yer imi simgesi mascot: Özel maskot (eski) media_cache_retention_period: Medya önbelleği saklama süresi + min_age: Azami yaş gereksinimi peers_api_enabled: API'de keşfedilen sunucuların listesini yayınla profile_directory: Profil dizinini etkinleştir registrations_mode: Kim kaydolabilir @@ -349,6 +354,9 @@ tr: jurisdiction: Yasal yetki alanı min_age: Minimum yaş user: + date_of_birth_1i: Gün + date_of_birth_2i: Ay + date_of_birth_3i: Yıl role: Rol time_zone: Zaman dilimi user_role: diff --git a/config/locales/simple_form.uk.yml b/config/locales/simple_form.uk.yml index 7d77aa813f..b43aaeb234 100644 --- a/config/locales/simple_form.uk.yml +++ b/config/locales/simple_form.uk.yml @@ -75,6 +75,7 @@ uk: filters: action: Виберіть дію для виконання коли допис збігається з фільтром actions: + blur: Приховати медіа за попередженням, не приховуючи сам текст hide: Повністю сховати фільтрований вміст, ніби його не існує warn: Сховати відфільтрований вміст за попередженням, у якому вказано заголовок фільтра form_admin_settings: @@ -88,6 +89,7 @@ uk: favicon: WEBP, PNG, GIF або JPG. Замінює стандартну піктограму Mastodon на власну. mascot: Змінює ілюстрацію в розширеному вебінтерфейсі. media_cache_retention_period: Медіафайли з дописів віддалених користувачів кешуються на вашому сервері. Якщо встановлено додатне значення, медіа буде видалено через вказану кількість днів. Якщо медіадані будуть запитані після видалення, вони будуть завантажені повторно, якщо вихідний вміст все ще доступний. Через обмеження на частоту опитування карток попереднього перегляду посилань на сторонніх сайтах, рекомендується встановити це значення не менше 14 днів, інакше картки попереднього перегляду посилань не будуть оновлюватися на вимогу раніше цього часу. + min_age: Користувачам буде запропоновано підтвердити дату народження під час реєстрації peers_api_enabled: Список доменів імен цього сервера з'явився у федівсесвіті. Сюди не входять дані чи ви пов'язані федерацією з цим сервером, а лише відомості, що вашому серверу відомо про нього. Його використовують служби, які збирають загальну статистику про федерації. profile_directory: У каталозі профілів перераховані всі користувачі, які погодились бути видимими. require_invite_text: Якщо реєстрація вимагає власноручного затвердження, зробіть текстове поле «Чому ви хочете приєднатися?» обов'язковим, а не додатковим @@ -145,6 +147,7 @@ uk: min_age: Не повинно бути нижче мінімального віку, необхідного законодавством вашої юрисдикції. user: chosen_languages: У глобальних стрічках будуть показані дописи тільки вибраними мовами + date_of_birth: Ми повинні переконатися, що вам принаймні %{age}, щоб використовувати Mastodon. Ми не будемо зберігати це. role: Роль визначає, які права має користувач. user_role: color: Колір, який буде використовуватися для ролі у всьому інтерфейсі, як RGB у форматі hex @@ -256,6 +259,7 @@ uk: name: Хештеґ filters: actions: + blur: Приховати медіа з попередженням hide: Сховати повністю warn: Сховати за попередженням form_admin_settings: @@ -269,6 +273,7 @@ uk: favicon: Піктограма сайту mascot: Користувацький символ (застарілий) media_cache_retention_period: Період збереження кешу медіа + min_age: Мінімальна вимога по віку peers_api_enabled: Опублікувати список знайдених серверів у API profile_directory: Увімкнути каталог профілів registrations_mode: Хто може зареєструватися @@ -346,6 +351,9 @@ uk: jurisdiction: Правова юрисдикція min_age: Мінімальний вік user: + date_of_birth_1i: День + date_of_birth_2i: Місяць + date_of_birth_3i: Рік role: Роль time_zone: Часовий пояс user_role: diff --git a/config/locales/simple_form.vi.yml b/config/locales/simple_form.vi.yml index 4d182c56c2..8b78787f86 100644 --- a/config/locales/simple_form.vi.yml +++ b/config/locales/simple_form.vi.yml @@ -75,6 +75,7 @@ vi: filters: action: Chọn hành động sẽ thực hiện khi một tút khớp với bộ lọc actions: + blur: Ẩn sau một cảnh báo, mà không ảnh hưởng nội dung hide: Ẩn hoàn toàn, như thể nó không tồn tại warn: Hiện cảnh báo và bộ lọc form_admin_settings: @@ -88,6 +89,7 @@ vi: favicon: WEBP, PNG, GIF hoặc JPG. Dùng favicon Maston tùy chỉnh. mascot: Ghi đè hình minh họa trong giao diện web nâng cao. media_cache_retention_period: Các tệp phương tiện từ các tút do người dùng máy chủ khác thực hiện sẽ được lưu vào bộ đệm trên máy chủ của bạn. Khi được đặt thành giá trị dương, phương tiện sẽ bị xóa sau số ngày được chỉ định. Nếu dữ liệu phương tiện được yêu cầu sau khi bị xóa, dữ liệu đó sẽ được tải xuống lại nếu nội dung nguồn vẫn còn. Do những hạn chế về tần suất thẻ xem trước liên kết thăm dò ý kiến các trang web của bên thứ ba, bạn nên đặt giá trị này thành ít nhất 14 ngày, nếu không thẻ xem trước liên kết sẽ không được cập nhật theo yêu cầu trước thời gian đó. + min_age: Thành viên sẽ được yêu cầu xác nhận ngày sinh của họ trong quá trình đăng ký peers_api_enabled: Danh sách các máy chủ khác mà máy chủ này đã liên hợp. Không có dữ liệu nào được đưa vào đây về việc bạn có liên kết với một máy chủ nhất định hay không, chỉ là máy chủ của bạn biết về nó. Điều này được sử dụng bởi các dịch vụ thu thập số liệu thống kê về liên kết theo nghĩa chung. profile_directory: Liệt kê tất cả người đã chọn tham gia để có thể khám phá. require_invite_text: Khi đăng ký yêu cầu phê duyệt thủ công, hãy đặt câu hỏi "Tại sao bạn muốn tham gia?" nhập văn bản bắt buộc thay vì tùy chọn @@ -146,6 +148,7 @@ vi: min_age: Không được dưới độ tuổi tối thiểu theo quy định của luật pháp tại khu vực của bạn. user: chosen_languages: Chỉ hiển thị những tút viết bằng các ngôn ngữ sau + date_of_birth: Chúng tôi phải đảm bảo rằng bạn ít nhất %{age} tuổi để sử dụng Mastodon. Chúng tôi không lưu trữ thông tin này. role: Vai trò kiểm soát những quyền mà người dùng có. user_role: color: Màu được sử dụng cho vai trò trong toàn bộ giao diện người dùng, dưới dạng RGB ở định dạng hex @@ -258,6 +261,7 @@ vi: name: Hashtag filters: actions: + blur: Ẩn kèm theo cảnh báo hide: Ẩn toàn bộ warn: Ẩn kèm theo cảnh báo form_admin_settings: @@ -271,6 +275,7 @@ vi: favicon: Favicon mascot: Tùy chỉnh linh vật (kế thừa) media_cache_retention_period: Thời hạn lưu trữ cache media + min_age: Độ tuổi tối thiểu peers_api_enabled: Công khai danh sách các máy chủ được phát hiện trong API profile_directory: Cho phép hiện danh sách thành viên registrations_mode: Ai có thể đăng ký @@ -349,6 +354,9 @@ vi: jurisdiction: Quyền tài phán pháp lý min_age: Độ tuổi tối thiểu user: + date_of_birth_1i: Ngày + date_of_birth_2i: Tháng + date_of_birth_3i: Năm role: Vai trò time_zone: Múi giờ user_role: diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml index 599c3de084..5fd28497af 100644 --- a/config/locales/simple_form.zh-CN.yml +++ b/config/locales/simple_form.zh-CN.yml @@ -88,6 +88,7 @@ zh-CN: favicon: WEBP、PNG、GIF 或 JPG。使用自定义图标覆盖 Mastodon 的默认图标。 mascot: 覆盖高级网页界面中的绘图形象。 media_cache_retention_period: 来自外站用户嘟文的媒体文件将被缓存到你的实例上。当该值被设为正值时,缓存的媒体文件将在指定天数后被清除。如果媒体文件在被清除后重新被请求,且源站内容仍然可用,它将被重新下载。由于链接预览卡拉取第三方站点的频率受到限制,建议将此值设置为至少 14 天,如果小于该值,链接预览卡将不会按需更新。 + min_age: 用户注册时必须确认出生日期 peers_api_enabled: 本站在联邦宇宙中遇到的站点列表。 此处不包含关于您是否与给定站点联合的数据,只是您的实例知道它。 这由收集一般意义上的联合统计信息的服务使用。 profile_directory: 个人资料目录会列出所有选择可被发现的用户。 require_invite_text: 当注册需要手动批准时,将“你为什么想要加入?”设为必填项 @@ -132,16 +133,21 @@ zh-CN: name: 你只能改变字母的大小写,让它更易读 terms_of_service: changelog: 可以使用 Markdown 语法。 + effective_date: 合理的时间范围可以是从您通知用户之日起 10 到 30 天。 text: 可以使用 Markdown 语法。 terms_of_service_generator: admin_email: 法务通知包括反通知、法院命令、内容下架要求与执法机关的要求。 + arbitration_address: 可以与上面的实际地址相同,如果使用电子邮件则为“N/A”。 + arbitration_website: 可以是网页表单,如果使用电子邮件则为“N/A”。 choice_of_law: 适用内部实质法律以管辖任何及所有索赔的城市、地区、领土或州。 dmca_address: 如果你是位于美国的运营者,请使用在 DMCA 指定代表名录中注册的地址。如果你需要使用邮政信箱,可以直接申请。请使用 DMCA 指定代表邮政信箱豁免申请表,通过电子邮件联系版权办公室,并声明你是居家内容审核员,因担心审核操作会招致报复或打击报复,需要使用邮政信箱以避免公开家庭住址。 + dmca_email: 可以与上面“法律声明的电子邮件地址”使用相同的电子邮件地址。 domain: 你所提供的在线服务的唯一标识。 jurisdiction: 请列出支付运营费用者所在的国家/地区。如果为公司或其他实体,请列出其注册的国家/地区以及相应的城市、地区、领地或州。 min_age: 不应低于您所在地法律管辖权要求的最低年龄。 user: chosen_languages: 仅选中语言的嘟文会出现在公共时间线上(全不选则显示所有语言的嘟文) + date_of_birth: 我们必须确认%{age}岁以上的用户才能使用Mastodon。我们不会存储该信息。 role: 角色用于控制用户拥有的权限。 user_role: color: 在界面各处用于标记该角色的颜色,以十六进制 RGB 格式表示 @@ -254,6 +260,7 @@ zh-CN: name: 话题 filters: actions: + blur: 隐藏媒体并显示警告 hide: 完全隐藏 warn: 隐藏时显示警告 form_admin_settings: @@ -267,6 +274,7 @@ zh-CN: favicon: Favicon mascot: 自定义吉祥物(旧) media_cache_retention_period: 媒体缓存保留期 + min_age: 最低年龄要求 peers_api_enabled: 在API中公开的已知实例的服务器的列表 profile_directory: 启用用户目录 registrations_mode: 谁可以注册 @@ -345,6 +353,9 @@ zh-CN: jurisdiction: 法律管辖区 min_age: 最低年龄 user: + date_of_birth_1i: 日 + date_of_birth_2i: 月 + date_of_birth_3i: 年 role: 角色 time_zone: 时区 user_role: diff --git a/config/locales/simple_form.zh-TW.yml b/config/locales/simple_form.zh-TW.yml index 47adb05010..fc86c77b74 100644 --- a/config/locales/simple_form.zh-TW.yml +++ b/config/locales/simple_form.zh-TW.yml @@ -75,6 +75,7 @@ zh-TW: filters: action: 請選擇當嘟文符合該過濾器時將被執行之動作 actions: + blur: 將多媒體隱藏於警告之後,而不隱藏文字內容 hide: 完全隱藏過濾內容,當作它似乎不曾存在過 warn: 隱藏過濾內容於過濾器標題之警告後 form_admin_settings: @@ -88,6 +89,7 @@ zh-TW: favicon: WEBP、PNG、GIF、或 JPG。使用自訂圖示替代預設 Mastodon favicon 圖示。 mascot: 覆寫進階網頁介面中的圖例。 media_cache_retention_period: 來自遠端伺服器嘟文中之多媒體內容將快取於您的伺服器。當設定為正值時,這些多媒體內容將於指定之天數後自您的儲存空間中自動刪除。若多媒體資料於刪除後被請求,且原始內容仍可存取,它們將被重新下載。由於連結預覽中第三方網站查詢頻率限制,建議將其設定為至少 14 日,否則於此之前連結預覽將不被即時更新。 + min_age: 使用者將於註冊時被要求確認他們的生日 peers_api_enabled: 浩瀚聯邦宇宙中與此伺服器曾經擦肩而過的網域列表。不包含關於您是否與此伺服器是否有與之串連,僅僅表示您的伺服器已知此網域。這是供收集聯邦宇宙中一般性統計資料服務使用。 profile_directory: 個人檔案目錄將會列出那些有選擇被發現的使用者。 require_invite_text: 如果已設定為手動審核註冊,請將「為什麼想要加入呢?」設定為必填項目。 @@ -146,6 +148,7 @@ zh-TW: min_age: 不應低於您所屬法律管轄區要求之最低年齡。 user: chosen_languages: 當選取時,只有選取語言之嘟文會於公開時間軸中顯示 + date_of_birth: 我們必須確認您至少年滿 %{age} 以使用 Mastodon。我們不會儲存此資料。 role: 角色控制使用者有哪些權限。 user_role: color: 於整個使用者介面中用於角色的顏色,十六進位格式的 RGB @@ -258,6 +261,7 @@ zh-TW: name: "「#」主題標籤" filters: actions: + blur: 將多媒體隱藏於警告之後 hide: 完全隱藏 warn: 隱藏於警告之後 form_admin_settings: @@ -271,6 +275,7 @@ zh-TW: favicon: 網站圖示 (Favicon) mascot: 自訂吉祥物 (legacy) media_cache_retention_period: 多媒體快取資料保留期間 + min_age: 最低年齡要求 peers_api_enabled: 於 API 中公開已知伺服器的列表 profile_directory: 啟用個人檔案目錄 registrations_mode: 誰能註冊 @@ -349,6 +354,9 @@ zh-TW: jurisdiction: 司法管轄區 min_age: 最低年齡 user: + date_of_birth_1i: 日 + date_of_birth_2i: 月 + date_of_birth_3i: 年 role: 角色 time_zone: 時區 user_role: diff --git a/config/locales/th.yml b/config/locales/th.yml index 2f8d03eec4..9b7ae7897d 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -1089,7 +1089,7 @@ th: salutation: "%{name}," settings: 'เปลี่ยนการกำหนดลักษณะอีเมล: %{link}' unsubscribe: เลิกบอกรับ - view: 'มุมมอง:' + view: 'ดู:' view_profile: ดูโปรไฟล์ view_status: ดูโพสต์ applications: diff --git a/config/locales/tok.yml b/config/locales/tok.yml index 2cb8429ac4..48b5406147 100644 --- a/config/locales/tok.yml +++ b/config/locales/tok.yml @@ -27,9 +27,14 @@ tok: accounts: approve: o wile are_you_sure: ni li pona ala pona? + avatar: sitelen delete: o ala e sona deleted: jan li ala e ni demote: o lili e ken + edit: ante toki + invite_request_text: nasin kama + ip: nanpa IP + joined: tenpo kama search: o alasa tags: search: o alasa diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 12b3aa44d4..5542975143 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -315,6 +315,9 @@ zh-CN: new: create: 创建公告 title: 新公告 + preview: + explanation_html: 此电子邮件将发送给 <strong>%{display_count} 用户</strong>。电子邮件将包含以下文本: + title: 预览公告通知 publish: 发布 published_msg: 公告已发布! scheduled_for: 定时在 %{time} @@ -1862,6 +1865,10 @@ zh-CN: recovery_instructions_html: 如果你的手机无法使用,你可以使用下列任意一个恢复代码来重新获得对账号的访问权。<strong>请妥善保管好你的恢复代码</strong>(例如,你可以将它们打印出来,然后和其他重要的文件放在一起)。 webauthn: 安全密钥 user_mailer: + announcement_published: + description: "%{domain}管理员发布了一则公告:" + subject: 服务公告 + title: "%{domain}服务公告" appeal_approved: action: 账号设置 explanation: 你于 %{appeal_date} 对 %{strike_date} 在你账号上做出的处罚提出的申诉已被批准,你的账号已回到正常状态。 diff --git a/config/webpack/rules/babel.js b/config/webpack/rules/babel.js index f1b53c3606..76e41f3df0 100644 --- a/config/webpack/rules/babel.js +++ b/config/webpack/rules/babel.js @@ -4,7 +4,7 @@ const { env, settings } = require('../configuration'); // Those modules contain modern ES code that need to be transpiled for Webpack to process it const nodeModulesToProcess = [ - '@reduxjs', 'fuzzysort', 'toygrad' + '@reduxjs', 'fuzzysort', 'toygrad', '@react-spring' ]; module.exports = { diff --git a/db/migrate/20250313123400_add_age_verified_at_to_users.rb b/db/migrate/20250313123400_add_age_verified_at_to_users.rb new file mode 100644 index 0000000000..c6cd6120ef --- /dev/null +++ b/db/migrate/20250313123400_add_age_verified_at_to_users.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddAgeVerifiedAtToUsers < ActiveRecord::Migration[8.0] + def change + add_column :users, :age_verified_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 3bdd09f15d..eb488a7342 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_03_05_074104) do +ActiveRecord::Schema[8.0].define(version: 2025_03_13_123400) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -1559,6 +1559,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_05_074104) do t.text "settings" t.string "time_zone" t.string "otp_secret" + t.datetime "age_verified_at" t.index ["account_id"], name: "index_users_on_account_id" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id", where: "(created_by_application_id IS NOT NULL)" diff --git a/lib/mastodon/middleware/prometheus_queue_time.rb b/lib/mastodon/middleware/prometheus_queue_time.rb new file mode 100644 index 0000000000..bb8add51ec --- /dev/null +++ b/lib/mastodon/middleware/prometheus_queue_time.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Mastodon + module Middleware + class PrometheusQueueTime < ::PrometheusExporter::Middleware + # Overwrite to only collect the queue time metric + def call(env) + queue_time = measure_queue_time(env) + + result = @app.call(env) + + result + ensure + obj = { + type: 'web', + queue_time: queue_time, + default_labels: {}, + } + + @client.send_json(obj) + end + end + end +end diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 86db57d35f..acf7a4e79a 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -96,7 +96,7 @@ module Mastodon def api_versions { - mastodon: 4, + mastodon: 5, kmyblue: KMYBLUE_API_VERSION, } end diff --git a/package.json b/package.json index 4bafcedebf..5a84692ad9 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,10 @@ "@github/webauthn-json": "^2.1.1", "@hello-pangea/dnd": "^17.0.0", "@rails/ujs": "7.1.501", + "@react-spring/web": "^9.7.5", "@reduxjs/toolkit": "^2.0.1", "@svgr/webpack": "^5.5.0", + "@use-gesture/react": "^10.3.1", "arrow-key-navigation": "^1.2.0", "async-mutex": "^0.5.0", "axios": "^1.4.0", @@ -108,7 +110,6 @@ "react-immutable-pure-component": "^2.2.2", "react-intl": "^7.0.0", "react-motion": "^0.5.2", - "react-notification": "^6.8.5", "react-overlays": "^5.2.1", "react-redux": "^9.0.4", "react-redux-loading-bar": "^5.0.8", diff --git a/spec/controllers/activitypub/inboxes_controller_spec.rb b/spec/controllers/activitypub/inboxes_controller_spec.rb deleted file mode 100644 index feca543cb7..0000000000 --- a/spec/controllers/activitypub/inboxes_controller_spec.rb +++ /dev/null @@ -1,112 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ActivityPub::InboxesController do - let(:remote_account) { nil } - - before do - allow(controller).to receive(:signed_request_actor).and_return(remote_account) - end - - describe 'POST #create' do - context 'with signature' do - let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub) } - - before do - post :create, body: '{}' - end - - it 'returns http accepted' do - expect(response).to have_http_status(202) - end - - context 'with a specific account' do - subject(:response) { post :create, params: { account_username: account.username }, body: '{}' } - - let(:account) { Fabricate(:account) } - - context 'when account is permanently suspended' do - before do - account.suspend! - account.deletion_request.destroy - end - - it 'returns http gone' do - expect(response).to have_http_status(410) - end - end - - context 'when account is temporarily suspended' do - before do - account.suspend! - end - - it 'returns http accepted' do - expect(response).to have_http_status(202) - end - end - end - end - - context 'with Collection-Synchronization header' do - let(:remote_account) { Fabricate(:account, followers_url: 'https://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor', protocol: :activitypub) } - let(:synchronization_collection) { remote_account.followers_url } - let(:synchronization_url) { 'https://example.com/followers-for-domain' } - let(:synchronization_hash) { 'somehash' } - let(:synchronization_header) { "collectionId=\"#{synchronization_collection}\", digest=\"#{synchronization_hash}\", url=\"#{synchronization_url}\"" } - - before do - allow(ActivityPub::FollowersSynchronizationWorker).to receive(:perform_async).and_return(nil) - allow(remote_account).to receive(:local_followers_hash).and_return('somehash') - - request.headers['Collection-Synchronization'] = synchronization_header - post :create, body: '{}' - end - - context 'with mismatching target collection' do - let(:synchronization_collection) { 'https://example.com/followers2' } - - it 'does not start a synchronization job' do - expect(ActivityPub::FollowersSynchronizationWorker).to_not have_received(:perform_async) - end - end - - context 'with mismatching domain in partial collection attribute' do - let(:synchronization_url) { 'https://example.org/followers' } - - it 'does not start a synchronization job' do - expect(ActivityPub::FollowersSynchronizationWorker).to_not have_received(:perform_async) - end - end - - context 'with matching digest' do - it 'does not start a synchronization job' do - expect(ActivityPub::FollowersSynchronizationWorker).to_not have_received(:perform_async) - end - end - - context 'with mismatching digest' do - let(:synchronization_hash) { 'wronghash' } - - it 'starts a synchronization job' do - expect(ActivityPub::FollowersSynchronizationWorker).to have_received(:perform_async) - end - end - - it 'returns http accepted' do - expect(response).to have_http_status(202) - end - end - - context 'without signature' do - before do - post :create, body: '{}' - end - - it 'returns http not authorized' do - expect(response).to have_http_status(401) - end - end - end -end diff --git a/spec/controllers/admin/account_actions_controller_spec.rb b/spec/controllers/admin/account_actions_controller_spec.rb deleted file mode 100644 index fabe5cef4d..0000000000 --- a/spec/controllers/admin/account_actions_controller_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Admin::AccountActionsController do - render_views - - let(:user) { Fabricate(:admin_user) } - - before do - sign_in user, scope: :user - end - - describe 'GET #new' do - let(:account) { Fabricate(:account) } - - it 'returns http success' do - get :new, params: { account_id: account.id } - - expect(response).to have_http_status(:success) - end - end - - describe 'POST #create' do - let(:account) { Fabricate(:account) } - - it 'records the account action' do - expect do - post :create, params: { account_id: account.id, admin_account_action: { type: 'silence' } } - end.to change { account.strikes.count }.by(1) - - expect(response).to redirect_to(admin_account_path(account.id)) - end - end -end diff --git a/spec/controllers/admin/change_emails_controller_spec.rb b/spec/controllers/admin/change_emails_controller_spec.rb deleted file mode 100644 index 899106e54e..0000000000 --- a/spec/controllers/admin/change_emails_controller_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Admin::ChangeEmailsController do - render_views - - let(:admin) { Fabricate(:admin_user) } - - before do - sign_in admin - end - - describe 'GET #show' do - it 'returns http success' do - user = Fabricate(:user) - - get :show, params: { account_id: user.account.id } - - expect(response).to have_http_status(200) - end - end - - describe 'GET #update' do - before do - allow(UserMailer).to receive(:confirmation_instructions) - .and_return(instance_double(ActionMailer::MessageDelivery, deliver_later: nil)) - end - - it 'returns http success' do - user = Fabricate(:user) - - previous_email = user.email - - post :update, params: { account_id: user.account.id, user: { unconfirmed_email: 'test@example.com' } } - - user.reload - - expect(user.email).to eq previous_email - expect(user.unconfirmed_email).to eq 'test@example.com' - expect(user.confirmation_token).to_not be_nil - - expect(UserMailer).to have_received(:confirmation_instructions).with(user, user.confirmation_token, { to: 'test@example.com' }) - - expect(response).to redirect_to(admin_account_path(user.account.id)) - end - end -end diff --git a/spec/controllers/admin/users/two_factor_authentications_controller_spec.rb b/spec/controllers/admin/users/two_factor_authentications_controller_spec.rb deleted file mode 100644 index 39af2ca914..0000000000 --- a/spec/controllers/admin/users/two_factor_authentications_controller_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'webauthn/fake_client' - -RSpec.describe Admin::Users::TwoFactorAuthenticationsController do - render_views - - let(:user) { Fabricate(:user) } - - before do - sign_in Fabricate(:admin_user), scope: :user - end - - describe 'DELETE #destroy' do - context 'when user has OTP enabled' do - before do - user.update(otp_required_for_login: true) - end - - it 'redirects to admin account page' do - delete :destroy, params: { user_id: user.id } - - user.reload - expect(user.otp_enabled?).to be false - expect(response).to redirect_to(admin_account_path(user.account_id)) - end - end - - context 'when user has OTP and WebAuthn enabled' do - let(:fake_client) { WebAuthn::FakeClient.new('http://test.host') } - - before do - user.update(otp_required_for_login: true, webauthn_id: WebAuthn.generate_user_id) - - public_key_credential = WebAuthn::Credential.from_create(fake_client.create) - Fabricate(:webauthn_credential, - user_id: user.id, - external_id: public_key_credential.id, - public_key: public_key_credential.public_key, - nickname: 'Security Key') - end - - it 'redirects to admin account page' do - delete :destroy, params: { user_id: user.id } - - user.reload - expect(user.otp_enabled?).to be false - expect(user.webauthn_enabled?).to be false - expect(response).to redirect_to(admin_account_path(user.account_id)) - end - end - end -end diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb index a16e933cf3..e7a8dd6d7f 100644 --- a/spec/controllers/auth/registrations_controller_spec.rb +++ b/spec/controllers/auth/registrations_controller_spec.rb @@ -342,6 +342,42 @@ RSpec.describe Auth::RegistrationsController do end end + context 'when age verification is enabled' do + subject { post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', agreement: 'true' }.merge(date_of_birth) } } + + before do + Setting.min_age = 16 + end + + let(:date_of_birth) { {} } + + context 'when date of birth is below age limit' do + let(:date_of_birth) { 13.years.ago.then { |date| { 'date_of_birth(1i)': date.day.to_s, 'date_of_birth(2i)': date.month.to_s, 'date_of_birth(3i)': date.year.to_s } } } + + it 'does not create user' do + subject + user = User.find_by(email: 'test@example.com') + expect(user).to be_nil + end + end + + context 'when date of birth is above age limit' do + let(:date_of_birth) { 17.years.ago.then { |date| { 'date_of_birth(1i)': date.day.to_s, 'date_of_birth(2i)': date.month.to_s, 'date_of_birth(3i)': date.year.to_s } } } + + it 'redirects to setup and creates user' do + subject + + expect(response).to redirect_to auth_setup_path + + expect(User.find_by(email: 'test@example.com')) + .to be_present + .and have_attributes( + age_verified_at: not_eq(nil) + ) + end + end + end + context 'when max user count is set' do subject do post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', agreement: 'true' } } diff --git a/spec/controllers/disputes/strikes_controller_spec.rb b/spec/controllers/disputes/strikes_controller_spec.rb deleted file mode 100644 index f6d28fc09a..0000000000 --- a/spec/controllers/disputes/strikes_controller_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Disputes::StrikesController do - render_views - - before { sign_in current_user, scope: :user } - - describe '#show' do - let(:current_user) { Fabricate(:user) } - let(:strike) { Fabricate(:account_warning, target_account: current_user.account) } - - before do - get :show, params: { id: strike.id } - end - - context 'when meant for the user' do - it 'returns http success' do - expect(response).to have_http_status(:success) - end - end - - context 'when meant for a different user' do - let(:strike) { Fabricate(:account_warning) } - - it 'returns http forbidden' do - expect(response).to have_http_status(403) - end - end - end -end diff --git a/spec/controllers/filters/statuses_controller_spec.rb b/spec/controllers/filters/statuses_controller_spec.rb deleted file mode 100644 index 7bad403571..0000000000 --- a/spec/controllers/filters/statuses_controller_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Filters::StatusesController do - render_views - - describe 'GET #index' do - let(:filter) { Fabricate(:custom_filter) } - - context 'with signed out user' do - it 'redirects' do - get :index, params: { filter_id: filter } - - expect(response).to be_redirect - end - end - - context 'with a signed in user' do - context 'with the filter user signed in' do - before do - sign_in(filter.account.user) - get :index, params: { filter_id: filter } - end - - it 'returns http success and private cache control headers' do - expect(response).to have_http_status(200) - - expect(response.headers['Cache-Control']).to include('private, no-store') - end - end - - context 'with another user signed in' do - before do - sign_in(Fabricate(:user)) - get :index, params: { filter_id: filter } - end - - it 'returns http not found' do - expect(response).to have_http_status(404) - end - end - end - end -end diff --git a/spec/controllers/oauth/tokens_controller_spec.rb b/spec/controllers/oauth/tokens_controller_spec.rb deleted file mode 100644 index a2eed797e0..0000000000 --- a/spec/controllers/oauth/tokens_controller_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Oauth::TokensController do - describe 'POST #revoke' do - let!(:user) { Fabricate(:user) } - let!(:application) { Fabricate(:application, confidential: false) } - let!(:access_token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: application) } - let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) } - - it 'revokes the token and removes subscriptions' do - post :revoke, params: { client_id: application.uid, token: access_token.token } - - expect(access_token.reload.revoked_at) - .to_not be_nil - expect(Web::PushSubscription.where(access_token: access_token).count) - .to eq(0) - expect { web_push_subscription.reload } - .to raise_error(ActiveRecord::RecordNotFound) - end - end -end diff --git a/spec/controllers/settings/deletes_controller_spec.rb b/spec/controllers/settings/deletes_controller_spec.rb deleted file mode 100644 index 98104b8454..0000000000 --- a/spec/controllers/settings/deletes_controller_spec.rb +++ /dev/null @@ -1,87 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Settings::DeletesController do - render_views - - describe 'GET #show' do - context 'when signed in' do - let(:user) { Fabricate(:user) } - - before do - sign_in user, scope: :user - get :show - end - - it 'renders confirmation page with private cache control headers', :aggregate_failures do - expect(response).to have_http_status(200) - expect(response.headers['Cache-Control']).to include('private, no-store') - end - - context 'when suspended' do - let(:user) { Fabricate(:user, account_attributes: { suspended_at: Time.now.utc }) } - - it 'returns http forbidden with private cache control headers', :aggregate_failures do - expect(response).to have_http_status(403) - expect(response.headers['Cache-Control']).to include('private, no-store') - end - end - end - - context 'when not signed in' do - it 'redirects' do - get :show - expect(response).to redirect_to '/auth/sign_in' - end - end - end - - describe 'DELETE #destroy' do - context 'when signed in' do - let(:user) { Fabricate(:user, password: 'petsmoldoggos') } - - before do - sign_in user, scope: :user - end - - context 'with correct password' do - before do - delete :destroy, params: { form_delete_confirmation: { password: 'petsmoldoggos' } } - end - - it 'removes user record and redirects', :aggregate_failures, :inline_jobs do - expect(response).to redirect_to '/auth/sign_in' - expect(User.find_by(id: user.id)).to be_nil - expect(user.account.reload).to be_suspended - expect(CanonicalEmailBlock.block?(user.email)).to be false - end - - context 'when suspended' do - let(:user) { Fabricate(:user, account_attributes: { suspended_at: Time.now.utc }) } - - it 'returns http forbidden' do - expect(response).to have_http_status(403) - end - end - end - - context 'with incorrect password' do - before do - delete :destroy, params: { form_delete_confirmation: { password: 'blaze420' } } - end - - it 'redirects back to confirmation page' do - expect(response).to redirect_to settings_delete_path - end - end - end - - context 'when not signed in' do - it 'redirects' do - delete :destroy - expect(response).to redirect_to '/auth/sign_in' - end - end - end -end diff --git a/spec/lib/mastodon/middleware/prometheus_queue_time_spec.rb b/spec/lib/mastodon/middleware/prometheus_queue_time_spec.rb new file mode 100644 index 0000000000..eaab93772d --- /dev/null +++ b/spec/lib/mastodon/middleware/prometheus_queue_time_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'prometheus_exporter' +require 'prometheus_exporter/middleware' +require 'mastodon/middleware/prometheus_queue_time' + +RSpec.describe Mastodon::Middleware::PrometheusQueueTime do + subject { described_class.new(app, client:) } + + let(:app) do + proc { |_env| [200, {}, 'OK'] } + end + let(:client) do + instance_double(PrometheusExporter::Client, send_json: true) + end + + describe '#call' do + let(:env) do + { + 'HTTP_X_REQUEST_START' => "t=#{(Time.now.to_f * 1000).to_i}", + } + end + + it 'reports a queue time to the client' do + subject.call(env) + + expect(client).to have_received(:send_json) + .with(hash_including(queue_time: instance_of(Float))) + end + end +end diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb index 5f91ae0967..43e9ed087b 100644 --- a/spec/models/media_attachment_spec.rb +++ b/spec/models/media_attachment_spec.rb @@ -295,12 +295,21 @@ RSpec.describe MediaAttachment, :attachment_processing do end it 'queues CacheBusterWorker jobs' do - original_path = media.file.path(:original) - small_path = media.file.path(:small) + original_url = media.file.url(:original) + small_url = media.file.url(:small) expect { media.destroy } - .to enqueue_sidekiq_job(CacheBusterWorker).with(original_path) - .and enqueue_sidekiq_job(CacheBusterWorker).with(small_path) + .to enqueue_sidekiq_job(CacheBusterWorker).with(original_url) + .and enqueue_sidekiq_job(CacheBusterWorker).with(small_url) + end + + context 'with a missing remote attachment' do + let(:media) { Fabricate(:media_attachment, remote_url: 'https://example.com/foo.png', file: nil) } + + it 'does not queue CacheBusterWorker jobs' do + expect { media.destroy } + .to_not enqueue_sidekiq_job(CacheBusterWorker) + end end end diff --git a/spec/controllers/activitypub/collections_controller_spec.rb b/spec/requests/activitypub/collections_spec.rb similarity index 59% rename from spec/controllers/activitypub/collections_controller_spec.rb rename to spec/requests/activitypub/collections_spec.rb index 408e0dd2f6..d2761f98ea 100644 --- a/spec/controllers/activitypub/collections_controller_spec.rb +++ b/spec/requests/activitypub/collections_spec.rb @@ -2,22 +2,19 @@ require 'rails_helper' -RSpec.describe ActivityPub::CollectionsController do +RSpec.describe 'ActivityPub Collections' do let!(:account) { Fabricate(:account) } let!(:private_pinned) { Fabricate(:status, account: account, text: 'secret private stuff', visibility: :private) } let(:remote_account) { nil } before do - allow(controller).to receive(:signed_request_actor).and_return(remote_account) - - Fabricate(:status_pin, account: account) - Fabricate(:status_pin, account: account) + Fabricate.times(2, :status_pin, account: account) Fabricate(:status_pin, account: account, status: private_pinned) Fabricate(:status, account: account, visibility: :private) end describe 'GET #show' do - subject(:response) { get :show, params: { id: id, account_username: account.username } } + subject { get account_collection_path(id: id, account_username: account.username), headers: nil, sign_with: remote_account } context 'when id is "featured"' do let(:id) { 'featured' } @@ -26,10 +23,13 @@ RSpec.describe ActivityPub::CollectionsController do let(:remote_account) { nil } it 'returns http success and correct media type and correct items' do + subject + expect(response) .to have_http_status(200) .and have_cacheable_headers - expect(response.media_type).to eq 'application/activity+json' + expect(response.media_type) + .to eq 'application/activity+json' expect(response.parsed_body[:orderedItems]) .to be_an(Array) @@ -45,17 +45,21 @@ RSpec.describe ActivityPub::CollectionsController do end it 'returns http gone' do - expect(response).to have_http_status(410) + subject + + expect(response) + .to have_http_status(410) end end context 'when account is temporarily suspended' do - before do - account.suspend! - end + before { account.suspend! } it 'returns http forbidden' do - expect(response).to have_http_status(403) + subject + + expect(response) + .to have_http_status(403) end end end @@ -65,11 +69,14 @@ RSpec.describe ActivityPub::CollectionsController do context 'when getting a featured resource' do it 'returns http success and correct media type and expected items' do + subject + expect(response) .to have_http_status(200) .and have_cacheable_headers - expect(response.media_type).to eq 'application/activity+json' + expect(response.media_type) + .to eq 'application/activity+json' expect(response.parsed_body[:orderedItems]) .to be_an(Array) @@ -80,39 +87,45 @@ RSpec.describe ActivityPub::CollectionsController do end context 'with authorized fetch mode' do - before do - allow(controller).to receive(:authorized_fetch_mode?).and_return(true) - end + before { Setting.authorized_fetch = true } context 'when signed request account is blocked' do - before do - account.block!(remote_account) - end + before { account.block!(remote_account) } it 'returns http success and correct media type and cache headers and empty items' do - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/activity+json' - expect(response.headers['Cache-Control']).to include 'private' + subject - expect(response.parsed_body[:orderedItems]) - .to be_an(Array) - .and be_empty + expect(response) + .to have_http_status(200) + expect(response.media_type) + .to eq('application/activity+json') + expect(response.headers['Cache-Control']) + .to include('private') + + expect(response.parsed_body) + .to include( + orderedItems: be_an(Array).and(be_empty) + ) end end context 'when signed request account is domain blocked' do - before do - account.block_domain!(remote_account.domain) - end + before { account.block_domain!(remote_account.domain) } it 'returns http success and correct media type and cache headers and empty items' do - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/activity+json' - expect(response.headers['Cache-Control']).to include 'private' + subject - expect(response.parsed_body[:orderedItems]) - .to be_an(Array) - .and be_empty + expect(response) + .to have_http_status(200) + expect(response.media_type) + .to eq('application/activity+json') + expect(response.headers['Cache-Control']) + .to include('private') + + expect(response.parsed_body) + .to include( + orderedItems: be_an(Array).and(be_empty) + ) end end end @@ -123,7 +136,10 @@ RSpec.describe ActivityPub::CollectionsController do let(:id) { 'hoge' } it 'returns http not found' do - expect(response).to have_http_status(404) + subject + + expect(response) + .to have_http_status(404) end end end diff --git a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb b/spec/requests/activitypub/followers_synchronizations_spec.rb similarity index 68% rename from spec/controllers/activitypub/followers_synchronizations_controller_spec.rb rename to spec/requests/activitypub/followers_synchronizations_spec.rb index cbd982f18f..97b8a7908e 100644 --- a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb +++ b/spec/requests/activitypub/followers_synchronizations_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe ActivityPub::FollowersSynchronizationsController do +RSpec.describe 'ActivityPub Follower Synchronizations' do let!(:account) { Fabricate(:account) } let!(:follower_example_com_user_a) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/a') } let!(:follower_example_com_user_b) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/b') } @@ -14,32 +14,34 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController do follower_example_com_user_b.follow!(account) follower_foo_com_user_a.follow!(account) follower_example_com_instance_actor.follow!(account) - - allow(controller).to receive(:signed_request_actor).and_return(remote_account) end describe 'GET #show' do context 'without signature' do - let(:remote_account) { nil } - - before do - get :show, params: { account_username: account.username } - end + subject { get account_followers_synchronization_path(account_username: account.username) } it 'returns http not authorized' do - expect(response).to have_http_status(401) + subject + + expect(response) + .to have_http_status(401) end end context 'with signature from example.com' do - subject(:response) { get :show, params: { account_username: account.username } } + subject { get account_followers_synchronization_path(account_username: account.username), headers: nil, sign_with: remote_account } let(:remote_account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/instance') } it 'returns http success and cache control and activity json types and correct items' do - expect(response).to have_http_status(200) - expect(response.headers['Cache-Control']).to eq 'max-age=0, private' - expect(response.media_type).to eq 'application/activity+json' + subject + + expect(response) + .to have_http_status(200) + expect(response.headers['Cache-Control']) + .to eq 'max-age=0, private' + expect(response.media_type) + .to eq 'application/activity+json' expect(response.parsed_body[:orderedItems]) .to be_an(Array) @@ -57,17 +59,21 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController do end it 'returns http gone' do - expect(response).to have_http_status(410) + subject + + expect(response) + .to have_http_status(410) end end context 'when account is temporarily suspended' do - before do - account.suspend! - end + before { account.suspend! } it 'returns http forbidden' do - expect(response).to have_http_status(403) + subject + + expect(response) + .to have_http_status(403) end end end diff --git a/spec/requests/activitypub/inboxes_spec.rb b/spec/requests/activitypub/inboxes_spec.rb new file mode 100644 index 0000000000..b21881b10f --- /dev/null +++ b/spec/requests/activitypub/inboxes_spec.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'ActivityPub Inboxes' do + let(:remote_account) { nil } + + describe 'POST #create' do + context 'with signature' do + let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub) } + + context 'without a named account' do + subject { post inbox_path, params: {}.to_json, sign_with: remote_account } + + it 'returns http accepted' do + subject + + expect(response) + .to have_http_status(202) + end + end + + context 'with a specific account' do + subject { post account_inbox_path(account_username: account.username), params: {}.to_json, sign_with: remote_account } + + let(:account) { Fabricate(:account) } + + context 'when account is permanently suspended' do + before do + account.suspend! + account.deletion_request.destroy + end + + it 'returns http gone' do + subject + + expect(response) + .to have_http_status(410) + end + end + + context 'when account is temporarily suspended' do + before { account.suspend! } + + it 'returns http accepted' do + subject + + expect(response) + .to have_http_status(202) + end + end + end + end + + context 'with Collection-Synchronization header' do + subject { post inbox_path, params: {}.to_json, headers: { 'Collection-Synchronization' => synchronization_header }, sign_with: remote_account } + + let(:remote_account) { Fabricate(:account, followers_url: 'https://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor', protocol: :activitypub) } + let(:synchronization_collection) { remote_account.followers_url } + let(:synchronization_url) { 'https://example.com/followers-for-domain' } + let(:synchronization_hash) { 'somehash' } + let(:synchronization_header) { "collectionId=\"#{synchronization_collection}\", digest=\"#{synchronization_hash}\", url=\"#{synchronization_url}\"" } + + before do + stub_follow_sync_worker + stub_followers_hash + end + + context 'with mismatching target collection' do + let(:synchronization_collection) { 'https://example.com/followers2' } + + it 'does not start a synchronization job' do + subject + + expect(response) + .to have_http_status(202) + expect(ActivityPub::FollowersSynchronizationWorker) + .to_not have_received(:perform_async) + end + end + + context 'with mismatching domain in partial collection attribute' do + let(:synchronization_url) { 'https://example.org/followers' } + + it 'does not start a synchronization job' do + subject + + expect(response) + .to have_http_status(202) + expect(ActivityPub::FollowersSynchronizationWorker) + .to_not have_received(:perform_async) + end + end + + context 'with matching digest' do + it 'does not start a synchronization job' do + subject + + expect(response) + .to have_http_status(202) + expect(ActivityPub::FollowersSynchronizationWorker) + .to_not have_received(:perform_async) + end + end + + context 'with mismatching digest' do + let(:synchronization_hash) { 'wronghash' } + + it 'starts a synchronization job' do + subject + + expect(response) + .to have_http_status(202) + expect(ActivityPub::FollowersSynchronizationWorker) + .to have_received(:perform_async) + end + end + + it 'returns http accepted' do + subject + + expect(response) + .to have_http_status(202) + end + + def stub_follow_sync_worker + allow(ActivityPub::FollowersSynchronizationWorker) + .to receive(:perform_async) + .and_return(nil) + end + + def stub_followers_hash + Rails.cache.write("followers_hash:#{remote_account.id}:local", 'somehash') # Populate value to match request + end + end + + context 'without signature' do + subject { post inbox_path, params: {}.to_json } + + it 'returns http not authorized' do + subject + + expect(response) + .to have_http_status(401) + end + end + end +end diff --git a/spec/controllers/activitypub/outboxes_controller_spec.rb b/spec/requests/activitypub/outboxes_spec.rb similarity index 63% rename from spec/controllers/activitypub/outboxes_controller_spec.rb rename to spec/requests/activitypub/outboxes_spec.rb index ca986dcabb..22b2f97c07 100644 --- a/spec/controllers/activitypub/outboxes_controller_spec.rb +++ b/spec/requests/activitypub/outboxes_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe ActivityPub::OutboxesController do +RSpec.describe 'ActivityPub Outboxes' do let!(:account) { Fabricate(:account) } before do @@ -11,13 +11,11 @@ RSpec.describe ActivityPub::OutboxesController do Fabricate(:status, account: account, visibility: :private) Fabricate(:status, account: account, visibility: :direct) Fabricate(:status, account: account, visibility: :limited) - - allow(controller).to receive(:signed_request_actor).and_return(remote_account) end describe 'GET #show' do context 'without signature' do - subject(:response) { get :show, params: { account_username: account.username, page: page } } + subject { get account_outbox_path(account_username: account.username, page: page) } let(:remote_account) { nil } @@ -25,13 +23,18 @@ RSpec.describe ActivityPub::OutboxesController do let(:page) { nil } it 'returns http success and correct media type and headers and items count' do + subject + expect(response) .to have_http_status(200) .and have_cacheable_headers - expect(response.media_type).to eq 'application/activity+json' - expect(response.headers['Vary']).to be_nil - expect(response.parsed_body[:totalItems]).to eq 4 + expect(response.media_type) + .to eq 'application/activity+json' + expect(response.headers['Vary']) + .to be_nil + expect(response.parsed_body[:totalItems]) + .to eq 4 end context 'when account is permanently suspended' do @@ -41,17 +44,21 @@ RSpec.describe ActivityPub::OutboxesController do end it 'returns http gone' do - expect(response).to have_http_status(410) + subject + + expect(response) + .to have_http_status(410) end end context 'when account is temporarily suspended' do - before do - account.suspend! - end + before { account.suspend! } it 'returns http forbidden' do - expect(response).to have_http_status(403) + subject + + expect(response) + .to have_http_status(403) end end end @@ -60,12 +67,16 @@ RSpec.describe ActivityPub::OutboxesController do let(:page) { 'true' } it 'returns http success and correct media type and vary header and items' do + subject + expect(response) .to have_http_status(200) .and have_cacheable_headers - expect(response.media_type).to eq 'application/activity+json' - expect(response.headers['Vary']).to include 'Signature' + expect(response.media_type) + .to eq 'application/activity+json' + expect(response.headers['Vary']) + .to include 'Signature' expect(response.parsed_body) .to include( @@ -82,35 +93,42 @@ RSpec.describe ActivityPub::OutboxesController do end it 'returns http gone' do - expect(response).to have_http_status(410) + subject + + expect(response) + .to have_http_status(410) end end context 'when account is temporarily suspended' do - before do - account.suspend! - end + before { account.suspend! } it 'returns http forbidden' do - expect(response).to have_http_status(403) + subject + + expect(response) + .to have_http_status(403) end end end end context 'with signature' do + subject { get account_outbox_path(account_username: account.username, page: page), headers: nil, sign_with: remote_account } + let(:remote_account) { Fabricate(:account, domain: 'example.com') } let(:page) { 'true' } context 'when signed request account does not follow account' do - before do - get :show, params: { account_username: account.username, page: page } - end - it 'returns http success and correct media type and headers and items' do - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/activity+json' - expect(response.headers['Cache-Control']).to eq 'max-age=60, private' + subject + + expect(response) + .to have_http_status(200) + expect(response.media_type) + .to eq 'application/activity+json' + expect(response.headers['Cache-Control']) + .to eq 'private, no-store' expect(response.parsed_body) .to include( @@ -122,15 +140,17 @@ RSpec.describe ActivityPub::OutboxesController do end context 'when signed request account follows account' do - before do - remote_account.follow!(account) - get :show, params: { account_username: account.username, page: page } - end + before { remote_account.follow!(account) } it 'returns http success and correct media type and headers and items' do - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/activity+json' - expect(response.headers['Cache-Control']).to eq 'max-age=60, private' + subject + + expect(response) + .to have_http_status(200) + expect(response.media_type) + .to eq 'application/activity+json' + expect(response.headers['Cache-Control']) + .to eq 'private, no-store' expect(response.parsed_body) .to include( @@ -142,15 +162,17 @@ RSpec.describe ActivityPub::OutboxesController do end context 'when signed request account is blocked' do - before do - account.block!(remote_account) - get :show, params: { account_username: account.username, page: page } - end + before { account.block!(remote_account) } it 'returns http success and correct media type and headers and items' do - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/activity+json' - expect(response.headers['Cache-Control']).to eq 'max-age=60, private' + subject + + expect(response) + .to have_http_status(200) + expect(response.media_type) + .to eq 'application/activity+json' + expect(response.headers['Cache-Control']) + .to eq 'private, no-store' expect(response.parsed_body) .to include( @@ -160,15 +182,17 @@ RSpec.describe ActivityPub::OutboxesController do end context 'when signed request account is domain blocked' do - before do - account.block_domain!(remote_account.domain) - get :show, params: { account_username: account.username, page: page } - end + before { account.block_domain!(remote_account.domain) } it 'returns http success and correct media type and headers and items' do - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/activity+json' - expect(response.headers['Cache-Control']).to eq 'max-age=60, private' + subject + + expect(response) + .to have_http_status(200) + expect(response.media_type) + .to eq 'application/activity+json' + expect(response.headers['Cache-Control']) + .to eq 'private, no-store' expect(response.parsed_body) .to include( diff --git a/spec/controllers/activitypub/replies_controller_spec.rb b/spec/requests/activitypub/replies_spec.rb similarity index 78% rename from spec/controllers/activitypub/replies_controller_spec.rb rename to spec/requests/activitypub/replies_spec.rb index d7c2c2d3b0..313cab2a44 100644 --- a/spec/controllers/activitypub/replies_controller_spec.rb +++ b/spec/requests/activitypub/replies_spec.rb @@ -2,9 +2,9 @@ require 'rails_helper' -RSpec.describe ActivityPub::RepliesController do +RSpec.describe 'ActivityPub Replies' do let(:status) { Fabricate(:status, visibility: parent_visibility) } - let(:remote_account) { Fabricate(:account, domain: 'foobar.com') } + let(:remote_account) { Fabricate(:account, domain: 'foobar.com') } let(:remote_reply_id) { 'https://foobar.com/statuses/1234' } let(:remote_querier) { nil } @@ -13,7 +13,10 @@ RSpec.describe ActivityPub::RepliesController do let(:parent_visibility) { :private } it 'returns http not found' do - expect(response).to have_http_status(404) + subject + + expect(response) + .to have_http_status(404) end end @@ -21,7 +24,10 @@ RSpec.describe ActivityPub::RepliesController do let(:parent_visibility) { :direct } it 'returns http not found' do - expect(response).to have_http_status(404) + subject + + expect(response) + .to have_http_status(404) end end end @@ -31,7 +37,10 @@ RSpec.describe ActivityPub::RepliesController do let(:parent_visibility) { :public } it 'returns http not found' do - expect(response).to have_http_status(404) + subject + + expect(response) + .to have_http_status(404) end end @@ -48,19 +57,23 @@ RSpec.describe ActivityPub::RepliesController do end it 'returns http gone' do - expect(response).to have_http_status(410) + subject + + expect(response) + .to have_http_status(410) end end context 'when account is temporarily suspended' do let(:parent_visibility) { :public } - before do - status.account.suspend! - end + before { status.account.suspend! } it 'returns http forbidden' do - expect(response).to have_http_status(403) + subject + + expect(response) + .to have_http_status(403) end end @@ -68,15 +81,20 @@ RSpec.describe ActivityPub::RepliesController do let(:parent_visibility) { :public } it 'returns http success and correct media type' do + subject + expect(response) .to have_http_status(200) .and have_cacheable_headers - expect(response.media_type).to eq 'application/activity+json' + expect(response.media_type) + .to eq 'application/activity+json' end - context 'without only_other_accounts' do + context 'without `only_other_accounts` param' do it "returns items with thread author's replies" do + subject + expect(response.parsed_body) .to include( first: be_a(Hash).and( @@ -91,6 +109,8 @@ RSpec.describe ActivityPub::RepliesController do context 'when there are few self-replies' do it 'points next to replies from other people' do + subject + expect(response.parsed_body) .to include( first: be_a(Hash).and( @@ -108,6 +128,8 @@ RSpec.describe ActivityPub::RepliesController do end it 'points next to other self-replies' do + subject + expect(response.parsed_body) .to include( first: be_a(Hash).and( @@ -120,31 +142,33 @@ RSpec.describe ActivityPub::RepliesController do end end - context 'with only_other_accounts' do + context 'with `only_other_accounts` param' do let(:only_other_accounts) { 'true' } - it 'returns items with other public or unlisted replies' do + it 'returns items with other public or unlisted replies and correctly inlines replies and uses IDs' do + subject + expect(response.parsed_body) .to include( first: be_a(Hash).and( include(items: be_an(Array).and(have_attributes(size: 3))) ) ) - end - it 'only inlines items that are local and public or unlisted replies' do + # Only inline replies that are local and public, or unlisted expect(inlined_replies) .to all(satisfy { |item| targets_public_collection?(item) }) .and all(satisfy { |item| ActivityPub::TagManager.instance.local_uri?(item[:id]) }) - end - it 'uses ids for remote toots' do + # Use ids for remote replies expect(remote_replies) .to all(satisfy { |item| item.is_a?(String) && !ActivityPub::TagManager.instance.local_uri?(item) }) end context 'when there are few replies' do it 'does not have a next page' do + subject + expect(response.parsed_body) .to include( first: be_a(Hash).and(not_include(next: be_present)) @@ -158,6 +182,8 @@ RSpec.describe ActivityPub::RepliesController do end it 'points next to other replies' do + subject + expect(response.parsed_body) .to include( first: be_a(Hash).and( @@ -176,10 +202,8 @@ RSpec.describe ActivityPub::RepliesController do before do stub_const 'ActivityPub::RepliesController::DESCENDANTS_LIMIT', 5 - allow(controller).to receive(:signed_request_actor).and_return(remote_querier) - Fabricate(:status, thread: status, visibility: :public) - Fabricate(:status, thread: status, visibility: :public) + Fabricate.times(2, :status, thread: status, visibility: :public) Fabricate(:status, thread: status, visibility: :private) Fabricate(:status, account: status.account, thread: status, visibility: :public) Fabricate(:status, account: status.account, thread: status, visibility: :private) @@ -188,31 +212,29 @@ RSpec.describe ActivityPub::RepliesController do end describe 'GET #index' do - subject(:response) { get :index, params: { account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts } } - let(:only_other_accounts) { nil } context 'with no signature' do + subject { get account_status_replies_path(account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts) } + it_behaves_like 'allowed access' end context 'with signature' do + subject { get account_status_replies_path(account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts), headers: nil, sign_with: remote_querier } + let(:remote_querier) { Fabricate(:account, domain: 'example.com') } it_behaves_like 'allowed access' context 'when signed request account is blocked' do - before do - status.account.block!(remote_querier) - end + before { status.account.block!(remote_querier) } it_behaves_like 'disallowed access' end context 'when signed request account is domain blocked' do - before do - status.account.block_domain!(remote_querier.domain) - end + before { status.account.block_domain!(remote_querier.domain) } it_behaves_like 'disallowed access' end diff --git a/spec/requests/api/v1/accounts_spec.rb b/spec/requests/api/v1/accounts_spec.rb index d423a08f12..329bb5f1e4 100644 --- a/spec/requests/api/v1/accounts_spec.rb +++ b/spec/requests/api/v1/accounts_spec.rb @@ -74,12 +74,45 @@ RSpec.describe '/api/v1/accounts' do describe 'POST /api/v1/accounts' do subject do - post '/api/v1/accounts', headers: headers, params: { username: 'test', password: '12345678', email: 'hello@world.tld', agreement: agreement } + post '/api/v1/accounts', headers: headers, params: { username: 'test', password: '12345678', email: 'hello@world.tld', agreement: agreement, date_of_birth: date_of_birth } end let(:client_app) { Fabricate(:application) } let(:token) { Doorkeeper::AccessToken.find_or_create_for(application: client_app, resource_owner: nil, scopes: 'read write', use_refresh_token: false) } let(:agreement) { nil } + let(:date_of_birth) { nil } + + context 'when age verification is enabled' do + before do + Setting.min_age = 16 + end + + let(:agreement) { 'true' } + + context 'when date of birth is below age limit' do + let(:date_of_birth) { 13.years.ago.strftime('%d.%m.%Y') } + + it 'returns http unprocessable entity' do + subject + + expect(response).to have_http_status(422) + expect(response.content_type) + .to start_with('application/json') + end + end + + context 'when date of birth is over age limit' do + let(:date_of_birth) { 17.years.ago.strftime('%d.%m.%Y') } + + it 'creates a user', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + end + end + end context 'when given truthy agreement' do let(:agreement) { 'true' } diff --git a/spec/requests/api/v1/trends/tags_spec.rb b/spec/requests/api/v1/trends/tags_spec.rb index 14ab73fc96..097393e58d 100644 --- a/spec/requests/api/v1/trends/tags_spec.rb +++ b/spec/requests/api/v1/trends/tags_spec.rb @@ -15,6 +15,8 @@ RSpec.describe 'API V1 Trends Tags' do .and not_have_http_link_header expect(response.content_type) .to start_with('application/json') + expect(response.headers['Deprecation']) + .to be_nil end end @@ -31,6 +33,8 @@ RSpec.describe 'API V1 Trends Tags' do .and have_http_link_header(api_v1_trends_tags_url(offset: 2)).for(rel: 'next') expect(response.content_type) .to start_with('application/json') + expect(response.headers['Deprecation']) + .to be_nil end def prepare_trends diff --git a/spec/requests/api/v1/trends_spec.rb b/spec/requests/api/v1/trends_spec.rb new file mode 100644 index 0000000000..5bfabdca1c --- /dev/null +++ b/spec/requests/api/v1/trends_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'deprecated API V1 Trends Tags' do + describe 'GET /api/v1/trends' do + context 'when trends are disabled' do + before { Setting.trends = false } + + it 'returns http success' do + get '/api/v1/trends' + + expect(response) + .to have_http_status(200) + .and not_have_http_link_header + expect(response.content_type) + .to start_with('application/json') + expect(response.headers['Deprecation']) + .to start_with '@' + end + end + + context 'when trends are enabled' do + before { Setting.trends = true } + + it 'returns http success' do + prepare_trends + stub_const('Api::V1::Trends::TagsController::DEFAULT_TAGS_LIMIT', 2) + get '/api/v1/trends' + + expect(response) + .to have_http_status(200) + .and have_http_link_header(api_v1_trends_tags_url(offset: 2)).for(rel: 'next') + expect(response.content_type) + .to start_with('application/json') + expect(response.headers['Deprecation']) + .to start_with '@' + end + + def prepare_trends + Fabricate.times(3, :tag, trendable: true).each do |tag| + 2.times { |i| Trends.tags.add(tag, i) } + end + Trends::Tags.new(threshold: 1).refresh + end + end + end +end diff --git a/spec/requests/api/v2/notifications_spec.rb b/spec/requests/api/v2/notifications_spec.rb index a7608e1419..69feb6cb6e 100644 --- a/spec/requests/api/v2/notifications_spec.rb +++ b/spec/requests/api/v2/notifications_spec.rb @@ -158,19 +158,18 @@ RSpec.describe 'Notifications' do expect(response).to have_http_status(200) expect(response.content_type) .to start_with('application/json') - expect(response.parsed_body[:notification_groups]).to contain_exactly( - a_hash_including( - type: 'favourite', - sample_account_ids: have_attributes(size: 5), - page_min_id: notification_ids.first.to_s, - page_max_id: notification_ids.last.to_s - ) - ) + expect(response.parsed_body[:notification_groups].size) + .to eq(1) + expect(response.parsed_body.dig(:notification_groups, 0)) + .to include(type: 'favourite') + .and(include(sample_account_ids: have_attributes(size: 5))) + .and(include(page_max_id: notification_ids.last.to_s)) + .and(include(page_min_id: notification_ids.first.to_s)) end end context 'with min_id param' do - let(:params) { { min_id: user.account.notifications.reload.first.id - 1 } } + let(:params) { { min_id: user.account.notifications.order(id: :asc).first.id - 1 } } it 'returns a notification group covering all notifications' do subject @@ -180,14 +179,13 @@ RSpec.describe 'Notifications' do expect(response).to have_http_status(200) expect(response.content_type) .to start_with('application/json') - expect(response.parsed_body[:notification_groups]).to contain_exactly( - a_hash_including( - type: 'favourite', - sample_account_ids: have_attributes(size: 5), - page_min_id: notification_ids.first.to_s, - page_max_id: notification_ids.last.to_s - ) - ) + expect(response.parsed_body[:notification_groups].size) + .to eq(1) + expect(response.parsed_body.dig(:notification_groups, 0)) + .to include(type: 'favourite') + .and(include(sample_account_ids: have_attributes(size: 5))) + .and(include(page_max_id: notification_ids.last.to_s)) + .and(include(page_min_id: notification_ids.first.to_s)) end end end diff --git a/spec/requests/disputes/strikes_spec.rb b/spec/requests/disputes/strikes_spec.rb new file mode 100644 index 0000000000..48685893c2 --- /dev/null +++ b/spec/requests/disputes/strikes_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Disputes Strikes' do + before { sign_in current_user } + + describe 'GET /disputes/strikes/:id' do + let(:current_user) { Fabricate(:user) } + + context 'when meant for a different user' do + let(:strike) { Fabricate(:account_warning) } + + it 'returns http forbidden' do + get disputes_strike_path(strike) + + expect(response) + .to have_http_status(403) + end + end + end +end diff --git a/spec/requests/filters/statuses_spec.rb b/spec/requests/filters/statuses_spec.rb index aa1d049da7..b462b56223 100644 --- a/spec/requests/filters/statuses_spec.rb +++ b/spec/requests/filters/statuses_spec.rb @@ -16,4 +16,30 @@ RSpec.describe 'Filters Statuses' do .to redirect_to(edit_filter_path(filter)) end end + + describe 'GET /filters/:filter_id/statuses' do + let(:filter) { Fabricate(:custom_filter) } + + context 'with signed out user' do + it 'redirects' do + get filter_statuses_path(filter) + + expect(response) + .to be_redirect + end + end + + context 'with a signed in user' do + context 'with another user signed in' do + before { sign_in(Fabricate(:user)) } + + it 'returns http not found' do + get filter_statuses_path(filter) + + expect(response) + .to have_http_status(404) + end + end + end + end end diff --git a/spec/controllers/intents_controller_spec.rb b/spec/requests/intents_spec.rb similarity index 90% rename from spec/controllers/intents_controller_spec.rb rename to spec/requests/intents_spec.rb index 668d833ea7..b62f570d7a 100644 --- a/spec/controllers/intents_controller_spec.rb +++ b/spec/requests/intents_spec.rb @@ -2,15 +2,15 @@ require 'rails_helper' -RSpec.describe IntentsController do - render_views - +RSpec.describe 'Intents' do let(:user) { Fabricate(:user) } before { sign_in user, scope: :user } - describe 'GET #show' do - subject { get :show, params: { uri: uri } } + describe 'GET /intent' do + subject { response } + + before { get intent_path(uri: uri) } context 'when schema is web+mastodon' do context 'when host is follow' do diff --git a/spec/requests/oauth/token_spec.rb b/spec/requests/oauth/token_spec.rb index 18d232e5ab..74f301c577 100644 --- a/spec/requests/oauth/token_spec.rb +++ b/spec/requests/oauth/token_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe 'Obtaining OAuth Tokens' do +RSpec.describe 'Managing OAuth Tokens' do describe 'POST /oauth/token' do subject do post '/oauth/token', params: params @@ -104,4 +104,23 @@ RSpec.describe 'Obtaining OAuth Tokens' do end end end + + describe 'POST /oauth/revoke' do + subject { post '/oauth/revoke', params: { client_id: application.uid, token: access_token.token } } + + let!(:user) { Fabricate(:user) } + let!(:application) { Fabricate(:application, confidential: false) } + let!(:access_token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: application) } + let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) } + + it 'revokes the token and removes subscriptions' do + expect { subject } + .to change { access_token.reload.revoked_at }.from(nil).to(be_present) + + expect(Web::PushSubscription.where(access_token: access_token).count) + .to eq(0) + expect { web_push_subscription.reload } + .to raise_error(ActiveRecord::RecordNotFound) + end + end end diff --git a/spec/requests/settings/deletes_spec.rb b/spec/requests/settings/deletes_spec.rb index 4563f639d5..c277181999 100644 --- a/spec/requests/settings/deletes_spec.rb +++ b/spec/requests/settings/deletes_spec.rb @@ -4,13 +4,65 @@ require 'rails_helper' RSpec.describe 'Settings Deletes' do describe 'DELETE /settings/delete' do - before { sign_in Fabricate(:user) } + context 'when signed in' do + before { sign_in(user) } - it 'gracefully handles invalid nested params' do - delete settings_delete_path(form_delete_confirmation: 'invalid') + let(:user) { Fabricate(:user) } - expect(response) - .to have_http_status(400) + it 'gracefully handles invalid nested params' do + delete settings_delete_path(form_delete_confirmation: 'invalid') + + expect(response) + .to have_http_status(400) + end + + context 'when suspended' do + let(:user) { Fabricate(:user, account_attributes: { suspended_at: Time.now.utc }) } + + it 'returns http forbidden' do + delete settings_delete_path + + expect(response) + .to have_http_status(403) + end + end + end + + context 'when not signed in' do + it 'redirects to sign in' do + delete settings_delete_path + + expect(response) + .to redirect_to(new_user_session_path) + end + end + end + + describe 'GET /settings/delete' do + context 'when signed in' do + before { sign_in(user) } + + context 'when suspended' do + let(:user) { Fabricate(:user, account_attributes: { suspended_at: Time.now.utc }) } + + it 'returns http forbidden with private cache control headers' do + get settings_delete_path + + expect(response) + .to have_http_status(403) + expect(response.headers['Cache-Control']) + .to include('private, no-store') + end + end + end + + context 'when not signed in' do + it 'redirects to sign in' do + get settings_delete_path + + expect(response) + .to redirect_to(new_user_session_path) + end end end end diff --git a/spec/services/activitypub/synchronize_followers_service_spec.rb b/spec/services/activitypub/synchronize_followers_service_spec.rb index 974368b7d7..70f27627e1 100644 --- a/spec/services/activitypub/synchronize_followers_service_spec.rb +++ b/spec/services/activitypub/synchronize_followers_service_spec.rb @@ -10,7 +10,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do let(:bob) { Fabricate(:account, username: 'bob') } let(:eve) { Fabricate(:account, username: 'eve') } let(:mallory) { Fabricate(:account, username: 'mallory') } - let(:collection_uri) { 'http://example.com/partial-followers' } + let(:collection_uri) { 'https://example.com/partial-followers' } let(:items) do [alice, eve, mallory].map do |account| @@ -27,14 +27,14 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do }.with_indifferent_access end + before do + alice.follow!(actor) + bob.follow!(actor) + mallory.request_follow!(actor) + end + shared_examples 'synchronizes followers' do before do - alice.follow!(actor) - bob.follow!(actor) - mallory.request_follow!(actor) - - allow(ActivityPub::DeliveryWorker).to receive(:perform_async) - subject.call(actor, collection_uri) end @@ -46,7 +46,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do expect(mallory) .to be_following(actor) # Convert follow request to follow when accepted expect(ActivityPub::DeliveryWorker) - .to have_received(:perform_async).with(anything, eve.id, actor.inbox_url) # Send Undo Follow to actor + .to have_enqueued_sidekiq_job(anything, eve.id, actor.inbox_url) # Send Undo Follow to actor end end @@ -76,7 +76,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do it_behaves_like 'synchronizes followers' end - context 'when the endpoint is a paginated Collection of actor URIs' do + context 'when the endpoint is a single-page paginated Collection of actor URIs' do let(:payload) do { '@context': 'https://www.w3.org/ns/activitystreams', @@ -96,5 +96,106 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do it_behaves_like 'synchronizes followers' end + + context 'when the endpoint is a paginated Collection of actor URIs split across multiple pages' do + before do + stub_request(:get, 'https://example.com/partial-followers') + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Collection', + id: 'https://example.com/partial-followers', + first: 'https://example.com/partial-followers/1', + })) + + stub_request(:get, 'https://example.com/partial-followers/1') + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'CollectionPage', + id: 'https://example.com/partial-followers/1', + partOf: 'https://example.com/partial-followers', + next: 'https://example.com/partial-followers/2', + items: [alice, eve].map { |account| ActivityPub::TagManager.instance.uri_for(account) }, + })) + + stub_request(:get, 'https://example.com/partial-followers/2') + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'CollectionPage', + id: 'https://example.com/partial-followers/2', + partOf: 'https://example.com/partial-followers', + items: ActivityPub::TagManager.instance.uri_for(mallory), + })) + end + + it_behaves_like 'synchronizes followers' + end + + context 'when the endpoint is a paginated Collection of actor URIs split across, but one page errors out' do + before do + stub_request(:get, 'https://example.com/partial-followers') + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Collection', + id: 'https://example.com/partial-followers', + first: 'https://example.com/partial-followers/1', + })) + + stub_request(:get, 'https://example.com/partial-followers/1') + .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'CollectionPage', + id: 'https://example.com/partial-followers/1', + partOf: 'https://example.com/partial-followers', + next: 'https://example.com/partial-followers/2', + items: [mallory].map { |account| ActivityPub::TagManager.instance.uri_for(account) }, + })) + + stub_request(:get, 'https://example.com/partial-followers/2') + .to_return(status: 404) + end + + it 'confirms pending follow request but does not remove extra followers' do + previous_follower_ids = actor.followers.pluck(:id) + + subject.call(actor, collection_uri) + + expect(previous_follower_ids - actor.followers.reload.pluck(:id)) + .to be_empty + expect(mallory) + .to be_following(actor) + end + end + + context 'when the endpoint is a paginated Collection of actor URIs with more pages than we allow' do + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Collection', + id: collection_uri, + first: { + type: 'CollectionPage', + partOf: collection_uri, + items: items, + next: "#{collection_uri}/page2", + }, + }.with_indifferent_access + end + + before do + stub_const('ActivityPub::SynchronizeFollowersService::MAX_COLLECTION_PAGES', 1) + stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' }) + end + + it 'confirms pending follow request but does not remove extra followers' do + previous_follower_ids = actor.followers.pluck(:id) + + subject.call(actor, collection_uri) + + expect(previous_follower_ids - actor.followers.reload.pluck(:id)) + .to be_empty + expect(mallory) + .to be_following(actor) + end + end end end diff --git a/spec/services/suspend_account_service_spec.rb b/spec/services/suspend_account_service_spec.rb index 4a2f494e0c..c15c23ca30 100644 --- a/spec/services/suspend_account_service_spec.rb +++ b/spec/services/suspend_account_service_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe SuspendAccountService, :inline_jobs do +RSpec.describe SuspendAccountService do shared_examples 'common behavior' do subject { described_class.new.call(account) } @@ -11,6 +11,7 @@ RSpec.describe SuspendAccountService, :inline_jobs do before do allow(FeedManager.instance).to receive_messages(unmerge_from_home: nil, unmerge_from_list: nil) + allow(Rails.configuration.x).to receive(:cache_buster_enabled).and_return(true) local_follower.follow!(account) list.accounts << account @@ -23,6 +24,7 @@ RSpec.describe SuspendAccountService, :inline_jobs do it 'unmerges from feeds of local followers and changes file mode and preserves suspended flag' do expect { subject } .to change_file_mode + .and enqueue_sidekiq_job(CacheBusterWorker).with(account.media_attachments.first.file.url(:original)) .and not_change_suspended_flag expect(FeedManager.instance).to have_received(:unmerge_from_home).with(account, local_follower) expect(FeedManager.instance).to have_received(:unmerge_from_list).with(account, list) @@ -38,17 +40,12 @@ RSpec.describe SuspendAccountService, :inline_jobs do end describe 'suspending a local account' do - def match_update_actor_request(req, account) - json = JSON.parse(req.body) + def match_update_actor_request(json, account) + json = JSON.parse(json) actor_id = ActivityPub::TagManager.instance.uri_for(account) json['type'] == 'Update' && json['actor'] == actor_id && json['object']['id'] == actor_id && json['object']['suspended'] end - before do - stub_request(:post, 'https://alice.com/inbox').to_return(status: 201) - stub_request(:post, 'https://bob.com/inbox').to_return(status: 201) - end - include_examples 'common behavior' do let!(:account) { Fabricate(:account) } let!(:remote_follower) { Fabricate(:account, uri: 'https://alice.com', inbox_url: 'https://alice.com/inbox', protocol: :activitypub, domain: 'alice.com') } @@ -61,22 +58,20 @@ RSpec.describe SuspendAccountService, :inline_jobs do it 'sends an Update actor activity to followers and reporters' do subject - expect(a_request(:post, remote_follower.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once - expect(a_request(:post, remote_reporter.inbox_url).with { |req| match_update_actor_request(req, account) }).to have_been_made.once + + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(satisfying { |json| match_update_actor_request(json, account) }, account.id, remote_follower.inbox_url).once + .and have_enqueued_sidekiq_job(satisfying { |json| match_update_actor_request(json, account) }, account.id, remote_reporter.inbox_url).once end end end describe 'suspending a remote account' do - def match_reject_follow_request(req, account, followee) - json = JSON.parse(req.body) + def match_reject_follow_request(json, account, followee) + json = JSON.parse(json) json['type'] == 'Reject' && json['actor'] == ActivityPub::TagManager.instance.uri_for(followee) && json['object']['actor'] == account.uri end - before do - stub_request(:post, 'https://bob.com/inbox').to_return(status: 201) - end - include_examples 'common behavior' do let!(:account) { Fabricate(:account, domain: 'bob.com', uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) } let!(:local_followee) { Fabricate(:account) } @@ -88,7 +83,8 @@ RSpec.describe SuspendAccountService, :inline_jobs do it 'sends a Reject Follow activity', :aggregate_failures do subject - expect(a_request(:post, account.inbox_url).with { |req| match_reject_follow_request(req, account, local_followee) }).to have_been_made.once + expect(ActivityPub::DeliveryWorker) + .to have_enqueued_sidekiq_job(satisfying { |json| match_reject_follow_request(json, account, local_followee) }, local_followee.id, account.inbox_url).once end end end diff --git a/spec/support/signed_request_helpers.rb b/spec/support/signed_request_helpers.rb index 8a52179cae..a4423af748 100644 --- a/spec/support/signed_request_helpers.rb +++ b/spec/support/signed_request_helpers.rb @@ -18,4 +18,24 @@ module SignedRequestHelpers super(path, headers: headers, **args) end + + def post(path, headers: nil, sign_with: nil, **args) + return super(path, headers: headers, **args) if sign_with.nil? + + headers ||= {} + headers['Date'] = Time.now.utc.httpdate + headers['Host'] = Rails.configuration.x.local_domain + headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest(args[:params].to_s)}" + + signed_headers = headers.merge('(request-target)' => "post #{path}").slice('(request-target)', 'Host', 'Date', 'Digest') + + key_id = ActivityPub::TagManager.instance.key_uri_for(sign_with) + keypair = sign_with.keypair + signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n") + signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) + + headers['Signature'] = "keyId=\"#{key_id}\",algorithm=\"rsa-sha256\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\"" + + super(path, headers: headers, **args) + end end diff --git a/spec/system/account_notes_spec.rb b/spec/system/account_notes_spec.rb new file mode 100644 index 0000000000..4677068f6a --- /dev/null +++ b/spec/system/account_notes_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Account notes', :inline_jobs, :js, :streaming do + include ProfileStories + + let(:email) { 'test@example.com' } + let(:password) { 'password' } + let(:confirmed_at) { Time.zone.now } + let(:finished_onboarding) { true } + + let!(:other_account) { Fabricate(:account) } + + before { as_a_logged_in_user } + + it 'can be written and viewed' do + visit_profile(other_account) + + note_text = 'This is a personal note' + fill_in 'Click to add note', with: note_text + + # This is a bit awkward since there is no button to save the change + # The easiest way is to send ctrl+enter ourselves + find_field(class: 'account__header__account-note__content').send_keys [:control, :enter] + + within('.account__header__account-note .inline-alert') do + expect(page) + .to have_content('SAVED') + end + + expect(page) + .to have_css('.account__header__account-note__content', text: note_text) + + # Navigate back and forth and ensure the comment is still here + visit root_url + visit_profile(other_account) + + expect(AccountNote.find_by(account: bob.account, target_account: other_account).comment) + .to eq note_text + + expect(page) + .to have_css('.account__header__account-note__content', text: note_text) + end + + def visit_profile(account) + visit short_account_path(account) + + expect(page) + .to have_css('div.app-holder') + .and have_css('form.compose-form') + end +end diff --git a/spec/system/admin/account_actions_spec.rb b/spec/system/admin/account_actions_spec.rb new file mode 100644 index 0000000000..787b988a0d --- /dev/null +++ b/spec/system/admin/account_actions_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Admin Account Actions' do + let(:user) { Fabricate(:admin_user) } + + before { sign_in user } + + describe 'Creating a new account action on an account' do + let(:account) { Fabricate(:account) } + + it 'creates the action and redirects to the account page' do + visit new_admin_account_action_path(account_id: account.id) + expect(page) + .to have_title(I18n.t('admin.account_actions.title', acct: account.pretty_acct)) + + choose(option: 'silence') + expect { submit_form } + .to change { account.strikes.count }.by(1) + expect(page) + .to have_title(account.pretty_acct) + end + + def submit_form + click_on I18n.t('admin.account_actions.action') + end + end +end diff --git a/spec/controllers/admin/action_logs_controller_spec.rb b/spec/system/admin/action_logs_spec.rb similarity index 65% rename from spec/controllers/admin/action_logs_controller_spec.rb rename to spec/system/admin/action_logs_spec.rb index d3108e8055..b6a6996f91 100644 --- a/spec/controllers/admin/action_logs_controller_spec.rb +++ b/spec/system/admin/action_logs_spec.rb @@ -2,29 +2,33 @@ require 'rails_helper' -RSpec.describe Admin::ActionLogsController do - render_views - +RSpec.describe 'Admin Action Logs' do # Action logs typically cause issues when their targets are not in the database let!(:account) { Fabricate(:account) } before do - orphaned_log_types.map do |type| - Fabricate(:action_log, account: account, action: 'destroy', target_type: type, target_id: 1312) - end + populate_action_logs + sign_in Fabricate(:admin_user) end - describe 'GET #index' do - it 'returns 200' do - sign_in Fabricate(:admin_user) - get :index, params: { page: 1 } + describe 'Viewing action logs' do + it 'shows page with action logs listed' do + visit admin_action_logs_path - expect(response).to have_http_status(200) + expect(page) + .to have_title(I18n.t('admin.action_logs.title')) + .and have_css('.log-entry') end end private + def populate_action_logs + orphaned_log_types.map do |type| + Fabricate(:action_log, account: account, action: 'destroy', target_type: type, target_id: 1312) + end + end + def orphaned_log_types %w( Account diff --git a/spec/system/admin/change_emails_spec.rb b/spec/system/admin/change_emails_spec.rb new file mode 100644 index 0000000000..6592ddff7c --- /dev/null +++ b/spec/system/admin/change_emails_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Admin Change Emails' do + let(:admin) { Fabricate(:admin_user) } + + before { sign_in admin } + + describe 'Changing the email address for a user', :inline_jobs do + let(:user) { Fabricate :user } + + it 'updates user details and sends email' do + visit admin_account_change_email_path(user.account.id) + expect(page) + .to have_title(I18n.t('admin.accounts.change_email.title', username: user.account.username)) + + fill_in 'user_unconfirmed_email', with: 'test@host.example' + emails = capture_emails { process_change_email } + expect(emails.first) + .to be_present + .and(deliver_to('test@host.example')) + .and(have_subject(/Confirm email/)) + expect(page) + .to have_title(user.account.pretty_acct) + end + + def process_change_email + expect { click_on I18n.t('admin.accounts.change_email.submit') } + .to not_change { user.reload.email } + .and(change { user.reload.unconfirmed_email }.to('test@host.example')) + .and(change { user.reload.confirmation_token }.from(nil).to(be_present)) + end + end +end diff --git a/spec/controllers/admin/dashboard_controller_spec.rb b/spec/system/admin/dashboard_spec.rb similarity index 50% rename from spec/controllers/admin/dashboard_controller_spec.rb rename to spec/system/admin/dashboard_spec.rb index 5a1ea848cc..06d31cde44 100644 --- a/spec/controllers/admin/dashboard_controller_spec.rb +++ b/spec/system/admin/dashboard_spec.rb @@ -2,10 +2,8 @@ require 'rails_helper' -RSpec.describe Admin::DashboardController do - render_views - - describe 'GET #index' do +RSpec.describe 'Admin Dashboard' do + describe 'Viewing the dashboard page' do let(:user) { Fabricate(:owner_user) } before do @@ -14,14 +12,12 @@ RSpec.describe Admin::DashboardController do sign_in(user) end - it 'returns http success and body with system check messages' do - get :index + it 'returns page with system check messages' do + visit admin_dashboard_path - expect(response) - .to have_http_status(200) - .and have_attributes( - body: include(I18n.t('admin.system_checks.software_version_patch_check.message_html')) - ) + expect(page) + .to have_title(I18n.t('admin.dashboard.title')) + .and have_content(I18n.t('admin.system_checks.software_version_patch_check.message_html')) end private diff --git a/spec/system/admin/users/two_factor_authentications_spec.rb b/spec/system/admin/users/two_factor_authentications_spec.rb new file mode 100644 index 0000000000..e09bc437b4 --- /dev/null +++ b/spec/system/admin/users/two_factor_authentications_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'webauthn/fake_client' + +RSpec.describe 'Admin Users TwoFactorAuthentications' do + let(:user) { Fabricate(:user) } + + before { sign_in Fabricate(:admin_user) } + + describe 'Disabling 2FA for users' do + before { stub_webauthn_credential } + + let(:fake_client) { WebAuthn::FakeClient.new('http://test.host') } + + context 'when user has OTP enabled' do + before { user.update(otp_required_for_login: true) } + + it 'disables OTP and redirects to admin account page' do + visit admin_account_path(user.account.id) + + expect { disable_two_factor } + .to change { user.reload.otp_enabled? }.to(false) + expect(page) + .to have_title(user.account.pretty_acct) + end + end + + context 'when user has OTP and WebAuthn enabled' do + before { user.update(otp_required_for_login: true, webauthn_id: WebAuthn.generate_user_id) } + + it 'disables OTP and webauthn and redirects to admin account page' do + visit admin_account_path(user.account.id) + + expect { disable_two_factor } + .to change { user.reload.otp_enabled? }.to(false) + .and(change { user.reload.webauthn_enabled? }.to(false)) + expect(page) + .to have_title(user.account.pretty_acct) + end + end + + def disable_two_factor + click_on I18n.t('admin.accounts.disable_two_factor_authentication') + end + + def stub_webauthn_credential + public_key_credential = WebAuthn::Credential.from_create(fake_client.create) + Fabricate( + :webauthn_credential, + external_id: public_key_credential.id, + nickname: 'Security Key', + public_key: public_key_credential.public_key, + user_id: user.id + ) + end + end +end diff --git a/spec/system/disputes/strikes_spec.rb b/spec/system/disputes/strikes_spec.rb new file mode 100644 index 0000000000..d2b6b08c46 --- /dev/null +++ b/spec/system/disputes/strikes_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Disputes Strikes' do + before { sign_in(current_user) } + + describe 'viewing strike disputes' do + let(:current_user) { Fabricate(:user) } + let!(:strike) { Fabricate(:account_warning, target_account: current_user.account) } + + it 'shows a list of strikes and details for each' do + visit disputes_strikes_path + expect(page) + .to have_title(I18n.t('settings.strikes')) + + find('.strike-entry').click + expect(page) + .to have_title(strike_page_title) + .and have_content(strike.text) + end + + def strike_page_title + I18n.t('disputes.strikes.title', action: I18n.t(strike.action, scope: 'disputes.strikes.title_actions'), date: I18n.l(strike.created_at.to_date)) + end + end +end diff --git a/spec/system/filters/statuses_spec.rb b/spec/system/filters/statuses_spec.rb new file mode 100644 index 0000000000..b353bd8674 --- /dev/null +++ b/spec/system/filters/statuses_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Filters Statuses' do + describe 'Viewing statuses under a filter' do + let(:filter) { Fabricate(:custom_filter, title: 'good filter') } + + context 'with the filter user signed in' do + before { sign_in(filter.account.user) } + + it 'returns a page with the status filters' do + visit filter_statuses_path(filter) + + expect(page) + .to have_private_cache_control + .and have_title(/good filter/) + end + end + end +end diff --git a/spec/system/settings/deletes_spec.rb b/spec/system/settings/deletes_spec.rb new file mode 100644 index 0000000000..91f7104252 --- /dev/null +++ b/spec/system/settings/deletes_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Settings Deletes' do + describe 'Deleting user from settings area' do + let(:user) { Fabricate(:user) } + + before { sign_in(user) } + + it 'requires password and deletes user record', :inline_jobs do + visit settings_delete_path + expect(page) + .to have_title(I18n.t('settings.delete')) + .and have_private_cache_control + + # Wrong confirmation value + fill_in 'form_delete_confirmation_password', with: 'wrongvalue' + click_on I18n.t('deletes.proceed') + expect(page) + .to have_content(I18n.t('deletes.challenge_not_passed')) + + # Correct confirmation value + fill_in 'form_delete_confirmation_password', with: user.password + click_on I18n.t('deletes.proceed') + expect(page) + .to have_content(I18n.t('deletes.success_msg')) + expect(page) + .to have_title(I18n.t('auth.login')) + expect(User.find_by(id: user.id)) + .to be_nil + expect(user.account.reload) + .to be_suspended + expect(CanonicalEmailBlock.block?(user.email)) + .to be(false) + end + end +end diff --git a/spec/validators/date_of_birth_validator_spec.rb b/spec/validators/date_of_birth_validator_spec.rb new file mode 100644 index 0000000000..33e69e811b --- /dev/null +++ b/spec/validators/date_of_birth_validator_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe DateOfBirthValidator do + let(:record_class) do + Class.new do + include ActiveModel::Validations + + attr_accessor :date_of_birth + + validates :date_of_birth, date_of_birth: true + end + end + + let(:record) { record_class.new } + + before do + Setting.min_age = 16 + end + + describe '#validate_each' do + context 'with an invalid date' do + it 'adds errors' do + record.date_of_birth = '76.830.10' + + expect(record).to_not be_valid + expect(record.errors.first.attribute).to eq(:date_of_birth) + expect(record.errors.first.type).to eq(:invalid) + end + end + + context 'with a date below age limit' do + it 'adds errors' do + record.date_of_birth = 13.years.ago.strftime('%d.%m.%Y') + + expect(record).to_not be_valid + expect(record.errors.first.attribute).to eq(:date_of_birth) + expect(record.errors.first.type).to eq(:below_limit) + end + end + + context 'with a date above age limit' do + it 'does not add errors' do + record.date_of_birth = 16.years.ago.strftime('%d.%m.%Y') + + expect(record).to be_valid + end + end + end +end diff --git a/streaming/database.js b/streaming/database.js index 60a3b34ef0..553c9149cc 100644 --- a/streaming/database.js +++ b/streaming/database.js @@ -49,7 +49,7 @@ export function configFromEnv(env, environment) { if (typeof parsedUrl.password === 'string') baseConfig.password = parsedUrl.password; if (typeof parsedUrl.host === 'string') baseConfig.host = parsedUrl.host; if (typeof parsedUrl.user === 'string') baseConfig.user = parsedUrl.user; - if (typeof parsedUrl.port === 'string') { + if (typeof parsedUrl.port === 'string' && parsedUrl.port) { const parsedPort = parseInt(parsedUrl.port, 10); if (isNaN(parsedPort)) { throw new Error('Invalid port specified in DATABASE_URL environment variable'); diff --git a/yarn.lock b/yarn.lock index 4e3bbb98e9..7bb1dd27c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -109,6 +109,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.27.0": + version: 7.27.0 + resolution: "@babel/generator@npm:7.27.0" + dependencies: + "@babel/parser": "npm:^7.27.0" + "@babel/types": "npm:^7.27.0" + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.25" + jsesc: "npm:^3.0.2" + checksum: 10c0/7cb10693d2b365c278f109a745dc08856cae139d262748b77b70ce1d97da84627f79648cab6940d847392c0e5d180441669ed958b3aee98d9c7d274b37c553bd + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.25.9": version: 7.25.9 resolution: "@babel/helper-annotate-as-pure@npm:7.25.9" @@ -142,19 +155,19 @@ __metadata: linkType: hard "@babel/helper-create-class-features-plugin@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-create-class-features-plugin@npm:7.25.9" + version: 7.27.0 + resolution: "@babel/helper-create-class-features-plugin@npm:7.27.0" dependencies: "@babel/helper-annotate-as-pure": "npm:^7.25.9" "@babel/helper-member-expression-to-functions": "npm:^7.25.9" "@babel/helper-optimise-call-expression": "npm:^7.25.9" - "@babel/helper-replace-supers": "npm:^7.25.9" + "@babel/helper-replace-supers": "npm:^7.26.5" "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.9" - "@babel/traverse": "npm:^7.25.9" + "@babel/traverse": "npm:^7.27.0" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/b2bdd39f38056a76b9ba00ec5b209dd84f5c5ebd998d0f4033cf0e73d5f2c357fbb49d1ce52db77a2709fb29ee22321f84a5734dc9914849bdfee9ad12ce8caf + checksum: 10c0/c4945903136d934050e070f69a4d72ec425f1f70634e0ddf14ad36695f935125a6df559f8d5b94cc1ed49abd4ce9c5be8ef3ba033fa8d09c5dd78d1a9b97d8cc languageName: node linkType: hard @@ -248,16 +261,16 @@ __metadata: languageName: node linkType: hard -"@babel/helper-replace-supers@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-replace-supers@npm:7.25.9" +"@babel/helper-replace-supers@npm:^7.25.9, @babel/helper-replace-supers@npm:^7.26.5": + version: 7.26.5 + resolution: "@babel/helper-replace-supers@npm:7.26.5" dependencies: "@babel/helper-member-expression-to-functions": "npm:^7.25.9" "@babel/helper-optimise-call-expression": "npm:^7.25.9" - "@babel/traverse": "npm:^7.25.9" + "@babel/traverse": "npm:^7.26.5" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/0b40d7d2925bd3ba4223b3519e2e4d2456d471ad69aa458f1c1d1783c80b522c61f8237d3a52afc9e47c7174129bbba650df06393a6787d5722f2ec7f223c3f4 + checksum: 10c0/b19b1245caf835207aaaaac3a494f03a16069ae55e76a2e1350b5acd560e6a820026997a8160e8ebab82ae873e8208759aa008eb8422a67a775df41f0a4633d4 languageName: node linkType: hard @@ -324,6 +337,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.27.0": + version: 7.27.0 + resolution: "@babel/parser@npm:7.27.0" + dependencies: + "@babel/types": "npm:^7.27.0" + bin: + parser: ./bin/babel-parser.js + checksum: 10c0/ba2ed3f41735826546a3ef2a7634a8d10351df221891906e59b29b0a0cd748f9b0e7a6f07576858a9de8e77785aad925c8389ddef146de04ea2842047c9d2859 + languageName: node + linkType: hard + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.25.9": version: 7.25.9 resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.25.9" @@ -1422,6 +1446,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.27.0": + version: 7.27.0 + resolution: "@babel/template@npm:7.27.0" + dependencies: + "@babel/code-frame": "npm:^7.26.2" + "@babel/parser": "npm:^7.27.0" + "@babel/types": "npm:^7.27.0" + checksum: 10c0/13af543756127edb5f62bf121f9b093c09a2b6fe108373887ccffc701465cfbcb17e07cf48aa7f440415b263f6ec006e9415c79dfc2e8e6010b069435f81f340 + languageName: node + linkType: hard + "@babel/traverse@npm:^7.25.0, @babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.26.8, @babel/traverse@npm:^7.26.9": version: 7.26.9 resolution: "@babel/traverse@npm:7.26.9" @@ -1437,6 +1472,21 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.26.5, @babel/traverse@npm:^7.27.0": + version: 7.27.0 + resolution: "@babel/traverse@npm:7.27.0" + dependencies: + "@babel/code-frame": "npm:^7.26.2" + "@babel/generator": "npm:^7.27.0" + "@babel/parser": "npm:^7.27.0" + "@babel/template": "npm:^7.27.0" + "@babel/types": "npm:^7.27.0" + debug: "npm:^4.3.1" + globals: "npm:^11.1.0" + checksum: 10c0/c7af29781960dacaae51762e8bc6c4b13d6ab4b17312990fbca9fc38e19c4ad7fecaae24b1cf52fb844e8e6cdc76c70ad597f90e496bcb3cc0a1d66b41a0aa5b + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.0.0-beta.49, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.9, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": version: 7.26.9 resolution: "@babel/types@npm:7.26.9" @@ -1447,6 +1497,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.27.0": + version: 7.27.0 + resolution: "@babel/types@npm:7.27.0" + dependencies: + "@babel/helper-string-parser": "npm:^7.25.9" + "@babel/helper-validator-identifier": "npm:^7.25.9" + checksum: 10c0/6f1592eabe243c89a608717b07b72969be9d9d2fce1dee21426238757ea1fa60fdfc09b29de9e48d8104311afc6e6fb1702565a9cc1e09bc1e76f2b2ddb0f6e1 + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -2767,6 +2827,7 @@ __metadata: "@github/webauthn-json": "npm:^2.1.1" "@hello-pangea/dnd": "npm:^17.0.0" "@rails/ujs": "npm:7.1.501" + "@react-spring/web": "npm:^9.7.5" "@reduxjs/toolkit": "npm:^2.0.1" "@svgr/webpack": "npm:^5.5.0" "@testing-library/dom": "npm:^10.2.0" @@ -2803,6 +2864,7 @@ __metadata: "@types/webpack-env": "npm:^1.18.4" "@typescript-eslint/eslint-plugin": "npm:^8.0.0" "@typescript-eslint/parser": "npm:^8.0.0" + "@use-gesture/react": "npm:^10.3.1" arrow-key-navigation: "npm:^1.2.0" async-mutex: "npm:^0.5.0" axios: "npm:^1.4.0" @@ -2869,7 +2931,6 @@ __metadata: react-immutable-pure-component: "npm:^2.2.2" react-intl: "npm:^7.0.0" react-motion: "npm:^0.5.2" - react-notification: "npm:^6.8.5" react-overlays: "npm:^5.2.1" react-redux: "npm:^9.0.4" react-redux-loading-bar: "npm:^5.0.8" @@ -3218,6 +3279,72 @@ __metadata: languageName: node linkType: hard +"@react-spring/animated@npm:~9.7.5": + version: 9.7.5 + resolution: "@react-spring/animated@npm:9.7.5" + dependencies: + "@react-spring/shared": "npm:~9.7.5" + "@react-spring/types": "npm:~9.7.5" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10c0/f8c2473c60f39a878c7dd0fdfcfcdbc720521e1506aa3f63c9de64780694a0a73d5ccc535a5ccec3520ddb70a71cf43b038b32c18e99531522da5388c510ecd7 + languageName: node + linkType: hard + +"@react-spring/core@npm:~9.7.5": + version: 9.7.5 + resolution: "@react-spring/core@npm:9.7.5" + dependencies: + "@react-spring/animated": "npm:~9.7.5" + "@react-spring/shared": "npm:~9.7.5" + "@react-spring/types": "npm:~9.7.5" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10c0/5bfd83dfe248cd91889f215f015d908c7714ef445740fd5afa054b27ebc7d5a456abf6c309e2459d9b5b436e78d6fda16b62b9601f96352e9130552c02270830 + languageName: node + linkType: hard + +"@react-spring/rafz@npm:~9.7.5": + version: 9.7.5 + resolution: "@react-spring/rafz@npm:9.7.5" + checksum: 10c0/8bdad180feaa9a0e870a513043a5e98a4e9b7292a9f887575b7e6fadab2677825bc894b7ff16c38511b35bfe6cc1072df5851c5fee64448d67551559578ca759 + languageName: node + linkType: hard + +"@react-spring/shared@npm:~9.7.5": + version: 9.7.5 + resolution: "@react-spring/shared@npm:9.7.5" + dependencies: + "@react-spring/rafz": "npm:~9.7.5" + "@react-spring/types": "npm:~9.7.5" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10c0/0207eacccdedd918a2fc55e78356ce937f445ce27ad9abd5d3accba8f9701a39349b55115641dc2b39bb9d3a155b058c185b411d292dc8cc5686bfa56f73b94f + languageName: node + linkType: hard + +"@react-spring/types@npm:~9.7.5": + version: 9.7.5 + resolution: "@react-spring/types@npm:9.7.5" + checksum: 10c0/85c05121853cacb64f7cf63a4855e9044635e1231f70371cd7b8c78bc10be6f4dd7c68f592f92a2607e8bb68051540989b4677a2ccb525dba937f5cd95dc8bc1 + languageName: node + linkType: hard + +"@react-spring/web@npm:^9.7.5": + version: 9.7.5 + resolution: "@react-spring/web@npm:9.7.5" + dependencies: + "@react-spring/animated": "npm:~9.7.5" + "@react-spring/core": "npm:~9.7.5" + "@react-spring/shared": "npm:~9.7.5" + "@react-spring/types": "npm:~9.7.5" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 10c0/bcd1e052e1b16341a12a19bf4515f153ca09d1fa86ff7752a5d02d7c4db58e8baf80e6283e64411f1e388c65340dce2254b013083426806b5dbae38bd151e53e + languageName: node + linkType: hard + "@reduxjs/toolkit@npm:^2.0.1": version: 2.6.1 resolution: "@reduxjs/toolkit@npm:2.6.1" @@ -4443,6 +4570,24 @@ __metadata: languageName: node linkType: hard +"@use-gesture/core@npm:10.3.1": + version: 10.3.1 + resolution: "@use-gesture/core@npm:10.3.1" + checksum: 10c0/2e3b5c0f7fe26cdb47be3a9c2a58a6a9edafc5b2895b07d2898eda9ab5a2b29fb0098b15597baa0856907b593075cd44cc69bba4785c9cfb7b6fabaa3b52cd3e + languageName: node + linkType: hard + +"@use-gesture/react@npm:^10.3.1": + version: 10.3.1 + resolution: "@use-gesture/react@npm:10.3.1" + dependencies: + "@use-gesture/core": "npm:10.3.1" + peerDependencies: + react: ">= 16.8.0" + checksum: 10c0/978da66e4e7c424866ad52eba8fdf0ce93a4c8fc44f8837c7043e68c6a6107cd67e817fffb27f7db2ae871ef2f6addb0c8ddf1586f24c67b7e6aef1646c668cf + languageName: node + linkType: hard + "@webassemblyjs/ast@npm:1.9.0": version: 1.9.0 resolution: "@webassemblyjs/ast@npm:1.9.0" @@ -14769,17 +14914,6 @@ __metadata: languageName: node linkType: hard -"react-notification@npm:^6.8.5": - version: 6.8.5 - resolution: "react-notification@npm:6.8.5" - dependencies: - prop-types: "npm:^15.6.2" - peerDependencies: - react: ^0.14.0 || ^15.0.0 || ^16.0.0 - checksum: 10c0/14ffb71a5b18301830699b814d1de2421f4f43f31df5b95efd95cd47548a0d7597ec58abc16a12191958cad398495eba9274193af3294863e2864d32ea79f2c6 - languageName: node - linkType: hard - "react-overlays@npm:^5.2.1": version: 5.2.1 resolution: "react-overlays@npm:5.2.1"