diff --git a/.ruby-version b/.ruby-version index fa7adc7ac72a28..9c25013dbb862f 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.3.5 +3.3.6 diff --git a/Dockerfile b/Dockerfile index c6d8466f43c048..a6c7d46b579ad8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ ARG BUILDPLATFORM=${BUILDPLATFORM} # Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.x"] # renovate: datasource=docker depName=docker.io/ruby -ARG RUBY_VERSION="3.3.5" +ARG RUBY_VERSION="3.3.6" # # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] # renovate: datasource=node-version depName=node ARG NODE_MAJOR_VERSION="22" diff --git a/Gemfile b/Gemfile index 0c3484a7aa9fd1..47506929b064ed 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,7 @@ ruby '>= 3.2.0' gem 'propshaft' gem 'puma', '~> 6.3' gem 'rack', '~> 2.2.7' -gem 'rails', '~> 7.1.1' +gem 'rails', '~> 7.2.0' gem 'thor', '~> 1.2' gem 'dotenv' @@ -25,7 +25,7 @@ gem 'ruby-vips', '~> 2.2', require: false gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.8' gem 'bootsnap', '~> 1.18.0', require: false -gem 'browser', '< 6' # https://github.com/fnando/browser/issues/543 +gem 'browser' gem 'charlock_holmes', '~> 0.7.7' gem 'chewy', '~> 7.3' gem 'devise', '~> 4.9' @@ -47,13 +47,14 @@ gem 'color_diff', '~> 0.1' gem 'csv', '~> 3.2' gem 'discard', '~> 1.2' gem 'doorkeeper', '~> 5.6' +gem 'faraday-httpclient' gem 'fast_blank', '~> 1.0' gem 'fastimage' gem 'hiredis', '~> 0.6' gem 'htmlentities', '~> 4.3' gem 'http', '~> 5.2.0' gem 'http_accept_language', '~> 2.1' -gem 'httplog', '~> 1.7.0' +gem 'httplog', '~> 1.7.0', require: false gem 'i18n' gem 'idn-ruby', require: 'idn' gem 'inline_svg' @@ -62,6 +63,7 @@ gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'mime-types', '~> 3.6.0', require: 'mime/types/columnar' +gem 'mutex_m' gem 'nokogiri', '~> 1.15' gem 'oj', '~> 3.14' gem 'ox', '~> 2.14' @@ -220,7 +222,7 @@ gem 'concurrent-ruby', require: false gem 'connection_pool', require: false gem 'xorcist', '~> 1.1' -gem 'net-http', '~> 0.4.0' +gem 'net-http', '~> 0.5.0' gem 'rubyzip', '~> 2.3' gem 'hcaptcha', '~> 7.1' diff --git a/Gemfile.lock b/Gemfile.lock index c7148381704ed4..1888485af08110 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,51 +10,46 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.1.4.2) - actionpack (= 7.1.4.2) - activesupport (= 7.1.4.2) + actioncable (7.2.2) + actionpack (= 7.2.2) + activesupport (= 7.2.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.4.2) - actionpack (= 7.1.4.2) - activejob (= 7.1.4.2) - activerecord (= 7.1.4.2) - activestorage (= 7.1.4.2) - activesupport (= 7.1.4.2) - mail (>= 2.7.1) - net-imap - net-pop - net-smtp - actionmailer (7.1.4.2) - actionpack (= 7.1.4.2) - actionview (= 7.1.4.2) - activejob (= 7.1.4.2) - activesupport (= 7.1.4.2) - mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp + actionmailbox (7.2.2) + actionpack (= 7.2.2) + activejob (= 7.2.2) + activerecord (= 7.2.2) + activestorage (= 7.2.2) + activesupport (= 7.2.2) + mail (>= 2.8.0) + actionmailer (7.2.2) + actionpack (= 7.2.2) + actionview (= 7.2.2) + activejob (= 7.2.2) + activesupport (= 7.2.2) + mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.1.4.2) - actionview (= 7.1.4.2) - activesupport (= 7.1.4.2) + actionpack (7.2.2) + actionview (= 7.2.2) + activesupport (= 7.2.2) nokogiri (>= 1.8.5) racc - rack (>= 2.2.4) + rack (>= 2.2.4, < 3.2) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actiontext (7.1.4.2) - actionpack (= 7.1.4.2) - activerecord (= 7.1.4.2) - activestorage (= 7.1.4.2) - activesupport (= 7.1.4.2) + useragent (~> 0.16) + actiontext (7.2.2) + actionpack (= 7.2.2) + activerecord (= 7.2.2) + activestorage (= 7.2.2) + activesupport (= 7.2.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.4.2) - activesupport (= 7.1.4.2) + actionview (7.2.2) + activesupport (= 7.2.2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) @@ -64,31 +59,33 @@ GEM activemodel (>= 4.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (7.1.4.2) - activesupport (= 7.1.4.2) + activejob (7.2.2) + activesupport (= 7.2.2) globalid (>= 0.3.6) - activemodel (7.1.4.2) - activesupport (= 7.1.4.2) - activerecord (7.1.4.2) - activemodel (= 7.1.4.2) - activesupport (= 7.1.4.2) + activemodel (7.2.2) + activesupport (= 7.2.2) + activerecord (7.2.2) + activemodel (= 7.2.2) + activesupport (= 7.2.2) timeout (>= 0.4.0) - activestorage (7.1.4.2) - actionpack (= 7.1.4.2) - activejob (= 7.1.4.2) - activerecord (= 7.1.4.2) - activesupport (= 7.1.4.2) + activestorage (7.2.2) + actionpack (= 7.2.2) + activejob (= 7.2.2) + activerecord (= 7.2.2) + activesupport (= 7.2.2) marcel (~> 1.0) - activesupport (7.1.4.2) + activesupport (7.2.2) base64 + benchmark (>= 0.3) bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) + concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) - mutex_m - tzinfo (~> 2.0) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) aes_key_wrap (1.1.0) @@ -100,8 +97,8 @@ GEM attr_required (1.0.2) awrence (1.2.1) aws-eventstream (1.3.0) - aws-partitions (1.997.0) - aws-sdk-core (3.211.0) + aws-partitions (1.1001.0) + aws-sdk-core (3.212.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -109,7 +106,7 @@ GEM aws-sdk-kms (1.95.0) aws-sdk-core (~> 3, >= 3.210.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.169.0) + aws-sdk-s3 (1.170.0) aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -120,6 +117,7 @@ GEM base64 (0.2.0) bcp47_spec (0.2.1) bcrypt (3.1.20) + benchmark (0.3.0) better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) @@ -133,7 +131,7 @@ GEM msgpack (~> 1.2) brakeman (6.2.2) racc - browser (5.3.1) + browser (6.0.0) brpoplpush-redis_script (0.1.3) concurrent-ruby (~> 1.0, >= 1.0.5) redis (>= 1.0, < 6) @@ -200,8 +198,8 @@ GEM devise (>= 4.0.0) rpam2 (~> 4.0) diff-lcs (1.5.1) - discard (1.3.0) - activerecord (>= 4.2, < 8) + discard (1.4.0) + activerecord (>= 4.2, < 9.0) docile (1.4.1) domain_name (0.6.20240107) doorkeeper (5.7.1) @@ -229,29 +227,14 @@ GEM fabrication (2.31.0) faker (3.5.1) i18n (>= 1.8.11, < 2) - faraday (1.10.3) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) - ruby2_keywords (>= 0.0.4) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) - faraday-net_http (1.0.2) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.3) + faraday (2.12.0) + faraday-net_http (>= 2.0, < 3.4) + json + logger + faraday-httpclient (2.0.1) + httpclient (>= 2.2) + faraday-net_http (3.3.0) + net-http fast_blank (1.0.1) fastimage (2.3.1) ffi (1.17.0) @@ -362,7 +345,7 @@ GEM rack (>= 2.2, < 4) rdf (~> 3.3) rexml (~> 3.2) - json-ld-preloaded (3.3.0) + json-ld-preloaded (3.3.1) json-ld (~> 3.3) rdf (~> 3.3) json-schema (5.0.1) @@ -430,9 +413,8 @@ GEM minitest (5.25.1) msgpack (1.7.3) multi_json (1.15.0) - multipart-post (2.4.1) mutex_m (0.2.0) - net-http (0.4.1) + net-http (0.5.0) uri net-imap (0.5.0) date @@ -448,7 +430,7 @@ GEM nokogiri (1.16.7) mini_portile2 (~> 2.8.2) racc (~> 1.4) - oj (3.16.6) + oj (3.16.7) bigdecimal (>= 3.0) ostruct (>= 0.2) omniauth (2.1.2) @@ -629,20 +611,20 @@ GEM rackup (1.0.0) rack (< 3) webrick - rails (7.1.4.2) - actioncable (= 7.1.4.2) - actionmailbox (= 7.1.4.2) - actionmailer (= 7.1.4.2) - actionpack (= 7.1.4.2) - actiontext (= 7.1.4.2) - actionview (= 7.1.4.2) - activejob (= 7.1.4.2) - activemodel (= 7.1.4.2) - activerecord (= 7.1.4.2) - activestorage (= 7.1.4.2) - activesupport (= 7.1.4.2) + rails (7.2.2) + actioncable (= 7.2.2) + actionmailbox (= 7.2.2) + actionmailer (= 7.2.2) + actionpack (= 7.2.2) + actiontext (= 7.2.2) + actionview (= 7.2.2) + activejob (= 7.2.2) + activemodel (= 7.2.2) + activerecord (= 7.2.2) + activestorage (= 7.2.2) + activesupport (= 7.2.2) bundler (>= 1.15.0) - railties (= 7.1.4.2) + railties (= 7.2.2) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -657,10 +639,10 @@ GEM rails-i18n (7.0.10) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - railties (7.1.4.2) - actionpack (= 7.1.4.2) - activesupport (= 7.1.4.2) - irb + railties (7.2.2) + actionpack (= 7.2.2) + activesupport (= 7.2.2) + irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) @@ -760,7 +742,6 @@ GEM ruby-vips (2.2.2) ffi (~> 1.12) logger - ruby2_keywords (0.0.5) rubyzip (2.3.2) rufus-scheduler (3.9.1) fugit (~> 1.1, >= 1.1.6) @@ -772,6 +753,7 @@ GEM scenic (1.8.0) activerecord (>= 4.0.0) railties (>= 4.0.0) + securerandom (0.3.1) selenium-webdriver (4.26.0) base64 (~> 0.2) logger (~> 1.4) @@ -855,6 +837,7 @@ GEM unf_ext (0.0.9.1) unicode-display_width (2.6.0) uri (0.13.1) + useragent (0.16.10) validate_email (0.1.6) activemodel (>= 3.0) mail (>= 2.2.5) @@ -908,7 +891,7 @@ DEPENDENCIES blurhash (~> 0.1) bootsnap (~> 1.18.0) brakeman (~> 6.0) - browser (< 6) + browser bundler-audit (~> 0.9) capybara (~> 3.39) charlock_holmes (~> 0.7.7) @@ -930,6 +913,7 @@ DEPENDENCIES email_spec fabrication (~> 2.30) faker (~> 3.2) + faraday-httpclient fast_blank (~> 1.0) fastimage flatware-rspec @@ -962,7 +946,8 @@ DEPENDENCIES mario-redis-lock (~> 1.2) memory_profiler mime-types (~> 3.6.0) - net-http (~> 0.4.0) + mutex_m + net-http (~> 0.5.0) net-ldap (~> 0.18) nokogiri (~> 1.15) oj (~> 3.14) @@ -1000,7 +985,7 @@ DEPENDENCIES rack-attack (~> 6.6) rack-cors (~> 2.0) rack-test (~> 2.1) - rails (~> 7.1.1) + rails (~> 7.2.0) rails-controller-testing (~> 1.0) rails-i18n (~> 7.0) rdf-normalize (~> 0.5) diff --git a/Rakefile b/Rakefile index e51cf0e17e838a..488c551fee2cd1 100644 --- a/Rakefile +++ b/Rakefile @@ -3,6 +3,6 @@ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require File.expand_path('config/application', __dir__) +require_relative 'config/application' Rails.application.load_tasks diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb index faa0a061a6ddd1..fe822d8c999d25 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -5,7 +5,7 @@ class EmailDomainBlocksController < BaseController def index authorize :email_domain_block, :index? - @email_domain_blocks = EmailDomainBlock.where(parent_id: nil).includes(:children).order(id: :desc).page(params[:page]) + @email_domain_blocks = EmailDomainBlock.parents.includes(:children).order(id: :desc).page(params[:page]) @form = Form::EmailDomainBlockBatch.new end diff --git a/app/controllers/admin/invites_controller.rb b/app/controllers/admin/invites_controller.rb index dabfe97655f933..614e2a32d000f3 100644 --- a/app/controllers/admin/invites_controller.rb +++ b/app/controllers/admin/invites_controller.rb @@ -32,7 +32,7 @@ def destroy def deactivate_all authorize :invite, :deactivate_all? - Invite.available.in_batches.update_all(expires_at: Time.now.utc) + Invite.available.in_batches.touch_all(:expires_at) redirect_to admin_invites_path end diff --git a/app/controllers/api/v1/annual_reports_controller.rb b/app/controllers/api/v1/annual_reports_controller.rb index 9bc8e68ac2430b..b1aee288dd8595 100644 --- a/app/controllers/api/v1/annual_reports_controller.rb +++ b/app/controllers/api/v1/annual_reports_controller.rb @@ -17,6 +17,17 @@ def index relationships: @relationships end + def show + with_read_replica do + @presenter = AnnualReportsPresenter.new([@annual_report]) + @relationships = StatusRelationshipsPresenter.new(@presenter.statuses, current_account.id) + end + + render json: @presenter, + serializer: REST::AnnualReportsSerializer, + relationships: @relationships + end + def read @annual_report.view! render_empty diff --git a/app/helpers/registration_helper.rb b/app/helpers/registration_helper.rb index ef5462ac887413..002d167c05833a 100644 --- a/app/helpers/registration_helper.rb +++ b/app/helpers/registration_helper.rb @@ -16,6 +16,6 @@ def omniauth_only? end def ip_blocked?(remote_ip) - IpBlock.where(severity: :sign_up_block).exists?(['ip >>= ?', remote_ip.to_s]) + IpBlock.severity_sign_up_block.containing(remote_ip.to_s).exists? end end diff --git a/app/javascript/flavours/glitch/api_types/notifications.ts b/app/javascript/flavours/glitch/api_types/notifications.ts index 17c2ede32bd86e..d173083dbd2143 100644 --- a/app/javascript/flavours/glitch/api_types/notifications.ts +++ b/app/javascript/flavours/glitch/api_types/notifications.ts @@ -20,6 +20,7 @@ export const allNotificationTypes = [ 'admin.report', 'moderation_warning', 'severed_relationships', + 'annual_report', ]; export type NotificationWithStatusType = @@ -37,7 +38,8 @@ export type NotificationType = | 'moderation_warning' | 'severed_relationships' | 'admin.sign_up' - | 'admin.report'; + | 'admin.report' + | 'annual_report'; export interface BaseNotificationJSON { id: string; @@ -130,6 +132,15 @@ interface AccountRelationshipSeveranceNotificationJSON event: ApiAccountRelationshipSeveranceEventJSON; } +export interface ApiAnnualReportEventJSON { + year: string; +} + +interface AnnualReportNotificationGroupJSON extends BaseNotificationGroupJSON { + type: 'annual_report'; + annual_report: ApiAnnualReportEventJSON; +} + export type ApiNotificationJSON = | SimpleNotificationJSON | ReportNotificationJSON @@ -142,7 +153,8 @@ export type ApiNotificationGroupJSON = | ReportNotificationGroupJSON | AccountRelationshipSeveranceNotificationGroupJSON | NotificationGroupWithStatusJSON - | ModerationWarningNotificationGroupJSON; + | ModerationWarningNotificationGroupJSON + | AnnualReportNotificationGroupJSON; export interface ApiNotificationGroupsResultJSON { accounts: ApiAccountJSON[]; diff --git a/app/javascript/flavours/glitch/components/media_gallery.jsx b/app/javascript/flavours/glitch/components/media_gallery.jsx index f2f8b21e3b5389..46ff875ab68910 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.jsx +++ b/app/javascript/flavours/glitch/components/media_gallery.jsx @@ -98,12 +98,12 @@ class Item extends PureComponent { height = 50; } - if (attachment.get('description')?.length > 0) { - badges.push(); - } - const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); + if (description?.length > 0) { + badges.push(); + } + if (attachment.get('type') === 'unknown') { return (
diff --git a/app/javascript/flavours/glitch/components/modal_root.jsx b/app/javascript/flavours/glitch/components/modal_root.jsx index 71b875cfeeb547..bfe9efea06e218 100644 --- a/app/javascript/flavours/glitch/components/modal_root.jsx +++ b/app/javascript/flavours/glitch/components/modal_root.jsx @@ -13,11 +13,14 @@ class ModalRoot extends PureComponent { static propTypes = { children: PropTypes.node, onClose: PropTypes.func.isRequired, - backgroundColor: PropTypes.shape({ - r: PropTypes.number, - g: PropTypes.number, - b: PropTypes.number, - }), + backgroundColor: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({ + r: PropTypes.number, + g: PropTypes.number, + b: PropTypes.number, + }), + ]), noEsc: PropTypes.bool, ignoreFocus: PropTypes.bool, ...WithOptionalRouterPropTypes, @@ -146,14 +149,17 @@ class ModalRoot extends PureComponent { let backgroundColor = null; - if (this.props.backgroundColor) { - backgroundColor = multiply({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.7 }); + if (this.props.backgroundColor && typeof this.props.backgroundColor === 'string') { + backgroundColor = this.props.backgroundColor; + } else if (this.props.backgroundColor) { + const darkenedColor = multiply({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.7 }); + backgroundColor = `rgb(${darkenedColor.r}, ${darkenedColor.g}, ${darkenedColor.b})`; } return (
-
+
{children}
diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index bbf947a4592e42..71a1d14d37f362 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -583,12 +583,15 @@ class Status extends ImmutablePureComponent { let prepend, rebloggedByText; + const matchedFilters = status.get('matched_filters'); + if (hidden) { return (
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} - {status.get('content')} + {status.get('spoiler_text').length > 0 && ({status.get('spoiler_text')})} + {isExpanded && {status.get('content')}}
); @@ -597,7 +600,6 @@ class Status extends ImmutablePureComponent { const connectUp = previousId && previousId === status.get('in_reply_to_id'); const connectToRoot = rootId && rootId === status.get('in_reply_to_id'); const connectReply = nextInReplyToId && nextInReplyToId === status.get('id'); - const matchedFilters = status.get('matched_filters'); if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) { const minHandlers = this.props.muted ? {} : { diff --git a/app/javascript/flavours/glitch/features/annual_report/archetype.tsx b/app/javascript/flavours/glitch/features/annual_report/archetype.tsx new file mode 100644 index 00000000000000..604f401039e1bb --- /dev/null +++ b/app/javascript/flavours/glitch/features/annual_report/archetype.tsx @@ -0,0 +1,69 @@ +import { FormattedMessage } from 'react-intl'; + +import booster from '@/images/archetypes/booster.png'; +import lurker from '@/images/archetypes/lurker.png'; +import oracle from '@/images/archetypes/oracle.png'; +import pollster from '@/images/archetypes/pollster.png'; +import replier from '@/images/archetypes/replier.png'; +import type { Archetype as ArchetypeData } from 'flavours/glitch/models/annual_report'; + +export const Archetype: React.FC<{ + data: ArchetypeData; +}> = ({ data }) => { + let illustration, label; + + switch (data) { + case 'booster': + illustration = booster; + label = ( + + ); + break; + case 'replier': + illustration = replier; + label = ( + + ); + break; + case 'pollster': + illustration = pollster; + label = ( + + ); + break; + case 'lurker': + illustration = lurker; + label = ( + + ); + break; + case 'oracle': + illustration = oracle; + label = ( + + ); + break; + } + + return ( +
+
{label}
+ +
+ ); +}; diff --git a/app/javascript/flavours/glitch/features/annual_report/followers.tsx b/app/javascript/flavours/glitch/features/annual_report/followers.tsx new file mode 100644 index 00000000000000..e5238705d797f0 --- /dev/null +++ b/app/javascript/flavours/glitch/features/annual_report/followers.tsx @@ -0,0 +1,69 @@ +import { FormattedMessage, FormattedNumber } from 'react-intl'; + +import { Sparklines, SparklinesCurve } from 'react-sparklines'; + +import { ShortNumber } from 'flavours/glitch/components/short_number'; +import type { TimeSeriesMonth } from 'flavours/glitch/models/annual_report'; + +export const Followers: React.FC<{ + data: TimeSeriesMonth[]; + total?: number; +}> = ({ data, total }) => { + const change = data.reduce((sum, item) => sum + item.followers, 0); + + const cumulativeGraph = data.reduce( + (newData, item) => [ + ...newData, + item.followers + (newData[newData.length - 1] ?? 0), + ], + [0], + ); + + return ( +
+ + + + + + + + + + + + + +
+
+ {change > -1 ? '+' : '-'} + +
+ +
+ + + +
+ }} + /> +
+
+
+
+ ); +}; diff --git a/app/javascript/flavours/glitch/features/annual_report/highlighted_post.tsx b/app/javascript/flavours/glitch/features/annual_report/highlighted_post.tsx new file mode 100644 index 00000000000000..e66752b53eb9dc --- /dev/null +++ b/app/javascript/flavours/glitch/features/annual_report/highlighted_post.tsx @@ -0,0 +1,109 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return, + @typescript-eslint/no-explicit-any, + @typescript-eslint/no-unsafe-assignment */ + +import { useCallback } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { toggleStatusSpoilers } from 'flavours/glitch/actions/statuses'; +import { DetailedStatus } from 'flavours/glitch/features/status/components/detailed_status'; +import { me } from 'flavours/glitch/initial_state'; +import type { TopStatuses } from 'flavours/glitch/models/annual_report'; +import { + makeGetStatus, + makeGetPictureInPicture, +} from 'flavours/glitch/selectors'; +import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; + +const getStatus = makeGetStatus() as unknown as (arg0: any, arg1: any) => any; +const getPictureInPicture = makeGetPictureInPicture() as unknown as ( + arg0: any, + arg1: any, +) => any; + +export const HighlightedPost: React.FC<{ + data: TopStatuses; +}> = ({ data }) => { + let statusId, label; + + if (data.by_reblogs) { + statusId = data.by_reblogs; + label = ( + + ); + } else if (data.by_favourites) { + statusId = data.by_favourites; + label = ( + + ); + } else { + statusId = data.by_replies; + label = ( + + ); + } + + const dispatch = useAppDispatch(); + const domain = useAppSelector((state) => state.meta.get('domain')); + const status = useAppSelector((state) => + statusId ? getStatus(state, { id: statusId }) : undefined, + ); + const pictureInPicture = useAppSelector((state) => + statusId ? getPictureInPicture(state, { id: statusId }) : undefined, + ); + const account = useAppSelector((state) => + me ? state.accounts.get(me) : undefined, + ); + + const handleToggleHidden = useCallback(() => { + dispatch(toggleStatusSpoilers(statusId)); + }, [dispatch, statusId]); + + if (!status) { + return ( +
+ ); + } + + const displayName = ( + + + + ), + }} + /> + + {label} + + ); + + return ( +
+ +
+ ); +}; diff --git a/app/javascript/flavours/glitch/features/annual_report/index.tsx b/app/javascript/flavours/glitch/features/annual_report/index.tsx new file mode 100644 index 00000000000000..0c737934b4bea1 --- /dev/null +++ b/app/javascript/flavours/glitch/features/annual_report/index.tsx @@ -0,0 +1,99 @@ +import { useState, useEffect } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { + importFetchedStatuses, + importFetchedAccounts, +} from 'flavours/glitch/actions/importer'; +import { apiRequestGet, apiRequestPost } from 'flavours/glitch/api'; +import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; +import { me } from 'flavours/glitch/initial_state'; +import type { Account } from 'flavours/glitch/models/account'; +import type { AnnualReport as AnnualReportData } from 'flavours/glitch/models/annual_report'; +import type { Status } from 'flavours/glitch/models/status'; +import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; + +import { Archetype } from './archetype'; +import { Followers } from './followers'; +import { HighlightedPost } from './highlighted_post'; +import { MostUsedHashtag } from './most_used_hashtag'; +import { NewPosts } from './new_posts'; +import { Percentile } from './percentile'; + +interface AnnualReportResponse { + annual_reports: AnnualReportData[]; + accounts: Account[]; + statuses: Status[]; +} + +export const AnnualReport: React.FC<{ + year: string; +}> = ({ year }) => { + const [response, setResponse] = useState(null); + const [loading, setLoading] = useState(false); + const currentAccount = useAppSelector((state) => + me ? state.accounts.get(me) : undefined, + ); + const dispatch = useAppDispatch(); + + useEffect(() => { + setLoading(true); + + apiRequestGet(`v1/annual_reports/${year}`) + .then((data) => { + dispatch(importFetchedStatuses(data.statuses)); + dispatch(importFetchedAccounts(data.accounts)); + + setResponse(data); + setLoading(false); + + return apiRequestPost(`v1/annual_reports/${year}/read`); + }) + .catch(() => { + setLoading(false); + }); + }, [dispatch, year, setResponse, setLoading]); + + if (loading) { + return ; + } + + const report = response?.annual_reports[0]; + + if (!report) { + return null; + } + + return ( +
+
+

+ +

+

+ +

+
+ +
+ + + + + + +
+
+ ); +}; diff --git a/app/javascript/flavours/glitch/features/annual_report/most_used_app.tsx b/app/javascript/flavours/glitch/features/annual_report/most_used_app.tsx new file mode 100644 index 00000000000000..f78a02b296a008 --- /dev/null +++ b/app/javascript/flavours/glitch/features/annual_report/most_used_app.tsx @@ -0,0 +1,29 @@ +import { FormattedMessage } from 'react-intl'; + +import type { NameAndCount } from 'flavours/glitch/models/annual_report'; + +export const MostUsedApp: React.FC<{ + data: NameAndCount[]; +}> = ({ data }) => { + const app = data[0]; + + if (!app) { + return ( +
+ ); + } + + return ( +
+
+ {app.name} +
+
+ +
+
+ ); +}; diff --git a/app/javascript/flavours/glitch/features/annual_report/most_used_hashtag.tsx b/app/javascript/flavours/glitch/features/annual_report/most_used_hashtag.tsx new file mode 100644 index 00000000000000..a6d5e08fe154ca --- /dev/null +++ b/app/javascript/flavours/glitch/features/annual_report/most_used_hashtag.tsx @@ -0,0 +1,29 @@ +import { FormattedMessage } from 'react-intl'; + +import type { NameAndCount } from 'flavours/glitch/models/annual_report'; + +export const MostUsedHashtag: React.FC<{ + data: NameAndCount[]; +}> = ({ data }) => { + const hashtag = data[0]; + + if (!hashtag) { + return ( +
+ ); + } + + return ( +
+
+ #{hashtag.name} +
+
+ +
+
+ ); +}; diff --git a/app/javascript/flavours/glitch/features/annual_report/new_posts.tsx b/app/javascript/flavours/glitch/features/annual_report/new_posts.tsx new file mode 100644 index 00000000000000..4ca286debb68d5 --- /dev/null +++ b/app/javascript/flavours/glitch/features/annual_report/new_posts.tsx @@ -0,0 +1,53 @@ +import { FormattedNumber, FormattedMessage } from 'react-intl'; + +import ChatBubbleIcon from '@/material-icons/400-24px/chat_bubble.svg?react'; +import type { TimeSeriesMonth } from 'flavours/glitch/models/annual_report'; + +export const NewPosts: React.FC<{ + data: TimeSeriesMonth[]; +}> = ({ data }) => { + const posts = data.reduce((sum, item) => sum + item.statuses, 0); + + return ( +
+ + + + + + + + + + + +
+ +
+
+ +
+
+ ); +}; diff --git a/app/javascript/flavours/glitch/features/annual_report/percentile.tsx b/app/javascript/flavours/glitch/features/annual_report/percentile.tsx new file mode 100644 index 00000000000000..322171ba212290 --- /dev/null +++ b/app/javascript/flavours/glitch/features/annual_report/percentile.tsx @@ -0,0 +1,53 @@ +/* eslint-disable react/jsx-no-useless-fragment */ +import { FormattedMessage, FormattedNumber } from 'react-intl'; + +import type { Percentiles } from 'flavours/glitch/models/annual_report'; + +export const Percentile: React.FC<{ + data: Percentiles; +}> = ({ data }) => { + const percentile = data.statuses; + + return ( +
+ ( +
+ {str} +
+ ), + percentage: () => ( +
+ +
+ ), + bottomLabel: (str) => ( +
+
+ {str} +
+ + {percentile < 6 && ( +
+ +
+ )} +
+ ), + }} + > + {(message) => <>{message}} +
+
+ ); +}; diff --git a/app/javascript/flavours/glitch/features/follow_requests/index.jsx b/app/javascript/flavours/glitch/features/follow_requests/index.jsx index a8f40a31d0d007..7d651f2ca69c24 100644 --- a/app/javascript/flavours/glitch/features/follow_requests/index.jsx +++ b/app/javascript/flavours/glitch/features/follow_requests/index.jsx @@ -68,7 +68,7 @@ class FollowRequests extends ImmutablePureComponent { ); return ( - + = ({ notification: { annualReport }, unread }) => { + const dispatch = useAppDispatch(); + const year = annualReport.year; + + const handleClick = useCallback(() => { + dispatch( + openModal({ + modalType: 'ANNUAL_REPORT', + modalProps: { year }, + }), + ); + }, [dispatch, year]); + + return ( +
+
+ +
+ +
+

+ +

+ +
+
+ ); +}; diff --git a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group.tsx b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group.tsx index f4275179c5efe9..9bd6c27a86acd8 100644 --- a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group.tsx +++ b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group.tsx @@ -9,6 +9,7 @@ import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; import { NotificationAdminReport } from './notification_admin_report'; import { NotificationAdminSignUp } from './notification_admin_sign_up'; +import { NotificationAnnualReport } from './notification_annual_report'; import { NotificationFavourite } from './notification_favourite'; import { NotificationFollow } from './notification_follow'; import { NotificationFollowRequest } from './notification_follow_request'; @@ -143,6 +144,14 @@ export const NotificationGroup: React.FC<{ /> ); break; + case 'annual_report': + content = ( + + ); + break; default: return null; } diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.tsx b/app/javascript/flavours/glitch/features/status/components/detailed_status.tsx index 0b08e88cf8f606..c6f0900c5fe2bf 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.tsx +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.tsx @@ -51,6 +51,7 @@ export const DetailedStatus: React.FC<{ domain: string; showMedia?: boolean; withLogo?: boolean; + overrideDisplayName?: React.ReactNode; pictureInPicture: any; onToggleHidden?: (status: any) => void; onToggleMediaVisibility?: () => void; @@ -65,6 +66,7 @@ export const DetailedStatus: React.FC<{ domain, showMedia, withLogo, + overrideDisplayName, pictureInPicture, onToggleMediaVisibility, onToggleHidden, @@ -378,7 +380,11 @@ export const DetailedStatus: React.FC<{
- + + {overrideDisplayName ?? ( + + )} + {withLogo && ( <>
diff --git a/app/javascript/flavours/glitch/features/ui/components/annual_report_modal.tsx b/app/javascript/flavours/glitch/features/ui/components/annual_report_modal.tsx new file mode 100644 index 00000000000000..b7f5db04ace68e --- /dev/null +++ b/app/javascript/flavours/glitch/features/ui/components/annual_report_modal.tsx @@ -0,0 +1,21 @@ +import { useEffect } from 'react'; + +import { AnnualReport } from 'flavours/glitch/features/annual_report'; + +const AnnualReportModal: React.FC<{ + year: string; + onChangeBackgroundColor: (arg0: string) => void; +}> = ({ year, onChangeBackgroundColor }) => { + useEffect(() => { + onChangeBackgroundColor('var(--indigo-1)'); + }, [onChangeBackgroundColor]); + + return ( +
+ +
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export default AnnualReportModal; diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx b/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx index 83345905becb97..506cccfbb2224d 100644 --- a/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx @@ -20,6 +20,7 @@ import { SubscribedLanguagesModal, ClosedRegistrationsModal, IgnoreNotificationsModal, + AnnualReportModal, } from 'flavours/glitch/features/ui/util/async-components'; import { getScrollbarWidth } from 'flavours/glitch/utils/scrollbar'; @@ -82,6 +83,7 @@ export const MODAL_COMPONENTS = { 'INTERACTION': InteractionModal, 'CLOSED_REGISTRATIONS': ClosedRegistrationsModal, 'IGNORE_NOTIFICATIONS': IgnoreNotificationsModal, + 'ANNUAL_REPORT': AnnualReportModal, }; export default class ModalRoot extends PureComponent { diff --git a/app/javascript/flavours/glitch/features/ui/util/async-components.js b/app/javascript/flavours/glitch/features/ui/util/async-components.js index c7f2e6cff9c813..1354c21f09b07e 100644 --- a/app/javascript/flavours/glitch/features/ui/util/async-components.js +++ b/app/javascript/flavours/glitch/features/ui/util/async-components.js @@ -229,3 +229,7 @@ export function NotificationRequest () { export function LinkTimeline () { return import(/*webpackChunkName: "features/glitch/link_timeline" */'../../link_timeline'); } + +export function AnnualReportModal () { + return import(/*webpackChunkName: "flavours/glitch/async/modals/annual_report_modal" */'../components/annual_report_modal'); +} diff --git a/app/javascript/flavours/glitch/models/annual_report.ts b/app/javascript/flavours/glitch/models/annual_report.ts new file mode 100644 index 00000000000000..c0a101e6c872da --- /dev/null +++ b/app/javascript/flavours/glitch/models/annual_report.ts @@ -0,0 +1,44 @@ +export interface Percentiles { + followers: number; + statuses: number; +} + +export interface NameAndCount { + name: string; + count: number; +} + +export interface TimeSeriesMonth { + month: number; + statuses: number; + following: number; + followers: number; +} + +export interface TopStatuses { + by_reblogs: number; + by_favourites: number; + by_replies: number; +} + +export type Archetype = + | 'lurker' + | 'booster' + | 'pollster' + | 'replier' + | 'oracle'; + +interface AnnualReportV1 { + most_used_apps: NameAndCount[]; + percentiles: Percentiles; + top_hashtags: NameAndCount[]; + top_statuses: TopStatuses; + time_series: TimeSeriesMonth[]; + archetype: Archetype; +} + +export interface AnnualReport { + year: number; + schema_version: number; + data: AnnualReportV1; +} diff --git a/app/javascript/flavours/glitch/models/notification_group.ts b/app/javascript/flavours/glitch/models/notification_group.ts index 26a4d6be844687..ff963307c64106 100644 --- a/app/javascript/flavours/glitch/models/notification_group.ts +++ b/app/javascript/flavours/glitch/models/notification_group.ts @@ -1,6 +1,7 @@ import type { ApiAccountRelationshipSeveranceEventJSON, ApiAccountWarningJSON, + ApiAnnualReportEventJSON, BaseNotificationGroupJSON, ApiNotificationGroupJSON, ApiNotificationJSON, @@ -65,6 +66,12 @@ export interface NotificationGroupSeveredRelationships event: AccountRelationshipSeveranceEvent; } +type AnnualReportEvent = ApiAnnualReportEventJSON; +export interface NotificationGroupAnnualReport + extends BaseNotification<'annual_report'> { + annualReport: AnnualReportEvent; +} + interface Report extends Omit { targetAccountId: string; } @@ -86,7 +93,8 @@ export type NotificationGroup = | NotificationGroupModerationWarning | NotificationGroupSeveredRelationships | NotificationGroupAdminSignUp - | NotificationGroupAdminReport; + | NotificationGroupAdminReport + | NotificationGroupAnnualReport; function createReportFromJSON(reportJSON: ApiReportJSON): Report { const { target_account, ...report } = reportJSON; @@ -112,6 +120,12 @@ function createAccountRelationshipSeveranceEventFromJSON( return eventJson; } +function createAnnualReportEventFromJSON( + eventJson: ApiAnnualReportEventJSON, +): AnnualReportEvent { + return eventJson; +} + export function createNotificationGroupFromJSON( groupJson: ApiNotificationGroupJSON, ): NotificationGroup { @@ -145,7 +159,6 @@ export function createNotificationGroupFromJSON( event: createAccountRelationshipSeveranceEventFromJSON(group.event), sampleAccountIds, }; - case 'moderation_warning': { const { moderation_warning, ...groupWithoutModerationWarning } = group; return { @@ -154,6 +167,14 @@ export function createNotificationGroupFromJSON( sampleAccountIds, }; } + case 'annual_report': { + const { annual_report, ...groupWithoutAnnualReport } = group; + return { + ...groupWithoutAnnualReport, + annualReport: createAnnualReportEventFromJSON(annual_report), + sampleAccountIds, + }; + } default: return { sampleAccountIds, diff --git a/app/javascript/flavours/glitch/styles/annual_reports.scss b/app/javascript/flavours/glitch/styles/annual_reports.scss new file mode 100644 index 00000000000000..39784e3b5ea12b --- /dev/null +++ b/app/javascript/flavours/glitch/styles/annual_reports.scss @@ -0,0 +1,335 @@ +:root { + --indigo-1: #17063b; + --indigo-2: #2f0c7a; + --indigo-3: #562cfc; + --indigo-5: #858afa; + --indigo-6: #cccfff; + --lime: #baff3b; + --goldenrod-2: #ffc954; +} + +.annual-report { + flex: 0 0 auto; + background: var(--indigo-1); + padding: 24px; + + &__header { + margin-bottom: 16px; + + h1 { + font-size: 25px; + font-weight: 600; + line-height: 30px; + color: var(--lime); + margin-bottom: 8px; + } + + p { + font-size: 16px; + font-weight: 600; + line-height: 20px; + color: var(--indigo-6); + } + } + + &__bento { + display: grid; + gap: 8px; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr); + grid-template-rows: minmax(0, auto) minmax(0, 1fr) minmax(0, auto) minmax( + 0, + auto + ); + + &__box { + padding: 16px; + border-radius: 8px; + background: var(--indigo-2); + color: var(--indigo-5); + } + } + + &__summary { + &__most-boosted-post { + grid-column: span 2; + grid-row: span 2; + padding: 0; + + .status__content, + .content-warning { + color: var(--indigo-6); + } + + .detailed-status { + border: 0; + } + + .content-warning { + border: 0; + background: var(--indigo-1); + + .link-button { + color: var(--indigo-5); + } + } + + .detailed-status__meta__line { + border-bottom-color: var(--indigo-3); + } + + .detailed-status__meta { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .detailed-status__meta, + .poll__footer, + .poll__link, + .detailed-status .logo, + .detailed-status__display-name { + color: var(--indigo-5); + } + + .detailed-status__meta .animated-number, + .detailed-status__display-name strong { + color: var(--indigo-6); + } + + .poll__chart { + background-color: var(--indigo-3); + + &.leading { + background-color: var(--goldenrod-2); + } + } + } + + &__followers { + grid-column: span 1; + text-align: center; + position: relative; + overflow: hidden; + padding-block-start: 24px; + padding-block-end: 24px; + + --sparkline-gradient-top: rgba(86, 44, 252, 50%); + --sparkline-gradient-bottom: rgba(86, 44, 252, 0%); + + &__foreground { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + position: relative; + z-index: 1; + } + + &__number { + font-size: 31px; + font-weight: 600; + line-height: 37px; + color: var(--lime); + } + + &__label { + font-size: 14px; + font-weight: 600; + line-height: 17px; + color: var(--indigo-6); + } + + &__footnote { + display: block; + font-weight: 400; + opacity: 0.5; + } + + svg { + position: absolute; + bottom: 0; + inset-inline-end: 0; + pointer-events: none; + z-index: 0; + height: 70%; + width: auto; + + path:first-child { + fill: url('#gradient') !important; + fill-opacity: 1 !important; + } + + path:last-child { + stroke: var(--indigo-3) !important; + fill: none !important; + } + } + } + + &__archetype { + grid-column: span 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + gap: 8px; + padding: 0; + + img { + display: block; + width: 100%; + height: auto; + border-radius: 8px; + } + + &__label { + padding: 16px; + padding-bottom: 8px; + font-size: 14px; + line-height: 17px; + font-weight: 600; + color: var(--lime); + } + } + + &__most-used-app { + grid-column: span 1; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + box-sizing: border-box; + + &__label { + font-size: 14px; + line-height: 17px; + font-weight: 600; + color: var(--indigo-6); + } + + &__icon { + font-size: 14px; + line-height: 17px; + font-weight: 600; + color: var(--goldenrod-2); + } + } + + &__percentile { + grid-row: span 2; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + text-align: center; + text-wrap: balance; + padding: 16px 8px; + + &__label { + font-size: 14px; + line-height: 17px; + } + + &__number { + font-size: 61px; + font-weight: 600; + line-height: 73px; + color: var(--goldenrod-2); + } + + &__footnote { + font-size: 11px; + line-height: 14px; + opacity: 0.5; + } + } + + &__new-posts { + grid-column: span 2; + text-align: center; + position: relative; + overflow: hidden; + + &__label { + font-size: 20px; + font-weight: 600; + line-height: 24px; + color: var(--indigo-6); + z-index: 1; + position: relative; + } + + &__number { + font-size: 76px; + font-weight: 600; + line-height: 91px; + color: var(--goldenrod-2); + z-index: 1; + position: relative; + } + + svg { + position: absolute; + inset-inline-start: -7px; + top: -4px; + z-index: 0; + } + } + + &__most-used-hashtag { + grid-column: span 2; + text-align: center; + overflow: hidden; + + &__hashtag { + font-size: 42px; + font-weight: 600; + line-height: 58px; + color: var(--indigo-6); + margin-inline-start: -100%; + margin-inline-end: -100%; + } + + &__label { + font-size: 14px; + font-weight: 600; + line-height: 17px; + } + } + } +} + +.annual-report-modal { + max-width: 480px; + background: var(--indigo-1); + border-radius: 16px; + display: flex; + flex-direction: column; + overflow-y: auto; + + .loading-indicator .circular-progress { + color: var(--lime); + } + + @media screen and (max-width: $no-columns-breakpoint) { + border-bottom: 0; + border-radius: 16px 16px 0 0; + } +} + +.notification-group--annual-report { + .notification-group__icon { + color: var(--lime); + } + + .notification-group__main .link-button { + font-weight: 500; + color: var(--lime); + } +} diff --git a/app/javascript/flavours/glitch/styles/application.scss b/app/javascript/flavours/glitch/styles/application.scss index fd55960311d24d..ac58cca66a557d 100644 --- a/app/javascript/flavours/glitch/styles/application.scss +++ b/app/javascript/flavours/glitch/styles/application.scss @@ -15,6 +15,7 @@ @import 'polls'; @import 'modal'; @import 'emoji_picker'; +@import 'annual_reports'; @import 'about'; @import 'tables'; @import 'admin'; diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss index 5048798a0c3b84..f35f5702b404ee 100644 --- a/app/javascript/flavours/glitch/styles/components.scss +++ b/app/javascript/flavours/glitch/styles/components.scss @@ -1856,7 +1856,8 @@ body > [data-popper-placement] { .status__wrapper-direct, .notification-ungrouped--direct, -.notification-group--direct { +.notification-group--direct, +.notification-group--annual-report { background: rgba($ui-highlight-color, 0.05); &:focus { @@ -6241,7 +6242,8 @@ a.status-card { inset-inline-start: 0; inset-inline-end: 0; bottom: 0; - background: rgba($base-overlay-background, 0.7); + opacity: 0.9; + background: $base-overlay-background; transition: background 0.5s; } diff --git a/app/javascript/images/archetypes/booster.png b/app/javascript/images/archetypes/booster.png new file mode 100755 index 00000000000000..18c92dfb7d57cd Binary files /dev/null and b/app/javascript/images/archetypes/booster.png differ diff --git a/app/javascript/images/archetypes/lurker.png b/app/javascript/images/archetypes/lurker.png new file mode 100755 index 00000000000000..8e1d6451b0b405 Binary files /dev/null and b/app/javascript/images/archetypes/lurker.png differ diff --git a/app/javascript/images/archetypes/oracle.png b/app/javascript/images/archetypes/oracle.png new file mode 100755 index 00000000000000..2afd3c72e1fb84 Binary files /dev/null and b/app/javascript/images/archetypes/oracle.png differ diff --git a/app/javascript/images/archetypes/pollster.png b/app/javascript/images/archetypes/pollster.png new file mode 100755 index 00000000000000..b838fccdd65de7 Binary files /dev/null and b/app/javascript/images/archetypes/pollster.png differ diff --git a/app/javascript/images/archetypes/replier.png b/app/javascript/images/archetypes/replier.png new file mode 100755 index 00000000000000..b298d4221cc412 Binary files /dev/null and b/app/javascript/images/archetypes/replier.png differ diff --git a/app/javascript/mastodon/api_types/notifications.ts b/app/javascript/mastodon/api_types/notifications.ts index 28ba7eb5c20909..190d8c83966512 100644 --- a/app/javascript/mastodon/api_types/notifications.ts +++ b/app/javascript/mastodon/api_types/notifications.ts @@ -20,6 +20,7 @@ export const allNotificationTypes = [ 'admin.report', 'moderation_warning', 'severed_relationships', + 'annual_report', ]; export type NotificationWithStatusType = @@ -37,7 +38,8 @@ export type NotificationType = | 'moderation_warning' | 'severed_relationships' | 'admin.sign_up' - | 'admin.report'; + | 'admin.report' + | 'annual_report'; export interface BaseNotificationJSON { id: string; @@ -130,6 +132,15 @@ interface AccountRelationshipSeveranceNotificationJSON event: ApiAccountRelationshipSeveranceEventJSON; } +export interface ApiAnnualReportEventJSON { + year: string; +} + +interface AnnualReportNotificationGroupJSON extends BaseNotificationGroupJSON { + type: 'annual_report'; + annual_report: ApiAnnualReportEventJSON; +} + export type ApiNotificationJSON = | SimpleNotificationJSON | ReportNotificationJSON @@ -142,7 +153,8 @@ export type ApiNotificationGroupJSON = | ReportNotificationGroupJSON | AccountRelationshipSeveranceNotificationGroupJSON | NotificationGroupWithStatusJSON - | ModerationWarningNotificationGroupJSON; + | ModerationWarningNotificationGroupJSON + | AnnualReportNotificationGroupJSON; export interface ApiNotificationGroupsResultJSON { accounts: ApiAccountJSON[]; diff --git a/app/javascript/mastodon/components/media_gallery.jsx b/app/javascript/mastodon/components/media_gallery.jsx index e059978442a131..59963a0a9f56d1 100644 --- a/app/javascript/mastodon/components/media_gallery.jsx +++ b/app/javascript/mastodon/components/media_gallery.jsx @@ -97,12 +97,12 @@ class Item extends PureComponent { height = 50; } - if (attachment.get('description')?.length > 0) { - badges.push(); - } - const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); + if (description?.length > 0) { + badges.push(); + } + if (attachment.get('type') === 'unknown') { return (
diff --git a/app/javascript/mastodon/components/modal_root.jsx b/app/javascript/mastodon/components/modal_root.jsx index e7fa5e6f9a797e..b0d88fe8f90eb9 100644 --- a/app/javascript/mastodon/components/modal_root.jsx +++ b/app/javascript/mastodon/components/modal_root.jsx @@ -13,11 +13,14 @@ class ModalRoot extends PureComponent { static propTypes = { children: PropTypes.node, onClose: PropTypes.func.isRequired, - backgroundColor: PropTypes.shape({ - r: PropTypes.number, - g: PropTypes.number, - b: PropTypes.number, - }), + backgroundColor: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({ + r: PropTypes.number, + g: PropTypes.number, + b: PropTypes.number, + }), + ]), ignoreFocus: PropTypes.bool, ...WithOptionalRouterPropTypes, }; @@ -141,14 +144,17 @@ class ModalRoot extends PureComponent { let backgroundColor = null; - if (this.props.backgroundColor) { - backgroundColor = multiply({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.7 }); + if (this.props.backgroundColor && typeof this.props.backgroundColor === 'string') { + backgroundColor = this.props.backgroundColor; + } else if (this.props.backgroundColor) { + const darkenedColor = multiply({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.7 }); + backgroundColor = `rgb(${darkenedColor.r}, ${darkenedColor.g}, ${darkenedColor.b})`; } return (
-
+
{children}
diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 479213636818c9..96ccf3aad4f465 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -393,13 +393,16 @@ class Status extends ImmutablePureComponent { }; let media, statusAvatar, prepend, rebloggedByText; + const matchedFilters = status.get('matched_filters'); + const expanded = (!matchedFilters || this.state.showDespiteFilter) && (!status.get('hidden') || status.get('spoiler_text').length === 0); if (hidden) { return (
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} - {status.get('content')} + {status.get('spoiler_text').length > 0 && ({status.get('spoiler_text')})} + {expanded && {status.get('content')}}
); @@ -408,7 +411,6 @@ class Status extends ImmutablePureComponent { const connectUp = previousId && previousId === status.get('in_reply_to_id'); const connectToRoot = rootId && rootId === status.get('in_reply_to_id'); const connectReply = nextInReplyToId && nextInReplyToId === status.get('id'); - const matchedFilters = status.get('matched_filters'); if (featured) { prepend = ( @@ -538,7 +540,6 @@ class Status extends ImmutablePureComponent { } const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status); - const expanded = (!matchedFilters || this.state.showDespiteFilter) && (!status.get('hidden') || status.get('spoiler_text').length === 0); return ( diff --git a/app/javascript/mastodon/features/annual_report/archetype.tsx b/app/javascript/mastodon/features/annual_report/archetype.tsx new file mode 100644 index 00000000000000..fffbc1803ecbe0 --- /dev/null +++ b/app/javascript/mastodon/features/annual_report/archetype.tsx @@ -0,0 +1,69 @@ +import { FormattedMessage } from 'react-intl'; + +import booster from '@/images/archetypes/booster.png'; +import lurker from '@/images/archetypes/lurker.png'; +import oracle from '@/images/archetypes/oracle.png'; +import pollster from '@/images/archetypes/pollster.png'; +import replier from '@/images/archetypes/replier.png'; +import type { Archetype as ArchetypeData } from 'mastodon/models/annual_report'; + +export const Archetype: React.FC<{ + data: ArchetypeData; +}> = ({ data }) => { + let illustration, label; + + switch (data) { + case 'booster': + illustration = booster; + label = ( + + ); + break; + case 'replier': + illustration = replier; + label = ( + + ); + break; + case 'pollster': + illustration = pollster; + label = ( + + ); + break; + case 'lurker': + illustration = lurker; + label = ( + + ); + break; + case 'oracle': + illustration = oracle; + label = ( + + ); + break; + } + + return ( +
+
{label}
+ +
+ ); +}; diff --git a/app/javascript/mastodon/features/annual_report/followers.tsx b/app/javascript/mastodon/features/annual_report/followers.tsx new file mode 100644 index 00000000000000..196013ae9d6b0c --- /dev/null +++ b/app/javascript/mastodon/features/annual_report/followers.tsx @@ -0,0 +1,69 @@ +import { FormattedMessage, FormattedNumber } from 'react-intl'; + +import { Sparklines, SparklinesCurve } from 'react-sparklines'; + +import { ShortNumber } from 'mastodon/components/short_number'; +import type { TimeSeriesMonth } from 'mastodon/models/annual_report'; + +export const Followers: React.FC<{ + data: TimeSeriesMonth[]; + total?: number; +}> = ({ data, total }) => { + const change = data.reduce((sum, item) => sum + item.followers, 0); + + const cumulativeGraph = data.reduce( + (newData, item) => [ + ...newData, + item.followers + (newData[newData.length - 1] ?? 0), + ], + [0], + ); + + return ( +
+ + + + + + + + + + + + + +
+
+ {change > -1 ? '+' : '-'} + +
+ +
+ + + +
+ }} + /> +
+
+
+
+ ); +}; diff --git a/app/javascript/mastodon/features/annual_report/highlighted_post.tsx b/app/javascript/mastodon/features/annual_report/highlighted_post.tsx new file mode 100644 index 00000000000000..3a2a70713da64b --- /dev/null +++ b/app/javascript/mastodon/features/annual_report/highlighted_post.tsx @@ -0,0 +1,105 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return, + @typescript-eslint/no-explicit-any, + @typescript-eslint/no-unsafe-assignment */ + +import { useCallback } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { toggleStatusSpoilers } from 'mastodon/actions/statuses'; +import { DetailedStatus } from 'mastodon/features/status/components/detailed_status'; +import { me } from 'mastodon/initial_state'; +import type { TopStatuses } from 'mastodon/models/annual_report'; +import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors'; +import { useAppSelector, useAppDispatch } from 'mastodon/store'; + +const getStatus = makeGetStatus() as unknown as (arg0: any, arg1: any) => any; +const getPictureInPicture = makeGetPictureInPicture() as unknown as ( + arg0: any, + arg1: any, +) => any; + +export const HighlightedPost: React.FC<{ + data: TopStatuses; +}> = ({ data }) => { + let statusId, label; + + if (data.by_reblogs) { + statusId = data.by_reblogs; + label = ( + + ); + } else if (data.by_favourites) { + statusId = data.by_favourites; + label = ( + + ); + } else { + statusId = data.by_replies; + label = ( + + ); + } + + const dispatch = useAppDispatch(); + const domain = useAppSelector((state) => state.meta.get('domain')); + const status = useAppSelector((state) => + statusId ? getStatus(state, { id: statusId }) : undefined, + ); + const pictureInPicture = useAppSelector((state) => + statusId ? getPictureInPicture(state, { id: statusId }) : undefined, + ); + const account = useAppSelector((state) => + me ? state.accounts.get(me) : undefined, + ); + + const handleToggleHidden = useCallback(() => { + dispatch(toggleStatusSpoilers(statusId)); + }, [dispatch, statusId]); + + if (!status) { + return ( +
+ ); + } + + const displayName = ( + + + + ), + }} + /> + + {label} + + ); + + return ( +
+ +
+ ); +}; diff --git a/app/javascript/mastodon/features/annual_report/index.tsx b/app/javascript/mastodon/features/annual_report/index.tsx new file mode 100644 index 00000000000000..91f03330c01472 --- /dev/null +++ b/app/javascript/mastodon/features/annual_report/index.tsx @@ -0,0 +1,99 @@ +import { useState, useEffect } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { + importFetchedStatuses, + importFetchedAccounts, +} from 'mastodon/actions/importer'; +import { apiRequestGet, apiRequestPost } from 'mastodon/api'; +import { LoadingIndicator } from 'mastodon/components/loading_indicator'; +import { me } from 'mastodon/initial_state'; +import type { Account } from 'mastodon/models/account'; +import type { AnnualReport as AnnualReportData } from 'mastodon/models/annual_report'; +import type { Status } from 'mastodon/models/status'; +import { useAppSelector, useAppDispatch } from 'mastodon/store'; + +import { Archetype } from './archetype'; +import { Followers } from './followers'; +import { HighlightedPost } from './highlighted_post'; +import { MostUsedHashtag } from './most_used_hashtag'; +import { NewPosts } from './new_posts'; +import { Percentile } from './percentile'; + +interface AnnualReportResponse { + annual_reports: AnnualReportData[]; + accounts: Account[]; + statuses: Status[]; +} + +export const AnnualReport: React.FC<{ + year: string; +}> = ({ year }) => { + const [response, setResponse] = useState(null); + const [loading, setLoading] = useState(false); + const currentAccount = useAppSelector((state) => + me ? state.accounts.get(me) : undefined, + ); + const dispatch = useAppDispatch(); + + useEffect(() => { + setLoading(true); + + apiRequestGet(`v1/annual_reports/${year}`) + .then((data) => { + dispatch(importFetchedStatuses(data.statuses)); + dispatch(importFetchedAccounts(data.accounts)); + + setResponse(data); + setLoading(false); + + return apiRequestPost(`v1/annual_reports/${year}/read`); + }) + .catch(() => { + setLoading(false); + }); + }, [dispatch, year, setResponse, setLoading]); + + if (loading) { + return ; + } + + const report = response?.annual_reports[0]; + + if (!report) { + return null; + } + + return ( +
+
+

+ +

+

+ +

+
+ +
+ + + + + + +
+
+ ); +}; diff --git a/app/javascript/mastodon/features/annual_report/most_used_app.tsx b/app/javascript/mastodon/features/annual_report/most_used_app.tsx new file mode 100644 index 00000000000000..2d8c8aa582a292 --- /dev/null +++ b/app/javascript/mastodon/features/annual_report/most_used_app.tsx @@ -0,0 +1,29 @@ +import { FormattedMessage } from 'react-intl'; + +import type { NameAndCount } from 'mastodon/models/annual_report'; + +export const MostUsedApp: React.FC<{ + data: NameAndCount[]; +}> = ({ data }) => { + const app = data[0]; + + if (!app) { + return ( +
+ ); + } + + return ( +
+
+ {app.name} +
+
+ +
+
+ ); +}; diff --git a/app/javascript/mastodon/features/annual_report/most_used_hashtag.tsx b/app/javascript/mastodon/features/annual_report/most_used_hashtag.tsx new file mode 100644 index 00000000000000..0e4c78f6398930 --- /dev/null +++ b/app/javascript/mastodon/features/annual_report/most_used_hashtag.tsx @@ -0,0 +1,29 @@ +import { FormattedMessage } from 'react-intl'; + +import type { NameAndCount } from 'mastodon/models/annual_report'; + +export const MostUsedHashtag: React.FC<{ + data: NameAndCount[]; +}> = ({ data }) => { + const hashtag = data[0]; + + if (!hashtag) { + return ( +
+ ); + } + + return ( +
+
+ #{hashtag.name} +
+
+ +
+
+ ); +}; diff --git a/app/javascript/mastodon/features/annual_report/new_posts.tsx b/app/javascript/mastodon/features/annual_report/new_posts.tsx new file mode 100644 index 00000000000000..9ead0176b2ad7d --- /dev/null +++ b/app/javascript/mastodon/features/annual_report/new_posts.tsx @@ -0,0 +1,53 @@ +import { FormattedNumber, FormattedMessage } from 'react-intl'; + +import ChatBubbleIcon from '@/material-icons/400-24px/chat_bubble.svg?react'; +import type { TimeSeriesMonth } from 'mastodon/models/annual_report'; + +export const NewPosts: React.FC<{ + data: TimeSeriesMonth[]; +}> = ({ data }) => { + const posts = data.reduce((sum, item) => sum + item.statuses, 0); + + return ( +
+ + + + + + + + + + + +
+ +
+
+ +
+
+ ); +}; diff --git a/app/javascript/mastodon/features/annual_report/percentile.tsx b/app/javascript/mastodon/features/annual_report/percentile.tsx new file mode 100644 index 00000000000000..a758db72c1abfa --- /dev/null +++ b/app/javascript/mastodon/features/annual_report/percentile.tsx @@ -0,0 +1,53 @@ +/* eslint-disable react/jsx-no-useless-fragment */ +import { FormattedMessage, FormattedNumber } from 'react-intl'; + +import type { Percentiles } from 'mastodon/models/annual_report'; + +export const Percentile: React.FC<{ + data: Percentiles; +}> = ({ data }) => { + const percentile = data.statuses; + + return ( +
+ ( +
+ {str} +
+ ), + percentage: () => ( +
+ +
+ ), + bottomLabel: (str) => ( +
+
+ {str} +
+ + {percentile < 6 && ( +
+ +
+ )} +
+ ), + }} + > + {(message) => <>{message}} +
+
+ ); +}; diff --git a/app/javascript/mastodon/features/follow_requests/index.jsx b/app/javascript/mastodon/features/follow_requests/index.jsx index a8f40a31d0d007..7d651f2ca69c24 100644 --- a/app/javascript/mastodon/features/follow_requests/index.jsx +++ b/app/javascript/mastodon/features/follow_requests/index.jsx @@ -68,7 +68,7 @@ class FollowRequests extends ImmutablePureComponent { ); return ( - + = ({ notification: { annualReport }, unread }) => { + const dispatch = useAppDispatch(); + const year = annualReport.year; + + const handleClick = useCallback(() => { + dispatch( + openModal({ + modalType: 'ANNUAL_REPORT', + modalProps: { year }, + }), + ); + }, [dispatch, year]); + + return ( +
+
+ +
+ +
+

+ +

+ +
+
+ ); +}; diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_group.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_group.tsx index 36f033261ae44d..d5eb851985cd99 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_group.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_group.tsx @@ -9,6 +9,7 @@ import { useAppSelector, useAppDispatch } from 'mastodon/store'; import { NotificationAdminReport } from './notification_admin_report'; import { NotificationAdminSignUp } from './notification_admin_sign_up'; +import { NotificationAnnualReport } from './notification_annual_report'; import { NotificationFavourite } from './notification_favourite'; import { NotificationFollow } from './notification_follow'; import { NotificationFollowRequest } from './notification_follow_request'; @@ -143,6 +144,14 @@ export const NotificationGroup: React.FC<{ /> ); break; + case 'annual_report': + content = ( + + ); + break; default: return null; } diff --git a/app/javascript/mastodon/features/status/components/detailed_status.tsx b/app/javascript/mastodon/features/status/components/detailed_status.tsx index 5811efb1907f99..deb330b9a091bf 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.tsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.tsx @@ -49,6 +49,7 @@ export const DetailedStatus: React.FC<{ domain: string; showMedia?: boolean; withLogo?: boolean; + overrideDisplayName?: React.ReactNode; pictureInPicture: any; onToggleHidden?: (status: any) => void; onToggleMediaVisibility?: () => void; @@ -62,6 +63,7 @@ export const DetailedStatus: React.FC<{ domain, showMedia, withLogo, + overrideDisplayName, pictureInPicture, onToggleMediaVisibility, onToggleHidden, @@ -319,7 +321,11 @@ export const DetailedStatus: React.FC<{
- + + {overrideDisplayName ?? ( + + )} + {withLogo && ( <>
diff --git a/app/javascript/mastodon/features/ui/components/annual_report_modal.tsx b/app/javascript/mastodon/features/ui/components/annual_report_modal.tsx new file mode 100644 index 00000000000000..8c39c0b3aa07a2 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/annual_report_modal.tsx @@ -0,0 +1,21 @@ +import { useEffect } from 'react'; + +import { AnnualReport } from 'mastodon/features/annual_report'; + +const AnnualReportModal: React.FC<{ + year: string; + onChangeBackgroundColor: (arg0: string) => void; +}> = ({ year, onChangeBackgroundColor }) => { + useEffect(() => { + onChangeBackgroundColor('var(--indigo-1)'); + }, [onChangeBackgroundColor]); + + return ( +
+ +
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export default AnnualReportModal; diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx index 64933fd1ae560f..42a00acf81f8cc 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.jsx +++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx @@ -18,6 +18,7 @@ import { SubscribedLanguagesModal, ClosedRegistrationsModal, IgnoreNotificationsModal, + AnnualReportModal, } from 'mastodon/features/ui/util/async-components'; import { getScrollbarWidth } from 'mastodon/utils/scrollbar'; @@ -72,6 +73,7 @@ export const MODAL_COMPONENTS = { 'INTERACTION': InteractionModal, 'CLOSED_REGISTRATIONS': ClosedRegistrationsModal, 'IGNORE_NOTIFICATIONS': IgnoreNotificationsModal, + 'ANNUAL_REPORT': AnnualReportModal, }; export default class ModalRoot extends PureComponent { diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 7e9a7af00a405e..ff5db653479cdd 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -217,3 +217,7 @@ export function NotificationRequest () { export function LinkTimeline () { return import(/*webpackChunkName: "features/link_timeline" */'../../link_timeline'); } + +export function AnnualReportModal () { + return import(/*webpackChunkName: "modals/annual_report_modal" */'../components/annual_report_modal'); +} diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json index 575bc567241b6a..13ca521ab07fd3 100644 --- a/app/javascript/mastodon/locales/br.json +++ b/app/javascript/mastodon/locales/br.json @@ -82,6 +82,8 @@ "alert.unexpected.message": "Ur fazi dic'hortozet zo degouezhet.", "alert.unexpected.title": "Hopala !", "announcement.announcement": "Kemennad", + "annual_report.summary.followers.followers": "heulier", + "annual_report.summary.highlighted_post.possessive": "{name}", "attachments_list.unprocessed": "(ket meret)", "audio.hide": "Kuzhat ar c'hleved", "block_modal.show_less": "Diskouez nebeutoc'h", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index e5c8440236b684..160a74af4fae6d 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -87,6 +87,18 @@ "alert.unexpected.title": "Vaja!", "alt_text_badge.title": "Text alternatiu", "announcement.announcement": "Anunci", + "annual_report.summary.archetype.oracle": "L'Oracle", + "annual_report.summary.followers.followers": "seguidors", + "annual_report.summary.followers.total": "{count} en total", + "annual_report.summary.here_it_is": "El repàs del vostre {year}:", + "annual_report.summary.highlighted_post.by_favourites": "la publicació més afavorida", + "annual_report.summary.highlighted_post.by_reblogs": "la publicació més impulsada", + "annual_report.summary.highlighted_post.by_replies": "la publicació amb més respostes", + "annual_report.summary.highlighted_post.possessive": "de {name}", + "annual_report.summary.most_used_app.most_used_app": "l'aplicació més utilitzada", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "l'etiqueta més utilitzada", + "annual_report.summary.new_posts.new_posts": "publicacions noves", + "annual_report.summary.thanks": "Gràcies per formar part de Mastodon!", "attachments_list.unprocessed": "(sense processar)", "audio.hide": "Amaga l'àudio", "block_modal.remote_users_caveat": "Li demanarem al servidor {domain} que respecti la vostra decisió, tot i que no podem garantir-ho, ja que alguns servidors gestionen de forma diferent els blocatges. És possible que els usuaris no autenticats puguin veure les publicacions públiques.", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 1d59b1472448af..d048a191e39adf 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -87,6 +87,24 @@ "alert.unexpected.title": "Oha!", "alt_text_badge.title": "Bildbeschreibung", "announcement.announcement": "Ankündigung", + "annual_report.summary.archetype.booster": "Trendjäger*in", + "annual_report.summary.archetype.lurker": "Beobachter*in", + "annual_report.summary.archetype.oracle": "Orakel", + "annual_report.summary.archetype.pollster": "Meinungsforscher*in", + "annual_report.summary.archetype.replier": "Geselliger Schmetterling", + "annual_report.summary.followers.followers": "Follower", + "annual_report.summary.followers.total": "{count} insgesamt", + "annual_report.summary.here_it_is": "Dein Jahresrückblick für {year}:", + "annual_report.summary.highlighted_post.by_favourites": "am häufigsten favorisierter Beitrag", + "annual_report.summary.highlighted_post.by_reblogs": "am häufigsten geteilter Beitrag", + "annual_report.summary.highlighted_post.by_replies": "Beitrag mit den meisten Antworten", + "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.summary.most_used_app.most_used_app": "am häufigsten verwendete App", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "am häufigsten verwendeter Hashtag", + "annual_report.summary.new_posts.new_posts": "neue Beiträge", + "annual_report.summary.percentile.text": "Damit gehörst du zu den oberstender Mastodon-Nutzer*innen.", + "annual_report.summary.percentile.we_wont_tell_bernie": "Wir werden Bernie nichts verraten.", + "annual_report.summary.thanks": "Danke, dass du Teil von Mastodon bist!", "attachments_list.unprocessed": "(ausstehend)", "audio.hide": "Audio ausblenden", "block_modal.remote_users_caveat": "Wir werden den Server {domain} bitten, deine Entscheidung zu respektieren. Allerdings kann nicht garantiert werden, dass sie eingehalten wird, weil einige Server Blockierungen unterschiedlich handhaben können. Öffentliche Beiträge können für nicht angemeldete Nutzer*innen weiterhin sichtbar sein.", @@ -508,6 +526,8 @@ "notification.admin.report_statuses_other": "{name} meldete {target}", "notification.admin.sign_up": "{name} registrierte sich", "notification.admin.sign_up.name_and_others": "{name} und {count, plural, one {# weitere Person} other {# weitere Personen}} registrierten sich", + "notification.annual_report.message": "Dein {year} #Wrapstodon erwartet dich! Lass deine Highlights und unvergesslichen Momente auf Mastodon erneut aufleben!", + "notification.annual_report.view": "#Wrapstodon ansehen", "notification.favourite": "{name} favorisierte deinen Beitrag", "notification.favourite.name_and_others_with_link": "{name} und {count, plural, one {# weitere Person} other {# weitere Personen}} favorisierten deinen Beitrag", "notification.follow": "{name} folgt dir", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index e91d6e52fce60d..5bc8c4ad711671 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -87,6 +87,24 @@ "alert.unexpected.title": "Oops!", "alt_text_badge.title": "Alt text", "announcement.announcement": "Announcement", + "annual_report.summary.archetype.booster": "The cool-hunter", + "annual_report.summary.archetype.lurker": "The lurker", + "annual_report.summary.archetype.oracle": "The oracle", + "annual_report.summary.archetype.pollster": "The pollster", + "annual_report.summary.archetype.replier": "The social butterfly", + "annual_report.summary.followers.followers": "followers", + "annual_report.summary.followers.total": "{count} total", + "annual_report.summary.here_it_is": "Here is your {year} in review:", + "annual_report.summary.highlighted_post.by_favourites": "most favourited post", + "annual_report.summary.highlighted_post.by_reblogs": "most boosted post", + "annual_report.summary.highlighted_post.by_replies": "post with the most replies", + "annual_report.summary.highlighted_post.possessive": "{name}'s", + "annual_report.summary.most_used_app.most_used_app": "most used app", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "most used hashtag", + "annual_report.summary.new_posts.new_posts": "new posts", + "annual_report.summary.percentile.text": "That puts you in the topof Mastodon users.", + "annual_report.summary.percentile.we_wont_tell_bernie": "We won't tell Bernie.", + "annual_report.summary.thanks": "Thanks for being part of Mastodon!", "attachments_list.unprocessed": "(unprocessed)", "audio.hide": "Hide audio", "block_modal.remote_users_caveat": "We will ask the server {domain} to respect your decision. However, compliance is not guaranteed since some servers may handle blocks differently. Public posts may still be visible to non-logged-in users.", @@ -508,6 +526,8 @@ "notification.admin.report_statuses_other": "{name} reported {target}", "notification.admin.sign_up": "{name} signed up", "notification.admin.sign_up.name_and_others": "{name} and {count, plural, one {# other} other {# others}} signed up", + "notification.annual_report.message": "Your {year} #Wrapstodon awaits! Unveil your year's highlights and memorable moments on Mastodon!", + "notification.annual_report.view": "View #Wrapstodon", "notification.favourite": "{name} favorited your post", "notification.favourite.name_and_others_with_link": "{name} and {count, plural, one {# other} other {# others}} favorited your post", "notification.follow": "{name} followed you", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 7abe0783c39f43..757bb39866ed1c 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -45,7 +45,7 @@ "account.languages": "Ŝanĝi la abonitajn lingvojn", "account.link_verified_on": "Propreco de tiu ligilo estis konfirmita je {date}", "account.locked_info": "Tiu konto estas privatigita. La posedanto mane akceptas tiun, kiu povas sekvi rin.", - "account.media": "Plurmedioj", + "account.media": "Plurmedio", "account.mention": "Mencii @{name}", "account.moved_to": "{name} indikis, ke ria nova konto estas nun:", "account.mute": "Silentigi @{name}", @@ -87,6 +87,11 @@ "alert.unexpected.title": "Aj!", "alt_text_badge.title": "Alt-teksto", "announcement.announcement": "Anonco", + "annual_report.summary.archetype.replier": "La plej societema", + "annual_report.summary.followers.followers": "sekvantoj", + "annual_report.summary.highlighted_post.by_replies": "afiŝo kun la plej multaj respondoj", + "annual_report.summary.new_posts.new_posts": "novaj afiŝoj", + "annual_report.summary.thanks": "Dankon pro esti parto de Mastodon!", "attachments_list.unprocessed": "(neprilaborita)", "audio.hide": "Kaŝi aŭdion", "block_modal.remote_users_caveat": "Ni petos al la servilo {domain} respekti vian elekton. Tamen, plenumo ne estas garantiita ĉar iuj serviloj eble manipulas blokojn malsame. Publikaj afiŝoj eble ankoraŭ estas videbla por ne-ensalutintaj uzantoj.", @@ -142,7 +147,7 @@ "column_header.unpin": "Malfiksi", "column_subheading.settings": "Agordoj", "community.column_settings.local_only": "Nur loka", - "community.column_settings.media_only": "Nur plurmedioj", + "community.column_settings.media_only": "Nur plurmedio", "community.column_settings.remote_only": "Nur fora", "compose.language.change": "Ŝanĝi lingvon", "compose.language.search": "Serĉi lingvojn...", @@ -508,6 +513,7 @@ "notification.admin.report_statuses_other": "{name} raportis {target}", "notification.admin.sign_up": "{name} kreis konton", "notification.admin.sign_up.name_and_others": "{name} kaj {count, plural, one {# alia} other {# aliaj}} kreis konton", + "notification.annual_report.view": "Vidu #Wrapstodon", "notification.favourite": "{name} stelumis vian afiŝon", "notification.favourite.name_and_others_with_link": "{name} kaj {count, plural, one {# alia} other {# aliaj}} ŝatis vian afiŝon", "notification.follow": "{name} eksekvis vin", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index e8302a4c75da44..a556d90e33b3db 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -87,6 +87,24 @@ "alert.unexpected.title": "¡Epa!", "alt_text_badge.title": "Texto alternativo", "announcement.announcement": "Anuncio", + "annual_report.summary.archetype.booster": "El cazador de tendencias", + "annual_report.summary.archetype.lurker": "El acechador", + "annual_report.summary.archetype.oracle": "El oráculo", + "annual_report.summary.archetype.pollster": "El encuestador", + "annual_report.summary.archetype.replier": "El más sociable", + "annual_report.summary.followers.followers": "seguidores", + "annual_report.summary.followers.total": "{count} en total", + "annual_report.summary.here_it_is": "Aquí está tu resumen de {year}:", + "annual_report.summary.highlighted_post.by_favourites": "publicación con más favoritos", + "annual_report.summary.highlighted_post.by_reblogs": "publicación más impulsada", + "annual_report.summary.highlighted_post.by_replies": "publicación con más respuestas", + "annual_report.summary.highlighted_post.possessive": "de {name}", + "annual_report.summary.most_used_app.most_used_app": "aplicación más usada", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "etiqueta más usada", + "annual_report.summary.new_posts.new_posts": "nuevas publicaciones", + "annual_report.summary.percentile.text": "Eso te pone en el topde usuarios de Mastodon.", + "annual_report.summary.percentile.we_wont_tell_bernie": "No se lo diremos a Bernie.", + "annual_report.summary.thanks": "¡Gracias por ser parte de Mastodon!", "attachments_list.unprocessed": "[sin procesar]", "audio.hide": "Ocultar audio", "block_modal.remote_users_caveat": "Le pediremos al servidor {domain} que respete tu decisión. Sin embargo, el cumplimiento no está garantizado, ya que algunos servidores pueden manejar los bloqueos de forma diferente. Los mensajes públicos todavía podrían estar visibles para los usuarios no conectados.", @@ -508,6 +526,8 @@ "notification.admin.report_statuses_other": "{name} denunció a {target}", "notification.admin.sign_up": "Se registró {name}", "notification.admin.sign_up.name_and_others": "Se registraron {name} y {count, plural, one {# cuenta más} other {# cuentas más}}", + "notification.annual_report.message": "¡Tu #Wrapstodon {year} te espera! ¡Desvela los momentos más destacados y memorables de tu año en Mastodon!", + "notification.annual_report.view": "Ver #Wrapstodon", "notification.favourite": "{name} marcó tu mensaje como favorito", "notification.favourite.name_and_others_with_link": "{name} y {count, plural, one {# cuenta más} other {# cuentas más}} marcaron tu mensaje como favorito", "notification.follow": "{name} te empezó a seguir", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index c73a64c4e0bda9..4faa35e5c87ada 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -87,6 +87,24 @@ "alert.unexpected.title": "¡Ups!", "alt_text_badge.title": "Texto alternativo", "announcement.announcement": "Anuncio", + "annual_report.summary.archetype.booster": "El cazador de tendencias", + "annual_report.summary.archetype.lurker": "El acechador", + "annual_report.summary.archetype.oracle": "El oráculo", + "annual_report.summary.archetype.pollster": "El encuestador", + "annual_report.summary.archetype.replier": "El más sociable", + "annual_report.summary.followers.followers": "seguidores", + "annual_report.summary.followers.total": "{count} en total", + "annual_report.summary.here_it_is": "Aquí está tu resumen de {year}:", + "annual_report.summary.highlighted_post.by_favourites": "publicación con más favoritos", + "annual_report.summary.highlighted_post.by_reblogs": "publicación más impulsada", + "annual_report.summary.highlighted_post.by_replies": "publicación con más respuestas", + "annual_report.summary.highlighted_post.possessive": "de {name}", + "annual_report.summary.most_used_app.most_used_app": "aplicación más usada", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "etiqueta más usada", + "annual_report.summary.new_posts.new_posts": "nuevas publicaciones", + "annual_report.summary.percentile.text": "Eso te pone en el topde usuarios de Mastodon.", + "annual_report.summary.percentile.we_wont_tell_bernie": "No se lo diremos a Bernie.", + "annual_report.summary.thanks": "¡Gracias por ser parte de Mastodon!", "attachments_list.unprocessed": "(sin procesar)", "audio.hide": "Ocultar audio", "block_modal.remote_users_caveat": "Le pediremos al servidor {domain} que respete tu decisión. Sin embargo, el cumplimiento no está garantizado ya que algunos servidores pueden manejar bloques de forma diferente. Las publicaciones públicas pueden ser todavía visibles para los usuarios no conectados.", @@ -508,6 +526,8 @@ "notification.admin.report_statuses_other": "{name} reportó {target}", "notification.admin.sign_up": "{name} se unio", "notification.admin.sign_up.name_and_others": "{name} y {count, plural, one {# otro} other {# otros}} se registraron", + "notification.annual_report.message": "¡Tu #Wrapstodon {year} te espera! ¡Desvela los momentos más destacados y memorables de tu año en Mastodon!", + "notification.annual_report.view": "Ver #Wrapstodon", "notification.favourite": "{name} marcó como favorita tu publicación", "notification.favourite.name_and_others_with_link": "{name} y {count, plural, one {# otro} other {# otros}} marcaron tu publicación como favorita", "notification.follow": "{name} te empezó a seguir", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index dea526f8a0c218..d14b7180160695 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -87,6 +87,24 @@ "alert.unexpected.title": "¡Ups!", "alt_text_badge.title": "Texto alternativo", "announcement.announcement": "Anuncio", + "annual_report.summary.archetype.booster": "El cazador de tendencias", + "annual_report.summary.archetype.lurker": "El acechador", + "annual_report.summary.archetype.oracle": "El oráculo", + "annual_report.summary.archetype.pollster": "El encuestador", + "annual_report.summary.archetype.replier": "El más sociable", + "annual_report.summary.followers.followers": "seguidores", + "annual_report.summary.followers.total": "{count} en total", + "annual_report.summary.here_it_is": "Aquí está tu resumen de {year}:", + "annual_report.summary.highlighted_post.by_favourites": "publicación con más favoritos", + "annual_report.summary.highlighted_post.by_reblogs": "publicación más impulsada", + "annual_report.summary.highlighted_post.by_replies": "publicación con más respuestas", + "annual_report.summary.highlighted_post.possessive": "de {name}", + "annual_report.summary.most_used_app.most_used_app": "aplicación más usada", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "etiqueta más usada", + "annual_report.summary.new_posts.new_posts": "nuevas publicaciones", + "annual_report.summary.percentile.text": "Eso te pone en el topde usuarios de Mastodon.", + "annual_report.summary.percentile.we_wont_tell_bernie": "No se lo diremos a Bernie.", + "annual_report.summary.thanks": "¡Gracias por ser parte de Mastodon!", "attachments_list.unprocessed": "(sin procesar)", "audio.hide": "Ocultar audio", "block_modal.remote_users_caveat": "Le pediremos al servidor {domain} que respete tu decisión. Sin embargo, el cumplimiento no está garantizado, ya que algunos servidores pueden manejar bloqueos de forma distinta. Los mensajes públicos pueden ser todavía visibles para los usuarios que no hayan iniciado sesión.", @@ -508,6 +526,8 @@ "notification.admin.report_statuses_other": "{name} informó de {target}", "notification.admin.sign_up": "{name} se registró", "notification.admin.sign_up.name_and_others": "{name} y {count, plural, one {# más} other {# más}} se registraron", + "notification.annual_report.message": "¡Tu #Wrapstodon {year} te espera! ¡Desvela los momentos más destacados y memorables de tu año en Mastodon!", + "notification.annual_report.view": "Ver #Wrapstodon", "notification.favourite": "{name} marcó como favorita tu publicación", "notification.favourite.name_and_others_with_link": "{name} y {count, plural, one {# más} other {# más}} marcaron tu publicación como favorita", "notification.follow": "{name} te empezó a seguir", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 2222e6038c08a5..32069b1a03801b 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -87,6 +87,11 @@ "alert.unexpected.title": "Hups!", "alt_text_badge.title": "Vaihtoehtoinen teksti", "announcement.announcement": "Tiedote", + "annual_report.summary.archetype.replier": "Sosiaalinen perhonen", + "annual_report.summary.highlighted_post.by_reblogs": "tehostetuin julkaisu", + "annual_report.summary.most_used_app.most_used_app": "käytetyin sovellus", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "käytetyin aihetunniste", + "annual_report.summary.thanks": "Kiitos, että olet osa Mastodonia!", "attachments_list.unprocessed": "(käsittelemätön)", "audio.hide": "Piilota ääni", "block_modal.remote_users_caveat": "Pyydämme palvelinta {domain} kunnioittamaan päätöstäsi. Myötämielisyyttä ei kuitenkaan taata, koska jotkin palvelimet voivat käsitellä estoja eri tavalla. Julkiset julkaisut voivat silti näkyä kirjautumattomille käyttäjille.", @@ -158,7 +163,7 @@ "compose_form.poll.duration": "Äänestyksen kesto", "compose_form.poll.multiple": "Monivalinta", "compose_form.poll.option_placeholder": "Vaihtoehto {number}", - "compose_form.poll.single": "Yksi vaihtoehto", + "compose_form.poll.single": "Yksittäisvalinta", "compose_form.poll.switch_to_multiple": "Muuta äänestys monivalinnaksi", "compose_form.poll.switch_to_single": "Muuta äänestys yksittäisvalinnaksi", "compose_form.poll.type": "Tyyli", @@ -386,7 +391,7 @@ "interaction_modal.description.follow": "Mastodon-tilillä voit seurata käyttäjää {name} saadaksesi hänen julkaisunsa kotisyötteeseesi.", "interaction_modal.description.reblog": "Mastodon-tilillä voit tehostaa tätä julkaisua jakaaksesi sen seuraajiesi kanssa.", "interaction_modal.description.reply": "Mastodon-tilillä voit vastata tähän julkaisuun.", - "interaction_modal.description.vote": "Osallistuminen äänestykseen onnistuu Mastodon-tilillä.", + "interaction_modal.description.vote": "Mastodon-tilillä voit osallistua tähän äänestykseen.", "interaction_modal.login.action": "Siirry kotiin", "interaction_modal.login.prompt": "Kotipalvelimesi verkkotunnus, kuten mastodon.social", "interaction_modal.no_account_yet": "Etkö ole vielä Mastodonissa?", @@ -508,6 +513,7 @@ "notification.admin.report_statuses_other": "{name} raportoi käyttäjän {target}", "notification.admin.sign_up": "{name} rekisteröityi", "notification.admin.sign_up.name_and_others": "{name} ja {count, plural, one {# muu} other {# muuta}} rekisteröityivät", + "notification.annual_report.view": "Näytä #Wrapstodon", "notification.favourite": "{name} lisäsi julkaisusi suosikkeihinsa", "notification.favourite.name_and_others_with_link": "{name} ja {count, plural, one {# muu} other {# muuta}} lisäsivät julkaisusi suosikkeihinsa", "notification.follow": "{name} seurasi sinua", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index e15385725a0097..6b26ed84fd7cf8 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -87,6 +87,24 @@ "alert.unexpected.title": "Ups!", "alt_text_badge.title": "Annar tekstur", "announcement.announcement": "Kunngerð", + "annual_report.summary.archetype.booster": "Kuli jagarin", + "annual_report.summary.archetype.lurker": "Lúrarin", + "annual_report.summary.archetype.oracle": "Oraklið", + "annual_report.summary.archetype.pollster": "Spyrjarin", + "annual_report.summary.archetype.replier": "Sosiali firvaldurin", + "annual_report.summary.followers.followers": "fylgjarar", + "annual_report.summary.followers.total": "{count} íalt", + "annual_report.summary.here_it_is": "Her er ein samandráttur av {year}:", + "annual_report.summary.highlighted_post.by_favourites": "mest dámdi postur", + "annual_report.summary.highlighted_post.by_reblogs": "oftast lyfti postur", + "annual_report.summary.highlighted_post.by_replies": "postur við flestum svarum", + "annual_report.summary.highlighted_post.possessive": "hjá {name}", + "annual_report.summary.most_used_app.most_used_app": "mest brúkta app", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "mest brúkta frámerki", + "annual_report.summary.new_posts.new_posts": "nýggir postar", + "annual_report.summary.percentile.text": "Tað fær teg í toppav Mastodon brúkarum.", + "annual_report.summary.percentile.we_wont_tell_bernie": "Vit fara ikki at fortelja Bernie tað.", + "annual_report.summary.thanks": "Takk fyri at tú er partur av Mastodon!", "attachments_list.unprocessed": "(óviðgjørt)", "audio.hide": "Fjal ljóð", "block_modal.remote_users_caveat": "Vit biðja ambætaran {domain} virða tína avgerð. Kortini er eingin vissa um samsvar, av tí at fleiri ambætarar handfara blokkar ymiskt. Almennir postar kunnu framvegis vera sjónligir fyri brúkarar, sum ikki eru innritaðir.", @@ -508,6 +526,8 @@ "notification.admin.report_statuses_other": "{name} meldaði {target}", "notification.admin.sign_up": "{name} meldaði seg til", "notification.admin.sign_up.name_and_others": "{name} og {count, plural, one {# annar/onnur} other {# onnur}} teknaðu seg", + "notification.annual_report.message": "Títt {year} #Wrapstodon bíðar! Avdúka hæddarpunktini og minniligu løturnar á Mastodon!", + "notification.annual_report.view": "Sí #Wrapstodon", "notification.favourite": "{name} dámdi postin hjá tær", "notification.favourite.name_and_others_with_link": "{name} og {count, plural, one {# annar/onnur} other {# onnur}} yndisfrámerktu postin hjá tær", "notification.follow": "{name} fylgdi tær", diff --git a/app/javascript/mastodon/locales/fy.json b/app/javascript/mastodon/locales/fy.json index e1c090cb0cb8cb..3f4d942b61e3b9 100644 --- a/app/javascript/mastodon/locales/fy.json +++ b/app/javascript/mastodon/locales/fy.json @@ -158,6 +158,7 @@ "compose_form.poll.duration": "Doer fan de enkête", "compose_form.poll.multiple": "Mearkar", "compose_form.poll.option_placeholder": "Opsje {number}", + "compose_form.poll.single": "Inkelde kar", "compose_form.poll.switch_to_multiple": "Enkête wizigje om meardere karren ta te stean", "compose_form.poll.switch_to_single": "Enkête wizigje om in inkelde kar ta te stean", "compose_form.poll.type": "Styl", @@ -196,6 +197,7 @@ "confirmations.unfollow.title": "Brûker net mear folgje?", "content_warning.hide": "Berjocht ferstopje", "content_warning.show": "Dochs toane", + "content_warning.show_more": "Mear toane", "conversation.delete": "Petear fuortsmite", "conversation.mark_as_read": "As lêzen markearje", "conversation.open": "Petear toane", @@ -304,6 +306,7 @@ "filter_modal.select_filter.subtitle": "In besteande kategory brûke of in nije oanmeitsje", "filter_modal.select_filter.title": "Dit berjocht filterje", "filter_modal.title.status": "In berjocht filterje", + "filter_warning.matches_filter": "Komt oerien mei filter ‘{title}’", "filtered_notifications_banner.pending_requests": "Fan {count, plural, =0 {net ien} one {ien persoan} other {# persoanen}} dy’t jo mooglik kinne", "filtered_notifications_banner.title": "Filtere meldingen", "firehose.all": "Alles", @@ -383,6 +386,7 @@ "interaction_modal.description.follow": "Jo kinne mei in Mastodon-account {name} folgje, om sa harren berjochten op jo starttiidline te ûntfangen.", "interaction_modal.description.reblog": "Jo kinne mei in Mastodon-account dit berjocht booste, om it sa mei jo folgers te dielen.", "interaction_modal.description.reply": "Jo kinne mei in Mastodon-account op dit berjocht reagearje.", + "interaction_modal.description.vote": "Mei in Mastodon-account kinne jo yn dizze enkête stimme.", "interaction_modal.login.action": "Gean nei start", "interaction_modal.login.prompt": "Domein fan jo server, byg. mastodon.social", "interaction_modal.no_account_yet": "Net op Mastodon?", @@ -394,6 +398,7 @@ "interaction_modal.title.follow": "{name} folgje", "interaction_modal.title.reblog": "Berjocht fan {name} booste", "interaction_modal.title.reply": "Op it berjocht fan {name} reagearje", + "interaction_modal.title.vote": "Stimme yn {name}’s peiling", "intervals.full.days": "{number, plural, one {# dei} other {# dagen}} lyn", "intervals.full.hours": "{number, plural, one {# oere} other {# oeren}} lyn", "intervals.full.minutes": "{number, plural, one {# minút} other {# minuten}} lyn", @@ -506,6 +511,7 @@ "notification.favourite": "{name} hat jo berjocht as favoryt markearre", "notification.favourite.name_and_others_with_link": "{name} en {count, plural, one {# oar} other {# oaren}} hawwe jo berjocht as favoryt markearre", "notification.follow": "{name} folget dy", + "notification.follow.name_and_others": "{name} en {count, plural, one {# oar persoan} other {# oare persoanen}} folgje jo no", "notification.follow_request": "{name} hat dy in folchfersyk stjoerd", "notification.follow_request.name_and_others": "{name} en {count, plural, one {# oar} other {# oaren}} hawwe frege om jo te folgjen", "notification.label.mention": "Fermelding", @@ -564,6 +570,7 @@ "notifications.column_settings.filter_bar.category": "Flugge filterbalke", "notifications.column_settings.follow": "Nije folgers:", "notifications.column_settings.follow_request": "Nij folchfersyk:", + "notifications.column_settings.group": "Groepearje", "notifications.column_settings.mention": "Fermeldingen:", "notifications.column_settings.poll": "Enkêteresultaten:", "notifications.column_settings.push": "Pushmeldingen", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index aa7892163bcd54..3181d7978e1e6e 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -87,6 +87,24 @@ "alert.unexpected.title": "Hiúps!", "alt_text_badge.title": "Téacs alt", "announcement.announcement": "Fógra", + "annual_report.summary.archetype.booster": "An sealgair fionnuar", + "annual_report.summary.archetype.lurker": "An lurker", + "annual_report.summary.archetype.oracle": "An oracal", + "annual_report.summary.archetype.pollster": "An pollaire", + "annual_report.summary.archetype.replier": "An féileacán sóisialta", + "annual_report.summary.followers.followers": "leanúna", + "annual_report.summary.followers.total": "{count} san iomlán", + "annual_report.summary.here_it_is": "Seo do {year} faoi athbhreithniú:", + "annual_report.summary.highlighted_post.by_favourites": "post is fearr leat", + "annual_report.summary.highlighted_post.by_reblogs": "post is treisithe", + "annual_report.summary.highlighted_post.by_replies": "post leis an líon is mó freagraí", + "annual_report.summary.highlighted_post.possessive": "{name}'s", + "annual_report.summary.most_used_app.most_used_app": "aip is mó a úsáidtear", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "hashtag is mó a úsáidtear", + "annual_report.summary.new_posts.new_posts": "postanna nua", + "annual_report.summary.percentile.text": "Cuireann sé sin i mbarr úsáideoirí Mastodon.", + "annual_report.summary.percentile.we_wont_tell_bernie": "Ní inseoidh muid do Bernie.", + "annual_report.summary.thanks": "Go raibh maith agat as a bheith mar chuid de Mastodon!", "attachments_list.unprocessed": "(neamhphróiseáilte)", "audio.hide": "Cuir fuaim i bhfolach", "block_modal.remote_users_caveat": "Iarrfaimid ar an bhfreastalaí {domain} meas a bheith agat ar do chinneadh. Mar sin féin, ní ráthaítear comhlíonadh toisc go bhféadfadh roinnt freastalaithe bloic a láimhseáil ar bhealach difriúil. Seans go mbeidh postálacha poiblí fós le feiceáil ag úsáideoirí nach bhfuil logáilte isteach.", @@ -386,6 +404,7 @@ "interaction_modal.description.follow": "Le cuntas ar Mastodon, is féidir leat {name} a leanúint chun a gcuid postálacha a fháil i do fhotha baile.", "interaction_modal.description.reblog": "Le cuntas ar Mastodon, is féidir leat an postáil seo a threisiú chun é a roinnt le do leantóirí féin.", "interaction_modal.description.reply": "Le cuntas ar Mastodon, is féidir leat freagra a thabhairt ar an bpostáil seo.", + "interaction_modal.description.vote": "Le cuntas ar Mastodon, is féidir leat vótáil sa vótaíocht seo.", "interaction_modal.login.action": "Thabhairt dom abhaile", "interaction_modal.login.prompt": "Fearann ​​do fhreastalaí baile, e.g. mastodon.sóisialta", "interaction_modal.no_account_yet": "Ní ar Mastodon?", @@ -397,6 +416,7 @@ "interaction_modal.title.follow": "Lean {name}", "interaction_modal.title.reblog": "Mol postáil de chuid {name}", "interaction_modal.title.reply": "Freagair postáil {name}", + "interaction_modal.title.vote": "Vótáil i vótaíocht {name}", "intervals.full.days": "{number, plural, one {# lá} other {# lá}}", "intervals.full.hours": "{number, plural, one {# uair} other {# uair}}", "intervals.full.minutes": "{number, plural, one {# nóiméad} other {# nóiméad}}", @@ -506,6 +526,8 @@ "notification.admin.report_statuses_other": "{name} tuairiscithe {target}", "notification.admin.sign_up": "Chláraigh {name}", "notification.admin.sign_up.name_and_others": "{name} agus {count, plural, one {# duine eile} two {# daoine eile} few {# daoine eile} many {# daoine eile} other {# daoine eile}} a chláraigh", + "notification.annual_report.message": "Tá do {year} #Wrapstodon ag fanacht! Nocht buaicphointí na bliana agus chuimhneacháin i gcuimhne ar Mastodon!", + "notification.annual_report.view": "Amharc #Wrapstodon", "notification.favourite": "Is fearr le {name} do phostáil", "notification.favourite.name_and_others_with_link": "{name} agus {count, plural, one {# duine eile} other {# daoine eile}} thaitin le do phost", "notification.follow": "Lean {name} thú", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index e89d386c853f55..23f444715b2631 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -87,6 +87,18 @@ "alert.unexpected.title": "Vaites!", "alt_text_badge.title": "Texto Alt", "announcement.announcement": "Anuncio", + "annual_report.summary.followers.followers": "seguidoras", + "annual_report.summary.followers.total": "{count} en total", + "annual_report.summary.here_it_is": "Este é o resumo do teu {year}:", + "annual_report.summary.highlighted_post.by_favourites": "a publicación mais favorecida", + "annual_report.summary.highlighted_post.by_reblogs": "a publicación con mais promocións", + "annual_report.summary.highlighted_post.by_replies": "a publicación con mais respostas", + "annual_report.summary.highlighted_post.possessive": "de {name}", + "annual_report.summary.most_used_app.most_used_app": "app que mais usaches", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "o cancelo mais utilizado", + "annual_report.summary.new_posts.new_posts": "novas publicacións", + "annual_report.summary.percentile.text": "Sitúante no top das usuarias de Mastodon.", + "annual_report.summary.thanks": "Grazas por ser parte de Mastodon!", "attachments_list.unprocessed": "(sen procesar)", "audio.hide": "Agochar audio", "block_modal.remote_users_caveat": "Ímoslle pedir ao servidor {domain} que respecte a túa decisión. Emporiso, non hai garantía de que atenda a petición xa que os servidores xestionan os bloqueos de formas diferentes. As publicacións públicas poderían aínda ser visibles para usuarias que non iniciaron sesión.", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index 8c87298910597d..adc86faf40ec24 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -87,6 +87,24 @@ "alert.unexpected.title": "Úbbs!", "alt_text_badge.title": "Hjálpartexti mynda", "announcement.announcement": "Auglýsing", + "annual_report.summary.archetype.booster": "Svali gaurinn", + "annual_report.summary.archetype.lurker": "Lurkurinn", + "annual_report.summary.archetype.oracle": "Völvan", + "annual_report.summary.archetype.pollster": "Kannanafíkillinn", + "annual_report.summary.archetype.replier": "Félagsveran", + "annual_report.summary.followers.followers": "fylgjendur", + "annual_report.summary.followers.total": "{count} alls", + "annual_report.summary.here_it_is": "Hér er yfirlitið þitt fyrir {year}:", + "annual_report.summary.highlighted_post.by_favourites": "færsla sett oftast í eftirlæti", + "annual_report.summary.highlighted_post.by_reblogs": "færsla oftast endurbirt", + "annual_report.summary.highlighted_post.by_replies": "færsla með flestum svörum", + "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.summary.most_used_app.most_used_app": "mest notaða forrit", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "mest notaða myllumerki", + "annual_report.summary.new_posts.new_posts": "nýjar færslur", + "annual_report.summary.percentile.text": "Það setur þig á meðal efstunotenda Mastodon.", + "annual_report.summary.percentile.we_wont_tell_bernie": "Við förum ekkert að raupa um þetta.", + "annual_report.summary.thanks": "Takk fyrir að vera hluti af Mastodon-samfélaginu!", "attachments_list.unprocessed": "(óunnið)", "audio.hide": "Fela hljóð", "block_modal.remote_users_caveat": "Við munum biðja {domain} netþjóninn um að virða ákvörðun þína. Hitt er svo annað mál hvort hann fari eftir þessu, ekki er hægt að tryggja eftirfylgni því sumir netþjónar meðhöndla útilokanir á sinn hátt. Opinberar færslur gætu verið sýnilegar notendum sem ekki eru skráðir inn.", @@ -508,6 +526,8 @@ "notification.admin.report_statuses_other": "{name} kærði {target}", "notification.admin.sign_up": "{name} skráði sig", "notification.admin.sign_up.name_and_others": "{name} og {count, plural, one {# í viðbót hefur} other {# í viðbót hafa}} skráð sig", + "notification.annual_report.message": "{year} á #Wrapstodon bíður! Afhjúpaðu hvað bar hæst á árinu og minnistæðustu augnablikin á Mastodon!", + "notification.annual_report.view": "Skoða #Wrapstodon", "notification.favourite": "{name} setti færsluna þína í eftirlæti", "notification.favourite.name_and_others_with_link": "{name} og {count, plural, one {# í viðbót hefur} other {# í viðbót hafa}} sett færsluna þína í eftirlæti", "notification.follow": "{name} fylgist með þér", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index a692fb7fd33836..13ba1f8836e5af 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -87,6 +87,19 @@ "alert.unexpected.title": "Oops!", "alt_text_badge.title": "Testo alternativo", "announcement.announcement": "Annuncio", + "annual_report.summary.archetype.oracle": "L'oracolo", + "annual_report.summary.followers.followers": "seguaci", + "annual_report.summary.followers.total": "{count} in totale", + "annual_report.summary.here_it_is": "Ecco il tuo {year} in sintesi:", + "annual_report.summary.highlighted_post.by_favourites": "il post più apprezzato", + "annual_report.summary.highlighted_post.by_reblogs": "il post più condiviso", + "annual_report.summary.highlighted_post.by_replies": "il post con più risposte", + "annual_report.summary.highlighted_post.possessive": "di {name}", + "annual_report.summary.most_used_app.most_used_app": "l'app più utilizzata", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "l'hashtag più usato", + "annual_report.summary.new_posts.new_posts": "nuovi post", + "annual_report.summary.percentile.text": "Questo ti colloca tra ildei migliori utenti Mastodon.", + "annual_report.summary.percentile.we_wont_tell_bernie": "Non lo diremo a Bernie.", "attachments_list.unprocessed": "(non elaborato)", "audio.hide": "Nascondi audio", "block_modal.remote_users_caveat": "Chiederemo al server {domain} di rispettare la tua decisione. Tuttavia, la conformità non è garantita poiché alcuni server potrebbero gestire i blocchi in modo diverso. I post pubblici potrebbero essere ancora visibili agli utenti che non hanno effettuato l'accesso.", @@ -508,6 +521,8 @@ "notification.admin.report_statuses_other": "{name} ha segnalato {target}", "notification.admin.sign_up": "{name} si è iscritto", "notification.admin.sign_up.name_and_others": "Si sono iscritti: {name} e {count, plural, one {# altro utente} other {altri # utenti}}", + "notification.annual_report.message": "Il tuo #Wrapstodon {year} ti aspetta! Scopri i momenti salienti e memorabili del tuo anno su Mastodon!", + "notification.annual_report.view": "Visualizza #Wrapstodon", "notification.favourite": "{name} ha aggiunto il tuo post ai preferiti", "notification.favourite.name_and_others_with_link": "{name} e {count, plural, one {# altro} other {altri #}} hanno aggiunto il tuo post ai preferiti", "notification.follow": "{name} ha iniziato a seguirti", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 3462675e3e0e1e..b5c8d4bd21b27a 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -87,6 +87,24 @@ "alert.unexpected.title": "앗!", "alt_text_badge.title": "대체 문구", "announcement.announcement": "공지사항", + "annual_report.summary.archetype.booster": "연쇄부스트마", + "annual_report.summary.archetype.lurker": "은둔자", + "annual_report.summary.archetype.oracle": "예언자", + "annual_report.summary.archetype.pollster": "여론조사원", + "annual_report.summary.archetype.replier": "인싸", + "annual_report.summary.followers.followers": "팔로워", + "annual_report.summary.followers.total": "총 {count}", + "annual_report.summary.here_it_is": "{year}년 결산입니다:", + "annual_report.summary.highlighted_post.by_favourites": "가장 많은 좋아요를 받은 게시물", + "annual_report.summary.highlighted_post.by_reblogs": "가장 많이 부스트된 게시물", + "annual_report.summary.highlighted_post.by_replies": "가장 많은 답글을 받은 게시물", + "annual_report.summary.highlighted_post.possessive": "{name} 님의", + "annual_report.summary.most_used_app.most_used_app": "가장 많이 사용한 앱", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "가장 많이 사용한 해시태그", + "annual_report.summary.new_posts.new_posts": "새 게시물", + "annual_report.summary.percentile.text": "마스토돈 사용자의 상위입니다.", + "annual_report.summary.percentile.we_wont_tell_bernie": "엄마한테 말 안 할게요.", + "annual_report.summary.thanks": "마스토돈과 함께 해주셔서 감사합니다!", "attachments_list.unprocessed": "(처리 안 됨)", "audio.hide": "소리 숨기기", "block_modal.remote_users_caveat": "우리는 {domain} 서버가 당신의 결정을 존중해 주길 부탁할 것입니다. 하지만 몇몇 서버는 차단을 다르게 취급할 수 있기 때문에 규정이 준수되는 것을 보장할 수는 없습니다. 공개 게시물은 로그인 하지 않은 사용자들에게 여전히 보여질 수 있습니다.", @@ -113,7 +131,7 @@ "bundle_modal_error.message": "컴포넌트를 불러오는 중 문제가 발생했습니다.", "bundle_modal_error.retry": "다시 시도", "closed_registrations.other_server_instructions": "마스토돈은 분산화 되어 있기 때문에, 다른 서버에서 계정을 만들더라도 이 서버와 상호작용 할 수 있습니다.", - "closed_registrations_modal.description": "{domain}은 현재 가입이 막혀있는 상태입니다, 마스토돈을 이용하기 위해 꼭 {domain}을 사용할 필요는 없다는 사실을 인지해 두세요.", + "closed_registrations_modal.description": "{domain}은 현재 가입이 불가능합니다. 하지만 마스토돈을 이용하기 위해 꼭 {domain}을 사용할 필요는 없다는 사실을 인지해 두세요.", "closed_registrations_modal.find_another_server": "다른 서버 찾기", "closed_registrations_modal.preamble": "마스토돈은 분산화 되어 있습니다, 그렇기 때문에 어디에서 계정을 생성하든, 이 서버에 있는 누구와도 팔로우와 상호작용을 할 수 있습니다. 심지어는 스스로 서버를 만드는 것도 가능합니다!", "closed_registrations_modal.title": "마스토돈에서 가입", @@ -386,6 +404,7 @@ "interaction_modal.description.follow": "마스토돈 계정을 통해, {name} 님을 팔로우 하고 그의 게시물을 홈 피드에서 받아 볼 수 있습니다.", "interaction_modal.description.reblog": "마스토돈 계정을 통해, 이 게시물을 부스트 하고 자신의 팔로워들에게 공유할 수 있습니다.", "interaction_modal.description.reply": "마스토돈 계정을 통해, 이 게시물에 응답할 수 있습니다.", + "interaction_modal.description.vote": "마스토돈 계정을 통해, 이 투표에 참여할 수 있습니다.", "interaction_modal.login.action": "홈 서버로 가기", "interaction_modal.login.prompt": "내 홈 서버의 도메인. 예시: mastodon.social", "interaction_modal.no_account_yet": "Mastodon 계정이 없나요?", @@ -397,6 +416,7 @@ "interaction_modal.title.follow": "{name} 님을 팔로우", "interaction_modal.title.reblog": "{name} 님의 게시물을 부스트", "interaction_modal.title.reply": "{name} 님의 게시물에 답글", + "interaction_modal.title.vote": "{name} 님의 투표에 참여", "intervals.full.days": "{number} 일", "intervals.full.hours": "{number} 시간", "intervals.full.minutes": "{number} 분", @@ -506,6 +526,8 @@ "notification.admin.report_statuses_other": "{name} 님이 {target}을 신고했습니다", "notification.admin.sign_up": "{name} 님이 가입했습니다", "notification.admin.sign_up.name_and_others": "{name} 외 {count, plural, other {# 명}}이 가입했습니다", + "notification.annual_report.message": "{year} #Wrapstodon 이 기다리고 있습니다! 올 해 마스토돈에서 있었던 최고의 순간과 기억들을 열어보세요!", + "notification.annual_report.view": "#Wrapstodon 보기", "notification.favourite": "{name} 님이 내 게시물을 좋아합니다", "notification.favourite.name_and_others_with_link": "{name} 외 {count, plural, other {# 명}}이 내 게시물을 좋아합니다", "notification.follow": "{name} 님이 나를 팔로우했습니다", diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json index 7d53c2702fc99e..d03833e11f53f4 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -87,6 +87,24 @@ "alert.unexpected.title": "Ups!", "alt_text_badge.title": "Alternatyvus tekstas", "announcement.announcement": "Skelbimas", + "annual_report.summary.archetype.booster": "Šaunus medžiotojas", + "annual_report.summary.archetype.lurker": "Stebėtojas", + "annual_report.summary.archetype.oracle": "Vydūnas", + "annual_report.summary.archetype.pollster": "Apklausos rengėjas", + "annual_report.summary.archetype.replier": "Socialinis drugelis", + "annual_report.summary.followers.followers": "sekėjai (-ų)", + "annual_report.summary.followers.total": "iš viso {count}", + "annual_report.summary.here_it_is": "Štai jūsų {year} apžvalga:", + "annual_report.summary.highlighted_post.by_favourites": "labiausiai pamėgtas įrašas", + "annual_report.summary.highlighted_post.by_reblogs": "labiausiai pasidalintas įrašas", + "annual_report.summary.highlighted_post.by_replies": "įrašas su daugiausiai atsakymų", + "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.summary.most_used_app.most_used_app": "labiausiai naudota programa", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "labiausiai naudotas saitažodis", + "annual_report.summary.new_posts.new_posts": "nauji įrašai", + "annual_report.summary.percentile.text": "Tai reiškia, kad esate tarppopuliariausių „Mastodon“ naudotojų.", + "annual_report.summary.percentile.we_wont_tell_bernie": "Mes nesakysime Bernie.", + "annual_report.summary.thanks": "Dėkojame, kad esate „Mastodon“ dalis!", "attachments_list.unprocessed": "(neapdorotas)", "audio.hide": "Slėpti garsą", "block_modal.remote_users_caveat": "Paprašysime serverio {domain} gerbti tavo sprendimą. Tačiau atitiktis negarantuojama, nes kai kurie serveriai gali skirtingai tvarkyti blokavimus. Vieši įrašai vis tiek gali būti matomi neprisijungusiems naudotojams.", @@ -507,6 +525,8 @@ "notification.admin.report_statuses": "{name} pranešė {target} kategorijai {category}", "notification.admin.report_statuses_other": "{name} pranešė {target}", "notification.admin.sign_up": "{name} užsiregistravo", + "notification.annual_report.message": "Jūsų laukia {year} #Wrapstodon! Atskleiskite savo metų svarbiausius įvykius ir įsimintinas akimirkas platformoje „Mastodon“.", + "notification.annual_report.view": "Peržiūrėti #Wrapstodon", "notification.favourite": "{name} pamėgo tavo įrašą", "notification.follow": "{name} seka tave", "notification.follow.name_and_others": "{name} ir {count, plural, one {# kitas} few {# kiti} many {# kito} other {# kitų}} seka tave", diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json index d7507ee8843dcd..30fc0ea3484f1b 100644 --- a/app/javascript/mastodon/locales/lv.json +++ b/app/javascript/mastodon/locales/lv.json @@ -36,6 +36,7 @@ "account.followers.empty": "Šim lietotājam vēl nav sekotāju.", "account.followers_counter": "{count, plural, zero {{count} sekotāju} one {{count} sekotājs} other {{count} sekotāji}}", "account.following": "Seko", + "account.following_counter": "{count, plural, one {seko {counter}} other {seko {counter}}}", "account.follows.empty": "Šis lietotājs pagaidām nevienam neseko.", "account.go_to_profile": "Doties uz profilu", "account.hide_reblogs": "Paslēpt @{name} pastiprinātos ierakstus", @@ -61,6 +62,7 @@ "account.requested_follow": "{name} nosūtīja Tev sekošanas pieprasījumu", "account.share": "Dalīties ar @{name} profilu", "account.show_reblogs": "Parādīt @{name} pastiprinātos ierakstus", + "account.statuses_counter": "{count, plural, zero {{counter} ierakstu} one {{counter} ieraksts} other {{counter} ieraksti}}", "account.unblock": "Atbloķēt @{name}", "account.unblock_domain": "Atbloķēt domēnu {domain}", "account.unblock_short": "Atbloķēt", @@ -191,6 +193,7 @@ "confirmations.unfollow.title": "Pārtraukt sekošanu lietotājam?", "content_warning.hide": "Paslēpt ierakstu", "content_warning.show": "Tomēr rādīt", + "content_warning.show_more": "Rādīt vairāk", "conversation.delete": "Dzēst sarunu", "conversation.mark_as_read": "Atzīmēt kā izlasītu", "conversation.open": "Skatīt sarunu", @@ -211,6 +214,7 @@ "dismissable_banner.explore_tags": "Šie ir tēmturi, kas šodien gūst uzmanību sabiedriskajā tīmeklī. Tēmturi, kurus izmanto vairāk dažādu cilvēku, tiek vērtēti augstāk.", "dismissable_banner.public_timeline": "Šie ir jaunākie publiskie ieraksti no lietotājiem sociālajā tīmeklī, kuriem {domain} seko cilvēki.", "domain_block_modal.block": "Bloķēt serveri", + "domain_block_modal.block_account_instead": "Tā vietā liegt @{name}", "domain_block_modal.they_cant_follow": "Neviens šajā serverī nevar Tev sekot.", "domain_block_modal.they_wont_know": "Viņi nezinās, ka tikuši bloķēti.", "domain_block_modal.title": "Bloķēt domēnu?", @@ -329,6 +333,7 @@ "home.pending_critical_update.link": "Skatīt jauninājumus", "home.pending_critical_update.title": "Ir pieejams būtisks drošības atjauninājums.", "home.show_announcements": "Rādīt paziņojumus", + "ignore_notifications_modal.ignore": "Neņemt vērā paziņojumus", "interaction_modal.description.favourite": "Ar Mastodon kontu tu vari pievienot šo ziņu izlasei, lai informētu autoru, ka to novērtē, un saglabātu to vēlākai lasīšanai.", "interaction_modal.description.follow": "Ar Mastodon kontu Tu vari sekot {name}, lai saņemtu lietotāja ierakstus savā mājas plūsmā.", "interaction_modal.description.reblog": "Ar Mastodon kontu Tu vari izvirzīt šo ierakstu, lai kopīgotu to ar saviem sekotājiem.", @@ -505,10 +510,10 @@ "onboarding.action.back": "Aizved mani atpakaļ", "onboarding.actions.back": "Aizved mani atpakaļ", "onboarding.actions.go_to_explore": "Skatīt tendences", - "onboarding.actions.go_to_home": "Dodieties uz manu mājas plūsmu", + "onboarding.actions.go_to_home": "Doties uz manu sākuma plūsmu", "onboarding.compose.template": "Sveiki, #Mastodon!", "onboarding.follows.empty": "Diemžēl pašlaik nevar parādīt rezultātus. Vari mēģināt izmantot meklēšanu vai pārlūkot izpētes lapu, lai atrastu cilvēkus, kuriem sekot, vai vēlāk mēģināt vēlreiz.", - "onboarding.follows.lead": "Tava mājas plūsma ir galvenais veids, kā pieredzēt Mastodon. Jo vairāk cilvēkiem sekosi, jo dzīvīgāka un aizraujošāka tā būs. Lai sāktu, šeit ir daži ieteikumi:", + "onboarding.follows.lead": "Tava sākuma plūsma ir galvenais veids, kā pieredzēt Mastodon. Jo vairāk cilvēkiem sekosi, jo dzīvīgāka un aizraujošāka tā būs. Lai sāktu, šeit ir daži ieteikumi:", "onboarding.follows.title": "Pielāgo savu mājas barotni", "onboarding.profile.discoverable": "Padarīt manu profilu atklājamu", "onboarding.profile.display_name": "Attēlojamais vārds", @@ -526,7 +531,7 @@ "onboarding.start.lead": "Tagad Tu esi daļa no Mastodon — vienreizējas, decentralizētas sociālās mediju platformas, kurā Tu, nevis algoritms, veido Tavu pieredzi. Sāksim darbu šajā jaunajā sociālajā jomā:", "onboarding.start.skip": "Nav nepieciešama palīdzība darba sākšanai?", "onboarding.start.title": "Tev tas izdevās!", - "onboarding.steps.follow_people.body": "Tu pats veido savu plūsmu. Piepildīsim to ar interesantiem cilvēkiem.", + "onboarding.steps.follow_people.body": "Sekošana aizraujošiem cilvēkiem ir tas, par ko ir Mastodon.", "onboarding.steps.follow_people.title": "Pielāgo savu mājas barotni", "onboarding.steps.publish_status.body": "Pasveicini pasauli ar tekstu, attēliem, video vai aptaujām {emoji}", "onboarding.steps.publish_status.title": "Izveido savu pirmo ziņu", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 9f265782e111b3..b66fcf09c04c0c 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -87,6 +87,24 @@ "alert.unexpected.title": "Oeps!", "alt_text_badge.title": "Alt-tekst", "announcement.announcement": "Mededeling", + "annual_report.summary.archetype.booster": "De cool-hunter", + "annual_report.summary.archetype.lurker": "De lurker", + "annual_report.summary.archetype.oracle": "Het orakel", + "annual_report.summary.archetype.pollster": "De opiniepeiler", + "annual_report.summary.archetype.replier": "De sociale vlinder", + "annual_report.summary.followers.followers": "volgers", + "annual_report.summary.followers.total": "totaal {count}", + "annual_report.summary.here_it_is": "Hier is jouw terugblik op {year}:", + "annual_report.summary.highlighted_post.by_favourites": "bericht met de meeste favorieten", + "annual_report.summary.highlighted_post.by_reblogs": "bericht met de meeste boosts", + "annual_report.summary.highlighted_post.by_replies": "bericht met de meeste reacties", + "annual_report.summary.highlighted_post.possessive": "{name}'s", + "annual_report.summary.most_used_app.most_used_app": "meest gebruikte app", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "meest gebruikte hashtag", + "annual_report.summary.new_posts.new_posts": "nieuwe berichten", + "annual_report.summary.percentile.text": "Dat zet je in de topvan Mastodon-gebruikers.", + "annual_report.summary.percentile.we_wont_tell_bernie": "We zullen Bernie niets vertellen.", + "annual_report.summary.thanks": "Bedankt dat je deel uitmaakt van Mastodon!", "attachments_list.unprocessed": "(niet verwerkt)", "audio.hide": "Audio verbergen", "block_modal.remote_users_caveat": "We vragen de server {domain} om je besluit te respecteren. Het naleven hiervan is echter niet gegarandeerd, omdat sommige servers blokkades anders kunnen interpreteren. Openbare berichten zijn mogelijk nog steeds zichtbaar voor niet-ingelogde gebruikers.", @@ -508,6 +526,8 @@ "notification.admin.report_statuses_other": "{name} rapporteerde {target}", "notification.admin.sign_up": "{name} heeft zich geregistreerd", "notification.admin.sign_up.name_and_others": "{name} en {count, plural, one {# ander persoon} other {# andere personen}} hebben zich geregistreerd", + "notification.annual_report.message": "Jouw {year} #Wrapstodon staat klaar! Laat de hoogtepunten en memorabele momenten van jouw jaar zien op Mastodon!", + "notification.annual_report.view": "#Wrapstodon bekijken", "notification.favourite": "{name} markeerde jouw bericht als favoriet", "notification.favourite.name_and_others_with_link": "{name} en {count, plural, one {# ander persoon} other {# andere personen}} hebben jouw bericht als favoriet gemarkeerd", "notification.follow": "{name} volgt jou nu", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index c620933abe2ae3..5e69741f201748 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -87,6 +87,24 @@ "alert.unexpected.title": "Eita!", "alt_text_badge.title": "Texto alternativo", "announcement.announcement": "Comunicados", + "annual_report.summary.archetype.booster": "Caçador legal", + "annual_report.summary.archetype.lurker": "O espreitador", + "annual_report.summary.archetype.oracle": "O oráculo", + "annual_report.summary.archetype.pollster": "O pesquisador", + "annual_report.summary.archetype.replier": "A borboleta social", + "annual_report.summary.followers.followers": "seguidores", + "annual_report.summary.followers.total": "{count} total", + "annual_report.summary.here_it_is": "Aqui está seu {year} em revisão:", + "annual_report.summary.highlighted_post.by_favourites": "publicação mais favoritada", + "annual_report.summary.highlighted_post.by_reblogs": "publicação mais impulsionada", + "annual_report.summary.highlighted_post.by_replies": "publicação com mais respostas", + "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.summary.most_used_app.most_used_app": "aplicativo mais usado", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "hashtag mais usada", + "annual_report.summary.new_posts.new_posts": "novas publicações", + "annual_report.summary.percentile.text": "Isso o coloca no topodos usuários de Mastodon.", + "annual_report.summary.percentile.we_wont_tell_bernie": "Não contaremos à Bernie.", + "annual_report.summary.thanks": "Obrigada por fazer parte do Mastodon!", "attachments_list.unprocessed": "(não processado)", "audio.hide": "Ocultar áudio", "block_modal.remote_users_caveat": "Pediremos ao servidor {domínio} que respeite sua decisão. No entanto, a conformidade não é garantida pois alguns servidores podem lidar com os blocos de maneira diferente. As postagens públicas ainda podem estar visíveis para usuários não logados.", @@ -508,6 +526,8 @@ "notification.admin.report_statuses_other": "{name} denunciou {target}", "notification.admin.sign_up": "{name} se inscreveu", "notification.admin.sign_up.name_and_others": "{name} e {count, plural, one {# other} other {# outros}}", + "notification.annual_report.message": "O #Wrapstodon do seu {year} está esperando! Desvende seus destaques do ano e momentos memoráveis no Mastodon!", + "notification.annual_report.view": "Ver #Wrapstodon", "notification.favourite": "{name} favoritou sua publicação", "notification.favourite.name_and_others_with_link": "{name} e {count, plural, one {# outro} other {# others}} favoritaram a publicação", "notification.follow": "{name} te seguiu", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index 2a985a1f848221..f5617550e16e7c 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -40,7 +40,7 @@ "account.follows.empty": "Этот пользователь пока ни на кого не подписался.", "account.go_to_profile": "Перейти к профилю", "account.hide_reblogs": "Скрыть продвижения от @{name}", - "account.in_memoriam": "В Памяти.", + "account.in_memoriam": "Вечная память.", "account.joined_short": "Присоединился", "account.languages": "Изменить языки подписки", "account.link_verified_on": "Владение этой ссылкой было проверено {date}", @@ -62,13 +62,13 @@ "account.requested_follow": "{name} отправил(а) вам запрос на подписку", "account.share": "Поделиться профилем @{name}", "account.show_reblogs": "Показывать продвижения от @{name}", - "account.statuses_counter": "{count, plural, one {# пост} few {# поста} many {# постов} other {# постов}}", + "account.statuses_counter": "{count, plural, one {{counter} пост} few {{counter} поста} other {{counter} постов}}", "account.unblock": "Разблокировать @{name}", "account.unblock_domain": "Разблокировать {domain}", "account.unblock_short": "Разблокировать", "account.unendorse": "Не рекомендовать в профиле", "account.unfollow": "Отписаться", - "account.unmute": "Убрать {name} из игнорируемых", + "account.unmute": "Перестать игнорировать @{name}", "account.unmute_notifications_short": "Включить уведомления", "account.unmute_short": "Не игнорировать", "account_note.placeholder": "Текст заметки", @@ -84,9 +84,23 @@ "alert.rate_limited.message": "Пожалуйста, повторите после {retry_time, time, medium}.", "alert.rate_limited.title": "Ограничение количества запросов", "alert.unexpected.message": "Произошла непредвиденная ошибка.", - "alert.unexpected.title": "Упс!", + "alert.unexpected.title": "Ой!", "alt_text_badge.title": "Альтернативный текст", "announcement.announcement": "Объявление", + "annual_report.summary.archetype.booster": "Репостер", + "annual_report.summary.archetype.lurker": "Молчун", + "annual_report.summary.archetype.pollster": "Опросчик", + "annual_report.summary.archetype.replier": "Душа компании", + "annual_report.summary.followers.followers": "подписчиков", + "annual_report.summary.here_it_is": "Вот ваши итоги {year} года:", + "annual_report.summary.highlighted_post.by_favourites": "пост с наибольшим количеством звёздочек", + "annual_report.summary.highlighted_post.by_reblogs": "пост с наибольшим количеством продвижений", + "annual_report.summary.highlighted_post.by_replies": "пост с наибольшим количеством ответов", + "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.summary.most_used_app.most_used_app": "наиболее часто используемое приложение", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "наиболее часто используемый хэштег", + "annual_report.summary.new_posts.new_posts": "новых постов", + "annual_report.summary.thanks": "Спасибо за то, что были вместе с Mastodon!", "attachments_list.unprocessed": "(не обработан)", "audio.hide": "Скрыть аудио", "block_modal.remote_users_caveat": "Мы попросим сервер {domain} уважать ваше решение. Однако, соблюдение требований не гарантировано, поскольку некоторые серверы могут работать с блокировками по-разному. Публичные записи по-прежнему могут быть видны неавторизованным пользователям.", @@ -95,11 +109,11 @@ "block_modal.they_cant_mention": "Он не может упоминать или подписываться на вас.", "block_modal.they_cant_see_posts": "Он не может видеть ваши сообщения, и вы не увидите его.", "block_modal.they_will_know": "Он может видеть, что он заблокирован.", - "block_modal.title": "Заблокировать пользователя?", + "block_modal.title": "Заблокируем пользователя?", "block_modal.you_wont_see_mentions": "Вы не увидите записи, которые упоминают его.", "boost_modal.combo": "{combo}, чтобы пропустить это в следующий раз", - "boost_modal.reblog": "Повысить пост?", - "boost_modal.undo_reblog": "Разгрузить пост?", + "boost_modal.reblog": "Репостнуть?", + "boost_modal.undo_reblog": "Убрать репост?", "bundle_column_error.copy_stacktrace": "Скопировать отчет об ошибке", "bundle_column_error.error.body": "Запрошенная страница не может быть отображена. Это может быть вызвано ошибкой в нашем коде или проблемой совместимости браузера.", "bundle_column_error.error.title": "О нет!", @@ -144,12 +158,12 @@ "community.column_settings.local_only": "Только локальные", "community.column_settings.media_only": "Только с медиафайлами", "community.column_settings.remote_only": "Только удалённые", - "compose.language.change": "Изменить язык", + "compose.language.change": "Сменить язык", "compose.language.search": "Поиск языков...", - "compose.published.body": "Запись опубликована.", + "compose.published.body": "Пост опубликован.", "compose.published.open": "Открыть", - "compose.saved.body": "Запись сохранена.", - "compose_form.direct_message_warning_learn_more": "Подробнее", + "compose.saved.body": "Пост отредактирован.", + "compose_form.direct_message_warning_learn_more": "Узнать больше", "compose_form.encryption_warning": "Посты в Mastodon не защищены сквозным шифрованием. Не делитесь конфиденциальной информацией через Mastodon.", "compose_form.hashtag_warning": "Этот пост не будет виден ни под одним из хэштегов, так как он не публичный. Только публичные посты можно найти по хэштегу.", "compose_form.lock_disclaimer": "Ваша учётная запись {locked}. Любой пользователь сможет подписаться на вас и просматривать посты для подписчиков.", @@ -158,16 +172,17 @@ "compose_form.poll.duration": "Продолжительность опроса", "compose_form.poll.multiple": "Несколько вариантов ответа", "compose_form.poll.option_placeholder": "Вариант {number}", + "compose_form.poll.single": "Один вариант ответа", "compose_form.poll.switch_to_multiple": "Разрешить выбор нескольких вариантов", "compose_form.poll.switch_to_single": "Переключить в режим выбора одного ответа", - "compose_form.poll.type": "Стиль", + "compose_form.poll.type": "Тип", "compose_form.publish": "Опубликовать", "compose_form.publish_form": "Опубликовать", "compose_form.reply": "Ответить", "compose_form.save_changes": "Сохранить", "compose_form.spoiler.marked": "Текст скрыт за предупреждением", "compose_form.spoiler.unmarked": "Текст не скрыт", - "compose_form.spoiler_placeholder": "Предупреждение о контенте (опционально)", + "compose_form.spoiler_placeholder": "Предупреждение о содержимом (необязательно)", "confirmation_modal.cancel": "Отмена", "confirmations.block.confirm": "Заблокировать", "confirmations.delete.confirm": "Удалить", @@ -177,17 +192,17 @@ "confirmations.delete_list.message": "Вы действительно хотите навсегда удалить этот список?", "confirmations.delete_list.title": "Удалить список?", "confirmations.discard_edit_media.confirm": "Отменить", - "confirmations.discard_edit_media.message": "У вас есть несохранённые изменения описания мультимедиа или предпросмотра, отменить их?", + "confirmations.discard_edit_media.message": "У вас имеются несохранённые изменения превью и описания медиафайла, отменить их?", "confirmations.edit.confirm": "Редактировать", - "confirmations.edit.message": "В данный момент, редактирование перезапишет составляемое вами сообщение. Вы уверены, что хотите продолжить?", + "confirmations.edit.message": "При редактировании, текст набираемого поста будет очищен. Продолжить?", "confirmations.edit.title": "Переписать сообщение?", "confirmations.logout.confirm": "Выйти", "confirmations.logout.message": "Вы уверены, что хотите выйти?", "confirmations.logout.title": "Выйти?", "confirmations.mute.confirm": "Игнорировать", "confirmations.redraft.confirm": "Удалить и исправить", - "confirmations.redraft.message": "Вы уверены, что хотите удалить и переписать этот пост? Отметки «избранного», продвижения и ответы к оригинальному посту будут удалены.", - "confirmations.redraft.title": "Удалим и исправим пост?", + "confirmations.redraft.message": "Вы уверены, что хотите удалить и переписать этот пост? Отметки «избранного», продвижения и ответы к оригинальному посту будут потеряны.", + "confirmations.redraft.title": "Создать пост заново?", "confirmations.reply.confirm": "Ответить", "confirmations.reply.message": "При ответе, текст набираемого поста будет очищен. Продолжить?", "confirmations.reply.title": "Перепишем пост?", @@ -196,6 +211,7 @@ "confirmations.unfollow.title": "Отписаться?", "content_warning.hide": "Скрыть пост", "content_warning.show": "Всё равно показать", + "content_warning.show_more": "Покажи ещё", "conversation.delete": "Удалить беседу", "conversation.mark_as_read": "Отметить как прочитанное", "conversation.open": "Просмотр беседы", @@ -221,6 +237,7 @@ "domain_block_modal.they_cant_follow": "Никто из этого сервера не может подписываться на вас.", "domain_block_modal.they_wont_know": "Он не будет знать, что его заблокировали.", "domain_block_modal.title": "Заблокировать домен?", + "domain_block_modal.you_will_lose_num_followers": "Вы потеряете {followersCount, plural, one {{followersCountDisplay} подписчика} other {{followersCountDisplay} подписчиков}} и {followingCount, plural, one {{followingCountDisplay} подписку} other {{followingCountDisplay} подписок}}.", "domain_block_modal.you_will_lose_relationships": "Вы потеряете всех подписчиков и людей, на которых вы подписаны, на этом сервере.", "domain_block_modal.you_wont_see_posts": "Вы не будете видеть записи или уведомления от пользователей на этом сервере.", "domain_pill.activitypub_lets_connect": "Это позволяет вам общаться и взаимодействовать с людьми не только на Mastodon, но и в различных социальных приложениях.", @@ -240,7 +257,7 @@ "embed.preview": "Так это будет выглядеть:", "emoji_button.activity": "Занятия", "emoji_button.clear": "Очистить", - "emoji_button.custom": "С этого узла", + "emoji_button.custom": "С этого сервера", "emoji_button.flags": "Флаги", "emoji_button.food": "Еда и напитки", "emoji_button.label": "Вставить эмодзи", @@ -303,6 +320,7 @@ "filter_modal.select_filter.subtitle": "Используйте существующую категорию или создайте новую", "filter_modal.select_filter.title": "Фильтровать этот пост", "filter_modal.title.status": "Фильтровать пост", + "filter_warning.matches_filter": "Соответствует фильтру \"{title}\"", "filtered_notifications_banner.pending_requests": "Вы можете знать {count, plural, =0 {ни одного человека} one {одного человека} other {# человек}}", "filtered_notifications_banner.title": "Отфильтрованные уведомления", "firehose.all": "Все", @@ -346,12 +364,12 @@ "hashtag.column_settings.tag_mode.any": "Любой из списка", "hashtag.column_settings.tag_mode.none": "Ни один из списка", "hashtag.column_settings.tag_toggle": "Включить дополнительные теги для этой колонки", - "hashtag.counter_by_accounts": "{count, plural, one {{counter} участник} few {{counter} участников} many {{counter} участников} other {{counter} участников}}", - "hashtag.counter_by_uses": "{count, plural, one {{counter} сообщение} few {{counter} сообщения} many {{counter} сообщения} other {{counter} сообщения}}", - "hashtag.counter_by_uses_today": "{count, plural, one {{counter} сообщение} other {{counter} сообщений}} сегодня", + "hashtag.counter_by_accounts": "{count, plural, one {{counter} пользователь} few {{counter} пользователя} other {{counter} пользователей}}", + "hashtag.counter_by_uses": "{count, plural, one {{counter} пост} few {{counter} поста} other {{counter} постов}}", + "hashtag.counter_by_uses_today": "{count, plural, one {{counter} пост} few {{counter} поста} other {{counter} постов}} сегодня", "hashtag.follow": "Подписаться на новые посты", "hashtag.unfollow": "Отписаться", - "hashtags.and_other": "...и {count, plural, other {# ещё}}", + "hashtags.and_other": "…и {count, plural, other {ещё #}}", "hints.profiles.followers_may_be_missing": "Подписчики у этого профиля могут отсутствовать.", "hints.profiles.follows_may_be_missing": "Фолловеры для этого профиля могут отсутствовать.", "hints.profiles.posts_may_be_missing": "Некоторые сообщения из этого профиля могут отсутствовать.", @@ -382,6 +400,7 @@ "interaction_modal.description.follow": "С учётной записью Mastodon вы можете подписаться на {name}, чтобы получать их посты в своей домашней ленте.", "interaction_modal.description.reblog": "С учётной записью Mastodon, вы можете продвинуть этот пост, чтобы поделиться им со своими подписчиками.", "interaction_modal.description.reply": "Вы можете ответить на этот пост с учётной записью Mastodon.", + "interaction_modal.description.vote": "Вы сможете проголосовать тут имея аккаунт Mastodon.", "interaction_modal.login.action": "Перейти на домашнюю страницу", "interaction_modal.login.prompt": "Домен вашего домашнего сервера, например, mastodon.social", "interaction_modal.no_account_yet": "Еще не на Mastodon?", @@ -393,6 +412,7 @@ "interaction_modal.title.follow": "Подписаться на {name}", "interaction_modal.title.reblog": "Продвинуть публикацию {name}", "interaction_modal.title.reply": "Ответить на пост {name}", + "interaction_modal.title.vote": "Голосовать в опросе {name}", "intervals.full.days": "{number, plural, one {# день} few {# дня} other {# дней}}", "intervals.full.hours": "{number, plural, one {# час} few {# часа} other {# часов}}", "intervals.full.minutes": "{number, plural, one {# минута} few {# минуты} other {# минут}}", @@ -405,8 +425,8 @@ "keyboard_shortcuts.direct": "чтобы открыть столбец личных упоминаний", "keyboard_shortcuts.down": "вниз по списку", "keyboard_shortcuts.enter": "открыть пост", - "keyboard_shortcuts.favourite": "Добавить пост в избранное", - "keyboard_shortcuts.favourites": "Открыть «Избранное»", + "keyboard_shortcuts.favourite": "добавить пост в избранное", + "keyboard_shortcuts.favourites": "открыть «Избранные»", "keyboard_shortcuts.federated": "перейти к глобальной ленте", "keyboard_shortcuts.heading": "Сочетания клавиш", "keyboard_shortcuts.home": "перейти к домашней ленте", @@ -414,7 +434,7 @@ "keyboard_shortcuts.legend": "показать это окно", "keyboard_shortcuts.local": "перейти к локальной ленте", "keyboard_shortcuts.mention": "упомянуть автора поста", - "keyboard_shortcuts.muted": "Открыть список игнорируемых", + "keyboard_shortcuts.muted": "открыть список игнорируемых", "keyboard_shortcuts.my_profile": "перейти к своему профилю", "keyboard_shortcuts.notifications": "перейти к уведомлениям", "keyboard_shortcuts.open_media": "открыть вложение", @@ -426,7 +446,7 @@ "keyboard_shortcuts.spoilers": "показать/скрыть поле предупреждения о содержании", "keyboard_shortcuts.start": "Перейти к разделу \"Начать\"", "keyboard_shortcuts.toggle_hidden": "показать/скрыть текст за предупреждением", - "keyboard_shortcuts.toggle_sensitivity": "Показать/скрыть медиафайлы", + "keyboard_shortcuts.toggle_sensitivity": "показать/скрыть медиафайлы", "keyboard_shortcuts.toot": "начать писать новый пост", "keyboard_shortcuts.unfocus": "убрать фокус с поля ввода/поиска", "keyboard_shortcuts.up": "вверх по списку", @@ -468,8 +488,8 @@ "mute_modal.you_wont_see_mentions": "Вы не увидите постов, которые их упоминают.", "mute_modal.you_wont_see_posts": "Они по-прежнему смогут видеть ваши посты, но вы не сможете видеть их посты.", "navigation_bar.about": "О проекте", - "navigation_bar.administration": "Администрация", - "navigation_bar.advanced_interface": "Включить многоколоночный интерфейс", + "navigation_bar.administration": "Администрирование", + "navigation_bar.advanced_interface": "Открыть в многоколоночном интерфейсе", "navigation_bar.blocks": "Заблокированные пользователи", "navigation_bar.bookmarks": "Закладки", "navigation_bar.community_timeline": "Локальная лента", @@ -495,26 +515,29 @@ "navigation_bar.search": "Поиск", "navigation_bar.security": "Безопасность", "not_signed_in_indicator.not_signed_in": "Вам нужно войти, чтобы иметь доступ к этому ресурсу.", - "notification.admin.report": "{name} сообщил о {target}", - "notification.admin.report_account": "{name} сообщил {count, plural, one {один пост} other {# постов}} от {target} для {category}", - "notification.admin.report_account_other": "{name} сообщил {count, plural, one {одно сообщение} other {# сообщений}} от {target}", - "notification.admin.report_statuses": "{name} сообщил {target} для {category}", - "notification.admin.report_statuses_other": "{name} сообщает {target}", - "notification.admin.sign_up": "{name} зарегистрирован", - "notification.admin.sign_up.name_and_others": "{name} и {count, plural, one {# другой} other {# другие}} подписались", + "notification.admin.report": "{name} пожаловался на {target}", + "notification.admin.report_account": "{name} пожаловался на {count, plural, one {# пост} few {# поста} other {# постов}} от пользователя {target} по причине: {category}", + "notification.admin.report_account_other": "{name} пожаловался на {count, plural, one {# пост} few {# поста} other {# постов}} от пользователя {target}", + "notification.admin.report_statuses": "{name} пожаловался на {target} по причине: {category}", + "notification.admin.report_statuses_other": "{name} пожаловался на {target}", + "notification.admin.sign_up": "{name} зарегистрировался", + "notification.admin.sign_up.name_and_others": "{name} и ещё {count, plural, one {# пользователь} few {# пользователя} other {# пользователей}} зарегистрировались", + "notification.annual_report.message": "#Wrapstodon за {year} год ждёт вас! Откройте для себя итоги и памятные моменты этого года в Mastodon!", + "notification.annual_report.view": "Перейти к #Wrapstodon", "notification.favourite": "{name} добавил(а) ваш пост в избранное", - "notification.favourite.name_and_others_with_link": "{name} и {count, plural, one {# другие} other {# другие}} отдали предпочтение вашему посту", + "notification.favourite.name_and_others_with_link": "{name} и ещё {count, plural, one {# пользователь} few {# пользователя} other {# пользователей}} добавили ваш пост в избранное", "notification.follow": "{name} подписался (-лась) на вас", + "notification.follow.name_and_others": "{name} и ещё {count, plural, one {# пользователь} few {# пользователя} other {# пользователей}} подписались на вас", "notification.follow_request": "{name} отправил запрос на подписку", "notification.follow_request.name_and_others": "{name} и ещё {count, plural, one {#} other {# других}} подписались на вас", "notification.label.mention": "Упоминание", - "notification.label.private_mention": "Частное упоминание", - "notification.label.private_reply": "Частный ответ", - "notification.label.reply": "Ответить", + "notification.label.private_mention": "Личное упоминание", + "notification.label.private_reply": "Приватный ответ", + "notification.label.reply": "Ответ", "notification.mention": "Упоминание", "notification.mentioned_you": "{name} упомянул(а) вас", "notification.moderation-warning.learn_more": "Узнать больше", - "notification.moderation_warning": "Вы получили предупреждение от модерации", + "notification.moderation_warning": "Модераторы вынесли вам предупреждение", "notification.moderation_warning.action_delete_statuses": "Некоторые из ваших публикаций были удалены.", "notification.moderation_warning.action_disable": "Ваша учётная запись была отключена.", "notification.moderation_warning.action_mark_statuses_as_sensitive": "Некоторые из ваших сообщений были отмечены как деликатные.", @@ -525,7 +548,7 @@ "notification.own_poll": "Ваш опрос закончился", "notification.poll": "Голосование, в котором вы приняли участие, завершилось", "notification.reblog": "{name} продвинул(а) ваш пост", - "notification.reblog.name_and_others_with_link": "{name} и {count, plural, one {# other} other {# others}} увеличили ваш пост", + "notification.reblog.name_and_others_with_link": "{name} и ещё {count, plural, one {# пользователь} few {# пользователя} other {# пользователей}} продвинули ваш пост", "notification.relationships_severance_event": "Потеряно соединение с {name}", "notification.relationships_severance_event.account_suspension": "Администратор {from} заблокировал {target}, что означает, что вы больше не сможете получать обновления от них или взаймодествовать с ними.", "notification.relationships_severance_event.domain_block": "Администратор {from} заблокировал {target} включая {followersCount} ваших подписчиков и {followingCount, plural, one {# аккаунт} few {# аккаунта} other {# аккаунтов}}, на которые вы подписаны.", @@ -534,10 +557,15 @@ "notification.status": "{name} только что запостил", "notification.update": "{name} изменил(а) пост", "notification_requests.accept": "Принять", + "notification_requests.accept_multiple": "{count, plural, one {Принять # запрос…} few {Принять # запроса…} other {Принять # запросов…}}", "notification_requests.confirm_accept_multiple.button": "{count, plural, one {Принять запрос} other {Принять запросы}}", + "notification_requests.confirm_accept_multiple.message": "Вы собираетесь принять {count, plural, one {# запрос на показ уведомлений} few {# запроса на показ уведомлений} other {# запросов на показ уведомлений}}. Продолжить?", "notification_requests.confirm_accept_multiple.title": "Принимать запросы на уведомления?", + "notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Отклонить запрос} other {Отклонить запросы}}", + "notification_requests.confirm_dismiss_multiple.message": "Вы собираетесь отклонить {count, plural, one {# запрос на показ уведомлений} few {# запроса на показ уведомлений} other {# запросов на показ уведомлений}}. Вы не сможете просмотреть {count, plural, other {их}} потом. Продолжить?", "notification_requests.confirm_dismiss_multiple.title": "Отклонять запросы на уведомления?", "notification_requests.dismiss": "Отклонить", + "notification_requests.dismiss_multiple": "{count, plural, one {Отклонить # запрос…} few {Отклонить # запроса…} other {Отклонить # запросов…}}", "notification_requests.edit_selection": "Редактировать", "notification_requests.exit_selection": "Готово", "notification_requests.explainer_for_limited_account": "Уведомления от этой учетной записи были отфильтрованы, поскольку учетная запись была ограничена модератором.", @@ -549,15 +577,16 @@ "notification_requests.view": "Просмотр уведомлений", "notifications.clear": "Очистить уведомления", "notifications.clear_confirmation": "Вы уверены, что хотите очистить все уведомления?", - "notifications.clear_title": "Сбросить уведомления?", + "notifications.clear_title": "Очистить уведомления?", "notifications.column_settings.admin.report": "Новые жалобы:", "notifications.column_settings.admin.sign_up": "Новые регистрации:", "notifications.column_settings.alert": "Уведомления на рабочем столе", - "notifications.column_settings.favourite": "Избранные:", - "notifications.column_settings.filter_bar.advanced": "Отображать все категории", + "notifications.column_settings.favourite": "Ваш пост добавили в избранные:", + "notifications.column_settings.filter_bar.advanced": "Показать все категории", "notifications.column_settings.filter_bar.category": "Панель сортировки", "notifications.column_settings.follow": "У вас новый подписчик:", "notifications.column_settings.follow_request": "Новые запросы на подписку:", + "notifications.column_settings.group": "Группировать", "notifications.column_settings.mention": "Вас упомянули в посте:", "notifications.column_settings.poll": "Опрос, в котором вы приняли участие, завершился:", "notifications.column_settings.push": "Пуш-уведомления", @@ -577,31 +606,32 @@ "notifications.filter.statuses": "Обновления от людей, на которых вы подписаны", "notifications.grant_permission": "Предоставить разрешение.", "notifications.group": "{count} уведомл.", - "notifications.mark_as_read": "Отмечать все уведомления прочитанными", + "notifications.mark_as_read": "Отметить все уведомления прочитанными", "notifications.permission_denied": "Уведомления на рабочем столе недоступны, так как вы запретили их отправку в браузере. Проверьте настройки для сайта, чтобы включить их обратно.", "notifications.permission_denied_alert": "Уведомления на рабочем столе недоступны, так как вы ранее отклонили запрос на их отправку.", "notifications.permission_required": "Чтобы включить уведомления на рабочем столе, необходимо разрешить их в браузере.", - "notifications.policy.accept": "Принять", - "notifications.policy.accept_hint": "Показать в уведомлениях", - "notifications.policy.drop": "Игнорируем", - "notifications.policy.drop_hint": "Отправить в пустоту, чтобы никогда больше не увидеть", - "notifications.policy.filter": "Фильтр", - "notifications.policy.filter_hint": "Отправка в папку фильтрованных уведомлений", - "notifications.policy.filter_limited_accounts_hint": "Ограничено модераторами сервера", - "notifications.policy.filter_limited_accounts_title": "Модерируемые аккаунты", - "notifications.policy.filter_new_accounts.hint": "Создано в течение последних {days, plural, one {один день} few {# дней} many {# дней} other {# дня}}", + "notifications.policy.accept": "Принимать", + "notifications.policy.accept_hint": "Показывать в уведомлениях", + "notifications.policy.drop": "Игнорировать", + "notifications.policy.drop_hint": "Отправлять в пустоту, чтобы никогда больше не увидеть", + "notifications.policy.filter": "Фильтровать", + "notifications.policy.filter_hint": "Отправлять в раздел отфильтрованных уведомлений", + "notifications.policy.filter_limited_accounts_hint": "Ограниченные модераторами сервера", + "notifications.policy.filter_limited_accounts_title": "Модерируемые учётные записи", + "notifications.policy.filter_new_accounts.hint": "Созданные в течение {days, plural, one {последнего # дня} other {последних # дней}}", "notifications.policy.filter_new_accounts_title": "Новые учётные записи", + "notifications.policy.filter_not_followers_hint": "Включая людей, которые подписаны на вас меньше чем {days, plural, one {# день} few {# дня} other {# дней}}", "notifications.policy.filter_not_followers_title": "Люди, не подписанные на вас", "notifications.policy.filter_not_following_hint": "Пока вы не одобрите их вручную", "notifications.policy.filter_not_following_title": "Люди, на которых вы не подписаны", - "notifications.policy.filter_private_mentions_hint": "Фильтруется, если только это не ответ на ваше собственное упоминание или если вы подписаны на отправителя", + "notifications.policy.filter_private_mentions_hint": "Фильтруются, если только это не ответ на ваше собственное упоминание или если вы подписаны на отправителя", "notifications.policy.filter_private_mentions_title": "Нежелательные личные упоминания", - "notifications.policy.title": "………Управлять уведомлениями от…", + "notifications.policy.title": "Управление уведомлениями", "notifications_permission_banner.enable": "Включить уведомления", "notifications_permission_banner.how_to_control": "Получайте уведомления даже когда Mastodon закрыт, включив уведомления на рабочем столе. А чтобы лишний шум не отвлекал, вы можете настроить какие уведомления вы хотите получать, нажав на кнопку {icon} выше.", "notifications_permission_banner.title": "Будьте в курсе происходящего", - "onboarding.action.back": "Вернуть меня", - "onboarding.actions.back": "Вернуть меня", + "onboarding.action.back": "Верните меня", + "onboarding.actions.back": "Верните меня", "onboarding.actions.go_to_explore": "Посмотреть, что актуально", "onboarding.actions.go_to_home": "Перейти к домашней ленте новостей", "onboarding.compose.template": "Привет, #Mastodon!", @@ -619,7 +649,7 @@ "onboarding.profile.title": "Настройка профиля", "onboarding.profile.upload_avatar": "Загрузить фотографию профиля", "onboarding.profile.upload_header": "Загрузить заголовок профиля", - "onboarding.share.lead": "Расскажите людям, как они могут найти вас на Mastodon!", + "onboarding.share.lead": "Расскажите людям, как найти вас на Mastodon!", "onboarding.share.message": "Я {username} на #Mastodon! Следуйте за мной по адресу {url}", "onboarding.share.next_steps": "Возможные дальнейшие шаги:", "onboarding.share.title": "Поделиться вашим профилем", @@ -634,7 +664,7 @@ "onboarding.steps.setup_profile.title": "Настройте свой профиль", "onboarding.steps.share_profile.body": "Расскажите своим друзьям как найти вас на Mastodon!", "onboarding.steps.share_profile.title": "Поделитесь вашим профилем", - "onboarding.tips.2fa": "Знаете ли вы? Вы можете защитить свой аккаунт, настроив двухфакторную аутентификацию в настройках аккаунта. Она работает с любым приложением TOTP по вашему выбору, номер телефона не требуется!", + "onboarding.tips.2fa": "А вы знали? Можно защитить свой аккаунт, настроив двухфакторную аутентификацию в настройках аккаунта. Она работает с любым приложением TOTP по вашему выбору, номер телефона не нужен!", "onboarding.tips.accounts_from_other_servers": "Знали ли вы? Поскольку Mastodon децентрализован, некоторые профили, с которыми вы столкнетесь, будут размещены на серверах, отличных от вашего. И все же вы можете взаимодействовать с ними без проблем! Их сервер находится во второй половине имени пользователя!", "onboarding.tips.migration": "Знаете ли вы? Если вы чувствуете, что {domain} не подходит вам в качестве сервера в будущем, вы можете переехать на другой сервер Mastodon без потери своих подписчиков. Вы даже можете разместить свой собственный сервер!", "onboarding.tips.verification": "Знали ли вы? Вы можете подтвердить свою учетную запись, разместив ссылку на свой профиль Mastodon на собственном сайте и добавив сайт в свой профиль. Никаких сборов или документов не требуется!", @@ -678,15 +708,15 @@ "relative_time.minutes": "{number} мин", "relative_time.seconds": "{number} с", "relative_time.today": "сегодня", - "reply_indicator.attachments": "{count, plural, one {# вложение} other {# вложения}}", + "reply_indicator.attachments": "{count, plural, one {# вложение} few {# вложения} other {# вложений}}", "reply_indicator.cancel": "Отмена", "reply_indicator.poll": "Опрос", "report.block": "Заблокировать", "report.block_explanation": "Вы перестанете видеть посты этого пользователя, и он(а) больше не сможет подписаться на вас и читать ваши посты. Он(а) сможет понять, что вы заблокировали его/её.", - "report.categories.legal": "Правовая информация", + "report.categories.legal": "Нарушение закона", "report.categories.other": "Другое", "report.categories.spam": "Спам", - "report.categories.violation": "Содержимое нарушает одно или несколько правил узла", + "report.categories.violation": "Содержимое нарушает одно или несколько правил сервера", "report.category.subtitle": "Выберите наиболее подходящее", "report.category.title": "Расскажите нам, что не так с {type}", "report.category.title_account": "этим профилем", @@ -757,31 +787,32 @@ "server_banner.about_active_users": "Люди, заходившие на этот сервер за последние 30 дней (ежемесячные активные пользователи)", "server_banner.active_users": "активные пользователи", "server_banner.administered_by": "Управляется:", - "server_banner.is_one_of_many": "{domain} - это один из многих независимых серверов Mastodon, которые вы можете использовать для участия в fediverse.", + "server_banner.is_one_of_many": "{domain} — это один из многих независимых серверов Mastodon, которые вы можете использовать для участия в сети Fediverse.", "server_banner.server_stats": "Статистика сервера:", "sign_in_banner.create_account": "Зарегистрироваться", - "sign_in_banner.follow_anyone": "Следите за любым человеком в федеральной вселенной и смотрите все в хронологическом порядке. Никаких алгоритмов, рекламы или клик бейта.", - "sign_in_banner.mastodon_is": "Mastodon - лучший способ быть в курсе всего происходящего.", + "sign_in_banner.follow_anyone": "Подписывайтесь на кого угодно в федивёрсе и смотрите ленту в хронологическом порядке. Никаких алгоритмов, рекламы или кликбейта.", + "sign_in_banner.mastodon_is": "Mastodon — лучший способ быть в курсе всего происходящего.", "sign_in_banner.sign_in": "Войти", "sign_in_banner.sso_redirect": "Войдите или Зарегистрируйтесь", "status.admin_account": "Открыть интерфейс модератора для @{name}", - "status.admin_domain": "Открыть интерфейс модерации {domain}", + "status.admin_domain": "Открыть интерфейс модератора для {domain}", "status.admin_status": "Открыть этот пост в интерфейсе модератора", "status.block": "Заблокировать @{name}", - "status.bookmark": "Сохранить в закладки", + "status.bookmark": "Добавить в закладки", "status.cancel_reblog_private": "Не продвигать", "status.cannot_reblog": "Этот пост не может быть продвинут", "status.continued_thread": "Продолжение темы", "status.copy": "Скопировать ссылку на пост", "status.delete": "Удалить", "status.detailed_status": "Подробный просмотр обсуждения", - "status.direct": "Лично упоминать @{name}", - "status.direct_indicator": "Личные упоминания", + "status.direct": "Упомянуть @{name} лично", + "status.direct_indicator": "Личное упоминание", "status.edit": "Изменить", "status.edited": "Дата последнего изменения: {date}", "status.edited_x_times": "{count, plural, one {{count} изменение} many {{count} изменений} other {{count} изменения}}", "status.embed": "Получить код для встраивания", - "status.favourite": "Избранное", + "status.favourite": "Добавить в избранное", + "status.favourites": "{count, plural, other {в избранном}}", "status.filter": "Фильтровать этот пост", "status.history.created": "{name} создал {date}", "status.history.edited": "{name} отредактировал(а) {date}", @@ -800,6 +831,7 @@ "status.reblog": "Продвинуть", "status.reblog_private": "Продвинуть для своей аудитории", "status.reblogged_by": "{name} продвинул(а)", + "status.reblogs": "{count, plural, one {boost} few {boosts} many {boosts} other {boosts}}", "status.reblogs.empty": "Никто ещё не продвинул этот пост. Как только кто-то это сделает, они появятся здесь.", "status.redraft": "Создать заново", "status.remove_bookmark": "Убрать из закладок", @@ -813,13 +845,13 @@ "status.show_less_all": "Свернуть все спойлеры в ветке", "status.show_more_all": "Развернуть все спойлеры в ветке", "status.show_original": "Показать оригинал", - "status.title.with_attachments": "{user} размещено {attachmentCount, plural, one {вложение} other {{attachmentCount} вложений}}", + "status.title.with_attachments": "{user} опубликовал {attachmentCount, plural, one {{attachmentCount} вложение} few {{attachmentCount} вложения} other {{attachmentCount} вложений}}", "status.translate": "Перевод", - "status.translated_from_with": "Переведено с {lang}, используя {provider}", - "status.uncached_media_warning": "Прослушивание недоступно", + "status.translated_from_with": "Переведено с {lang} с помощью {provider}", + "status.uncached_media_warning": "Предварительный просмотр недоступен", "status.unmute_conversation": "Не игнорировать обсуждение", "status.unpin": "Открепить от профиля", - "subscribed_languages.lead": "Посты только на выбранных языках будут отображаться на вашей домашней странице и в списке лент после изменения. Выберите «Нет», чтобы получать посты на всех языках.", + "subscribed_languages.lead": "Посты лишь на выбранных языках будут появляться в вашей домашней ленте и в списках после изменения. Снимите выбор, чтобы получать посты на всех языках.", "subscribed_languages.save": "Сохранить изменения", "subscribed_languages.target": "Изменить языки подписки для {target}", "tabs_bar.home": "Главная", @@ -829,7 +861,7 @@ "time_remaining.minutes": "{number, plural, one {осталась # минута} few {осталось # минуты} many {осталось # минут} other {осталось # минут}}", "time_remaining.moments": "остались считанные мгновения", "time_remaining.seconds": "{number, plural, one {# секунда} many {# секунд} other {# секунды}}", - "trends.counter_by_accounts": "{count, plural, few {{counter} человека} other {{counter} человек}} за {days, plural, one {последний день} few {последние {days} дня} other {последние {days} дней}}", + "trends.counter_by_accounts": "{count, plural, few {{counter} человека} other {{counter} человек}} за {days, plural, one {последний {days} день} few {последние {days} дня} other {последние {days} дней}}", "trends.trending_now": "Самое актуальное", "ui.beforeunload": "Ваш черновик будет утерян, если вы покинете Mastodon.", "units.short.billion": "{count} млрд", @@ -856,11 +888,11 @@ "upload_modal.detect_text": "Найти текст на картинке", "upload_modal.edit_media": "Изменить файл", "upload_modal.hint": "Нажмите и перетащите круг в предпросмотре в точку фокуса, которая всегда будет видна на эскизах.", - "upload_modal.preparing_ocr": "Подготовка распознования…", + "upload_modal.preparing_ocr": "Подготовка распознавания…", "upload_modal.preview_label": "Предпросмотр ({ratio})", "upload_progress.label": "Загрузка...", "upload_progress.processing": "Обработка…", - "username.taken": "Данное имя пользователя уже занято. Выберите другое.", + "username.taken": "Это имя пользователя уже занято. Выберите другое", "video.close": "Закрыть видео", "video.download": "Загрузить файл", "video.exit_fullscreen": "Покинуть полноэкранный режим", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index a1bcdb9ff9fd0c..1102754fd7634a 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -87,6 +87,13 @@ "alert.unexpected.title": "Ups!", "alt_text_badge.title": "Alternatívny popis", "announcement.announcement": "Oznámenie", + "annual_report.summary.followers.followers": "sledovatelia", + "annual_report.summary.highlighted_post.by_reblogs": "najviac vyzdvihovaný príspevok", + "annual_report.summary.highlighted_post.by_replies": "príspevok s najviac odpoveďami", + "annual_report.summary.most_used_app.most_used_app": "najviac používaná aplikácia", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "najviac užívaný hashtag", + "annual_report.summary.new_posts.new_posts": "nové príspevky", + "annual_report.summary.percentile.we_wont_tell_bernie": "Nepovieme Berniemu.", "attachments_list.unprocessed": "(nespracované)", "audio.hide": "Skryť zvuk", "block_modal.show_less": "Zobraziť menej", @@ -194,6 +201,7 @@ "confirmations.unfollow.title": "Prestať sledovať užívateľa?", "content_warning.hide": "Skryť príspevok", "content_warning.show": "Aj tak zobraziť", + "content_warning.show_more": "Ukázať viac", "conversation.delete": "Vymazať konverzáciu", "conversation.mark_as_read": "Označiť ako prečítanú", "conversation.open": "Zobraziť konverzáciu", @@ -344,6 +352,9 @@ "home.pending_critical_update.link": "Zobraziť aktualizácie", "home.pending_critical_update.title": "Je dostupná kritická bezpečnostná aktualizácia.", "home.show_announcements": "Zobraziť oznámenia", + "ignore_notifications_modal.filter_instead": "Radšej triediť", + "ignore_notifications_modal.filter_to_act_users": "Stále budeš môcť akceptovať, odmietnuť, alebo nahlásiť užívateľov", + "ignore_notifications_modal.filter_to_avoid_confusion": "Triedenie pomáha vyvarovať sa možnému zmäteniu", "ignore_notifications_modal.ignore": "Ignoruj upozornenia", "interaction_modal.description.favourite": "S účtom na Mastodone môžete tento príspevok ohviezdičkovať, tak dať autorovi vedieť, že sa vám páči, a uložiť si ho na neskôr.", "interaction_modal.description.follow": "S účtom na Mastodone môžete {name} sledovať a vidieť ich príspevky vo svojom domovskom kanáli.", @@ -400,6 +411,7 @@ "lightbox.close": "Zatvoriť", "lightbox.next": "Ďalej", "lightbox.previous": "Späť", + "lightbox.zoom_out": "Priblížiť na mieru", "limited_account_hint.action": "Aj tak zobraziť profil", "limited_account_hint.title": "Tento profil bol skrytý správcami servera {domain}.", "link_preview.author": "Autor: {name}", @@ -427,7 +439,11 @@ "mute_modal.hide_options": "Skryť možnosti", "mute_modal.indefinite": "Pokiaľ ich neodtíšim", "mute_modal.show_options": "Zobraziť možnosti", + "mute_modal.they_can_mention_and_follow": "Môže ťa spomenúť a nasledovať, ale ty ho/ju neuvidíš.", + "mute_modal.they_wont_know": "Nebude vedieť, že bol/a stíšený/á.", "mute_modal.title": "Stíšiť užívateľa?", + "mute_modal.you_wont_see_mentions": "Neuvidíš príspevky, ktoré ho/ju spomínajú.", + "mute_modal.you_wont_see_posts": "Stále uvidí tvoje príspevky, ale ty neuvidíš jeho/jej.", "navigation_bar.about": "O tomto serveri", "navigation_bar.administration": "Spravovanie", "navigation_bar.advanced_interface": "Otvoriť v pokročilom webovom rozhraní", @@ -467,12 +483,15 @@ "notification.label.private_reply": "Súkromná odpoveď", "notification.label.reply": "Odpoveď", "notification.mention": "Zmienka", + "notification.mentioned_you": "{name} ťa spomenul/a", "notification.moderation-warning.learn_more": "Zisti viac", + "notification.moderation_warning": "Dostal/a si varovanie od moderátora", "notification.moderation_warning.action_delete_statuses": "Niektoré z tvojich príspevkov boli odstránené.", "notification.moderation_warning.action_disable": "Tvoj účet bol vypnutý.", "notification.moderation_warning.action_silence": "Tvoj účet bol obmedzený.", "notification.moderation_warning.action_suspend": "Tvoj účet bol pozastavený.", "notification.own_poll": "Vaša anketa sa skončila", + "notification.poll": "Anketa, v ktorej si hlasoval/a, skončila", "notification.reblog": "{name} zdieľa váš príspevok", "notification.relationships_severance_event": "Stratené prepojenia s {name}", "notification.relationships_severance_event.account_suspension": "Správca z {from} pozastavil/a {target}, čo znamená, že od nich viac nemôžeš dostávať aktualizácie, alebo s nimi interaktovať.", @@ -484,7 +503,7 @@ "notification_requests.edit_selection": "Uprav", "notification_requests.exit_selection": "Hotovo", "notification_requests.notifications_from": "Oboznámenia od {name}", - "notification_requests.title": "Filtrované oboznámenia", + "notification_requests.title": "Filtrované oznámenia", "notification_requests.view": "Zobraz upozornenia", "notifications.clear": "Vyčistiť upozornenia", "notifications.clear_confirmation": "Určite chcete nenávratne odstrániť všetky svoje upozornenia?", @@ -519,6 +538,8 @@ "notifications.permission_denied": "Upozornenia na ploche sú nedostupné pre už skôr zamietnutú požiadavku prehliadača", "notifications.permission_denied_alert": "Upozornenia na ploche nemôžu byť zapnuté, pretože požiadavka prehliadača bola už skôr zamietnutá", "notifications.permission_required": "Upozornenia na ploche sú nedostupné, pretože neboli udelené potrebné povolenia.", + "notifications.policy.accept": "Prijať", + "notifications.policy.accept_hint": "Ukáž v oznámeniach", "notifications.policy.drop": "Ignoruj", "notifications.policy.filter": "Triediť", "notifications.policy.filter_limited_accounts_title": "Moderované účty", @@ -526,6 +547,7 @@ "notifications.policy.filter_not_followers_title": "Ľudia, ktorí ťa nenasledujú", "notifications.policy.filter_not_following_title": "Ľudia, ktorých nenasleduješ", "notifications.policy.filter_private_mentions_title": "Nevyžiadané priame spomenutia", + "notifications.policy.title": "Spravuj oznámenia od…", "notifications_permission_banner.enable": "Povoliť upozornenia na ploche", "notifications_permission_banner.how_to_control": "Ak chcete dostávať upozornenia, keď Mastodon nie je otvorený, povoľte upozornenia na ploche. Po ich zapnutí môžete presne kontrolovať, ktoré typy interakcií generujú upozornenia na ploche, a to prostredníctvom tlačidla {icon} vyššie.", "notifications_permission_banner.title": "Nenechajte si nič ujsť", @@ -696,6 +718,7 @@ "status.bookmark": "Pridať záložku", "status.cancel_reblog_private": "Zrušiť zdieľanie", "status.cannot_reblog": "Tento príspevok nie je možné zdieľať", + "status.continued_thread": "Pokračujúce vlákno", "status.copy": "Kopírovať odkaz na príspevok", "status.delete": "Vymazať", "status.detailed_status": "Podrobný náhľad celej konverzácie", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index 410d7c2ef3dd2f..088b48a3f3e3e7 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -87,6 +87,19 @@ "alert.unexpected.title": "Hëm!", "alt_text_badge.title": "Tekst alternativ", "announcement.announcement": "Lajmërim", + "annual_report.summary.followers.followers": "ndjekës", + "annual_report.summary.followers.total": "{count} gjithsej", + "annual_report.summary.here_it_is": "Ja {year} juaj e shqyrtuar:", + "annual_report.summary.highlighted_post.by_favourites": "potimi më i parapëlqyer", + "annual_report.summary.highlighted_post.by_reblogs": "postimi me më shumë përforcime", + "annual_report.summary.highlighted_post.by_replies": "postimi me më tepër përgjigje", + "annual_report.summary.highlighted_post.possessive": "nga {name}", + "annual_report.summary.most_used_app.most_used_app": "aplikacioni më i përdorur", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "hashtag-u më i përdorur", + "annual_report.summary.new_posts.new_posts": "postime të reja", + "annual_report.summary.percentile.text": "Kjo ju vë në kryetë përdoruesve të Mastodon-it.", + "annual_report.summary.percentile.we_wont_tell_bernie": "Nuk do t’ia themi Bernit.", + "annual_report.summary.thanks": "Faleminderit që jeni pjesë e Mastodon-it!", "attachments_list.unprocessed": "(e papërpunuar)", "audio.hide": "Fshihe audion", "block_modal.remote_users_caveat": "Do t’i kërkojmë shërbyesit {domain} të respektojë vendimin tuaj. Por, pajtimi s’është i garantuar, ngaqë disa shërbyes mund t’i trajtojnë ndryshe bllokimet. Psotimet publike mundet të jenë ende të dukshme për përdorues pa bërë hyrje në llogari.", @@ -508,6 +521,8 @@ "notification.admin.report_statuses_other": "{name} raportoi {target}", "notification.admin.sign_up": "{name} u regjistrua", "notification.admin.sign_up.name_and_others": "U regjistrua {name} dhe {count, plural, one {# tjetër} other {# të tjerë}}", + "notification.annual_report.message": "#Wrapstodon juaj për {year} pret! Zbuloni pikat e theksuara dhe çastet e paharrueshëm të këtij viti për ju në Mastodon!", + "notification.annual_report.view": "Shihni #Wrapstodon", "notification.favourite": "{name} i vuri shenjë postimit tuaj si të parapëlqyer", "notification.favourite.name_and_others_with_link": "{name} dhe {count, plural, one {# tjetër} other {# të tjerë}} i vunë shenjë postimit tuaj si të parapëlqyer", "notification.follow": "{name} zuri t’ju ndjekë", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index 7824d076295713..e3571704bb0131 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -82,11 +82,29 @@ "admin.impact_report.instance_follows": "Följare som deras användare skulle förlora", "admin.impact_report.title": "Sammanfattning av påverkan", "alert.rate_limited.message": "Vänligen försök igen efter {retry_time, time, medium}.", - "alert.rate_limited.title": "Mängd begränsad", + "alert.rate_limited.title": "Hastighetsbegränsad", "alert.unexpected.message": "Ett oväntat fel uppstod.", "alert.unexpected.title": "Hoppsan!", "alt_text_badge.title": "Alt-Text", "announcement.announcement": "Meddelande", + "annual_report.summary.archetype.booster": "Häftighetsjägaren", + "annual_report.summary.archetype.lurker": "Smygaren", + "annual_report.summary.archetype.oracle": "Oraklet", + "annual_report.summary.archetype.pollster": "Frågaren", + "annual_report.summary.archetype.replier": "Den sociala fjärilen", + "annual_report.summary.followers.followers": "följare", + "annual_report.summary.followers.total": "{count} totalt", + "annual_report.summary.here_it_is": "Här är en tillbakablick på ditt {year}:", + "annual_report.summary.highlighted_post.by_favourites": "mest favoritmarkerat inlägg", + "annual_report.summary.highlighted_post.by_reblogs": "mest boostat inlägg", + "annual_report.summary.highlighted_post.by_replies": "inlägg med flest svar", + "annual_report.summary.highlighted_post.possessive": "{name}s", + "annual_report.summary.most_used_app.most_used_app": "mest använda app", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "mest använda hashtag", + "annual_report.summary.new_posts.new_posts": "nya inlägg", + "annual_report.summary.percentile.text": "Det placerar dig i toppav Mastodon-användare.", + "annual_report.summary.percentile.we_wont_tell_bernie": "Vi berättar inte för Bernie.", + "annual_report.summary.thanks": "Tack för att du är en del av Mastodon!", "attachments_list.unprocessed": "(obehandlad)", "audio.hide": "Dölj audio", "block_modal.remote_users_caveat": "Vi kommer att be servern {domain} att respektera ditt beslut. Dock garanteras inte efterlevnad eftersom vissa servrar kan hantera blockeringar på olika sätt. Offentliga inlägg kan fortfarande vara synliga för icke-inloggade användare.", @@ -271,7 +289,7 @@ "empty_column.follow_requests": "Du har inga följarförfrågningar än. När du får en kommer den visas här.", "empty_column.followed_tags": "Du följer inga hashtaggar ännu. När du gör det kommer de att dyka upp här.", "empty_column.hashtag": "Det finns inget i denna hashtag ännu.", - "empty_column.home": "Din hemma-tidslinje är tom! Följ fler användare för att fylla den. {suggestions}", + "empty_column.home": "Din hemma-tidslinje är tom! Följ fler användare för att fylla den.", "empty_column.list": "Det finns inget i denna lista än. När listmedlemmar publicerar nya inlägg kommer de synas här.", "empty_column.lists": "Du har inga listor än. När skapar en kommer den dyka upp här.", "empty_column.mutes": "Du har ännu inte tystat några användare.", @@ -383,7 +401,7 @@ "ignore_notifications_modal.not_following_title": "Vill du blockera aviseringar från personer som du inte följer dig?", "ignore_notifications_modal.private_mentions_title": "Vill du ignorera aviseringar från oönskade privata omnämningar?", "interaction_modal.description.favourite": "Med ett Mastodon-konto kan du favoritmarkera detta inlägg för att visa författaren att du gillar det och för att spara det till senare.", - "interaction_modal.description.follow": "Med ett Mastodon-konto kan du följa {name} för att se hens inlägg i ditt hemflöde.", + "interaction_modal.description.follow": "Med ett Mastodon-konto kan du följa {name} för att se deras inlägg i ditt hemflöde.", "interaction_modal.description.reblog": "Med ett Mastodon-konto kan du boosta detta inlägg för att dela den med dina egna följare.", "interaction_modal.description.reply": "Med ett Mastodon-konto kan du svara på detta inlägg.", "interaction_modal.description.vote": "Med ett konto på Mastodon kan du delta i denna omröstning.", @@ -405,11 +423,11 @@ "keyboard_shortcuts.back": "Gå bakåt", "keyboard_shortcuts.blocked": "Öppna listan över blockerade användare", "keyboard_shortcuts.boost": "Boosta inlägg", - "keyboard_shortcuts.column": "för att fokusera en status i en av kolumnerna", + "keyboard_shortcuts.column": "Fokusera kolumn", "keyboard_shortcuts.compose": "för att fokusera skrivfältet", "keyboard_shortcuts.description": "Beskrivning", "keyboard_shortcuts.direct": "för att öppna privata nämningskolumnen", - "keyboard_shortcuts.down": "för att flytta nedåt i listan", + "keyboard_shortcuts.down": "Flytta ner i listan", "keyboard_shortcuts.enter": "Öppna inlägg", "keyboard_shortcuts.favourite": "Favoritmarkera inlägg", "keyboard_shortcuts.favourites": "Öppna favoritlistan", @@ -429,13 +447,13 @@ "keyboard_shortcuts.reply": "Svara på inlägg", "keyboard_shortcuts.requests": "för att öppna Följförfrågningar", "keyboard_shortcuts.search": "för att fokusera sökfältet", - "keyboard_shortcuts.spoilers": "visa/dölja CW-fält", - "keyboard_shortcuts.start": "för att öppna \"Kom igång\"-kolumnen", - "keyboard_shortcuts.toggle_hidden": "för att visa/gömma text bakom CW", - "keyboard_shortcuts.toggle_sensitivity": "för att visa/gömma media", + "keyboard_shortcuts.spoilers": "Visa/dölja CW-fält", + "keyboard_shortcuts.start": "Öppna \"Kom igång\"-kolumnen", + "keyboard_shortcuts.toggle_hidden": "Visa/gömma text bakom CW", + "keyboard_shortcuts.toggle_sensitivity": "Visa/gömma media", "keyboard_shortcuts.toot": "Starta nytt inlägg", - "keyboard_shortcuts.unfocus": "för att avfokusera skrivfält/sökfält", - "keyboard_shortcuts.up": "för att flytta uppåt i listan", + "keyboard_shortcuts.unfocus": "Avfokusera skrivfält/sökfält", + "keyboard_shortcuts.up": "Flytta uppåt i listan", "lightbox.close": "Stäng", "lightbox.next": "Nästa", "lightbox.previous": "Tidigare", @@ -508,6 +526,8 @@ "notification.admin.report_statuses_other": "{name} rapporterade {target}", "notification.admin.sign_up": "{name} registrerade sig", "notification.admin.sign_up.name_and_others": "{name} och {count, plural, one {# en annan} other {# andra}} har registrerat sig", + "notification.annual_report.message": "Din {year} #Wrapstodon väntar! Upptäck ditt års höjdpunkter och minnesvärda stunder på Mastodon!", + "notification.annual_report.view": "Visa #Wrapstodon", "notification.favourite": "{name} favoritmarkerade ditt inlägg", "notification.favourite.name_and_others_with_link": "{name} och {count, plural, one {# annan} other {# andra}} har favoritmarkerat ditt inlägg", "notification.follow": "{name} följer dig", @@ -617,11 +637,11 @@ "onboarding.action.back": "Ta mig tillbaka", "onboarding.actions.back": "Ta mig tillbaka", "onboarding.actions.go_to_explore": "See what's trending", - "onboarding.actions.go_to_home": "Go to your home feed", + "onboarding.actions.go_to_home": "Ta mig till mitt hemflöde", "onboarding.compose.template": "Hallå #Mastodon!", "onboarding.follows.empty": "Tyvärr kan inga resultat visas just nu. Du kan prova att använda sökfunktionen eller utforska sidan för att hitta personer att följa, eller försök igen senare.", - "onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!", - "onboarding.follows.title": "Popular on Mastodon", + "onboarding.follows.lead": "Ditt hemflöde är det primära sättet att uppleva Mastodon. Ju fler människor du följer, desto mer aktiv och intressant blir det. För att komma igång, är här några förslag:", + "onboarding.follows.title": "Anpassa ditt hemflöde", "onboarding.profile.discoverable": "Gör min profil upptäckbar", "onboarding.profile.discoverable_hint": "När du väljer att vara upptäckbar på Mastodon kan dina inlägg visas i sök- och trendresultat, och din profil kan föreslås för personer med liknande intressen som du.", "onboarding.profile.display_name": "Visningsnamn", @@ -642,7 +662,7 @@ "onboarding.start.title": "Du klarade det!", "onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.", "onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}", - "onboarding.steps.publish_status.body": "Say hello to the world.", + "onboarding.steps.publish_status.body": "Säg hej till världen med text, foton, videor eller omröstningar {emoji}", "onboarding.steps.publish_status.title": "Gör ditt första inlägg", "onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.", "onboarding.steps.setup_profile.title": "Customize your profile", @@ -733,8 +753,8 @@ "report.thanks.take_action_actionable": "Medan vi granskar detta kan du vidta åtgärder mot {name}:", "report.thanks.title": "Vill du inte se det här?", "report.thanks.title_actionable": "Tack för att du rapporterar, vi kommer att titta på detta.", - "report.unfollow": "Sluta följ @{username}", - "report.unfollow_explanation": "Du följer detta konto. Avfölj hen för att inte se hens inlägg i ditt hemflöde.", + "report.unfollow": "Sluta följ @{name}", + "report.unfollow_explanation": "Du följer detta konto. Avfölj det för att inte se dess inlägg i ditt hemflöde.", "report_notification.attached_statuses": "bifogade {count, plural, one {{count} inlägg} other {{count} inlägg}}", "report_notification.categories.legal": "Rättsligt", "report_notification.categories.legal_sentence": "olagligt innehåll", @@ -829,7 +849,7 @@ "status.show_less_all": "Visa mindre för alla", "status.show_more_all": "Visa mer för alla", "status.show_original": "Visa original", - "status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}", + "status.title.with_attachments": "{user} lade upp {attachmentCount, plural, one {en bilaga} other {{attachmentCount} bilagor}}", "status.translate": "Översätt", "status.translated_from_with": "Översatt från {lang} med {provider}", "status.uncached_media_warning": "Förhandsvisning inte tillgänglig", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index b0ad6e1c66e1cf..5cf95554bc0e4c 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -44,7 +44,7 @@ "account.joined_short": "เข้าร่วมเมื่อ", "account.languages": "เปลี่ยนภาษาที่บอกรับ", "account.link_verified_on": "ตรวจสอบความเป็นเจ้าของของลิงก์นี้เมื่อ {date}", - "account.locked_info": "สถานะความเป็นส่วนตัวของบัญชีนี้ถูกตั้งค่าเป็นล็อค เจ้าของตรวจสอบด้วยตนเองว่าใครสามารถติดตามพวกเขาได้", + "account.locked_info": "มีการตั้งสถานะความเป็นส่วนตัวของบัญชีนี้เป็นล็อคอยู่ เจ้าของตรวจทานผู้ที่สามารถติดตามเขาด้วยตนเอง", "account.media": "สื่อ", "account.mention": "กล่าวถึง @{name}", "account.moved_to": "{name} ได้ระบุว่าบัญชีใหม่ของเขาในตอนนี้คือ:", @@ -97,7 +97,7 @@ "block_modal.they_will_know": "เขาสามารถเห็นว่ามีการปิดกั้นเขา", "block_modal.title": "ปิดกั้นผู้ใช้?", "block_modal.you_wont_see_mentions": "คุณจะไม่เห็นโพสต์ที่กล่าวถึงเขา", - "boost_modal.combo": "คุณสามารถกด {combo} เพื่อข้ามสิ่งนี้ในครั้งถัดไป", + "boost_modal.combo": "คุณสามารถกดปุ่ม {combo} เพื่อข้ามสิ่งนี้ในครั้งถัดไป", "boost_modal.reblog": "ดันโพสต์?", "boost_modal.undo_reblog": "เลิกดันโพสต์?", "bundle_column_error.copy_stacktrace": "คัดลอกรายงานข้อผิดพลาด", @@ -386,6 +386,7 @@ "interaction_modal.description.follow": "ด้วยบัญชีใน Mastodon คุณสามารถติดตาม {name} เพื่อรับโพสต์ของเขาในฟีดหน้าแรกของคุณ", "interaction_modal.description.reblog": "ด้วยบัญชีใน Mastodon คุณสามารถดันโพสต์นี้เพื่อแชร์โพสต์กับผู้ติดตามของคุณเอง", "interaction_modal.description.reply": "ด้วยบัญชีใน Mastodon คุณสามารถตอบกลับโพสต์นี้", + "interaction_modal.description.vote": "ด้วยบัญชีใน Mastodon คุณสามารถลงคะแนนในการสำรวจความคิดเห็นนี้", "interaction_modal.login.action": "นำฉันกลับบ้าน", "interaction_modal.login.prompt": "โดเมนของเซิร์ฟเวอร์บ้านของคุณ เช่น mastodon.social", "interaction_modal.no_account_yet": "ไม่ได้อยู่ใน Mastodon?", @@ -397,6 +398,7 @@ "interaction_modal.title.follow": "ติดตาม {name}", "interaction_modal.title.reblog": "ดันโพสต์ของ {name}", "interaction_modal.title.reply": "ตอบกลับโพสต์ของ {name}", + "interaction_modal.title.vote": "ลงคะแนนในการสำรวจความคิดเห็นของ {name}", "intervals.full.days": "{number, plural, other {# วัน}}", "intervals.full.hours": "{number, plural, other {# ชั่วโมง}}", "intervals.full.minutes": "{number, plural, other {# นาที}}", @@ -855,11 +857,11 @@ "upload_error.poll": "ไม่อนุญาตการอัปโหลดไฟล์โดยมีการสำรวจความคิดเห็น", "upload_form.audio_description": "อธิบายสำหรับผู้ที่สูญเสียการได้ยิน", "upload_form.description": "อธิบายสำหรับผู้คนที่พิการทางการมองเห็นหรือมีสายตาเลือนราง", - "upload_form.drag_and_drop.instructions": "หากต้องการเลือกไฟล์สื่อ ให้กดปุ่มที Space หรือ Enter บนแป้นพิมพ์ เมื่อเลือกไฟล์ได้แล้ว ก็สามารถใช้ปุ่มลูกศรเพื่อเลื่อนไฟล์ไปในทิศทางที่ต้องการได้ เมื่อต้องการวางไฟล์ในตำแหน่งใหม่ ให้กดปุ่ม Space หรือ Enter ดูอีกครั้ง หรือหากต้องการยกเลิกการเลือก ให้กดปุ่ม Esc ได้", - "upload_form.drag_and_drop.on_drag_cancel": "การลากไฟล์นั้นหยุดชะงัก ไฟล์ที่กำลังแนบมาถูกยกเลิก {item} ได้ถูกลบทิ้งแล้ว", - "upload_form.drag_and_drop.on_drag_end": "ไฟล์แนบ {item} ได้ถูกยกเลิกแล้ว", - "upload_form.drag_and_drop.on_drag_over": "ไฟล์แนบ {item} ได้ถูกย้ายไปแล้ว", - "upload_form.drag_and_drop.on_drag_start": "ได้รับไฟล์แนบเรียบร้อยแล้ว {item}.", + "upload_form.drag_and_drop.instructions": "เพื่อหยิบไฟล์แนบสื่อ กดปุ่มเว้นวรรคหรือขึ้นบรรทัดใหม่ ขณะลาก ใช้ปุ่มลูกศรเพื่อย้ายไฟล์แนบสื่อในทิศทางใดก็ตามที่กำหนด กดปุ่มเว้นวรรคหรือขึ้นบรรทัดใหม่อีกครั้งเพื่อปล่อยไฟล์แนบสื่อในตำแหน่งใหม่ หรือกดปุ่ม Escape เพื่อยกเลิก", + "upload_form.drag_and_drop.on_drag_cancel": "ยกเลิกการลากแล้ว ปล่อยไฟล์แนบสื่อ {item} แล้ว", + "upload_form.drag_and_drop.on_drag_end": "ปล่อยไฟล์แนบสื่อ {item} แล้ว", + "upload_form.drag_and_drop.on_drag_over": "ย้ายไฟล์แนบสื่อ {item} แล้ว", + "upload_form.drag_and_drop.on_drag_start": "หยิบไฟล์แนบสื่อ {item} แล้ว", "upload_form.edit": "แก้ไข", "upload_form.thumbnail": "เปลี่ยนภาพขนาดย่อ", "upload_form.video_description": "อธิบายสำหรับผู้คนที่พิการทางการได้ยิน ได้ยินไม่ชัด พิการทางการมองเห็น หรือมีสายตาเลือนราง", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index 5ac5a3368b4c5d..4699b9f4bda3f5 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -87,6 +87,24 @@ "alert.unexpected.title": "Hay aksi!", "alt_text_badge.title": "Alternatif metin", "announcement.announcement": "Duyuru", + "annual_report.summary.archetype.booster": "Trend takipçisi", + "annual_report.summary.archetype.lurker": "Gizli meraklı", + "annual_report.summary.archetype.oracle": "Kahin", + "annual_report.summary.archetype.pollster": "Anketör", + "annual_report.summary.archetype.replier": "Sosyal kelebek", + "annual_report.summary.followers.followers": "takipçiler", + "annual_report.summary.followers.total": "{count} toplam", + "annual_report.summary.here_it_is": "İşte {year} yılı değerlendirmeniz:", + "annual_report.summary.highlighted_post.by_favourites": "en çok beğenilen gönderi", + "annual_report.summary.highlighted_post.by_reblogs": "en çok paylaşılan gönderi", + "annual_report.summary.highlighted_post.by_replies": "en çok yanıt alan gönderi", + "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.summary.most_used_app.most_used_app": "en çok kullanılan uygulama", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "en çok kullanılan etiket", + "annual_report.summary.new_posts.new_posts": "yeni gönderiler", + "annual_report.summary.percentile.text": "Mastodon kullanıcılarınınüst dilimindesiniz.", + "annual_report.summary.percentile.we_wont_tell_bernie": "Bernie'ye söylemeyiz.", + "annual_report.summary.thanks": "Mastodon'un bir parçası olduğunuz için teşekkürler!", "attachments_list.unprocessed": "(işlenmemiş)", "audio.hide": "Sesi gizle", "block_modal.remote_users_caveat": "{domain} sunucusundan kararınıza saygı duymasını isteyeceğiz. Ancak, Uymaları garanti değildir çünkü bazı sunucular engellemeyi farklı şekilde yapıyorlar. Herkese açık gönderiler giriş yapmamış kullanıcılara görüntülenmeye devam edebilir.", @@ -508,6 +526,8 @@ "notification.admin.report_statuses_other": "{name}, {target} kişisini bildirdi", "notification.admin.sign_up": "{name} kaydoldu", "notification.admin.sign_up.name_and_others": "{name} ve {count, plural, one {# diğer kişi} other {# diğer kişi}} kaydoldu", + "notification.annual_report.message": "{year} yılı #Wrapstodon'unuz bekliyor! Yılınızın Mastodon'daki öne çıkanlarını ve anılarınızı gösterin!", + "notification.annual_report.view": "#Wrapstodon'u Görüntüle", "notification.favourite": "{name} gönderinizi beğendi", "notification.favourite.name_and_others_with_link": "{name} ve {count, plural, one {# diğer kişi} other {# diğer kişi}} gönderinizi beğendi", "notification.follow": "{name} seni takip etti", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index fc396aa9339cca..b34785ebc3feab 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -87,6 +87,7 @@ "alert.unexpected.title": "Ой!", "alt_text_badge.title": "Альтернативний текст", "announcement.announcement": "Оголошення", + "annual_report.summary.new_posts.new_posts": "нові дописи", "attachments_list.unprocessed": "(не оброблено)", "audio.hide": "Сховати аудіо", "block_modal.remote_users_caveat": "Ми попросимо сервер {domain} поважати ваше рішення. Однак дотримання вимог не гарантується, оскільки деякі сервери можуть обробляти блоки по-різному. Загальнодоступні дописи все ще можуть бути видимими для користувачів, які не увійшли в систему.", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 7ae955c1b0ca6f..2b5401a878f7d6 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -87,6 +87,24 @@ "alert.unexpected.title": "Ốiii!", "alt_text_badge.title": "Văn bản thay thế", "announcement.announcement": "Có gì mới?", + "annual_report.summary.archetype.booster": "Hiệp sĩ ngầu", + "annual_report.summary.archetype.lurker": "Kẻ rình mò", + "annual_report.summary.archetype.oracle": "Nhà tiên tri", + "annual_report.summary.archetype.pollster": "Chuyên gia khảo sát", + "annual_report.summary.archetype.replier": "Bướm xã hội", + "annual_report.summary.followers.followers": "người theo dõi", + "annual_report.summary.followers.total": "tổng {count}", + "annual_report.summary.here_it_is": "Nhìn lại năm {year} của bạn:", + "annual_report.summary.highlighted_post.by_favourites": "tút được thích nhiều nhất", + "annual_report.summary.highlighted_post.by_reblogs": "tút được đăng lại nhiều nhất", + "annual_report.summary.highlighted_post.by_replies": "tút được trả lời nhiều nhất", + "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.summary.most_used_app.most_used_app": "app dùng nhiều nhất", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "hashtag dùng nhiều nhất", + "annual_report.summary.new_posts.new_posts": "tút mới", + "annual_report.summary.percentile.text": "Bạn nằm trong topthành viên Mastodon.", + "annual_report.summary.percentile.we_wont_tell_bernie": "Chúng tôi sẽ không kể cho Bernie.", + "annual_report.summary.thanks": "Cảm ơn đã trở thành một phần của Mastodon!", "attachments_list.unprocessed": "(chưa xử lí)", "audio.hide": "Ẩn âm thanh", "block_modal.remote_users_caveat": "Chúng tôi sẽ yêu cầu {domain} tôn trọng quyết định của bạn. Tuy nhiên, việc tuân thủ không được đảm bảo vì một số máy chủ có thể xử lý việc chặn theo cách khác nhau. Các tút công khai vẫn có thể hiển thị đối với người dùng chưa đăng nhập.", @@ -508,6 +526,8 @@ "notification.admin.report_statuses_other": "{name} báo cáo {target}", "notification.admin.sign_up": "{name} tham gia máy chủ của bạn", "notification.admin.sign_up.name_and_others": "{name} và {count, plural, other {# người}} đã đăng ký", + "notification.annual_report.message": "#Wrapstodon {year} của bạn có rồi đây! Hãy chia sẻ những điểm nhấn và khoảnh khắc đáng nhớ trên Mastodon của bạn trong năm qua!", + "notification.annual_report.view": "Xem #Wrapstodon", "notification.favourite": "{name} thích tút của bạn", "notification.favourite.name_and_others_with_link": "{name} và {count, plural, other {# người khác}} đã thích tút của bạn", "notification.follow": "{name} theo dõi bạn", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 603ec5d791a6a7..57b2011bb18d8d 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -87,6 +87,24 @@ "alert.unexpected.title": "哎呀!", "alt_text_badge.title": "替代文本", "announcement.announcement": "公告", + "annual_report.summary.archetype.booster": "潮流捕手", + "annual_report.summary.archetype.lurker": "吃瓜群众", + "annual_report.summary.archetype.oracle": "预言家", + "annual_report.summary.archetype.pollster": "投票狂魔", + "annual_report.summary.archetype.replier": "评论区原住民", + "annual_report.summary.followers.followers": "关注者", + "annual_report.summary.followers.total": "{count} 人", + "annual_report.summary.here_it_is": "你的 {year} 年度回顾在此:", + "annual_report.summary.highlighted_post.by_favourites": "最受欢迎嘟嘟", + "annual_report.summary.highlighted_post.by_reblogs": "传播最广嘟嘟", + "annual_report.summary.highlighted_post.by_replies": "最热闹嘟嘟", + "annual_report.summary.highlighted_post.possessive": "{name} 的", + "annual_report.summary.most_used_app.most_used_app": "最常用的应用", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "最常用的话题", + "annual_report.summary.new_posts.new_posts": "新发嘟", + "annual_report.summary.percentile.text": "这使你跻身 Mastodon 用户的前", + "annual_report.summary.percentile.we_wont_tell_bernie": "我们打死也不会告诉Bernie。", + "annual_report.summary.thanks": "感谢你这一年与 Mastodon 一路同行!", "attachments_list.unprocessed": "(未处理)", "audio.hide": "隐藏音频", "block_modal.remote_users_caveat": "我们将要求服务器 {domain} 尊重您的决定。然而,我们无法保证对方一定遵从,因为某些服务器可能会以不同的方案处理屏蔽操作。公开嘟文仍然可能对未登录的用户可见。", @@ -508,6 +526,8 @@ "notification.admin.report_statuses_other": "{name} 举报了 {target}", "notification.admin.sign_up": "{name} 注册了", "notification.admin.sign_up.name_and_others": "{name} 和 {count, plural, other {另外 # 人}}注册了", + "notification.annual_report.message": "你的 {year} #Wrapstodon 年度回顾来啦!快来看看这一年你在 Mastodon 上的精彩瞬间!", + "notification.annual_report.view": "查看 #Wrapstodon", "notification.favourite": "{name} 喜欢了你的嘟文", "notification.favourite.name_and_others_with_link": "{name} 和 {count, plural, other {另外 # 人}} 喜欢了你的嘟文", "notification.follow": "{name} 开始关注你", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index a3de26ffc40dca..e1f9eeb210b250 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -87,6 +87,23 @@ "alert.unexpected.title": "哎呀!", "alt_text_badge.title": "ALT 說明文字", "announcement.announcement": "公告", + "annual_report.summary.archetype.booster": "酷炫的獵人", + "annual_report.summary.archetype.lurker": "潛伏者", + "annual_report.summary.archetype.oracle": "先知", + "annual_report.summary.archetype.pollster": "民調人員", + "annual_report.summary.archetype.replier": "社交菁英", + "annual_report.summary.followers.followers": "跟隨者", + "annual_report.summary.followers.total": "總共 {count}", + "annual_report.summary.here_it_is": "以下是您的{year}年度回顧:", + "annual_report.summary.highlighted_post.by_favourites": "最愛的嘟文", + "annual_report.summary.highlighted_post.by_reblogs": "最多轉嘟的嘟文", + "annual_report.summary.highlighted_post.by_replies": "最多回覆的嘟文", + "annual_report.summary.highlighted_post.possessive": "{name} 的", + "annual_report.summary.most_used_app.most_used_app": "最常使用的應用程式", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "最常使用的主題標籤", + "annual_report.summary.new_posts.new_posts": "新嘟文", + "annual_report.summary.percentile.text": "這讓您成為前Mastodon 的使用者。", + "annual_report.summary.thanks": "感謝您成為 Mastodon 的一員!", "attachments_list.unprocessed": "(未經處理)", "audio.hide": "隱藏音訊", "block_modal.remote_users_caveat": "我們會要求 {domain} 伺服器尊重您的決定。然而,我們無法保證所有伺服器皆會遵守,某些伺服器可能以不同方式處理封鎖。未登入之使用者仍可能看見您的公開嘟文。", diff --git a/app/javascript/mastodon/models/annual_report.ts b/app/javascript/mastodon/models/annual_report.ts new file mode 100644 index 00000000000000..c0a101e6c872da --- /dev/null +++ b/app/javascript/mastodon/models/annual_report.ts @@ -0,0 +1,44 @@ +export interface Percentiles { + followers: number; + statuses: number; +} + +export interface NameAndCount { + name: string; + count: number; +} + +export interface TimeSeriesMonth { + month: number; + statuses: number; + following: number; + followers: number; +} + +export interface TopStatuses { + by_reblogs: number; + by_favourites: number; + by_replies: number; +} + +export type Archetype = + | 'lurker' + | 'booster' + | 'pollster' + | 'replier' + | 'oracle'; + +interface AnnualReportV1 { + most_used_apps: NameAndCount[]; + percentiles: Percentiles; + top_hashtags: NameAndCount[]; + top_statuses: TopStatuses; + time_series: TimeSeriesMonth[]; + archetype: Archetype; +} + +export interface AnnualReport { + year: number; + schema_version: number; + data: AnnualReportV1; +} diff --git a/app/javascript/mastodon/models/notification_group.ts b/app/javascript/mastodon/models/notification_group.ts index 09d407d44954a3..01a341e8dd5423 100644 --- a/app/javascript/mastodon/models/notification_group.ts +++ b/app/javascript/mastodon/models/notification_group.ts @@ -1,6 +1,7 @@ import type { ApiAccountRelationshipSeveranceEventJSON, ApiAccountWarningJSON, + ApiAnnualReportEventJSON, BaseNotificationGroupJSON, ApiNotificationGroupJSON, ApiNotificationJSON, @@ -65,6 +66,12 @@ export interface NotificationGroupSeveredRelationships event: AccountRelationshipSeveranceEvent; } +type AnnualReportEvent = ApiAnnualReportEventJSON; +export interface NotificationGroupAnnualReport + extends BaseNotification<'annual_report'> { + annualReport: AnnualReportEvent; +} + interface Report extends Omit { targetAccountId: string; } @@ -86,7 +93,8 @@ export type NotificationGroup = | NotificationGroupModerationWarning | NotificationGroupSeveredRelationships | NotificationGroupAdminSignUp - | NotificationGroupAdminReport; + | NotificationGroupAdminReport + | NotificationGroupAnnualReport; function createReportFromJSON(reportJSON: ApiReportJSON): Report { const { target_account, ...report } = reportJSON; @@ -112,6 +120,12 @@ function createAccountRelationshipSeveranceEventFromJSON( return eventJson; } +function createAnnualReportEventFromJSON( + eventJson: ApiAnnualReportEventJSON, +): AnnualReportEvent { + return eventJson; +} + export function createNotificationGroupFromJSON( groupJson: ApiNotificationGroupJSON, ): NotificationGroup { @@ -145,7 +159,6 @@ export function createNotificationGroupFromJSON( event: createAccountRelationshipSeveranceEventFromJSON(group.event), sampleAccountIds, }; - case 'moderation_warning': { const { moderation_warning, ...groupWithoutModerationWarning } = group; return { @@ -154,6 +167,14 @@ export function createNotificationGroupFromJSON( sampleAccountIds, }; } + case 'annual_report': { + const { annual_report, ...groupWithoutAnnualReport } = group; + return { + ...groupWithoutAnnualReport, + annualReport: createAnnualReportEventFromJSON(annual_report), + sampleAccountIds, + }; + } default: return { sampleAccountIds, diff --git a/app/javascript/material-icons/400-24px/celebration-fill.svg b/app/javascript/material-icons/400-24px/celebration-fill.svg new file mode 100644 index 00000000000000..d715cf2fb3cbe6 --- /dev/null +++ b/app/javascript/material-icons/400-24px/celebration-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/celebration.svg b/app/javascript/material-icons/400-24px/celebration.svg new file mode 100644 index 00000000000000..1d1b19ee72cfaf --- /dev/null +++ b/app/javascript/material-icons/400-24px/celebration.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/explore-fill.svg b/app/javascript/material-icons/400-24px/explore-fill.svg index febe0a63b44b3f..919ecb580a3d08 100644 --- a/app/javascript/material-icons/400-24px/explore-fill.svg +++ b/app/javascript/material-icons/400-24px/explore-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/explore.svg b/app/javascript/material-icons/400-24px/explore.svg index 547a9994217ab5..bb8ba1f4c63fe3 100644 --- a/app/javascript/material-icons/400-24px/explore.svg +++ b/app/javascript/material-icons/400-24px/explore.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index 465b748078f2e6..109b69bca5f5ae 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -15,6 +15,7 @@ @import 'mastodon/polls'; @import 'mastodon/modal'; @import 'mastodon/emoji_picker'; +@import 'mastodon/annual_reports'; @import 'mastodon/about'; @import 'mastodon/tables'; @import 'mastodon/admin'; diff --git a/app/javascript/styles/mastodon/annual_reports.scss b/app/javascript/styles/mastodon/annual_reports.scss new file mode 100644 index 00000000000000..39784e3b5ea12b --- /dev/null +++ b/app/javascript/styles/mastodon/annual_reports.scss @@ -0,0 +1,335 @@ +:root { + --indigo-1: #17063b; + --indigo-2: #2f0c7a; + --indigo-3: #562cfc; + --indigo-5: #858afa; + --indigo-6: #cccfff; + --lime: #baff3b; + --goldenrod-2: #ffc954; +} + +.annual-report { + flex: 0 0 auto; + background: var(--indigo-1); + padding: 24px; + + &__header { + margin-bottom: 16px; + + h1 { + font-size: 25px; + font-weight: 600; + line-height: 30px; + color: var(--lime); + margin-bottom: 8px; + } + + p { + font-size: 16px; + font-weight: 600; + line-height: 20px; + color: var(--indigo-6); + } + } + + &__bento { + display: grid; + gap: 8px; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr); + grid-template-rows: minmax(0, auto) minmax(0, 1fr) minmax(0, auto) minmax( + 0, + auto + ); + + &__box { + padding: 16px; + border-radius: 8px; + background: var(--indigo-2); + color: var(--indigo-5); + } + } + + &__summary { + &__most-boosted-post { + grid-column: span 2; + grid-row: span 2; + padding: 0; + + .status__content, + .content-warning { + color: var(--indigo-6); + } + + .detailed-status { + border: 0; + } + + .content-warning { + border: 0; + background: var(--indigo-1); + + .link-button { + color: var(--indigo-5); + } + } + + .detailed-status__meta__line { + border-bottom-color: var(--indigo-3); + } + + .detailed-status__meta { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .detailed-status__meta, + .poll__footer, + .poll__link, + .detailed-status .logo, + .detailed-status__display-name { + color: var(--indigo-5); + } + + .detailed-status__meta .animated-number, + .detailed-status__display-name strong { + color: var(--indigo-6); + } + + .poll__chart { + background-color: var(--indigo-3); + + &.leading { + background-color: var(--goldenrod-2); + } + } + } + + &__followers { + grid-column: span 1; + text-align: center; + position: relative; + overflow: hidden; + padding-block-start: 24px; + padding-block-end: 24px; + + --sparkline-gradient-top: rgba(86, 44, 252, 50%); + --sparkline-gradient-bottom: rgba(86, 44, 252, 0%); + + &__foreground { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + position: relative; + z-index: 1; + } + + &__number { + font-size: 31px; + font-weight: 600; + line-height: 37px; + color: var(--lime); + } + + &__label { + font-size: 14px; + font-weight: 600; + line-height: 17px; + color: var(--indigo-6); + } + + &__footnote { + display: block; + font-weight: 400; + opacity: 0.5; + } + + svg { + position: absolute; + bottom: 0; + inset-inline-end: 0; + pointer-events: none; + z-index: 0; + height: 70%; + width: auto; + + path:first-child { + fill: url('#gradient') !important; + fill-opacity: 1 !important; + } + + path:last-child { + stroke: var(--indigo-3) !important; + fill: none !important; + } + } + } + + &__archetype { + grid-column: span 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + gap: 8px; + padding: 0; + + img { + display: block; + width: 100%; + height: auto; + border-radius: 8px; + } + + &__label { + padding: 16px; + padding-bottom: 8px; + font-size: 14px; + line-height: 17px; + font-weight: 600; + color: var(--lime); + } + } + + &__most-used-app { + grid-column: span 1; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + box-sizing: border-box; + + &__label { + font-size: 14px; + line-height: 17px; + font-weight: 600; + color: var(--indigo-6); + } + + &__icon { + font-size: 14px; + line-height: 17px; + font-weight: 600; + color: var(--goldenrod-2); + } + } + + &__percentile { + grid-row: span 2; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + text-align: center; + text-wrap: balance; + padding: 16px 8px; + + &__label { + font-size: 14px; + line-height: 17px; + } + + &__number { + font-size: 61px; + font-weight: 600; + line-height: 73px; + color: var(--goldenrod-2); + } + + &__footnote { + font-size: 11px; + line-height: 14px; + opacity: 0.5; + } + } + + &__new-posts { + grid-column: span 2; + text-align: center; + position: relative; + overflow: hidden; + + &__label { + font-size: 20px; + font-weight: 600; + line-height: 24px; + color: var(--indigo-6); + z-index: 1; + position: relative; + } + + &__number { + font-size: 76px; + font-weight: 600; + line-height: 91px; + color: var(--goldenrod-2); + z-index: 1; + position: relative; + } + + svg { + position: absolute; + inset-inline-start: -7px; + top: -4px; + z-index: 0; + } + } + + &__most-used-hashtag { + grid-column: span 2; + text-align: center; + overflow: hidden; + + &__hashtag { + font-size: 42px; + font-weight: 600; + line-height: 58px; + color: var(--indigo-6); + margin-inline-start: -100%; + margin-inline-end: -100%; + } + + &__label { + font-size: 14px; + font-weight: 600; + line-height: 17px; + } + } + } +} + +.annual-report-modal { + max-width: 480px; + background: var(--indigo-1); + border-radius: 16px; + display: flex; + flex-direction: column; + overflow-y: auto; + + .loading-indicator .circular-progress { + color: var(--lime); + } + + @media screen and (max-width: $no-columns-breakpoint) { + border-bottom: 0; + border-radius: 16px 16px 0 0; + } +} + +.notification-group--annual-report { + .notification-group__icon { + color: var(--lime); + } + + .notification-group__main .link-button { + font-weight: 500; + color: var(--lime); + } +} diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index dece37289add80..8b1c9dc382bdf4 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1686,7 +1686,8 @@ body > [data-popper-placement] { .status__wrapper-direct, .notification-ungrouped--direct, -.notification-group--direct { +.notification-group--direct, +.notification-group--annual-report { background: rgba($ui-highlight-color, 0.05); &:focus { @@ -5784,7 +5785,8 @@ a.status-card { inset-inline-start: 0; inset-inline-end: 0; bottom: 0; - background: rgba($base-overlay-background, 0.7); + opacity: 0.9; + background: $base-overlay-background; transition: background 0.5s; } diff --git a/app/lib/admin/system_check/database_schema_check.rb b/app/lib/admin/system_check/database_schema_check.rb index c2f01fd55b528c..a3ef0613ea0c1d 100644 --- a/app/lib/admin/system_check/database_schema_check.rb +++ b/app/lib/admin/system_check/database_schema_check.rb @@ -6,7 +6,7 @@ def skip? end def pass? - !ActiveRecord::Base.connection.migration_context.needs_migration? + !ActiveRecord::Base.connection_pool.migration_context.needs_migration? end def message diff --git a/app/lib/annual_report.rb b/app/lib/annual_report.rb index cf4297f2a450ea..275cc4b87d3cad 100644 --- a/app/lib/annual_report.rb +++ b/app/lib/annual_report.rb @@ -17,11 +17,21 @@ class AnnualReport SCHEMA = 1 + def self.table_name_prefix + 'annual_report_' + end + def initialize(account, year) @account = account @year = year end + def self.prepare(year) + SOURCES.each do |klass| + klass.prepare(year) + end + end + def generate return if GeneratedAnnualReport.exists?(account: @account, year: @year) diff --git a/app/lib/annual_report/percentiles.rb b/app/lib/annual_report/percentiles.rb index 0251cb66ad27e9..2b0305c41554fd 100644 --- a/app/lib/annual_report/percentiles.rb +++ b/app/lib/annual_report/percentiles.rb @@ -1,62 +1,37 @@ # frozen_string_literal: true class AnnualReport::Percentiles < AnnualReport::Source + def self.prepare(year) + AnnualReport::StatusesPerAccountCount.connection.exec_query(<<~SQL.squish, nil, [year, Mastodon::Snowflake.id_at(DateTime.new(year).beginning_of_year), Mastodon::Snowflake.id_at(DateTime.new(year).end_of_year)]) + INSERT INTO annual_report_statuses_per_account_counts (year, account_id, statuses_count) + SELECT $1, account_id, count(*) + FROM statuses + WHERE id BETWEEN $2 AND $3 + AND (local OR uri IS NULL) + GROUP BY account_id + ON CONFLICT (year, account_id) DO NOTHING + SQL + end + def generate { percentiles: { - followers: (total_with_fewer_followers / (total_with_any_followers + 1.0)) * 100, - statuses: (total_with_fewer_statuses / (total_with_any_statuses + 1.0)) * 100, + statuses: 100.0 - ((total_with_fewer_statuses / (total_with_any_statuses + 1.0)) * 100), }, } end private - def followers_gained - @followers_gained ||= @account.passive_relationships.where("date_part('year', follows.created_at) = ?", @year).count - end - def statuses_created @statuses_created ||= report_statuses.count end - def total_with_fewer_followers - @total_with_fewer_followers ||= Follow.find_by_sql([<<~SQL.squish, { year: @year, comparison: followers_gained }]).first.total - WITH tmp0 AS ( - SELECT follows.target_account_id - FROM follows - INNER JOIN accounts ON accounts.id = follows.target_account_id - WHERE date_part('year', follows.created_at) = :year - AND accounts.domain IS NULL - GROUP BY follows.target_account_id - HAVING COUNT(*) < :comparison - ) - SELECT count(*) AS total - FROM tmp0 - SQL - end - def total_with_fewer_statuses - @total_with_fewer_statuses ||= Status.find_by_sql([<<~SQL.squish, { comparison: statuses_created, min_id: year_as_snowflake_range.first, max_id: year_as_snowflake_range.last }]).first.total - WITH tmp0 AS ( - SELECT statuses.account_id - FROM statuses - INNER JOIN accounts ON accounts.id = statuses.account_id - WHERE statuses.id BETWEEN :min_id AND :max_id - AND accounts.domain IS NULL - GROUP BY statuses.account_id - HAVING count(*) < :comparison - ) - SELECT count(*) AS total - FROM tmp0 - SQL - end - - def total_with_any_followers - @total_with_any_followers ||= Follow.where("date_part('year', follows.created_at) = ?", @year).joins(:target_account).merge(Account.local).count('distinct follows.target_account_id') + @total_with_fewer_statuses ||= AnnualReport::StatusesPerAccountCount.where(year: year).where(statuses_count: ...statuses_created).count end def total_with_any_statuses - @total_with_any_statuses ||= Status.where(id: year_as_snowflake_range).joins(:account).merge(Account.local).count('distinct statuses.account_id') + @total_with_any_statuses ||= AnnualReport::StatusesPerAccountCount.where(year: year).count end end diff --git a/app/lib/annual_report/source.rb b/app/lib/annual_report/source.rb index cb9f7b16e32a10..86528731f51080 100644 --- a/app/lib/annual_report/source.rb +++ b/app/lib/annual_report/source.rb @@ -8,6 +8,14 @@ def initialize(account, year) @year = year end + def self.prepare(_year) + # Use this method if any pre-calculations must be made before individual annual reports are generated + end + + def generate + raise NotImplementedError + end + protected def report_statuses diff --git a/app/lib/suspicious_sign_in_detector.rb b/app/lib/suspicious_sign_in_detector.rb index 74f49aa5587e7d..60e5fdad4f8be7 100644 --- a/app/lib/suspicious_sign_in_detector.rb +++ b/app/lib/suspicious_sign_in_detector.rb @@ -19,7 +19,7 @@ def sufficient_security_measures? end def previously_seen_ip?(request) - @user.ips.exists?(['ip <<= ?', masked_ip(request)]) + @user.ips.contained_by(masked_ip(request)).exists? end def freshly_signed_up? diff --git a/app/models/account_statuses_cleanup_policy.rb b/app/models/account_statuses_cleanup_policy.rb index 6e998e2dcaf6b0..ef41bb3ee3aa08 100644 --- a/app/models/account_statuses_cleanup_policy.rb +++ b/app/models/account_statuses_cleanup_policy.rb @@ -130,7 +130,7 @@ def validate_local_account end def without_direct_scope - Status.where.not(visibility: :direct) + Status.not_direct_visibility end def old_enough_scope(max_id = nil) diff --git a/app/models/admin/status_filter.rb b/app/models/admin/status_filter.rb index 8d20e4f6abce94..fd4d0ef59ddff0 100644 --- a/app/models/admin/status_filter.rb +++ b/app/models/admin/status_filter.rb @@ -32,7 +32,7 @@ def results def scope_for(key, _value) case key.to_s when 'media' - Status.joins(:media_attachments).merge(@account.media_attachments).group(:id).reorder('statuses.id desc') + Status.joins(:media_attachments).merge(@account.media_attachments).group(:id).recent else raise Mastodon::InvalidParameterError, "Unknown filter: #{key}" end diff --git a/app/models/annual_report/statuses_per_account_count.rb b/app/models/annual_report/statuses_per_account_count.rb new file mode 100644 index 00000000000000..05a2f53c9d5947 --- /dev/null +++ b/app/models/annual_report/statuses_per_account_count.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: annual_report_statuses_per_account_counts +# +# id :bigint(8) not null, primary key +# year :integer not null +# account_id :bigint(8) not null +# statuses_count :bigint(8) not null +# + +class AnnualReport::StatusesPerAccountCount < ApplicationRecord + # This table facilitates percentile calculations +end diff --git a/app/models/concerns/inet_container.rb b/app/models/concerns/inet_container.rb new file mode 100644 index 00000000000000..da03bcc5d73379 --- /dev/null +++ b/app/models/concerns/inet_container.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module InetContainer + extend ActiveSupport::Concern + + included do + scope :containing, ->(value) { where('ip >>= ?', value) } + scope :contained_by, ->(value) { where('ip <<= ?', value) } + end +end diff --git a/app/models/concerns/status/safe_reblog_insert.rb b/app/models/concerns/status/safe_reblog_insert.rb index 60ddb78e52c40e..48d585ea18c241 100644 --- a/app/models/concerns/status/safe_reblog_insert.rb +++ b/app/models/concerns/status/safe_reblog_insert.rb @@ -15,7 +15,9 @@ module Status::SafeReblogInsert # # The code is kept similar to ActiveRecord::Persistence code and calls it # directly when we are not handling a reblog. - def _insert_record(values, returning) + # + # https://github.com/rails/rails/blob/v7.2.1.1/activerecord/lib/active_record/persistence.rb#L238-L263 + def _insert_record(connection, values, returning) return super unless values.is_a?(Hash) && values['reblog_of_id']&.value.present? primary_key = self.primary_key @@ -30,14 +32,19 @@ def _insert_record(values, returning) # The following line departs from stock ActiveRecord # Original code was: - # im.insert(values.transform_keys { |name| arel_table[name] }) + # im = Arel::InsertManager.new(arel_table) # Instead, we use a custom builder when a reblog is happening: im = _compile_reblog_insert(values) - connection.insert(im, "#{self} Create", primary_key || false, primary_key_value, returning: returning).tap do |result| - # Since we are using SELECT instead of VALUES, a non-error `nil` return is possible. - # For our purposes, it's equivalent to a foreign key constraint violation - raise ActiveRecord::InvalidForeignKey, "(reblog_of_id)=(#{values['reblog_of_id'].value}) is not present in table \"statuses\"" if result.nil? + with_connection do |_c| + connection.insert( + im, "#{self} Create", primary_key || false, primary_key_value, + returning: returning + ).tap do |result| + # Since we are using SELECT instead of VALUES, a non-error `nil` return is possible. + # For our purposes, it's equivalent to a foreign key constraint violation + raise ActiveRecord::InvalidForeignKey, "(reblog_of_id)=(#{values['reblog_of_id'].value}) is not present in table \"statuses\"" if result.nil? + end end end diff --git a/app/models/email_domain_block.rb b/app/models/email_domain_block.rb index f3a86eae8f3533..583d2e6c1b18fa 100644 --- a/app/models/email_domain_block.rb +++ b/app/models/email_domain_block.rb @@ -28,6 +28,8 @@ class EmailDomainBlock < ApplicationRecord validates :domain, presence: true, uniqueness: true, domain: true + scope :parents, -> { where(parent_id: nil) } + # Used for adding multiple blocks at once attr_accessor :other_domains diff --git a/app/models/featured_tag.rb b/app/models/featured_tag.rb index a4e7b7cf6f0262..529056f9c60e2c 100644 --- a/app/models/featured_tag.rb +++ b/app/models/featured_tag.rb @@ -44,8 +44,16 @@ def increment(timestamp) update(statuses_count: statuses_count + 1, last_status_at: timestamp) end - def decrement(deleted_status_id) - update(statuses_count: [0, statuses_count - 1].max, last_status_at: visible_tagged_account_statuses.where.not(id: deleted_status_id).pick(:created_at)) + def decrement(deleted_status) + if statuses_count <= 1 + update(statuses_count: 0, last_status_at: nil) + elsif last_status_at > deleted_status.created_at + update(statuses_count: statuses_count - 1) + else + # Fetching the latest status creation time can be expensive, so only perform it + # if we know we are deleting the latest status using this tag + update(statuses_count: statuses_count - 1, last_status_at: visible_tagged_account_statuses.where(id: ...deleted_status.id).pick(:created_at)) + end end private diff --git a/app/models/ip_block.rb b/app/models/ip_block.rb index 5ed4d2a84626d4..416ae38382722c 100644 --- a/app/models/ip_block.rb +++ b/app/models/ip_block.rb @@ -17,6 +17,7 @@ class IpBlock < ApplicationRecord CACHE_KEY = 'blocked_ips' include Expireable + include InetContainer include Paginable enum :severity, { diff --git a/app/models/notification.rb b/app/models/notification.rb index 695f39a316f6ca..7b90fd92f96ce3 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -67,6 +67,9 @@ class Notification < ApplicationRecord moderation_warning: { filterable: false, }.freeze, + annual_report: { + filterable: false, + }.freeze, 'admin.sign_up': { filterable: false, }.freeze, @@ -101,6 +104,7 @@ class Notification < ApplicationRecord belongs_to :report, inverse_of: false belongs_to :account_relationship_severance_event, inverse_of: false belongs_to :account_warning, inverse_of: false + belongs_to :generated_annual_report, inverse_of: false end validates :type, inclusion: { in: TYPES } @@ -309,7 +313,7 @@ def set_from_account self.from_account_id = activity&.status&.account_id when 'Account' self.from_account_id = activity&.id - when 'AccountRelationshipSeveranceEvent', 'AccountWarning' + when 'AccountRelationshipSeveranceEvent', 'AccountWarning', 'GeneratedAnnualReport' # These do not really have an originating account, but this is mandatory # in the data model, and the recipient's account will by definition # always exist diff --git a/app/models/notification_group.rb b/app/models/notification_group.rb index b6aa4d309caa5a..9331b9406f793b 100644 --- a/app/models/notification_group.rb +++ b/app/models/notification_group.rb @@ -51,6 +51,7 @@ def self.from_notifications(notifications, pagination_range: nil, grouped_types: :report, :account_relationship_severance_event, :account_warning, + :generated_annual_report, to: :notification, prefix: false class << self diff --git a/app/models/scheduled_status.rb b/app/models/scheduled_status.rb index 27f0cbd280cbf2..cb49e907057ac1 100644 --- a/app/models/scheduled_status.rb +++ b/app/models/scheduled_status.rb @@ -15,6 +15,7 @@ class ScheduledStatus < ApplicationRecord TOTAL_LIMIT = 300 DAILY_LIMIT = 25 + MINIMUM_OFFSET = 5.minutes.freeze belongs_to :account, inverse_of: :scheduled_statuses has_many :media_attachments, inverse_of: :scheduled_status, dependent: :nullify @@ -26,7 +27,7 @@ class ScheduledStatus < ApplicationRecord private def validate_future_date - errors.add(:scheduled_at, I18n.t('scheduled_statuses.too_soon')) if scheduled_at.present? && scheduled_at <= Time.now.utc + PostStatusService::MIN_SCHEDULE_OFFSET + errors.add(:scheduled_at, I18n.t('scheduled_statuses.too_soon')) if scheduled_at.present? && scheduled_at <= Time.now.utc + MINIMUM_OFFSET end def validate_total_limit diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb index 31f18fd7ef09be..d99ecf8adba43d 100644 --- a/app/models/session_activation.rb +++ b/app/models/session_activation.rb @@ -30,6 +30,8 @@ class SessionActivation < ApplicationRecord DEFAULT_SCOPES = %w(read write follow).freeze + scope :latest, -> { order(id: :desc) } + class << self def active?(id) id && exists?(session_id: id) @@ -48,7 +50,7 @@ def deactivate(id) end def purge_old - order('created_at desc').offset(Rails.configuration.x.max_session_activations).destroy_all + latest.offset(Rails.configuration.x.max_session_activations).destroy_all end def exclusive(id) diff --git a/app/models/status.rb b/app/models/status.rb index ba7bae3cadbede..573e27d1a706d9 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -130,6 +130,7 @@ class Status < ApplicationRecord } scope :distributable_visibility, -> { where(visibility: %i(public unlisted)) } scope :list_eligible_visibility, -> { where(visibility: %i(public unlisted private)) } + scope :not_direct_visibility, -> { where.not(visibility: :direct) } scope :not_local_only, -> { where(local_only: [false, nil]) } diff --git a/app/models/user.rb b/app/models/user.rb index 1a970cec7f9bbd..00596e3439f26e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -125,7 +125,7 @@ class User < ApplicationRecord scope :signed_in_recently, -> { where(current_sign_in_at: ACTIVE_DURATION.ago..) } scope :not_signed_in_recently, -> { where(current_sign_in_at: ...ACTIVE_DURATION.ago) } scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) } - scope :matches_ip, ->(value) { left_joins(:ips).where('user_ips.ip <<= ?', value).group('users.id') } + scope :matches_ip, ->(value) { left_joins(:ips).merge(IpBlock.contained_by(value)).group('users.id') } before_validation :sanitize_role before_create :set_approved @@ -340,7 +340,7 @@ def revoke_access! Doorkeeper::AccessGrant.by_resource_owner(self).update_all(revoked_at: Time.now.utc) Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch| - batch.update_all(revoked_at: Time.now.utc) + batch.touch_all(:revoked_at) Web::PushSubscription.where(access_token_id: batch).delete_all # Revoke each access token for the Streaming API, since `update_all`` @@ -444,7 +444,7 @@ def wrap_email_confirmation end def sign_up_from_ip_requires_approval? - sign_up_ip.present? && IpBlock.severity_sign_up_requires_approval.exists?(['ip >>= ?', sign_up_ip.to_s]) + sign_up_ip.present? && IpBlock.severity_sign_up_requires_approval.containing(sign_up_ip.to_s).exists? end def sign_up_email_requires_approval? diff --git a/app/models/user_ip.rb b/app/models/user_ip.rb index a6da2c07401b44..25aa81ccd42f71 100644 --- a/app/models/user_ip.rb +++ b/app/models/user_ip.rb @@ -11,6 +11,7 @@ class UserIp < ApplicationRecord include DatabaseViewRecord + include InetContainer self.primary_key = :user_id diff --git a/app/serializers/rest/annual_report_event_serializer.rb b/app/serializers/rest/annual_report_event_serializer.rb new file mode 100644 index 00000000000000..555a596357cdef --- /dev/null +++ b/app/serializers/rest/annual_report_event_serializer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class REST::AnnualReportEventSerializer < ActiveModel::Serializer + attributes :year + + def year + object.year.to_s + end +end diff --git a/app/serializers/rest/notification_group_serializer.rb b/app/serializers/rest/notification_group_serializer.rb index 7e8f00df3c7189..f4af842e38dca7 100644 --- a/app/serializers/rest/notification_group_serializer.rb +++ b/app/serializers/rest/notification_group_serializer.rb @@ -13,6 +13,7 @@ class REST::NotificationGroupSerializer < ActiveModel::Serializer belongs_to :report, if: :report_type?, serializer: REST::ReportSerializer belongs_to :account_relationship_severance_event, key: :event, if: :relationship_severance_event?, serializer: REST::AccountRelationshipSeveranceEventSerializer belongs_to :account_warning, key: :moderation_warning, if: :moderation_warning_event?, serializer: REST::AccountWarningSerializer + belongs_to :generated_annual_report, key: :annual_report, if: :annual_report_event?, serializer: REST::AnnualReportEventSerializer def sample_account_ids object.sample_accounts.pluck(:id).map(&:to_s) @@ -38,6 +39,10 @@ def moderation_warning_event? object.type == :moderation_warning end + def annual_report_event? + object.type == :annual_report + end + def page_min_id object.pagination_data[:min_id].to_s end diff --git a/app/serializers/rest/v1/instance_serializer.rb b/app/serializers/rest/v1/instance_serializer.rb index 62bc415b576ef2..77ee92ca0c44b2 100644 --- a/app/serializers/rest/v1/instance_serializer.rb +++ b/app/serializers/rest/v1/instance_serializer.rb @@ -61,14 +61,6 @@ def urls { streaming_api: Rails.configuration.x.streaming_api_base_url } end - def usage - { - users: { - active_month: instance_presenter.active_user_count(4), - }, - } - end - def configuration { accounts: { diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 9aebab787ee165..0cf56c5a247b1d 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -3,7 +3,7 @@ class NotifyService < BaseService include Redisable - # TODO: the severed_relationships type probably warrants email notifications + # TODO: the severed_relationships and annual_report types probably warrants email notifications NON_EMAIL_TYPES = %i( admin.report admin.sign_up @@ -12,6 +12,7 @@ class NotifyService < BaseService status moderation_warning severed_relationships + annual_report ).freeze class BaseCondition @@ -25,6 +26,7 @@ class BaseCondition poll update account_warning + annual_report ).freeze def initialize(notification) @@ -100,7 +102,7 @@ def statuses_that_mention_sender class DropCondition < BaseCondition def drop? blocked = @recipient.unavailable? - blocked ||= from_self? && %i(poll severed_relationships moderation_warning).exclude?(@notification.type) + blocked ||= from_self? && %i(poll severed_relationships moderation_warning annual_report).exclude?(@notification.type) return blocked if message? && from_staff? diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 427137545fc303..be6df69ab82666 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -4,8 +4,6 @@ class PostStatusService < BaseService include Redisable include LanguagesHelper - MIN_SCHEDULE_OFFSET = 5.minutes.freeze - class UnexpectedMentionsError < StandardError attr_reader :accounts diff --git a/app/services/process_hashtags_service.rb b/app/services/process_hashtags_service.rb index 17c347b08895be..0baea0185cef0b 100644 --- a/app/services/process_hashtags_service.rb +++ b/app/services/process_hashtags_service.rb @@ -33,7 +33,7 @@ def update_featured_tags! unless removed_tags.empty? @account.featured_tags.where(tag_id: removed_tags.map(&:id)).find_each do |featured_tag| - featured_tag.decrement(@status.id) + featured_tag.decrement(@status) end end end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 4e1849a098cf7c..3b7a1b8580f93c 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -117,7 +117,7 @@ def remove_reblogs def remove_from_hashtags @account.featured_tags.where(tag_id: @status.tags.map(&:id)).find_each do |featured_tag| - featured_tag.decrement(@status.id) + featured_tag.decrement(@status) end return unless @status.public_visibility? diff --git a/app/services/unallow_domain_service.rb b/app/services/unallow_domain_service.rb index bdc71b1c08e7ab..bbe957145057c0 100644 --- a/app/services/unallow_domain_service.rb +++ b/app/services/unallow_domain_service.rb @@ -12,7 +12,7 @@ def call(domain_allow) private def suspend_accounts!(domain) - Account.where(domain: domain).in_batches.update_all(suspended_at: Time.now.utc) + Account.where(domain: domain).in_batches.touch_all(:suspended_at) AfterUnallowDomainWorker.perform_async(domain) end end diff --git a/app/workers/scheduler/scheduled_statuses_scheduler.rb b/app/workers/scheduler/scheduled_statuses_scheduler.rb index 4e251780de5f1a..e2ccabf70f84ca 100644 --- a/app/workers/scheduler/scheduled_statuses_scheduler.rb +++ b/app/workers/scheduler/scheduled_statuses_scheduler.rb @@ -20,7 +20,7 @@ def publish_scheduled_statuses! end def due_statuses - ScheduledStatus.where(scheduled_at: ..Time.now.utc + PostStatusService::MIN_SCHEDULE_OFFSET) + ScheduledStatus.where(scheduled_at: ..time_due_at) end def publish_scheduled_announcements! @@ -30,7 +30,7 @@ def publish_scheduled_announcements! end def due_announcements - Announcement.unpublished.where('scheduled_at IS NOT NULL AND scheduled_at <= ?', Time.now.utc + PostStatusService::MIN_SCHEDULE_OFFSET) + Announcement.unpublished.where('scheduled_at IS NOT NULL AND scheduled_at <= ?', time_due_at) end def unpublish_expired_announcements! @@ -40,4 +40,8 @@ def unpublish_expired_announcements! def expired_announcements Announcement.published.where('ends_at IS NOT NULL AND ends_at <= ?', Time.now.utc) end + + def time_due_at + Time.now.utc + ScheduledStatus::MINIMUM_OFFSET + end end diff --git a/config.ru b/config.ru index afd13e211238e2..842bccc3402da2 100644 --- a/config.ru +++ b/config.ru @@ -2,5 +2,6 @@ # This file is used by Rack-based servers to start the application. -require File.expand_path('config/environment', __dir__) +require_relative 'config/environment' + run Rails.application diff --git a/config/application.rb b/config/application.rb index f94366d36f49db..18a1b1a0fdc688 100644 --- a/config/application.rb +++ b/config/application.rb @@ -52,8 +52,6 @@ require_relative '../lib/stoplight/redis_data_store_extensions' require_relative '../lib/active_record/database_tasks_extensions' require_relative '../lib/active_record/batches' -require_relative '../lib/active_record/with_recursive' -require_relative '../lib/arel/union_parenthesizing' require_relative '../lib/simple_navigation/item_extensions' Bundler.require(:pam_authentication) if ENV['PAM_ENABLED'] == 'true' @@ -61,10 +59,7 @@ module Mastodon class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.1 - - # Explicitly set the cache format version to align with Rails version - config.active_support.cache_format_version = 7.1 + config.load_defaults 7.2 # Please, add to the `ignore` list any other `lib` subdirectories that do # not contain `.rb` files, or that should not be reloaded or eager loaded. diff --git a/config/environments/development.rb b/config/environments/development.rb index f5f22a3c1ff158..8533935a8835ba 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -37,11 +37,6 @@ config.action_controller.forgery_protection_origin_check = ENV['DISABLE_FORGERY_REQUEST_PROTECTION'].nil? - ActiveSupport::Logger.new($stdout).tap do |logger| - logger.formatter = config.log_formatter - config.logger = ActiveSupport::TaggedLogging.new(logger) - end - # Generate random VAPID keys Webpush.generate_key.tap do |vapid_key| config.x.vapid_private_key = vapid_key.private_key @@ -51,6 +46,8 @@ # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false + # Disable caching for Action Mailer templates even if Action Controller + # caching is enabled. config.action_mailer.perform_caching = false # Print deprecation notices to the Rails logger. @@ -89,6 +86,9 @@ # Raise error when a before_action's only/except options reference missing actions. config.action_controller.raise_on_missing_callback_actions = true + + # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. + config.generators.apply_rubocop_autocorrect_after_generate! end Redis.raise_deprecations = true diff --git a/config/environments/production.rb b/config/environments/production.rb index 65e43098f174ad..f37a1ebc98d730 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -17,7 +17,6 @@ # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true - config.action_controller.asset_host = ENV['CDN_HOST'] if ENV['CDN_HOST'].present? # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). @@ -26,8 +25,11 @@ # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false + # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. + # config.public_file_server.enabled = false + # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.asset_host = "http://assets.example.com" + config.asset_host = ENV['CDN_HOST'] if ENV['CDN_HOST'].present? # Specifies the header that your server uses for sending files. config.action_dispatch.x_sendfile_header = ENV['SENDFILE_HEADER'] if ENV['SENDFILE_HEADER'].present? @@ -37,6 +39,10 @@ # Allow to specify public IP of reverse proxy if it's needed config.action_dispatch.trusted_proxies = ENV['TRUSTED_PROXY_IP'].split(/(?:\s*,\s*|\s+)/).map { |item| IPAddr.new(item) } if ENV['TRUSTED_PROXY_IP'].present? + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. + # config.assume_ssl = true + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true # Skip http-to-https redirect for the default health check endpoint. @@ -46,14 +52,19 @@ }, } - # Info include generic and useful information about system operation, but avoids logging too much - # information to avoid inadvertent exposure of personally identifiable information (PII). If you - # want to log everything, set the level to "debug". - config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'info').to_sym + # Log to STDOUT by default + config.logger = ActiveSupport::Logger.new($stdout) + .tap { |logger| logger.formatter = ::Logger::Formatter.new } + .then { |logger| ActiveSupport::TaggedLogging.new(logger) } # Prepend all log lines with the following tags. config.log_tags = [:request_id] + # "info" includes generic and useful information about system operation, but avoids logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). If you + # want to log everything, set the level to "debug". + config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'info') + # Use a different cache store in production. config.cache_store = :redis_cache_store, REDIS_CONFIGURATION.cache @@ -61,6 +72,8 @@ # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "mastodon_production" + # Disable caching for Action Mailer templates even if Action Controller + # caching is enabled. config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. @@ -87,18 +100,8 @@ { key: controller.signature_key_id } if controller.respond_to?(:signed_request?) && controller.signed_request? end - # Use a different logger for distributed setups. - # require "syslog/logger" - # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") - - # Log to STDOUT by default - config.logger = ActiveSupport::Logger.new($stdout) - .tap { |logger| logger.formatter = ::Logger::Formatter.new } - .then { |logger| ActiveSupport::TaggedLogging.new(logger) } - # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false - config.action_mailer.perform_caching = false # E-mails diff --git a/config/environments/test.rb b/config/environments/test.rb index eb29ab1e176775..5406eac9ecac6d 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -19,8 +19,6 @@ # loading is working properly before deploying your code. config.eager_load = ENV['CI'].present? - config.assets_digest = false - # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false @@ -32,6 +30,8 @@ # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false + # Disable caching for Action Mailer templates even if Action Controller + # caching is enabled. config.action_mailer.perform_caching = false config.action_mailer.default_options = { from: 'notifications@localhost' } @@ -41,6 +41,10 @@ # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test + # Unlike controllers, the mailer instance doesn't have any context about the + # incoming request so you'll need to provide the :host parameter yourself. + config.action_mailer.default_url_options = { host: 'www.example.com' } + # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr @@ -58,7 +62,6 @@ # Raise exceptions for disallowed deprecations. config.active_support.disallowed_deprecation = :raise - config.i18n.default_locale = :en config.i18n.fallbacks = true # Tell Active Support which deprecation messages to disallow. diff --git a/config/initializers/cookie_rotator.rb b/config/initializers/cookie_rotator.rb deleted file mode 100644 index ccc2c6b21f786b..00000000000000 --- a/config/initializers/cookie_rotator.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -# TODO: remove this file some time after 4.3.0 - -Rails.application.config.after_initialize do - Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies| - authenticated_encrypted_cookie_salt = Rails.application.config.action_dispatch.authenticated_encrypted_cookie_salt - signed_cookie_salt = Rails.application.config.action_dispatch.signed_cookie_salt - - secret_key_base = Rails.application.secret_key_base - - key_generator = ActiveSupport::KeyGenerator.new( - secret_key_base, iterations: 1000, hash_digest_class: OpenSSL::Digest::SHA1 - ) - key_len = ActiveSupport::MessageEncryptor.key_len - - old_encrypted_secret = key_generator.generate_key(authenticated_encrypted_cookie_salt, key_len) - old_signed_secret = key_generator.generate_key(signed_cookie_salt) - - cookies.rotate :encrypted, old_encrypted_secret - cookies.rotate :signed, old_signed_secret - end -end diff --git a/config/initializers/httplog.rb b/config/initializers/httplog.rb index 02fcef4120ac06..7a009e84d1e07b 100644 --- a/config/initializers/httplog.rb +++ b/config/initializers/httplog.rb @@ -1,7 +1,12 @@ # frozen_string_literal: true -HttpLog.configure do |config| - config.logger = Rails.logger - config.color = { color: :yellow } - config.compact_log = true +# Disable httplog in production unless log_level is `debug` +if !Rails.env.production? || Rails.configuration.log_level == :debug + require 'httplog' + + HttpLog.configure do |config| + config.logger = Rails.logger + config.color = { color: :yellow } + config.compact_log = true + end end diff --git a/config/locales/doorkeeper.ru.yml b/config/locales/doorkeeper.ru.yml index 1dcb2093d517d2..c7ea94c2e34fb2 100644 --- a/config/locales/doorkeeper.ru.yml +++ b/config/locales/doorkeeper.ru.yml @@ -60,6 +60,7 @@ ru: error: title: Произошла ошибка new: + prompt_html: "%{client_name} хочет получить доступ к вашему аккаунту. Принимайте запрос только в том случае, если узнаёте, откуда он, и доверяете источнику." review_permissions: Просмотр разрешений title: Требуется авторизация show: diff --git a/config/locales/doorkeeper.th.yml b/config/locales/doorkeeper.th.yml index 2dfc72cd6ccc9b..3b52e170ea3eb0 100644 --- a/config/locales/doorkeeper.th.yml +++ b/config/locales/doorkeeper.th.yml @@ -60,7 +60,7 @@ th: error: title: เกิดข้อผิดพลาด new: - prompt_html: "%{client_name} ร้องขอสิทธิ์ในการเข้าถึงข้อมูลในบัญชีของคุณ อนุมัติคำขอนี้ได้ก็ต่อเมื่อคุณมั่นใจและเชื่อถือในแหล่งที่มาของข้อมูลนี้" + prompt_html: "%{client_name} ต้องการสิทธิอนุญาตเพื่อเข้าถึงบัญชีของคุณ อนุมัติคำขอนี้เฉพาะหากคุณรู้จักและเชื่อถือแหล่งที่มานี้เท่านั้น" review_permissions: ตรวจทานสิทธิอนุญาต title: ต้องการการอนุญาต show: diff --git a/config/locales/fo.yml b/config/locales/fo.yml index 3ade3007f44a56..1060ee1609c2bd 100644 --- a/config/locales/fo.yml +++ b/config/locales/fo.yml @@ -1368,6 +1368,44 @@ fo: merge_long: Varðveit verandi teigarøð og legg nýggjar afturat overwrite: Skriva omaná overwrite_long: Legg nýggj teigarøð inn fyri tey verandi + overwrite_preambles: + blocking_html: + one: Tú ert í ferð við at útskifta blokeringslistan hjá tær við upp til %{count} kontu frá %{filename}. + other: Tú ert í ferð við at útskifta blokeringslistan hjá tær við upp til %{count} kontum frá %{filename}. + bookmarks_html: + one: Tú ert í ferð við at útskifta tíni bókamerki við upp til %{count} posti frá %{filename}. + other: Tú ert í ferð við at útskifta tíni bókamerki við upp til %{count} postum frá %{filename}. + domain_blocking_html: + one: Tú ert í ferð við at útskifta navnaøkisblokeringslistan hjá tær við upp til %{count} navnaøki frá %{filename}. + other: Tú ert í ferð við at útskifta navnaøkisblokeringslistan hjá tær við upp til %{count} navnaøkjum frá %{filename}. + following_html: + one: Tú ert í ferð við at fylgja upp til %{count} kontu frá %{filename} og at gevast at fylgja øðrum. + other: Tú ert í ferð við at fylgja upp til %{count} kontum frá %{filename} og at gevast at fylgja øðrum. + lists_html: + one: Tú ert í ferð við at skifta listarnar hjá tær út við tað, sum er í %{filename}. Upp til %{count} konta verður løgd afturat nýggjum listum. + other: Tú ert í ferð við at skifta listarnar hjá tær út við tað, sum er í %{filename}. Upp til %{count} kontur verða lagdar afturat nýggjum listum. + muting_html: + one: Tú ert í ferð við at útskifta listan hjá tær við doyvdum kontum við upp til %{count} kontu frá %{filename}. + other: Tú ert í ferð við at útskifta listan hjá tær við doyvdum kontum við upp til %{count} kontum frá %{filename}. + preambles: + blocking_html: + one: Tú ert í ferð við at blokera upp til %{count} kontu frá %{filename}. + other: Tú ert í ferð við at blokera upp til %{count} kontur frá %{filename}. + bookmarks_html: + one: Tú ert í ferð við at leggja upp til %{count} post frá %{filename} afturat tínum bókamerkjum. + other: Tú ert í ferð við at leggja upp til %{count} postar frá %{filename} afturat tínum bókamerkjum. + domain_blocking_html: + one: Tú ert í ferð við at blokera upp til %{count} navnaøki frá %{filename}. + other: Tú ert í ferð við at blokera upp til %{count} navnaøki frá %{filename}. + following_html: + one: Tú ert í ferð við at fylgja upp til %{count} kontu frá %{filename}. + other: Tú ert í ferð við at fylgja upp til %{count} kontur frá %{filename}. + lists_html: + one: Tú ert í ferð við at leggja upp til %{count} kontu frá %{filename} afturat tínum listum. Nýggir listar verða stovnaðir, um eingin listi er at leggja afturat. + other: Tú ert í ferð við at leggja upp til %{count} kontur frá %{filename} afturat tínum listum. Nýggir listar verða stovnaðir, um eingin listi er at leggja afturat. + muting_html: + one: Tú ert í ferð við at doyva upp til %{count} kontu frá %{filename}. + other: Tú ert í ferð við at doyva upp til %{count} kontur frá %{filename}. preface: Tú kanst innlesa dátur, sum tú hevur útlisið frá einum øðrum ambætara, so sum listar av fólki, sum tú fylgir ella blokerar. recent_imports: Feskar innflytingar states: diff --git a/config/locales/fy.yml b/config/locales/fy.yml index 8ccedd2be17753..a6656f91b84cc1 100644 --- a/config/locales/fy.yml +++ b/config/locales/fy.yml @@ -21,6 +21,7 @@ fy: one: Toot other: Berjochten posts_tab_heading: Berjochten + self_follow_error: It folgjen fan dyn eigen account is net tastien admin: account_actions: action: Aksje útfiere @@ -1165,8 +1166,11 @@ fy: use_security_key: Befeiligingskaai brûke author_attribution: example_title: Faorbyldtekst + hint_html: Skriuwe jo nijs- of blogartikelen bûten Mastodon? Bepaal hoe’t jo oahelle wurde as dizze dield wurde op Mastodon. + instructions: 'Soargj derfoar dat dizze koade yn de HTML fan jo artikel sit:' more_from_html: Mear fan %{name} s_blog: Weblog fan %{name} + then_instructions: Foegje dernei de domeinnamme fan de publikaasje yn it ûndersteande fjild ta. title: Auteur-attribúsje challenge: confirm: Trochgean diff --git a/config/locales/gd.yml b/config/locales/gd.yml index a05ce6ed3cd435..c804f425232877 100644 --- a/config/locales/gd.yml +++ b/config/locales/gd.yml @@ -262,7 +262,7 @@ gd: destroy_domain_allow_html: Dì-cheadaich %{name} co-nasgadh leis an àrainn %{target} destroy_domain_block_html: Dì-bhac %{name} an àrainn %{target} destroy_email_domain_block_html: Dì-bhac %{name} an àrainn puist-d %{target} - destroy_instance_html: Purgaidich %{name} an àrainn %{target} + destroy_instance_html: Phurgaidich %{name} an àrainn %{target} destroy_ip_block_html: Sguab %{name} às riaghailt dhan IP %{target} destroy_status_html: Thug %{name} post aig %{target} air falbh destroy_unavailable_domain_html: Lean %{name} air adhart leis an lìbhrigeadh dhan àrainn %{target} diff --git a/config/locales/ru.yml b/config/locales/ru.yml index b7a10269fa9425..831f68b24b3a33 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -25,6 +25,7 @@ ru: one: Пост other: статусов posts_tab_heading: Посты + self_follow_error: Нельзя подписаться на самого себя admin: account_actions: action: Выполнить действие @@ -64,7 +65,7 @@ ru: demote: Разжаловать destroyed_msg: Данные %{username} поставлены в очередь на удаление disable: Заморозка - disable_sign_in_token_auth: Отключите аутентификацию с помощью маркера электронной почты + disable_sign_in_token_auth: Отключить аутентификацию по e-mail кодам disable_two_factor_authentication: Отключить 2FA disabled: Отключено display_name: Отображаемое имя @@ -73,7 +74,7 @@ ru: email: E-mail email_status: Статус e-mail enable: Включить - enable_sign_in_token_auth: Включите аутентификацию с помощью маркера электронной почты + enable_sign_in_token_auth: Включить аутентификацию по e-mail кодам enabled: Включен enabled_msg: Учётная запись %{username} успешно разморожена followers: Подписчики @@ -146,8 +147,8 @@ ru: security_measures: only_password: Только пароль password_and_2fa: Пароль и 2FA - sensitive: Отметить как «деликатного содержания» - sensitized: отмечено как «деликатного характера» + sensitive: Деликатный + sensitized: отмечено как деликатный контент shared_inbox_url: URL общих входящих show: created_reports: Жалобы, отправленные с этой учётной записи @@ -165,7 +166,7 @@ ru: unblock_email: Разблокировать e-mail адрес unblocked_email_msg: E-mail адрес %{username} разблокирован unconfirmed_email: Неподтверждённый e-mail - undo_sensitized: Убрать отметку «деликатного содержания» + undo_sensitized: Снять отметку "деликатный" undo_silenced: Отменить скрытие undo_suspension: Снять блокировку unsilenced_msg: Ограничения с учётной записи %{username} сняты успешно @@ -462,6 +463,7 @@ ru: title: Блокировка нового почтового домена no_email_domain_block_selected: Блоки почтовых доменов не были изменены, так как ни один из них не был выбран not_permitted: Не разрешено + resolved_dns_records_hint_html: Доменное имя указывает на следующие MX-домены, которые в конечном итоге отвечают за прием электронной почты. Блокировка MX-домена будет блокировать регистрации с любого адреса электронной почты, который использует тот же MX-домен, даже если видимое доменное имя отличается от него. Будьте осторожны, чтобы не заблокировать основных провайдеров электронной почты resolved_through_html: Разрешено через %{domain} title: Заблокированные e-mail домены export_domain_allows: @@ -625,6 +627,7 @@ ru: resolve_description_html: Никаких действий не будет выполнено относительно доложенного содержимого. Жалоба будет закрыта. silence_description_html: Учетная запись будет видна только тем пользователям, которые уже подписаны на неё, либо открыли его вручную. Это действие можно отменить в любой момент, и отменяет все жалобы против аккаунта. suspend_description_html: Аккаунт и все его содержимое будут недоступны и в конечном итоге удалены, и взаимодействие с ним будет невозможно. Это действие можно отменить в течение 30 дней. Отменяет все жалобы против этого аккаунта. + actions_description_html: Выберите действие, чтобы разрешить данную жалобу. Если вы примете меры модерации против аккаунта, его владелец получит уведомление по электронной почте, кроме тех случаев, когда выбрана категория Спам. actions_description_remote_html: Решите вопрос о том, какие меры необходимо принять для урегулирования этой жалобы. Это повлияет только на то, как ваш сервер взаимодействует с этим удаленным аккаунтом и обрабатывает его содержимое. actions_no_posts: У этого отчета нет связанных с ним сообщений для удаления add_to_report: Прикрепить ещё @@ -902,6 +905,7 @@ ru: sidekiq_process_check: message_html: Ни один Sidekiq не запущен для %{value} очереди(-ей). Пожалуйста, просмотрите настройки Sidekiq software_version_check: + action: Посмотреть доступные обновления message_html: Доступно обновление для Mastodon. software_version_critical_check: action: Посмотреть доступные обновления @@ -1198,8 +1202,11 @@ ru: use_security_key: Использовать ключ безопасности author_attribution: example_title: Образец текста + hint_html: Публикуете ли вы свои статьи где-либо ещё кроме Mastodon? Если да, то ваше авторство может быть упомянуто, когда ими делятся в Mastodon. + instructions: 'Добавьте код ниже в HTML ваших статей:' more_from_html: Больше от %{name} s_blog: "%{name}'S Блог" + then_instructions: Затем добавьте доменное имя сайта, где вы публикуетесь, в поле ниже. title: Авторская атрибуция challenge: confirm: Продолжить @@ -1309,7 +1316,7 @@ ru: csv: CSV domain_blocks: Доменные блокировки lists: Списки - mutes: Ваши игнорируете + mutes: Ваши игнорируемые storage: Ваши файлы featured_tags: add_new: Добавить @@ -1411,6 +1418,68 @@ ru: merge_long: Сохранить имеющиеся данные и добавить новые. overwrite: Перезаписать overwrite_long: Перезаписать имеющиеся данные новыми. + overwrite_preambles: + blocking_html: + few: Вы собираетесь заменить свой список блокировки, в котором сейчас %{count} аккаунта, из файла %{filename}. + many: Вы собираетесь заменить свой список блокировки, в котором сейчас %{count} аккаунов, из файла %{filename}. + one: Вы собираетесь заменить свой список блокировки, в котором сейчас %{count} аккаунт, из файла %{filename}. + other: Вы собираетесь заменить свой список блокировки, в котором сейчас %{count} аккаунтов, из файла %{filename}. + bookmarks_html: + few: Вы собираетесь заменить свои закладки, в которых сейчас %{count} поста, из файла %{filename}. + many: Вы собираетесь заменить свои закладки, в которых сейчас %{count} постов, из файла %{filename}. + one: Вы собираетесь заменить свои закладки, в которых сейчас %{count} пост, из файла %{filename}. + other: Вы собираетесь заменить свои закладки, в которых сейчас %{count} постов, из файла %{filename}. + domain_blocking_html: + few: Вы собираетесь заменить свой список доменных блокировок, в котором сейчас %{count} домена, из файла %{filename}. + many: Вы собираетесь заменить свой список доменных блокировок, в котором сейчас %{count} доменов, из файла %{filename}. + one: Вы собираетесь заменить свой список доменных блокировок, в котором сейчас %{count} домен, из файла %{filename}. + other: Вы собираетесь заменить свой список доменных блокировок, в котором сейчас %{count} доменов, из файла %{filename}. + following_html: + few: Вы собираетесь подписаться на %{count} аккаунта из файла %{filename} и отписаться от всех прочих. + many: Вы собираетесь подписаться на %{count} аккаунтов из файла %{filename} и отписаться от всех прочих. + one: Вы собираетесь подписаться на %{count} аккаунт из файла %{filename} и отписаться от всех прочих. + other: Вы собираетесь подписаться на %{count} аккаунтов из файла %{filename} и отписаться от всех прочих. + lists_html: + few: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будут добавлены %{count} аккаунта. + many: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будут добавлены %{count} аккаунтов. + one: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будет добавлен %{count} аккаунт. + other: Вы собираетесь заменить свои списки содержимым файла %{filename}. В новые списки будут добавлены %{count} аккаунтов. + muting_html: + few: Вы собираетесь заменить свой список игнорируемых пользователей списком из %{count} аккаунтов из файла %{filename}. + many: Вы собираетесь заменить свой список игнорируемых пользователей списком из %{count} аккаунтов из файла %{filename}. + one: Вы собираетесь заменить свой список игнорируемых пользователей списком из %{count} аккаунта из файла %{filename}. + other: Вы собираетесь заменить свой список игнорируемых пользователей списком из %{count} аккаунтов из файла %{filename}. + preambles: + blocking_html: + few: Вы собираетесь заблокировать %{count} аккаунта из файла %{filename}. + many: Вы собираетесь заблокировать %{count} аккаунтов из файла %{filename}. + one: Вы собираетесь заблокировать %{count} аккаунт из файла %{filename}. + other: Вы собираетесь заблокировать %{count} аккаунтов из файла %{filename}. + bookmarks_html: + few: Вы собираетесь добавить %{count} поста из файла %{filename} в свои закладки. + many: Вы собираетесь добавить %{count} постов из файла %{filename} в свои закладки. + one: Вы собираетесь добавить %{count} пост из файла %{filename} в свои закладки. + other: Вы собираетесь добавить %{count} постов из файла %{filename} в свои закладки. + domain_blocking_html: + few: Вы собираетесь заблокировать %{count} домена из файла %{filename}. + many: Вы собираетесь заблокировать %{count} доменов из файла %{filename}. + one: Вы собираетесь заблокировать %{count} домен из файла %{filename}. + other: Вы собираетесь заблокировать %{count} доменов из файла %{filename}. + following_html: + few: Вы собираетесь подписаться на %{count} аккаунта из файла %{filename}. + many: Вы собираетесь подписаться на %{count} аккаунтов из файла %{filename}. + one: Вы собираетесь подписаться на %{count} аккаунт из файла %{filename}. + other: Вы собираетесь подписаться на %{count} аккаунтов из файла %{filename}. + lists_html: + few: Вы собираетесь добавить %{count} аккаунта из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. + many: Вы собираетесь добавить %{count} аккаунтов из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. + one: Вы собираетесь добавить %{count} аккаунт из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. + other: Вы собираетесь добавить %{count} аккаунтов из файла %{filename} в свои списки. Если соответствующих списков нет, они будут созданы. + muting_html: + few: Вы собираетесь начать игнорировать %{count} аккаунта из файла %{filename}. + many: Вы собираетесь начать игнорировать %{count} аккаунтов из файла %{filename}. + one: Вы собираетесь начать игнорировать %{count} аккаунт из файла %{filename}. + other: Вы собираетесь начать игнорировать %{count} аккаунтов из файла %{filename}. preface: Вы можете загрузить некоторые данные, например, списки людей, на которых Вы подписаны или которых блокируете, в Вашу учётную запись на этом узле из файлов, экспортированных с другого узла. recent_imports: Недавно импортированное states: @@ -1483,6 +1552,7 @@ ru: unsubscribe: action: Да, отписаться complete: Подписка отменена + confirmation_html: Вы точно желаете отписаться от всех уведомления типа «%{type}», доставляемых из сервера Mastodon %{domain} на ваш адрес электронной почты %{email}? Вы всегда сможете подписаться снова в настройках e-mail уведомлений. emails: notification_emails: favourite: любимые электронные письма с уведомлениями @@ -1728,6 +1798,7 @@ ru: delete: Удаление учётной записи development: Разработчикам edit_profile: Изменить профиль + export: Экспорт featured_tags: Избранные хэштеги import: Импорт import_and_export: Импорт и экспорт diff --git a/config/locales/simple_form.fy.yml b/config/locales/simple_form.fy.yml index 8a753dcc898d51..c840342db5cf4d 100644 --- a/config/locales/simple_form.fy.yml +++ b/config/locales/simple_form.fy.yml @@ -3,6 +3,7 @@ fy: simple_form: hints: account: + attribution_domains_as_text: Ien per rigel. Beskermet tsjin falske attribúsjes. discoverable: Jo iepenbiere berjochten kinne útljochte wurde op ferskate plakken binnen Mastodon en jo account kin oanrekommandearre wurde oan oare brûkers. display_name: Jo folsleine namme of in aardige bynamme. fields: Jo website, persoanlike foarnammewurden, leeftiid, alles wat jo mar kwyt wolle. @@ -143,6 +144,7 @@ fy: url: Wêr’t eveneminten nei ta stjoerd wurde labels: account: + attribution_domains_as_text: Websites dy’t jo wurdearring jaan meie discoverable: Profyl en bydragen yn sykalgoritmen opnimme litte fields: name: Label diff --git a/config/locales/simple_form.ko.yml b/config/locales/simple_form.ko.yml index c40814d61b1593..b2ebf5666def38 100644 --- a/config/locales/simple_form.ko.yml +++ b/config/locales/simple_form.ko.yml @@ -32,9 +32,9 @@ ko: warning_preset_id: 선택사항. 틀의 마지막에 임의의 텍스트를 추가 할 수 있습니다 announcement: all_day: 체크 되었을 경우, 그 시간에 속한 날짜들에만 표시됩니다 - ends_at: 옵션입니다. 공지사항이 이 시간에 자동으로 발행 중지 됩니다 + ends_at: 선택사항. 공지사항이 이 시간에 자동으로 발행 중지 됩니다 scheduled_at: 공백으로 두면 공지사항이 곧바로 발행 됩니다 - starts_at: 공지사항이 특정한 시간에 종속 될 때를 위한 옵션입니다 + starts_at: 선택사항. 공지사항이 특정한 시간에 종속 될 때를 위한 옵션입니다 text: 게시물 문법을 사용할 수 있습니다. 공지사항은 사용자의 화면 상단 공간을 차지한다는 것을 명심하세요 appeal: text: 처벌에 대해 단 한 번만 이의제기를 할 수 있습니다 @@ -110,7 +110,7 @@ ko: invite_request: text: 이 정보는 신청을 검토하는데 도움을 줄 수 있습니다. ip_block: - comment: 필수 아님. 왜 이 규칙을 추가했는지 기억하세요. + comment: 선택사항. 왜 이 규칙을 추가했는지 기억하세요. expires_in: IP 주소는 한정된 자원입니다, 이것들은 가끔 공유 되거나 자주 소유자가 바뀌기도 합니다. 이런 이유로 인해, IP 차단을 영구히 유지하는 것은 추천하지 않습니다. ip: IPv4 또는 IPv6 주소를 입력하세요. CIDR 문법을 사용해서 모든 범위를 차단할 수도 있습니다. 자기 자신을 잠가버리지 않도록 주의하세요! severities: @@ -119,7 +119,7 @@ ko: sign_up_requires_approval: 새 가입이 승인을 필요로 하도록 합니다 severity: 해당 IP로부터의 요청에 대해 무엇이 일어나게 할 지 고르세요 rule: - hint: 옵션사항. 규칙에 대한 더 상세한 정보를 제공하세요 + hint: 선택사항. 규칙에 대한 더 상세한 정보를 제공하세요 text: 이 서버 사용자들이 지켜야 할 규칙과 요구사항을 설명해주세요. 짧고 간단하게 작성해주세요 sessions: otp: '휴대전화에서 생성된 이중 인증 코드를 입력하거나, 복구 코드 중 하나를 사용하세요:' diff --git a/config/locales/simple_form.lv.yml b/config/locales/simple_form.lv.yml index 974008b5ebc5e9..106e29d2de1aa0 100644 --- a/config/locales/simple_form.lv.yml +++ b/config/locales/simple_form.lv.yml @@ -61,7 +61,7 @@ lv: setting_display_media_hide_all: Vienmēr slēpt multividi setting_display_media_show_all: Vienmēr rādīt multividi setting_use_blurhash: Pāreju pamatā ir paslēpto uzskatāmo līdzekļu krāsas, bet saturs tiek padarīts neskaidrs - setting_use_pending_items: Paslēpt laika skalas atjauninājumus aiz klikšķa, nevis automātiski ritini plūsmu + setting_use_pending_items: Paslēpt laika skalas atjauninājumus aiz klikšķa, nevis ar automātisku plūsmas ritināšanu username: Tu vari lietot burtus, ciparus un zemsvītras whole_word: Ja atslēgvārds vai frāze ir tikai burtciparu, tas tiks lietots tikai tad, ja tas atbilst visam vārdam domain_allow: diff --git a/config/locales/simple_form.nl.yml b/config/locales/simple_form.nl.yml index 7280ba6d801626..5318059f4a4ccf 100644 --- a/config/locales/simple_form.nl.yml +++ b/config/locales/simple_form.nl.yml @@ -3,7 +3,7 @@ nl: simple_form: hints: account: - attribution_domains_as_text: Eén per regel. Beschermt tegen valse attribueringen. + attribution_domains_as_text: Eén per regel. Beschermt tegen valse toeschrijvingen. discoverable: Jouw openbare berichten kunnen worden uitgelicht op verschillende plekken binnen Mastodon en jouw account kan worden aanbevolen aan andere gebruikers. display_name: Jouw volledige naam of een leuke bijnaam. fields: Jouw website, persoonlijke voornaamwoorden, leeftijd, alles wat je maar kwijt wilt. diff --git a/config/locales/simple_form.ru.yml b/config/locales/simple_form.ru.yml index 1bdffc6f1d5634..37626ff9fd1ec2 100644 --- a/config/locales/simple_form.ru.yml +++ b/config/locales/simple_form.ru.yml @@ -3,6 +3,7 @@ ru: simple_form: hints: account: + attribution_domains_as_text: По одному на строку. Защищает от ложных атрибуций. discoverable: Ваши публичные сообщения и профиль могут быть показаны или рекомендованы в различных разделах Mastodon, и ваш профиль может быть предложен другим пользователям. display_name: Ваше полное имя или псевдоним. fields: Ваша домашняя страница, местоимения, возраст - все, что угодно. @@ -143,6 +144,7 @@ ru: url: Куда события будут отправляться labels: account: + attribution_domains_as_text: Веб-сайты, которым разрешено ссылаться на вас discoverable: Профиль и сообщения в алгоритмах обнаружения fields: name: Пункт diff --git a/config/locales/simple_form.sv.yml b/config/locales/simple_form.sv.yml index 36d1def01244d4..72c3f000f60cac 100644 --- a/config/locales/simple_form.sv.yml +++ b/config/locales/simple_form.sv.yml @@ -74,7 +74,7 @@ sv: filters: action: Välj vilken åtgärd som ska utföras när ett inlägg matchar filtret actions: - hide: Dölj det filtrerade innehållet helt (beter sig som om det inte fanns) + hide: Dölj det filtrerade innehållet helt, beter sig som om det inte fanns warn: Dölj det filtrerade innehållet bakom en varning som visar filtrets rubrik form_admin_settings: activity_api_enabled: Antalet lokalt publicerade inlägg, aktiva användare och nya registrerade konton per vecka diff --git a/config/locales/sv.yml b/config/locales/sv.yml index f3ce6443f6d65b..bd33e837bda0bf 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -59,7 +59,7 @@ sv: delete: Radera data deleted: Raderad demote: Degradera - destroyed_msg: "%{username}'s data har nu lagts till kön för att raderas omedelbart" + destroyed_msg: "%{username}s data har nu lagts till kön för att raderas omedelbart" disable: inaktivera disable_sign_in_token_auth: Inaktivera autentisering med pollett via e-post disable_two_factor_authentication: Inaktivera 2FA @@ -72,7 +72,7 @@ sv: enable: Aktivera enable_sign_in_token_auth: Aktivera autentisering med pollett via e-post enabled: Aktiverad - enabled_msg: Uppfrysningen av %{username}'s konto lyckades + enabled_msg: Uppfrysningen av %{username}s konto lyckades followers: Följare follows: Följs header: Rubrik @@ -268,21 +268,21 @@ sv: enable_custom_emoji_html: "%{name} aktiverade emoji %{target}" enable_sign_in_token_auth_user_html: "%{name} aktiverade e-posttokenautentisering för %{target}" enable_user_html: "%{name} aktiverade inloggning för användaren %{target}" - memorialize_account_html: "%{name} gjorde %{target}'s konto till en minnessida" + memorialize_account_html: "%{name} gjorde %{target}s konto till en minnessida" promote_user_html: "%{name} befordrade användaren %{target}" reject_appeal_html: "%{name} avvisade överklagande av modereringsbeslut från %{target}" reject_user_html: "%{name} avvisade registrering från %{target}" - remove_avatar_user_html: "%{name} tog bort %{target}'s avatar" + remove_avatar_user_html: "%{name} tog bort %{target}s avatar" reopen_report_html: "%{name} öppnade rapporten igen %{target}" resend_user_html: "%{name} skickade bekräftelsemail för %{target} på nytt" reset_password_user_html: "%{name} återställ användarens lösenord %{target}" resolve_report_html: "%{name} löste rapporten %{target}" - sensitive_account_html: "%{name} markerade %{target}'s media som känsligt" - silence_account_html: "%{name} begränsade %{target}'s konto" + sensitive_account_html: "%{name} markerade %{target}s media som känsligt" + silence_account_html: "%{name} begränsade %{target}s konto" suspend_account_html: "%{name} stängde av %{target}s konto" unassigned_report_html: "%{name} tog bort tilldelning av rapporten %{target}" unblock_email_account_html: "%{name} avblockerade %{target}s e-postadress" - unsensitive_account_html: "%{name} avmarkerade %{target}'s media som känsligt" + unsensitive_account_html: "%{name} avmarkerade %{target}s media som känsligt" unsilence_account_html: "%{name} tog bort begränsning av %{target}s konto" unsuspend_account_html: "%{name} ångrade avstängningen av %{target}s konto" update_announcement_html: "%{name} uppdaterade kungörelsen %{target}" @@ -569,7 +569,7 @@ sv: no_ip_block_selected: Inga IP-regler ändrades då inga var valda title: IP-regler relationships: - title: "%{acct}'s relationer" + title: "%{acct}s relationer" relays: add_new: Lägg till nytt relä delete: Radera @@ -899,7 +899,7 @@ sv: review_requested: Granskning begärd reviewed: Granskat title: Status - trendable: + trendable: Kan trenda unreviewed: Ogranskad usable: Användbar name: Namn @@ -1826,8 +1826,8 @@ sv: tags: does_not_match_previous_name: matchar inte det föregående namnet themes: - contrast: Hög kontrast - default: Mastodon + contrast: Mastodon (Hög kontrast) + default: Mastodon (Mörk) mastodon-light: Mastodon (ljust) system: Automatisk (använd systemtema) time: diff --git a/config/locales/th.yml b/config/locales/th.yml index cdc56d98ce0f17..ea039a2b9902c1 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -1148,11 +1148,11 @@ th: use_security_key: ใช้กุญแจความปลอดภัย author_attribution: example_title: ข้อความตัวอย่าง - hint_html: คุณเขียนข่าวหรือลงบทความที่อื่นๆที่นอกจาก Mastodon บ้างไหม? ถ้าหากคุณเขียนอยู่ล่ะก็ คุณสามารถบอกได้นะว่าอยากให้คนอื่นให้เครดิตคุณยังไงเวลาแชร์ไปที่ Mastodon นะ - instructions: 'โปรดตรวจสอบให้แน่ใจว่าโค้ดนี้ได้มีอยู่ในบทความของคุณแล้ว HTML:' + hint_html: คุณกำลังเขียนข่าวหรือบทความบล็อกภายนอก Mastodon หรือไม่? ควบคุมวิธีที่คุณได้รับเครดิตเมื่อมีการแบ่งปันข่าวหรือบทความบล็อกใน Mastodon + instructions: 'ตรวจสอบให้แน่ใจว่าโค้ดนี้อยู่ใน HTML ของบทความของคุณ:' more_from_html: เพิ่มเติมจาก %{name} s_blog: บล็อกของ %{name} - then_instructions: จากนั้น ให้เพิ่มใส่ชื่อเว็บไซต์ของสิ่งพิมพ์ลงไปในช่องว่างข้างล่าง + then_instructions: จากนั้น เพิ่มชื่อโดเมนของการเผยแพร่ในช่องด้านล่าง title: การระบุแหล่งที่มาผู้สร้าง challenge: confirm: ดำเนินการต่อ diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 334e806227cf1a..517cd91e06ddd1 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -40,8 +40,10 @@ end end - get '/settings', to: redirect('/admin/settings/branding') - get '/settings/edit', to: redirect('/admin/settings/branding') + with_options to: redirect('/admin/settings/branding') do + get '/settings' + get '/settings/edit' + end namespace :settings do resource :branding, only: [:show, :update], controller: 'branding' diff --git a/config/routes/api.rb b/config/routes/api.rb index 94e418393d42af..fdc1daf4c3d3f6 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -45,15 +45,17 @@ resources :list, only: :show end - get '/streaming', to: 'streaming#index' - get '/streaming/(*any)', to: 'streaming#index' + with_options to: 'streaming#index' do + get '/streaming' + get '/streaming/(*any)' + end resources :custom_emojis, only: [:index] resources :suggestions, only: [:index, :destroy] resources :scheduled_statuses, only: [:index, :show, :update, :destroy] resources :preferences, only: [:index] - resources :annual_reports, only: [:index] do + resources :annual_reports, only: [:index, :show] do member do post :read end diff --git a/db/migrate/20241104082851_create_annual_report_statuses_per_account_counts.rb b/db/migrate/20241104082851_create_annual_report_statuses_per_account_counts.rb new file mode 100644 index 00000000000000..c8ec30ad57adbe --- /dev/null +++ b/db/migrate/20241104082851_create_annual_report_statuses_per_account_counts.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class CreateAnnualReportStatusesPerAccountCounts < ActiveRecord::Migration[7.1] + def change + create_table :annual_report_statuses_per_account_counts do |t| # rubocop:disable Rails/CreateTableWithTimestamps + t.integer :year, null: false + t.bigint :account_id, null: false + t.bigint :statuses_count, null: false + end + + add_index :annual_report_statuses_per_account_counts, [:year, :account_id], unique: true + end +end diff --git a/db/post_migrate/20230803082451_add_unique_index_on_preview_cards_statuses.rb b/db/post_migrate/20230803082451_add_unique_index_on_preview_cards_statuses.rb index 4271f8c08af7f4..c35ad800282343 100644 --- a/db/post_migrate/20230803082451_add_unique_index_on_preview_cards_statuses.rb +++ b/db/post_migrate/20230803082451_add_unique_index_on_preview_cards_statuses.rb @@ -15,21 +15,10 @@ def down private - def supports_concurrent_reindex? - @supports_concurrent_reindex ||= begin - ActiveRecord::Base.connection.database_version >= 120_000 - end - end - def deduplicate_and_reindex! deduplicate_preview_cards! - if supports_concurrent_reindex? - safety_assured { execute 'REINDEX INDEX CONCURRENTLY preview_cards_statuses_pkey' } - else - remove_index :preview_cards_statuses, name: :preview_cards_statuses_pkey - add_index :preview_cards_statuses, [:status_id, :preview_card_id], name: :preview_cards_statuses_pkey, algorithm: :concurrently, unique: true - end + safety_assured { execute 'REINDEX INDEX CONCURRENTLY preview_cards_statuses_pkey' } rescue ActiveRecord::RecordNotUnique retry end diff --git a/db/schema.rb b/db/schema.rb index 1c4a423c5c9de1..2f8e184d8b7128 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[7.1].define(version: 2024_10_22_214312) do +ActiveRecord::Schema[7.1].define(version: 2024_11_04_082851) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -260,6 +260,13 @@ t.bigint "status_ids", array: true end + create_table "annual_report_statuses_per_account_counts", force: :cascade do |t| + t.integer "year", null: false + t.bigint "account_id", null: false + t.bigint "statuses_count", null: false + t.index ["year", "account_id"], name: "idx_on_year_account_id_ff3e167cef", unique: true + end + create_table "appeals", force: :cascade do |t| t.bigint "account_id", null: false t.bigint "account_warning_id", null: false diff --git a/lib/active_record/with_recursive.rb b/lib/active_record/with_recursive.rb deleted file mode 100644 index 4bd3e81eedc1c9..00000000000000 --- a/lib/active_record/with_recursive.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -# Add support for writing recursive CTEs in ActiveRecord - -# Initially from Lorin Thwaits (https://github.com/lorint) as per comment: -# https://github.com/vlado/activerecord-cte/issues/16#issuecomment-1433043310 - -# Modified from the above code to change the signature to -# `with_recursive(hash)` and extending CTE hash values to also includes arrays -# of values that get turned into UNION ALL expressions. - -# This implementation has been merged in Rails: https://github.com/rails/rails/pull/51601 - -module ActiveRecord - module QueryMethodsExtensions - def with_recursive(*args) - @with_is_recursive = true - check_if_method_has_arguments!(__callee__, args) - spawn.with_recursive!(*args) - end - - # Like #with_recursive but modifies the relation in place. - def with_recursive!(*args) # :nodoc: - self.with_values += args - @with_is_recursive = true - self - end - - private - - def build_with(arel) - return if with_values.empty? - - with_statements = with_values.map do |with_value| - raise ArgumentError, "Unsupported argument type: #{with_value} #{with_value.class}" unless with_value.is_a?(Hash) - - build_with_value_from_hash(with_value) - end - - # Was: arel.with(with_statements) - @with_is_recursive ? arel.with(:recursive, with_statements) : arel.with(with_statements) - end - - def build_with_value_from_hash(hash) - hash.map do |name, value| - Arel::Nodes::TableAlias.new(build_with_expression_from_value(value), name) - end - end - - def build_with_expression_from_value(value) - case value - when Arel::Nodes::SqlLiteral then Arel::Nodes::Grouping.new(value) - when ActiveRecord::Relation then value.arel - when Arel::SelectManager then value - when Array then value.map { |e| build_with_expression_from_value(e) }.reduce { |result, value| Arel::Nodes::UnionAll.new(result, value) } - else - raise ArgumentError, "Unsupported argument type: `#{value}` #{value.class}" - end - end - end -end - -ActiveSupport.on_load(:active_record) do - ActiveRecord::QueryMethods.prepend(ActiveRecord::QueryMethodsExtensions) -end diff --git a/lib/arel/union_parenthesizing.rb b/lib/arel/union_parenthesizing.rb deleted file mode 100644 index 852d8e92d8a4f3..00000000000000 --- a/lib/arel/union_parenthesizing.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -# Fix an issue with `LIMIT` ocurring on the left side of a `UNION` causing syntax errors. -# See https://github.com/rails/rails/issues/40181 - -# The fix has been merged in ActiveRecord: https://github.com/rails/rails/pull/51549 -# TODO: drop this when available in ActiveRecord - -# rubocop:disable all -- This is a mostly vendored file - -module Arel - module Visitors - class ToSql - private - - def infix_value_with_paren(o, collector, value, suppress_parens = false) - collector << "( " unless suppress_parens - collector = if o.left.class == o.class - infix_value_with_paren(o.left, collector, value, true) - else - select_parentheses o.left, collector, false # Changed from `visit o.left, collector` - end - collector << value - collector = if o.right.class == o.class - infix_value_with_paren(o.right, collector, value, true) - else - select_parentheses o.right, collector, false # Changed from `visit o.right, collector` - end - collector << " )" unless suppress_parens - collector - end - - def select_parentheses(o, collector, always_wrap_selects = true) - if o.is_a?(Nodes::SelectStatement) && (always_wrap_selects || require_parentheses?(o)) - collector << "(" - visit o, collector - collector << ")" - collector - else - visit o, collector - end - end - - def require_parentheses?(o) - !o.orders.empty? || o.limit || o.offset - end - end - end -end - -# rubocop:enable all diff --git a/lib/mastodon/cli/accounts.rb b/lib/mastodon/cli/accounts.rb index 23c907543f2884..c8e91224cf6712 100644 --- a/lib/mastodon/cli/accounts.rb +++ b/lib/mastodon/cli/accounts.rb @@ -321,7 +321,9 @@ def cull(*domains) unless skip_domains.empty? say('The following domains were not available during the check:', :yellow) - skip_domains.each { |domain| say(" #{domain}") } + shell.indent(2) do + skip_domains.each { |domain| say(domain) } + end end end diff --git a/lib/mastodon/cli/cache.rb b/lib/mastodon/cli/cache.rb index f32ab292ee29b2..cfb6cba1ff201f 100644 --- a/lib/mastodon/cli/cache.rb +++ b/lib/mastodon/cli/cache.rb @@ -52,7 +52,7 @@ def recount_account_stats(account) account.account_stat.tap do |account_stat| account_stat.following_count = account.active_relationships.count account_stat.followers_count = account.passive_relationships.count - account_stat.statuses_count = account.statuses.where.not(visibility: :direct).count + account_stat.statuses_count = account.statuses.not_direct_visibility.count account_stat.save if account_stat.changed? end @@ -60,7 +60,7 @@ def recount_account_stats(account) def recount_status_stats(status) status.status_stat.tap do |status_stat| - status_stat.replies_count = status.replies.where.not(visibility: :direct).count + status_stat.replies_count = status.replies.not_direct_visibility.count status_stat.reblogs_count = status.reblogs.count status_stat.favourites_count = status.favourites.count diff --git a/lib/mastodon/cli/email_domain_blocks.rb b/lib/mastodon/cli/email_domain_blocks.rb index 6b9107c8ade6a6..7b2da8ef9f4db0 100644 --- a/lib/mastodon/cli/email_domain_blocks.rb +++ b/lib/mastodon/cli/email_domain_blocks.rb @@ -7,11 +7,13 @@ module Mastodon::CLI class EmailDomainBlocks < Base desc 'list', 'List blocked e-mail domains' def list - EmailDomainBlock.where(parent_id: nil).find_each do |entry| - say(entry.domain.to_s, :white) + EmailDomainBlock.parents.find_each do |parent| + say(parent.domain.to_s, :white) - EmailDomainBlock.where(parent_id: entry.id).find_each do |child| - say(" #{child.domain}", :cyan) + shell.indent do + EmailDomainBlock.where(parent_id: parent.id).find_each do |child| + say(child.domain, :cyan) + end end end end diff --git a/lib/mastodon/cli/emoji.rb b/lib/mastodon/cli/emoji.rb index 4a8949de0e412c..206961f854898c 100644 --- a/lib/mastodon/cli/emoji.rb +++ b/lib/mastodon/cli/emoji.rb @@ -62,7 +62,9 @@ def import(path) failed += 1 say('Failure/Error: ', :red) say(entry.full_name) - say(" #{custom_emoji.errors[:image].join(', ')}", :red) + shell.indent(2) do + say(custom_emoji.errors[:image].join(', '), :red) + end end end end diff --git a/lib/mastodon/cli/ip_blocks.rb b/lib/mastodon/cli/ip_blocks.rb index ef24f2e047e30c..f1f40c99ce33d4 100644 --- a/lib/mastodon/cli/ip_blocks.rb +++ b/lib/mastodon/cli/ip_blocks.rb @@ -80,9 +80,9 @@ def remove(*addresses) end ip_blocks = if options[:force] - IpBlock.where('ip >>= ?', address) + IpBlock.containing(address) else - IpBlock.where('ip <<= ?', address) + IpBlock.contained_by(address) end if ip_blocks.empty? diff --git a/lib/mastodon/cli/media.rb b/lib/mastodon/cli/media.rb index 996b7fd92ca3b6..68420fb15d8839 100644 --- a/lib/mastodon/cli/media.rb +++ b/lib/mastodon/cli/media.rb @@ -6,6 +6,8 @@ module Mastodon::CLI class Media < Base include ActionView::Helpers::NumberHelper + class UnrecognizedOrphanType < StandardError; end + VALID_PATH_SEGMENTS_SIZE = [7, 10].freeze option :days, type: :numeric, default: 7, aliases: [:d] @@ -120,23 +122,10 @@ def remove_orphans object.acl.put(acl: s3_permissions) if options[:fix_permissions] && !dry_run? path_segments = object.key.split('/') - path_segments.delete('cache') - - unless VALID_PATH_SEGMENTS_SIZE.include?(path_segments.size) - progress.log(pastel.yellow("Unrecognized file found: #{object.key}")) - next - end - - model_name = path_segments.first.classify - attachment_name = path_segments[1].singularize - record_id = path_segments[2...-2].join.to_i - file_name = path_segments.last - record = record_map.dig(model_name, record_id) - attachment = record&.public_send(attachment_name) progress.increment - next unless attachment.blank? || !attachment.variant?(file_name) + next unless orphaned_file?(path_segments, record_map) begin object.delete unless dry_run? @@ -148,6 +137,8 @@ def remove_orphans rescue => e progress.log(pastel.red("Error processing #{object.key}: #{e}")) end + rescue UnrecognizedOrphanType + progress.log(pastel.yellow("Unrecognized file found: #{object.key}")) end end when :fog @@ -165,26 +156,10 @@ def remove_orphans key = path.gsub("#{root_path}#{File::SEPARATOR}", '') path_segments = key.split(File::SEPARATOR) - path_segments.delete('cache') - - unless VALID_PATH_SEGMENTS_SIZE.include?(path_segments.size) - progress.log(pastel.yellow("Unrecognized file found: #{key}")) - next - end - - model_name = path_segments.first.classify - record_id = path_segments[2...-2].join.to_i - attachment_name = path_segments[1].singularize - file_name = path_segments.last - - next unless PRELOADED_MODELS.include?(model_name) - - record = model_name.constantize.find_by(id: record_id) - attachment = record&.public_send(attachment_name) progress.increment - next unless attachment.blank? || !attachment.variant?(file_name) + next unless orphaned_file?(path_segments) begin size = File.size(path) @@ -205,6 +180,8 @@ def remove_orphans rescue => e progress.log(pastel.red("Error processing #{key}: #{e}")) end + rescue UnrecognizedOrphanType + progress.log(pastel.yellow("Unrecognized file found: #{path}")) end end @@ -364,5 +341,23 @@ def preload_records_from_mixed_objects(objects) model_map[model_name] = model_name.constantize.where(id: record_ids).index_by(&:id) end end + + def orphaned_file?(path_segments, record_map = nil) + path_segments.delete('cache') + + raise UnrecognizedOrphanType unless VALID_PATH_SEGMENTS_SIZE.include?(path_segments.size) + + model_name = path_segments.first.classify + record_id = path_segments[2...-2].join.to_i + attachment_name = path_segments[1].singularize + file_name = path_segments.last + + raise UnrecognizedOrphanType unless PRELOADED_MODELS.include?(model_name) + + record = record_map.present? ? record_map.dig(model_name, record_id) : model_name.constantize.find_by(id: record_id) + attachment = record&.public_send(attachment_name) + + attachment.blank? || !attachment.variant?(file_name) + end end end diff --git a/lib/mastodon/cli/progress_helper.rb b/lib/mastodon/cli/progress_helper.rb index 56343437967650..da9527ffd79083 100644 --- a/lib/mastodon/cli/progress_helper.rb +++ b/lib/mastodon/cli/progress_helper.rb @@ -5,7 +5,7 @@ Rails.logger = dev_null ActiveRecord::Base.logger = dev_null ActiveJob::Base.logger = dev_null -HttpLog.configuration.logger = dev_null +HttpLog.configuration.logger = dev_null if defined?(HttpLog) Paperclip.options[:log] = false Chewy.logger = dev_null diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index 2822f2eeb126ed..e1e2dd8194fcf0 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -596,7 +596,7 @@ def disable_log_stdout! Rails.logger = dev_null ActiveRecord::Base.logger = dev_null - HttpLog.configuration.logger = dev_null + HttpLog.configuration.logger = dev_null if defined?(HttpLog) Paperclip.options[:log] = false end diff --git a/lib/tasks/repo.rake b/lib/tasks/repo.rake index 539c44273f4a3a..c8f977f6512dc2 100644 --- a/lib/tasks/repo.rake +++ b/lib/tasks/repo.rake @@ -18,7 +18,7 @@ namespace :repo do url = "https://api.github.com/repos/#{REPOSITORY_NAME}/contributors?anon=1" - HttpLog.config.compact_log = true + HttpLog.config.compact_log = true if defined?(HttpLog) while url.present? response = HTTP.get(url) @@ -43,7 +43,7 @@ namespace :repo do path = Rails.root.join('CHANGELOG.md') tmp = Tempfile.new - HttpLog.config.compact_log = true + HttpLog.config.compact_log = true if defined?(HttpLog) begin File.open(path, 'r') do |file| diff --git a/lib/tasks/tests.rake b/lib/tasks/tests.rake index cb7fce31399f4d..74269439ddde83 100644 --- a/lib/tasks/tests.rake +++ b/lib/tasks/tests.rake @@ -11,7 +11,7 @@ namespace :tests do '3_3_0' => 2020_12_18_054746, }.each do |release, version| ActiveRecord::Tasks::DatabaseTasks - .migration_connection + .migration_connection_pool .migration_context .migrate(version) Rake::Task["tests:migrations:populate_v#{release}"] diff --git a/spec/lib/account_statuses_filter_spec.rb b/spec/lib/account_statuses_filter_spec.rb index 77745851248331..351d3dae574052 100644 --- a/spec/lib/account_statuses_filter_spec.rb +++ b/spec/lib/account_statuses_filter_spec.rb @@ -52,36 +52,24 @@ def status_with_media_attachment!(visibility) end shared_examples 'filter params' do - context 'with only_media param' do - let(:params) { { only_media: true } } + it 'respects param options in results' do + expect(results_for(only_media: true)) + .to all(satisfy(&:with_media?)) - it 'returns only statuses with media' do - expect(subject.all?(&:with_media?)).to be true - end - end + expect(results_for(tagged: tag.name)) + .to all(satisfy { |status| status.tags.include?(tag) }) - context 'with tagged param' do - let(:params) { { tagged: tag.name } } + expect(results_for(exclude_replies: true)) + .to all(satisfy { |status| !status.reply? }) - it 'returns only statuses with tag' do - expect(subject.all? { |s| s.tags.include?(tag) }).to be true - end + expect(results_for(exclude_reblogs: true)) + .to all(satisfy { |status| !status.reblog? }) end - context 'with exclude_replies param' do - let(:params) { { exclude_replies: true } } - - it 'returns only statuses that are not replies' do - expect(subject.none?(&:reply?)).to be true - end - end - - context 'with exclude_reblogs param' do - let(:params) { { exclude_reblogs: true } } - - it 'returns only statuses that are not reblogs' do - expect(subject.none?(&:reblog?)).to be true - end + def results_for(params) + described_class + .new(account, current_account, params) + .results end end diff --git a/spec/lib/admin/system_check/database_schema_check_spec.rb b/spec/lib/admin/system_check/database_schema_check_spec.rb index 311d5249563ea2..3dc5763f92d0fb 100644 --- a/spec/lib/admin/system_check/database_schema_check_spec.rb +++ b/spec/lib/admin/system_check/database_schema_check_spec.rb @@ -13,7 +13,7 @@ context 'when database needs migration' do before do context = instance_double(ActiveRecord::MigrationContext, needs_migration?: true) - allow(ActiveRecord::Base.connection).to receive(:migration_context).and_return(context) + allow(ActiveRecord::Base.connection_pool).to receive(:migration_context).and_return(context) end it 'returns false' do @@ -24,7 +24,7 @@ context 'when database does not need migration' do before do context = instance_double(ActiveRecord::MigrationContext, needs_migration?: false) - allow(ActiveRecord::Base.connection).to receive(:migration_context).and_return(context) + allow(ActiveRecord::Base.connection_pool).to receive(:migration_context).and_return(context) end it 'returns true' do diff --git a/spec/lib/annual_report/percentiles_spec.rb b/spec/lib/annual_report/percentiles_spec.rb index 1d1df3166b78e9..11df81cfb6c80a 100644 --- a/spec/lib/annual_report/percentiles_spec.rb +++ b/spec/lib/annual_report/percentiles_spec.rb @@ -4,17 +4,20 @@ RSpec.describe AnnualReport::Percentiles do describe '#generate' do - subject { described_class.new(account, Time.zone.now.year) } + subject { described_class.new(account, year) } + + let(:year) { Time.zone.now.year } context 'with an inactive account' do let(:account) { Fabricate :account } it 'builds a report for an account' do + described_class.prepare(year) + expect(subject.generate) .to include( percentiles: include( - followers: 0, - statuses: 0 + statuses: 100 ) ) end @@ -25,16 +28,15 @@ before do Fabricate.times 2, :status # Others as `account` - Fabricate.times 2, :follow # Others as `target_account` Fabricate.times 2, :status, account: account - Fabricate.times 2, :follow, target_account: account end it 'builds a report for an account' do + described_class.prepare(year) + expect(subject.generate) .to include( percentiles: include( - followers: 50, statuses: 50 ) ) diff --git a/spec/models/featured_tag_spec.rb b/spec/models/featured_tag_spec.rb index 0f5ead8f978ac1..20059cfba4c638 100644 --- a/spec/models/featured_tag_spec.rb +++ b/spec/models/featured_tag_spec.rb @@ -126,16 +126,54 @@ end describe '#decrement' do - it 'decreases the count and updates the last_status_at timestamp' do - tag = Fabricate :tag, name: 'test' - status = Fabricate :status, visibility: :public, created_at: 10.days.ago - status.tags << tag + let(:tag) { Fabricate(:tag, name: 'test') } + let(:account) { Fabricate(:account) } + let(:featured_tag) { Fabricate(:featured_tag, name: 'test', account: account) } - featured_tag = Fabricate :featured_tag, name: 'test', account: status.account + context 'when removing the last status using the tag' do + let(:status) { Fabricate(:status, visibility: :public, account: account, created_at: 10.days.ago) } - expect { featured_tag.decrement(status.id) } - .to change(featured_tag, :statuses_count).from(1).to(0) - .and change(featured_tag, :last_status_at).to(nil) + before do + status.tags << tag + end + + it 'decreases the count and updates the last_status_at timestamp' do + expect { featured_tag.decrement(status) } + .to change(featured_tag, :statuses_count).from(1).to(0) + .and change(featured_tag, :last_status_at).to(nil) + end + end + + context 'when removing a previous status using the tag' do + let(:previous_status) { Fabricate(:status, visibility: :public, account: account, created_at: 1.month.ago) } + let(:status) { Fabricate(:status, visibility: :public, account: account, created_at: 10.days.ago) } + + before do + previous_status.tags << tag + status.tags << tag + end + + it 'decreases the count and updates the last_status_at timestamp' do + expect { featured_tag.decrement(previous_status) } + .to change(featured_tag, :statuses_count).from(2).to(1) + .and not_change(featured_tag, :last_status_at) + end + end + + context 'when removing the most recent use of the tag' do + let(:previous_status) { Fabricate(:status, visibility: :public, account: account, created_at: 1.month.ago) } + let(:status) { Fabricate(:status, visibility: :public, account: account, created_at: 10.days.ago) } + + before do + previous_status.tags << tag + status.tags << tag + end + + it 'decreases the count and updates the last_status_at timestamp' do + expect { featured_tag.decrement(status) } + .to change(featured_tag, :statuses_count).from(2).to(1) + .and change(featured_tag, :last_status_at) + end end end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index d2ad40be7310fb..574d5438442fcc 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -32,7 +32,7 @@ STREAMING_PORT = ENV.fetch('TEST_STREAMING_PORT', '4020') ENV['STREAMING_API_BASE_URL'] = "http://localhost:#{STREAMING_PORT}" -require File.expand_path('../config/environment', __dir__) +require_relative '../config/environment' abort('The Rails environment is running in production mode!') if Rails.env.production? diff --git a/spec/serializers/activitypub/actor_serializer_spec.rb b/spec/serializers/activitypub/actor_serializer_spec.rb new file mode 100644 index 00000000000000..ad2445595375ac --- /dev/null +++ b/spec/serializers/activitypub/actor_serializer_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::ActorSerializer do + subject { serialized_record_json(record, described_class) } + + describe '#type' do + context 'with the instance actor' do + let(:record) { Account.find(Account::INSTANCE_ACTOR_ID) } + + it { is_expected.to include('type' => 'Application') } + end + + context 'with an application actor' do + let(:record) { Fabricate :account, actor_type: 'Application' } + + it { is_expected.to include('type' => 'Service') } + end + + context 'with a service actor' do + let(:record) { Fabricate :account, actor_type: 'Service' } + + it { is_expected.to include('type' => 'Service') } + end + + context 'with a Group actor' do + let(:record) { Fabricate :account, actor_type: 'Group' } + + it { is_expected.to include('type' => 'Group') } + end + + context 'with a Person actor' do + let(:record) { Fabricate :account, actor_type: 'Person' } + + it { is_expected.to include('type' => 'Person') } + end + end +end diff --git a/spec/serializers/activitypub/add_serializer_spec.rb b/spec/serializers/activitypub/add_serializer_spec.rb new file mode 100644 index 00000000000000..3b3eaeb1b015b1 --- /dev/null +++ b/spec/serializers/activitypub/add_serializer_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::AddSerializer do + describe '.serializer_for' do + subject { described_class.serializer_for(model, {}) } + + context 'with a Status model' do + let(:model) { Status.new } + + it { is_expected.to eq(described_class::UriSerializer) } + end + + context 'with a FeaturedTag model' do + let(:model) { FeaturedTag.new } + + it { is_expected.to eq(ActivityPub::HashtagSerializer) } + end + + context 'with an Array' do + let(:model) { [] } + + it { is_expected.to eq(ActiveModel::Serializer::CollectionSerializer) } + end + end +end diff --git a/spec/serializers/activitypub/collection_serializer_spec.rb b/spec/serializers/activitypub/collection_serializer_spec.rb new file mode 100644 index 00000000000000..7726df914f258c --- /dev/null +++ b/spec/serializers/activitypub/collection_serializer_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::CollectionSerializer do + describe '.serializer_for' do + subject { described_class.serializer_for(model, {}) } + + context 'with a Status model' do + let(:model) { Status.new } + + it { is_expected.to eq(ActivityPub::NoteSerializer) } + end + + context 'with a FeaturedTag model' do + let(:model) { FeaturedTag.new } + + it { is_expected.to eq(ActivityPub::HashtagSerializer) } + end + + context 'with an ActivityPub::CollectionPresenter' do + let(:model) { ActivityPub::CollectionPresenter.new } + + it { is_expected.to eq(described_class) } + end + + context 'with a String' do + let(:model) { '' } + + it { is_expected.to eq(described_class::StringSerializer) } + end + + context 'with an Array' do + let(:model) { [] } + + it { is_expected.to eq(ActiveModel::Serializer::CollectionSerializer) } + end + end +end diff --git a/spec/serializers/activitypub/remove_serializer_spec.rb b/spec/serializers/activitypub/remove_serializer_spec.rb new file mode 100644 index 00000000000000..0e4b199838e629 --- /dev/null +++ b/spec/serializers/activitypub/remove_serializer_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::RemoveSerializer do + describe '.serializer_for' do + subject { described_class.serializer_for(model, {}) } + + context 'with a Status model' do + let(:model) { Status.new } + + it { is_expected.to eq(described_class::UriSerializer) } + end + + context 'with a FeaturedTag model' do + let(:model) { FeaturedTag.new } + + it { is_expected.to eq(ActivityPub::HashtagSerializer) } + end + + context 'with an Array' do + let(:model) { [] } + + it { is_expected.to eq(ActiveModel::Serializer::CollectionSerializer) } + end + end +end diff --git a/spec/serializers/rest/account_relationship_severance_event_serializer_spec.rb b/spec/serializers/rest/account_relationship_severance_event_serializer_spec.rb new file mode 100644 index 00000000000000..95b4d7dcd2a6b0 --- /dev/null +++ b/spec/serializers/rest/account_relationship_severance_event_serializer_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe REST::AccountRelationshipSeveranceEventSerializer do + subject { serialized_record_json(record, described_class) } + + let(:record) { Fabricate.build :account_relationship_severance_event, id: 123 } + + describe 'serialization' do + it 'returns expected values' do + expect(subject) + .to include( + 'id' => be_a(String).and(eq('123')) + ) + end + end +end diff --git a/spec/serializers/rest/account_warning_serializer_spec.rb b/spec/serializers/rest/account_warning_serializer_spec.rb new file mode 100644 index 00000000000000..0f335d12159f43 --- /dev/null +++ b/spec/serializers/rest/account_warning_serializer_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe REST::AccountWarningSerializer do + subject { serialized_record_json(record, described_class) } + + let(:record) { Fabricate :account_warning, id: 123, status_ids: [456, 789] } + + describe 'serialization' do + it 'returns expected values' do + expect(subject) + .to include( + 'id' => be_a(String).and(eq('123')), + 'status_ids' => be_a(Array).and(eq(['456', '789'])) + ) + end + end +end diff --git a/spec/serializers/rest/admin/account_serializer_spec.rb b/spec/serializers/rest/admin/account_serializer_spec.rb new file mode 100644 index 00000000000000..58f58a997b17c8 --- /dev/null +++ b/spec/serializers/rest/admin/account_serializer_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe REST::Admin::AccountSerializer do + subject { serialized_record_json(record, described_class) } + + describe 'created_by_application_id' do + context 'when account is application-created' do + let(:record) { Fabricate :account, user: Fabricate(:user, created_by_application: application) } + let(:application) { Fabricate :application } + + it { is_expected.to include('created_by_application_id' => application.id.to_s) } + end + end + + describe 'invited_by_account_id' do + context 'when account was invited' do + let(:record) { Fabricate :account, user: Fabricate(:user, invite: invite) } + let(:invite) { Fabricate :invite } + + it { is_expected.to include('invited_by_account_id' => invite.user.account.id.to_s) } + end + end +end diff --git a/spec/serializers/rest/admin/cohort_serializer_spec.rb b/spec/serializers/rest/admin/cohort_serializer_spec.rb new file mode 100644 index 00000000000000..ed6067c0d21f07 --- /dev/null +++ b/spec/serializers/rest/admin/cohort_serializer_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe REST::Admin::CohortSerializer do + subject { serialized_record_json(record, described_class) } + + let(:record) { Admin::Metrics::Retention.new('2024-01-01', '2024-01-02', 'day').cohorts.first } + + describe 'serialization' do + it 'returns expected values' do + expect(subject) + .to include( + 'data' => be_a(Array), + 'period' => /2024-01-01/ + ) + end + end +end diff --git a/spec/serializers/rest/admin/webhook_event_serializer_spec.rb b/spec/serializers/rest/admin/webhook_event_serializer_spec.rb new file mode 100644 index 00000000000000..3cbfbd92a316d3 --- /dev/null +++ b/spec/serializers/rest/admin/webhook_event_serializer_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe REST::Admin::WebhookEventSerializer do + describe '.serializer_for' do + subject { described_class.serializer_for(model, {}) } + + context 'with an Account model' do + let(:model) { Account.new } + + it { is_expected.to eq(REST::Admin::AccountSerializer) } + end + + context 'with a Report model' do + let(:model) { Report.new } + + it { is_expected.to eq(REST::Admin::ReportSerializer) } + end + + context 'with a Status model' do + let(:model) { Status.new } + + it { is_expected.to eq(REST::StatusSerializer) } + end + + context 'with an Array' do + let(:model) { [] } + + it { is_expected.to eq(ActiveModel::Serializer::CollectionSerializer) } + end + end +end diff --git a/spec/serializers/rest/appeal_serializer_spec.rb b/spec/serializers/rest/appeal_serializer_spec.rb new file mode 100644 index 00000000000000..1ae6617de5776a --- /dev/null +++ b/spec/serializers/rest/appeal_serializer_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe REST::AppealSerializer do + subject { serialized_record_json(record, described_class) } + + describe 'state' do + context 'when appeal is approved' do + let(:record) { Fabricate.build :appeal, approved_at: 2.days.ago } + + it { is_expected.to include('state' => 'approved') } + end + + context 'when appeal is rejected' do + let(:record) { Fabricate.build :appeal, rejected_at: 2.days.ago } + + it { is_expected.to include('state' => 'rejected') } + end + + context 'when appeal is not approved or rejected' do + let(:record) { Fabricate.build :appeal, approved_at: nil, rejected_at: nil } + + it { is_expected.to include('state' => 'pending') } + end + end +end diff --git a/spec/serializers/rest/custom_emoji_serializer_spec.rb b/spec/serializers/rest/custom_emoji_serializer_spec.rb new file mode 100644 index 00000000000000..a694ca515a5568 --- /dev/null +++ b/spec/serializers/rest/custom_emoji_serializer_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe REST::CustomEmojiSerializer do + subject { serialized_record_json(record, described_class) } + + let(:record) { Fabricate.build :custom_emoji, id: 123, category: Fabricate(:custom_emoji_category, name: 'Category Name') } + + describe 'serialization' do + it 'returns expected values' do + expect(subject) + .to include( + 'category' => be_a(String).and(eq('Category Name')) + ) + end + end +end diff --git a/spec/serializers/rest/extended_description_serializer_spec.rb b/spec/serializers/rest/extended_description_serializer_spec.rb new file mode 100644 index 00000000000000..d7bd078e1d6f02 --- /dev/null +++ b/spec/serializers/rest/extended_description_serializer_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe REST::ExtendedDescriptionSerializer do + subject { serialized_record_json(record, described_class) } + + describe 'serialization' do + context 'with text present' do + let(:record) { ExtendedDescription.new text: 'Hello world', updated_at: Date.new(2024, 1, 1) } + + it 'returns expected values' do + expect(subject) + .to include( + 'content' => eq(<<~HTML), +

Hello world

+ HTML + 'updated_at' => eq('2024-01-01') + ) + end + end + + context 'with text missing' do + let(:record) { ExtendedDescription.new text: nil, updated_at: Date.new(2024, 1, 1) } + + it 'returns expected values' do + expect(subject) + .to include( + 'content' => eq(''), + 'updated_at' => eq('2024-01-01') + ) + end + end + end +end diff --git a/spec/serializers/rest/rule_serializer_spec.rb b/spec/serializers/rest/rule_serializer_spec.rb new file mode 100644 index 00000000000000..4d801e77d36b9c --- /dev/null +++ b/spec/serializers/rest/rule_serializer_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe REST::RuleSerializer do + subject { serialized_record_json(record, described_class) } + + let(:record) { Fabricate.build :rule, id: 123 } + + describe 'serialization' do + it 'returns expected values' do + expect(subject) + .to include( + 'id' => be_a(String).and(eq('123')) + ) + end + end +end diff --git a/yarn.lock b/yarn.lock index af8f7119ce485a..36e799c5623161 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1463,13 +1463,13 @@ __metadata: languageName: node linkType: hard -"@csstools/cascade-layer-name-parser@npm:^2.0.3": - version: 2.0.3 - resolution: "@csstools/cascade-layer-name-parser@npm:2.0.3" +"@csstools/cascade-layer-name-parser@npm:^2.0.4": + version: 2.0.4 + resolution: "@csstools/cascade-layer-name-parser@npm:2.0.4" peerDependencies: - "@csstools/css-parser-algorithms": ^3.0.3 - "@csstools/css-tokenizer": ^3.0.2 - checksum: 10c0/a0f0d6c94122d0a82b0da94ee2619915a9e6e98211de6e1f28afebe34332a29cab1b7905a34b6e5c1df30a125df9f9860ce5fa960c676484417d4ef2c4db4757 + "@csstools/css-parser-algorithms": ^3.0.4 + "@csstools/css-tokenizer": ^3.0.3 + checksum: 10c0/774f2bcc96a576183853191bdfd31df15e22c51901ee01678ee47f1d1afcb4ab0e6d9a78e08f7383ac089c7e0b390013633f45ff1f1d577c9aefd252589bcced languageName: node linkType: hard @@ -1480,42 +1480,42 @@ __metadata: languageName: node linkType: hard -"@csstools/css-calc@npm:^2.0.3": - version: 2.0.3 - resolution: "@csstools/css-calc@npm:2.0.3" +"@csstools/css-calc@npm:^2.0.4": + version: 2.0.4 + resolution: "@csstools/css-calc@npm:2.0.4" peerDependencies: - "@csstools/css-parser-algorithms": ^3.0.3 - "@csstools/css-tokenizer": ^3.0.2 - checksum: 10c0/a3dacda66bde67edab0f2384e85462dcd46b5bb62bc2a6396f0577a006d9ad59eae270353cbd6fe1a5f7081d0546fe4b1fd9b2df242758da54ec8b7022296570 + "@csstools/css-parser-algorithms": ^3.0.4 + "@csstools/css-tokenizer": ^3.0.3 + checksum: 10c0/ecb68fc9f8d930b861d5c9c5d4fc6eb43dabc619a4cd6c82dc18034ee746f907dba106fa6cdb909ac727fbfc9b1f7ceac1f356641542faeb2fab99c5fcd52a2e languageName: node linkType: hard -"@csstools/css-color-parser@npm:^3.0.4": - version: 3.0.4 - resolution: "@csstools/css-color-parser@npm:3.0.4" +"@csstools/css-color-parser@npm:^3.0.5": + version: 3.0.5 + resolution: "@csstools/css-color-parser@npm:3.0.5" dependencies: "@csstools/color-helpers": "npm:^5.0.1" - "@csstools/css-calc": "npm:^2.0.3" + "@csstools/css-calc": "npm:^2.0.4" peerDependencies: - "@csstools/css-parser-algorithms": ^3.0.3 - "@csstools/css-tokenizer": ^3.0.2 - checksum: 10c0/8f05264254d3768e45e46ce10e9355fe8ca0f6e4d2f648a22572f46c6d05b2378c37c2e5d970ed3b7bdfa0b152ea2af0c56a0556249bd85973aee9db7b091aa7 + "@csstools/css-parser-algorithms": ^3.0.4 + "@csstools/css-tokenizer": ^3.0.3 + checksum: 10c0/535e2a38222ad7ee9e1143c3ef09ba07b2e70a4214bbdf7ca13398388b8d2db9ee295fe15d9d306342036e554d97dd235567a8c8ee03fea6825483f1c4dbbf7e languageName: node linkType: hard -"@csstools/css-parser-algorithms@npm:^3.0.1, @csstools/css-parser-algorithms@npm:^3.0.3": - version: 3.0.3 - resolution: "@csstools/css-parser-algorithms@npm:3.0.3" +"@csstools/css-parser-algorithms@npm:^3.0.1, @csstools/css-parser-algorithms@npm:^3.0.4": + version: 3.0.4 + resolution: "@csstools/css-parser-algorithms@npm:3.0.4" peerDependencies: - "@csstools/css-tokenizer": ^3.0.2 - checksum: 10c0/c1e634384affb10c3726a36f090867247005a2a470cb223de88debeab07921b78c0e7875e7847d90949c2b0ba88a290c71565f1839f7739c21a5bf1c870e137d + "@csstools/css-tokenizer": ^3.0.3 + checksum: 10c0/d411f07765e14eede17bccc6bd4f90ff303694df09aabfede3fd104b2dfacfd4fe3697cd25ddad14684c850328f3f9420ebfa9f78380892492974db24ae47dbd languageName: node linkType: hard -"@csstools/css-tokenizer@npm:^3.0.1, @csstools/css-tokenizer@npm:^3.0.2": - version: 3.0.2 - resolution: "@csstools/css-tokenizer@npm:3.0.2" - checksum: 10c0/a74e5829420ed35982fd33be272c2a19cb2380179d357abe750aa848be6d6699d0437008f47a57eb7c6ff64a34b0c8f91a97dd63dbddd08249b7cf7983767e5e +"@csstools/css-tokenizer@npm:^3.0.1, @csstools/css-tokenizer@npm:^3.0.3": + version: 3.0.3 + resolution: "@csstools/css-tokenizer@npm:3.0.3" + checksum: 10c0/c31bf410e1244b942e71798e37c54639d040cb59e0121b21712b40015fced2b0fb1ffe588434c5f8923c9cd0017cfc1c1c8f3921abc94c96edf471aac2eba5e5 languageName: node linkType: hard @@ -1529,13 +1529,13 @@ __metadata: languageName: node linkType: hard -"@csstools/media-query-list-parser@npm:^4.0.1": - version: 4.0.1 - resolution: "@csstools/media-query-list-parser@npm:4.0.1" +"@csstools/media-query-list-parser@npm:^4.0.2": + version: 4.0.2 + resolution: "@csstools/media-query-list-parser@npm:4.0.2" peerDependencies: - "@csstools/css-parser-algorithms": ^3.0.3 - "@csstools/css-tokenizer": ^3.0.2 - checksum: 10c0/037f57520b6ca7d58ba1d71a477bd99bdbe3a6d8e7b972ed0f436d649e574f5b429e54fc47d0272daa2ac24ffb819bd9d260a90e154c4a265b8ee4d1cfb1d279 + "@csstools/css-parser-algorithms": ^3.0.4 + "@csstools/css-tokenizer": ^3.0.3 + checksum: 10c0/5d008a70f5d4fd96224066a433f5cdefa76cfd78a74416a20d6d5b2bb1bc8282b140e8373015d807d4dadb91daf3deb73eb13f853ec4e0479d0cb92e80c6f20d languageName: node linkType: hard @@ -1551,60 +1551,60 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-color-function@npm:^4.0.4": - version: 4.0.4 - resolution: "@csstools/postcss-color-function@npm:4.0.4" +"@csstools/postcss-color-function@npm:^4.0.5": + version: 4.0.5 + resolution: "@csstools/postcss-color-function@npm:4.0.5" dependencies: - "@csstools/css-color-parser": "npm:^3.0.4" - "@csstools/css-parser-algorithms": "npm:^3.0.3" - "@csstools/css-tokenizer": "npm:^3.0.2" + "@csstools/css-color-parser": "npm:^3.0.5" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/e7735c6d7c84f039c84dc1b180410aa4db7c404d55477c2e7872bc8306a3150bf37883583ec080ebd8b40b765f700cc78892d61dc167578f76e324543d551c04 + checksum: 10c0/b47cc4567445eb12dd3e67a4298470d62a449a4d754fd4dd32eb78b23615ba6bd1fe8dacccc6c95087ce8749a6552bace8de0b9b6eee0c9a7b7202a0bcb8fcac languageName: node linkType: hard -"@csstools/postcss-color-mix-function@npm:^3.0.4": - version: 3.0.4 - resolution: "@csstools/postcss-color-mix-function@npm:3.0.4" +"@csstools/postcss-color-mix-function@npm:^3.0.5": + version: 3.0.5 + resolution: "@csstools/postcss-color-mix-function@npm:3.0.5" dependencies: - "@csstools/css-color-parser": "npm:^3.0.4" - "@csstools/css-parser-algorithms": "npm:^3.0.3" - "@csstools/css-tokenizer": "npm:^3.0.2" + "@csstools/css-color-parser": "npm:^3.0.5" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/3e01f04853e49c9f69ee0538b817209b630649844039ee5b195cfbae94d41cf6e0d8a50a3d02dbe066f84855eefa1b7047f6750e4744858519978468577e567f + checksum: 10c0/a17d20dcc42d2c1ccd7a5ceb8d1413133ae9461bf973a9aad997a5782bf4cbbd847aef06accc20fa391ac6fbd44a1003e18c30a621d7c77052d4256fe39619aa languageName: node linkType: hard -"@csstools/postcss-content-alt-text@npm:^2.0.3": - version: 2.0.3 - resolution: "@csstools/postcss-content-alt-text@npm:2.0.3" +"@csstools/postcss-content-alt-text@npm:^2.0.4": + version: 2.0.4 + resolution: "@csstools/postcss-content-alt-text@npm:2.0.4" dependencies: - "@csstools/css-parser-algorithms": "npm:^3.0.3" - "@csstools/css-tokenizer": "npm:^3.0.2" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/0389292a1ba11483f58db1bdd38cc774b0ec18c73f03d1db5f4a1f38edc861bb8b8d750b5c5b3615a98aaf995c0464dcfd6db5421888be0f868548c69455b5b3 + checksum: 10c0/84caccedd8a519df434babd58b14104c5a92cd326057ce509bdbaa2a4bb3130afb1c1456caf30235ba14da52d1628a5411ea4f5d2fb558d603d234f795538017 languageName: node linkType: hard -"@csstools/postcss-exponential-functions@npm:^2.0.3": - version: 2.0.3 - resolution: "@csstools/postcss-exponential-functions@npm:2.0.3" +"@csstools/postcss-exponential-functions@npm:^2.0.4": + version: 2.0.4 + resolution: "@csstools/postcss-exponential-functions@npm:2.0.4" dependencies: - "@csstools/css-calc": "npm:^2.0.3" - "@csstools/css-parser-algorithms": "npm:^3.0.3" - "@csstools/css-tokenizer": "npm:^3.0.2" + "@csstools/css-calc": "npm:^2.0.4" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" peerDependencies: postcss: ^8.4 - checksum: 10c0/690ac6c5ec72e085160401100cc8465a040fd70d59efadf61ef767094edac28bbb2653f53ea7cfa47d8220d0be0b9e23c3e3c2b80d9e93ad07b1db8031163d70 + checksum: 10c0/b174faa8782b7b3f3a9636412f90963740ff033c6506acf662f002fa07f59b370eb23937dbe3f1a05d7df277373f662d6970237d34ae938cf282a0b8dbde9a47 languageName: node linkType: hard @@ -1620,46 +1620,46 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-gamut-mapping@npm:^2.0.4": - version: 2.0.4 - resolution: "@csstools/postcss-gamut-mapping@npm:2.0.4" +"@csstools/postcss-gamut-mapping@npm:^2.0.5": + version: 2.0.5 + resolution: "@csstools/postcss-gamut-mapping@npm:2.0.5" dependencies: - "@csstools/css-color-parser": "npm:^3.0.4" - "@csstools/css-parser-algorithms": "npm:^3.0.3" - "@csstools/css-tokenizer": "npm:^3.0.2" + "@csstools/css-color-parser": "npm:^3.0.5" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" peerDependencies: postcss: ^8.4 - checksum: 10c0/60c6ff13cc71a0cda7ce184fa8b42924975f2bd05e3bc096059de853ae01d48e36f27d02d7e6286d9e8c4eb4dfd526f7c302ff51be19c5f7949dd6079915a3a9 + checksum: 10c0/a9d7365bed5c323dddad06d53c137ef3508870e541c6275dbdf3a73a50d44437830c22fa62c510d9316441d1e9f3b5552730778d1b73d083422245673bd00dac languageName: node linkType: hard -"@csstools/postcss-gradients-interpolation-method@npm:^5.0.4": - version: 5.0.4 - resolution: "@csstools/postcss-gradients-interpolation-method@npm:5.0.4" +"@csstools/postcss-gradients-interpolation-method@npm:^5.0.5": + version: 5.0.5 + resolution: "@csstools/postcss-gradients-interpolation-method@npm:5.0.5" dependencies: - "@csstools/css-color-parser": "npm:^3.0.4" - "@csstools/css-parser-algorithms": "npm:^3.0.3" - "@csstools/css-tokenizer": "npm:^3.0.2" + "@csstools/css-color-parser": "npm:^3.0.5" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/3b6aa4404851be09a083f35b596acc7e3f74f525386dcd2887720438638ca1893f79e699e5dad0e3a35487cb9527e08d0dcd29f1331f87800f53c13423321d6e + checksum: 10c0/0ef258478270005ab3a879132dc6dbf2bf80788c79861515c826607953c551df1c2ef7005ba9592c31c6dad332d12ee247928a9fdd1569a51927a18a75dcc865 languageName: node linkType: hard -"@csstools/postcss-hwb-function@npm:^4.0.4": - version: 4.0.4 - resolution: "@csstools/postcss-hwb-function@npm:4.0.4" +"@csstools/postcss-hwb-function@npm:^4.0.5": + version: 4.0.5 + resolution: "@csstools/postcss-hwb-function@npm:4.0.5" dependencies: - "@csstools/css-color-parser": "npm:^3.0.4" - "@csstools/css-parser-algorithms": "npm:^3.0.3" - "@csstools/css-tokenizer": "npm:^3.0.2" + "@csstools/css-color-parser": "npm:^3.0.5" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/2460cc7e6324f0793d12976de175ff33344c3410ff7a04ca8a5ce0560ee3354d1d8034f9f07c0759c2b7e222a4681d02298f6a29c2a326c76b33060830da83b4 + checksum: 10c0/9ce0a66bf288c9ec7f1fc28323d56ec54d38a30cb2ab722d1afbd46ec070cc5e61121bacb503803cb00293fc5ed117269969ac21071d7f4ce94b385c96168445 languageName: node linkType: hard @@ -1697,17 +1697,17 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-light-dark-function@npm:^2.0.6": - version: 2.0.6 - resolution: "@csstools/postcss-light-dark-function@npm:2.0.6" +"@csstools/postcss-light-dark-function@npm:^2.0.7": + version: 2.0.7 + resolution: "@csstools/postcss-light-dark-function@npm:2.0.7" dependencies: - "@csstools/css-parser-algorithms": "npm:^3.0.3" - "@csstools/css-tokenizer": "npm:^3.0.2" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/f3e93b58a23f0f01c9d032a892070a18e3393e60997e1d2b57c5bddb2cb36f7ee6cbaeaeb3d531b065a545a03e54d86567d34b45eddf85ed48ac360eb4ab66cd + checksum: 10c0/c116bfd2d3f4d0caabdedf8954c2a25908ffb29f9bbe2c57d44a2974277c7e46ee79862eea848385dc040275d343f2330350394a2095ec30f0aa17f72e2f4e39 languageName: node linkType: hard @@ -1749,42 +1749,42 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-logical-viewport-units@npm:^3.0.2": - version: 3.0.2 - resolution: "@csstools/postcss-logical-viewport-units@npm:3.0.2" +"@csstools/postcss-logical-viewport-units@npm:^3.0.3": + version: 3.0.3 + resolution: "@csstools/postcss-logical-viewport-units@npm:3.0.3" dependencies: - "@csstools/css-tokenizer": "npm:^3.0.2" + "@csstools/css-tokenizer": "npm:^3.0.3" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/31f525e774bc053f545a159eb53bb21465ea2930118e87c40207ad90fa97d3151e6de83efd02f84803fb0e93ed4a4b42a3904b734423410e73ac4c6ce9666ab4 + checksum: 10c0/8ec746598d7ce8697c3dafd83cb3a319a90079ad755dd78e3ec92f4ba9ad849c4cdaba33b16e9dcbac1e9489b3d7c48262030110c20ce1d88cdacbe9f5987cec languageName: node linkType: hard -"@csstools/postcss-media-minmax@npm:^2.0.3": - version: 2.0.3 - resolution: "@csstools/postcss-media-minmax@npm:2.0.3" +"@csstools/postcss-media-minmax@npm:^2.0.4": + version: 2.0.4 + resolution: "@csstools/postcss-media-minmax@npm:2.0.4" dependencies: - "@csstools/css-calc": "npm:^2.0.3" - "@csstools/css-parser-algorithms": "npm:^3.0.3" - "@csstools/css-tokenizer": "npm:^3.0.2" - "@csstools/media-query-list-parser": "npm:^4.0.1" + "@csstools/css-calc": "npm:^2.0.4" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" + "@csstools/media-query-list-parser": "npm:^4.0.2" peerDependencies: postcss: ^8.4 - checksum: 10c0/87c1ed6fe6ed487125e383ae7bc356ba0f68885c41cd129b85c323af69255031a8ac7b2e994d4f9b6b65d6a9f6833aeb1301ca230b592547825aedb3e3acab4b + checksum: 10c0/f9fe1e99b83a0724250f9426335da6f13bfbc28be6d10ebfafbf037766fc5245240ec33be7e68c9c5b60c013fbc2fa0be96fbed0d41383c919ee9e6f1b8225ce languageName: node linkType: hard -"@csstools/postcss-media-queries-aspect-ratio-number-values@npm:^3.0.3": - version: 3.0.3 - resolution: "@csstools/postcss-media-queries-aspect-ratio-number-values@npm:3.0.3" +"@csstools/postcss-media-queries-aspect-ratio-number-values@npm:^3.0.4": + version: 3.0.4 + resolution: "@csstools/postcss-media-queries-aspect-ratio-number-values@npm:3.0.4" dependencies: - "@csstools/css-parser-algorithms": "npm:^3.0.3" - "@csstools/css-tokenizer": "npm:^3.0.2" - "@csstools/media-query-list-parser": "npm:^4.0.1" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" + "@csstools/media-query-list-parser": "npm:^4.0.2" peerDependencies: postcss: ^8.4 - checksum: 10c0/3bbadd5c79ff2102b4a2e0f107d7f3cbb89d2840b935915531d0a1bfc711d58386e6e234fccb9b4abb37e48f07cdfaa4dd66b5c795dfac9e317fa8b5cfd38e06 + checksum: 10c0/27dc9419b0f4315774647588f599348e7cc593984f59b414c51c910066501fd087cbe232deb762907c18bd21dd4184e7b6e0e0b730e5c72341ab9cc696c75739 languageName: node linkType: hard @@ -1811,18 +1811,18 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-oklab-function@npm:^4.0.4": - version: 4.0.4 - resolution: "@csstools/postcss-oklab-function@npm:4.0.4" +"@csstools/postcss-oklab-function@npm:^4.0.5": + version: 4.0.5 + resolution: "@csstools/postcss-oklab-function@npm:4.0.5" dependencies: - "@csstools/css-color-parser": "npm:^3.0.4" - "@csstools/css-parser-algorithms": "npm:^3.0.3" - "@csstools/css-tokenizer": "npm:^3.0.2" + "@csstools/css-color-parser": "npm:^3.0.5" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/4fd2a5c22ebaf69053e005d9d009ccb41b6879f0246677159bfb6a28208e90af857446c443d34fe3efdaf50bccdd67b16fcd0c1e7c52961396a48bd84e45311f + checksum: 10c0/a49e2ee60066f7da18316c7c80e5b2ced286ec962052f83c01f1952a1c5fe5b65ba9803a04698260125270a1dd47416f0b287a8f42e591f0d88c9a660ae844ce languageName: node linkType: hard @@ -1837,18 +1837,18 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-relative-color-syntax@npm:^3.0.4": - version: 3.0.4 - resolution: "@csstools/postcss-relative-color-syntax@npm:3.0.4" +"@csstools/postcss-relative-color-syntax@npm:^3.0.5": + version: 3.0.5 + resolution: "@csstools/postcss-relative-color-syntax@npm:3.0.5" dependencies: - "@csstools/css-color-parser": "npm:^3.0.4" - "@csstools/css-parser-algorithms": "npm:^3.0.3" - "@csstools/css-tokenizer": "npm:^3.0.2" + "@csstools/css-color-parser": "npm:^3.0.5" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/f2ae6f9053e976e2431d08bdc5aa40ca5242754e23370abdfd7b6087d7ed56037e46911d1be801cf7e760aa8aee94bcab6a46f4f68696cf6befacf707c1534b7 + checksum: 10c0/505da97e7b219095a9fb6fa0ad6c6a1358ce1f5debada00564e635e1aa11d034c588be9ce2f76263172a8d1bd07d90647fde826f4565de22dcfc0a76beb68ff1 languageName: node linkType: hard @@ -1863,16 +1863,16 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-stepped-value-functions@npm:^4.0.3": - version: 4.0.3 - resolution: "@csstools/postcss-stepped-value-functions@npm:4.0.3" +"@csstools/postcss-stepped-value-functions@npm:^4.0.4": + version: 4.0.4 + resolution: "@csstools/postcss-stepped-value-functions@npm:4.0.4" dependencies: - "@csstools/css-calc": "npm:^2.0.3" - "@csstools/css-parser-algorithms": "npm:^3.0.3" - "@csstools/css-tokenizer": "npm:^3.0.2" + "@csstools/css-calc": "npm:^2.0.4" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" peerDependencies: postcss: ^8.4 - checksum: 10c0/31af5a650d4b2de97d3a3ef77e7ad0777b3f013f7463e23455ac31ffc946d2f79f365a1fdead52c7c535176232ab57f6f086ecdbeca1bb0b250161b1302e8c8c + checksum: 10c0/c8413a855a5ee9ad5b800263545d24d1bbd815f5ae0693c7dfd10c5f5b47bae5a9d985efdb2c46b03194ed3cae39f3d82c3ccfde502dee0b35d620fa539906be languageName: node linkType: hard @@ -1888,16 +1888,16 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-trigonometric-functions@npm:^4.0.3": - version: 4.0.3 - resolution: "@csstools/postcss-trigonometric-functions@npm:4.0.3" +"@csstools/postcss-trigonometric-functions@npm:^4.0.4": + version: 4.0.4 + resolution: "@csstools/postcss-trigonometric-functions@npm:4.0.4" dependencies: - "@csstools/css-calc": "npm:^2.0.3" - "@csstools/css-parser-algorithms": "npm:^3.0.3" - "@csstools/css-tokenizer": "npm:^3.0.2" + "@csstools/css-calc": "npm:^2.0.4" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" peerDependencies: postcss: ^8.4 - checksum: 10c0/a13dd72c00e45e6db16c03e135f1cd1b2b412e83b7681696b49feef3a4c36ab80f5f806f3589d33405e2d14dd0dfb13572ec12007ebe3d294077b0bd3d645b2b + checksum: 10c0/344c07961d24894f315faebf43c0128167fb2e33964c70d0d39d99a051c045678511d7378959105d672c5286a5360fde28fe390cdebfe823f3949e3cd1d50c4e languageName: node linkType: hard @@ -2206,8 +2206,8 @@ __metadata: linkType: hard "@formatjs/cli@npm:^6.1.1": - version: 6.3.5 - resolution: "@formatjs/cli@npm:6.3.5" + version: 6.3.6 + resolution: "@formatjs/cli@npm:6.3.6" peerDependencies: "@glimmer/env": ^0.1.7 "@glimmer/reference": ^0.91.1 || ^0.92.0 @@ -2236,7 +2236,7 @@ __metadata: optional: true bin: formatjs: bin/formatjs - checksum: 10c0/f3fd57de6df3ef0cfa668813c49acd7dcbecbf52444f30e8edec4ee7bc2d8c47c2e6fb4b7ce3c9c5b4595bdf8416c37953ebb05b400f99874a7f810943f6265b + checksum: 10c0/55f10699644c8b879d7fcf38660e7d9b0642c5f40ca59076c25ba190f350b48d7edb7f3d8481cc6139d3a708ee73f877c2c0a7d1865f15e80a73667270c9b7e3 languageName: node linkType: hard @@ -2251,14 +2251,14 @@ __metadata: languageName: node linkType: hard -"@formatjs/ecma402-abstract@npm:2.2.1": - version: 2.2.1 - resolution: "@formatjs/ecma402-abstract@npm:2.2.1" +"@formatjs/ecma402-abstract@npm:2.2.3": + version: 2.2.3 + resolution: "@formatjs/ecma402-abstract@npm:2.2.3" dependencies: - "@formatjs/fast-memoize": "npm:2.2.2" - "@formatjs/intl-localematcher": "npm:0.5.6" + "@formatjs/fast-memoize": "npm:2.2.3" + "@formatjs/intl-localematcher": "npm:0.5.7" tslib: "npm:2" - checksum: 10c0/bb0b01282368d201d9bcfb3232963af90069f6048da5824d38a674118fd49a0cb92b05d19d2526fe6d035428b2e47e1abd671bb59a673317f1418e67974253fa + checksum: 10c0/611d12bf320fc5c5b85cb2b57e3dcebe8490a51c6a0459c857c7a3560656cd2bdba5b117e9dd7cf174f5aa120c11eaad7a65a6783637b816caa59a1bc5c727f6 languageName: node linkType: hard @@ -2271,12 +2271,12 @@ __metadata: languageName: node linkType: hard -"@formatjs/fast-memoize@npm:2.2.2": - version: 2.2.2 - resolution: "@formatjs/fast-memoize@npm:2.2.2" +"@formatjs/fast-memoize@npm:2.2.3": + version: 2.2.3 + resolution: "@formatjs/fast-memoize@npm:2.2.3" dependencies: tslib: "npm:2" - checksum: 10c0/cebbf632a6ed8798ed67dba59d05be8bbe81c9ada7bcbc8ce69798fbaa5e0117c35792f212097d28b8b06fb99cf73bdbc7754702c06cda34f847e5082814643b + checksum: 10c0/f1004c3b280de7e362bd37c5f48ff34c2ba1d6271d4a7b695fed561d1201a3379397824d8bffbf15fecee344d1e70398393bbb04297f242692310a305f12e75b languageName: node linkType: hard @@ -2291,14 +2291,14 @@ __metadata: languageName: node linkType: hard -"@formatjs/icu-messageformat-parser@npm:2.9.1": - version: 2.9.1 - resolution: "@formatjs/icu-messageformat-parser@npm:2.9.1" +"@formatjs/icu-messageformat-parser@npm:2.9.3": + version: 2.9.3 + resolution: "@formatjs/icu-messageformat-parser@npm:2.9.3" dependencies: - "@formatjs/ecma402-abstract": "npm:2.2.1" - "@formatjs/icu-skeleton-parser": "npm:1.8.5" + "@formatjs/ecma402-abstract": "npm:2.2.3" + "@formatjs/icu-skeleton-parser": "npm:1.8.7" tslib: "npm:2" - checksum: 10c0/53554edc7d41f6bd737dc3bb3e03dcd4b4081b4fc9a70383612b87993c1639747d1d3827d8ebb13f8bf3566c60442549a0a42df4959699eef76633d4c7b49b25 + checksum: 10c0/519b59f7b4cf90681315c5382f7fcd105eb1974486f0d62d9227b6d0498895114ccc818792c208baae1ef79571d93b0edb9914c676e5ab76924dddb7fd6c28a0 languageName: node linkType: hard @@ -2312,35 +2312,35 @@ __metadata: languageName: node linkType: hard -"@formatjs/icu-skeleton-parser@npm:1.8.5": - version: 1.8.5 - resolution: "@formatjs/icu-skeleton-parser@npm:1.8.5" +"@formatjs/icu-skeleton-parser@npm:1.8.7": + version: 1.8.7 + resolution: "@formatjs/icu-skeleton-parser@npm:1.8.7" dependencies: - "@formatjs/ecma402-abstract": "npm:2.2.1" + "@formatjs/ecma402-abstract": "npm:2.2.3" tslib: "npm:2" - checksum: 10c0/3e5ddd26e5dc983547320ca09b4a5daa692a00c15382e85474a9a539c223b581dba2290708d71aae12589c3ddd053b3bfd0dab0a07a11c44033fb313605566bc + checksum: 10c0/e29eb4151580f2d324e6591509dc4543e2326266fc209a08580c94d502acab14acc3560d98b3aaf9ffbd5ff8e2683601ff08c65b32886f22da015c31ca35c5d0 languageName: node linkType: hard -"@formatjs/intl-displaynames@npm:6.8.1": - version: 6.8.1 - resolution: "@formatjs/intl-displaynames@npm:6.8.1" +"@formatjs/intl-displaynames@npm:6.8.3": + version: 6.8.3 + resolution: "@formatjs/intl-displaynames@npm:6.8.3" dependencies: - "@formatjs/ecma402-abstract": "npm:2.2.1" - "@formatjs/intl-localematcher": "npm:0.5.6" + "@formatjs/ecma402-abstract": "npm:2.2.3" + "@formatjs/intl-localematcher": "npm:0.5.7" tslib: "npm:2" - checksum: 10c0/6d39c7bbc3cf0b1398463d662662e5c7c8b873fe977e93b7d702a337f1ed639246492b71b7138c1f193df9e1045add5609edb3fd3efc76c6174ce134c02a186f + checksum: 10c0/54d3ecaabc45dc8494be4cd9633bbf5eb0bc40c5f62ed5e5073aa5b214c4a9a9620d61d1c8a6f0074618474fd9c2ccc42904605078d2f6341c242bf43627bb3a languageName: node linkType: hard -"@formatjs/intl-listformat@npm:7.7.1": - version: 7.7.1 - resolution: "@formatjs/intl-listformat@npm:7.7.1" +"@formatjs/intl-listformat@npm:7.7.3": + version: 7.7.3 + resolution: "@formatjs/intl-listformat@npm:7.7.3" dependencies: - "@formatjs/ecma402-abstract": "npm:2.2.1" - "@formatjs/intl-localematcher": "npm:0.5.6" + "@formatjs/ecma402-abstract": "npm:2.2.3" + "@formatjs/intl-localematcher": "npm:0.5.7" tslib: "npm:2" - checksum: 10c0/9ac38d34956ee8018c000bbd594c84f1de283d18441f1376b072658844735a4ebee61ac4711f824dd0a89f4a855acfc7a24159e4cd8a4747609b1d2153316a61 + checksum: 10c0/2683513e86cc7885528f23237079e3ab9e3a8bc7111aa10d4c685dcbe368ef07039573115df240112fb20f2ec0b70c8ea189ea3b79cbfed7e3dc46024a4667ff languageName: node linkType: hard @@ -2353,43 +2353,43 @@ __metadata: languageName: node linkType: hard -"@formatjs/intl-localematcher@npm:0.5.6": - version: 0.5.6 - resolution: "@formatjs/intl-localematcher@npm:0.5.6" +"@formatjs/intl-localematcher@npm:0.5.7": + version: 0.5.7 + resolution: "@formatjs/intl-localematcher@npm:0.5.7" dependencies: tslib: "npm:2" - checksum: 10c0/39fc454641b7871f4b37416a01c7851624db6f1bf6cdcd5c854dfe06b95c0ca7540ec7c803a654140fc3f1c758596a684de8054bc95c4d6c7962091c251f5671 + checksum: 10c0/1ae374ca146a0d7457794926eed808c99971628e594f704a42ae2540b1f38928b26cbf942a7bbcc2796cc9fe8d9d7a603ac422bd9b89b714d2f91b506da40792 languageName: node linkType: hard "@formatjs/intl-pluralrules@npm:^5.2.2": - version: 5.3.1 - resolution: "@formatjs/intl-pluralrules@npm:5.3.1" + version: 5.3.3 + resolution: "@formatjs/intl-pluralrules@npm:5.3.3" dependencies: - "@formatjs/ecma402-abstract": "npm:2.2.1" - "@formatjs/intl-localematcher": "npm:0.5.6" + "@formatjs/ecma402-abstract": "npm:2.2.3" + "@formatjs/intl-localematcher": "npm:0.5.7" tslib: "npm:2" - checksum: 10c0/71a87aa81d5d69d56dca0636144f8f9714176b95fe3bc92ae1e455322c09b5b09ba0018315d81def00e89fc95e4fbffe41110104c86eb189ba6d58ac09efa48c + checksum: 10c0/003d33af6f5d902517f230b7038e39b8336da3a84500fe5f4278fa5cac0c9a1b746de484f1c2bb2315026fd771491eafdc0aa59a809bb25189b63093efb8400d languageName: node linkType: hard -"@formatjs/intl@npm:2.10.11": - version: 2.10.11 - resolution: "@formatjs/intl@npm:2.10.11" +"@formatjs/intl@npm:2.10.13": + version: 2.10.13 + resolution: "@formatjs/intl@npm:2.10.13" dependencies: - "@formatjs/ecma402-abstract": "npm:2.2.1" - "@formatjs/fast-memoize": "npm:2.2.2" - "@formatjs/icu-messageformat-parser": "npm:2.9.1" - "@formatjs/intl-displaynames": "npm:6.8.1" - "@formatjs/intl-listformat": "npm:7.7.1" - intl-messageformat: "npm:10.7.3" + "@formatjs/ecma402-abstract": "npm:2.2.3" + "@formatjs/fast-memoize": "npm:2.2.3" + "@formatjs/icu-messageformat-parser": "npm:2.9.3" + "@formatjs/intl-displaynames": "npm:6.8.3" + "@formatjs/intl-listformat": "npm:7.7.3" + intl-messageformat: "npm:10.7.5" tslib: "npm:2" peerDependencies: typescript: ^4.7 || 5 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/83baa76d1194dbaae38c21d28cdc4e1ce024f8c49a37e2c02629a9f6afe18912f640435e346fae11af7d8128dac6355b36b93cff37685542ab3095edf3ec6d19 + checksum: 10c0/14edcc45addc181c7a45845bc34cec5c73bb5544dcf8ed455eca916bbd3a5023c7204a8e8e289060545b85006933c6f297c33c4f04a8a4cb3890aa7baae5bfbf languageName: node linkType: hard @@ -2413,13 +2413,13 @@ __metadata: languageName: node linkType: hard -"@formatjs/ts-transformer@npm:3.13.20": - version: 3.13.20 - resolution: "@formatjs/ts-transformer@npm:3.13.20" +"@formatjs/ts-transformer@npm:3.13.22": + version: 3.13.22 + resolution: "@formatjs/ts-transformer@npm:3.13.22" dependencies: - "@formatjs/icu-messageformat-parser": "npm:2.9.1" + "@formatjs/icu-messageformat-parser": "npm:2.9.3" "@types/json-stable-stringify": "npm:1" - "@types/node": "npm:14 || 16 || 17 || 18 || 20" + "@types/node": "npm:14 || 16 || 17 || 18 || 20 || 22" chalk: "npm:4" json-stable-stringify: "npm:1" tslib: "npm:2" @@ -2429,7 +2429,7 @@ __metadata: peerDependenciesMeta: ts-jest: optional: true - checksum: 10c0/0c1a203ce2e0efc24b9c1fc087771330f9630ebb4220e34dd2fffe64d5ee94816554df8ed8402a54dd588c186617765efb6c200a3373cf1d5dfb093ba8d70416 + checksum: 10c0/42503292248bcae728181fdf68e79eac4169c18064953beb9245097d2c58e5434ae7a7978e6ce2829dfd6eb4b4155f78fecef70ac6820626a338f231c66f60cb languageName: node linkType: hard @@ -3448,8 +3448,8 @@ __metadata: linkType: hard "@testing-library/jest-dom@npm:^6.0.0": - version: 6.6.2 - resolution: "@testing-library/jest-dom@npm:6.6.2" + version: 6.6.3 + resolution: "@testing-library/jest-dom@npm:6.6.3" dependencies: "@adobe/css-tools": "npm:^4.4.0" aria-query: "npm:^5.0.0" @@ -3458,7 +3458,7 @@ __metadata: dom-accessibility-api: "npm:^0.6.3" lodash: "npm:^4.17.21" redent: "npm:^3.0.0" - checksum: 10c0/1c51390f97063ce8b06a7267534aac0d6ac9b1f27042a0ae43ca52d886b32c9d2b0ff85e62e7cfeb7e1f089ef03c4d75fda4f2b7b1f08afb4bdb09a5d56566d9 + checksum: 10c0/5566b6c0b7b0709bc244aec3aa3dc9e5f4663e8fb2b99d8cd456fc07279e59db6076cbf798f9d3099a98fca7ef4cd50e4e1f4c4dec5a60a8fad8d24a638a5bf6 languageName: node linkType: hard @@ -3779,9 +3779,9 @@ __metadata: linkType: hard "@types/lodash@npm:^4.14.195": - version: 4.17.12 - resolution: "@types/lodash@npm:4.17.12" - checksum: 10c0/106008f628ea3c74ed7ee7842dee79e230c84e3721ac38c293700031adb5bd130113048c22f476dbde0d0c119506b0fc447d4bd62eca922682d11e00e1377967 + version: 4.17.13 + resolution: "@types/lodash@npm:4.17.13" + checksum: 10c0/c3d0b7efe7933ac0369b99f2f7bff9240d960680fdb74b41ed4bd1b3ca60cca1e31fe4046d9abbde778f941a41bc2a75eb629abf8659fa6c27b66efbbb0802a9 languageName: node linkType: hard @@ -3806,12 +3806,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:14 || 16 || 17 || 18 || 20": - version: 20.16.13 - resolution: "@types/node@npm:20.16.13" +"@types/node@npm:*, @types/node@npm:14 || 16 || 17 || 18 || 20 || 22": + version: 22.8.6 + resolution: "@types/node@npm:22.8.6" dependencies: - undici-types: "npm:~6.19.2" - checksum: 10c0/7f4fd7176db0802c62e11ebbf7a66d0248e2dc8ec9153d9fc8bcb164d9aed581d91c407046c823c522e60d0babfdc24a1618dba7f2c6920ef1a24e1d416c9550 + undici-types: "npm:~6.19.8" + checksum: 10c0/d3a11f2549234a91a4c5d0ff35ab4bdcb7ba34db4d3f1d189be39b8bd41c19aac98d117150a95a9c5a9d45b1014135477ea240b2b8317c86ae3d3cf1c3b3f8f4 languageName: node linkType: hard @@ -4001,7 +4001,7 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:>=16.9.11, @types/react@npm:^18.2.7, @types/react@npm:^18.3.11": +"@types/react@npm:*, @types/react@npm:16 || 17 || 18, @types/react@npm:>=16.9.11, @types/react@npm:^18.2.7": version: 18.3.12 resolution: "@types/react@npm:18.3.12" dependencies: @@ -4154,11 +4154,11 @@ __metadata: linkType: hard "@types/ws@npm:^8.5.9": - version: 8.5.12 - resolution: "@types/ws@npm:8.5.12" + version: 8.5.13 + resolution: "@types/ws@npm:8.5.13" dependencies: "@types/node": "npm:*" - checksum: 10c0/3fd77c9e4e05c24ce42bfc7647f7506b08c40a40fe2aea236ef6d4e96fc7cb4006a81ed1b28ec9c457e177a74a72924f4768b7b4652680b42dfd52bc380e15f9 + checksum: 10c0/a5430aa479bde588e69cb9175518d72f9338b6999e3b2ae16fc03d3bdcff8347e486dc031e4ed14601260463c07e1f9a0d7511dfc653712b047c439c680b0b34 languageName: node linkType: hard @@ -5190,21 +5190,21 @@ __metadata: linkType: hard "babel-plugin-formatjs@npm:^10.5.1": - version: 10.5.22 - resolution: "babel-plugin-formatjs@npm:10.5.22" + version: 10.5.24 + resolution: "babel-plugin-formatjs@npm:10.5.24" dependencies: "@babel/core": "npm:^7.25.0" "@babel/helper-plugin-utils": "npm:^7.25.0" "@babel/plugin-syntax-jsx": "npm:^7.25.0" "@babel/traverse": "npm:^7.25.0" "@babel/types": "npm:^7.25.0" - "@formatjs/icu-messageformat-parser": "npm:2.9.1" - "@formatjs/ts-transformer": "npm:3.13.20" + "@formatjs/icu-messageformat-parser": "npm:2.9.3" + "@formatjs/ts-transformer": "npm:3.13.22" "@types/babel__core": "npm:^7.20.5" "@types/babel__helper-plugin-utils": "npm:^7.10.3" "@types/babel__traverse": "npm:^7.20.6" tslib: "npm:2" - checksum: 10c0/c35ec81d89eb228d23d3d62e7e4d0e5d7216f0d09f91b1dc3c46e049c5515a201fcfe77c1079924c6bfb9883b2941af91431f479a0f5a24a0574b695f18b64bc + checksum: 10c0/a99c92e62edb30e0b6a5bf1115811eb18336b851109f8c096b5a9aa2be3eb72299eea3a8cb706e03705eb79edfa5a62d69bb52d80a375c1d5a1c963e3d00c40e languageName: node linkType: hard @@ -9765,15 +9765,15 @@ __metadata: languageName: node linkType: hard -"intl-messageformat@npm:10.7.3, intl-messageformat@npm:^10.3.5": - version: 10.7.3 - resolution: "intl-messageformat@npm:10.7.3" +"intl-messageformat@npm:10.7.5, intl-messageformat@npm:^10.3.5": + version: 10.7.5 + resolution: "intl-messageformat@npm:10.7.5" dependencies: - "@formatjs/ecma402-abstract": "npm:2.2.1" - "@formatjs/fast-memoize": "npm:2.2.2" - "@formatjs/icu-messageformat-parser": "npm:2.9.1" + "@formatjs/ecma402-abstract": "npm:2.2.3" + "@formatjs/fast-memoize": "npm:2.2.3" + "@formatjs/icu-messageformat-parser": "npm:2.9.3" tslib: "npm:2" - checksum: 10c0/7ddd118c04023c9ecdf9a2270753abd876e90a428d5106ce8373622ddefe72ff1daa33993b12065a3339b7f838e9266c3db9048aa7dd20012c8703c6ebe26fd4 + checksum: 10c0/1aa173a8c16ace50520af3de7d3f0ce9bafda90d47b6d674eb88cc47c12c3460d4d53c97ca412fae1141b00abb8ff2ba313250997a040837f01a25379165c949 languageName: node linkType: hard @@ -13294,18 +13294,18 @@ __metadata: languageName: node linkType: hard -"postcss-color-functional-notation@npm:^7.0.4": - version: 7.0.4 - resolution: "postcss-color-functional-notation@npm:7.0.4" +"postcss-color-functional-notation@npm:^7.0.5": + version: 7.0.5 + resolution: "postcss-color-functional-notation@npm:7.0.5" dependencies: - "@csstools/css-color-parser": "npm:^3.0.4" - "@csstools/css-parser-algorithms": "npm:^3.0.3" - "@csstools/css-tokenizer": "npm:^3.0.2" + "@csstools/css-color-parser": "npm:^3.0.5" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/1e39c47f27610ebf6a6308cfea2904fbe1f157b13654325ada23153a944666722c6132b53dfc7660632406a636479dda68eeb97a246c2e593ad6eed1e9bf838d + checksum: 10c0/5e1771b8a43abd3cb18142842e8ac100b205f37a6757d6686da49e0c61ecf33d09244d256f3802962105e7ef79fbde2dffbfc1eedf1700198c54d2d5e48f3dc1 languageName: node linkType: hard @@ -13359,46 +13359,46 @@ __metadata: languageName: node linkType: hard -"postcss-custom-media@npm:^11.0.4": - version: 11.0.4 - resolution: "postcss-custom-media@npm:11.0.4" +"postcss-custom-media@npm:^11.0.5": + version: 11.0.5 + resolution: "postcss-custom-media@npm:11.0.5" dependencies: - "@csstools/cascade-layer-name-parser": "npm:^2.0.3" - "@csstools/css-parser-algorithms": "npm:^3.0.3" - "@csstools/css-tokenizer": "npm:^3.0.2" - "@csstools/media-query-list-parser": "npm:^4.0.1" + "@csstools/cascade-layer-name-parser": "npm:^2.0.4" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" + "@csstools/media-query-list-parser": "npm:^4.0.2" peerDependencies: postcss: ^8.4 - checksum: 10c0/31e82e5802f6933a15f5fcf64ff41c8107af9c1d5dcba3c4422a6de3fac77bce9e3dc4ed2b79ae7f6ae460549c5da5a013669af05c1ae21549dbb09b66a9492d + checksum: 10c0/5ba1ca0383818e83d5f6f398a2b0c12cfda066b5d552adfc0e030a2c5f8690c2cc6224f9a1832a9c780dae3fd8d00d78c4a5c88eb36b731da1752f0c3917d488 languageName: node linkType: hard -"postcss-custom-properties@npm:^14.0.3": - version: 14.0.3 - resolution: "postcss-custom-properties@npm:14.0.3" +"postcss-custom-properties@npm:^14.0.4": + version: 14.0.4 + resolution: "postcss-custom-properties@npm:14.0.4" dependencies: - "@csstools/cascade-layer-name-parser": "npm:^2.0.3" - "@csstools/css-parser-algorithms": "npm:^3.0.3" - "@csstools/css-tokenizer": "npm:^3.0.2" + "@csstools/cascade-layer-name-parser": "npm:^2.0.4" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" "@csstools/utilities": "npm:^2.0.0" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/878ffd171ab9bbbba711930cf1b5bd48a12779e3b45d69f41366c2f65d84518f9338a92a277c0e390518c9e12272e06892cdc6575783bcdecfa6d26ebde3d043 + checksum: 10c0/5b101ee71289657cc2e5a16f4912009c10441052e2c54bd9e4f3d4d72b652bab56adb662ddaa96881413e375cf9852e2159b3c778d953442ce86efb781c3b2bf languageName: node linkType: hard -"postcss-custom-selectors@npm:^8.0.3": - version: 8.0.3 - resolution: "postcss-custom-selectors@npm:8.0.3" +"postcss-custom-selectors@npm:^8.0.4": + version: 8.0.4 + resolution: "postcss-custom-selectors@npm:8.0.4" dependencies: - "@csstools/cascade-layer-name-parser": "npm:^2.0.3" - "@csstools/css-parser-algorithms": "npm:^3.0.3" - "@csstools/css-tokenizer": "npm:^3.0.2" + "@csstools/cascade-layer-name-parser": "npm:^2.0.4" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" postcss-selector-parser: "npm:^7.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/3acdada43e385c23130f1dde41b189acc7739e5bbd14751ae28b2bd97b0d97f30c2a53da4f9558d108a893cc48717f76b28395a9ff38dcbbe3c3a1721ee4a68a + checksum: 10c0/09d494d2580d0a99f57684f79793d03358286c32460b61a84063c33bdde24865771cb1205efe9a8e26a508be24eba4fb93fc7f1e96ba21ca96a5d17fadb24863 languageName: node linkType: hard @@ -13516,18 +13516,18 @@ __metadata: languageName: node linkType: hard -"postcss-lab-function@npm:^7.0.4": - version: 7.0.4 - resolution: "postcss-lab-function@npm:7.0.4" +"postcss-lab-function@npm:^7.0.5": + version: 7.0.5 + resolution: "postcss-lab-function@npm:7.0.5" dependencies: - "@csstools/css-color-parser": "npm:^3.0.4" - "@csstools/css-parser-algorithms": "npm:^3.0.3" - "@csstools/css-tokenizer": "npm:^3.0.2" + "@csstools/css-color-parser": "npm:^3.0.5" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0" "@csstools/utilities": "npm:^2.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/79ce3cc20484aef0e0bbc2d18c8f91d7362226a3db52f06a931ac8e1c58ffbc60876c6439f364093e5f16ef92260a420230cfb2a9e7b95c6c11e6f7d57626260 + checksum: 10c0/b688f6332ac12974452c97c5a40f4d0a604ce16b074124a1106add9cf9c25fbe7717d23423d6e92d937dc6e288e03e885b70e0793b96f41d28d2f7403bce7bf5 languageName: node linkType: hard @@ -13848,38 +13848,38 @@ __metadata: linkType: hard "postcss-preset-env@npm:^10.0.0": - version: 10.0.8 - resolution: "postcss-preset-env@npm:10.0.8" + version: 10.0.9 + resolution: "postcss-preset-env@npm:10.0.9" dependencies: "@csstools/postcss-cascade-layers": "npm:^5.0.1" - "@csstools/postcss-color-function": "npm:^4.0.4" - "@csstools/postcss-color-mix-function": "npm:^3.0.4" - "@csstools/postcss-content-alt-text": "npm:^2.0.3" - "@csstools/postcss-exponential-functions": "npm:^2.0.3" + "@csstools/postcss-color-function": "npm:^4.0.5" + "@csstools/postcss-color-mix-function": "npm:^3.0.5" + "@csstools/postcss-content-alt-text": "npm:^2.0.4" + "@csstools/postcss-exponential-functions": "npm:^2.0.4" "@csstools/postcss-font-format-keywords": "npm:^4.0.0" - "@csstools/postcss-gamut-mapping": "npm:^2.0.4" - "@csstools/postcss-gradients-interpolation-method": "npm:^5.0.4" - "@csstools/postcss-hwb-function": "npm:^4.0.4" + "@csstools/postcss-gamut-mapping": "npm:^2.0.5" + "@csstools/postcss-gradients-interpolation-method": "npm:^5.0.5" + "@csstools/postcss-hwb-function": "npm:^4.0.5" "@csstools/postcss-ic-unit": "npm:^4.0.0" "@csstools/postcss-initial": "npm:^2.0.0" "@csstools/postcss-is-pseudo-class": "npm:^5.0.1" - "@csstools/postcss-light-dark-function": "npm:^2.0.6" + "@csstools/postcss-light-dark-function": "npm:^2.0.7" "@csstools/postcss-logical-float-and-clear": "npm:^3.0.0" "@csstools/postcss-logical-overflow": "npm:^2.0.0" "@csstools/postcss-logical-overscroll-behavior": "npm:^2.0.0" "@csstools/postcss-logical-resize": "npm:^3.0.0" - "@csstools/postcss-logical-viewport-units": "npm:^3.0.2" - "@csstools/postcss-media-minmax": "npm:^2.0.3" - "@csstools/postcss-media-queries-aspect-ratio-number-values": "npm:^3.0.3" + "@csstools/postcss-logical-viewport-units": "npm:^3.0.3" + "@csstools/postcss-media-minmax": "npm:^2.0.4" + "@csstools/postcss-media-queries-aspect-ratio-number-values": "npm:^3.0.4" "@csstools/postcss-nested-calc": "npm:^4.0.0" "@csstools/postcss-normalize-display-values": "npm:^4.0.0" - "@csstools/postcss-oklab-function": "npm:^4.0.4" + "@csstools/postcss-oklab-function": "npm:^4.0.5" "@csstools/postcss-progressive-custom-properties": "npm:^4.0.0" - "@csstools/postcss-relative-color-syntax": "npm:^3.0.4" + "@csstools/postcss-relative-color-syntax": "npm:^3.0.5" "@csstools/postcss-scope-pseudo-class": "npm:^4.0.1" - "@csstools/postcss-stepped-value-functions": "npm:^4.0.3" + "@csstools/postcss-stepped-value-functions": "npm:^4.0.4" "@csstools/postcss-text-decoration-shorthand": "npm:^4.0.1" - "@csstools/postcss-trigonometric-functions": "npm:^4.0.3" + "@csstools/postcss-trigonometric-functions": "npm:^4.0.4" "@csstools/postcss-unset-value": "npm:^4.0.0" autoprefixer: "npm:^10.4.19" browserslist: "npm:^4.23.1" @@ -13889,12 +13889,12 @@ __metadata: cssdb: "npm:^8.1.2" postcss-attribute-case-insensitive: "npm:^7.0.1" postcss-clamp: "npm:^4.1.0" - postcss-color-functional-notation: "npm:^7.0.4" + postcss-color-functional-notation: "npm:^7.0.5" postcss-color-hex-alpha: "npm:^10.0.0" postcss-color-rebeccapurple: "npm:^10.0.0" - postcss-custom-media: "npm:^11.0.4" - postcss-custom-properties: "npm:^14.0.3" - postcss-custom-selectors: "npm:^8.0.3" + postcss-custom-media: "npm:^11.0.5" + postcss-custom-properties: "npm:^14.0.4" + postcss-custom-selectors: "npm:^8.0.4" postcss-dir-pseudo-class: "npm:^9.0.1" postcss-double-position-gradients: "npm:^6.0.0" postcss-focus-visible: "npm:^10.0.1" @@ -13902,7 +13902,7 @@ __metadata: postcss-font-variant: "npm:^5.0.0" postcss-gap-properties: "npm:^6.0.0" postcss-image-set-function: "npm:^7.0.0" - postcss-lab-function: "npm:^7.0.4" + postcss-lab-function: "npm:^7.0.5" postcss-logical: "npm:^8.0.0" postcss-nesting: "npm:^13.0.1" postcss-opacity-percentage: "npm:^3.0.0" @@ -13914,7 +13914,7 @@ __metadata: postcss-selector-not: "npm:^8.0.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/3f7dc8eb2e7f4e7a5eee0a9456972a5b18bd40109d1aa3328f60cd645d352f3a70c83c507ea58f83b820153b0f5c3f14af3f376573c56599c36d5739c943b6f1 + checksum: 10c0/985ef4cfd2dd6a94d8a802db8e392c4b4c47ae0de8fd5224c60d8172cac3d7ce9011e63ab80470cddc77f21f09b2be850f97ac2478f651bc0a8956b538b079fe languageName: node linkType: hard @@ -14498,18 +14498,18 @@ __metadata: linkType: hard "react-intl@npm:^6.4.2": - version: 6.8.4 - resolution: "react-intl@npm:6.8.4" - dependencies: - "@formatjs/ecma402-abstract": "npm:2.2.1" - "@formatjs/icu-messageformat-parser": "npm:2.9.1" - "@formatjs/intl": "npm:2.10.11" - "@formatjs/intl-displaynames": "npm:6.8.1" - "@formatjs/intl-listformat": "npm:7.7.1" + version: 6.8.6 + resolution: "react-intl@npm:6.8.6" + dependencies: + "@formatjs/ecma402-abstract": "npm:2.2.3" + "@formatjs/icu-messageformat-parser": "npm:2.9.3" + "@formatjs/intl": "npm:2.10.13" + "@formatjs/intl-displaynames": "npm:6.8.3" + "@formatjs/intl-listformat": "npm:7.7.3" "@types/hoist-non-react-statics": "npm:3" - "@types/react": "npm:^18.3.11" + "@types/react": "npm:16 || 17 || 18" hoist-non-react-statics: "npm:3" - intl-messageformat: "npm:10.7.3" + intl-messageformat: "npm:10.7.5" tslib: "npm:2" peerDependencies: react: ^16.6.0 || 17 || 18 @@ -14517,7 +14517,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/a6b417c1cfb36ffe38645fe9daaaf3e1a25660468b8dbd4ccf386be063cc7c0fdeecc3c0490c671721408696b6578262b8db5882d00b76d4ec48d94601b14eab + checksum: 10c0/ec88cc2b1d0edf089f04c8061ffda730840fb3317177d0dc1a6df208245ec278f52b4ca8546ff9511b0b28ff2d89863c5837a274d33731d4c8f75a3b3baafbe2 languageName: node linkType: hard @@ -17363,7 +17363,7 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~6.19.2": +"undici-types@npm:~6.19.8": version: 6.19.8 resolution: "undici-types@npm:6.19.8" checksum: 10c0/078afa5990fba110f6824823ace86073b4638f1d5112ee26e790155f481f2a868cc3e0615505b6f4282bdf74a3d8caad715fd809e870c2bb0704e3ea6082f344 @@ -17623,12 +17623,12 @@ __metadata: linkType: hard "utf-8-validate@npm:^6.0.3": - version: 6.0.4 - resolution: "utf-8-validate@npm:6.0.4" + version: 6.0.5 + resolution: "utf-8-validate@npm:6.0.5" dependencies: node-gyp: "npm:latest" node-gyp-build: "npm:^4.3.0" - checksum: 10c0/f7042d94aec6ca02461b64e725bdc7262266610dbb787331e5bbd49374ef6f75fe9900600df3fc63d97906c23614a965c8989b4bf95d70bf35dc617da99215e7 + checksum: 10c0/6dc63c513adb001e47a51819072cdd414158430091c49c21d4947ea99f16df5167b671f680df8fb2b6f2ae6a7f30264b4ec111bd3e573720dfe371da1ab99a81 languageName: node linkType: hard