mirror of
https://github.com/mastodon/mastodon.git
synced 2025-05-07 20:26:15 +00:00
Merge branch 'main' into main
This commit is contained in:
commit
a6d2ed4f3e
2
.github/workflows/lint-haml.yml
vendored
2
.github/workflows/lint-haml.yml
vendored
|
@ -43,4 +43,4 @@ jobs:
|
|||
- name: Run haml-lint
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json"
|
||||
bin/haml-lint --parallel --reporter github
|
||||
bin/haml-lint --reporter github
|
||||
|
|
|
@ -63,6 +63,7 @@ docker-compose.override.yml
|
|||
|
||||
# Ignore emoji map file
|
||||
/app/javascript/mastodon/features/emoji/emoji_map.json
|
||||
/app/javascript/mastodon/features/emoji/emoji_sheet.json
|
||||
|
||||
# Ignore locale files
|
||||
/app/javascript/mastodon/locales/*.json
|
||||
|
|
|
@ -18,6 +18,7 @@ inherit_from:
|
|||
- .rubocop/rspec_rails.yml
|
||||
- .rubocop/rspec.yml
|
||||
- .rubocop/style.yml
|
||||
- .rubocop/i18n.yml
|
||||
- .rubocop/custom.yml
|
||||
- .rubocop_todo.yml
|
||||
- .rubocop/strict.yml
|
||||
|
@ -30,6 +31,7 @@ plugins:
|
|||
- rubocop-rails
|
||||
- rubocop-rspec
|
||||
- rubocop-performance
|
||||
- rubocop-i18n
|
||||
|
||||
require:
|
||||
- rubocop-rspec_rails
|
||||
|
|
12
.rubocop/i18n.yml
Normal file
12
.rubocop/i18n.yml
Normal file
|
@ -0,0 +1,12 @@
|
|||
I18n/RailsI18n:
|
||||
Enabled: true
|
||||
Exclude:
|
||||
- 'config/**/*'
|
||||
- 'db/**/*'
|
||||
- 'lib/**/*'
|
||||
- 'spec/**/*'
|
||||
I18n/GetText:
|
||||
Enabled: false
|
||||
|
||||
I18n/RailsI18n/DecorateStringFormattingUsingInterpolation:
|
||||
Enabled: false
|
|
@ -1,6 +1,6 @@
|
|||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
|
||||
# using RuboCop version 1.72.2.
|
||||
# using RuboCop version 1.73.1.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
|
|
35
CHANGELOG.md
35
CHANGELOG.md
|
@ -2,6 +2,41 @@
|
|||
|
||||
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
|
||||
|
||||
### Security
|
||||
|
|
|
@ -96,6 +96,9 @@ RUN \
|
|||
# Set /opt/mastodon as working directory
|
||||
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
|
||||
RUN \
|
||||
# Mount Apt cache and lib directories from Docker buildx caches
|
||||
|
@ -165,7 +168,7 @@ RUN \
|
|||
libexif-dev \
|
||||
libexpat1-dev \
|
||||
libgirepository1.0-dev \
|
||||
libheif-dev \
|
||||
libheif-dev/bookworm-backports \
|
||||
libimagequant-dev \
|
||||
libjpeg62-turbo-dev \
|
||||
liblcms2-dev \
|
||||
|
@ -348,7 +351,7 @@ RUN \
|
|||
# libvips components
|
||||
libcgif0 \
|
||||
libexif12 \
|
||||
libheif1 \
|
||||
libheif1/bookworm-backports \
|
||||
libimagequant0 \
|
||||
libjpeg62-turbo \
|
||||
liblcms2-2 \
|
||||
|
|
6
Gemfile
6
Gemfile
|
@ -39,7 +39,7 @@ gem 'net-ldap', '~> 0.18'
|
|||
|
||||
gem 'omniauth', '~> 2.0'
|
||||
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-saml', '~> 2.0'
|
||||
|
||||
|
@ -145,9 +145,6 @@ group :test do
|
|||
# Used to mock environment variables
|
||||
gem 'climate_control'
|
||||
|
||||
# Add back helpers functions removed in Rails 5.1
|
||||
gem 'rails-controller-testing', '~> 1.0'
|
||||
|
||||
# Validate schemas in specs
|
||||
gem 'json-schema', '~> 5.0'
|
||||
|
||||
|
@ -168,6 +165,7 @@ group :development do
|
|||
# Code linting CLI and plugins
|
||||
gem 'rubocop', require: false
|
||||
gem 'rubocop-capybara', require: false
|
||||
gem 'rubocop-i18n', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
gem 'rubocop-rails', require: false
|
||||
gem 'rubocop-rspec', require: false
|
||||
|
|
99
Gemfile.lock
99
Gemfile.lock
|
@ -194,7 +194,7 @@ GEM
|
|||
devise_pam_authenticatable2 (9.2.0)
|
||||
devise (>= 4.0.0)
|
||||
rpam2 (~> 4.0)
|
||||
diff-lcs (1.5.1)
|
||||
diff-lcs (1.6.0)
|
||||
discard (1.4.0)
|
||||
activerecord (>= 4.2, < 9.0)
|
||||
docile (1.4.1)
|
||||
|
@ -217,6 +217,8 @@ GEM
|
|||
htmlentities (~> 4.3.3)
|
||||
launchy (>= 2.1, < 4.0)
|
||||
mail (~> 2.7)
|
||||
email_validator (2.2.4)
|
||||
activemodel
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
|
@ -228,6 +230,8 @@ GEM
|
|||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday-follow_redirects (0.3.0)
|
||||
faraday (>= 1, < 3)
|
||||
faraday-httpclient (2.0.1)
|
||||
httpclient (>= 2.2)
|
||||
faraday-net_http (3.4.0)
|
||||
|
@ -273,7 +277,7 @@ GEM
|
|||
activesupport (>= 5.1)
|
||||
haml (>= 4.0.6)
|
||||
railties (>= 5.1)
|
||||
haml_lint (0.60.0)
|
||||
haml_lint (0.61.0)
|
||||
haml (>= 5.0)
|
||||
parallel (~> 1.10)
|
||||
rainbow
|
||||
|
@ -330,11 +334,13 @@ GEM
|
|||
jmespath (1.6.2)
|
||||
json (2.10.1)
|
||||
json-canonicalization (1.0.0)
|
||||
json-jwt (1.15.3.1)
|
||||
json-jwt (1.16.7)
|
||||
activesupport (>= 4.2)
|
||||
aes_key_wrap
|
||||
base64
|
||||
bindata
|
||||
httpclient
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
json-ld (3.3.2)
|
||||
htmlentities (~> 4.3)
|
||||
json-canonicalization (~> 1.0)
|
||||
|
@ -409,11 +415,11 @@ GEM
|
|||
mime-types (3.6.0)
|
||||
logger
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2025.0204)
|
||||
mime-types-data (3.2025.0220)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.8)
|
||||
minitest (5.25.4)
|
||||
msgpack (1.7.5)
|
||||
msgpack (1.8.0)
|
||||
multi_json (1.15.0)
|
||||
mutex_m (0.3.0)
|
||||
net-http (0.6.0)
|
||||
|
@ -432,37 +438,39 @@ GEM
|
|||
nokogiri (1.18.3)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
oj (3.16.9)
|
||||
oj (3.16.10)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
omniauth (2.1.2)
|
||||
omniauth (2.1.3)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 2.2.3)
|
||||
rack-protection
|
||||
omniauth-cas (3.0.0)
|
||||
omniauth-cas (3.0.1)
|
||||
addressable (~> 2.8)
|
||||
nokogiri (~> 1.12)
|
||||
omniauth (~> 2.1)
|
||||
omniauth-rails_csrf_protection (1.0.2)
|
||||
actionpack (>= 4.2)
|
||||
omniauth (~> 2.0)
|
||||
omniauth-saml (2.2.1)
|
||||
omniauth-saml (2.2.2)
|
||||
omniauth (~> 2.1)
|
||||
ruby-saml (~> 1.17)
|
||||
omniauth_openid_connect (0.6.1)
|
||||
omniauth_openid_connect (0.8.0)
|
||||
omniauth (>= 1.9, < 3)
|
||||
openid_connect (~> 1.1)
|
||||
openid_connect (1.4.2)
|
||||
openid_connect (~> 2.2)
|
||||
openid_connect (2.3.1)
|
||||
activemodel
|
||||
attr_required (>= 1.0.0)
|
||||
json-jwt (>= 1.15.0)
|
||||
net-smtp
|
||||
rack-oauth2 (~> 1.21)
|
||||
swd (~> 1.3)
|
||||
email_validator
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
json-jwt (>= 1.16)
|
||||
mail
|
||||
rack-oauth2 (~> 2.2)
|
||||
swd (~> 2.0)
|
||||
tzinfo
|
||||
validate_email
|
||||
validate_url
|
||||
webfinger (~> 1.2)
|
||||
webfinger (~> 2.0)
|
||||
openssl (3.3.0)
|
||||
openssl-signature_algorithm (1.3.0)
|
||||
openssl (> 2.0)
|
||||
|
@ -600,19 +608,20 @@ GEM
|
|||
public_suffix (6.0.1)
|
||||
puma (6.6.0)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.4.0)
|
||||
pundit (2.5.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.11)
|
||||
rack (2.2.12)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-cors (2.0.2)
|
||||
rack (>= 2.0.0)
|
||||
rack-oauth2 (1.21.3)
|
||||
rack-oauth2 (2.2.1)
|
||||
activesupport
|
||||
attr_required
|
||||
httpclient
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
json-jwt (>= 1.11.0)
|
||||
rack (>= 2.1.0)
|
||||
rack-protection (3.2.0)
|
||||
|
@ -641,10 +650,6 @@ GEM
|
|||
activesupport (= 8.0.1)
|
||||
bundler (>= 1.15.0)
|
||||
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)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
|
@ -673,7 +678,7 @@ GEM
|
|||
rdf (~> 3.3)
|
||||
rdoc (6.12.0)
|
||||
psych (>= 4.0.0)
|
||||
redcarpet (3.6.0)
|
||||
redcarpet (3.6.1)
|
||||
redis (4.8.1)
|
||||
redis-namespace (1.11.0)
|
||||
redis (>= 4)
|
||||
|
@ -687,7 +692,7 @@ GEM
|
|||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rexml (3.4.0)
|
||||
rexml (3.4.1)
|
||||
rotp (6.3.0)
|
||||
rouge (4.5.1)
|
||||
rpam2 (4.0.2)
|
||||
|
@ -723,7 +728,7 @@ GEM
|
|||
rspec-mocks (~> 3.0)
|
||||
sidekiq (>= 5, < 8)
|
||||
rspec-support (3.13.2)
|
||||
rubocop (1.72.2)
|
||||
rubocop (1.73.2)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
|
@ -734,15 +739,18 @@ GEM
|
|||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.38.0)
|
||||
rubocop-ast (1.38.1)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-capybara (2.21.0)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-i18n (3.2.3)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (>= 1.72.1)
|
||||
rubocop-performance (1.24.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (>= 1.72.1, < 2.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop-rails (2.30.1)
|
||||
rubocop-rails (2.30.3)
|
||||
activesupport (>= 4.2.0)
|
||||
lint_roller (~> 1.1)
|
||||
rack (>= 1.1)
|
||||
|
@ -774,7 +782,7 @@ GEM
|
|||
activerecord (>= 4.0.0)
|
||||
railties (>= 4.0.0)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.28.0)
|
||||
selenium-webdriver (4.29.1)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
|
@ -814,13 +822,14 @@ GEM
|
|||
stackprof (0.2.27)
|
||||
stoplight (4.1.1)
|
||||
redlock (~> 1.0)
|
||||
stringio (3.1.2)
|
||||
stringio (3.1.4)
|
||||
strong_migrations (2.2.0)
|
||||
activerecord (>= 7)
|
||||
swd (1.3.0)
|
||||
swd (2.0.3)
|
||||
activesupport (>= 3)
|
||||
attr_required (>= 0.0.5)
|
||||
httpclient (>= 2.4)
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
sysexits (1.2.0)
|
||||
temple (0.10.3)
|
||||
terminal-table (4.0.0)
|
||||
|
@ -858,11 +867,8 @@ GEM
|
|||
unicode-display_width (3.1.4)
|
||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||
unicode-emoji (4.0.4)
|
||||
uri (1.0.2)
|
||||
uri (1.0.3)
|
||||
useragent (0.16.11)
|
||||
validate_email (0.1.6)
|
||||
activemodel (>= 3.0)
|
||||
mail (>= 2.2.5)
|
||||
validate_url (1.0.15)
|
||||
activemodel (>= 3.0.0)
|
||||
public_suffix
|
||||
|
@ -876,9 +882,10 @@ GEM
|
|||
openssl (>= 2.2)
|
||||
safety_net_attestation (~> 0.4.0)
|
||||
tpm-key_attestation (~> 0.14.0)
|
||||
webfinger (1.2.0)
|
||||
webfinger (2.1.3)
|
||||
activesupport
|
||||
httpclient (>= 2.4)
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
webmock (3.25.0)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
|
@ -898,7 +905,7 @@ GEM
|
|||
xorcist (1.1.3)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.7.1)
|
||||
zeitwerk (2.7.2)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
@ -977,7 +984,7 @@ DEPENDENCIES
|
|||
omniauth-cas (~> 3.0.0.beta.1)
|
||||
omniauth-rails_csrf_protection (~> 1.0)
|
||||
omniauth-saml (~> 2.0)
|
||||
omniauth_openid_connect (~> 0.6.1)
|
||||
omniauth_openid_connect (~> 0.8.0)
|
||||
opentelemetry-api (~> 1.4.0)
|
||||
opentelemetry-exporter-otlp (~> 0.29.0)
|
||||
opentelemetry-instrumentation-active_job (~> 0.8.0)
|
||||
|
@ -1009,7 +1016,6 @@ DEPENDENCIES
|
|||
rack-cors (~> 2.0)
|
||||
rack-test (~> 2.1)
|
||||
rails (~> 8.0)
|
||||
rails-controller-testing (~> 1.0)
|
||||
rails-i18n (~> 8.0)
|
||||
rdf-normalize (~> 0.5)
|
||||
redcarpet (~> 3.6)
|
||||
|
@ -1021,6 +1027,7 @@ DEPENDENCIES
|
|||
rspec-sidekiq (~> 5.0)
|
||||
rubocop
|
||||
rubocop-capybara
|
||||
rubocop-i18n
|
||||
rubocop-performance
|
||||
rubocop-rails
|
||||
rubocop-rspec
|
||||
|
@ -1059,4 +1066,4 @@ RUBY VERSION
|
|||
ruby 3.4.1p0
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.3
|
||||
2.6.5
|
||||
|
|
|
@ -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
|
16
app/controllers/admin/announcements/previews_controller.rb
Normal file
16
app/controllers/admin/announcements/previews_controller.rb
Normal 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
|
17
app/controllers/admin/announcements/tests_controller.rb
Normal file
17
app/controllers/admin/announcements/tests_controller.rb
Normal 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
|
|
@ -23,7 +23,7 @@ class Admin::TermsOfService::DraftsController < Admin::BaseController
|
|||
private
|
||||
|
||||
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
|
||||
|
||||
def current_terms_of_service
|
||||
|
@ -32,6 +32,6 @@ class Admin::TermsOfService::DraftsController < Admin::BaseController
|
|||
|
||||
def resource_params
|
||||
params
|
||||
.expect(terms_of_service: [:text, :changelog])
|
||||
.expect(terms_of_service: [:text, :changelog, :effective_date])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
class Admin::TermsOfServiceController < Admin::BaseController
|
||||
def index
|
||||
authorize :terms_of_service, :index?
|
||||
@terms_of_service = TermsOfService.live.first
|
||||
@terms_of_service = TermsOfService.published.first
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,7 +31,7 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def set_domain_blocks
|
||||
|
@ -47,6 +47,6 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr
|
|||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -5,12 +5,18 @@ class Api::V1::Instances::TermsOfServicesController < Api::V1::Instances::BaseCo
|
|||
|
||||
def show
|
||||
cache_even_if_authenticated!
|
||||
render json: @terms_of_service, serializer: REST::PrivacyPolicySerializer
|
||||
render json: @terms_of_service, serializer: REST::TermsOfServiceSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
class Api::V1::MediaController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:media' }
|
||||
before_action :require_user!
|
||||
before_action :set_media_attachment, except: [:create]
|
||||
before_action :check_processing, except: [:create]
|
||||
before_action :set_media_attachment, except: [:create, :destroy]
|
||||
before_action :check_processing, except: [:create, :destroy]
|
||||
|
||||
def show
|
||||
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
|
||||
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
|
||||
|
||||
def status_code_for_media_attachment
|
||||
|
@ -54,4 +63,8 @@ class Api::V1::MediaController < Api::BaseController
|
|||
def processing_error
|
||||
{ error: 'Error processing thumbnail for uploaded media' }
|
||||
end
|
||||
|
||||
def in_usage_error
|
||||
{ error: 'Media attachment is currently used by a status' }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -111,7 +111,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
@status.account.statuses_count = @status.account.statuses_count - 1
|
||||
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
|
||||
end
|
||||
|
|
|
@ -46,7 +46,7 @@ class Api::V2::NotificationsController < Api::BaseController
|
|||
end
|
||||
|
||||
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]))
|
||||
render json: presenter, serializer: REST::DedupNotificationGroupSerializer
|
||||
end
|
||||
|
@ -57,7 +57,7 @@ class Api::V2::NotificationsController < Api::BaseController
|
|||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import './public-path';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { afterInitialRender } from 'mastodon/../hooks/useRenderSignal';
|
||||
import { afterInitialRender } from 'mastodon/hooks/useRenderSignal';
|
||||
|
||||
import { start } from '../mastodon/common';
|
||||
import { Status } from '../mastodon/features/standalone/status';
|
||||
|
|
|
@ -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 }) {
|
||||
return (dispatch, getState) => {
|
||||
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
||||
|
|
|
@ -29,7 +29,7 @@ const debouncedSave = debounce((dispatch, getState) => {
|
|||
api().put('/api/web/settings', { data })
|
||||
.then(() => dispatch({ type: SETTING_SAVE }))
|
||||
.catch(error => dispatch(showAlertForError(error)));
|
||||
}, 5000, { trailing: true });
|
||||
}, 2000, { leading: true, trailing: true });
|
||||
|
||||
export function saveSettings() {
|
||||
return (dispatch, getState) => debouncedSave(dispatch, getState);
|
||||
|
|
|
@ -138,7 +138,7 @@ export function deleteStatus(id, withRedraft = false) {
|
|||
|
||||
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(deleteFromTimelines(id));
|
||||
dispatch(importFetchedAccount(response.data.account));
|
||||
|
|
|
@ -4,8 +4,12 @@ import type {
|
|||
ApiPrivacyPolicyJSON,
|
||||
} from 'mastodon/api_types/instance';
|
||||
|
||||
export const apiGetTermsOfService = () =>
|
||||
apiRequestGet<ApiTermsOfServiceJSON>('v1/instance/terms_of_service');
|
||||
export const apiGetTermsOfService = (version?: string) =>
|
||||
apiRequestGet<ApiTermsOfServiceJSON>(
|
||||
version
|
||||
? `v1/instance/terms_of_service/${version}`
|
||||
: 'v1/instance/terms_of_service',
|
||||
);
|
||||
|
||||
export const apiGetPrivacyPolicy = () =>
|
||||
apiRequestGet<ApiPrivacyPolicyJSON>('v1/instance/privacy_policy');
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
export interface ApiTermsOfServiceJSON {
|
||||
updated_at: string;
|
||||
effective_date: string;
|
||||
effective: boolean;
|
||||
succeeded_by: string | null;
|
||||
content: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useLinks } from 'mastodon/../hooks/useLinks';
|
||||
import { useLinks } from 'mastodon/hooks/useLinks';
|
||||
|
||||
export const AccountBio: React.FC<{
|
||||
note: string;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import classNames from 'classnames';
|
||||
|
||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||
import { useLinks } from 'mastodon/../hooks/useLinks';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { useLinks } from 'mastodon/hooks/useLinks';
|
||||
import type { Account } from 'mastodon/models/account';
|
||||
|
||||
export const AccountFields: React.FC<{
|
||||
|
|
|
@ -8,7 +8,7 @@ import type {
|
|||
UsePopperOptions,
|
||||
} from 'react-overlays/esm/usePopper';
|
||||
|
||||
import { useSelectableClick } from '@/hooks/useSelectableClick';
|
||||
import { useSelectableClick } from 'mastodon/hooks/useSelectableClick';
|
||||
|
||||
const offset = [0, 4] as OffsetValue;
|
||||
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useState, useCallback } from 'react';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { useHovering } from 'mastodon/../hooks/useHovering';
|
||||
import { useHovering } from 'mastodon/hooks/useHovering';
|
||||
import { autoPlayGif } from 'mastodon/initial_state';
|
||||
import type { Account } from 'mastodon/models/account';
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { useHovering } from 'mastodon/hooks/useHovering';
|
||||
import { autoPlayGif } from 'mastodon/initial_state';
|
||||
import type { Account } from 'mastodon/models/account';
|
||||
|
||||
import { useHovering } from '../../hooks/useHovering';
|
||||
import { autoPlayGif } from '../initial_state';
|
||||
|
||||
interface Props {
|
||||
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
|
||||
|
|
|
@ -5,8 +5,8 @@ import { FormattedMessage } from 'react-intl';
|
|||
import classNames from 'classnames';
|
||||
|
||||
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
|
||||
import { useTimeout } from 'mastodon/../hooks/useTimeout';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { useTimeout } from 'mastodon/hooks/useTimeout';
|
||||
|
||||
export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
|
|
@ -55,7 +55,7 @@ export const FollowButton: React.FC<{
|
|||
);
|
||||
}
|
||||
|
||||
if (!relationship) return;
|
||||
if (!relationship || !accountId) return;
|
||||
|
||||
if (accountId === me) {
|
||||
return;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useHovering } from '@/hooks/useHovering';
|
||||
import { useHovering } from 'mastodon/hooks/useHovering';
|
||||
import { autoPlayGif } from 'mastodon/initial_state';
|
||||
|
||||
export const GIF: React.FC<{
|
||||
|
|
|
@ -151,7 +151,7 @@ export const Hashtag: React.FC<HashtagProps> = ({
|
|||
<Sparklines
|
||||
width={50}
|
||||
height={28}
|
||||
data={history ? history : Array.from(Array(7)).map(() => 0)}
|
||||
data={history ?? Array.from(Array(7)).map(() => 0)}
|
||||
>
|
||||
<SparklinesCurve style={{ fill: 'none' }} />
|
||||
</Sparklines>
|
||||
|
|
|
@ -8,8 +8,8 @@ import type {
|
|||
UsePopperOptions,
|
||||
} from 'react-overlays/esm/usePopper';
|
||||
|
||||
import { useTimeout } from 'mastodon/../hooks/useTimeout';
|
||||
import { HoverCardAccount } from 'mastodon/components/hover_card_account';
|
||||
import { useTimeout } from 'mastodon/hooks/useTimeout';
|
||||
|
||||
const offset = [-12, 4] as OffsetValue;
|
||||
const enterDelay = 750;
|
||||
|
|
|
@ -149,6 +149,7 @@ export class IconButton extends PureComponent<Props, States> {
|
|||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||
onKeyPress={this.handleKeyPress}
|
||||
style={style}
|
||||
tabIndex={tabIndex}
|
||||
|
|
|
@ -81,6 +81,7 @@ class ScrollableList extends PureComponent {
|
|||
bindToDocument: PropTypes.bool,
|
||||
preventScroll: PropTypes.bool,
|
||||
footer: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -325,7 +326,7 @@ class ScrollableList extends PureComponent {
|
|||
};
|
||||
|
||||
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 childrenCount = Children.count(children);
|
||||
|
||||
|
@ -336,9 +337,9 @@ class ScrollableList extends PureComponent {
|
|||
if (showLoading) {
|
||||
scrollableArea = (
|
||||
<div className='scrollable scrollable--flex' ref={this.setRef}>
|
||||
<div role='feed' className='item-list'>
|
||||
{prepend}
|
||||
</div>
|
||||
{prepend}
|
||||
|
||||
<div role='feed' className='item-list' />
|
||||
|
||||
<div className='scrollable__append'>
|
||||
<LoadingIndicator />
|
||||
|
@ -350,9 +351,9 @@ class ScrollableList extends PureComponent {
|
|||
} else if (isLoading || childrenCount > 0 || numPending > 0 || hasMore || !emptyMessage) {
|
||||
scrollableArea = (
|
||||
<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}
|
||||
|
||||
{Children.map(this.props.children, (child, index) => (
|
||||
|
|
|
@ -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)));
|
|
@ -11,11 +11,15 @@ import { Icon } from 'mastodon/components/icon';
|
|||
import { formatTime } from 'mastodon/features/video';
|
||||
import { autoPlayGif, displayMedia, useBlurhash } from 'mastodon/initial_state';
|
||||
import type { Status, MediaAttachment } from 'mastodon/models/status';
|
||||
import { useAppSelector } from 'mastodon/store';
|
||||
|
||||
export const MediaItem: React.FC<{
|
||||
attachment: MediaAttachment;
|
||||
onOpenMedia: (arg0: MediaAttachment) => void;
|
||||
}> = ({ attachment, onOpenMedia }) => {
|
||||
const account = useAppSelector((state) =>
|
||||
state.accounts.get(attachment.getIn(['status', 'account']) as string),
|
||||
);
|
||||
const [visible, setVisible] = useState(
|
||||
(displayMedia !== 'hide_all' &&
|
||||
!attachment.getIn(['status', 'sensitive'])) ||
|
||||
|
@ -70,7 +74,6 @@ export const MediaItem: React.FC<{
|
|||
const lang = status.get('language') as string;
|
||||
const blurhash = attachment.get('blurhash') as string;
|
||||
const statusId = status.get('id') as string;
|
||||
const acct = status.getIn(['account', 'acct']) as string;
|
||||
const type = attachment.get('type') as string;
|
||||
|
||||
let thumbnail;
|
||||
|
@ -181,7 +184,7 @@ export const MediaItem: React.FC<{
|
|||
|
||||
<a
|
||||
className='media-gallery__item-thumbnail'
|
||||
href={`/@${acct}/${statusId}`}
|
||||
href={`/@${account?.acct}/${statusId}`}
|
||||
onClick={handleClick}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
|
|
|
@ -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);
|
283
app/javascript/mastodon/features/account_gallery/index.tsx
Normal file
283
app/javascript/mastodon/features/account_gallery/index.tsx
Normal 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
|
@ -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;
|
|
@ -1,11 +1,12 @@
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const MemorialNote = () => (
|
||||
export const MemorialNote: React.FC = () => (
|
||||
<div className='account-memorial-banner'>
|
||||
<div className='account-memorial-banner__message'>
|
||||
<FormattedMessage id='account.in_memoriam' defaultMessage='In Memoriam.' />
|
||||
<FormattedMessage
|
||||
id='account.in_memoriam'
|
||||
defaultMessage='In Memoriam.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default MemorialNote;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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));
|
|
@ -11,7 +11,7 @@ import { TimelineHint } from 'mastodon/components/timeline_hint';
|
|||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
||||
import { getAccountHidden } from 'mastodon/selectors';
|
||||
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
||||
import { useAppSelector } from 'mastodon/store';
|
||||
|
||||
import { lookupAccount, fetchAccount } from '../../actions/accounts';
|
||||
|
@ -22,8 +22,8 @@ import { LoadingIndicator } from '../../components/loading_indicator';
|
|||
import StatusList from '../../components/status_list';
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
import { AccountHeader } from './components/account_header';
|
||||
import { LimitedAccountHint } from './components/limited_account_hint';
|
||||
import HeaderContainer from './containers/header_container';
|
||||
|
||||
const emptyList = ImmutableList();
|
||||
|
||||
|
@ -198,7 +198,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
<ColumnBackButton />
|
||||
|
||||
<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
|
||||
append={remoteMessage}
|
||||
scrollKey='account_timeline'
|
||||
|
|
|
@ -6,9 +6,9 @@ import classNames from 'classnames';
|
|||
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
import { useSelectableClick } from '@/hooks/useSelectableClick';
|
||||
import QuestionMarkIcon from '@/material-icons/400-24px/question_mark.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { useSelectableClick } from 'mastodon/hooks/useSelectableClick';
|
||||
|
||||
const messages = defineMessages({
|
||||
help: { id: 'info_button.label', defaultMessage: 'Help' },
|
||||
|
|
|
@ -120,7 +120,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
this.props.onSubmit(missingAltTextModal && this.props.missingAltText);
|
||||
this.props.onSubmit(missingAltTextModal && this.props.missingAltText && this.props.privacy !== 'direct');
|
||||
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
|
|
|
@ -12,11 +12,14 @@ import Overlay from 'react-overlays/Overlay';
|
|||
|
||||
import MoodIcon from '@/material-icons/400-20px/mood.svg?react';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import emojiCompressed from 'mastodon/features/emoji/emoji_compressed';
|
||||
import { assetHost } from 'mastodon/utils/config';
|
||||
|
||||
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
|
||||
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
|
||||
|
||||
const nimblePickerData = emojiCompressed[5];
|
||||
|
||||
const messages = defineMessages({
|
||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||
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 backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`;
|
||||
const backgroundImageFn = () => `${assetHost}/emoji/sheet_15.png`;
|
||||
|
||||
const notFoundFn = () => (
|
||||
<div className='emoji-mart-no-results'>
|
||||
<Emoji
|
||||
data={nimblePickerData}
|
||||
emoji='sleuth_or_spy'
|
||||
set='twitter'
|
||||
size={32}
|
||||
sheetSize={32}
|
||||
sheetColumns={62}
|
||||
sheetRows={62}
|
||||
backgroundImageFn={backgroundImageFn}
|
||||
/>
|
||||
|
||||
|
@ -67,7 +73,7 @@ class ModifierPickerMenu extends PureComponent {
|
|||
this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
|
||||
};
|
||||
|
||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.active) {
|
||||
this.attachListeners();
|
||||
} else {
|
||||
|
@ -75,7 +81,7 @@ class ModifierPickerMenu extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
componentWillUnmount() {
|
||||
this.removeListeners();
|
||||
}
|
||||
|
||||
|
@ -85,12 +91,12 @@ class ModifierPickerMenu extends PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
attachListeners () {
|
||||
attachListeners() {
|
||||
document.addEventListener('click', this.handleDocumentClick, { capture: true });
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
}
|
||||
|
||||
removeListeners () {
|
||||
removeListeners() {
|
||||
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
|
||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
}
|
||||
|
@ -99,17 +105,17 @@ class ModifierPickerMenu extends PureComponent {
|
|||
this.node = c;
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { active } = this.props;
|
||||
|
||||
return (
|
||||
<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={2}><Emoji 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={4}><Emoji 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={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} 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 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 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 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 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 data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -139,12 +145,12 @@ class ModifierPicker extends PureComponent {
|
|||
this.props.onClose();
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { active, modifier } = this.props;
|
||||
|
||||
return (
|
||||
<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} />
|
||||
</div>
|
||||
);
|
||||
|
@ -184,7 +190,7 @@ class EmojiPickerMenuImpl extends PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
document.addEventListener('click', this.handleDocumentClick, { capture: true });
|
||||
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('touchend', this.handleDocumentClick, listenerOptions);
|
||||
}
|
||||
|
@ -252,7 +258,7 @@ class EmojiPickerMenuImpl extends PureComponent {
|
|||
this.props.onSkinTone(modifier);
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
|
||||
|
||||
if (loading) {
|
||||
|
@ -280,6 +286,9 @@ class EmojiPickerMenuImpl extends PureComponent {
|
|||
return (
|
||||
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
|
||||
<EmojiPicker
|
||||
data={nimblePickerData}
|
||||
sheetColumns={62}
|
||||
sheetRows={62}
|
||||
perLine={8}
|
||||
emojiSize={22}
|
||||
sheetSize={32}
|
||||
|
@ -345,7 +354,7 @@ class EmojiPickerDropdown extends PureComponent {
|
|||
|
||||
EmojiPickerAsync().then(EmojiMart => {
|
||||
EmojiPicker = EmojiMart.Picker;
|
||||
Emoji = EmojiMart.Emoji;
|
||||
Emoji = EmojiMart.Emoji;
|
||||
|
||||
this.setState({ loading: false });
|
||||
}).catch(() => {
|
||||
|
@ -386,7 +395,7 @@ class EmojiPickerDropdown extends PureComponent {
|
|||
this.setState({ placement: state.placement });
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
|
||||
const title = intl.formatMessage(messages.emoji);
|
||||
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 }}>
|
||||
{({ props, placement })=> (
|
||||
{({ props, placement }) => (
|
||||
<div {...props} style={{ ...props.style }}>
|
||||
<div className={`dropdown-animation ${placement}`}>
|
||||
<EmojiPickerMenu
|
||||
|
|
|
@ -22,10 +22,9 @@ import { LoadMore } from 'mastodon/components/load_more';
|
|||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import { RadioButton } from 'mastodon/components/radio_button';
|
||||
import ScrollContainer from 'mastodon/containers/scroll_container';
|
||||
import { useSearchParam } from 'mastodon/hooks/useSearchParam';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
import { useSearchParam } from '../../../hooks/useSearchParam';
|
||||
|
||||
import { AccountCard } from './components/account_card';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
|
|
@ -45,6 +45,7 @@ type EmojiCompressed = [
|
|||
Category[],
|
||||
Data['aliases'],
|
||||
EmojisWithoutShortCodes,
|
||||
Data,
|
||||
];
|
||||
|
||||
/*
|
||||
|
|
|
@ -9,18 +9,91 @@
|
|||
|
||||
// This version comment should be bumped each time the emoji data is changed
|
||||
// to ensure that the prevaled file is regenerated by Babel
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
const { emojiIndex } = require('emoji-mart');
|
||||
let data = require('emoji-mart/data/all.json');
|
||||
// This json file contains the names of the categories.
|
||||
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 _ = require('lodash');
|
||||
|
||||
|
||||
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 { unicodeToUnifiedName } = require('./unicode_to_unified_name');
|
||||
|
||||
if(data.compressed) {
|
||||
data = emojiMartUncompress(data);
|
||||
// Grabbed from `emoji_utils` to avoid circular dependency
|
||||
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;
|
||||
|
@ -32,15 +105,10 @@ const shortcodeMap = {};
|
|||
const shortCodesToEmojiData = {};
|
||||
const emojisWithoutShortCodes = [];
|
||||
|
||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
||||
let emoji = emojiIndex.emojis[key];
|
||||
Object.keys(emojiMart5Data.emojis).forEach(key => {
|
||||
let emoji = emojiMart5Data.emojis[key];
|
||||
|
||||
// Emojis with skin tone modifiers are stored like this
|
||||
if (Object.hasOwn(emoji, '1')) {
|
||||
emoji = emoji['1'];
|
||||
}
|
||||
|
||||
shortcodeMap[emoji.native] = emoji.id;
|
||||
shortcodeMap[emoji.skins[0].native] = emoji.id;
|
||||
});
|
||||
|
||||
const stripModifiers = unicode => {
|
||||
|
@ -84,13 +152,9 @@ Object.keys(emojiMap).forEach(key => {
|
|||
}
|
||||
});
|
||||
|
||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
||||
let emoji = emojiIndex.emojis[key];
|
||||
Object.keys(emojiMartData.emojis).forEach(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;
|
||||
let { short_names, search, unified } = emojiMartData.emojis[key];
|
||||
|
@ -135,4 +199,5 @@ module.exports = JSON.parse(JSON.stringify([
|
|||
emojiMartData.categories,
|
||||
emojiMartData.aliases,
|
||||
emojisWithoutShortCodes,
|
||||
emojiMartData
|
||||
]));
|
||||
|
|
|
@ -8,14 +8,15 @@ import type { Search, ShortCodesToEmojiData } from './emoji_compressed';
|
|||
import emojiCompressed from './emoji_compressed';
|
||||
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
||||
|
||||
type Emojis = {
|
||||
[key in NonNullable<keyof ShortCodesToEmojiData>]: {
|
||||
type Emojis = Record<
|
||||
NonNullable<keyof ShortCodesToEmojiData>,
|
||||
{
|
||||
native: BaseEmoji['native'];
|
||||
search: Search;
|
||||
short_names: Emoji['short_names'];
|
||||
unified: Emoji['unified'];
|
||||
};
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
const [
|
||||
shortCodesToEmojiData,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Emoji from 'emoji-mart/dist-es/components/emoji/emoji';
|
||||
import Picker from 'emoji-mart/dist-es/components/picker/picker';
|
||||
import Emoji from 'emoji-mart/dist-es/components/emoji/nimble-emoji';
|
||||
import Picker from 'emoji-mart/dist-es/components/picker/nimble-picker';
|
||||
|
||||
export {
|
||||
Picker,
|
||||
|
|
1
app/javascript/mastodon/features/emoji/emoji_sheet.json
Normal file
1
app/javascript/mastodon/features/emoji/emoji_sheet.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -9,12 +9,13 @@ import type {
|
|||
import emojiCompressed from './emoji_compressed';
|
||||
import { unicodeToFilename } from './unicode_to_filename';
|
||||
|
||||
type UnicodeMapping = {
|
||||
[key in FilenameData[number][0]]: {
|
||||
type UnicodeMapping = Record<
|
||||
FilenameData[number][0],
|
||||
{
|
||||
shortCode: ShortCodesToEmojiDataKey;
|
||||
filename: FilenameData[number][number];
|
||||
};
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
const [
|
||||
shortCodesToEmojiData,
|
||||
|
|
|
@ -10,9 +10,10 @@ import { debounce } from 'lodash';
|
|||
|
||||
import { Account } from 'mastodon/components/account';
|
||||
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 { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
||||
import { getAccountHidden } from 'mastodon/selectors';
|
||||
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
||||
import { useAppSelector } from 'mastodon/store';
|
||||
|
||||
import {
|
||||
|
@ -25,7 +26,6 @@ import { ColumnBackButton } from '../../components/column_back_button';
|
|||
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
|
||||
import HeaderContainer from '../account_timeline/containers/header_container';
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||
|
@ -168,7 +168,7 @@ class Followers extends ImmutablePureComponent {
|
|||
hasMore={!forceEmptyState && hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
|
||||
prepend={<AccountHeader accountId={this.props.accountId} hideTabs />}
|
||||
alwaysPrepend
|
||||
append={remoteMessage}
|
||||
emptyMessage={emptyMessage}
|
||||
|
|
|
@ -10,9 +10,10 @@ import { debounce } from 'lodash';
|
|||
|
||||
import { Account } from 'mastodon/components/account';
|
||||
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 { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
||||
import { getAccountHidden } from 'mastodon/selectors';
|
||||
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
||||
import { useAppSelector } from 'mastodon/store';
|
||||
|
||||
import {
|
||||
|
@ -25,7 +26,6 @@ import { ColumnBackButton } from '../../components/column_back_button';
|
|||
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
|
||||
import HeaderContainer from '../account_timeline/containers/header_container';
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||
|
@ -168,7 +168,7 @@ class Following extends ImmutablePureComponent {
|
|||
hasMore={!forceEmptyState && hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
|
||||
prepend={<AccountHeader accountId={this.props.accountId} hideTabs />}
|
||||
alwaysPrepend
|
||||
append={remoteMessage}
|
||||
emptyMessage={emptyMessage}
|
||||
|
|
|
@ -17,7 +17,7 @@ export const ColumnSettings: React.FC = () => {
|
|||
|
||||
const dispatch = useAppDispatch();
|
||||
const onChange = useCallback(
|
||||
(key: string, checked: boolean) => {
|
||||
(key: string[], checked: boolean) => {
|
||||
dispatch(changeSetting(['home', ...key], checked));
|
||||
},
|
||||
[dispatch],
|
||||
|
|
|
@ -4,7 +4,6 @@ import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
|||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { useSearchParam } from '@/hooks/useSearchParam';
|
||||
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
|
||||
import PeopleIcon from '@/material-icons/400-24px/group.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 Status from 'mastodon/containers/status_container';
|
||||
import { Search } from 'mastodon/features/compose/components/search';
|
||||
import { useSearchParam } from 'mastodon/hooks/useSearchParam';
|
||||
import type { Hashtag as HashtagType } from 'mastodon/models/tags';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
|
|
|
@ -6,11 +6,11 @@ import { useEffect, useCallback } from 'react';
|
|||
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { useRenderSignal } from 'mastodon/../hooks/useRenderSignal';
|
||||
import { fetchStatus, toggleStatusSpoilers } from 'mastodon/actions/statuses';
|
||||
import { hydrateStore } from 'mastodon/actions/store';
|
||||
import { Router } from 'mastodon/components/router';
|
||||
import { DetailedStatus } from 'mastodon/features/status/components/detailed_status';
|
||||
import { useRenderSignal } from 'mastodon/hooks/useRenderSignal';
|
||||
import initialState from 'mastodon/initial_state';
|
||||
import { IntlProvider } from 'mastodon/locales';
|
||||
import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors';
|
||||
|
|
|
@ -221,12 +221,12 @@ export const DetailedStatus: React.FC<{
|
|||
/>
|
||||
);
|
||||
}
|
||||
} else if (status.get('spoiler_text').length === 0) {
|
||||
} else if (status.get('card')) {
|
||||
media = (
|
||||
<Card
|
||||
sensitive={status.get('sensitive')}
|
||||
onOpenMedia={onOpenMedia}
|
||||
card={status.get('card', null)}
|
||||
card={status.get('card')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,26 +8,31 @@ import {
|
|||
} from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
|
||||
import { apiGetTermsOfService } from 'mastodon/api/instance';
|
||||
import type { ApiTermsOfServiceJSON } from 'mastodon/api_types/instance';
|
||||
import { Column } from 'mastodon/components/column';
|
||||
import { Skeleton } from 'mastodon/components/skeleton';
|
||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'terms_of_service.title', defaultMessage: 'Terms of Service' },
|
||||
});
|
||||
|
||||
interface Params {
|
||||
date?: string;
|
||||
}
|
||||
|
||||
const TermsOfService: React.FC<{
|
||||
multiColumn: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
const intl = useIntl();
|
||||
const { date } = useParams<Params>();
|
||||
const [response, setResponse] = useState<ApiTermsOfServiceJSON>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
apiGetTermsOfService()
|
||||
apiGetTermsOfService(date)
|
||||
.then((data) => {
|
||||
setResponse(data);
|
||||
setLoading(false);
|
||||
|
@ -36,7 +41,7 @@ const TermsOfService: React.FC<{
|
|||
.catch(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
}, [date]);
|
||||
|
||||
if (!loading && !response) {
|
||||
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
|
||||
|
@ -55,23 +60,60 @@ const TermsOfService: React.FC<{
|
|||
defaultMessage='Terms of Service'
|
||||
/>
|
||||
</h3>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='privacy_policy.last_updated'
|
||||
defaultMessage='Last updated {date}'
|
||||
values={{
|
||||
date: loading ? (
|
||||
<Skeleton width='10ch' />
|
||||
) : (
|
||||
<FormattedDate
|
||||
value={response?.updated_at}
|
||||
year='numeric'
|
||||
month='short'
|
||||
day='2-digit'
|
||||
<p className='prose'>
|
||||
{response?.effective ? (
|
||||
<FormattedMessage
|
||||
id='privacy_policy.last_updated'
|
||||
defaultMessage='Last updated {date}'
|
||||
values={{
|
||||
date: (
|
||||
<FormattedDate
|
||||
value={response.effective_date}
|
||||
year='numeric'
|
||||
month='short'
|
||||
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>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -101,6 +101,7 @@ const EmbedModal: React.FC<{
|
|||
/>
|
||||
|
||||
<iframe
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||
frameBorder='0'
|
||||
ref={iframeRef}
|
||||
sandbox='allow-scripts allow-same-origin'
|
||||
|
|
|
@ -205,7 +205,7 @@ class SwitchingColumnsArea extends PureComponent {
|
|||
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
|
||||
<WrappedRoute path='/about' component={About} 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} />
|
||||
<Redirect from='/timelines/public' to='/public' exact />
|
||||
|
|
|
@ -592,7 +592,6 @@
|
|||
"poll_button.remove_poll": "إزالة استطلاع الرأي",
|
||||
"privacy.change": "اضبط خصوصية المنشور",
|
||||
"privacy.direct.long": "كل من ذُكر في المنشور",
|
||||
"privacy.direct.short": "أشخاص محددون",
|
||||
"privacy.private.long": "متابعيك فقط",
|
||||
"privacy.private.short": "للمتابِعين",
|
||||
"privacy.public.long": "أي شخص على أو خارج ماستدون",
|
||||
|
|
|
@ -4,15 +4,16 @@
|
|||
"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.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.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.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.rules": "Normes del sirvidor",
|
||||
"account.account_note_header": "Nota personal",
|
||||
"account.add_or_remove_from_list": "Amestar o quitar de les llistes",
|
||||
"account.badges.bot": "Automatizóse",
|
||||
"account.badges.group": "Grupu",
|
||||
"account.block": "Bloquiar a @{name}",
|
||||
"account.block_domain": "Bloquiar el dominiu {domain}",
|
||||
|
@ -25,7 +26,7 @@
|
|||
"account.edit_profile": "Editar el perfil",
|
||||
"account.enable_notifications": "Avisame cuando @{name} espublice artículos",
|
||||
"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.follow": "Siguir",
|
||||
"account.follow_back": "Siguir tamién",
|
||||
|
@ -107,6 +108,7 @@
|
|||
"column.domain_blocks": "Dominios bloquiaos",
|
||||
"column.edit_list": "Editar la llista",
|
||||
"column.favourites": "Favoritos",
|
||||
"column.firehose": "Feed en direuto",
|
||||
"column.follow_requests": "Solicitúes de siguimientu",
|
||||
"column.home": "Aniciu",
|
||||
"column.lists": "Llistes",
|
||||
|
@ -126,9 +128,9 @@
|
|||
"community.column_settings.remote_only": "Namás lo remoto",
|
||||
"compose.language.change": "Camudar la llingua",
|
||||
"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.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.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.",
|
||||
|
@ -137,34 +139,33 @@
|
|||
"compose_form.poll.option_placeholder": "Opción {number}",
|
||||
"compose_form.poll.type": "Tipu",
|
||||
"compose_form.publish": "Espublizar",
|
||||
"compose_form.publish_form": "Artículu nuevu",
|
||||
"compose_form.publish_form": "Publicación nueva",
|
||||
"compose_form.reply": "Responder",
|
||||
"confirmation_modal.cancel": "Encaboxar",
|
||||
"confirmations.block.confirm": "Bloquiar",
|
||||
"confirmations.delete.confirm": "Desaniciar",
|
||||
"confirmations.delete.message": "¿De xuru que quies desaniciar esti artículu?",
|
||||
"confirmations.delete.title": "¿Desaniciar l'artículu?",
|
||||
"confirmations.delete.message": "¿De xuru que quies desaniciar esta publicación?",
|
||||
"confirmations.delete.title": "¿Quies desaniciar esta publicación?",
|
||||
"confirmations.delete_list.confirm": "Desaniciar",
|
||||
"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.edit.confirm": "Editar",
|
||||
"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.logout.confirm": "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.title": "¿Quies amestar testu alternativu?",
|
||||
"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 ya reeditar l'artículu?",
|
||||
"confirmations.redraft.title": "¿Desaniciar y reeditar la publicación?",
|
||||
"confirmations.reply.confirm": "Responder",
|
||||
"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.message": "¿De xuru que quies dexar de siguir a {name}?",
|
||||
"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_more": "Amosar más",
|
||||
"conversation.delete": "Desaniciar la conversación",
|
||||
|
@ -186,7 +187,7 @@
|
|||
"domain_block_modal.title": "Bloquiar el dominiu?",
|
||||
"domain_pill.server": "Sirvidor",
|
||||
"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:",
|
||||
"emoji_button.activity": "Actividá",
|
||||
"emoji_button.flags": "Banderes",
|
||||
|
@ -201,9 +202,9 @@
|
|||
"emoji_button.symbols": "Símbolos",
|
||||
"emoji_button.travel": "Viaxes y llugares",
|
||||
"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.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.domain_blocks": "Nun hai nengún dominiu bloquiáu.",
|
||||
"empty_column.explore_statuses": "Agora nun hai nada en tendencia. ¡Volvi equí dempués!",
|
||||
|
@ -223,21 +224,21 @@
|
|||
"explore.trending_links": "Noticies",
|
||||
"explore.trending_statuses": "Artículos",
|
||||
"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.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.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.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.select_filter.expired": "caducó",
|
||||
"filter_modal.select_filter.prompt_new": "Categoría nueva: {name}",
|
||||
"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.title": "Peñerar esti artículu",
|
||||
"filter_modal.title.status": "Peñera d'un artículu",
|
||||
"filter_modal.select_filter.title": "Peñerar esta publicación",
|
||||
"filter_modal.title.status": "Peñera d'una publicación",
|
||||
"firehose.all": "Tolos sirvidores",
|
||||
"firehose.local": "Esti sirvidor",
|
||||
"firehose.remote": "Otros sirvidores",
|
||||
|
@ -285,20 +286,20 @@
|
|||
"interaction_modal.on_another_server": "N'otru sirvidor",
|
||||
"interaction_modal.on_this_server": "Nesti sirvidor",
|
||||
"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}",
|
||||
"intervals.full.days": "{number, plural, one {# día} other {# díes}}",
|
||||
"intervals.full.hours": "{number, plural, one {# hora} other {# hores}}",
|
||||
"intervals.full.minutes": "{number, plural, one {# minutu} other {# minutos}}",
|
||||
"keyboard_shortcuts.back": "Dir p'atrás",
|
||||
"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.compose": "Enfocar l'área de composición",
|
||||
"keyboard_shortcuts.description": "Descripción",
|
||||
"keyboard_shortcuts.direct": "p'abrir la columna de les menciones privaes",
|
||||
"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.heading": "Atayos del tecláu",
|
||||
"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.pinned": "Abrir la llista d'artículos fixaos",
|
||||
"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.search": "Enfocar la barra de busca",
|
||||
"keyboard_shortcuts.start": "Abrir la columna «Entamar»",
|
||||
"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.up": "Xubir na llista",
|
||||
"lightbox.close": "Zarrar",
|
||||
|
@ -377,9 +378,9 @@
|
|||
"notification.mentioned_you": "{name} mentóte",
|
||||
"notification.moderation-warning.learn_more": "Deprender más",
|
||||
"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.update": "{name} editó un artículu",
|
||||
"notification.update": "{name} editó una publicación",
|
||||
"notification_requests.edit_selection": "Editar",
|
||||
"notification_requests.exit_selection": "Fecho",
|
||||
"notifications.clear": "Borrar los avisos",
|
||||
|
@ -421,10 +422,10 @@
|
|||
"poll.votes": "{votes, plural, one {# votu} other {# votos}}",
|
||||
"poll_button.add_poll": "Amestar una encuesta",
|
||||
"poll_button.remove_poll": "Quitar la encuesta",
|
||||
"privacy.change": "Configurar la privacidá del artículu",
|
||||
"privacy.direct.short": "Perfiles específicos",
|
||||
"privacy.change": "Configurar la privacidá de la publicación",
|
||||
"privacy.direct.short": "Mención privada",
|
||||
"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.title": "Política de privacidá",
|
||||
"refresh": "Anovar",
|
||||
|
@ -448,7 +449,7 @@
|
|||
"report.category.subtitle": "Escueyi la meyor opción",
|
||||
"report.category.title": "Dinos qué pasa con esti {type}",
|
||||
"report.category.title_account": "perfil",
|
||||
"report.category.title_status": "artículu",
|
||||
"report.category.title_status": "publicación",
|
||||
"report.close": "Fecho",
|
||||
"report.comment.title": "¿Hai daqué más qu'habríemos saber?",
|
||||
"report.forward": "Reunviar a {target}",
|
||||
|
@ -468,7 +469,7 @@
|
|||
"report.rules.subtitle": "Seleiciona tolo que s'axuste",
|
||||
"report.rules.title": "¿Qué normes s'incumplen?",
|
||||
"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.target": "Informe de: {target}",
|
||||
"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.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_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_sentence": "conteníu illegal",
|
||||
"report_notification.categories.spam": "Spam",
|
||||
|
@ -490,6 +491,7 @@
|
|||
"search.quick_action.go_to_hashtag": "Dir a la etiqueta {x}",
|
||||
"search.quick_action.status_search": "Artículos que concasen con {x}",
|
||||
"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.options": "Opciones de busca",
|
||||
"search_popout.quick_actions": "Aiciones rápides",
|
||||
|
@ -501,22 +503,25 @@
|
|||
"search_results.hashtags": "Etiquetes",
|
||||
"search_results.see_all": "Ver too",
|
||||
"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:",
|
||||
"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",
|
||||
"status.admin_account": "Abrir la interfaz de moderación pa @{name}",
|
||||
"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.bookmark": "Meter en Marcadores",
|
||||
"status.cannot_reblog": "Esti artículu nun se pue compartir",
|
||||
"status.copy": "Copiar l'enllaz al artículu",
|
||||
"status.cannot_reblog": "Esta publicación nun se pue compartir",
|
||||
"status.copy": "Copiar l'enllaz a la publicación",
|
||||
"status.delete": "Desaniciar",
|
||||
"status.direct": "Mentar a @{name} per privao",
|
||||
"status.direct_indicator": "Mención privada",
|
||||
"status.edited_x_times": "Editóse {count, plural, one {{count} vegada} other {{count} vegaes}}",
|
||||
"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.edited": "{name} editó {date}",
|
||||
"status.load_more": "Cargar más",
|
||||
|
@ -525,13 +530,13 @@
|
|||
"status.more": "Más",
|
||||
"status.mute": "Desactivar los avisos de @{name}",
|
||||
"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.pinned": "Artículu fixáu",
|
||||
"status.pinned": "Publicación fixada",
|
||||
"status.read_more": "Lleer más",
|
||||
"status.reblog": "Compartir",
|
||||
"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.remove_bookmark": "Desaniciar el marcador",
|
||||
"status.replied_to": "En rempuesta a {name}",
|
||||
|
|
|
@ -642,7 +642,6 @@
|
|||
"poll_button.remove_poll": "Выдаліць апытанне",
|
||||
"privacy.change": "Змяніць прыватнасць допісу",
|
||||
"privacy.direct.long": "Усе згаданыя ў допісе",
|
||||
"privacy.direct.short": "Канкрэтныя людзі",
|
||||
"privacy.private.long": "Толькі вашыя падпісчыкі",
|
||||
"privacy.private.short": "Падпісчыкі",
|
||||
"privacy.public.long": "Усе, хто ёсць і каго няма ў Mastodon",
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
"poll_button.remove_poll": "Премахване на анкета",
|
||||
"privacy.change": "Промяна на поверителността на публикация",
|
||||
"privacy.direct.long": "Споменатите в публикацията",
|
||||
"privacy.direct.short": "Определени хора",
|
||||
"privacy.direct.short": "Частно споменаване",
|
||||
"privacy.private.long": "Само последователите ви",
|
||||
"privacy.private.short": "Последователи",
|
||||
"privacy.public.long": "Всеки във и извън Mastodon",
|
||||
|
|
|
@ -436,7 +436,6 @@
|
|||
"poll_button.add_poll": "Ouzhpennañ ur sontadeg",
|
||||
"poll_button.remove_poll": "Dilemel ar sontadeg",
|
||||
"privacy.change": "Cheñch prevezded an embannadur",
|
||||
"privacy.direct.short": "Tud resis",
|
||||
"privacy.private.short": "Heulierien",
|
||||
"privacy.public.short": "Publik",
|
||||
"privacy_policy.last_updated": "Hizivadenn ziwezhañ {date}",
|
||||
|
|
|
@ -696,7 +696,7 @@
|
|||
"poll_button.remove_poll": "Elimina l'enquesta",
|
||||
"privacy.change": "Canvia la privacitat del tut",
|
||||
"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.short": "Seguidors",
|
||||
"privacy.public.long": "Tothom dins o fora Mastodon",
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
"poll_button.remove_poll": "Odebrat anketu",
|
||||
"privacy.change": "Změnit soukromí 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.short": "Sledující",
|
||||
"privacy.public.long": "Kdokoliv na Mastodonu i mimo něj",
|
||||
|
|
|
@ -696,7 +696,6 @@
|
|||
"poll_button.remove_poll": "Tynnu pleidlais",
|
||||
"privacy.change": "Addasu preifatrwdd y post",
|
||||
"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.short": "Dilynwyr",
|
||||
"privacy.public.long": "Unrhyw ar ac oddi ar Mastodon",
|
||||
|
|
|
@ -697,12 +697,12 @@
|
|||
"poll_button.remove_poll": "Fjern afstemning",
|
||||
"privacy.change": "Tilpas indlægsfortrolighed",
|
||||
"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.short": "Følgere",
|
||||
"privacy.public.long": "Alle på og udenfor Mastodon",
|
||||
"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.short": "Stille offentligt",
|
||||
"privacy_policy.last_updated": "Senest opdateret {date}",
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
"poll_button.remove_poll": "Umfrage entfernen",
|
||||
"privacy.change": "Sichtbarkeit anpassen",
|
||||
"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.short": "Follower",
|
||||
"privacy.public.long": "Alle in und außerhalb von Mastodon",
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
"poll_button.remove_poll": "Αφαίρεση δημοσκόπησης",
|
||||
"privacy.change": "Προσαρμογή ιδιωτικότητας ανάρτησης",
|
||||
"privacy.direct.long": "Όλοι όσοι αναφέρθηκαν στη δημοσίευση",
|
||||
"privacy.direct.short": "Συγκεκριμένα άτομα",
|
||||
"privacy.direct.short": "Ιδιωτική επισήμανση",
|
||||
"privacy.private.long": "Μόνο οι ακόλουθοί σας",
|
||||
"privacy.private.short": "Ακόλουθοι",
|
||||
"privacy.public.long": "Όλοι εντός και εκτός του Mastodon",
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
"poll_button.remove_poll": "Remove poll",
|
||||
"privacy.change": "Change post privacy",
|
||||
"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.short": "Followers",
|
||||
"privacy.public.long": "Anyone on and off Mastodon",
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
"poll_button.remove_poll": "Remove poll",
|
||||
"privacy.change": "Change post privacy",
|
||||
"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.short": "Followers",
|
||||
"privacy.public.long": "Anyone on and off Mastodon",
|
||||
|
@ -872,7 +872,9 @@
|
|||
"subscribed_languages.target": "Change subscribed languages for {target}",
|
||||
"tabs_bar.home": "Home",
|
||||
"tabs_bar.notifications": "Notifications",
|
||||
"terms_of_service.effective_as_of": "Effective as of {date}",
|
||||
"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.hours": "{number, plural, one {# hour} other {# hours}} left",
|
||||
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
"poll_button.remove_poll": "Forigi balotenketon",
|
||||
"privacy.change": "Ŝanĝu afiŝan privatecon",
|
||||
"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.short": "Sekvantoj",
|
||||
"privacy.public.long": "Ĉiujn ajn ĉe kaj ekster Mastodon",
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
"poll_button.remove_poll": "Quitar encuesta",
|
||||
"privacy.change": "Configurar privacidad del 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.short": "Seguidores",
|
||||
"privacy.public.long": "Cualquier persona dentro y fuera de Mastodon",
|
||||
|
|
|
@ -192,10 +192,10 @@
|
|||
"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.type": "Estilo",
|
||||
"compose_form.publish": "Publicación",
|
||||
"compose_form.publish": "Publicar",
|
||||
"compose_form.publish_form": "Nueva publicación",
|
||||
"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.unmarked": "Añadir advertencia de contenido",
|
||||
"compose_form.spoiler_placeholder": "Advertencia de contenido (opcional)",
|
||||
|
@ -697,7 +697,7 @@
|
|||
"poll_button.remove_poll": "Eliminar encuesta",
|
||||
"privacy.change": "Ajustar privacidad",
|
||||
"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.short": "Seguidores",
|
||||
"privacy.public.long": "Cualquiera dentro y fuera de Mastodon",
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
"poll_button.remove_poll": "Eliminar encuesta",
|
||||
"privacy.change": "Ajustar privacidad",
|
||||
"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.short": "Seguidores",
|
||||
"privacy.public.long": "Visible por todo el mundo, dentro y fuera de Mastodon",
|
||||
|
|
|
@ -696,7 +696,6 @@
|
|||
"poll_button.remove_poll": "Eemalda küsitlus",
|
||||
"privacy.change": "Muuda postituse nähtavust",
|
||||
"privacy.direct.long": "Kõik postituses mainitud",
|
||||
"privacy.direct.short": "Määratud kasutajad",
|
||||
"privacy.private.long": "Ainult jälgijad",
|
||||
"privacy.private.short": "Jälgijad",
|
||||
"privacy.public.long": "Nii kasutajad kui mittekasutajad",
|
||||
|
|
|
@ -644,7 +644,6 @@
|
|||
"poll_button.remove_poll": "Kendu inkesta",
|
||||
"privacy.change": "Aldatu bidalketaren pribatutasuna",
|
||||
"privacy.direct.long": "Argitalpen honetan aipatutako denak",
|
||||
"privacy.direct.short": "Jende jakina",
|
||||
"privacy.private.long": "Soilik jarraitzaileak",
|
||||
"privacy.private.short": "Jarraitzaileak",
|
||||
"privacy.public.long": "Mastodonen dagoen edo ez dagoen edonor",
|
||||
|
|
|
@ -218,6 +218,10 @@
|
|||
"confirmations.logout.confirm": "خروج از حساب",
|
||||
"confirmations.logout.message": "مطمئنید میخواهید خارج شوید؟",
|
||||
"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.redraft.confirm": "حذف و بازنویسی",
|
||||
"confirmations.redraft.message": "مطمئنید که میخواهید این فرسته را حذف کنید و از نو بنویسید؟ با این کار تقویتها و پسندهایش از دست رفته و پاسخها به آن بیمرجع میشود.",
|
||||
|
@ -693,7 +697,7 @@
|
|||
"poll_button.remove_poll": "برداشتن نظرسنجی",
|
||||
"privacy.change": "تغییر محرمانگی فرسته",
|
||||
"privacy.direct.long": "هرکسی که در فرسته نام برده شده",
|
||||
"privacy.direct.short": "افراد مشخّص",
|
||||
"privacy.direct.short": "ذکر خصوصی",
|
||||
"privacy.private.long": "تنها پیگیرندگانتان",
|
||||
"privacy.private.short": "پیگیرندگان",
|
||||
"privacy.public.long": "هرکسی در و بیرون از ماستودون",
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
"poll_button.remove_poll": "Poista äänestys",
|
||||
"privacy.change": "Muuta julkaisun näkyvyyttä",
|
||||
"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.short": "Seuraajat",
|
||||
"privacy.public.long": "Kuka tahansa Mastodonissa ja sen ulkopuolella",
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
"poll_button.remove_poll": "Strika atkvøðugreiðslu",
|
||||
"privacy.change": "Broyt privatverju av posti",
|
||||
"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.short": "Fylgjarar",
|
||||
"privacy.public.long": "Øll í og uttanfyri Mastodon",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"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.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.",
|
||||
|
@ -697,7 +697,7 @@
|
|||
"poll_button.remove_poll": "Supprimer le sondage",
|
||||
"privacy.change": "Changer la confidentialité des messages",
|
||||
"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.short": "Abonnés",
|
||||
"privacy.public.long": "Tout le monde sur et en dehors de Mastodon",
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
"poll_button.remove_poll": "Supprimer le sondage",
|
||||
"privacy.change": "Ajuster la confidentialité du message",
|
||||
"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.short": "Abonnés",
|
||||
"privacy.public.long": "Tout le monde sur et en dehors de Mastodon",
|
||||
|
|
|
@ -682,7 +682,6 @@
|
|||
"poll_button.remove_poll": "Enkête fuortsmite",
|
||||
"privacy.change": "Sichtberheid fan berjocht oanpasse",
|
||||
"privacy.direct.long": "Elkenien dy’ yn it berjocht fermeld wurdt",
|
||||
"privacy.direct.short": "Bepaalde minsken",
|
||||
"privacy.private.long": "Allinnich jo folgers",
|
||||
"privacy.private.short": "Folgers",
|
||||
"privacy.public.long": "Elkenien op Mastodon en dêrbûten",
|
||||
|
|
|
@ -697,7 +697,7 @@
|
|||
"poll_button.remove_poll": "Bain suirbhé",
|
||||
"privacy.change": "Athraigh príobháideacht postála",
|
||||
"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.short": "Leantóirí",
|
||||
"privacy.public.long": "Duine ar bith ar agus amach Mastodon",
|
||||
|
|
|
@ -633,7 +633,6 @@
|
|||
"poll_button.remove_poll": "Thoir air falbh an cunntas-bheachd",
|
||||
"privacy.change": "Cuir gleus air prìobhaideachd a’ phuist",
|
||||
"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.short": "Luchd-leantainn",
|
||||
"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
Loading…
Reference in New Issue
Block a user