Merge branch 'main' into main

This commit is contained in:
kechpaja 2025-03-06 21:29:58 +02:00 committed by GitHub
commit a6d2ed4f3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
331 changed files with 4542 additions and 3081 deletions

View File

@ -43,4 +43,4 @@ jobs:
- name: Run haml-lint - name: Run haml-lint
run: | run: |
echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json" echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json"
bin/haml-lint --parallel --reporter github bin/haml-lint --reporter github

View File

@ -63,6 +63,7 @@ docker-compose.override.yml
# Ignore emoji map file # Ignore emoji map file
/app/javascript/mastodon/features/emoji/emoji_map.json /app/javascript/mastodon/features/emoji/emoji_map.json
/app/javascript/mastodon/features/emoji/emoji_sheet.json
# Ignore locale files # Ignore locale files
/app/javascript/mastodon/locales/*.json /app/javascript/mastodon/locales/*.json

View File

@ -18,6 +18,7 @@ inherit_from:
- .rubocop/rspec_rails.yml - .rubocop/rspec_rails.yml
- .rubocop/rspec.yml - .rubocop/rspec.yml
- .rubocop/style.yml - .rubocop/style.yml
- .rubocop/i18n.yml
- .rubocop/custom.yml - .rubocop/custom.yml
- .rubocop_todo.yml - .rubocop_todo.yml
- .rubocop/strict.yml - .rubocop/strict.yml
@ -30,6 +31,7 @@ plugins:
- rubocop-rails - rubocop-rails
- rubocop-rspec - rubocop-rspec
- rubocop-performance - rubocop-performance
- rubocop-i18n
require: require:
- rubocop-rspec_rails - rubocop-rspec_rails

12
.rubocop/i18n.yml Normal file
View File

@ -0,0 +1,12 @@
I18n/RailsI18n:
Enabled: true
Exclude:
- 'config/**/*'
- 'db/**/*'
- 'lib/**/*'
- 'spec/**/*'
I18n/GetText:
Enabled: false
I18n/RailsI18n/DecorateStringFormattingUsingInterpolation:
Enabled: false

View File

@ -1,6 +1,6 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp` # `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.72.2. # using RuboCop version 1.73.1.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new

View File

@ -2,6 +2,41 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [4.3.4] - 2025-02-27
### Security
- Update dependencies
- Change HTML sanitization to remove unusable and unused `embed` tag (#34021 by @ClearlyClaire, [GHSA-mq2m-hr29-8gqf](https://github.com/mastodon/mastodon/security/advisories/GHSA-mq2m-hr29-8gqf))
- Fix rate-limit on sign-up email verification ([GHSA-v39f-c9jj-8w7h](https://github.com/mastodon/mastodon/security/advisories/GHSA-v39f-c9jj-8w7h))
- Fix improper disclosure of domain blocks to unverified users ([GHSA-94h4-fj37-c825](https://github.com/mastodon/mastodon/security/advisories/GHSA-94h4-fj37-c825))
### Changed
- Change preview cards to be shown when Content Warnings are expanded (#33827 by @ClearlyClaire)
- Change warnings against changing encryption secrets to be even more noticeable (#33631 by @ClearlyClaire)
- Change `mastodon:setup` to prevent overwriting already-configured servers (#33603, #33616, and #33684 by @ClearlyClaire and @mjankowski)
- Change notifications from moderators to not be filtered (#32974 and #33654 by @ClearlyClaire and @mjankowski)
### Fixed
- Fix `GET /api/v2/notifications/:id` and `POST /api/v2/notifications/:id/dismiss` for ungrouped notifications (#33990 by @ClearlyClaire)
- Fix issue with some versions of libvips on some systems (#33853 by @kleisauke)
- Fix handling of duplicate mentions in incoming status `Update` (#33911 by @ClearlyClaire)
- Fix inefficiencies in timeline generation (#33839 and #33842 by @ClearlyClaire)
- Fix emoji rewrite adding unnecessary curft to the DOM for most emoji (#33818 by @ClearlyClaire)
- Fix `tootctl feeds build` not building list timelines (#33783 by @ClearlyClaire)
- Fix flaky test in `/api/v2/notifications` tests (#33773 by @ClearlyClaire)
- Fix incorrect signature after HTTP redirect (#33757 and #33769 by @ClearlyClaire)
- Fix polls not being validated on edition (#33755 by @ClearlyClaire)
- Fix media preview height in compose form when 3 or more images are attached (#33571 by @ClearlyClaire)
- Fix preview card sizing in “Author attribution” in profile settings (#33482 by @ClearlyClaire)
- Fix processing of incoming notifications for unfilterable types (#33429 by @ClearlyClaire)
- Fix featured tags for remote accounts not being kept up to date (#33372, #33406, and #33425 by @ClearlyClaire and @mjankowski)
- Fix notification polling showing a loading bar in web UI (#32960 by @Gargron)
- Fix accounts table long display name (#29316 by @WebCoder49)
- Fix exclusive lists interfering with notifications (#28162 by @ShadowJonathan)
## [4.3.3] - 2025-01-16 ## [4.3.3] - 2025-01-16
### Security ### Security

View File

@ -96,6 +96,9 @@ RUN \
# Set /opt/mastodon as working directory # Set /opt/mastodon as working directory
WORKDIR /opt/mastodon WORKDIR /opt/mastodon
# Add backport repository for some specific packages where we need the latest version
RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list
# hadolint ignore=DL3008,DL3005 # hadolint ignore=DL3008,DL3005
RUN \ RUN \
# Mount Apt cache and lib directories from Docker buildx caches # Mount Apt cache and lib directories from Docker buildx caches
@ -165,7 +168,7 @@ RUN \
libexif-dev \ libexif-dev \
libexpat1-dev \ libexpat1-dev \
libgirepository1.0-dev \ libgirepository1.0-dev \
libheif-dev \ libheif-dev/bookworm-backports \
libimagequant-dev \ libimagequant-dev \
libjpeg62-turbo-dev \ libjpeg62-turbo-dev \
liblcms2-dev \ liblcms2-dev \
@ -348,7 +351,7 @@ RUN \
# libvips components # libvips components
libcgif0 \ libcgif0 \
libexif12 \ libexif12 \
libheif1 \ libheif1/bookworm-backports \
libimagequant0 \ libimagequant0 \
libjpeg62-turbo \ libjpeg62-turbo \
liblcms2-2 \ liblcms2-2 \

View File

@ -39,7 +39,7 @@ gem 'net-ldap', '~> 0.18'
gem 'omniauth', '~> 2.0' gem 'omniauth', '~> 2.0'
gem 'omniauth-cas', '~> 3.0.0.beta.1' gem 'omniauth-cas', '~> 3.0.0.beta.1'
gem 'omniauth_openid_connect', '~> 0.6.1' gem 'omniauth_openid_connect', '~> 0.8.0'
gem 'omniauth-rails_csrf_protection', '~> 1.0' gem 'omniauth-rails_csrf_protection', '~> 1.0'
gem 'omniauth-saml', '~> 2.0' gem 'omniauth-saml', '~> 2.0'
@ -145,9 +145,6 @@ group :test do
# Used to mock environment variables # Used to mock environment variables
gem 'climate_control' gem 'climate_control'
# Add back helpers functions removed in Rails 5.1
gem 'rails-controller-testing', '~> 1.0'
# Validate schemas in specs # Validate schemas in specs
gem 'json-schema', '~> 5.0' gem 'json-schema', '~> 5.0'
@ -168,6 +165,7 @@ group :development do
# Code linting CLI and plugins # Code linting CLI and plugins
gem 'rubocop', require: false gem 'rubocop', require: false
gem 'rubocop-capybara', require: false gem 'rubocop-capybara', require: false
gem 'rubocop-i18n', require: false
gem 'rubocop-performance', require: false gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false gem 'rubocop-rails', require: false
gem 'rubocop-rspec', require: false gem 'rubocop-rspec', require: false

View File

@ -194,7 +194,7 @@ GEM
devise_pam_authenticatable2 (9.2.0) devise_pam_authenticatable2 (9.2.0)
devise (>= 4.0.0) devise (>= 4.0.0)
rpam2 (~> 4.0) rpam2 (~> 4.0)
diff-lcs (1.5.1) diff-lcs (1.6.0)
discard (1.4.0) discard (1.4.0)
activerecord (>= 4.2, < 9.0) activerecord (>= 4.2, < 9.0)
docile (1.4.1) docile (1.4.1)
@ -217,6 +217,8 @@ GEM
htmlentities (~> 4.3.3) htmlentities (~> 4.3.3)
launchy (>= 2.1, < 4.0) launchy (>= 2.1, < 4.0)
mail (~> 2.7) mail (~> 2.7)
email_validator (2.2.4)
activemodel
erubi (1.13.1) erubi (1.13.1)
et-orbi (1.2.11) et-orbi (1.2.11)
tzinfo tzinfo
@ -228,6 +230,8 @@ GEM
faraday-net_http (>= 2.0, < 3.5) faraday-net_http (>= 2.0, < 3.5)
json json
logger logger
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-httpclient (2.0.1) faraday-httpclient (2.0.1)
httpclient (>= 2.2) httpclient (>= 2.2)
faraday-net_http (3.4.0) faraday-net_http (3.4.0)
@ -273,7 +277,7 @@ GEM
activesupport (>= 5.1) activesupport (>= 5.1)
haml (>= 4.0.6) haml (>= 4.0.6)
railties (>= 5.1) railties (>= 5.1)
haml_lint (0.60.0) haml_lint (0.61.0)
haml (>= 5.0) haml (>= 5.0)
parallel (~> 1.10) parallel (~> 1.10)
rainbow rainbow
@ -330,11 +334,13 @@ GEM
jmespath (1.6.2) jmespath (1.6.2)
json (2.10.1) json (2.10.1)
json-canonicalization (1.0.0) json-canonicalization (1.0.0)
json-jwt (1.15.3.1) json-jwt (1.16.7)
activesupport (>= 4.2) activesupport (>= 4.2)
aes_key_wrap aes_key_wrap
base64
bindata bindata
httpclient faraday (~> 2.0)
faraday-follow_redirects
json-ld (3.3.2) json-ld (3.3.2)
htmlentities (~> 4.3) htmlentities (~> 4.3)
json-canonicalization (~> 1.0) json-canonicalization (~> 1.0)
@ -409,11 +415,11 @@ GEM
mime-types (3.6.0) mime-types (3.6.0)
logger logger
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2025.0204) mime-types-data (3.2025.0220)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.8) mini_portile2 (2.8.8)
minitest (5.25.4) minitest (5.25.4)
msgpack (1.7.5) msgpack (1.8.0)
multi_json (1.15.0) multi_json (1.15.0)
mutex_m (0.3.0) mutex_m (0.3.0)
net-http (0.6.0) net-http (0.6.0)
@ -432,37 +438,39 @@ GEM
nokogiri (1.18.3) nokogiri (1.18.3)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
oj (3.16.9) oj (3.16.10)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
ostruct (>= 0.2) ostruct (>= 0.2)
omniauth (2.1.2) omniauth (2.1.3)
hashie (>= 3.4.6) hashie (>= 3.4.6)
rack (>= 2.2.3) rack (>= 2.2.3)
rack-protection rack-protection
omniauth-cas (3.0.0) omniauth-cas (3.0.1)
addressable (~> 2.8) addressable (~> 2.8)
nokogiri (~> 1.12) nokogiri (~> 1.12)
omniauth (~> 2.1) omniauth (~> 2.1)
omniauth-rails_csrf_protection (1.0.2) omniauth-rails_csrf_protection (1.0.2)
actionpack (>= 4.2) actionpack (>= 4.2)
omniauth (~> 2.0) omniauth (~> 2.0)
omniauth-saml (2.2.1) omniauth-saml (2.2.2)
omniauth (~> 2.1) omniauth (~> 2.1)
ruby-saml (~> 1.17) ruby-saml (~> 1.17)
omniauth_openid_connect (0.6.1) omniauth_openid_connect (0.8.0)
omniauth (>= 1.9, < 3) omniauth (>= 1.9, < 3)
openid_connect (~> 1.1) openid_connect (~> 2.2)
openid_connect (1.4.2) openid_connect (2.3.1)
activemodel activemodel
attr_required (>= 1.0.0) attr_required (>= 1.0.0)
json-jwt (>= 1.15.0) email_validator
net-smtp faraday (~> 2.0)
rack-oauth2 (~> 1.21) faraday-follow_redirects
swd (~> 1.3) json-jwt (>= 1.16)
mail
rack-oauth2 (~> 2.2)
swd (~> 2.0)
tzinfo tzinfo
validate_email
validate_url validate_url
webfinger (~> 1.2) webfinger (~> 2.0)
openssl (3.3.0) openssl (3.3.0)
openssl-signature_algorithm (1.3.0) openssl-signature_algorithm (1.3.0)
openssl (> 2.0) openssl (> 2.0)
@ -600,19 +608,20 @@ GEM
public_suffix (6.0.1) public_suffix (6.0.1)
puma (6.6.0) puma (6.6.0)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.4.0) pundit (2.5.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.8.1) racc (1.8.1)
rack (2.2.11) rack (2.2.12)
rack-attack (6.7.0) rack-attack (6.7.0)
rack (>= 1.0, < 4) rack (>= 1.0, < 4)
rack-cors (2.0.2) rack-cors (2.0.2)
rack (>= 2.0.0) rack (>= 2.0.0)
rack-oauth2 (1.21.3) rack-oauth2 (2.2.1)
activesupport activesupport
attr_required attr_required
httpclient faraday (~> 2.0)
faraday-follow_redirects
json-jwt (>= 1.11.0) json-jwt (>= 1.11.0)
rack (>= 2.1.0) rack (>= 2.1.0)
rack-protection (3.2.0) rack-protection (3.2.0)
@ -641,10 +650,6 @@ GEM
activesupport (= 8.0.1) activesupport (= 8.0.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 8.0.1) railties (= 8.0.1)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.2.0) rails-dom-testing (2.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
@ -673,7 +678,7 @@ GEM
rdf (~> 3.3) rdf (~> 3.3)
rdoc (6.12.0) rdoc (6.12.0)
psych (>= 4.0.0) psych (>= 4.0.0)
redcarpet (3.6.0) redcarpet (3.6.1)
redis (4.8.1) redis (4.8.1)
redis-namespace (1.11.0) redis-namespace (1.11.0)
redis (>= 4) redis (>= 4)
@ -687,7 +692,7 @@ GEM
responders (3.1.1) responders (3.1.1)
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
rexml (3.4.0) rexml (3.4.1)
rotp (6.3.0) rotp (6.3.0)
rouge (4.5.1) rouge (4.5.1)
rpam2 (4.0.2) rpam2 (4.0.2)
@ -723,7 +728,7 @@ GEM
rspec-mocks (~> 3.0) rspec-mocks (~> 3.0)
sidekiq (>= 5, < 8) sidekiq (>= 5, < 8)
rspec-support (3.13.2) rspec-support (3.13.2)
rubocop (1.72.2) rubocop (1.73.2)
json (~> 2.3) json (~> 2.3)
language_server-protocol (~> 3.17.0.2) language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0) lint_roller (~> 1.1.0)
@ -734,15 +739,18 @@ GEM
rubocop-ast (>= 1.38.0, < 2.0) rubocop-ast (>= 1.38.0, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0) unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.38.0) rubocop-ast (1.38.1)
parser (>= 3.3.1.0) parser (>= 3.3.1.0)
rubocop-capybara (2.21.0) rubocop-capybara (2.21.0)
rubocop (~> 1.41) rubocop (~> 1.41)
rubocop-i18n (3.2.3)
lint_roller (~> 1.1)
rubocop (>= 1.72.1)
rubocop-performance (1.24.0) rubocop-performance (1.24.0)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rubocop (>= 1.72.1, < 2.0) rubocop (>= 1.72.1, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0) rubocop-ast (>= 1.38.0, < 2.0)
rubocop-rails (2.30.1) rubocop-rails (2.30.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rack (>= 1.1) rack (>= 1.1)
@ -774,7 +782,7 @@ GEM
activerecord (>= 4.0.0) activerecord (>= 4.0.0)
railties (>= 4.0.0) railties (>= 4.0.0)
securerandom (0.4.1) securerandom (0.4.1)
selenium-webdriver (4.28.0) selenium-webdriver (4.29.1)
base64 (~> 0.2) base64 (~> 0.2)
logger (~> 1.4) logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
@ -814,13 +822,14 @@ GEM
stackprof (0.2.27) stackprof (0.2.27)
stoplight (4.1.1) stoplight (4.1.1)
redlock (~> 1.0) redlock (~> 1.0)
stringio (3.1.2) stringio (3.1.4)
strong_migrations (2.2.0) strong_migrations (2.2.0)
activerecord (>= 7) activerecord (>= 7)
swd (1.3.0) swd (2.0.3)
activesupport (>= 3) activesupport (>= 3)
attr_required (>= 0.0.5) attr_required (>= 0.0.5)
httpclient (>= 2.4) faraday (~> 2.0)
faraday-follow_redirects
sysexits (1.2.0) sysexits (1.2.0)
temple (0.10.3) temple (0.10.3)
terminal-table (4.0.0) terminal-table (4.0.0)
@ -858,11 +867,8 @@ GEM
unicode-display_width (3.1.4) unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4) unicode-emoji (4.0.4)
uri (1.0.2) uri (1.0.3)
useragent (0.16.11) useragent (0.16.11)
validate_email (0.1.6)
activemodel (>= 3.0)
mail (>= 2.2.5)
validate_url (1.0.15) validate_url (1.0.15)
activemodel (>= 3.0.0) activemodel (>= 3.0.0)
public_suffix public_suffix
@ -876,9 +882,10 @@ GEM
openssl (>= 2.2) openssl (>= 2.2)
safety_net_attestation (~> 0.4.0) safety_net_attestation (~> 0.4.0)
tpm-key_attestation (~> 0.14.0) tpm-key_attestation (~> 0.14.0)
webfinger (1.2.0) webfinger (2.1.3)
activesupport activesupport
httpclient (>= 2.4) faraday (~> 2.0)
faraday-follow_redirects
webmock (3.25.0) webmock (3.25.0)
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
@ -898,7 +905,7 @@ GEM
xorcist (1.1.3) xorcist (1.1.3)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.7.1) zeitwerk (2.7.2)
PLATFORMS PLATFORMS
ruby ruby
@ -977,7 +984,7 @@ DEPENDENCIES
omniauth-cas (~> 3.0.0.beta.1) omniauth-cas (~> 3.0.0.beta.1)
omniauth-rails_csrf_protection (~> 1.0) omniauth-rails_csrf_protection (~> 1.0)
omniauth-saml (~> 2.0) omniauth-saml (~> 2.0)
omniauth_openid_connect (~> 0.6.1) omniauth_openid_connect (~> 0.8.0)
opentelemetry-api (~> 1.4.0) opentelemetry-api (~> 1.4.0)
opentelemetry-exporter-otlp (~> 0.29.0) opentelemetry-exporter-otlp (~> 0.29.0)
opentelemetry-instrumentation-active_job (~> 0.8.0) opentelemetry-instrumentation-active_job (~> 0.8.0)
@ -1009,7 +1016,6 @@ DEPENDENCIES
rack-cors (~> 2.0) rack-cors (~> 2.0)
rack-test (~> 2.1) rack-test (~> 2.1)
rails (~> 8.0) rails (~> 8.0)
rails-controller-testing (~> 1.0)
rails-i18n (~> 8.0) rails-i18n (~> 8.0)
rdf-normalize (~> 0.5) rdf-normalize (~> 0.5)
redcarpet (~> 3.6) redcarpet (~> 3.6)
@ -1021,6 +1027,7 @@ DEPENDENCIES
rspec-sidekiq (~> 5.0) rspec-sidekiq (~> 5.0)
rubocop rubocop
rubocop-capybara rubocop-capybara
rubocop-i18n
rubocop-performance rubocop-performance
rubocop-rails rubocop-rails
rubocop-rspec rubocop-rspec
@ -1059,4 +1066,4 @@ RUBY VERSION
ruby 3.4.1p0 ruby 3.4.1p0
BUNDLED WITH BUNDLED WITH
2.6.3 2.6.5

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class Admin::Announcements::DistributionsController < Admin::BaseController
before_action :set_announcement
def create
authorize @announcement, :distribute?
@announcement.touch(:notification_sent_at)
Admin::DistributeAnnouncementNotificationWorker.perform_async(@announcement.id)
redirect_to admin_announcements_path
end
private
def set_announcement
@announcement = Announcement.find(params[:announcement_id])
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class Admin::Announcements::PreviewsController < Admin::BaseController
before_action :set_announcement
def show
authorize @announcement, :distribute?
@user_count = @announcement.scope_for_notification.count
end
private
def set_announcement
@announcement = Announcement.find(params[:announcement_id])
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class Admin::Announcements::TestsController < Admin::BaseController
before_action :set_announcement
def create
authorize @announcement, :distribute?
UserMailer.announcement_published(current_user, @announcement).deliver_later!
redirect_to admin_announcements_path
end
private
def set_announcement
@announcement = Announcement.find(params[:announcement_id])
end
end

View File

@ -23,7 +23,7 @@ class Admin::TermsOfService::DraftsController < Admin::BaseController
private private
def set_terms_of_service def set_terms_of_service
@terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text) @terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text, effective_date: 10.days.from_now)
end end
def current_terms_of_service def current_terms_of_service
@ -32,6 +32,6 @@ class Admin::TermsOfService::DraftsController < Admin::BaseController
def resource_params def resource_params
params params
.expect(terms_of_service: [:text, :changelog]) .expect(terms_of_service: [:text, :changelog, :effective_date])
end end
end end

View File

@ -3,6 +3,6 @@
class Admin::TermsOfServiceController < Admin::BaseController class Admin::TermsOfServiceController < Admin::BaseController
def index def index
authorize :terms_of_service, :index? authorize :terms_of_service, :index?
@terms_of_service = TermsOfService.live.first @terms_of_service = TermsOfService.published.first
end end
end end

View File

@ -31,7 +31,7 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr
end end
def show_domain_blocks_to_user? def show_domain_blocks_to_user?
Setting.show_domain_blocks == 'users' && user_signed_in? Setting.show_domain_blocks == 'users' && user_signed_in? && current_user.functional_or_moved?
end end
def set_domain_blocks def set_domain_blocks
@ -47,6 +47,6 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr
end end
def show_rationale_for_user? def show_rationale_for_user?
Setting.show_domain_blocks_rationale == 'users' && user_signed_in? Setting.show_domain_blocks_rationale == 'users' && user_signed_in? && current_user.functional_or_moved?
end end
end end

View File

@ -5,12 +5,18 @@ class Api::V1::Instances::TermsOfServicesController < Api::V1::Instances::BaseCo
def show def show
cache_even_if_authenticated! cache_even_if_authenticated!
render json: @terms_of_service, serializer: REST::PrivacyPolicySerializer render json: @terms_of_service, serializer: REST::TermsOfServiceSerializer
end end
private private
def set_terms_of_service def set_terms_of_service
@terms_of_service = TermsOfService.live.first! @terms_of_service = begin
if params[:date].present?
TermsOfService.published.find_by!(effective_date: params[:date])
else
TermsOfService.live.first || TermsOfService.published.first! # For the case when none of the published terms have become effective yet
end
end
end end
end end

View File

@ -3,8 +3,8 @@
class Api::V1::MediaController < Api::BaseController class Api::V1::MediaController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:media' } before_action -> { doorkeeper_authorize! :write, :'write:media' }
before_action :require_user! before_action :require_user!
before_action :set_media_attachment, except: [:create] before_action :set_media_attachment, except: [:create, :destroy]
before_action :check_processing, except: [:create] before_action :check_processing, except: [:create, :destroy]
def show def show
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
@ -25,6 +25,15 @@ class Api::V1::MediaController < Api::BaseController
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
end end
def destroy
@media_attachment = current_account.media_attachments.find(params[:id])
return render json: in_usage_error, status: 422 unless @media_attachment.status_id.nil?
@media_attachment.destroy
render_empty
end
private private
def status_code_for_media_attachment def status_code_for_media_attachment
@ -54,4 +63,8 @@ class Api::V1::MediaController < Api::BaseController
def processing_error def processing_error
{ error: 'Error processing thumbnail for uploaded media' } { error: 'Error processing thumbnail for uploaded media' }
end end
def in_usage_error
{ error: 'Media attachment is currently used by a status' }
end
end end

View File

@ -111,7 +111,7 @@ class Api::V1::StatusesController < Api::BaseController
@status.account.statuses_count = @status.account.statuses_count - 1 @status.account.statuses_count = @status.account.statuses_count - 1
json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
RemovalWorker.perform_async(@status.id, { 'redraft' => true }) RemovalWorker.perform_async(@status.id, { 'redraft' => !truthy_param?(:delete_media) })
render json: json render json: json
end end

View File

@ -46,7 +46,7 @@ class Api::V2::NotificationsController < Api::BaseController
end end
def show def show
@notification = current_account.notifications.without_suspended.find_by!(group_key: params[:group_key]) @notification = current_account.notifications.without_suspended.by_group_key(params[:group_key]).take!
presenter = GroupedNotificationsPresenter.new(NotificationGroup.from_notifications([@notification])) presenter = GroupedNotificationsPresenter.new(NotificationGroup.from_notifications([@notification]))
render json: presenter, serializer: REST::DedupNotificationGroupSerializer render json: presenter, serializer: REST::DedupNotificationGroupSerializer
end end
@ -57,7 +57,7 @@ class Api::V2::NotificationsController < Api::BaseController
end end
def dismiss def dismiss
current_account.notifications.where(group_key: params[:group_key]).destroy_all current_account.notifications.by_group_key(params[:group_key]).destroy_all
render_empty render_empty
end end

View File

@ -1,7 +1,7 @@
import './public-path'; import './public-path';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { afterInitialRender } from 'mastodon/../hooks/useRenderSignal'; import { afterInitialRender } from 'mastodon/hooks/useRenderSignal';
import { start } from '../mastodon/common'; import { start } from '../mastodon/common';
import { Status } from '../mastodon/features/standalone/status'; import { Status } from '../mastodon/features/standalone/status';

View File

@ -142,6 +142,13 @@ export function fetchAccountFail(id, error) {
}; };
} }
/**
* @param {string} id
* @param {Object} options
* @param {boolean} [options.reblogs]
* @param {boolean} [options.notify]
* @returns {function(): void}
*/
export function followAccount(id, options = { reblogs: true }) { export function followAccount(id, options = { reblogs: true }) {
return (dispatch, getState) => { return (dispatch, getState) => {
const alreadyFollowing = getState().getIn(['relationships', id, 'following']); const alreadyFollowing = getState().getIn(['relationships', id, 'following']);

View File

@ -29,7 +29,7 @@ const debouncedSave = debounce((dispatch, getState) => {
api().put('/api/web/settings', { data }) api().put('/api/web/settings', { data })
.then(() => dispatch({ type: SETTING_SAVE })) .then(() => dispatch({ type: SETTING_SAVE }))
.catch(error => dispatch(showAlertForError(error))); .catch(error => dispatch(showAlertForError(error)));
}, 5000, { trailing: true }); }, 2000, { leading: true, trailing: true });
export function saveSettings() { export function saveSettings() {
return (dispatch, getState) => debouncedSave(dispatch, getState); return (dispatch, getState) => debouncedSave(dispatch, getState);

View File

@ -138,7 +138,7 @@ export function deleteStatus(id, withRedraft = false) {
dispatch(deleteStatusRequest(id)); dispatch(deleteStatusRequest(id));
api().delete(`/api/v1/statuses/${id}`).then(response => { api().delete(`/api/v1/statuses/${id}`, { params: { delete_media: !withRedraft } }).then(response => {
dispatch(deleteStatusSuccess(id)); dispatch(deleteStatusSuccess(id));
dispatch(deleteFromTimelines(id)); dispatch(deleteFromTimelines(id));
dispatch(importFetchedAccount(response.data.account)); dispatch(importFetchedAccount(response.data.account));

View File

@ -4,8 +4,12 @@ import type {
ApiPrivacyPolicyJSON, ApiPrivacyPolicyJSON,
} from 'mastodon/api_types/instance'; } from 'mastodon/api_types/instance';
export const apiGetTermsOfService = () => export const apiGetTermsOfService = (version?: string) =>
apiRequestGet<ApiTermsOfServiceJSON>('v1/instance/terms_of_service'); apiRequestGet<ApiTermsOfServiceJSON>(
version
? `v1/instance/terms_of_service/${version}`
: 'v1/instance/terms_of_service',
);
export const apiGetPrivacyPolicy = () => export const apiGetPrivacyPolicy = () =>
apiRequestGet<ApiPrivacyPolicyJSON>('v1/instance/privacy_policy'); apiRequestGet<ApiPrivacyPolicyJSON>('v1/instance/privacy_policy');

View File

@ -1,5 +1,7 @@
export interface ApiTermsOfServiceJSON { export interface ApiTermsOfServiceJSON {
updated_at: string; effective_date: string;
effective: boolean;
succeeded_by: string | null;
content: string; content: string;
} }

View File

@ -1,4 +1,4 @@
import { useLinks } from 'mastodon/../hooks/useLinks'; import { useLinks } from 'mastodon/hooks/useLinks';
export const AccountBio: React.FC<{ export const AccountBio: React.FC<{
note: string; note: string;

View File

@ -1,8 +1,8 @@
import classNames from 'classnames'; import classNames from 'classnames';
import CheckIcon from '@/material-icons/400-24px/check.svg?react'; import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import { useLinks } from 'mastodon/../hooks/useLinks';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { useLinks } from 'mastodon/hooks/useLinks';
import type { Account } from 'mastodon/models/account'; import type { Account } from 'mastodon/models/account';
export const AccountFields: React.FC<{ export const AccountFields: React.FC<{

View File

@ -8,7 +8,7 @@ import type {
UsePopperOptions, UsePopperOptions,
} from 'react-overlays/esm/usePopper'; } from 'react-overlays/esm/usePopper';
import { useSelectableClick } from '@/hooks/useSelectableClick'; import { useSelectableClick } from 'mastodon/hooks/useSelectableClick';
const offset = [0, 4] as OffsetValue; const offset = [0, 4] as OffsetValue;
const popperConfig = { strategy: 'fixed' } as UsePopperOptions; const popperConfig = { strategy: 'fixed' } as UsePopperOptions;

View File

@ -2,7 +2,7 @@ import { useState, useCallback } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { useHovering } from 'mastodon/../hooks/useHovering'; import { useHovering } from 'mastodon/hooks/useHovering';
import { autoPlayGif } from 'mastodon/initial_state'; import { autoPlayGif } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account'; import type { Account } from 'mastodon/models/account';

View File

@ -1,8 +1,7 @@
import { useHovering } from 'mastodon/hooks/useHovering';
import { autoPlayGif } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account'; import type { Account } from 'mastodon/models/account';
import { useHovering } from '../../hooks/useHovering';
import { autoPlayGif } from '../initial_state';
interface Props { interface Props {
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there

View File

@ -5,8 +5,8 @@ import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react'; import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
import { useTimeout } from 'mastodon/../hooks/useTimeout';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { useTimeout } from 'mastodon/hooks/useTimeout';
export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => { export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
const inputRef = useRef<HTMLTextAreaElement>(null); const inputRef = useRef<HTMLTextAreaElement>(null);

View File

@ -55,7 +55,7 @@ export const FollowButton: React.FC<{
); );
} }
if (!relationship) return; if (!relationship || !accountId) return;
if (accountId === me) { if (accountId === me) {
return; return;

View File

@ -1,4 +1,4 @@
import { useHovering } from '@/hooks/useHovering'; import { useHovering } from 'mastodon/hooks/useHovering';
import { autoPlayGif } from 'mastodon/initial_state'; import { autoPlayGif } from 'mastodon/initial_state';
export const GIF: React.FC<{ export const GIF: React.FC<{

View File

@ -151,7 +151,7 @@ export const Hashtag: React.FC<HashtagProps> = ({
<Sparklines <Sparklines
width={50} width={50}
height={28} height={28}
data={history ? history : Array.from(Array(7)).map(() => 0)} data={history ?? Array.from(Array(7)).map(() => 0)}
> >
<SparklinesCurve style={{ fill: 'none' }} /> <SparklinesCurve style={{ fill: 'none' }} />
</Sparklines> </Sparklines>

View File

@ -8,8 +8,8 @@ import type {
UsePopperOptions, UsePopperOptions,
} from 'react-overlays/esm/usePopper'; } from 'react-overlays/esm/usePopper';
import { useTimeout } from 'mastodon/../hooks/useTimeout';
import { HoverCardAccount } from 'mastodon/components/hover_card_account'; import { HoverCardAccount } from 'mastodon/components/hover_card_account';
import { useTimeout } from 'mastodon/hooks/useTimeout';
const offset = [-12, 4] as OffsetValue; const offset = [-12, 4] as OffsetValue;
const enterDelay = 750; const enterDelay = 750;

View File

@ -149,6 +149,7 @@ export class IconButton extends PureComponent<Props, States> {
onClick={this.handleClick} onClick={this.handleClick}
onMouseDown={this.handleMouseDown} onMouseDown={this.handleMouseDown}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
// eslint-disable-next-line @typescript-eslint/no-deprecated
onKeyPress={this.handleKeyPress} onKeyPress={this.handleKeyPress}
style={style} style={style}
tabIndex={tabIndex} tabIndex={tabIndex}

View File

@ -81,6 +81,7 @@ class ScrollableList extends PureComponent {
bindToDocument: PropTypes.bool, bindToDocument: PropTypes.bool,
preventScroll: PropTypes.bool, preventScroll: PropTypes.bool,
footer: PropTypes.node, footer: PropTypes.node,
className: PropTypes.string,
}; };
static defaultProps = { static defaultProps = {
@ -325,7 +326,7 @@ class ScrollableList extends PureComponent {
}; };
render () { render () {
const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, footer, emptyMessage, onLoadMore } = this.props; const { children, scrollKey, className, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, footer, emptyMessage, onLoadMore } = this.props;
const { fullscreen } = this.state; const { fullscreen } = this.state;
const childrenCount = Children.count(children); const childrenCount = Children.count(children);
@ -336,9 +337,9 @@ class ScrollableList extends PureComponent {
if (showLoading) { if (showLoading) {
scrollableArea = ( scrollableArea = (
<div className='scrollable scrollable--flex' ref={this.setRef}> <div className='scrollable scrollable--flex' ref={this.setRef}>
<div role='feed' className='item-list'> {prepend}
{prepend}
</div> <div role='feed' className='item-list' />
<div className='scrollable__append'> <div className='scrollable__append'>
<LoadingIndicator /> <LoadingIndicator />
@ -350,9 +351,9 @@ class ScrollableList extends PureComponent {
} else if (isLoading || childrenCount > 0 || numPending > 0 || hasMore || !emptyMessage) { } else if (isLoading || childrenCount > 0 || numPending > 0 || hasMore || !emptyMessage) {
scrollableArea = ( scrollableArea = (
<div className={classNames('scrollable scrollable--flex', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}> <div className={classNames('scrollable scrollable--flex', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
<div role='feed' className='item-list'> {prepend}
{prepend}
<div role='feed' className={classNames('item-list', className)}>
{loadPending} {loadPending}
{Children.map(this.props.children, (child, index) => ( {Children.map(this.props.children, (child, index) => (

View File

@ -1,528 +0,0 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { NavLink, withRouter } from 'react-router-dom';
import { isFulfilled, isRejected } from '@reduxjs/toolkit';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react';
import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import { Avatar } from 'mastodon/components/avatar';
import { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge';
import { Button } from 'mastodon/components/button';
import { CopyIconButton } from 'mastodon/components/copy_icon_button';
import { FollowersCounter, FollowingCounter, StatusesCounter } from 'mastodon/components/counters';
import { Icon } from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { ShortNumber } from 'mastodon/components/short_number';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import AccountNoteContainer from '../containers/account_note_container';
import FollowRequestNoteContainer from '../containers/follow_request_note_container';
import { DomainPill } from './domain_pill';
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' },
mutual: { id: 'account.mutual', defaultMessage: 'Mutual' },
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
direct: { id: 'account.direct', defaultMessage: 'Privately mention @{name}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
report: { id: 'account.report', defaultMessage: 'Report @{name}' },
share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
copy: { id: 'account.copy', defaultMessage: 'Copy link to profile' },
media: { id: 'account.media', defaultMessage: 'Media' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' },
disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' },
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
});
const titleFromAccount = account => {
const displayName = account.get('display_name');
const acct = account.get('acct') === account.get('username') ? `${account.get('username')}@${localDomain}` : account.get('acct');
const prefix = displayName.trim().length === 0 ? account.get('username') : displayName;
return `${prefix} (@${acct})`;
};
const messageForFollowButton = relationship => {
if(!relationship) return messages.follow;
if (relationship.get('following') && relationship.get('followed_by')) {
return messages.mutual;
} else if (relationship.get('following') || relationship.get('requested')) {
return messages.unfollow;
} else if (relationship.get('followed_by')) {
return messages.followBack;
} else {
return messages.follow;
}
};
const dateFormatOptions = {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
};
class Header extends ImmutablePureComponent {
static propTypes = {
identity: identityContextPropShape,
account: ImmutablePropTypes.record,
identity_props: ImmutablePropTypes.list,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,
onReblogToggle: PropTypes.func.isRequired,
onNotifyToggle: PropTypes.func.isRequired,
onReport: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
onBlockDomain: PropTypes.func.isRequired,
onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
onEditAccountNote: PropTypes.func.isRequired,
onChangeLanguages: PropTypes.func.isRequired,
onInteractionModal: PropTypes.func.isRequired,
onOpenAvatar: PropTypes.func.isRequired,
onOpenURL: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
domain: PropTypes.string.isRequired,
hidden: PropTypes.bool,
...WithRouterPropTypes,
};
setRef = c => {
this.node = c;
};
openEditProfile = () => {
window.open('/settings/profile', '_blank');
};
isStatusesPageActive = (match, location) => {
if (!match) {
return false;
}
return !location.pathname.match(/\/(followers|following)\/?$/);
};
handleMouseEnter = ({ currentTarget }) => {
if (autoPlayGif) {
return;
}
const emojis = currentTarget.querySelectorAll('.custom-emoji');
for (var i = 0; i < emojis.length; i++) {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-original');
}
};
handleMouseLeave = ({ currentTarget }) => {
if (autoPlayGif) {
return;
}
const emojis = currentTarget.querySelectorAll('.custom-emoji');
for (var i = 0; i < emojis.length; i++) {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-static');
}
};
handleAvatarClick = e => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.props.onOpenAvatar();
}
};
handleShare = () => {
const { account } = this.props;
navigator.share({
url: account.get('url'),
}).catch((e) => {
if (e.name !== 'AbortError') console.error(e);
});
};
handleHashtagClick = e => {
const { history } = this.props;
const value = e.currentTarget.textContent.replace(/^#/, '');
if (history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
history.push(`/tags/${value}`);
}
};
handleMentionClick = e => {
const { history, onOpenURL } = this.props;
if (history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
const link = e.currentTarget;
onOpenURL(link.href).then((result) => {
if (isFulfilled(result)) {
if (result.payload.accounts[0]) {
history.push(`/@${result.payload.accounts[0].acct}`);
} else if (result.payload.statuses[0]) {
history.push(`/@${result.payload.statuses[0].account.acct}/${result.payload.statuses[0].id}`);
} else {
window.location = link.href;
}
} else if (isRejected(result)) {
window.location = link.href;
}
}).catch(() => {
// Nothing
});
}
};
_attachLinkEvents () {
const node = this.node;
if (!node) {
return;
}
const links = node.querySelectorAll('a');
let link;
for (var i = 0; i < links.length; ++i) {
link = links[i];
if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
link.addEventListener('click', this.handleHashtagClick, false);
} else if (link.classList.contains('mention')) {
link.addEventListener('click', this.handleMentionClick, false);
}
}
}
componentDidMount () {
this._attachLinkEvents();
}
componentDidUpdate () {
this._attachLinkEvents();
}
render () {
const { account, hidden, intl } = this.props;
const { signedIn, permissions } = this.props.identity;
if (!account) {
return null;
}
const suspended = account.get('suspended');
const isRemote = account.get('acct') !== account.get('username');
const remoteDomain = isRemote ? account.get('acct').split('@')[1] : null;
let actionBtn, bellBtn, lockedIcon, shareBtn;
let info = [];
let menu = [];
if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) {
info.push(<span key='blocked' className='relationship-tag'><FormattedMessage id='account.blocked' defaultMessage='Blocked' /></span>);
}
if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) {
info.push(<span key='muted' className='relationship-tag'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>);
} else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) {
info.push(<span key='domain_blocked' className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain blocked' /></span>);
}
if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
bellBtn = <IconButton icon={account.getIn(['relationship', 'notifying']) ? 'bell' : 'bell-o'} iconComponent={account.getIn(['relationship', 'notifying']) ? NotificationsActiveIcon : NotificationsIcon} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
}
if ('share' in navigator) {
shareBtn = <IconButton className='optional' iconComponent={ShareIcon} title={intl.formatMessage(messages.share, { name: account.get('username') })} onClick={this.handleShare} />;
} else {
shareBtn = <CopyIconButton className='optional' title={intl.formatMessage(messages.copy)} value={account.get('url')} />;
}
if (me !== account.get('id')) {
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = <Button disabled><LoadingIndicator /></Button>;
} else if (!account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) })} text={intl.formatMessage(messageForFollowButton(account.get('relationship')))} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
} else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
}
} else {
actionBtn = <Button text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
}
if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
actionBtn = '';
}
if (account.get('locked')) {
lockedIcon = <Icon id='lock' icon={LockIcon} title={intl.formatMessage(messages.account_locked)} />;
}
if (signedIn && account.get('id') !== me && !account.get('suspended')) {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
menu.push(null);
}
if (isRemote) {
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
menu.push(null);
}
if (account.get('id') === me) {
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
} else if (signedIn) {
if (account.getIn(['relationship', 'following'])) {
if (!account.getIn(['relationship', 'muting'])) {
if (account.getIn(['relationship', 'showing_reblogs'])) {
menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
} else {
menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
}
menu.push({ text: intl.formatMessage(messages.languages), action: this.props.onChangeLanguages });
menu.push(null);
}
menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
menu.push(null);
}
if (account.getIn(['relationship', 'muting'])) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
} else {
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute, dangerous: true });
}
if (account.getIn(['relationship', 'blocking'])) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock });
} else {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true });
}
if (!account.get('suspended')) {
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true });
}
}
if (signedIn && isRemote) {
menu.push(null);
if (account.getIn(['relationship', 'domain_blocking'])) {
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain: remoteDomain }), action: this.props.onUnblockDomain });
} else {
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain, dangerous: true });
}
}
if ((account.get('id') !== me && (permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
menu.push(null);
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` });
}
if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: remoteDomain }), href: `/admin/instances/${remoteDomain}` });
}
}
const content = { __html: account.get('note_emojified') };
const displayNameHtml = { __html: account.get('display_name_html') };
const fields = account.get('fields');
const isLocal = account.get('acct').indexOf('@') === -1;
const username = account.get('acct').split('@')[0];
const domain = isLocal ? localDomain : account.get('acct').split('@')[1];
const isIndexable = !account.get('noindex');
const badges = [];
if (account.get('bot')) {
badges.push(<AutomatedBadge key='bot-badge' />);
} else if (account.get('group')) {
badges.push(<GroupBadge key='group-badge' />);
}
account.get('roles', []).forEach((role) => {
badges.push(<Badge key={`role-badge-${role.get('id')}`} label={<span>{role.get('name')}</span>} domain={domain} roleId={role.get('id')} />);
});
return (
<div className={classNames('account__header', { inactive: !!account.get('moved') })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
{!(suspended || hidden || account.get('moved')) && account.getIn(['relationship', 'requested_by']) && <FollowRequestNoteContainer account={account} />}
<div className='account__header__image'>
<div className='account__header__info'>
{info}
</div>
{!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />}
</div>
<div className='account__header__bar'>
<div className='account__header__tabs'>
<a className='avatar' href={account.get('avatar')} rel='noopener' target='_blank' onClick={this.handleAvatarClick}>
<Avatar account={suspended || hidden ? undefined : account} size={90} />
</a>
<div className='account__header__tabs__buttons'>
{!hidden && bellBtn}
{!hidden && shareBtn}
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
{!hidden && actionBtn}
</div>
</div>
<div className='account__header__tabs__name'>
<h1>
<span dangerouslySetInnerHTML={displayNameHtml} />
<small>
<span>@{username}<span className='invisible'>@{domain}</span></span>
<DomainPill username={username} domain={domain} isSelf={me === account.get('id')} />
{lockedIcon}
</small>
</h1>
</div>
{badges.length > 0 && (
<div className='account__header__badges'>
{badges}
</div>
)}
{!(suspended || hidden) && (
<div className='account__header__extra'>
<div className='account__header__bio' ref={this.setRef}>
{(account.get('id') !== me && signedIn) && <AccountNoteContainer account={account} />}
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />}
<div className='account__header__fields'>
<dl>
<dt><FormattedMessage id='account.joined_short' defaultMessage='Joined' /></dt>
<dd>{intl.formatDate(account.get('created_at'), { year: 'numeric', month: 'short', day: '2-digit' })}</dd>
</dl>
{fields.map((pair, i) => (
<dl key={i} className={classNames({ verified: pair.get('verified_at') })}>
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' />
<dd className='translate' title={pair.get('value_plain')}>
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' icon={CheckIcon} className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
</dd>
</dl>
))}
</div>
</div>
<div className='account__header__extra__links'>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}>
<ShortNumber
value={account.get('statuses_count')}
renderer={StatusesCounter}
/>
</NavLink>
<NavLink exact activeClassName='active' to={`/@${account.get('acct')}/following`} title={intl.formatNumber(account.get('following_count'))}>
<ShortNumber
value={account.get('following_count')}
renderer={FollowingCounter}
/>
</NavLink>
<NavLink exact activeClassName='active' to={`/@${account.get('acct')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
<ShortNumber
value={account.get('followers_count')}
renderer={FollowersCounter}
/>
</NavLink>
</div>
</div>
)}
</div>
<Helmet>
<title>{titleFromAccount(account)}</title>
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
<link rel='canonical' href={account.get('url')} />
</Helmet>
</div>
);
}
}
export default withRouter(withIdentity(injectIntl(Header)));

View File

@ -11,11 +11,15 @@ import { Icon } from 'mastodon/components/icon';
import { formatTime } from 'mastodon/features/video'; import { formatTime } from 'mastodon/features/video';
import { autoPlayGif, displayMedia, useBlurhash } from 'mastodon/initial_state'; import { autoPlayGif, displayMedia, useBlurhash } from 'mastodon/initial_state';
import type { Status, MediaAttachment } from 'mastodon/models/status'; import type { Status, MediaAttachment } from 'mastodon/models/status';
import { useAppSelector } from 'mastodon/store';
export const MediaItem: React.FC<{ export const MediaItem: React.FC<{
attachment: MediaAttachment; attachment: MediaAttachment;
onOpenMedia: (arg0: MediaAttachment) => void; onOpenMedia: (arg0: MediaAttachment) => void;
}> = ({ attachment, onOpenMedia }) => { }> = ({ attachment, onOpenMedia }) => {
const account = useAppSelector((state) =>
state.accounts.get(attachment.getIn(['status', 'account']) as string),
);
const [visible, setVisible] = useState( const [visible, setVisible] = useState(
(displayMedia !== 'hide_all' && (displayMedia !== 'hide_all' &&
!attachment.getIn(['status', 'sensitive'])) || !attachment.getIn(['status', 'sensitive'])) ||
@ -70,7 +74,6 @@ export const MediaItem: React.FC<{
const lang = status.get('language') as string; const lang = status.get('language') as string;
const blurhash = attachment.get('blurhash') as string; const blurhash = attachment.get('blurhash') as string;
const statusId = status.get('id') as string; const statusId = status.get('id') as string;
const acct = status.getIn(['account', 'acct']) as string;
const type = attachment.get('type') as string; const type = attachment.get('type') as string;
let thumbnail; let thumbnail;
@ -181,7 +184,7 @@ export const MediaItem: React.FC<{
<a <a
className='media-gallery__item-thumbnail' className='media-gallery__item-thumbnail'
href={`/@${acct}/${statusId}`} href={`/@${account?.acct}/${statusId}`}
onClick={handleClick} onClick={handleClick}
target='_blank' target='_blank'
rel='noopener noreferrer' rel='noopener noreferrer'

View File

@ -1,241 +0,0 @@
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
import { LoadMore } from 'mastodon/components/load_more';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import ScrollContainer from 'mastodon/containers/scroll_container';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountGallery } from 'mastodon/selectors';
import { expandAccountMediaTimeline } from '../../actions/timelines';
import HeaderContainer from '../account_timeline/containers/header_container';
import Column from '../ui/components/column';
import { MediaItem } from './components/media_item';
const mapStateToProps = (state, { params: { acct, id } }) => {
const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]);
if (!accountId) {
return {
isLoading: true,
};
}
return {
accountId,
isAccount: !!state.getIn(['accounts', accountId]),
attachments: getAccountGallery(state, accountId),
isLoading: state.getIn(['timelines', `account:${accountId}:media`, 'isLoading']),
hasMore: state.getIn(['timelines', `account:${accountId}:media`, 'hasMore']),
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
};
};
class LoadMoreMedia extends ImmutablePureComponent {
static propTypes = {
maxId: PropTypes.string,
onLoadMore: PropTypes.func.isRequired,
};
handleLoadMore = () => {
this.props.onLoadMore(this.props.maxId);
};
render () {
return (
<LoadMore
disabled={this.props.disabled}
onClick={this.handleLoadMore}
/>
);
}
}
class AccountGallery extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.shape({
acct: PropTypes.string,
id: PropTypes.string,
}).isRequired,
accountId: PropTypes.string,
dispatch: PropTypes.func.isRequired,
attachments: ImmutablePropTypes.list.isRequired,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
isAccount: PropTypes.bool,
blockedBy: PropTypes.bool,
suspended: PropTypes.bool,
multiColumn: PropTypes.bool,
};
state = {
width: 323,
};
_load () {
const { accountId, isAccount, dispatch } = this.props;
if (!isAccount) dispatch(fetchAccount(accountId));
dispatch(expandAccountMediaTimeline(accountId));
}
componentDidMount () {
const { params: { acct }, accountId, dispatch } = this.props;
if (accountId) {
this._load();
} else {
dispatch(lookupAccount(acct));
}
}
componentDidUpdate (prevProps) {
const { params: { acct }, accountId, dispatch } = this.props;
if (prevProps.accountId !== accountId && accountId) {
this._load();
} else if (prevProps.params.acct !== acct) {
dispatch(lookupAccount(acct));
}
}
handleScrollToBottom = () => {
if (this.props.hasMore) {
this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined);
}
};
handleScroll = e => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
const offset = scrollHeight - scrollTop - clientHeight;
if (150 > offset && !this.props.isLoading) {
this.handleScrollToBottom();
}
};
handleLoadMore = maxId => {
this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, { maxId }));
};
handleLoadOlder = e => {
e.preventDefault();
this.handleScrollToBottom();
};
handleOpenMedia = attachment => {
const { dispatch } = this.props;
const statusId = attachment.getIn(['status', 'id']);
const lang = attachment.getIn(['status', 'language']);
if (attachment.get('type') === 'video') {
dispatch(openModal({
modalType: 'VIDEO',
modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } },
}));
} else if (attachment.get('type') === 'audio') {
dispatch(openModal({
modalType: 'AUDIO',
modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } },
}));
} else {
const media = attachment.getIn(['status', 'media_attachments']);
const index = media.findIndex(x => x.get('id') === attachment.get('id'));
dispatch(openModal({
modalType: 'MEDIA',
modalProps: { media, index, statusId, lang },
}));
}
};
handleRef = c => {
if (c) {
this.setState({ width: c.offsetWidth });
}
};
render () {
const { attachments, isLoading, hasMore, isAccount, multiColumn, blockedBy, suspended } = this.props;
const { width } = this.state;
if (!isAccount) {
return (
<BundleColumnError multiColumn={multiColumn} errorType='routing' />
);
}
if (!attachments && isLoading) {
return (
<Column>
<LoadingIndicator />
</Column>
);
}
let loadOlder = null;
if (hasMore && !(isLoading && attachments.size === 0)) {
loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
}
let emptyMessage;
if (suspended) {
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
} else if (blockedBy) {
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
}
return (
<Column>
<ColumnBackButton />
<ScrollContainer scrollKey='account_gallery'>
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
<HeaderContainer accountId={this.props.accountId} />
{(suspended || blockedBy) ? (
<div className='empty-column-indicator'>
{emptyMessage}
</div>
) : (
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
{attachments.map((attachment, index) => attachment === null ? (
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
) : (
<MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
))}
{loadOlder}
</div>
)}
{isLoading && attachments.size === 0 && (
<div className='scrollable__append'>
<LoadingIndicator />
</div>
)}
</div>
</ScrollContainer>
</Column>
);
}
}
export default connect(mapStateToProps)(AccountGallery);

View File

@ -0,0 +1,283 @@
import { useEffect, useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { useParams } from 'react-router-dom';
import { createSelector } from '@reduxjs/toolkit';
import type { Map as ImmutableMap } from 'immutable';
import { List as ImmutableList } from 'immutable';
import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal';
import { expandAccountMediaTimeline } from 'mastodon/actions/timelines';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
import ScrollableList from 'mastodon/components/scrollable_list';
import { TimelineHint } from 'mastodon/components/timeline_hint';
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
import { LimitedAccountHint } from 'mastodon/features/account_timeline/components/limited_account_hint';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import Column from 'mastodon/features/ui/components/column';
import type { MediaAttachment } from 'mastodon/models/media_attachment';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors/accounts';
import type { RootState } from 'mastodon/store';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { MediaItem } from './components/media_item';
const getAccountGallery = createSelector(
[
(state: RootState, accountId: string) =>
(state.timelines as ImmutableMap<string, unknown>).getIn(
[`account:${accountId}:media`, 'items'],
ImmutableList(),
) as ImmutableList<string>,
(state: RootState) => state.statuses,
],
(statusIds, statuses) => {
let items = ImmutableList<MediaAttachment>();
statusIds.forEach((statusId) => {
const status = statuses.get(statusId) as
| ImmutableMap<string, unknown>
| undefined;
if (status) {
items = items.concat(
(
status.get('media_attachments') as ImmutableList<MediaAttachment>
).map((media) => media.set('status', status)),
);
}
});
return items;
},
);
interface Params {
acct?: string;
id?: string;
}
const RemoteHint: React.FC<{
accountId: string;
}> = ({ accountId }) => {
const account = useAppSelector((state) => state.accounts.get(accountId));
const acct = account?.acct;
const url = account?.url;
const domain = acct ? acct.split('@')[1] : undefined;
if (!url) {
return null;
}
return (
<TimelineHint
url={url}
message={
<FormattedMessage
id='hints.profiles.posts_may_be_missing'
defaultMessage='Some posts from this profile may be missing.'
/>
}
label={
<FormattedMessage
id='hints.profiles.see_more_posts'
defaultMessage='See more posts on {domain}'
values={{ domain: <strong>{domain}</strong> }}
/>
}
/>
);
};
export const AccountGallery: React.FC<{
multiColumn: boolean;
}> = ({ multiColumn }) => {
const { acct, id } = useParams<Params>();
const dispatch = useAppDispatch();
const accountId = useAppSelector(
(state) =>
id ??
(state.accounts_map.get(normalizeForLookup(acct)) as string | undefined),
);
const attachments = useAppSelector((state) =>
accountId
? getAccountGallery(state, accountId)
: ImmutableList<MediaAttachment>(),
);
const isLoading = useAppSelector((state) =>
(state.timelines as ImmutableMap<string, unknown>).getIn([
`account:${accountId}:media`,
'isLoading',
]),
);
const hasMore = useAppSelector((state) =>
(state.timelines as ImmutableMap<string, unknown>).getIn([
`account:${accountId}:media`,
'hasMore',
]),
);
const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined,
);
const blockedBy = useAppSelector(
(state) =>
state.relationships.getIn([accountId, 'blocked_by'], false) as boolean,
);
const suspended = useAppSelector(
(state) => state.accounts.getIn([accountId, 'suspended'], false) as boolean,
);
const isAccount = !!account;
const remote = account?.acct !== account?.username;
const hidden = useAppSelector((state) =>
accountId ? getAccountHidden(state, accountId) : false,
);
const maxId = attachments.last()?.getIn(['status', 'id']) as
| string
| undefined;
useEffect(() => {
if (!accountId) {
dispatch(lookupAccount(acct));
}
}, [dispatch, accountId, acct]);
useEffect(() => {
if (accountId && !isAccount) {
dispatch(fetchAccount(accountId));
}
if (accountId && isAccount) {
void dispatch(expandAccountMediaTimeline(accountId));
}
}, [dispatch, accountId, isAccount]);
const handleLoadMore = useCallback(() => {
if (maxId) {
void dispatch(expandAccountMediaTimeline(accountId, { maxId }));
}
}, [dispatch, accountId, maxId]);
const handleOpenMedia = useCallback(
(attachment: MediaAttachment) => {
const statusId = attachment.getIn(['status', 'id']);
const lang = attachment.getIn(['status', 'language']);
if (attachment.get('type') === 'video') {
dispatch(
openModal({
modalType: 'VIDEO',
modalProps: {
media: attachment,
statusId,
lang,
options: { autoPlay: true },
},
}),
);
} else if (attachment.get('type') === 'audio') {
dispatch(
openModal({
modalType: 'AUDIO',
modalProps: {
media: attachment,
statusId,
lang,
options: { autoPlay: true },
},
}),
);
} else {
const media = attachment.getIn([
'status',
'media_attachments',
]) as ImmutableList<MediaAttachment>;
const index = media.findIndex(
(x) => x.get('id') === attachment.get('id'),
);
dispatch(
openModal({
modalType: 'MEDIA',
modalProps: { media, index, statusId, lang },
}),
);
}
},
[dispatch],
);
if (accountId && !isAccount) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
}
let emptyMessage;
if (accountId) {
if (suspended) {
emptyMessage = (
<FormattedMessage
id='empty_column.account_suspended'
defaultMessage='Account suspended'
/>
);
} else if (hidden) {
emptyMessage = <LimitedAccountHint accountId={accountId} />;
} else if (blockedBy) {
emptyMessage = (
<FormattedMessage
id='empty_column.account_unavailable'
defaultMessage='Profile unavailable'
/>
);
} else if (remote && attachments.isEmpty()) {
emptyMessage = <RemoteHint accountId={accountId} />;
} else {
emptyMessage = (
<FormattedMessage
id='empty_column.account_timeline'
defaultMessage='No posts found'
/>
);
}
}
const forceEmptyState = suspended || blockedBy || hidden;
return (
<Column>
<ColumnBackButton />
<ScrollableList
className='account-gallery__container'
prepend={
accountId && (
<AccountHeader accountId={accountId} hideTabs={forceEmptyState} />
)
}
alwaysPrepend
append={remote && accountId && <RemoteHint accountId={accountId} />}
scrollKey='account_gallery'
isLoading={isLoading}
hasMore={!forceEmptyState && hasMore}
onLoadMore={handleLoadMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{attachments.map((attachment) => (
<MediaItem
key={attachment.get('id') as string}
attachment={attachment}
onOpenMedia={handleOpenMedia}
/>
))}
</ScrollableList>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default AccountGallery;

File diff suppressed because it is too large Load Diff

View File

@ -1,155 +0,0 @@
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { NavLink } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import InnerHeader from '../../account/components/header';
import MemorialNote from './memorial_note';
import MovedNote from './moved_note';
class Header extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.record,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,
onReblogToggle: PropTypes.func.isRequired,
onReport: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
onBlockDomain: PropTypes.func.isRequired,
onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
onChangeLanguages: PropTypes.func.isRequired,
onInteractionModal: PropTypes.func.isRequired,
onOpenAvatar: PropTypes.func.isRequired,
onOpenURL: PropTypes.func.isRequired,
hideTabs: PropTypes.bool,
domain: PropTypes.string.isRequired,
hidden: PropTypes.bool,
};
handleFollow = () => {
this.props.onFollow(this.props.account);
};
handleBlock = () => {
this.props.onBlock(this.props.account);
};
handleMention = () => {
this.props.onMention(this.props.account);
};
handleDirect = () => {
this.props.onDirect(this.props.account);
};
handleReport = () => {
this.props.onReport(this.props.account);
};
handleReblogToggle = () => {
this.props.onReblogToggle(this.props.account);
};
handleNotifyToggle = () => {
this.props.onNotifyToggle(this.props.account);
};
handleMute = () => {
this.props.onMute(this.props.account);
};
handleBlockDomain = () => {
this.props.onBlockDomain(this.props.account);
};
handleUnblockDomain = () => {
const domain = this.props.account.get('acct').split('@')[1];
if (!domain) return;
this.props.onUnblockDomain(domain);
};
handleEndorseToggle = () => {
this.props.onEndorseToggle(this.props.account);
};
handleAddToList = () => {
this.props.onAddToList(this.props.account);
};
handleEditAccountNote = () => {
this.props.onEditAccountNote(this.props.account);
};
handleChangeLanguages = () => {
this.props.onChangeLanguages(this.props.account);
};
handleInteractionModal = () => {
this.props.onInteractionModal(this.props.account);
};
handleOpenAvatar = () => {
this.props.onOpenAvatar(this.props.account);
};
render () {
const { account, hidden, hideTabs } = this.props;
if (account === null) {
return null;
}
return (
<div className='account-timeline__header'>
{(!hidden && account.get('memorial')) && <MemorialNote />}
{(!hidden && account.get('moved')) && <MovedNote from={account} to={account.get('moved')} />}
<InnerHeader
account={account}
onFollow={this.handleFollow}
onBlock={this.handleBlock}
onMention={this.handleMention}
onDirect={this.handleDirect}
onReblogToggle={this.handleReblogToggle}
onNotifyToggle={this.handleNotifyToggle}
onReport={this.handleReport}
onMute={this.handleMute}
onBlockDomain={this.handleBlockDomain}
onUnblockDomain={this.handleUnblockDomain}
onEndorseToggle={this.handleEndorseToggle}
onAddToList={this.handleAddToList}
onEditAccountNote={this.handleEditAccountNote}
onChangeLanguages={this.handleChangeLanguages}
onInteractionModal={this.handleInteractionModal}
onOpenAvatar={this.handleOpenAvatar}
onOpenURL={this.props.onOpenURL}
domain={this.props.domain}
hidden={hidden}
/>
{!(hideTabs || hidden) && (
<div className='account__section-headline'>
<NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Posts' /></NavLink>
<NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Posts and replies' /></NavLink>
<NavLink exact to={`/@${account.get('acct')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
</div>
)}
</div>
);
}
}
export default Header;

View File

@ -1,11 +1,12 @@
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
const MemorialNote = () => ( export const MemorialNote: React.FC = () => (
<div className='account-memorial-banner'> <div className='account-memorial-banner'>
<div className='account-memorial-banner__message'> <div className='account-memorial-banner__message'>
<FormattedMessage id='account.in_memoriam' defaultMessage='In Memoriam.' /> <FormattedMessage
id='account.in_memoriam'
defaultMessage='In Memoriam.'
/>
</div> </div>
</div> </div>
); );
export default MemorialNote;

View File

@ -1,39 +0,0 @@
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { AvatarOverlay } from '../../../components/avatar_overlay';
import { DisplayName } from '../../../components/display_name';
export default class MovedNote extends ImmutablePureComponent {
static propTypes = {
from: ImmutablePropTypes.map.isRequired,
to: ImmutablePropTypes.map.isRequired,
};
render () {
const { from, to } = this.props;
return (
<div className='moved-account-banner'>
<div className='moved-account-banner__message'>
<FormattedMessage id='account.moved_to' defaultMessage='{name} has indicated that their new account is now:' values={{ name: <bdi><strong dangerouslySetInnerHTML={{ __html: from.get('display_name_html') }} /></bdi> }} />
</div>
<div className='moved-account-banner__action'>
<Link to={`/@${to.get('acct')}`} className='detailed-status__display-name'>
<div className='detailed-status__display-avatar'><AvatarOverlay account={to} friend={from} /></div>
<DisplayName account={to} />
</Link>
<Link to={`/@${to.get('acct')}`} className='button'><FormattedMessage id='account.go_to_profile' defaultMessage='Go to profile' /></Link>
</div>
</div>
);
}
}

View File

@ -0,0 +1,53 @@
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { AvatarOverlay } from 'mastodon/components/avatar_overlay';
import { DisplayName } from 'mastodon/components/display_name';
import { useAppSelector } from 'mastodon/store';
export const MovedNote: React.FC<{
accountId: string;
targetAccountId: string;
}> = ({ accountId, targetAccountId }) => {
const from = useAppSelector((state) => state.accounts.get(accountId));
const to = useAppSelector((state) => state.accounts.get(targetAccountId));
return (
<div className='moved-account-banner'>
<div className='moved-account-banner__message'>
<FormattedMessage
id='account.moved_to'
defaultMessage='{name} has indicated that their new account is now:'
values={{
name: (
<bdi>
<strong
dangerouslySetInnerHTML={{
__html: from?.display_name_html ?? '',
}}
/>
</bdi>
),
}}
/>
</div>
<div className='moved-account-banner__action'>
<Link to={`/@${to?.acct}`} className='detailed-status__display-name'>
<div className='detailed-status__display-avatar'>
<AvatarOverlay account={to} friend={from} />
</div>
<DisplayName account={to} />
</Link>
<Link to={`/@${to?.acct}`} className='button'>
<FormattedMessage
id='account.go_to_profile'
defaultMessage='Go to profile'
/>
</Link>
</div>
</div>
);
};

View File

@ -1,153 +0,0 @@
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { openURL } from 'mastodon/actions/search';
import {
followAccount,
unblockAccount,
unmuteAccount,
pinAccount,
unpinAccount,
} from '../../../actions/accounts';
import { initBlockModal } from '../../../actions/blocks';
import {
mentionCompose,
directCompose,
} from '../../../actions/compose';
import { initDomainBlockModal, unblockDomain } from '../../../actions/domain_blocks';
import { openModal } from '../../../actions/modal';
import { initMuteModal } from '../../../actions/mutes';
import { initReport } from '../../../actions/reports';
import { makeGetAccount, getAccountHidden } from '../../../selectors';
import Header from '../components/header';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, { accountId }) => ({
account: getAccount(state, accountId),
domain: state.getIn(['meta', 'domain']),
hidden: getAccountHidden(state, accountId),
});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch) => ({
onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
dispatch(openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }));
} else {
dispatch(followAccount(account.get('id')));
}
},
onInteractionModal (account) {
dispatch(openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'follow',
accountId: account.get('id'),
url: account.get('uri'),
},
}));
},
onBlock (account) {
if (account.getIn(['relationship', 'blocking'])) {
dispatch(unblockAccount(account.get('id')));
} else {
dispatch(initBlockModal(account));
}
},
onMention (account) {
dispatch(mentionCompose(account));
},
onDirect (account) {
dispatch(directCompose(account));
},
onReblogToggle (account) {
if (account.getIn(['relationship', 'showing_reblogs'])) {
dispatch(followAccount(account.get('id'), { reblogs: false }));
} else {
dispatch(followAccount(account.get('id'), { reblogs: true }));
}
},
onEndorseToggle (account) {
if (account.getIn(['relationship', 'endorsed'])) {
dispatch(unpinAccount(account.get('id')));
} else {
dispatch(pinAccount(account.get('id')));
}
},
onNotifyToggle (account) {
if (account.getIn(['relationship', 'notifying'])) {
dispatch(followAccount(account.get('id'), { notify: false }));
} else {
dispatch(followAccount(account.get('id'), { notify: true }));
}
},
onReport (account) {
dispatch(initReport(account));
},
onMute (account) {
if (account.getIn(['relationship', 'muting'])) {
dispatch(unmuteAccount(account.get('id')));
} else {
dispatch(initMuteModal(account));
}
},
onBlockDomain (account) {
dispatch(initDomainBlockModal(account));
},
onUnblockDomain (domain) {
dispatch(unblockDomain(domain));
},
onAddToList (account) {
dispatch(openModal({
modalType: 'LIST_ADDER',
modalProps: {
accountId: account.get('id'),
},
}));
},
onChangeLanguages (account) {
dispatch(openModal({
modalType: 'SUBSCRIBED_LANGUAGES',
modalProps: {
accountId: account.get('id'),
},
}));
},
onOpenAvatar (account) {
dispatch(openModal({
modalType: 'IMAGE',
modalProps: {
src: account.get('avatar'),
alt: '',
},
}));
},
onOpenURL (url) {
return dispatch(openURL({ url }));
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));

View File

@ -11,7 +11,7 @@ import { TimelineHint } from 'mastodon/components/timeline_hint';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { me } from 'mastodon/initial_state'; import { me } from 'mastodon/initial_state';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map'; import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors'; import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector } from 'mastodon/store'; import { useAppSelector } from 'mastodon/store';
import { lookupAccount, fetchAccount } from '../../actions/accounts'; import { lookupAccount, fetchAccount } from '../../actions/accounts';
@ -22,8 +22,8 @@ import { LoadingIndicator } from '../../components/loading_indicator';
import StatusList from '../../components/status_list'; import StatusList from '../../components/status_list';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import { AccountHeader } from './components/account_header';
import { LimitedAccountHint } from './components/limited_account_hint'; import { LimitedAccountHint } from './components/limited_account_hint';
import HeaderContainer from './containers/header_container';
const emptyList = ImmutableList(); const emptyList = ImmutableList();
@ -198,7 +198,7 @@ class AccountTimeline extends ImmutablePureComponent {
<ColumnBackButton /> <ColumnBackButton />
<StatusList <StatusList
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />} prepend={<AccountHeader accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
alwaysPrepend alwaysPrepend
append={remoteMessage} append={remoteMessage}
scrollKey='account_timeline' scrollKey='account_timeline'

View File

@ -6,9 +6,9 @@ import classNames from 'classnames';
import Overlay from 'react-overlays/Overlay'; import Overlay from 'react-overlays/Overlay';
import { useSelectableClick } from '@/hooks/useSelectableClick';
import QuestionMarkIcon from '@/material-icons/400-24px/question_mark.svg?react'; import QuestionMarkIcon from '@/material-icons/400-24px/question_mark.svg?react';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { useSelectableClick } from 'mastodon/hooks/useSelectableClick';
const messages = defineMessages({ const messages = defineMessages({
help: { id: 'info_button.label', defaultMessage: 'Help' }, help: { id: 'info_button.label', defaultMessage: 'Help' },

View File

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

View File

@ -12,11 +12,14 @@ import Overlay from 'react-overlays/Overlay';
import MoodIcon from '@/material-icons/400-20px/mood.svg?react'; import MoodIcon from '@/material-icons/400-20px/mood.svg?react';
import { IconButton } from 'mastodon/components/icon_button'; import { IconButton } from 'mastodon/components/icon_button';
import emojiCompressed from 'mastodon/features/emoji/emoji_compressed';
import { assetHost } from 'mastodon/utils/config'; import { assetHost } from 'mastodon/utils/config';
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji'; import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
const nimblePickerData = emojiCompressed[5];
const messages = defineMessages({ const messages = defineMessages({
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' }, emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
@ -37,15 +40,18 @@ let EmojiPicker, Emoji; // load asynchronously
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
const backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`; const backgroundImageFn = () => `${assetHost}/emoji/sheet_15.png`;
const notFoundFn = () => ( const notFoundFn = () => (
<div className='emoji-mart-no-results'> <div className='emoji-mart-no-results'>
<Emoji <Emoji
data={nimblePickerData}
emoji='sleuth_or_spy' emoji='sleuth_or_spy'
set='twitter' set='twitter'
size={32} size={32}
sheetSize={32} sheetSize={32}
sheetColumns={62}
sheetRows={62}
backgroundImageFn={backgroundImageFn} backgroundImageFn={backgroundImageFn}
/> />
@ -67,7 +73,7 @@ class ModifierPickerMenu extends PureComponent {
this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1); this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
}; };
UNSAFE_componentWillReceiveProps (nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.active) { if (nextProps.active) {
this.attachListeners(); this.attachListeners();
} else { } else {
@ -75,7 +81,7 @@ class ModifierPickerMenu extends PureComponent {
} }
} }
componentWillUnmount () { componentWillUnmount() {
this.removeListeners(); this.removeListeners();
} }
@ -85,12 +91,12 @@ class ModifierPickerMenu extends PureComponent {
} }
}; };
attachListeners () { attachListeners() {
document.addEventListener('click', this.handleDocumentClick, { capture: true }); document.addEventListener('click', this.handleDocumentClick, { capture: true });
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
} }
removeListeners () { removeListeners() {
document.removeEventListener('click', this.handleDocumentClick, { capture: true }); document.removeEventListener('click', this.handleDocumentClick, { capture: true });
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
} }
@ -99,17 +105,17 @@ class ModifierPickerMenu extends PureComponent {
this.node = c; this.node = c;
}; };
render () { render() {
const { active } = this.props; const { active } = this.props;
return ( return (
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}> <div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
<button type='button' onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button> <button type='button' onClick={this.handleClick} data-index={1}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button> <button type='button' onClick={this.handleClick} data-index={2}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button> <button type='button' onClick={this.handleClick} data-index={3}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button> <button type='button' onClick={this.handleClick} data-index={4}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button> <button type='button' onClick={this.handleClick} data-index={5}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button> <button type='button' onClick={this.handleClick} data-index={6}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
</div> </div>
); );
} }
@ -139,12 +145,12 @@ class ModifierPicker extends PureComponent {
this.props.onClose(); this.props.onClose();
}; };
render () { render() {
const { active, modifier } = this.props; const { active, modifier } = this.props;
return ( return (
<div className='emoji-picker-dropdown__modifiers'> <div className='emoji-picker-dropdown__modifiers'>
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} /> <Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} /> <ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
</div> </div>
); );
@ -184,7 +190,7 @@ class EmojiPickerMenuImpl extends PureComponent {
} }
}; };
componentDidMount () { componentDidMount() {
document.addEventListener('click', this.handleDocumentClick, { capture: true }); document.addEventListener('click', this.handleDocumentClick, { capture: true });
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
@ -199,7 +205,7 @@ class EmojiPickerMenuImpl extends PureComponent {
}); });
} }
componentWillUnmount () { componentWillUnmount() {
document.removeEventListener('click', this.handleDocumentClick, { capture: true }); document.removeEventListener('click', this.handleDocumentClick, { capture: true });
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
} }
@ -252,7 +258,7 @@ class EmojiPickerMenuImpl extends PureComponent {
this.props.onSkinTone(modifier); this.props.onSkinTone(modifier);
}; };
render () { render() {
const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props; const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
if (loading) { if (loading) {
@ -280,6 +286,9 @@ class EmojiPickerMenuImpl extends PureComponent {
return ( return (
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}> <div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
<EmojiPicker <EmojiPicker
data={nimblePickerData}
sheetColumns={62}
sheetRows={62}
perLine={8} perLine={8}
emojiSize={22} emojiSize={22}
sheetSize={32} sheetSize={32}
@ -345,7 +354,7 @@ class EmojiPickerDropdown extends PureComponent {
EmojiPickerAsync().then(EmojiMart => { EmojiPickerAsync().then(EmojiMart => {
EmojiPicker = EmojiMart.Picker; EmojiPicker = EmojiMart.Picker;
Emoji = EmojiMart.Emoji; Emoji = EmojiMart.Emoji;
this.setState({ loading: false }); this.setState({ loading: false });
}).catch(() => { }).catch(() => {
@ -386,7 +395,7 @@ class EmojiPickerDropdown extends PureComponent {
this.setState({ placement: state.placement }); this.setState({ placement: state.placement });
}; };
render () { render() {
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props; const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
const title = intl.formatMessage(messages.emoji); const title = intl.formatMessage(messages.emoji);
const { active, loading, placement } = this.state; const { active, loading, placement } = this.state;
@ -403,7 +412,7 @@ class EmojiPickerDropdown extends PureComponent {
/> />
<Overlay show={active} placement={placement} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}> <Overlay show={active} placement={placement} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
{({ props, placement })=> ( {({ props, placement }) => (
<div {...props} style={{ ...props.style }}> <div {...props} style={{ ...props.style }}>
<div className={`dropdown-animation ${placement}`}> <div className={`dropdown-animation ${placement}`}>
<EmojiPickerMenu <EmojiPickerMenu

View File

@ -22,10 +22,9 @@ import { LoadMore } from 'mastodon/components/load_more';
import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { RadioButton } from 'mastodon/components/radio_button'; import { RadioButton } from 'mastodon/components/radio_button';
import ScrollContainer from 'mastodon/containers/scroll_container'; import ScrollContainer from 'mastodon/containers/scroll_container';
import { useSearchParam } from 'mastodon/hooks/useSearchParam';
import { useAppDispatch, useAppSelector } from 'mastodon/store'; import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { useSearchParam } from '../../../hooks/useSearchParam';
import { AccountCard } from './components/account_card'; import { AccountCard } from './components/account_card';
const messages = defineMessages({ const messages = defineMessages({

View File

@ -45,6 +45,7 @@ type EmojiCompressed = [
Category[], Category[],
Data['aliases'], Data['aliases'],
EmojisWithoutShortCodes, EmojisWithoutShortCodes,
Data,
]; ];
/* /*

View File

@ -9,18 +9,91 @@
// This version comment should be bumped each time the emoji data is changed // This version comment should be bumped each time the emoji data is changed
// to ensure that the prevaled file is regenerated by Babel // to ensure that the prevaled file is regenerated by Babel
// version: 2 // version: 3
const { emojiIndex } = require('emoji-mart'); // This json file contains the names of the categories.
let data = require('emoji-mart/data/all.json'); const emojiMart5LocalesData = require('@emoji-mart/data/i18n/en.json');
const emojiMart5Data = require('@emoji-mart/data/sets/15/all.json');
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data'); const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
const _ = require('lodash');
const emojiMap = require('./emoji_map.json'); const emojiMap = require('./emoji_map.json');
// This json file is downloaded from https://github.com/iamcal/emoji-data/
// and is used to correct the sheet coordinates since we're using that repo's sheet
const emojiSheetData = require('./emoji_sheet.json');
const { unicodeToFilename } = require('./unicode_to_filename'); const { unicodeToFilename } = require('./unicode_to_filename');
const { unicodeToUnifiedName } = require('./unicode_to_unified_name'); const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
if(data.compressed) { // Grabbed from `emoji_utils` to avoid circular dependency
data = emojiMartUncompress(data); function unifiedToNative(unified) {
let unicodes = unified.split('-'),
codePoints = unicodes.map((u) => `0x${u}`);
return String.fromCodePoint(...codePoints);
}
let data = {
compressed: true,
categories: emojiMart5Data.categories.map(cat => {
return {
...cat,
name: emojiMart5LocalesData.categories[cat.id]
};
}),
aliases: emojiMart5Data.aliases,
emojis: _(emojiMart5Data.emojis).values().map(emoji => {
let skin_variations = {};
const unified = emoji.skins[0].unified.toUpperCase();
const emojiFromRawData = emojiSheetData.find(e => e.unified === unified);
if (!emojiFromRawData) {
return undefined;
}
if (emoji.skins.length > 1) {
const [, ...nonDefaultSkins] = emoji.skins;
nonDefaultSkins.forEach(skin => {
const [matchingRawCodePoints,matchingRawEmoji] = Object.entries(emojiFromRawData.skin_variations).find((pair) => {
const [, value] = pair;
return value.unified.toLowerCase() === skin.unified;
});
if (matchingRawEmoji && matchingRawCodePoints) {
// At the time of writing, the json from `@emoji-mart/data` doesn't have data
// for emoji like `woman-heart-woman` with two different skin tones.
const skinToneCode = matchingRawCodePoints.split('-')[0];
skin_variations[skinToneCode] = {
unified: matchingRawEmoji.unified.toUpperCase(),
non_qualified: null,
sheet_x: matchingRawEmoji.sheet_x,
sheet_y: matchingRawEmoji.sheet_y,
has_img_twitter: true,
native: unifiedToNative(matchingRawEmoji.unified.toUpperCase())
};
}
});
}
return {
a: emoji.name,
b: unified,
c: undefined,
f: true,
j: [emoji.id, ...emoji.keywords],
k: [emojiFromRawData.sheet_x, emojiFromRawData.sheet_y],
m: emoji.emoticons?.[0],
l: emoji.emoticons,
o: emoji.version,
id: emoji.id,
skin_variations,
native: unifiedToNative(unified.toUpperCase())
};
}).compact().keyBy(e => e.id).mapValues(e => _.omit(e, 'id')).value()
};
if (data.compressed) {
emojiMartUncompress(data);
} }
const emojiMartData = data; const emojiMartData = data;
@ -32,15 +105,10 @@ const shortcodeMap = {};
const shortCodesToEmojiData = {}; const shortCodesToEmojiData = {};
const emojisWithoutShortCodes = []; const emojisWithoutShortCodes = [];
Object.keys(emojiIndex.emojis).forEach(key => { Object.keys(emojiMart5Data.emojis).forEach(key => {
let emoji = emojiIndex.emojis[key]; let emoji = emojiMart5Data.emojis[key];
// Emojis with skin tone modifiers are stored like this shortcodeMap[emoji.skins[0].native] = emoji.id;
if (Object.hasOwn(emoji, '1')) {
emoji = emoji['1'];
}
shortcodeMap[emoji.native] = emoji.id;
}); });
const stripModifiers = unicode => { const stripModifiers = unicode => {
@ -84,13 +152,9 @@ Object.keys(emojiMap).forEach(key => {
} }
}); });
Object.keys(emojiIndex.emojis).forEach(key => { Object.keys(emojiMartData.emojis).forEach(key => {
let emoji = emojiIndex.emojis[key]; let emoji = emojiMartData.emojis[key];
// Emojis with skin tone modifiers are stored like this
if (Object.hasOwn(emoji, '1')) {
emoji = emoji['1'];
}
const { native } = emoji; const { native } = emoji;
let { short_names, search, unified } = emojiMartData.emojis[key]; let { short_names, search, unified } = emojiMartData.emojis[key];
@ -135,4 +199,5 @@ module.exports = JSON.parse(JSON.stringify([
emojiMartData.categories, emojiMartData.categories,
emojiMartData.aliases, emojiMartData.aliases,
emojisWithoutShortCodes, emojisWithoutShortCodes,
emojiMartData
])); ]));

View File

@ -8,14 +8,15 @@ import type { Search, ShortCodesToEmojiData } from './emoji_compressed';
import emojiCompressed from './emoji_compressed'; import emojiCompressed from './emoji_compressed';
import { unicodeToUnifiedName } from './unicode_to_unified_name'; import { unicodeToUnifiedName } from './unicode_to_unified_name';
type Emojis = { type Emojis = Record<
[key in NonNullable<keyof ShortCodesToEmojiData>]: { NonNullable<keyof ShortCodesToEmojiData>,
{
native: BaseEmoji['native']; native: BaseEmoji['native'];
search: Search; search: Search;
short_names: Emoji['short_names']; short_names: Emoji['short_names'];
unified: Emoji['unified']; unified: Emoji['unified'];
}; }
}; >;
const [ const [
shortCodesToEmojiData, shortCodesToEmojiData,

View File

@ -1,5 +1,5 @@
import Emoji from 'emoji-mart/dist-es/components/emoji/emoji'; import Emoji from 'emoji-mart/dist-es/components/emoji/nimble-emoji';
import Picker from 'emoji-mart/dist-es/components/picker/picker'; import Picker from 'emoji-mart/dist-es/components/picker/nimble-picker';
export { export {
Picker, Picker,

File diff suppressed because one or more lines are too long

View File

@ -9,12 +9,13 @@ import type {
import emojiCompressed from './emoji_compressed'; import emojiCompressed from './emoji_compressed';
import { unicodeToFilename } from './unicode_to_filename'; import { unicodeToFilename } from './unicode_to_filename';
type UnicodeMapping = { type UnicodeMapping = Record<
[key in FilenameData[number][0]]: { FilenameData[number][0],
{
shortCode: ShortCodesToEmojiDataKey; shortCode: ShortCodesToEmojiDataKey;
filename: FilenameData[number][number]; filename: FilenameData[number][number];
}; }
}; >;
const [ const [
shortCodesToEmojiData, shortCodesToEmojiData,

View File

@ -10,9 +10,10 @@ import { debounce } from 'lodash';
import { Account } from 'mastodon/components/account'; import { Account } from 'mastodon/components/account';
import { TimelineHint } from 'mastodon/components/timeline_hint'; import { TimelineHint } from 'mastodon/components/timeline_hint';
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map'; import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors'; import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector } from 'mastodon/store'; import { useAppSelector } from 'mastodon/store';
import { import {
@ -25,7 +26,6 @@ import { ColumnBackButton } from '../../components/column_back_button';
import { LoadingIndicator } from '../../components/loading_indicator'; import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint'; import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
import HeaderContainer from '../account_timeline/containers/header_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
const mapStateToProps = (state, { params: { acct, id } }) => { const mapStateToProps = (state, { params: { acct, id } }) => {
@ -168,7 +168,7 @@ class Followers extends ImmutablePureComponent {
hasMore={!forceEmptyState && hasMore} hasMore={!forceEmptyState && hasMore}
isLoading={isLoading} isLoading={isLoading}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />} prepend={<AccountHeader accountId={this.props.accountId} hideTabs />}
alwaysPrepend alwaysPrepend
append={remoteMessage} append={remoteMessage}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}

View File

@ -10,9 +10,10 @@ import { debounce } from 'lodash';
import { Account } from 'mastodon/components/account'; import { Account } from 'mastodon/components/account';
import { TimelineHint } from 'mastodon/components/timeline_hint'; import { TimelineHint } from 'mastodon/components/timeline_hint';
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map'; import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors'; import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector } from 'mastodon/store'; import { useAppSelector } from 'mastodon/store';
import { import {
@ -25,7 +26,6 @@ import { ColumnBackButton } from '../../components/column_back_button';
import { LoadingIndicator } from '../../components/loading_indicator'; import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint'; import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
import HeaderContainer from '../account_timeline/containers/header_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
const mapStateToProps = (state, { params: { acct, id } }) => { const mapStateToProps = (state, { params: { acct, id } }) => {
@ -168,7 +168,7 @@ class Following extends ImmutablePureComponent {
hasMore={!forceEmptyState && hasMore} hasMore={!forceEmptyState && hasMore}
isLoading={isLoading} isLoading={isLoading}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />} prepend={<AccountHeader accountId={this.props.accountId} hideTabs />}
alwaysPrepend alwaysPrepend
append={remoteMessage} append={remoteMessage}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}

View File

@ -17,7 +17,7 @@ export const ColumnSettings: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const onChange = useCallback( const onChange = useCallback(
(key: string, checked: boolean) => { (key: string[], checked: boolean) => {
dispatch(changeSetting(['home', ...key], checked)); dispatch(changeSetting(['home', ...key], checked));
}, },
[dispatch], [dispatch],

View File

@ -4,7 +4,6 @@ import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { useSearchParam } from '@/hooks/useSearchParam';
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react'; import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react'; import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react'; import SearchIcon from '@/material-icons/400-24px/search.svg?react';
@ -20,6 +19,7 @@ import { Icon } from 'mastodon/components/icon';
import ScrollableList from 'mastodon/components/scrollable_list'; import ScrollableList from 'mastodon/components/scrollable_list';
import Status from 'mastodon/containers/status_container'; import Status from 'mastodon/containers/status_container';
import { Search } from 'mastodon/features/compose/components/search'; import { Search } from 'mastodon/features/compose/components/search';
import { useSearchParam } from 'mastodon/hooks/useSearchParam';
import type { Hashtag as HashtagType } from 'mastodon/models/tags'; import type { Hashtag as HashtagType } from 'mastodon/models/tags';
import { useAppDispatch, useAppSelector } from 'mastodon/store'; import { useAppDispatch, useAppSelector } from 'mastodon/store';

View File

@ -6,11 +6,11 @@ import { useEffect, useCallback } from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { useRenderSignal } from 'mastodon/../hooks/useRenderSignal';
import { fetchStatus, toggleStatusSpoilers } from 'mastodon/actions/statuses'; import { fetchStatus, toggleStatusSpoilers } from 'mastodon/actions/statuses';
import { hydrateStore } from 'mastodon/actions/store'; import { hydrateStore } from 'mastodon/actions/store';
import { Router } from 'mastodon/components/router'; import { Router } from 'mastodon/components/router';
import { DetailedStatus } from 'mastodon/features/status/components/detailed_status'; import { DetailedStatus } from 'mastodon/features/status/components/detailed_status';
import { useRenderSignal } from 'mastodon/hooks/useRenderSignal';
import initialState from 'mastodon/initial_state'; import initialState from 'mastodon/initial_state';
import { IntlProvider } from 'mastodon/locales'; import { IntlProvider } from 'mastodon/locales';
import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors'; import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors';

View File

@ -221,12 +221,12 @@ export const DetailedStatus: React.FC<{
/> />
); );
} }
} else if (status.get('spoiler_text').length === 0) { } else if (status.get('card')) {
media = ( media = (
<Card <Card
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
onOpenMedia={onOpenMedia} onOpenMedia={onOpenMedia}
card={status.get('card', null)} card={status.get('card')}
/> />
); );
} }

View File

@ -8,26 +8,31 @@ import {
} from 'react-intl'; } from 'react-intl';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Link, useParams } from 'react-router-dom';
import { apiGetTermsOfService } from 'mastodon/api/instance'; import { apiGetTermsOfService } from 'mastodon/api/instance';
import type { ApiTermsOfServiceJSON } from 'mastodon/api_types/instance'; import type { ApiTermsOfServiceJSON } from 'mastodon/api_types/instance';
import { Column } from 'mastodon/components/column'; import { Column } from 'mastodon/components/column';
import { Skeleton } from 'mastodon/components/skeleton';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'terms_of_service.title', defaultMessage: 'Terms of Service' }, title: { id: 'terms_of_service.title', defaultMessage: 'Terms of Service' },
}); });
interface Params {
date?: string;
}
const TermsOfService: React.FC<{ const TermsOfService: React.FC<{
multiColumn: boolean; multiColumn: boolean;
}> = ({ multiColumn }) => { }> = ({ multiColumn }) => {
const intl = useIntl(); const intl = useIntl();
const { date } = useParams<Params>();
const [response, setResponse] = useState<ApiTermsOfServiceJSON>(); const [response, setResponse] = useState<ApiTermsOfServiceJSON>();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
apiGetTermsOfService() apiGetTermsOfService(date)
.then((data) => { .then((data) => {
setResponse(data); setResponse(data);
setLoading(false); setLoading(false);
@ -36,7 +41,7 @@ const TermsOfService: React.FC<{
.catch(() => { .catch(() => {
setLoading(false); setLoading(false);
}); });
}, []); }, [date]);
if (!loading && !response) { if (!loading && !response) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />; return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
@ -55,23 +60,60 @@ const TermsOfService: React.FC<{
defaultMessage='Terms of Service' defaultMessage='Terms of Service'
/> />
</h3> </h3>
<p> <p className='prose'>
<FormattedMessage {response?.effective ? (
id='privacy_policy.last_updated' <FormattedMessage
defaultMessage='Last updated {date}' id='privacy_policy.last_updated'
values={{ defaultMessage='Last updated {date}'
date: loading ? ( values={{
<Skeleton width='10ch' /> date: (
) : ( <FormattedDate
<FormattedDate value={response.effective_date}
value={response?.updated_at} year='numeric'
year='numeric' month='short'
month='short' day='2-digit'
day='2-digit' />
),
}}
/>
) : (
<FormattedMessage
id='terms_of_service.effective_as_of'
defaultMessage='Effective as of {date}'
values={{
date: (
<FormattedDate
value={response?.effective_date}
year='numeric'
month='short'
day='2-digit'
/>
),
}}
/>
)}
{response?.succeeded_by && (
<>
{' · '}
<Link to={`/terms-of-service/${response.succeeded_by}`}>
<FormattedMessage
id='terms_of_service.upcoming_changes_on'
defaultMessage='Upcoming changes on {date}'
values={{
date: (
<FormattedDate
value={response.succeeded_by}
year='numeric'
month='short'
day='2-digit'
/>
),
}}
/> />
), </Link>
}} </>
/> )}
</p> </p>
</div> </div>

View File

@ -101,6 +101,7 @@ const EmbedModal: React.FC<{
/> />
<iframe <iframe
// eslint-disable-next-line @typescript-eslint/no-deprecated
frameBorder='0' frameBorder='0'
ref={iframeRef} ref={iframeRef}
sandbox='allow-scripts allow-same-origin' sandbox='allow-scripts allow-same-origin'

View File

@ -205,7 +205,7 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} /> <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
<WrappedRoute path='/about' component={About} content={children} /> <WrappedRoute path='/about' component={About} content={children} />
<WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} /> <WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
<WrappedRoute path='/terms-of-service' component={TermsOfService} content={children} /> <WrappedRoute path='/terms-of-service/:date?' component={TermsOfService} content={children} />
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} /> <WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
<Redirect from='/timelines/public' to='/public' exact /> <Redirect from='/timelines/public' to='/public' exact />

View File

@ -592,7 +592,6 @@
"poll_button.remove_poll": "إزالة استطلاع الرأي", "poll_button.remove_poll": "إزالة استطلاع الرأي",
"privacy.change": "اضبط خصوصية المنشور", "privacy.change": "اضبط خصوصية المنشور",
"privacy.direct.long": "كل من ذُكر في المنشور", "privacy.direct.long": "كل من ذُكر في المنشور",
"privacy.direct.short": "أشخاص محددون",
"privacy.private.long": "متابعيك فقط", "privacy.private.long": "متابعيك فقط",
"privacy.private.short": "للمتابِعين", "privacy.private.short": "للمتابِعين",
"privacy.public.long": "أي شخص على أو خارج ماستدون", "privacy.public.long": "أي شخص على أو خارج ماستدون",

View File

@ -4,15 +4,16 @@
"about.disclaimer": "Mastodon ye software gratuito y de códigu llibre, y una marca rexistrada de Mastodon gGmbH.", "about.disclaimer": "Mastodon ye software gratuito y de códigu llibre, y una marca rexistrada de Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "El motivu nun ta disponible", "about.domain_blocks.no_reason_available": "El motivu nun ta disponible",
"about.domain_blocks.preamble": "Polo xeneral, Mastodon permítete ver el conteníu ya interactuar colos perfiles d'otros sirvidores nel fediversu. Estes son les esceiciones que se ficieron nesti sirvidor.", "about.domain_blocks.preamble": "Polo xeneral, Mastodon permítete ver el conteníu ya interactuar colos perfiles d'otros sirvidores nel fediversu. Estes son les esceiciones que se ficieron nesti sirvidor.",
"about.domain_blocks.silenced.explanation": "Polo xeneral, nun ves los perfiles ya'l conteníu d'esti sirvidor sacante que los busques o decidas siguilos.", "about.domain_blocks.silenced.explanation": "Polo xeneral, nun ves los perfiles y el conteníu d'esti sirvidor sacante que los busques o decidas siguilos.",
"about.domain_blocks.silenced.title": "Llendóse", "about.domain_blocks.silenced.title": "Llendóse",
"about.domain_blocks.suspended.explanation": "Nun se procesa, atroxa nin intercambia nengún datu d'esti sirvidor, lo que fai que cualesquier interaición o comunicación colos sos perfiles seya imposible.", "about.domain_blocks.suspended.explanation": "Nun se procesa, atroxa nin intercambia nengún datu d'esti sirvidor, lo que fai imposible cualesquier interaición o comunicación colos sos perfiles.",
"about.domain_blocks.suspended.title": "Suspendióse", "about.domain_blocks.suspended.title": "Suspendióse",
"about.not_available": "Esta información nun ta disponible nesti sirvidor.", "about.not_available": "Esta información nun ta disponible nesti sirvidor.",
"about.powered_by": "Una rede social descentralizada que tien la teunoloxía de {mastodon}", "about.powered_by": "Una rede social descentralizada que tien la teunoloxía de {mastodon}",
"about.rules": "Normes del sirvidor", "about.rules": "Normes del sirvidor",
"account.account_note_header": "Nota personal", "account.account_note_header": "Nota personal",
"account.add_or_remove_from_list": "Amestar o quitar de les llistes", "account.add_or_remove_from_list": "Amestar o quitar de les llistes",
"account.badges.bot": "Automatizóse",
"account.badges.group": "Grupu", "account.badges.group": "Grupu",
"account.block": "Bloquiar a @{name}", "account.block": "Bloquiar a @{name}",
"account.block_domain": "Bloquiar el dominiu {domain}", "account.block_domain": "Bloquiar el dominiu {domain}",
@ -25,7 +26,7 @@
"account.edit_profile": "Editar el perfil", "account.edit_profile": "Editar el perfil",
"account.enable_notifications": "Avisame cuando @{name} espublice artículos", "account.enable_notifications": "Avisame cuando @{name} espublice artículos",
"account.endorse": "Destacar nel perfil", "account.endorse": "Destacar nel perfil",
"account.featured_tags.last_status_never": "Nun hai nengún artículu", "account.featured_tags.last_status_never": "Nun hai nenguna publicación",
"account.featured_tags.title": "Etiquetes destacaes de: {name}", "account.featured_tags.title": "Etiquetes destacaes de: {name}",
"account.follow": "Siguir", "account.follow": "Siguir",
"account.follow_back": "Siguir tamién", "account.follow_back": "Siguir tamién",
@ -107,6 +108,7 @@
"column.domain_blocks": "Dominios bloquiaos", "column.domain_blocks": "Dominios bloquiaos",
"column.edit_list": "Editar la llista", "column.edit_list": "Editar la llista",
"column.favourites": "Favoritos", "column.favourites": "Favoritos",
"column.firehose": "Feed en direuto",
"column.follow_requests": "Solicitúes de siguimientu", "column.follow_requests": "Solicitúes de siguimientu",
"column.home": "Aniciu", "column.home": "Aniciu",
"column.lists": "Llistes", "column.lists": "Llistes",
@ -126,9 +128,9 @@
"community.column_settings.remote_only": "Namás lo remoto", "community.column_settings.remote_only": "Namás lo remoto",
"compose.language.change": "Camudar la llingua", "compose.language.change": "Camudar la llingua",
"compose.language.search": "Buscar llingües…", "compose.language.search": "Buscar llingües…",
"compose.published.body": "Espublizóse l'artículu.", "compose.published.body": "Publicóse la publicación.",
"compose.published.open": "Abrir", "compose.published.open": "Abrir",
"compose.saved.body": "Post guardáu.", "compose.saved.body": "Guardóse la publicación.",
"compose_form.direct_message_warning_learn_more": "Saber más", "compose_form.direct_message_warning_learn_more": "Saber más",
"compose_form.encryption_warning": "Los artículos de Mastodon nun tán cifraos de puntu a puntu. Nun compartas nengún tipu d'información sensible per Mastodon.", "compose_form.encryption_warning": "Los artículos de Mastodon nun tán cifraos de puntu a puntu. Nun compartas nengún tipu d'información sensible per Mastodon.",
"compose_form.lock_disclaimer": "La to cuenta nun ye {locked}. Cualesquier perfil pue siguite pa ver los artículos que son namás pa siguidores.", "compose_form.lock_disclaimer": "La to cuenta nun ye {locked}. Cualesquier perfil pue siguite pa ver los artículos que son namás pa siguidores.",
@ -137,34 +139,33 @@
"compose_form.poll.option_placeholder": "Opción {number}", "compose_form.poll.option_placeholder": "Opción {number}",
"compose_form.poll.type": "Tipu", "compose_form.poll.type": "Tipu",
"compose_form.publish": "Espublizar", "compose_form.publish": "Espublizar",
"compose_form.publish_form": "Artículu nuevu", "compose_form.publish_form": "Publicación nueva",
"compose_form.reply": "Responder", "compose_form.reply": "Responder",
"confirmation_modal.cancel": "Encaboxar", "confirmation_modal.cancel": "Encaboxar",
"confirmations.block.confirm": "Bloquiar", "confirmations.block.confirm": "Bloquiar",
"confirmations.delete.confirm": "Desaniciar", "confirmations.delete.confirm": "Desaniciar",
"confirmations.delete.message": "¿De xuru que quies desaniciar esti artículu?", "confirmations.delete.message": "¿De xuru que quies desaniciar esta publicación?",
"confirmations.delete.title": "¿Desaniciar l'artículu?", "confirmations.delete.title": "¿Quies desaniciar esta publicación?",
"confirmations.delete_list.confirm": "Desaniciar", "confirmations.delete_list.confirm": "Desaniciar",
"confirmations.delete_list.message": "¿De xuru que quies desaniciar permanentemente esta llista?", "confirmations.delete_list.message": "¿De xuru que quies desaniciar permanentemente esta llista?",
"confirmations.delete_list.title": "¿Desaniciar la llista?", "confirmations.delete_list.title": "¿Quies desaniciar la llista?",
"confirmations.discard_edit_media.confirm": "Escartar", "confirmations.discard_edit_media.confirm": "Escartar",
"confirmations.edit.confirm": "Editar", "confirmations.edit.confirm": "Editar",
"confirmations.edit.message": "La edición va sobrescribir el mensaxe que tas escribiendo. ¿De xuru que quies siguir?", "confirmations.edit.message": "La edición va sobrescribir el mensaxe que tas escribiendo. ¿De xuru que quies siguir?",
"confirmations.follow_to_list.title": "¿Siguir al usuariu?", "confirmations.follow_to_list.title": "¿Siguir al usuariu?",
"confirmations.logout.confirm": "Zarrar la sesión", "confirmations.logout.confirm": "Zarrar la sesión",
"confirmations.logout.message": "¿De xuru que quies zarrar la sesión?", "confirmations.logout.message": "¿De xuru que quies zarrar la sesión?",
"confirmations.logout.title": "¿Zarrar la sesión?", "confirmations.logout.title": "¿Quies zarrar la sesión?",
"confirmations.missing_alt_text.confirm": "Amestar testu alternativu", "confirmations.missing_alt_text.confirm": "Amestar testu alternativu",
"confirmations.missing_alt_text.title": "¿Quies amestar testu alternativu?", "confirmations.missing_alt_text.title": "¿Quies amestar testu alternativu?",
"confirmations.redraft.confirm": "Desaniciar y reeditar", "confirmations.redraft.confirm": "Desaniciar y reeditar",
"confirmations.redraft.message": "¿De xuru que quies desaniciar esti artículu y reeditalu? Van perdese los favoritos y comparticiones, y les rempuestes al artículu orixinal van quedar güérfanes.", "confirmations.redraft.title": "¿Desaniciar y reeditar la publicación?",
"confirmations.redraft.title": "¿Desaniciar ya reeditar l'artículu?",
"confirmations.reply.confirm": "Responder", "confirmations.reply.confirm": "Responder",
"confirmations.reply.message": "Responder agora va sobrescribir el mensaxe que tas componiendo anguaño. ¿De xuru que quies siguir?", "confirmations.reply.message": "Responder agora va sobrescribir el mensaxe que tas componiendo anguaño. ¿De xuru que quies siguir?",
"confirmations.unfollow.confirm": "Dexar de siguir", "confirmations.unfollow.confirm": "Dexar de siguir",
"confirmations.unfollow.message": "¿De xuru que quies dexar de siguir a {name}?", "confirmations.unfollow.message": "¿De xuru que quies dexar de siguir a {name}?",
"confirmations.unfollow.title": "¿Dexar de siguir al usuariu?", "confirmations.unfollow.title": "¿Dexar de siguir al usuariu?",
"content_warning.hide": "Anubrir l'artículu", "content_warning.hide": "Esconder la publicación",
"content_warning.show": "Amosar de toes toes", "content_warning.show": "Amosar de toes toes",
"content_warning.show_more": "Amosar más", "content_warning.show_more": "Amosar más",
"conversation.delete": "Desaniciar la conversación", "conversation.delete": "Desaniciar la conversación",
@ -186,7 +187,7 @@
"domain_block_modal.title": "Bloquiar el dominiu?", "domain_block_modal.title": "Bloquiar el dominiu?",
"domain_pill.server": "Sirvidor", "domain_pill.server": "Sirvidor",
"domain_pill.username": "Nome d'usuariu", "domain_pill.username": "Nome d'usuariu",
"embed.instructions": "Empotra esti artículu nel to sitiu web copiando'l códigu d'abaxo.", "embed.instructions": "Empotra esta publicación nel to sitiu web copiando'l códigu d'abaxo.",
"embed.preview": "Va apaecer asina:", "embed.preview": "Va apaecer asina:",
"emoji_button.activity": "Actividá", "emoji_button.activity": "Actividá",
"emoji_button.flags": "Banderes", "emoji_button.flags": "Banderes",
@ -201,9 +202,9 @@
"emoji_button.symbols": "Símbolos", "emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viaxes y llugares", "emoji_button.travel": "Viaxes y llugares",
"empty_column.account_suspended": "Cuenta suspendida", "empty_column.account_suspended": "Cuenta suspendida",
"empty_column.account_timeline": "¡Equí nun hai nengún artículu!", "empty_column.account_timeline": "¡Equí nun hai nenguna publicación!",
"empty_column.blocks": "Nun bloquiesti a nengún perfil.", "empty_column.blocks": "Nun bloquiesti a nengún perfil.",
"empty_column.bookmarked_statuses": "Nun tienes nengún artículu en Marcadores. Cuando amiestes dalgún, apaez equí.", "empty_column.bookmarked_statuses": "Nun tienes nenguna publicación en Marcadores. Cuando amiestes dalguna, va apaecer equí.",
"empty_column.direct": "Nun tienes nenguna mención privada. Cuando unvies o recibas dalguna, apaez equí.", "empty_column.direct": "Nun tienes nenguna mención privada. Cuando unvies o recibas dalguna, apaez equí.",
"empty_column.domain_blocks": "Nun hai nengún dominiu bloquiáu.", "empty_column.domain_blocks": "Nun hai nengún dominiu bloquiáu.",
"empty_column.explore_statuses": "Agora nun hai nada en tendencia. ¡Volvi equí dempués!", "empty_column.explore_statuses": "Agora nun hai nada en tendencia. ¡Volvi equí dempués!",
@ -223,21 +224,21 @@
"explore.trending_links": "Noticies", "explore.trending_links": "Noticies",
"explore.trending_statuses": "Artículos", "explore.trending_statuses": "Artículos",
"explore.trending_tags": "Etiquetes", "explore.trending_tags": "Etiquetes",
"filter_modal.added.context_mismatch_explanation": "Esta categoría de peñera nun s'aplica al contestu nel qu'accediesti a esti artículu. Si tamién quies que se peñere l'artículu nesti contestu, tienes d'editar la peñera.", "filter_modal.added.context_mismatch_explanation": "Esta categoría de peñera nun s'aplica al contestu nel qu'accediesti a esta publicación. Si tamién quies que se peñere la publicación nesti contestu, tienes d'editar la peñera.",
"filter_modal.added.context_mismatch_title": "¡El contestu nun coincide!", "filter_modal.added.context_mismatch_title": "¡El contestu nun coincide!",
"filter_modal.added.expired_explanation": "Esta categoría de peñera caducó, tienes de camudar la so data de caducidá p'aplicala.", "filter_modal.added.expired_explanation": "Esta categoría de peñera caducó, tienes de camudar la so data de caducidá p'aplicala.",
"filter_modal.added.expired_title": "¡La peñera caducó!", "filter_modal.added.expired_title": "¡La peñera caducó!",
"filter_modal.added.review_and_configure": "Pa revisar y configurar a fondu esta categoría de peñera, vete a la {settings_link}.", "filter_modal.added.review_and_configure": "Pa revisar y configurar a fondu esta categoría de peñera, vete a la {settings_link}.",
"filter_modal.added.review_and_configure_title": "Configuración de la peñera", "filter_modal.added.review_and_configure_title": "Configuración de la peñera",
"filter_modal.added.settings_link": "páxina de configuración", "filter_modal.added.settings_link": "páxina de configuración",
"filter_modal.added.short_explanation": "Esti artículu amestóse a la categoría de peñera siguiente: {title}.", "filter_modal.added.short_explanation": "Esta publicación amestóse a la categoría de peñera siguiente: {title}.",
"filter_modal.added.title": "¡Amestóse la peñera!", "filter_modal.added.title": "¡Amestóse la peñera!",
"filter_modal.select_filter.expired": "caducó", "filter_modal.select_filter.expired": "caducó",
"filter_modal.select_filter.prompt_new": "Categoría nueva: {name}", "filter_modal.select_filter.prompt_new": "Categoría nueva: {name}",
"filter_modal.select_filter.search": "Buscar o crear", "filter_modal.select_filter.search": "Buscar o crear",
"filter_modal.select_filter.subtitle": "Usa una categoría esistente o créala", "filter_modal.select_filter.subtitle": "Usa una categoría esistente o créala",
"filter_modal.select_filter.title": "Peñerar esti artículu", "filter_modal.select_filter.title": "Peñerar esta publicación",
"filter_modal.title.status": "Peñera d'un artículu", "filter_modal.title.status": "Peñera d'una publicación",
"firehose.all": "Tolos sirvidores", "firehose.all": "Tolos sirvidores",
"firehose.local": "Esti sirvidor", "firehose.local": "Esti sirvidor",
"firehose.remote": "Otros sirvidores", "firehose.remote": "Otros sirvidores",
@ -285,20 +286,20 @@
"interaction_modal.on_another_server": "N'otru sirvidor", "interaction_modal.on_another_server": "N'otru sirvidor",
"interaction_modal.on_this_server": "Nesti sirvidor", "interaction_modal.on_this_server": "Nesti sirvidor",
"interaction_modal.title.follow": "Siguir a {name}", "interaction_modal.title.follow": "Siguir a {name}",
"interaction_modal.title.reply": "Rempuesta al artículu de: {name}", "interaction_modal.title.reply": "Rempuesta a la publicación de: {name}",
"interaction_modal.title.vote": "Vota na encuesta de {name}", "interaction_modal.title.vote": "Vota na encuesta de {name}",
"intervals.full.days": "{number, plural, one {# día} other {# díes}}", "intervals.full.days": "{number, plural, one {# día} other {# díes}}",
"intervals.full.hours": "{number, plural, one {# hora} other {# hores}}", "intervals.full.hours": "{number, plural, one {# hora} other {# hores}}",
"intervals.full.minutes": "{number, plural, one {# minutu} other {# minutos}}", "intervals.full.minutes": "{number, plural, one {# minutu} other {# minutos}}",
"keyboard_shortcuts.back": "Dir p'atrás", "keyboard_shortcuts.back": "Dir p'atrás",
"keyboard_shortcuts.blocked": "Abrir la llista de perfiles bloquiaos", "keyboard_shortcuts.blocked": "Abrir la llista de perfiles bloquiaos",
"keyboard_shortcuts.boost": "Compartir un artículu", "keyboard_shortcuts.boost": "Compartir una publicación",
"keyboard_shortcuts.column": "Enfocar una columna", "keyboard_shortcuts.column": "Enfocar una columna",
"keyboard_shortcuts.compose": "Enfocar l'área de composición", "keyboard_shortcuts.compose": "Enfocar l'área de composición",
"keyboard_shortcuts.description": "Descripción", "keyboard_shortcuts.description": "Descripción",
"keyboard_shortcuts.direct": "p'abrir la columna de les menciones privaes", "keyboard_shortcuts.direct": "p'abrir la columna de les menciones privaes",
"keyboard_shortcuts.down": "Baxar na llista", "keyboard_shortcuts.down": "Baxar na llista",
"keyboard_shortcuts.enter": "Abrir un artículu", "keyboard_shortcuts.enter": "Abrir una publicación",
"keyboard_shortcuts.federated": "Abrir la llinia de tiempu federada", "keyboard_shortcuts.federated": "Abrir la llinia de tiempu federada",
"keyboard_shortcuts.heading": "Atayos del tecláu", "keyboard_shortcuts.heading": "Atayos del tecláu",
"keyboard_shortcuts.home": "Abrir la llinia de tiempu del aniciu", "keyboard_shortcuts.home": "Abrir la llinia de tiempu del aniciu",
@ -312,12 +313,12 @@
"keyboard_shortcuts.open_media": "Abrir el conteníu mutimedia", "keyboard_shortcuts.open_media": "Abrir el conteníu mutimedia",
"keyboard_shortcuts.pinned": "Abrir la llista d'artículos fixaos", "keyboard_shortcuts.pinned": "Abrir la llista d'artículos fixaos",
"keyboard_shortcuts.profile": "Abrir el perfil del autor/a", "keyboard_shortcuts.profile": "Abrir el perfil del autor/a",
"keyboard_shortcuts.reply": "Responder a un artículu", "keyboard_shortcuts.reply": "Responder a una publicación",
"keyboard_shortcuts.requests": "Abrir la llista de solicitúes de siguimientu", "keyboard_shortcuts.requests": "Abrir la llista de solicitúes de siguimientu",
"keyboard_shortcuts.search": "Enfocar la barra de busca", "keyboard_shortcuts.search": "Enfocar la barra de busca",
"keyboard_shortcuts.start": "Abrir la columna «Entamar»", "keyboard_shortcuts.start": "Abrir la columna «Entamar»",
"keyboard_shortcuts.toggle_sensitivity": "Amosar/esconder el conteníu multimedia", "keyboard_shortcuts.toggle_sensitivity": "Amosar/esconder el conteníu multimedia",
"keyboard_shortcuts.toot": "Comenzar un artículu nuevu", "keyboard_shortcuts.toot": "Escribir una publicación nueva",
"keyboard_shortcuts.unfocus": "Desenfocar l'área de composición/busca", "keyboard_shortcuts.unfocus": "Desenfocar l'área de composición/busca",
"keyboard_shortcuts.up": "Xubir na llista", "keyboard_shortcuts.up": "Xubir na llista",
"lightbox.close": "Zarrar", "lightbox.close": "Zarrar",
@ -377,9 +378,9 @@
"notification.mentioned_you": "{name} mentóte", "notification.mentioned_you": "{name} mentóte",
"notification.moderation-warning.learn_more": "Deprender más", "notification.moderation-warning.learn_more": "Deprender más",
"notification.poll": "Finó una encuesta na que votesti", "notification.poll": "Finó una encuesta na que votesti",
"notification.reblog": "{name} compartió'l to artículu", "notification.reblog": "{name} compartió la to publicación",
"notification.status": "{name} ta acabante d'espublizar", "notification.status": "{name} ta acabante d'espublizar",
"notification.update": "{name} editó un artículu", "notification.update": "{name} editó una publicación",
"notification_requests.edit_selection": "Editar", "notification_requests.edit_selection": "Editar",
"notification_requests.exit_selection": "Fecho", "notification_requests.exit_selection": "Fecho",
"notifications.clear": "Borrar los avisos", "notifications.clear": "Borrar los avisos",
@ -421,10 +422,10 @@
"poll.votes": "{votes, plural, one {# votu} other {# votos}}", "poll.votes": "{votes, plural, one {# votu} other {# votos}}",
"poll_button.add_poll": "Amestar una encuesta", "poll_button.add_poll": "Amestar una encuesta",
"poll_button.remove_poll": "Quitar la encuesta", "poll_button.remove_poll": "Quitar la encuesta",
"privacy.change": "Configurar la privacidá del artículu", "privacy.change": "Configurar la privacidá de la publicación",
"privacy.direct.short": "Perfiles específicos", "privacy.direct.short": "Mención privada",
"privacy.private.short": "Siguidores", "privacy.private.short": "Siguidores",
"privacy.public.short": "Artículu públicu", "privacy.public.short": "Publicación pública",
"privacy_policy.last_updated": "Data del últimu anovamientu: {date}", "privacy_policy.last_updated": "Data del últimu anovamientu: {date}",
"privacy_policy.title": "Política de privacidá", "privacy_policy.title": "Política de privacidá",
"refresh": "Anovar", "refresh": "Anovar",
@ -448,7 +449,7 @@
"report.category.subtitle": "Escueyi la meyor opción", "report.category.subtitle": "Escueyi la meyor opción",
"report.category.title": "Dinos qué pasa con esti {type}", "report.category.title": "Dinos qué pasa con esti {type}",
"report.category.title_account": "perfil", "report.category.title_account": "perfil",
"report.category.title_status": "artículu", "report.category.title_status": "publicación",
"report.close": "Fecho", "report.close": "Fecho",
"report.comment.title": "¿Hai daqué más qu'habríemos saber?", "report.comment.title": "¿Hai daqué más qu'habríemos saber?",
"report.forward": "Reunviar a {target}", "report.forward": "Reunviar a {target}",
@ -468,7 +469,7 @@
"report.rules.subtitle": "Seleiciona tolo que s'axuste", "report.rules.subtitle": "Seleiciona tolo que s'axuste",
"report.rules.title": "¿Qué normes s'incumplen?", "report.rules.title": "¿Qué normes s'incumplen?",
"report.statuses.subtitle": "Seleiciona tolo que s'axuste", "report.statuses.subtitle": "Seleiciona tolo que s'axuste",
"report.statuses.title": "¿Hai dalgún artículu qu'apoye esti informe?", "report.statuses.title": "¿Hai dalguna publicación qu'apoye esti informe?",
"report.submit": "Unviar", "report.submit": "Unviar",
"report.target": "Informe de: {target}", "report.target": "Informe de: {target}",
"report.thanks.take_action": "Equí tienes les opciones pa controlar qué ves en Mastodon:", "report.thanks.take_action": "Equí tienes les opciones pa controlar qué ves en Mastodon:",
@ -477,7 +478,7 @@
"report.thanks.title_actionable": "Gracies pol informe, el casu yá ta n'investigación.", "report.thanks.title_actionable": "Gracies pol informe, el casu yá ta n'investigación.",
"report.unfollow": "Dexar de siguir a @{name}", "report.unfollow": "Dexar de siguir a @{name}",
"report.unfollow_explanation": "Sigues a esta cuenta. Pa dexar de ver los sos artículos nel to feed d'aniciu, dexa de siguila.", "report.unfollow_explanation": "Sigues a esta cuenta. Pa dexar de ver los sos artículos nel to feed d'aniciu, dexa de siguila.",
"report_notification.attached_statuses": "{count, plural, one {Axuntóse {count} artículu} other {Axuntáronse {count} artículos}}", "report_notification.attached_statuses": "{count, plural, one {Axuntóse {count} publicación} other {Axuntáronse {count} publicaciones}}",
"report_notification.categories.legal": "Llegal", "report_notification.categories.legal": "Llegal",
"report_notification.categories.legal_sentence": "conteníu illegal", "report_notification.categories.legal_sentence": "conteníu illegal",
"report_notification.categories.spam": "Spam", "report_notification.categories.spam": "Spam",
@ -490,6 +491,7 @@
"search.quick_action.go_to_hashtag": "Dir a la etiqueta {x}", "search.quick_action.go_to_hashtag": "Dir a la etiqueta {x}",
"search.quick_action.status_search": "Artículos que concasen con {x}", "search.quick_action.status_search": "Artículos que concasen con {x}",
"search.search_or_paste": "Busca o apiega una URL", "search.search_or_paste": "Busca o apiega una URL",
"search_popout.full_text_search_disabled_message": "Nun ta disponible nel dominiu {domain}.",
"search_popout.language_code": "códigu de llingua ISO", "search_popout.language_code": "códigu de llingua ISO",
"search_popout.options": "Opciones de busca", "search_popout.options": "Opciones de busca",
"search_popout.quick_actions": "Aiciones rápides", "search_popout.quick_actions": "Aiciones rápides",
@ -501,22 +503,25 @@
"search_results.hashtags": "Etiquetes", "search_results.hashtags": "Etiquetes",
"search_results.see_all": "Ver too", "search_results.see_all": "Ver too",
"search_results.statuses": "Artículos", "search_results.statuses": "Artículos",
"server_banner.is_one_of_many": "{domain} ye unu de los munchos sirvidores independientes de Mastodon que pues usar pa participar nel fediversu.",
"server_banner.server_stats": "Estadístiques del sirvidor:", "server_banner.server_stats": "Estadístiques del sirvidor:",
"sign_in_banner.create_account": "Crear una cuenta", "sign_in_banner.create_account": "Crear una cuenta",
"sign_in_banner.mastodon_is": "Mastodon ye la meyor manera de siguir al momentu qué pasa.",
"sign_in_banner.sign_in": "Aniciar la sesión",
"sign_in_banner.sso_redirect": "Aniciar la sesión o rexistrase", "sign_in_banner.sso_redirect": "Aniciar la sesión o rexistrase",
"status.admin_account": "Abrir la interfaz de moderación pa @{name}", "status.admin_account": "Abrir la interfaz de moderación pa @{name}",
"status.admin_domain": "Abrir la interfaz de moderación pa «{domain}»", "status.admin_domain": "Abrir la interfaz de moderación pa «{domain}»",
"status.admin_status": "Abrir esti artículu na interfaz de moderación", "status.admin_status": "Abrir esta publicación na interfaz de moderación",
"status.block": "Bloquiar a @{name}", "status.block": "Bloquiar a @{name}",
"status.bookmark": "Meter en Marcadores", "status.bookmark": "Meter en Marcadores",
"status.cannot_reblog": "Esti artículu nun se pue compartir", "status.cannot_reblog": "Esta publicación nun se pue compartir",
"status.copy": "Copiar l'enllaz al artículu", "status.copy": "Copiar l'enllaz a la publicación",
"status.delete": "Desaniciar", "status.delete": "Desaniciar",
"status.direct": "Mentar a @{name} per privao", "status.direct": "Mentar a @{name} per privao",
"status.direct_indicator": "Mención privada", "status.direct_indicator": "Mención privada",
"status.edited_x_times": "Editóse {count, plural, one {{count} vegada} other {{count} vegaes}}", "status.edited_x_times": "Editóse {count, plural, one {{count} vegada} other {{count} vegaes}}",
"status.embed": "Consiguir el códigu pa empotrar", "status.embed": "Consiguir el códigu pa empotrar",
"status.filter": "Peñerar esti artículu", "status.filter": "Peñerar esta publicación",
"status.history.created": "{name} creó {date}", "status.history.created": "{name} creó {date}",
"status.history.edited": "{name} editó {date}", "status.history.edited": "{name} editó {date}",
"status.load_more": "Cargar más", "status.load_more": "Cargar más",
@ -525,13 +530,13 @@
"status.more": "Más", "status.more": "Más",
"status.mute": "Desactivar los avisos de @{name}", "status.mute": "Desactivar los avisos de @{name}",
"status.mute_conversation": "Desactivar los avisos de la conversación", "status.mute_conversation": "Desactivar los avisos de la conversación",
"status.open": "Espander esti artículu", "status.open": "Espander esta publicación",
"status.pin": "Fixar nel perfil", "status.pin": "Fixar nel perfil",
"status.pinned": "Artículu fixáu", "status.pinned": "Publicación fixada",
"status.read_more": "Lleer más", "status.read_more": "Lleer más",
"status.reblog": "Compartir", "status.reblog": "Compartir",
"status.reblogged_by": "{name} compartió", "status.reblogged_by": "{name} compartió",
"status.reblogs.empty": "Naide nun compartió esti artículu. Cuando daquién lo faiga, apaez equí.", "status.reblogs.empty": "Naide nun compartió esta publicación. Cuando daquién lo faiga, va apaecer equí.",
"status.redraft": "Desaniciar y reeditar", "status.redraft": "Desaniciar y reeditar",
"status.remove_bookmark": "Desaniciar el marcador", "status.remove_bookmark": "Desaniciar el marcador",
"status.replied_to": "En rempuesta a {name}", "status.replied_to": "En rempuesta a {name}",

View File

@ -642,7 +642,6 @@
"poll_button.remove_poll": "Выдаліць апытанне", "poll_button.remove_poll": "Выдаліць апытанне",
"privacy.change": "Змяніць прыватнасць допісу", "privacy.change": "Змяніць прыватнасць допісу",
"privacy.direct.long": "Усе згаданыя ў допісе", "privacy.direct.long": "Усе згаданыя ў допісе",
"privacy.direct.short": "Канкрэтныя людзі",
"privacy.private.long": "Толькі вашыя падпісчыкі", "privacy.private.long": "Толькі вашыя падпісчыкі",
"privacy.private.short": "Падпісчыкі", "privacy.private.short": "Падпісчыкі",
"privacy.public.long": "Усе, хто ёсць і каго няма ў Mastodon", "privacy.public.long": "Усе, хто ёсць і каго няма ў Mastodon",

View File

@ -697,7 +697,7 @@
"poll_button.remove_poll": "Премахване на анкета", "poll_button.remove_poll": "Премахване на анкета",
"privacy.change": "Промяна на поверителността на публикация", "privacy.change": "Промяна на поверителността на публикация",
"privacy.direct.long": "Споменатите в публикацията", "privacy.direct.long": "Споменатите в публикацията",
"privacy.direct.short": "Определени хора", "privacy.direct.short": "Частно споменаване",
"privacy.private.long": "Само последователите ви", "privacy.private.long": "Само последователите ви",
"privacy.private.short": "Последователи", "privacy.private.short": "Последователи",
"privacy.public.long": "Всеки във и извън Mastodon", "privacy.public.long": "Всеки във и извън Mastodon",

View File

@ -436,7 +436,6 @@
"poll_button.add_poll": "Ouzhpennañ ur sontadeg", "poll_button.add_poll": "Ouzhpennañ ur sontadeg",
"poll_button.remove_poll": "Dilemel ar sontadeg", "poll_button.remove_poll": "Dilemel ar sontadeg",
"privacy.change": "Cheñch prevezded an embannadur", "privacy.change": "Cheñch prevezded an embannadur",
"privacy.direct.short": "Tud resis",
"privacy.private.short": "Heulierien", "privacy.private.short": "Heulierien",
"privacy.public.short": "Publik", "privacy.public.short": "Publik",
"privacy_policy.last_updated": "Hizivadenn ziwezhañ {date}", "privacy_policy.last_updated": "Hizivadenn ziwezhañ {date}",

View File

@ -696,7 +696,7 @@
"poll_button.remove_poll": "Elimina l'enquesta", "poll_button.remove_poll": "Elimina l'enquesta",
"privacy.change": "Canvia la privacitat del tut", "privacy.change": "Canvia la privacitat del tut",
"privacy.direct.long": "Tothom mencionat a la publicació", "privacy.direct.long": "Tothom mencionat a la publicació",
"privacy.direct.short": "Persones concretes", "privacy.direct.short": "Menció privada",
"privacy.private.long": "Només els vostres seguidors", "privacy.private.long": "Només els vostres seguidors",
"privacy.private.short": "Seguidors", "privacy.private.short": "Seguidors",
"privacy.public.long": "Tothom dins o fora Mastodon", "privacy.public.long": "Tothom dins o fora Mastodon",

View File

@ -697,7 +697,7 @@
"poll_button.remove_poll": "Odebrat anketu", "poll_button.remove_poll": "Odebrat anketu",
"privacy.change": "Změnit soukromí příspěvku", "privacy.change": "Změnit soukromí příspěvku",
"privacy.direct.long": "Všichni zmínění v příspěvku", "privacy.direct.long": "Všichni zmínění v příspěvku",
"privacy.direct.short": "Vybraní lidé", "privacy.direct.short": "Soukromá zmínka",
"privacy.private.long": "Jen vaši sledující", "privacy.private.long": "Jen vaši sledující",
"privacy.private.short": "Sledující", "privacy.private.short": "Sledující",
"privacy.public.long": "Kdokoliv na Mastodonu i mimo něj", "privacy.public.long": "Kdokoliv na Mastodonu i mimo něj",

View File

@ -696,7 +696,6 @@
"poll_button.remove_poll": "Tynnu pleidlais", "poll_button.remove_poll": "Tynnu pleidlais",
"privacy.change": "Addasu preifatrwdd y post", "privacy.change": "Addasu preifatrwdd y post",
"privacy.direct.long": "Pawb sydd â sôn amdanyn nhw yn y postiad", "privacy.direct.long": "Pawb sydd â sôn amdanyn nhw yn y postiad",
"privacy.direct.short": "Pobl benodol",
"privacy.private.long": "Eich dilynwyr yn unig", "privacy.private.long": "Eich dilynwyr yn unig",
"privacy.private.short": "Dilynwyr", "privacy.private.short": "Dilynwyr",
"privacy.public.long": "Unrhyw ar ac oddi ar Mastodon", "privacy.public.long": "Unrhyw ar ac oddi ar Mastodon",

View File

@ -697,12 +697,12 @@
"poll_button.remove_poll": "Fjern afstemning", "poll_button.remove_poll": "Fjern afstemning",
"privacy.change": "Tilpas indlægsfortrolighed", "privacy.change": "Tilpas indlægsfortrolighed",
"privacy.direct.long": "Alle omtalt i indlægget", "privacy.direct.long": "Alle omtalt i indlægget",
"privacy.direct.short": "Bestemte personer", "privacy.direct.short": "Privat omtale",
"privacy.private.long": "Kun dine følgere", "privacy.private.long": "Kun dine følgere",
"privacy.private.short": "Følgere", "privacy.private.short": "Følgere",
"privacy.public.long": "Alle på og udenfor Mastodon", "privacy.public.long": "Alle på og udenfor Mastodon",
"privacy.public.short": "Offentlig", "privacy.public.short": "Offentlig",
"privacy.unlisted.additional": "Dette er præcis som offentlig adfærd, dog vises indlægget ikke i realtids-strømme/etiketter, udforsk eller Mastodon-søgning, selv hvis valget gælder hele kontoen.", "privacy.unlisted.additional": "Dette er præcis som offentlig adfærd, dog vises indlægget ikke i tidslinjer, under etiketter, udforsk eller Mastodon-søgning, selv hvis du ellers har sagt at dine opslag godt må være søgbare.",
"privacy.unlisted.long": "Færre algoritmiske fanfarer", "privacy.unlisted.long": "Færre algoritmiske fanfarer",
"privacy.unlisted.short": "Stille offentligt", "privacy.unlisted.short": "Stille offentligt",
"privacy_policy.last_updated": "Senest opdateret {date}", "privacy_policy.last_updated": "Senest opdateret {date}",

View File

@ -697,7 +697,7 @@
"poll_button.remove_poll": "Umfrage entfernen", "poll_button.remove_poll": "Umfrage entfernen",
"privacy.change": "Sichtbarkeit anpassen", "privacy.change": "Sichtbarkeit anpassen",
"privacy.direct.long": "Alle in diesem Beitrag erwähnten Profile", "privacy.direct.long": "Alle in diesem Beitrag erwähnten Profile",
"privacy.direct.short": "Ausgewählte Profile", "privacy.direct.short": "Private Erwähnung",
"privacy.private.long": "Nur deine Follower", "privacy.private.long": "Nur deine Follower",
"privacy.private.short": "Follower", "privacy.private.short": "Follower",
"privacy.public.long": "Alle in und außerhalb von Mastodon", "privacy.public.long": "Alle in und außerhalb von Mastodon",

View File

@ -697,7 +697,7 @@
"poll_button.remove_poll": "Αφαίρεση δημοσκόπησης", "poll_button.remove_poll": "Αφαίρεση δημοσκόπησης",
"privacy.change": "Προσαρμογή ιδιωτικότητας ανάρτησης", "privacy.change": "Προσαρμογή ιδιωτικότητας ανάρτησης",
"privacy.direct.long": "Όλοι όσοι αναφέρθηκαν στη δημοσίευση", "privacy.direct.long": "Όλοι όσοι αναφέρθηκαν στη δημοσίευση",
"privacy.direct.short": "Συγκεκριμένα άτομα", "privacy.direct.short": "Ιδιωτική επισήμανση",
"privacy.private.long": "Μόνο οι ακόλουθοί σας", "privacy.private.long": "Μόνο οι ακόλουθοί σας",
"privacy.private.short": "Ακόλουθοι", "privacy.private.short": "Ακόλουθοι",
"privacy.public.long": "Όλοι εντός και εκτός του Mastodon", "privacy.public.long": "Όλοι εντός και εκτός του Mastodon",

View File

@ -697,7 +697,7 @@
"poll_button.remove_poll": "Remove poll", "poll_button.remove_poll": "Remove poll",
"privacy.change": "Change post privacy", "privacy.change": "Change post privacy",
"privacy.direct.long": "Everyone mentioned in the post", "privacy.direct.long": "Everyone mentioned in the post",
"privacy.direct.short": "Specific people", "privacy.direct.short": "Private mention",
"privacy.private.long": "Only your followers", "privacy.private.long": "Only your followers",
"privacy.private.short": "Followers", "privacy.private.short": "Followers",
"privacy.public.long": "Anyone on and off Mastodon", "privacy.public.long": "Anyone on and off Mastodon",

View File

@ -697,7 +697,7 @@
"poll_button.remove_poll": "Remove poll", "poll_button.remove_poll": "Remove poll",
"privacy.change": "Change post privacy", "privacy.change": "Change post privacy",
"privacy.direct.long": "Everyone mentioned in the post", "privacy.direct.long": "Everyone mentioned in the post",
"privacy.direct.short": "Specific people", "privacy.direct.short": "Private mention",
"privacy.private.long": "Only your followers", "privacy.private.long": "Only your followers",
"privacy.private.short": "Followers", "privacy.private.short": "Followers",
"privacy.public.long": "Anyone on and off Mastodon", "privacy.public.long": "Anyone on and off Mastodon",
@ -872,7 +872,9 @@
"subscribed_languages.target": "Change subscribed languages for {target}", "subscribed_languages.target": "Change subscribed languages for {target}",
"tabs_bar.home": "Home", "tabs_bar.home": "Home",
"tabs_bar.notifications": "Notifications", "tabs_bar.notifications": "Notifications",
"terms_of_service.effective_as_of": "Effective as of {date}",
"terms_of_service.title": "Terms of Service", "terms_of_service.title": "Terms of Service",
"terms_of_service.upcoming_changes_on": "Upcoming changes on {date}",
"time_remaining.days": "{number, plural, one {# day} other {# days}} left", "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",

View File

@ -697,7 +697,7 @@
"poll_button.remove_poll": "Forigi balotenketon", "poll_button.remove_poll": "Forigi balotenketon",
"privacy.change": "Ŝanĝu afiŝan privatecon", "privacy.change": "Ŝanĝu afiŝan privatecon",
"privacy.direct.long": "Ĉiuj menciitaj en la afiŝo", "privacy.direct.long": "Ĉiuj menciitaj en la afiŝo",
"privacy.direct.short": "Specifaj homoj", "privacy.direct.short": "Privata mencio",
"privacy.private.long": "Nur viaj sekvantoj", "privacy.private.long": "Nur viaj sekvantoj",
"privacy.private.short": "Sekvantoj", "privacy.private.short": "Sekvantoj",
"privacy.public.long": "Ĉiujn ajn ĉe kaj ekster Mastodon", "privacy.public.long": "Ĉiujn ajn ĉe kaj ekster Mastodon",

View File

@ -697,7 +697,7 @@
"poll_button.remove_poll": "Quitar encuesta", "poll_button.remove_poll": "Quitar encuesta",
"privacy.change": "Configurar privacidad del mensaje", "privacy.change": "Configurar privacidad del mensaje",
"privacy.direct.long": "Todas las cuentas mencionadas en el mensaje", "privacy.direct.long": "Todas las cuentas mencionadas en el mensaje",
"privacy.direct.short": "Cuentas específicas", "privacy.direct.short": "Mención privada",
"privacy.private.long": "Solo tus seguidores", "privacy.private.long": "Solo tus seguidores",
"privacy.private.short": "Seguidores", "privacy.private.short": "Seguidores",
"privacy.public.long": "Cualquier persona dentro y fuera de Mastodon", "privacy.public.long": "Cualquier persona dentro y fuera de Mastodon",

View File

@ -192,10 +192,10 @@
"compose_form.poll.switch_to_multiple": "Cambiar la encuesta para permitir múltiples opciones", "compose_form.poll.switch_to_multiple": "Cambiar la encuesta para permitir múltiples opciones",
"compose_form.poll.switch_to_single": "Cambiar la encuesta para permitir una única opción", "compose_form.poll.switch_to_single": "Cambiar la encuesta para permitir una única opción",
"compose_form.poll.type": "Estilo", "compose_form.poll.type": "Estilo",
"compose_form.publish": "Publicación", "compose_form.publish": "Publicar",
"compose_form.publish_form": "Nueva publicación", "compose_form.publish_form": "Nueva publicación",
"compose_form.reply": "Respuesta", "compose_form.reply": "Respuesta",
"compose_form.save_changes": "Actualización", "compose_form.save_changes": "Actualizar",
"compose_form.spoiler.marked": "Quitar advertencia de contenido", "compose_form.spoiler.marked": "Quitar advertencia de contenido",
"compose_form.spoiler.unmarked": "Añadir advertencia de contenido", "compose_form.spoiler.unmarked": "Añadir advertencia de contenido",
"compose_form.spoiler_placeholder": "Advertencia de contenido (opcional)", "compose_form.spoiler_placeholder": "Advertencia de contenido (opcional)",
@ -697,7 +697,7 @@
"poll_button.remove_poll": "Eliminar encuesta", "poll_button.remove_poll": "Eliminar encuesta",
"privacy.change": "Ajustar privacidad", "privacy.change": "Ajustar privacidad",
"privacy.direct.long": "Todos los mencionados en la publicación", "privacy.direct.long": "Todos los mencionados en la publicación",
"privacy.direct.short": "Personas específicas", "privacy.direct.short": "Mención privada",
"privacy.private.long": "Sólo tus seguidores", "privacy.private.long": "Sólo tus seguidores",
"privacy.private.short": "Seguidores", "privacy.private.short": "Seguidores",
"privacy.public.long": "Cualquiera dentro y fuera de Mastodon", "privacy.public.long": "Cualquiera dentro y fuera de Mastodon",

View File

@ -697,7 +697,7 @@
"poll_button.remove_poll": "Eliminar encuesta", "poll_button.remove_poll": "Eliminar encuesta",
"privacy.change": "Ajustar privacidad", "privacy.change": "Ajustar privacidad",
"privacy.direct.long": "Visible únicamente por los mencionados en la publicación", "privacy.direct.long": "Visible únicamente por los mencionados en la publicación",
"privacy.direct.short": "Personas específicas", "privacy.direct.short": "Mención privada",
"privacy.private.long": "Visible únicamente por tus seguidores", "privacy.private.long": "Visible únicamente por tus seguidores",
"privacy.private.short": "Seguidores", "privacy.private.short": "Seguidores",
"privacy.public.long": "Visible por todo el mundo, dentro y fuera de Mastodon", "privacy.public.long": "Visible por todo el mundo, dentro y fuera de Mastodon",

View File

@ -696,7 +696,6 @@
"poll_button.remove_poll": "Eemalda küsitlus", "poll_button.remove_poll": "Eemalda küsitlus",
"privacy.change": "Muuda postituse nähtavust", "privacy.change": "Muuda postituse nähtavust",
"privacy.direct.long": "Kõik postituses mainitud", "privacy.direct.long": "Kõik postituses mainitud",
"privacy.direct.short": "Määratud kasutajad",
"privacy.private.long": "Ainult jälgijad", "privacy.private.long": "Ainult jälgijad",
"privacy.private.short": "Jälgijad", "privacy.private.short": "Jälgijad",
"privacy.public.long": "Nii kasutajad kui mittekasutajad", "privacy.public.long": "Nii kasutajad kui mittekasutajad",

View File

@ -644,7 +644,6 @@
"poll_button.remove_poll": "Kendu inkesta", "poll_button.remove_poll": "Kendu inkesta",
"privacy.change": "Aldatu bidalketaren pribatutasuna", "privacy.change": "Aldatu bidalketaren pribatutasuna",
"privacy.direct.long": "Argitalpen honetan aipatutako denak", "privacy.direct.long": "Argitalpen honetan aipatutako denak",
"privacy.direct.short": "Jende jakina",
"privacy.private.long": "Soilik jarraitzaileak", "privacy.private.long": "Soilik jarraitzaileak",
"privacy.private.short": "Jarraitzaileak", "privacy.private.short": "Jarraitzaileak",
"privacy.public.long": "Mastodonen dagoen edo ez dagoen edonor", "privacy.public.long": "Mastodonen dagoen edo ez dagoen edonor",

View File

@ -218,6 +218,10 @@
"confirmations.logout.confirm": "خروج از حساب", "confirmations.logout.confirm": "خروج از حساب",
"confirmations.logout.message": "مطمئنید می‌خواهید خارج شوید؟", "confirmations.logout.message": "مطمئنید می‌خواهید خارج شوید؟",
"confirmations.logout.title": "خروج؟", "confirmations.logout.title": "خروج؟",
"confirmations.missing_alt_text.confirm": "متن جایگزین را اضافه کنید",
"confirmations.missing_alt_text.message": "پست شما حاوی رسانه بدون متن جایگزین است. افزودن توضیحات کمک می کند تا محتوای شما برای افراد بیشتری قابل دسترسی باشد.",
"confirmations.missing_alt_text.secondary": "به هر حال پست کن",
"confirmations.missing_alt_text.title": "متن جایگزین اضافه شود؟",
"confirmations.mute.confirm": "خموش", "confirmations.mute.confirm": "خموش",
"confirmations.redraft.confirm": "حذف و بازنویسی", "confirmations.redraft.confirm": "حذف و بازنویسی",
"confirmations.redraft.message": "مطمئنید که می‌خواهید این فرسته را حذف کنید و از نو بنویسید؟ با این کار تقویت‌ها و پسندهایش از دست رفته و پاسخ‌ها به آن بی‌مرجع می‌شود.", "confirmations.redraft.message": "مطمئنید که می‌خواهید این فرسته را حذف کنید و از نو بنویسید؟ با این کار تقویت‌ها و پسندهایش از دست رفته و پاسخ‌ها به آن بی‌مرجع می‌شود.",
@ -693,7 +697,7 @@
"poll_button.remove_poll": "برداشتن نظرسنجی", "poll_button.remove_poll": "برداشتن نظرسنجی",
"privacy.change": "تغییر محرمانگی فرسته", "privacy.change": "تغییر محرمانگی فرسته",
"privacy.direct.long": "هرکسی که در فرسته نام برده شده", "privacy.direct.long": "هرکسی که در فرسته نام برده شده",
"privacy.direct.short": "افراد مشخّص", "privacy.direct.short": "ذکر خصوصی",
"privacy.private.long": "تنها پی‌گیرندگانتان", "privacy.private.long": "تنها پی‌گیرندگانتان",
"privacy.private.short": "پی‌گیرندگان", "privacy.private.short": "پی‌گیرندگان",
"privacy.public.long": "هرکسی در و بیرون از ماستودون", "privacy.public.long": "هرکسی در و بیرون از ماستودون",

View File

@ -697,7 +697,7 @@
"poll_button.remove_poll": "Poista äänestys", "poll_button.remove_poll": "Poista äänestys",
"privacy.change": "Muuta julkaisun näkyvyyttä", "privacy.change": "Muuta julkaisun näkyvyyttä",
"privacy.direct.long": "Kaikki tässä julkaisussa mainitut", "privacy.direct.long": "Kaikki tässä julkaisussa mainitut",
"privacy.direct.short": "Tietyt käyttäjät", "privacy.direct.short": "Yksityismaininta",
"privacy.private.long": "Vain seuraajasi", "privacy.private.long": "Vain seuraajasi",
"privacy.private.short": "Seuraajat", "privacy.private.short": "Seuraajat",
"privacy.public.long": "Kuka tahansa Mastodonissa ja sen ulkopuolella", "privacy.public.long": "Kuka tahansa Mastodonissa ja sen ulkopuolella",

View File

@ -697,7 +697,7 @@
"poll_button.remove_poll": "Strika atkvøðugreiðslu", "poll_button.remove_poll": "Strika atkvøðugreiðslu",
"privacy.change": "Broyt privatverju av posti", "privacy.change": "Broyt privatverju av posti",
"privacy.direct.long": "Øll, sum eru nevnd í postinum", "privacy.direct.long": "Øll, sum eru nevnd í postinum",
"privacy.direct.short": "Ávís fólk", "privacy.direct.short": "Privat umrøða",
"privacy.private.long": "Einans tey, ið fylgja tær", "privacy.private.long": "Einans tey, ið fylgja tær",
"privacy.private.short": "Fylgjarar", "privacy.private.short": "Fylgjarar",
"privacy.public.long": "Øll í og uttanfyri Mastodon", "privacy.public.long": "Øll í og uttanfyri Mastodon",

View File

@ -1,6 +1,6 @@
{ {
"about.blocks": "Serveurs modérés", "about.blocks": "Serveurs modérés",
"about.contact": "Contact:", "about.contact": "Contact :",
"about.disclaimer": "Mastodon est un logiciel open-source gratuit et une marque déposée de Mastodon gGmbH.", "about.disclaimer": "Mastodon est un logiciel open-source gratuit et une marque déposée de Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Raison non disponible", "about.domain_blocks.no_reason_available": "Raison non disponible",
"about.domain_blocks.preamble": "Mastodon vous permet généralement de visualiser le contenu et d'interagir avec des comptes de n'importe quel serveur dans le fediverse. Voici les exceptions qui ont été faites sur ce serveur en particulier.", "about.domain_blocks.preamble": "Mastodon vous permet généralement de visualiser le contenu et d'interagir avec des comptes de n'importe quel serveur dans le fediverse. Voici les exceptions qui ont été faites sur ce serveur en particulier.",
@ -697,7 +697,7 @@
"poll_button.remove_poll": "Supprimer le sondage", "poll_button.remove_poll": "Supprimer le sondage",
"privacy.change": "Changer la confidentialité des messages", "privacy.change": "Changer la confidentialité des messages",
"privacy.direct.long": "Toutes les personnes mentionnées dans le post", "privacy.direct.long": "Toutes les personnes mentionnées dans le post",
"privacy.direct.short": "Personnes spécifiques", "privacy.direct.short": "Mention privée",
"privacy.private.long": "Seulement vos abonnés", "privacy.private.long": "Seulement vos abonnés",
"privacy.private.short": "Abonnés", "privacy.private.short": "Abonnés",
"privacy.public.long": "Tout le monde sur et en dehors de Mastodon", "privacy.public.long": "Tout le monde sur et en dehors de Mastodon",

View File

@ -697,7 +697,7 @@
"poll_button.remove_poll": "Supprimer le sondage", "poll_button.remove_poll": "Supprimer le sondage",
"privacy.change": "Ajuster la confidentialité du message", "privacy.change": "Ajuster la confidentialité du message",
"privacy.direct.long": "Toutes les personnes mentionnées dans le post", "privacy.direct.long": "Toutes les personnes mentionnées dans le post",
"privacy.direct.short": "Personnes spécifiques", "privacy.direct.short": "Mention privée",
"privacy.private.long": "Seulement vos abonnés", "privacy.private.long": "Seulement vos abonnés",
"privacy.private.short": "Abonnés", "privacy.private.short": "Abonnés",
"privacy.public.long": "Tout le monde sur et en dehors de Mastodon", "privacy.public.long": "Tout le monde sur et en dehors de Mastodon",

View File

@ -682,7 +682,6 @@
"poll_button.remove_poll": "Enkête fuortsmite", "poll_button.remove_poll": "Enkête fuortsmite",
"privacy.change": "Sichtberheid fan berjocht oanpasse", "privacy.change": "Sichtberheid fan berjocht oanpasse",
"privacy.direct.long": "Elkenien dy yn it berjocht fermeld wurdt", "privacy.direct.long": "Elkenien dy yn it berjocht fermeld wurdt",
"privacy.direct.short": "Bepaalde minsken",
"privacy.private.long": "Allinnich jo folgers", "privacy.private.long": "Allinnich jo folgers",
"privacy.private.short": "Folgers", "privacy.private.short": "Folgers",
"privacy.public.long": "Elkenien op Mastodon en dêrbûten", "privacy.public.long": "Elkenien op Mastodon en dêrbûten",

View File

@ -697,7 +697,7 @@
"poll_button.remove_poll": "Bain suirbhé", "poll_button.remove_poll": "Bain suirbhé",
"privacy.change": "Athraigh príobháideacht postála", "privacy.change": "Athraigh príobháideacht postála",
"privacy.direct.long": "Luaigh gach duine sa phost", "privacy.direct.long": "Luaigh gach duine sa phost",
"privacy.direct.short": "Daoine ar leith", "privacy.direct.short": "Tagairt phríobháideach",
"privacy.private.long": "Do leanúna amháin", "privacy.private.long": "Do leanúna amháin",
"privacy.private.short": "Leantóirí", "privacy.private.short": "Leantóirí",
"privacy.public.long": "Duine ar bith ar agus amach Mastodon", "privacy.public.long": "Duine ar bith ar agus amach Mastodon",

View File

@ -633,7 +633,6 @@
"poll_button.remove_poll": "Thoir air falbh an cunntas-bheachd", "poll_button.remove_poll": "Thoir air falbh an cunntas-bheachd",
"privacy.change": "Cuir gleus air prìobhaideachd a phuist", "privacy.change": "Cuir gleus air prìobhaideachd a phuist",
"privacy.direct.long": "A h-uile duine air a bheil iomradh sa phost", "privacy.direct.long": "A h-uile duine air a bheil iomradh sa phost",
"privacy.direct.short": "Daoine àraidh",
"privacy.private.long": "An luchd-leantainn agad a-mhàin", "privacy.private.long": "An luchd-leantainn agad a-mhàin",
"privacy.private.short": "Luchd-leantainn", "privacy.private.short": "Luchd-leantainn",
"privacy.public.long": "Duine sam bith taobh a-staigh no a-muigh Mhastodon", "privacy.public.long": "Duine sam bith taobh a-staigh no a-muigh Mhastodon",

Some files were not shown because too many files have changed in this diff Show More