mirror of
https://github.com/mastodon/mastodon.git
synced 2025-05-11 20:21:10 +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
|
- name: Run haml-lint
|
||||||
run: |
|
run: |
|
||||||
echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json"
|
echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json"
|
||||||
bin/haml-lint --parallel --reporter github
|
bin/haml-lint --reporter github
|
||||||
|
|
|
@ -63,6 +63,7 @@ docker-compose.override.yml
|
||||||
|
|
||||||
# Ignore emoji map file
|
# Ignore emoji map file
|
||||||
/app/javascript/mastodon/features/emoji/emoji_map.json
|
/app/javascript/mastodon/features/emoji/emoji_map.json
|
||||||
|
/app/javascript/mastodon/features/emoji/emoji_sheet.json
|
||||||
|
|
||||||
# Ignore locale files
|
# Ignore locale files
|
||||||
/app/javascript/mastodon/locales/*.json
|
/app/javascript/mastodon/locales/*.json
|
||||||
|
|
|
@ -18,6 +18,7 @@ inherit_from:
|
||||||
- .rubocop/rspec_rails.yml
|
- .rubocop/rspec_rails.yml
|
||||||
- .rubocop/rspec.yml
|
- .rubocop/rspec.yml
|
||||||
- .rubocop/style.yml
|
- .rubocop/style.yml
|
||||||
|
- .rubocop/i18n.yml
|
||||||
- .rubocop/custom.yml
|
- .rubocop/custom.yml
|
||||||
- .rubocop_todo.yml
|
- .rubocop_todo.yml
|
||||||
- .rubocop/strict.yml
|
- .rubocop/strict.yml
|
||||||
|
@ -30,6 +31,7 @@ plugins:
|
||||||
- rubocop-rails
|
- rubocop-rails
|
||||||
- rubocop-rspec
|
- rubocop-rspec
|
||||||
- rubocop-performance
|
- rubocop-performance
|
||||||
|
- rubocop-i18n
|
||||||
|
|
||||||
require:
|
require:
|
||||||
- rubocop-rspec_rails
|
- rubocop-rspec_rails
|
||||||
|
|
12
.rubocop/i18n.yml
Normal file
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
|
# This configuration was generated by
|
||||||
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
|
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
|
||||||
# using RuboCop version 1.72.2.
|
# using RuboCop version 1.73.1.
|
||||||
# The point is for the user to remove these configuration records
|
# The point is for the user to remove these configuration records
|
||||||
# one by one as the offenses are removed from the code base.
|
# one by one as the offenses are removed from the code base.
|
||||||
# Note that changes in the inspected code, or installation of new
|
# Note that changes in the inspected code, or installation of new
|
||||||
|
|
35
CHANGELOG.md
35
CHANGELOG.md
|
@ -2,6 +2,41 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [4.3.4] - 2025-02-27
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Update dependencies
|
||||||
|
- Change HTML sanitization to remove unusable and unused `embed` tag (#34021 by @ClearlyClaire, [GHSA-mq2m-hr29-8gqf](https://github.com/mastodon/mastodon/security/advisories/GHSA-mq2m-hr29-8gqf))
|
||||||
|
- Fix rate-limit on sign-up email verification ([GHSA-v39f-c9jj-8w7h](https://github.com/mastodon/mastodon/security/advisories/GHSA-v39f-c9jj-8w7h))
|
||||||
|
- Fix improper disclosure of domain blocks to unverified users ([GHSA-94h4-fj37-c825](https://github.com/mastodon/mastodon/security/advisories/GHSA-94h4-fj37-c825))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change preview cards to be shown when Content Warnings are expanded (#33827 by @ClearlyClaire)
|
||||||
|
- Change warnings against changing encryption secrets to be even more noticeable (#33631 by @ClearlyClaire)
|
||||||
|
- Change `mastodon:setup` to prevent overwriting already-configured servers (#33603, #33616, and #33684 by @ClearlyClaire and @mjankowski)
|
||||||
|
- Change notifications from moderators to not be filtered (#32974 and #33654 by @ClearlyClaire and @mjankowski)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix `GET /api/v2/notifications/:id` and `POST /api/v2/notifications/:id/dismiss` for ungrouped notifications (#33990 by @ClearlyClaire)
|
||||||
|
- Fix issue with some versions of libvips on some systems (#33853 by @kleisauke)
|
||||||
|
- Fix handling of duplicate mentions in incoming status `Update` (#33911 by @ClearlyClaire)
|
||||||
|
- Fix inefficiencies in timeline generation (#33839 and #33842 by @ClearlyClaire)
|
||||||
|
- Fix emoji rewrite adding unnecessary curft to the DOM for most emoji (#33818 by @ClearlyClaire)
|
||||||
|
- Fix `tootctl feeds build` not building list timelines (#33783 by @ClearlyClaire)
|
||||||
|
- Fix flaky test in `/api/v2/notifications` tests (#33773 by @ClearlyClaire)
|
||||||
|
- Fix incorrect signature after HTTP redirect (#33757 and #33769 by @ClearlyClaire)
|
||||||
|
- Fix polls not being validated on edition (#33755 by @ClearlyClaire)
|
||||||
|
- Fix media preview height in compose form when 3 or more images are attached (#33571 by @ClearlyClaire)
|
||||||
|
- Fix preview card sizing in “Author attribution” in profile settings (#33482 by @ClearlyClaire)
|
||||||
|
- Fix processing of incoming notifications for unfilterable types (#33429 by @ClearlyClaire)
|
||||||
|
- Fix featured tags for remote accounts not being kept up to date (#33372, #33406, and #33425 by @ClearlyClaire and @mjankowski)
|
||||||
|
- Fix notification polling showing a loading bar in web UI (#32960 by @Gargron)
|
||||||
|
- Fix accounts table long display name (#29316 by @WebCoder49)
|
||||||
|
- Fix exclusive lists interfering with notifications (#28162 by @ShadowJonathan)
|
||||||
|
|
||||||
## [4.3.3] - 2025-01-16
|
## [4.3.3] - 2025-01-16
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
|
@ -96,6 +96,9 @@ RUN \
|
||||||
# Set /opt/mastodon as working directory
|
# Set /opt/mastodon as working directory
|
||||||
WORKDIR /opt/mastodon
|
WORKDIR /opt/mastodon
|
||||||
|
|
||||||
|
# Add backport repository for some specific packages where we need the latest version
|
||||||
|
RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list
|
||||||
|
|
||||||
# hadolint ignore=DL3008,DL3005
|
# hadolint ignore=DL3008,DL3005
|
||||||
RUN \
|
RUN \
|
||||||
# Mount Apt cache and lib directories from Docker buildx caches
|
# Mount Apt cache and lib directories from Docker buildx caches
|
||||||
|
@ -165,7 +168,7 @@ RUN \
|
||||||
libexif-dev \
|
libexif-dev \
|
||||||
libexpat1-dev \
|
libexpat1-dev \
|
||||||
libgirepository1.0-dev \
|
libgirepository1.0-dev \
|
||||||
libheif-dev \
|
libheif-dev/bookworm-backports \
|
||||||
libimagequant-dev \
|
libimagequant-dev \
|
||||||
libjpeg62-turbo-dev \
|
libjpeg62-turbo-dev \
|
||||||
liblcms2-dev \
|
liblcms2-dev \
|
||||||
|
@ -348,7 +351,7 @@ RUN \
|
||||||
# libvips components
|
# libvips components
|
||||||
libcgif0 \
|
libcgif0 \
|
||||||
libexif12 \
|
libexif12 \
|
||||||
libheif1 \
|
libheif1/bookworm-backports \
|
||||||
libimagequant0 \
|
libimagequant0 \
|
||||||
libjpeg62-turbo \
|
libjpeg62-turbo \
|
||||||
liblcms2-2 \
|
liblcms2-2 \
|
||||||
|
|
6
Gemfile
6
Gemfile
|
@ -39,7 +39,7 @@ gem 'net-ldap', '~> 0.18'
|
||||||
|
|
||||||
gem 'omniauth', '~> 2.0'
|
gem 'omniauth', '~> 2.0'
|
||||||
gem 'omniauth-cas', '~> 3.0.0.beta.1'
|
gem 'omniauth-cas', '~> 3.0.0.beta.1'
|
||||||
gem 'omniauth_openid_connect', '~> 0.6.1'
|
gem 'omniauth_openid_connect', '~> 0.8.0'
|
||||||
gem 'omniauth-rails_csrf_protection', '~> 1.0'
|
gem 'omniauth-rails_csrf_protection', '~> 1.0'
|
||||||
gem 'omniauth-saml', '~> 2.0'
|
gem 'omniauth-saml', '~> 2.0'
|
||||||
|
|
||||||
|
@ -145,9 +145,6 @@ group :test do
|
||||||
# Used to mock environment variables
|
# Used to mock environment variables
|
||||||
gem 'climate_control'
|
gem 'climate_control'
|
||||||
|
|
||||||
# Add back helpers functions removed in Rails 5.1
|
|
||||||
gem 'rails-controller-testing', '~> 1.0'
|
|
||||||
|
|
||||||
# Validate schemas in specs
|
# Validate schemas in specs
|
||||||
gem 'json-schema', '~> 5.0'
|
gem 'json-schema', '~> 5.0'
|
||||||
|
|
||||||
|
@ -168,6 +165,7 @@ group :development do
|
||||||
# Code linting CLI and plugins
|
# Code linting CLI and plugins
|
||||||
gem 'rubocop', require: false
|
gem 'rubocop', require: false
|
||||||
gem 'rubocop-capybara', require: false
|
gem 'rubocop-capybara', require: false
|
||||||
|
gem 'rubocop-i18n', require: false
|
||||||
gem 'rubocop-performance', require: false
|
gem 'rubocop-performance', require: false
|
||||||
gem 'rubocop-rails', require: false
|
gem 'rubocop-rails', require: false
|
||||||
gem 'rubocop-rspec', require: false
|
gem 'rubocop-rspec', require: false
|
||||||
|
|
99
Gemfile.lock
99
Gemfile.lock
|
@ -194,7 +194,7 @@ GEM
|
||||||
devise_pam_authenticatable2 (9.2.0)
|
devise_pam_authenticatable2 (9.2.0)
|
||||||
devise (>= 4.0.0)
|
devise (>= 4.0.0)
|
||||||
rpam2 (~> 4.0)
|
rpam2 (~> 4.0)
|
||||||
diff-lcs (1.5.1)
|
diff-lcs (1.6.0)
|
||||||
discard (1.4.0)
|
discard (1.4.0)
|
||||||
activerecord (>= 4.2, < 9.0)
|
activerecord (>= 4.2, < 9.0)
|
||||||
docile (1.4.1)
|
docile (1.4.1)
|
||||||
|
@ -217,6 +217,8 @@ GEM
|
||||||
htmlentities (~> 4.3.3)
|
htmlentities (~> 4.3.3)
|
||||||
launchy (>= 2.1, < 4.0)
|
launchy (>= 2.1, < 4.0)
|
||||||
mail (~> 2.7)
|
mail (~> 2.7)
|
||||||
|
email_validator (2.2.4)
|
||||||
|
activemodel
|
||||||
erubi (1.13.1)
|
erubi (1.13.1)
|
||||||
et-orbi (1.2.11)
|
et-orbi (1.2.11)
|
||||||
tzinfo
|
tzinfo
|
||||||
|
@ -228,6 +230,8 @@ GEM
|
||||||
faraday-net_http (>= 2.0, < 3.5)
|
faraday-net_http (>= 2.0, < 3.5)
|
||||||
json
|
json
|
||||||
logger
|
logger
|
||||||
|
faraday-follow_redirects (0.3.0)
|
||||||
|
faraday (>= 1, < 3)
|
||||||
faraday-httpclient (2.0.1)
|
faraday-httpclient (2.0.1)
|
||||||
httpclient (>= 2.2)
|
httpclient (>= 2.2)
|
||||||
faraday-net_http (3.4.0)
|
faraday-net_http (3.4.0)
|
||||||
|
@ -273,7 +277,7 @@ GEM
|
||||||
activesupport (>= 5.1)
|
activesupport (>= 5.1)
|
||||||
haml (>= 4.0.6)
|
haml (>= 4.0.6)
|
||||||
railties (>= 5.1)
|
railties (>= 5.1)
|
||||||
haml_lint (0.60.0)
|
haml_lint (0.61.0)
|
||||||
haml (>= 5.0)
|
haml (>= 5.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
rainbow
|
rainbow
|
||||||
|
@ -330,11 +334,13 @@ GEM
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.10.1)
|
json (2.10.1)
|
||||||
json-canonicalization (1.0.0)
|
json-canonicalization (1.0.0)
|
||||||
json-jwt (1.15.3.1)
|
json-jwt (1.16.7)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
aes_key_wrap
|
aes_key_wrap
|
||||||
|
base64
|
||||||
bindata
|
bindata
|
||||||
httpclient
|
faraday (~> 2.0)
|
||||||
|
faraday-follow_redirects
|
||||||
json-ld (3.3.2)
|
json-ld (3.3.2)
|
||||||
htmlentities (~> 4.3)
|
htmlentities (~> 4.3)
|
||||||
json-canonicalization (~> 1.0)
|
json-canonicalization (~> 1.0)
|
||||||
|
@ -409,11 +415,11 @@ GEM
|
||||||
mime-types (3.6.0)
|
mime-types (3.6.0)
|
||||||
logger
|
logger
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2025.0204)
|
mime-types-data (3.2025.0220)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.8)
|
mini_portile2 (2.8.8)
|
||||||
minitest (5.25.4)
|
minitest (5.25.4)
|
||||||
msgpack (1.7.5)
|
msgpack (1.8.0)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
mutex_m (0.3.0)
|
mutex_m (0.3.0)
|
||||||
net-http (0.6.0)
|
net-http (0.6.0)
|
||||||
|
@ -432,37 +438,39 @@ GEM
|
||||||
nokogiri (1.18.3)
|
nokogiri (1.18.3)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oj (3.16.9)
|
oj (3.16.10)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
ostruct (>= 0.2)
|
ostruct (>= 0.2)
|
||||||
omniauth (2.1.2)
|
omniauth (2.1.3)
|
||||||
hashie (>= 3.4.6)
|
hashie (>= 3.4.6)
|
||||||
rack (>= 2.2.3)
|
rack (>= 2.2.3)
|
||||||
rack-protection
|
rack-protection
|
||||||
omniauth-cas (3.0.0)
|
omniauth-cas (3.0.1)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
nokogiri (~> 1.12)
|
nokogiri (~> 1.12)
|
||||||
omniauth (~> 2.1)
|
omniauth (~> 2.1)
|
||||||
omniauth-rails_csrf_protection (1.0.2)
|
omniauth-rails_csrf_protection (1.0.2)
|
||||||
actionpack (>= 4.2)
|
actionpack (>= 4.2)
|
||||||
omniauth (~> 2.0)
|
omniauth (~> 2.0)
|
||||||
omniauth-saml (2.2.1)
|
omniauth-saml (2.2.2)
|
||||||
omniauth (~> 2.1)
|
omniauth (~> 2.1)
|
||||||
ruby-saml (~> 1.17)
|
ruby-saml (~> 1.17)
|
||||||
omniauth_openid_connect (0.6.1)
|
omniauth_openid_connect (0.8.0)
|
||||||
omniauth (>= 1.9, < 3)
|
omniauth (>= 1.9, < 3)
|
||||||
openid_connect (~> 1.1)
|
openid_connect (~> 2.2)
|
||||||
openid_connect (1.4.2)
|
openid_connect (2.3.1)
|
||||||
activemodel
|
activemodel
|
||||||
attr_required (>= 1.0.0)
|
attr_required (>= 1.0.0)
|
||||||
json-jwt (>= 1.15.0)
|
email_validator
|
||||||
net-smtp
|
faraday (~> 2.0)
|
||||||
rack-oauth2 (~> 1.21)
|
faraday-follow_redirects
|
||||||
swd (~> 1.3)
|
json-jwt (>= 1.16)
|
||||||
|
mail
|
||||||
|
rack-oauth2 (~> 2.2)
|
||||||
|
swd (~> 2.0)
|
||||||
tzinfo
|
tzinfo
|
||||||
validate_email
|
|
||||||
validate_url
|
validate_url
|
||||||
webfinger (~> 1.2)
|
webfinger (~> 2.0)
|
||||||
openssl (3.3.0)
|
openssl (3.3.0)
|
||||||
openssl-signature_algorithm (1.3.0)
|
openssl-signature_algorithm (1.3.0)
|
||||||
openssl (> 2.0)
|
openssl (> 2.0)
|
||||||
|
@ -600,19 +608,20 @@ GEM
|
||||||
public_suffix (6.0.1)
|
public_suffix (6.0.1)
|
||||||
puma (6.6.0)
|
puma (6.6.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.4.0)
|
pundit (2.5.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (2.2.11)
|
rack (2.2.12)
|
||||||
rack-attack (6.7.0)
|
rack-attack (6.7.0)
|
||||||
rack (>= 1.0, < 4)
|
rack (>= 1.0, < 4)
|
||||||
rack-cors (2.0.2)
|
rack-cors (2.0.2)
|
||||||
rack (>= 2.0.0)
|
rack (>= 2.0.0)
|
||||||
rack-oauth2 (1.21.3)
|
rack-oauth2 (2.2.1)
|
||||||
activesupport
|
activesupport
|
||||||
attr_required
|
attr_required
|
||||||
httpclient
|
faraday (~> 2.0)
|
||||||
|
faraday-follow_redirects
|
||||||
json-jwt (>= 1.11.0)
|
json-jwt (>= 1.11.0)
|
||||||
rack (>= 2.1.0)
|
rack (>= 2.1.0)
|
||||||
rack-protection (3.2.0)
|
rack-protection (3.2.0)
|
||||||
|
@ -641,10 +650,6 @@ GEM
|
||||||
activesupport (= 8.0.1)
|
activesupport (= 8.0.1)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 8.0.1)
|
railties (= 8.0.1)
|
||||||
rails-controller-testing (1.0.5)
|
|
||||||
actionpack (>= 5.0.1.rc1)
|
|
||||||
actionview (>= 5.0.1.rc1)
|
|
||||||
activesupport (>= 5.0.1.rc1)
|
|
||||||
rails-dom-testing (2.2.0)
|
rails-dom-testing (2.2.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
|
@ -673,7 +678,7 @@ GEM
|
||||||
rdf (~> 3.3)
|
rdf (~> 3.3)
|
||||||
rdoc (6.12.0)
|
rdoc (6.12.0)
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
redcarpet (3.6.0)
|
redcarpet (3.6.1)
|
||||||
redis (4.8.1)
|
redis (4.8.1)
|
||||||
redis-namespace (1.11.0)
|
redis-namespace (1.11.0)
|
||||||
redis (>= 4)
|
redis (>= 4)
|
||||||
|
@ -687,7 +692,7 @@ GEM
|
||||||
responders (3.1.1)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml (3.4.0)
|
rexml (3.4.1)
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rouge (4.5.1)
|
rouge (4.5.1)
|
||||||
rpam2 (4.0.2)
|
rpam2 (4.0.2)
|
||||||
|
@ -723,7 +728,7 @@ GEM
|
||||||
rspec-mocks (~> 3.0)
|
rspec-mocks (~> 3.0)
|
||||||
sidekiq (>= 5, < 8)
|
sidekiq (>= 5, < 8)
|
||||||
rspec-support (3.13.2)
|
rspec-support (3.13.2)
|
||||||
rubocop (1.72.2)
|
rubocop (1.73.2)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (~> 3.17.0.2)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
lint_roller (~> 1.1.0)
|
lint_roller (~> 1.1.0)
|
||||||
|
@ -734,15 +739,18 @@ GEM
|
||||||
rubocop-ast (>= 1.38.0, < 2.0)
|
rubocop-ast (>= 1.38.0, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 4.0)
|
unicode-display_width (>= 2.4.0, < 4.0)
|
||||||
rubocop-ast (1.38.0)
|
rubocop-ast (1.38.1)
|
||||||
parser (>= 3.3.1.0)
|
parser (>= 3.3.1.0)
|
||||||
rubocop-capybara (2.21.0)
|
rubocop-capybara (2.21.0)
|
||||||
rubocop (~> 1.41)
|
rubocop (~> 1.41)
|
||||||
|
rubocop-i18n (3.2.3)
|
||||||
|
lint_roller (~> 1.1)
|
||||||
|
rubocop (>= 1.72.1)
|
||||||
rubocop-performance (1.24.0)
|
rubocop-performance (1.24.0)
|
||||||
lint_roller (~> 1.1)
|
lint_roller (~> 1.1)
|
||||||
rubocop (>= 1.72.1, < 2.0)
|
rubocop (>= 1.72.1, < 2.0)
|
||||||
rubocop-ast (>= 1.38.0, < 2.0)
|
rubocop-ast (>= 1.38.0, < 2.0)
|
||||||
rubocop-rails (2.30.1)
|
rubocop-rails (2.30.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
lint_roller (~> 1.1)
|
lint_roller (~> 1.1)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
|
@ -774,7 +782,7 @@ GEM
|
||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
railties (>= 4.0.0)
|
railties (>= 4.0.0)
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
selenium-webdriver (4.28.0)
|
selenium-webdriver (4.29.1)
|
||||||
base64 (~> 0.2)
|
base64 (~> 0.2)
|
||||||
logger (~> 1.4)
|
logger (~> 1.4)
|
||||||
rexml (~> 3.2, >= 3.2.5)
|
rexml (~> 3.2, >= 3.2.5)
|
||||||
|
@ -814,13 +822,14 @@ GEM
|
||||||
stackprof (0.2.27)
|
stackprof (0.2.27)
|
||||||
stoplight (4.1.1)
|
stoplight (4.1.1)
|
||||||
redlock (~> 1.0)
|
redlock (~> 1.0)
|
||||||
stringio (3.1.2)
|
stringio (3.1.4)
|
||||||
strong_migrations (2.2.0)
|
strong_migrations (2.2.0)
|
||||||
activerecord (>= 7)
|
activerecord (>= 7)
|
||||||
swd (1.3.0)
|
swd (2.0.3)
|
||||||
activesupport (>= 3)
|
activesupport (>= 3)
|
||||||
attr_required (>= 0.0.5)
|
attr_required (>= 0.0.5)
|
||||||
httpclient (>= 2.4)
|
faraday (~> 2.0)
|
||||||
|
faraday-follow_redirects
|
||||||
sysexits (1.2.0)
|
sysexits (1.2.0)
|
||||||
temple (0.10.3)
|
temple (0.10.3)
|
||||||
terminal-table (4.0.0)
|
terminal-table (4.0.0)
|
||||||
|
@ -858,11 +867,8 @@ GEM
|
||||||
unicode-display_width (3.1.4)
|
unicode-display_width (3.1.4)
|
||||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||||
unicode-emoji (4.0.4)
|
unicode-emoji (4.0.4)
|
||||||
uri (1.0.2)
|
uri (1.0.3)
|
||||||
useragent (0.16.11)
|
useragent (0.16.11)
|
||||||
validate_email (0.1.6)
|
|
||||||
activemodel (>= 3.0)
|
|
||||||
mail (>= 2.2.5)
|
|
||||||
validate_url (1.0.15)
|
validate_url (1.0.15)
|
||||||
activemodel (>= 3.0.0)
|
activemodel (>= 3.0.0)
|
||||||
public_suffix
|
public_suffix
|
||||||
|
@ -876,9 +882,10 @@ GEM
|
||||||
openssl (>= 2.2)
|
openssl (>= 2.2)
|
||||||
safety_net_attestation (~> 0.4.0)
|
safety_net_attestation (~> 0.4.0)
|
||||||
tpm-key_attestation (~> 0.14.0)
|
tpm-key_attestation (~> 0.14.0)
|
||||||
webfinger (1.2.0)
|
webfinger (2.1.3)
|
||||||
activesupport
|
activesupport
|
||||||
httpclient (>= 2.4)
|
faraday (~> 2.0)
|
||||||
|
faraday-follow_redirects
|
||||||
webmock (3.25.0)
|
webmock (3.25.0)
|
||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
|
@ -898,7 +905,7 @@ GEM
|
||||||
xorcist (1.1.3)
|
xorcist (1.1.3)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.7.1)
|
zeitwerk (2.7.2)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
@ -977,7 +984,7 @@ DEPENDENCIES
|
||||||
omniauth-cas (~> 3.0.0.beta.1)
|
omniauth-cas (~> 3.0.0.beta.1)
|
||||||
omniauth-rails_csrf_protection (~> 1.0)
|
omniauth-rails_csrf_protection (~> 1.0)
|
||||||
omniauth-saml (~> 2.0)
|
omniauth-saml (~> 2.0)
|
||||||
omniauth_openid_connect (~> 0.6.1)
|
omniauth_openid_connect (~> 0.8.0)
|
||||||
opentelemetry-api (~> 1.4.0)
|
opentelemetry-api (~> 1.4.0)
|
||||||
opentelemetry-exporter-otlp (~> 0.29.0)
|
opentelemetry-exporter-otlp (~> 0.29.0)
|
||||||
opentelemetry-instrumentation-active_job (~> 0.8.0)
|
opentelemetry-instrumentation-active_job (~> 0.8.0)
|
||||||
|
@ -1009,7 +1016,6 @@ DEPENDENCIES
|
||||||
rack-cors (~> 2.0)
|
rack-cors (~> 2.0)
|
||||||
rack-test (~> 2.1)
|
rack-test (~> 2.1)
|
||||||
rails (~> 8.0)
|
rails (~> 8.0)
|
||||||
rails-controller-testing (~> 1.0)
|
|
||||||
rails-i18n (~> 8.0)
|
rails-i18n (~> 8.0)
|
||||||
rdf-normalize (~> 0.5)
|
rdf-normalize (~> 0.5)
|
||||||
redcarpet (~> 3.6)
|
redcarpet (~> 3.6)
|
||||||
|
@ -1021,6 +1027,7 @@ DEPENDENCIES
|
||||||
rspec-sidekiq (~> 5.0)
|
rspec-sidekiq (~> 5.0)
|
||||||
rubocop
|
rubocop
|
||||||
rubocop-capybara
|
rubocop-capybara
|
||||||
|
rubocop-i18n
|
||||||
rubocop-performance
|
rubocop-performance
|
||||||
rubocop-rails
|
rubocop-rails
|
||||||
rubocop-rspec
|
rubocop-rspec
|
||||||
|
@ -1059,4 +1066,4 @@ RUBY VERSION
|
||||||
ruby 3.4.1p0
|
ruby 3.4.1p0
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.6.3
|
2.6.5
|
||||||
|
|
|
@ -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
|
private
|
||||||
|
|
||||||
def set_terms_of_service
|
def set_terms_of_service
|
||||||
@terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text)
|
@terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text, effective_date: 10.days.from_now)
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_terms_of_service
|
def current_terms_of_service
|
||||||
|
@ -32,6 +32,6 @@ class Admin::TermsOfService::DraftsController < Admin::BaseController
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params
|
params
|
||||||
.expect(terms_of_service: [:text, :changelog])
|
.expect(terms_of_service: [:text, :changelog, :effective_date])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,6 @@
|
||||||
class Admin::TermsOfServiceController < Admin::BaseController
|
class Admin::TermsOfServiceController < Admin::BaseController
|
||||||
def index
|
def index
|
||||||
authorize :terms_of_service, :index?
|
authorize :terms_of_service, :index?
|
||||||
@terms_of_service = TermsOfService.live.first
|
@terms_of_service = TermsOfService.published.first
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,7 +31,7 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr
|
||||||
end
|
end
|
||||||
|
|
||||||
def show_domain_blocks_to_user?
|
def show_domain_blocks_to_user?
|
||||||
Setting.show_domain_blocks == 'users' && user_signed_in?
|
Setting.show_domain_blocks == 'users' && user_signed_in? && current_user.functional_or_moved?
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_domain_blocks
|
def set_domain_blocks
|
||||||
|
@ -47,6 +47,6 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr
|
||||||
end
|
end
|
||||||
|
|
||||||
def show_rationale_for_user?
|
def show_rationale_for_user?
|
||||||
Setting.show_domain_blocks_rationale == 'users' && user_signed_in?
|
Setting.show_domain_blocks_rationale == 'users' && user_signed_in? && current_user.functional_or_moved?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,12 +5,18 @@ class Api::V1::Instances::TermsOfServicesController < Api::V1::Instances::BaseCo
|
||||||
|
|
||||||
def show
|
def show
|
||||||
cache_even_if_authenticated!
|
cache_even_if_authenticated!
|
||||||
render json: @terms_of_service, serializer: REST::PrivacyPolicySerializer
|
render json: @terms_of_service, serializer: REST::TermsOfServiceSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_terms_of_service
|
def set_terms_of_service
|
||||||
@terms_of_service = TermsOfService.live.first!
|
@terms_of_service = begin
|
||||||
|
if params[:date].present?
|
||||||
|
TermsOfService.published.find_by!(effective_date: params[:date])
|
||||||
|
else
|
||||||
|
TermsOfService.live.first || TermsOfService.published.first! # For the case when none of the published terms have become effective yet
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
class Api::V1::MediaController < Api::BaseController
|
class Api::V1::MediaController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:media' }
|
before_action -> { doorkeeper_authorize! :write, :'write:media' }
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
before_action :set_media_attachment, except: [:create]
|
before_action :set_media_attachment, except: [:create, :destroy]
|
||||||
before_action :check_processing, except: [:create]
|
before_action :check_processing, except: [:create, :destroy]
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
|
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
|
||||||
|
@ -25,6 +25,15 @@ class Api::V1::MediaController < Api::BaseController
|
||||||
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
|
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@media_attachment = current_account.media_attachments.find(params[:id])
|
||||||
|
|
||||||
|
return render json: in_usage_error, status: 422 unless @media_attachment.status_id.nil?
|
||||||
|
|
||||||
|
@media_attachment.destroy
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def status_code_for_media_attachment
|
def status_code_for_media_attachment
|
||||||
|
@ -54,4 +63,8 @@ class Api::V1::MediaController < Api::BaseController
|
||||||
def processing_error
|
def processing_error
|
||||||
{ error: 'Error processing thumbnail for uploaded media' }
|
{ error: 'Error processing thumbnail for uploaded media' }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def in_usage_error
|
||||||
|
{ error: 'Media attachment is currently used by a status' }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -111,7 +111,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
@status.account.statuses_count = @status.account.statuses_count - 1
|
@status.account.statuses_count = @status.account.statuses_count - 1
|
||||||
json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
|
json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
|
||||||
|
|
||||||
RemovalWorker.perform_async(@status.id, { 'redraft' => true })
|
RemovalWorker.perform_async(@status.id, { 'redraft' => !truthy_param?(:delete_media) })
|
||||||
|
|
||||||
render json: json
|
render json: json
|
||||||
end
|
end
|
||||||
|
|
|
@ -46,7 +46,7 @@ class Api::V2::NotificationsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@notification = current_account.notifications.without_suspended.find_by!(group_key: params[:group_key])
|
@notification = current_account.notifications.without_suspended.by_group_key(params[:group_key]).take!
|
||||||
presenter = GroupedNotificationsPresenter.new(NotificationGroup.from_notifications([@notification]))
|
presenter = GroupedNotificationsPresenter.new(NotificationGroup.from_notifications([@notification]))
|
||||||
render json: presenter, serializer: REST::DedupNotificationGroupSerializer
|
render json: presenter, serializer: REST::DedupNotificationGroupSerializer
|
||||||
end
|
end
|
||||||
|
@ -57,7 +57,7 @@ class Api::V2::NotificationsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def dismiss
|
def dismiss
|
||||||
current_account.notifications.where(group_key: params[:group_key]).destroy_all
|
current_account.notifications.by_group_key(params[:group_key]).destroy_all
|
||||||
render_empty
|
render_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import './public-path';
|
import './public-path';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
import { afterInitialRender } from 'mastodon/../hooks/useRenderSignal';
|
import { afterInitialRender } from 'mastodon/hooks/useRenderSignal';
|
||||||
|
|
||||||
import { start } from '../mastodon/common';
|
import { start } from '../mastodon/common';
|
||||||
import { Status } from '../mastodon/features/standalone/status';
|
import { Status } from '../mastodon/features/standalone/status';
|
||||||
|
|
|
@ -142,6 +142,13 @@ export function fetchAccountFail(id, error) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {boolean} [options.reblogs]
|
||||||
|
* @param {boolean} [options.notify]
|
||||||
|
* @returns {function(): void}
|
||||||
|
*/
|
||||||
export function followAccount(id, options = { reblogs: true }) {
|
export function followAccount(id, options = { reblogs: true }) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
||||||
|
|
|
@ -29,7 +29,7 @@ const debouncedSave = debounce((dispatch, getState) => {
|
||||||
api().put('/api/web/settings', { data })
|
api().put('/api/web/settings', { data })
|
||||||
.then(() => dispatch({ type: SETTING_SAVE }))
|
.then(() => dispatch({ type: SETTING_SAVE }))
|
||||||
.catch(error => dispatch(showAlertForError(error)));
|
.catch(error => dispatch(showAlertForError(error)));
|
||||||
}, 5000, { trailing: true });
|
}, 2000, { leading: true, trailing: true });
|
||||||
|
|
||||||
export function saveSettings() {
|
export function saveSettings() {
|
||||||
return (dispatch, getState) => debouncedSave(dispatch, getState);
|
return (dispatch, getState) => debouncedSave(dispatch, getState);
|
||||||
|
|
|
@ -138,7 +138,7 @@ export function deleteStatus(id, withRedraft = false) {
|
||||||
|
|
||||||
dispatch(deleteStatusRequest(id));
|
dispatch(deleteStatusRequest(id));
|
||||||
|
|
||||||
api().delete(`/api/v1/statuses/${id}`).then(response => {
|
api().delete(`/api/v1/statuses/${id}`, { params: { delete_media: !withRedraft } }).then(response => {
|
||||||
dispatch(deleteStatusSuccess(id));
|
dispatch(deleteStatusSuccess(id));
|
||||||
dispatch(deleteFromTimelines(id));
|
dispatch(deleteFromTimelines(id));
|
||||||
dispatch(importFetchedAccount(response.data.account));
|
dispatch(importFetchedAccount(response.data.account));
|
||||||
|
|
|
@ -4,8 +4,12 @@ import type {
|
||||||
ApiPrivacyPolicyJSON,
|
ApiPrivacyPolicyJSON,
|
||||||
} from 'mastodon/api_types/instance';
|
} from 'mastodon/api_types/instance';
|
||||||
|
|
||||||
export const apiGetTermsOfService = () =>
|
export const apiGetTermsOfService = (version?: string) =>
|
||||||
apiRequestGet<ApiTermsOfServiceJSON>('v1/instance/terms_of_service');
|
apiRequestGet<ApiTermsOfServiceJSON>(
|
||||||
|
version
|
||||||
|
? `v1/instance/terms_of_service/${version}`
|
||||||
|
: 'v1/instance/terms_of_service',
|
||||||
|
);
|
||||||
|
|
||||||
export const apiGetPrivacyPolicy = () =>
|
export const apiGetPrivacyPolicy = () =>
|
||||||
apiRequestGet<ApiPrivacyPolicyJSON>('v1/instance/privacy_policy');
|
apiRequestGet<ApiPrivacyPolicyJSON>('v1/instance/privacy_policy');
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
export interface ApiTermsOfServiceJSON {
|
export interface ApiTermsOfServiceJSON {
|
||||||
updated_at: string;
|
effective_date: string;
|
||||||
|
effective: boolean;
|
||||||
|
succeeded_by: string | null;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useLinks } from 'mastodon/../hooks/useLinks';
|
import { useLinks } from 'mastodon/hooks/useLinks';
|
||||||
|
|
||||||
export const AccountBio: React.FC<{
|
export const AccountBio: React.FC<{
|
||||||
note: string;
|
note: string;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||||
import { useLinks } from 'mastodon/../hooks/useLinks';
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import { useLinks } from 'mastodon/hooks/useLinks';
|
||||||
import type { Account } from 'mastodon/models/account';
|
import type { Account } from 'mastodon/models/account';
|
||||||
|
|
||||||
export const AccountFields: React.FC<{
|
export const AccountFields: React.FC<{
|
||||||
|
|
|
@ -8,7 +8,7 @@ import type {
|
||||||
UsePopperOptions,
|
UsePopperOptions,
|
||||||
} from 'react-overlays/esm/usePopper';
|
} from 'react-overlays/esm/usePopper';
|
||||||
|
|
||||||
import { useSelectableClick } from '@/hooks/useSelectableClick';
|
import { useSelectableClick } from 'mastodon/hooks/useSelectableClick';
|
||||||
|
|
||||||
const offset = [0, 4] as OffsetValue;
|
const offset = [0, 4] as OffsetValue;
|
||||||
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { useHovering } from 'mastodon/../hooks/useHovering';
|
import { useHovering } from 'mastodon/hooks/useHovering';
|
||||||
import { autoPlayGif } from 'mastodon/initial_state';
|
import { autoPlayGif } from 'mastodon/initial_state';
|
||||||
import type { Account } from 'mastodon/models/account';
|
import type { Account } from 'mastodon/models/account';
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
|
import { useHovering } from 'mastodon/hooks/useHovering';
|
||||||
|
import { autoPlayGif } from 'mastodon/initial_state';
|
||||||
import type { Account } from 'mastodon/models/account';
|
import type { Account } from 'mastodon/models/account';
|
||||||
|
|
||||||
import { useHovering } from '../../hooks/useHovering';
|
|
||||||
import { autoPlayGif } from '../initial_state';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
||||||
friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { FormattedMessage } from 'react-intl';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
|
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
|
||||||
import { useTimeout } from 'mastodon/../hooks/useTimeout';
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import { useTimeout } from 'mastodon/hooks/useTimeout';
|
||||||
|
|
||||||
export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
|
export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
|
@ -55,7 +55,7 @@ export const FollowButton: React.FC<{
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!relationship) return;
|
if (!relationship || !accountId) return;
|
||||||
|
|
||||||
if (accountId === me) {
|
if (accountId === me) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useHovering } from '@/hooks/useHovering';
|
import { useHovering } from 'mastodon/hooks/useHovering';
|
||||||
import { autoPlayGif } from 'mastodon/initial_state';
|
import { autoPlayGif } from 'mastodon/initial_state';
|
||||||
|
|
||||||
export const GIF: React.FC<{
|
export const GIF: React.FC<{
|
||||||
|
|
|
@ -151,7 +151,7 @@ export const Hashtag: React.FC<HashtagProps> = ({
|
||||||
<Sparklines
|
<Sparklines
|
||||||
width={50}
|
width={50}
|
||||||
height={28}
|
height={28}
|
||||||
data={history ? history : Array.from(Array(7)).map(() => 0)}
|
data={history ?? Array.from(Array(7)).map(() => 0)}
|
||||||
>
|
>
|
||||||
<SparklinesCurve style={{ fill: 'none' }} />
|
<SparklinesCurve style={{ fill: 'none' }} />
|
||||||
</Sparklines>
|
</Sparklines>
|
||||||
|
|
|
@ -8,8 +8,8 @@ import type {
|
||||||
UsePopperOptions,
|
UsePopperOptions,
|
||||||
} from 'react-overlays/esm/usePopper';
|
} from 'react-overlays/esm/usePopper';
|
||||||
|
|
||||||
import { useTimeout } from 'mastodon/../hooks/useTimeout';
|
|
||||||
import { HoverCardAccount } from 'mastodon/components/hover_card_account';
|
import { HoverCardAccount } from 'mastodon/components/hover_card_account';
|
||||||
|
import { useTimeout } from 'mastodon/hooks/useTimeout';
|
||||||
|
|
||||||
const offset = [-12, 4] as OffsetValue;
|
const offset = [-12, 4] as OffsetValue;
|
||||||
const enterDelay = 750;
|
const enterDelay = 750;
|
||||||
|
|
|
@ -149,6 +149,7 @@ export class IconButton extends PureComponent<Props, States> {
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
onMouseDown={this.handleMouseDown}
|
onMouseDown={this.handleMouseDown}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||||
onKeyPress={this.handleKeyPress}
|
onKeyPress={this.handleKeyPress}
|
||||||
style={style}
|
style={style}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
|
|
|
@ -81,6 +81,7 @@ class ScrollableList extends PureComponent {
|
||||||
bindToDocument: PropTypes.bool,
|
bindToDocument: PropTypes.bool,
|
||||||
preventScroll: PropTypes.bool,
|
preventScroll: PropTypes.bool,
|
||||||
footer: PropTypes.node,
|
footer: PropTypes.node,
|
||||||
|
className: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -325,7 +326,7 @@ class ScrollableList extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, footer, emptyMessage, onLoadMore } = this.props;
|
const { children, scrollKey, className, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, footer, emptyMessage, onLoadMore } = this.props;
|
||||||
const { fullscreen } = this.state;
|
const { fullscreen } = this.state;
|
||||||
const childrenCount = Children.count(children);
|
const childrenCount = Children.count(children);
|
||||||
|
|
||||||
|
@ -336,9 +337,9 @@ class ScrollableList extends PureComponent {
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
scrollableArea = (
|
scrollableArea = (
|
||||||
<div className='scrollable scrollable--flex' ref={this.setRef}>
|
<div className='scrollable scrollable--flex' ref={this.setRef}>
|
||||||
<div role='feed' className='item-list'>
|
{prepend}
|
||||||
{prepend}
|
|
||||||
</div>
|
<div role='feed' className='item-list' />
|
||||||
|
|
||||||
<div className='scrollable__append'>
|
<div className='scrollable__append'>
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
|
@ -350,9 +351,9 @@ class ScrollableList extends PureComponent {
|
||||||
} else if (isLoading || childrenCount > 0 || numPending > 0 || hasMore || !emptyMessage) {
|
} else if (isLoading || childrenCount > 0 || numPending > 0 || hasMore || !emptyMessage) {
|
||||||
scrollableArea = (
|
scrollableArea = (
|
||||||
<div className={classNames('scrollable scrollable--flex', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
<div className={classNames('scrollable scrollable--flex', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
||||||
<div role='feed' className='item-list'>
|
{prepend}
|
||||||
{prepend}
|
|
||||||
|
|
||||||
|
<div role='feed' className={classNames('item-list', className)}>
|
||||||
{loadPending}
|
{loadPending}
|
||||||
|
|
||||||
{Children.map(this.props.children, (child, index) => (
|
{Children.map(this.props.children, (child, index) => (
|
||||||
|
|
|
@ -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 { formatTime } from 'mastodon/features/video';
|
||||||
import { autoPlayGif, displayMedia, useBlurhash } from 'mastodon/initial_state';
|
import { autoPlayGif, displayMedia, useBlurhash } from 'mastodon/initial_state';
|
||||||
import type { Status, MediaAttachment } from 'mastodon/models/status';
|
import type { Status, MediaAttachment } from 'mastodon/models/status';
|
||||||
|
import { useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
export const MediaItem: React.FC<{
|
export const MediaItem: React.FC<{
|
||||||
attachment: MediaAttachment;
|
attachment: MediaAttachment;
|
||||||
onOpenMedia: (arg0: MediaAttachment) => void;
|
onOpenMedia: (arg0: MediaAttachment) => void;
|
||||||
}> = ({ attachment, onOpenMedia }) => {
|
}> = ({ attachment, onOpenMedia }) => {
|
||||||
|
const account = useAppSelector((state) =>
|
||||||
|
state.accounts.get(attachment.getIn(['status', 'account']) as string),
|
||||||
|
);
|
||||||
const [visible, setVisible] = useState(
|
const [visible, setVisible] = useState(
|
||||||
(displayMedia !== 'hide_all' &&
|
(displayMedia !== 'hide_all' &&
|
||||||
!attachment.getIn(['status', 'sensitive'])) ||
|
!attachment.getIn(['status', 'sensitive'])) ||
|
||||||
|
@ -70,7 +74,6 @@ export const MediaItem: React.FC<{
|
||||||
const lang = status.get('language') as string;
|
const lang = status.get('language') as string;
|
||||||
const blurhash = attachment.get('blurhash') as string;
|
const blurhash = attachment.get('blurhash') as string;
|
||||||
const statusId = status.get('id') as string;
|
const statusId = status.get('id') as string;
|
||||||
const acct = status.getIn(['account', 'acct']) as string;
|
|
||||||
const type = attachment.get('type') as string;
|
const type = attachment.get('type') as string;
|
||||||
|
|
||||||
let thumbnail;
|
let thumbnail;
|
||||||
|
@ -181,7 +184,7 @@ export const MediaItem: React.FC<{
|
||||||
|
|
||||||
<a
|
<a
|
||||||
className='media-gallery__item-thumbnail'
|
className='media-gallery__item-thumbnail'
|
||||||
href={`/@${acct}/${statusId}`}
|
href={`/@${account?.acct}/${statusId}`}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
|
|
|
@ -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';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const MemorialNote = () => (
|
export const MemorialNote: React.FC = () => (
|
||||||
<div className='account-memorial-banner'>
|
<div className='account-memorial-banner'>
|
||||||
<div className='account-memorial-banner__message'>
|
<div className='account-memorial-banner__message'>
|
||||||
<FormattedMessage id='account.in_memoriam' defaultMessage='In Memoriam.' />
|
<FormattedMessage
|
||||||
|
id='account.in_memoriam'
|
||||||
|
defaultMessage='In Memoriam.'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default MemorialNote;
|
|
|
@ -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 BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||||
import { me } from 'mastodon/initial_state';
|
import { me } from 'mastodon/initial_state';
|
||||||
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
||||||
import { getAccountHidden } from 'mastodon/selectors';
|
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
||||||
import { useAppSelector } from 'mastodon/store';
|
import { useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
import { lookupAccount, fetchAccount } from '../../actions/accounts';
|
import { lookupAccount, fetchAccount } from '../../actions/accounts';
|
||||||
|
@ -22,8 +22,8 @@ import { LoadingIndicator } from '../../components/loading_indicator';
|
||||||
import StatusList from '../../components/status_list';
|
import StatusList from '../../components/status_list';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
|
||||||
|
import { AccountHeader } from './components/account_header';
|
||||||
import { LimitedAccountHint } from './components/limited_account_hint';
|
import { LimitedAccountHint } from './components/limited_account_hint';
|
||||||
import HeaderContainer from './containers/header_container';
|
|
||||||
|
|
||||||
const emptyList = ImmutableList();
|
const emptyList = ImmutableList();
|
||||||
|
|
||||||
|
@ -198,7 +198,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
<ColumnBackButton />
|
<ColumnBackButton />
|
||||||
|
|
||||||
<StatusList
|
<StatusList
|
||||||
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
|
prepend={<AccountHeader accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
|
||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
append={remoteMessage}
|
append={remoteMessage}
|
||||||
scrollKey='account_timeline'
|
scrollKey='account_timeline'
|
||||||
|
|
|
@ -6,9 +6,9 @@ import classNames from 'classnames';
|
||||||
|
|
||||||
import Overlay from 'react-overlays/Overlay';
|
import Overlay from 'react-overlays/Overlay';
|
||||||
|
|
||||||
import { useSelectableClick } from '@/hooks/useSelectableClick';
|
|
||||||
import QuestionMarkIcon from '@/material-icons/400-24px/question_mark.svg?react';
|
import QuestionMarkIcon from '@/material-icons/400-24px/question_mark.svg?react';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import { useSelectableClick } from 'mastodon/hooks/useSelectableClick';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
help: { id: 'info_button.label', defaultMessage: 'Help' },
|
help: { id: 'info_button.label', defaultMessage: 'Help' },
|
||||||
|
|
|
@ -120,7 +120,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onSubmit(missingAltTextModal && this.props.missingAltText);
|
this.props.onSubmit(missingAltTextModal && this.props.missingAltText && this.props.privacy !== 'direct');
|
||||||
|
|
||||||
if (e) {
|
if (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
@ -12,11 +12,14 @@ import Overlay from 'react-overlays/Overlay';
|
||||||
|
|
||||||
import MoodIcon from '@/material-icons/400-20px/mood.svg?react';
|
import MoodIcon from '@/material-icons/400-20px/mood.svg?react';
|
||||||
import { IconButton } from 'mastodon/components/icon_button';
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
|
import emojiCompressed from 'mastodon/features/emoji/emoji_compressed';
|
||||||
import { assetHost } from 'mastodon/utils/config';
|
import { assetHost } from 'mastodon/utils/config';
|
||||||
|
|
||||||
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
|
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
|
||||||
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
|
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
|
||||||
|
|
||||||
|
const nimblePickerData = emojiCompressed[5];
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||||
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
|
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
|
||||||
|
@ -37,15 +40,18 @@ let EmojiPicker, Emoji; // load asynchronously
|
||||||
|
|
||||||
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
|
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
|
||||||
|
|
||||||
const backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`;
|
const backgroundImageFn = () => `${assetHost}/emoji/sheet_15.png`;
|
||||||
|
|
||||||
const notFoundFn = () => (
|
const notFoundFn = () => (
|
||||||
<div className='emoji-mart-no-results'>
|
<div className='emoji-mart-no-results'>
|
||||||
<Emoji
|
<Emoji
|
||||||
|
data={nimblePickerData}
|
||||||
emoji='sleuth_or_spy'
|
emoji='sleuth_or_spy'
|
||||||
set='twitter'
|
set='twitter'
|
||||||
size={32}
|
size={32}
|
||||||
sheetSize={32}
|
sheetSize={32}
|
||||||
|
sheetColumns={62}
|
||||||
|
sheetRows={62}
|
||||||
backgroundImageFn={backgroundImageFn}
|
backgroundImageFn={backgroundImageFn}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -67,7 +73,7 @@ class ModifierPickerMenu extends PureComponent {
|
||||||
this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
|
this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
if (nextProps.active) {
|
if (nextProps.active) {
|
||||||
this.attachListeners();
|
this.attachListeners();
|
||||||
} else {
|
} else {
|
||||||
|
@ -75,7 +81,7 @@ class ModifierPickerMenu extends PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount() {
|
||||||
this.removeListeners();
|
this.removeListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,12 +91,12 @@ class ModifierPickerMenu extends PureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
attachListeners () {
|
attachListeners() {
|
||||||
document.addEventListener('click', this.handleDocumentClick, { capture: true });
|
document.addEventListener('click', this.handleDocumentClick, { capture: true });
|
||||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeListeners () {
|
removeListeners() {
|
||||||
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
|
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
|
||||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
}
|
}
|
||||||
|
@ -99,17 +105,17 @@ class ModifierPickerMenu extends PureComponent {
|
||||||
this.node = c;
|
this.node = c;
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { active } = this.props;
|
const { active } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
|
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
|
||||||
<button type='button' onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
|
<button type='button' onClick={this.handleClick} data-index={1}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
|
<button type='button' onClick={this.handleClick} data-index={2}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
|
<button type='button' onClick={this.handleClick} data-index={3}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
|
<button type='button' onClick={this.handleClick} data-index={4}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
|
<button type='button' onClick={this.handleClick} data-index={5}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
|
<button type='button' onClick={this.handleClick} data-index={6}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -139,12 +145,12 @@ class ModifierPicker extends PureComponent {
|
||||||
this.props.onClose();
|
this.props.onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { active, modifier } = this.props;
|
const { active, modifier } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='emoji-picker-dropdown__modifiers'>
|
<div className='emoji-picker-dropdown__modifiers'>
|
||||||
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
|
<Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
|
||||||
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
|
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -184,7 +190,7 @@ class EmojiPickerMenuImpl extends PureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount() {
|
||||||
document.addEventListener('click', this.handleDocumentClick, { capture: true });
|
document.addEventListener('click', this.handleDocumentClick, { capture: true });
|
||||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
|
|
||||||
|
@ -199,7 +205,7 @@ class EmojiPickerMenuImpl extends PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount() {
|
||||||
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
|
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
|
||||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
}
|
}
|
||||||
|
@ -252,7 +258,7 @@ class EmojiPickerMenuImpl extends PureComponent {
|
||||||
this.props.onSkinTone(modifier);
|
this.props.onSkinTone(modifier);
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
|
const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
@ -280,6 +286,9 @@ class EmojiPickerMenuImpl extends PureComponent {
|
||||||
return (
|
return (
|
||||||
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
|
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
|
data={nimblePickerData}
|
||||||
|
sheetColumns={62}
|
||||||
|
sheetRows={62}
|
||||||
perLine={8}
|
perLine={8}
|
||||||
emojiSize={22}
|
emojiSize={22}
|
||||||
sheetSize={32}
|
sheetSize={32}
|
||||||
|
@ -345,7 +354,7 @@ class EmojiPickerDropdown extends PureComponent {
|
||||||
|
|
||||||
EmojiPickerAsync().then(EmojiMart => {
|
EmojiPickerAsync().then(EmojiMart => {
|
||||||
EmojiPicker = EmojiMart.Picker;
|
EmojiPicker = EmojiMart.Picker;
|
||||||
Emoji = EmojiMart.Emoji;
|
Emoji = EmojiMart.Emoji;
|
||||||
|
|
||||||
this.setState({ loading: false });
|
this.setState({ loading: false });
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
@ -386,7 +395,7 @@ class EmojiPickerDropdown extends PureComponent {
|
||||||
this.setState({ placement: state.placement });
|
this.setState({ placement: state.placement });
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
|
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
|
||||||
const title = intl.formatMessage(messages.emoji);
|
const title = intl.formatMessage(messages.emoji);
|
||||||
const { active, loading, placement } = this.state;
|
const { active, loading, placement } = this.state;
|
||||||
|
@ -403,7 +412,7 @@ class EmojiPickerDropdown extends PureComponent {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Overlay show={active} placement={placement} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
<Overlay show={active} placement={placement} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||||
{({ props, placement })=> (
|
{({ props, placement }) => (
|
||||||
<div {...props} style={{ ...props.style }}>
|
<div {...props} style={{ ...props.style }}>
|
||||||
<div className={`dropdown-animation ${placement}`}>
|
<div className={`dropdown-animation ${placement}`}>
|
||||||
<EmojiPickerMenu
|
<EmojiPickerMenu
|
||||||
|
|
|
@ -22,10 +22,9 @@ import { LoadMore } from 'mastodon/components/load_more';
|
||||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||||
import { RadioButton } from 'mastodon/components/radio_button';
|
import { RadioButton } from 'mastodon/components/radio_button';
|
||||||
import ScrollContainer from 'mastodon/containers/scroll_container';
|
import ScrollContainer from 'mastodon/containers/scroll_container';
|
||||||
|
import { useSearchParam } from 'mastodon/hooks/useSearchParam';
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
import { useSearchParam } from '../../../hooks/useSearchParam';
|
|
||||||
|
|
||||||
import { AccountCard } from './components/account_card';
|
import { AccountCard } from './components/account_card';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
|
|
@ -45,6 +45,7 @@ type EmojiCompressed = [
|
||||||
Category[],
|
Category[],
|
||||||
Data['aliases'],
|
Data['aliases'],
|
||||||
EmojisWithoutShortCodes,
|
EmojisWithoutShortCodes,
|
||||||
|
Data,
|
||||||
];
|
];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -9,18 +9,91 @@
|
||||||
|
|
||||||
// This version comment should be bumped each time the emoji data is changed
|
// This version comment should be bumped each time the emoji data is changed
|
||||||
// to ensure that the prevaled file is regenerated by Babel
|
// to ensure that the prevaled file is regenerated by Babel
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
const { emojiIndex } = require('emoji-mart');
|
// This json file contains the names of the categories.
|
||||||
let data = require('emoji-mart/data/all.json');
|
const emojiMart5LocalesData = require('@emoji-mart/data/i18n/en.json');
|
||||||
|
const emojiMart5Data = require('@emoji-mart/data/sets/15/all.json');
|
||||||
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
|
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
|
||||||
const emojiMap = require('./emoji_map.json');
|
const emojiMap = require('./emoji_map.json');
|
||||||
|
// This json file is downloaded from https://github.com/iamcal/emoji-data/
|
||||||
|
// and is used to correct the sheet coordinates since we're using that repo's sheet
|
||||||
|
const emojiSheetData = require('./emoji_sheet.json');
|
||||||
const { unicodeToFilename } = require('./unicode_to_filename');
|
const { unicodeToFilename } = require('./unicode_to_filename');
|
||||||
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
|
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
|
||||||
|
|
||||||
if(data.compressed) {
|
// Grabbed from `emoji_utils` to avoid circular dependency
|
||||||
data = emojiMartUncompress(data);
|
function unifiedToNative(unified) {
|
||||||
|
let unicodes = unified.split('-'),
|
||||||
|
codePoints = unicodes.map((u) => `0x${u}`);
|
||||||
|
|
||||||
|
return String.fromCodePoint(...codePoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
compressed: true,
|
||||||
|
categories: emojiMart5Data.categories.map(cat => {
|
||||||
|
return {
|
||||||
|
...cat,
|
||||||
|
name: emojiMart5LocalesData.categories[cat.id]
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
aliases: emojiMart5Data.aliases,
|
||||||
|
emojis: _(emojiMart5Data.emojis).values().map(emoji => {
|
||||||
|
let skin_variations = {};
|
||||||
|
const unified = emoji.skins[0].unified.toUpperCase();
|
||||||
|
const emojiFromRawData = emojiSheetData.find(e => e.unified === unified);
|
||||||
|
|
||||||
|
if (!emojiFromRawData) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emoji.skins.length > 1) {
|
||||||
|
const [, ...nonDefaultSkins] = emoji.skins;
|
||||||
|
nonDefaultSkins.forEach(skin => {
|
||||||
|
const [matchingRawCodePoints,matchingRawEmoji] = Object.entries(emojiFromRawData.skin_variations).find((pair) => {
|
||||||
|
const [, value] = pair;
|
||||||
|
return value.unified.toLowerCase() === skin.unified;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matchingRawEmoji && matchingRawCodePoints) {
|
||||||
|
// At the time of writing, the json from `@emoji-mart/data` doesn't have data
|
||||||
|
// for emoji like `woman-heart-woman` with two different skin tones.
|
||||||
|
const skinToneCode = matchingRawCodePoints.split('-')[0];
|
||||||
|
skin_variations[skinToneCode] = {
|
||||||
|
unified: matchingRawEmoji.unified.toUpperCase(),
|
||||||
|
non_qualified: null,
|
||||||
|
sheet_x: matchingRawEmoji.sheet_x,
|
||||||
|
sheet_y: matchingRawEmoji.sheet_y,
|
||||||
|
has_img_twitter: true,
|
||||||
|
native: unifiedToNative(matchingRawEmoji.unified.toUpperCase())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
a: emoji.name,
|
||||||
|
b: unified,
|
||||||
|
c: undefined,
|
||||||
|
f: true,
|
||||||
|
j: [emoji.id, ...emoji.keywords],
|
||||||
|
k: [emojiFromRawData.sheet_x, emojiFromRawData.sheet_y],
|
||||||
|
m: emoji.emoticons?.[0],
|
||||||
|
l: emoji.emoticons,
|
||||||
|
o: emoji.version,
|
||||||
|
id: emoji.id,
|
||||||
|
skin_variations,
|
||||||
|
native: unifiedToNative(unified.toUpperCase())
|
||||||
|
};
|
||||||
|
}).compact().keyBy(e => e.id).mapValues(e => _.omit(e, 'id')).value()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.compressed) {
|
||||||
|
emojiMartUncompress(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
const emojiMartData = data;
|
const emojiMartData = data;
|
||||||
|
@ -32,15 +105,10 @@ const shortcodeMap = {};
|
||||||
const shortCodesToEmojiData = {};
|
const shortCodesToEmojiData = {};
|
||||||
const emojisWithoutShortCodes = [];
|
const emojisWithoutShortCodes = [];
|
||||||
|
|
||||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
Object.keys(emojiMart5Data.emojis).forEach(key => {
|
||||||
let emoji = emojiIndex.emojis[key];
|
let emoji = emojiMart5Data.emojis[key];
|
||||||
|
|
||||||
// Emojis with skin tone modifiers are stored like this
|
shortcodeMap[emoji.skins[0].native] = emoji.id;
|
||||||
if (Object.hasOwn(emoji, '1')) {
|
|
||||||
emoji = emoji['1'];
|
|
||||||
}
|
|
||||||
|
|
||||||
shortcodeMap[emoji.native] = emoji.id;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const stripModifiers = unicode => {
|
const stripModifiers = unicode => {
|
||||||
|
@ -84,13 +152,9 @@ Object.keys(emojiMap).forEach(key => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
Object.keys(emojiMartData.emojis).forEach(key => {
|
||||||
let emoji = emojiIndex.emojis[key];
|
let emoji = emojiMartData.emojis[key];
|
||||||
|
|
||||||
// Emojis with skin tone modifiers are stored like this
|
|
||||||
if (Object.hasOwn(emoji, '1')) {
|
|
||||||
emoji = emoji['1'];
|
|
||||||
}
|
|
||||||
|
|
||||||
const { native } = emoji;
|
const { native } = emoji;
|
||||||
let { short_names, search, unified } = emojiMartData.emojis[key];
|
let { short_names, search, unified } = emojiMartData.emojis[key];
|
||||||
|
@ -135,4 +199,5 @@ module.exports = JSON.parse(JSON.stringify([
|
||||||
emojiMartData.categories,
|
emojiMartData.categories,
|
||||||
emojiMartData.aliases,
|
emojiMartData.aliases,
|
||||||
emojisWithoutShortCodes,
|
emojisWithoutShortCodes,
|
||||||
|
emojiMartData
|
||||||
]));
|
]));
|
||||||
|
|
|
@ -8,14 +8,15 @@ import type { Search, ShortCodesToEmojiData } from './emoji_compressed';
|
||||||
import emojiCompressed from './emoji_compressed';
|
import emojiCompressed from './emoji_compressed';
|
||||||
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
||||||
|
|
||||||
type Emojis = {
|
type Emojis = Record<
|
||||||
[key in NonNullable<keyof ShortCodesToEmojiData>]: {
|
NonNullable<keyof ShortCodesToEmojiData>,
|
||||||
|
{
|
||||||
native: BaseEmoji['native'];
|
native: BaseEmoji['native'];
|
||||||
search: Search;
|
search: Search;
|
||||||
short_names: Emoji['short_names'];
|
short_names: Emoji['short_names'];
|
||||||
unified: Emoji['unified'];
|
unified: Emoji['unified'];
|
||||||
};
|
}
|
||||||
};
|
>;
|
||||||
|
|
||||||
const [
|
const [
|
||||||
shortCodesToEmojiData,
|
shortCodesToEmojiData,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Emoji from 'emoji-mart/dist-es/components/emoji/emoji';
|
import Emoji from 'emoji-mart/dist-es/components/emoji/nimble-emoji';
|
||||||
import Picker from 'emoji-mart/dist-es/components/picker/picker';
|
import Picker from 'emoji-mart/dist-es/components/picker/nimble-picker';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Picker,
|
Picker,
|
||||||
|
|
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 emojiCompressed from './emoji_compressed';
|
||||||
import { unicodeToFilename } from './unicode_to_filename';
|
import { unicodeToFilename } from './unicode_to_filename';
|
||||||
|
|
||||||
type UnicodeMapping = {
|
type UnicodeMapping = Record<
|
||||||
[key in FilenameData[number][0]]: {
|
FilenameData[number][0],
|
||||||
|
{
|
||||||
shortCode: ShortCodesToEmojiDataKey;
|
shortCode: ShortCodesToEmojiDataKey;
|
||||||
filename: FilenameData[number][number];
|
filename: FilenameData[number][number];
|
||||||
};
|
}
|
||||||
};
|
>;
|
||||||
|
|
||||||
const [
|
const [
|
||||||
shortCodesToEmojiData,
|
shortCodesToEmojiData,
|
||||||
|
|
|
@ -10,9 +10,10 @@ import { debounce } from 'lodash';
|
||||||
|
|
||||||
import { Account } from 'mastodon/components/account';
|
import { Account } from 'mastodon/components/account';
|
||||||
import { TimelineHint } from 'mastodon/components/timeline_hint';
|
import { TimelineHint } from 'mastodon/components/timeline_hint';
|
||||||
|
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
|
||||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||||
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
||||||
import { getAccountHidden } from 'mastodon/selectors';
|
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
||||||
import { useAppSelector } from 'mastodon/store';
|
import { useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -25,7 +26,6 @@ import { ColumnBackButton } from '../../components/column_back_button';
|
||||||
import { LoadingIndicator } from '../../components/loading_indicator';
|
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
|
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
|
||||||
import HeaderContainer from '../account_timeline/containers/header_container';
|
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
|
||||||
const mapStateToProps = (state, { params: { acct, id } }) => {
|
const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||||
|
@ -168,7 +168,7 @@ class Followers extends ImmutablePureComponent {
|
||||||
hasMore={!forceEmptyState && hasMore}
|
hasMore={!forceEmptyState && hasMore}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
|
prepend={<AccountHeader accountId={this.props.accountId} hideTabs />}
|
||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
append={remoteMessage}
|
append={remoteMessage}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
|
|
|
@ -10,9 +10,10 @@ import { debounce } from 'lodash';
|
||||||
|
|
||||||
import { Account } from 'mastodon/components/account';
|
import { Account } from 'mastodon/components/account';
|
||||||
import { TimelineHint } from 'mastodon/components/timeline_hint';
|
import { TimelineHint } from 'mastodon/components/timeline_hint';
|
||||||
|
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
|
||||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||||
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
||||||
import { getAccountHidden } from 'mastodon/selectors';
|
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
||||||
import { useAppSelector } from 'mastodon/store';
|
import { useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -25,7 +26,6 @@ import { ColumnBackButton } from '../../components/column_back_button';
|
||||||
import { LoadingIndicator } from '../../components/loading_indicator';
|
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
|
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
|
||||||
import HeaderContainer from '../account_timeline/containers/header_container';
|
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
|
||||||
const mapStateToProps = (state, { params: { acct, id } }) => {
|
const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||||
|
@ -168,7 +168,7 @@ class Following extends ImmutablePureComponent {
|
||||||
hasMore={!forceEmptyState && hasMore}
|
hasMore={!forceEmptyState && hasMore}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
|
prepend={<AccountHeader accountId={this.props.accountId} hideTabs />}
|
||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
append={remoteMessage}
|
append={remoteMessage}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
|
|
|
@ -17,7 +17,7 @@ export const ColumnSettings: React.FC = () => {
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(key: string, checked: boolean) => {
|
(key: string[], checked: boolean) => {
|
||||||
dispatch(changeSetting(['home', ...key], checked));
|
dispatch(changeSetting(['home', ...key], checked));
|
||||||
},
|
},
|
||||||
[dispatch],
|
[dispatch],
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
import { useSearchParam } from '@/hooks/useSearchParam';
|
|
||||||
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
|
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
|
||||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
||||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||||
|
@ -20,6 +19,7 @@ import { Icon } from 'mastodon/components/icon';
|
||||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||||
import Status from 'mastodon/containers/status_container';
|
import Status from 'mastodon/containers/status_container';
|
||||||
import { Search } from 'mastodon/features/compose/components/search';
|
import { Search } from 'mastodon/features/compose/components/search';
|
||||||
|
import { useSearchParam } from 'mastodon/hooks/useSearchParam';
|
||||||
import type { Hashtag as HashtagType } from 'mastodon/models/tags';
|
import type { Hashtag as HashtagType } from 'mastodon/models/tags';
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,11 @@ import { useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import { useRenderSignal } from 'mastodon/../hooks/useRenderSignal';
|
|
||||||
import { fetchStatus, toggleStatusSpoilers } from 'mastodon/actions/statuses';
|
import { fetchStatus, toggleStatusSpoilers } from 'mastodon/actions/statuses';
|
||||||
import { hydrateStore } from 'mastodon/actions/store';
|
import { hydrateStore } from 'mastodon/actions/store';
|
||||||
import { Router } from 'mastodon/components/router';
|
import { Router } from 'mastodon/components/router';
|
||||||
import { DetailedStatus } from 'mastodon/features/status/components/detailed_status';
|
import { DetailedStatus } from 'mastodon/features/status/components/detailed_status';
|
||||||
|
import { useRenderSignal } from 'mastodon/hooks/useRenderSignal';
|
||||||
import initialState from 'mastodon/initial_state';
|
import initialState from 'mastodon/initial_state';
|
||||||
import { IntlProvider } from 'mastodon/locales';
|
import { IntlProvider } from 'mastodon/locales';
|
||||||
import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors';
|
import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors';
|
||||||
|
|
|
@ -221,12 +221,12 @@ export const DetailedStatus: React.FC<{
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (status.get('spoiler_text').length === 0) {
|
} else if (status.get('card')) {
|
||||||
media = (
|
media = (
|
||||||
<Card
|
<Card
|
||||||
sensitive={status.get('sensitive')}
|
sensitive={status.get('sensitive')}
|
||||||
onOpenMedia={onOpenMedia}
|
onOpenMedia={onOpenMedia}
|
||||||
card={status.get('card', null)}
|
card={status.get('card')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,26 +8,31 @@ import {
|
||||||
} from 'react-intl';
|
} from 'react-intl';
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { Link, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { apiGetTermsOfService } from 'mastodon/api/instance';
|
import { apiGetTermsOfService } from 'mastodon/api/instance';
|
||||||
import type { ApiTermsOfServiceJSON } from 'mastodon/api_types/instance';
|
import type { ApiTermsOfServiceJSON } from 'mastodon/api_types/instance';
|
||||||
import { Column } from 'mastodon/components/column';
|
import { Column } from 'mastodon/components/column';
|
||||||
import { Skeleton } from 'mastodon/components/skeleton';
|
|
||||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'terms_of_service.title', defaultMessage: 'Terms of Service' },
|
title: { id: 'terms_of_service.title', defaultMessage: 'Terms of Service' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface Params {
|
||||||
|
date?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const TermsOfService: React.FC<{
|
const TermsOfService: React.FC<{
|
||||||
multiColumn: boolean;
|
multiColumn: boolean;
|
||||||
}> = ({ multiColumn }) => {
|
}> = ({ multiColumn }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const { date } = useParams<Params>();
|
||||||
const [response, setResponse] = useState<ApiTermsOfServiceJSON>();
|
const [response, setResponse] = useState<ApiTermsOfServiceJSON>();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
apiGetTermsOfService()
|
apiGetTermsOfService(date)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setResponse(data);
|
setResponse(data);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
@ -36,7 +41,7 @@ const TermsOfService: React.FC<{
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}, []);
|
}, [date]);
|
||||||
|
|
||||||
if (!loading && !response) {
|
if (!loading && !response) {
|
||||||
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
|
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
|
||||||
|
@ -55,23 +60,60 @@ const TermsOfService: React.FC<{
|
||||||
defaultMessage='Terms of Service'
|
defaultMessage='Terms of Service'
|
||||||
/>
|
/>
|
||||||
</h3>
|
</h3>
|
||||||
<p>
|
<p className='prose'>
|
||||||
<FormattedMessage
|
{response?.effective ? (
|
||||||
id='privacy_policy.last_updated'
|
<FormattedMessage
|
||||||
defaultMessage='Last updated {date}'
|
id='privacy_policy.last_updated'
|
||||||
values={{
|
defaultMessage='Last updated {date}'
|
||||||
date: loading ? (
|
values={{
|
||||||
<Skeleton width='10ch' />
|
date: (
|
||||||
) : (
|
<FormattedDate
|
||||||
<FormattedDate
|
value={response.effective_date}
|
||||||
value={response?.updated_at}
|
year='numeric'
|
||||||
year='numeric'
|
month='short'
|
||||||
month='short'
|
day='2-digit'
|
||||||
day='2-digit'
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
id='terms_of_service.effective_as_of'
|
||||||
|
defaultMessage='Effective as of {date}'
|
||||||
|
values={{
|
||||||
|
date: (
|
||||||
|
<FormattedDate
|
||||||
|
value={response?.effective_date}
|
||||||
|
year='numeric'
|
||||||
|
month='short'
|
||||||
|
day='2-digit'
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{response?.succeeded_by && (
|
||||||
|
<>
|
||||||
|
{' · '}
|
||||||
|
<Link to={`/terms-of-service/${response.succeeded_by}`}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='terms_of_service.upcoming_changes_on'
|
||||||
|
defaultMessage='Upcoming changes on {date}'
|
||||||
|
values={{
|
||||||
|
date: (
|
||||||
|
<FormattedDate
|
||||||
|
value={response.succeeded_by}
|
||||||
|
year='numeric'
|
||||||
|
month='short'
|
||||||
|
day='2-digit'
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
</Link>
|
||||||
}}
|
</>
|
||||||
/>
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -101,6 +101,7 @@ const EmbedModal: React.FC<{
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<iframe
|
<iframe
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||||
frameBorder='0'
|
frameBorder='0'
|
||||||
ref={iframeRef}
|
ref={iframeRef}
|
||||||
sandbox='allow-scripts allow-same-origin'
|
sandbox='allow-scripts allow-same-origin'
|
||||||
|
|
|
@ -205,7 +205,7 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
|
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
|
||||||
<WrappedRoute path='/about' component={About} content={children} />
|
<WrappedRoute path='/about' component={About} content={children} />
|
||||||
<WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
|
<WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
|
||||||
<WrappedRoute path='/terms-of-service' component={TermsOfService} content={children} />
|
<WrappedRoute path='/terms-of-service/:date?' component={TermsOfService} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
|
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
|
||||||
<Redirect from='/timelines/public' to='/public' exact />
|
<Redirect from='/timelines/public' to='/public' exact />
|
||||||
|
|
|
@ -592,7 +592,6 @@
|
||||||
"poll_button.remove_poll": "إزالة استطلاع الرأي",
|
"poll_button.remove_poll": "إزالة استطلاع الرأي",
|
||||||
"privacy.change": "اضبط خصوصية المنشور",
|
"privacy.change": "اضبط خصوصية المنشور",
|
||||||
"privacy.direct.long": "كل من ذُكر في المنشور",
|
"privacy.direct.long": "كل من ذُكر في المنشور",
|
||||||
"privacy.direct.short": "أشخاص محددون",
|
|
||||||
"privacy.private.long": "متابعيك فقط",
|
"privacy.private.long": "متابعيك فقط",
|
||||||
"privacy.private.short": "للمتابِعين",
|
"privacy.private.short": "للمتابِعين",
|
||||||
"privacy.public.long": "أي شخص على أو خارج ماستدون",
|
"privacy.public.long": "أي شخص على أو خارج ماستدون",
|
||||||
|
|
|
@ -4,15 +4,16 @@
|
||||||
"about.disclaimer": "Mastodon ye software gratuito y de códigu llibre, y una marca rexistrada de Mastodon gGmbH.",
|
"about.disclaimer": "Mastodon ye software gratuito y de códigu llibre, y una marca rexistrada de Mastodon gGmbH.",
|
||||||
"about.domain_blocks.no_reason_available": "El motivu nun ta disponible",
|
"about.domain_blocks.no_reason_available": "El motivu nun ta disponible",
|
||||||
"about.domain_blocks.preamble": "Polo xeneral, Mastodon permítete ver el conteníu ya interactuar colos perfiles d'otros sirvidores nel fediversu. Estes son les esceiciones que se ficieron nesti sirvidor.",
|
"about.domain_blocks.preamble": "Polo xeneral, Mastodon permítete ver el conteníu ya interactuar colos perfiles d'otros sirvidores nel fediversu. Estes son les esceiciones que se ficieron nesti sirvidor.",
|
||||||
"about.domain_blocks.silenced.explanation": "Polo xeneral, nun ves los perfiles ya'l conteníu d'esti sirvidor sacante que los busques o decidas siguilos.",
|
"about.domain_blocks.silenced.explanation": "Polo xeneral, nun ves los perfiles y el conteníu d'esti sirvidor sacante que los busques o decidas siguilos.",
|
||||||
"about.domain_blocks.silenced.title": "Llendóse",
|
"about.domain_blocks.silenced.title": "Llendóse",
|
||||||
"about.domain_blocks.suspended.explanation": "Nun se procesa, atroxa nin intercambia nengún datu d'esti sirvidor, lo que fai que cualesquier interaición o comunicación colos sos perfiles seya imposible.",
|
"about.domain_blocks.suspended.explanation": "Nun se procesa, atroxa nin intercambia nengún datu d'esti sirvidor, lo que fai imposible cualesquier interaición o comunicación colos sos perfiles.",
|
||||||
"about.domain_blocks.suspended.title": "Suspendióse",
|
"about.domain_blocks.suspended.title": "Suspendióse",
|
||||||
"about.not_available": "Esta información nun ta disponible nesti sirvidor.",
|
"about.not_available": "Esta información nun ta disponible nesti sirvidor.",
|
||||||
"about.powered_by": "Una rede social descentralizada que tien la teunoloxía de {mastodon}",
|
"about.powered_by": "Una rede social descentralizada que tien la teunoloxía de {mastodon}",
|
||||||
"about.rules": "Normes del sirvidor",
|
"about.rules": "Normes del sirvidor",
|
||||||
"account.account_note_header": "Nota personal",
|
"account.account_note_header": "Nota personal",
|
||||||
"account.add_or_remove_from_list": "Amestar o quitar de les llistes",
|
"account.add_or_remove_from_list": "Amestar o quitar de les llistes",
|
||||||
|
"account.badges.bot": "Automatizóse",
|
||||||
"account.badges.group": "Grupu",
|
"account.badges.group": "Grupu",
|
||||||
"account.block": "Bloquiar a @{name}",
|
"account.block": "Bloquiar a @{name}",
|
||||||
"account.block_domain": "Bloquiar el dominiu {domain}",
|
"account.block_domain": "Bloquiar el dominiu {domain}",
|
||||||
|
@ -25,7 +26,7 @@
|
||||||
"account.edit_profile": "Editar el perfil",
|
"account.edit_profile": "Editar el perfil",
|
||||||
"account.enable_notifications": "Avisame cuando @{name} espublice artículos",
|
"account.enable_notifications": "Avisame cuando @{name} espublice artículos",
|
||||||
"account.endorse": "Destacar nel perfil",
|
"account.endorse": "Destacar nel perfil",
|
||||||
"account.featured_tags.last_status_never": "Nun hai nengún artículu",
|
"account.featured_tags.last_status_never": "Nun hai nenguna publicación",
|
||||||
"account.featured_tags.title": "Etiquetes destacaes de: {name}",
|
"account.featured_tags.title": "Etiquetes destacaes de: {name}",
|
||||||
"account.follow": "Siguir",
|
"account.follow": "Siguir",
|
||||||
"account.follow_back": "Siguir tamién",
|
"account.follow_back": "Siguir tamién",
|
||||||
|
@ -107,6 +108,7 @@
|
||||||
"column.domain_blocks": "Dominios bloquiaos",
|
"column.domain_blocks": "Dominios bloquiaos",
|
||||||
"column.edit_list": "Editar la llista",
|
"column.edit_list": "Editar la llista",
|
||||||
"column.favourites": "Favoritos",
|
"column.favourites": "Favoritos",
|
||||||
|
"column.firehose": "Feed en direuto",
|
||||||
"column.follow_requests": "Solicitúes de siguimientu",
|
"column.follow_requests": "Solicitúes de siguimientu",
|
||||||
"column.home": "Aniciu",
|
"column.home": "Aniciu",
|
||||||
"column.lists": "Llistes",
|
"column.lists": "Llistes",
|
||||||
|
@ -126,9 +128,9 @@
|
||||||
"community.column_settings.remote_only": "Namás lo remoto",
|
"community.column_settings.remote_only": "Namás lo remoto",
|
||||||
"compose.language.change": "Camudar la llingua",
|
"compose.language.change": "Camudar la llingua",
|
||||||
"compose.language.search": "Buscar llingües…",
|
"compose.language.search": "Buscar llingües…",
|
||||||
"compose.published.body": "Espublizóse l'artículu.",
|
"compose.published.body": "Publicóse la publicación.",
|
||||||
"compose.published.open": "Abrir",
|
"compose.published.open": "Abrir",
|
||||||
"compose.saved.body": "Post guardáu.",
|
"compose.saved.body": "Guardóse la publicación.",
|
||||||
"compose_form.direct_message_warning_learn_more": "Saber más",
|
"compose_form.direct_message_warning_learn_more": "Saber más",
|
||||||
"compose_form.encryption_warning": "Los artículos de Mastodon nun tán cifraos de puntu a puntu. Nun compartas nengún tipu d'información sensible per Mastodon.",
|
"compose_form.encryption_warning": "Los artículos de Mastodon nun tán cifraos de puntu a puntu. Nun compartas nengún tipu d'información sensible per Mastodon.",
|
||||||
"compose_form.lock_disclaimer": "La to cuenta nun ye {locked}. Cualesquier perfil pue siguite pa ver los artículos que son namás pa siguidores.",
|
"compose_form.lock_disclaimer": "La to cuenta nun ye {locked}. Cualesquier perfil pue siguite pa ver los artículos que son namás pa siguidores.",
|
||||||
|
@ -137,34 +139,33 @@
|
||||||
"compose_form.poll.option_placeholder": "Opción {number}",
|
"compose_form.poll.option_placeholder": "Opción {number}",
|
||||||
"compose_form.poll.type": "Tipu",
|
"compose_form.poll.type": "Tipu",
|
||||||
"compose_form.publish": "Espublizar",
|
"compose_form.publish": "Espublizar",
|
||||||
"compose_form.publish_form": "Artículu nuevu",
|
"compose_form.publish_form": "Publicación nueva",
|
||||||
"compose_form.reply": "Responder",
|
"compose_form.reply": "Responder",
|
||||||
"confirmation_modal.cancel": "Encaboxar",
|
"confirmation_modal.cancel": "Encaboxar",
|
||||||
"confirmations.block.confirm": "Bloquiar",
|
"confirmations.block.confirm": "Bloquiar",
|
||||||
"confirmations.delete.confirm": "Desaniciar",
|
"confirmations.delete.confirm": "Desaniciar",
|
||||||
"confirmations.delete.message": "¿De xuru que quies desaniciar esti artículu?",
|
"confirmations.delete.message": "¿De xuru que quies desaniciar esta publicación?",
|
||||||
"confirmations.delete.title": "¿Desaniciar l'artículu?",
|
"confirmations.delete.title": "¿Quies desaniciar esta publicación?",
|
||||||
"confirmations.delete_list.confirm": "Desaniciar",
|
"confirmations.delete_list.confirm": "Desaniciar",
|
||||||
"confirmations.delete_list.message": "¿De xuru que quies desaniciar permanentemente esta llista?",
|
"confirmations.delete_list.message": "¿De xuru que quies desaniciar permanentemente esta llista?",
|
||||||
"confirmations.delete_list.title": "¿Desaniciar la llista?",
|
"confirmations.delete_list.title": "¿Quies desaniciar la llista?",
|
||||||
"confirmations.discard_edit_media.confirm": "Escartar",
|
"confirmations.discard_edit_media.confirm": "Escartar",
|
||||||
"confirmations.edit.confirm": "Editar",
|
"confirmations.edit.confirm": "Editar",
|
||||||
"confirmations.edit.message": "La edición va sobrescribir el mensaxe que tas escribiendo. ¿De xuru que quies siguir?",
|
"confirmations.edit.message": "La edición va sobrescribir el mensaxe que tas escribiendo. ¿De xuru que quies siguir?",
|
||||||
"confirmations.follow_to_list.title": "¿Siguir al usuariu?",
|
"confirmations.follow_to_list.title": "¿Siguir al usuariu?",
|
||||||
"confirmations.logout.confirm": "Zarrar la sesión",
|
"confirmations.logout.confirm": "Zarrar la sesión",
|
||||||
"confirmations.logout.message": "¿De xuru que quies zarrar la sesión?",
|
"confirmations.logout.message": "¿De xuru que quies zarrar la sesión?",
|
||||||
"confirmations.logout.title": "¿Zarrar la sesión?",
|
"confirmations.logout.title": "¿Quies zarrar la sesión?",
|
||||||
"confirmations.missing_alt_text.confirm": "Amestar testu alternativu",
|
"confirmations.missing_alt_text.confirm": "Amestar testu alternativu",
|
||||||
"confirmations.missing_alt_text.title": "¿Quies amestar testu alternativu?",
|
"confirmations.missing_alt_text.title": "¿Quies amestar testu alternativu?",
|
||||||
"confirmations.redraft.confirm": "Desaniciar y reeditar",
|
"confirmations.redraft.confirm": "Desaniciar y reeditar",
|
||||||
"confirmations.redraft.message": "¿De xuru que quies desaniciar esti artículu y reeditalu? Van perdese los favoritos y comparticiones, y les rempuestes al artículu orixinal van quedar güérfanes.",
|
"confirmations.redraft.title": "¿Desaniciar y reeditar la publicación?",
|
||||||
"confirmations.redraft.title": "¿Desaniciar ya reeditar l'artículu?",
|
|
||||||
"confirmations.reply.confirm": "Responder",
|
"confirmations.reply.confirm": "Responder",
|
||||||
"confirmations.reply.message": "Responder agora va sobrescribir el mensaxe que tas componiendo anguaño. ¿De xuru que quies siguir?",
|
"confirmations.reply.message": "Responder agora va sobrescribir el mensaxe que tas componiendo anguaño. ¿De xuru que quies siguir?",
|
||||||
"confirmations.unfollow.confirm": "Dexar de siguir",
|
"confirmations.unfollow.confirm": "Dexar de siguir",
|
||||||
"confirmations.unfollow.message": "¿De xuru que quies dexar de siguir a {name}?",
|
"confirmations.unfollow.message": "¿De xuru que quies dexar de siguir a {name}?",
|
||||||
"confirmations.unfollow.title": "¿Dexar de siguir al usuariu?",
|
"confirmations.unfollow.title": "¿Dexar de siguir al usuariu?",
|
||||||
"content_warning.hide": "Anubrir l'artículu",
|
"content_warning.hide": "Esconder la publicación",
|
||||||
"content_warning.show": "Amosar de toes toes",
|
"content_warning.show": "Amosar de toes toes",
|
||||||
"content_warning.show_more": "Amosar más",
|
"content_warning.show_more": "Amosar más",
|
||||||
"conversation.delete": "Desaniciar la conversación",
|
"conversation.delete": "Desaniciar la conversación",
|
||||||
|
@ -186,7 +187,7 @@
|
||||||
"domain_block_modal.title": "Bloquiar el dominiu?",
|
"domain_block_modal.title": "Bloquiar el dominiu?",
|
||||||
"domain_pill.server": "Sirvidor",
|
"domain_pill.server": "Sirvidor",
|
||||||
"domain_pill.username": "Nome d'usuariu",
|
"domain_pill.username": "Nome d'usuariu",
|
||||||
"embed.instructions": "Empotra esti artículu nel to sitiu web copiando'l códigu d'abaxo.",
|
"embed.instructions": "Empotra esta publicación nel to sitiu web copiando'l códigu d'abaxo.",
|
||||||
"embed.preview": "Va apaecer asina:",
|
"embed.preview": "Va apaecer asina:",
|
||||||
"emoji_button.activity": "Actividá",
|
"emoji_button.activity": "Actividá",
|
||||||
"emoji_button.flags": "Banderes",
|
"emoji_button.flags": "Banderes",
|
||||||
|
@ -201,9 +202,9 @@
|
||||||
"emoji_button.symbols": "Símbolos",
|
"emoji_button.symbols": "Símbolos",
|
||||||
"emoji_button.travel": "Viaxes y llugares",
|
"emoji_button.travel": "Viaxes y llugares",
|
||||||
"empty_column.account_suspended": "Cuenta suspendida",
|
"empty_column.account_suspended": "Cuenta suspendida",
|
||||||
"empty_column.account_timeline": "¡Equí nun hai nengún artículu!",
|
"empty_column.account_timeline": "¡Equí nun hai nenguna publicación!",
|
||||||
"empty_column.blocks": "Nun bloquiesti a nengún perfil.",
|
"empty_column.blocks": "Nun bloquiesti a nengún perfil.",
|
||||||
"empty_column.bookmarked_statuses": "Nun tienes nengún artículu en Marcadores. Cuando amiestes dalgún, apaez equí.",
|
"empty_column.bookmarked_statuses": "Nun tienes nenguna publicación en Marcadores. Cuando amiestes dalguna, va apaecer equí.",
|
||||||
"empty_column.direct": "Nun tienes nenguna mención privada. Cuando unvies o recibas dalguna, apaez equí.",
|
"empty_column.direct": "Nun tienes nenguna mención privada. Cuando unvies o recibas dalguna, apaez equí.",
|
||||||
"empty_column.domain_blocks": "Nun hai nengún dominiu bloquiáu.",
|
"empty_column.domain_blocks": "Nun hai nengún dominiu bloquiáu.",
|
||||||
"empty_column.explore_statuses": "Agora nun hai nada en tendencia. ¡Volvi equí dempués!",
|
"empty_column.explore_statuses": "Agora nun hai nada en tendencia. ¡Volvi equí dempués!",
|
||||||
|
@ -223,21 +224,21 @@
|
||||||
"explore.trending_links": "Noticies",
|
"explore.trending_links": "Noticies",
|
||||||
"explore.trending_statuses": "Artículos",
|
"explore.trending_statuses": "Artículos",
|
||||||
"explore.trending_tags": "Etiquetes",
|
"explore.trending_tags": "Etiquetes",
|
||||||
"filter_modal.added.context_mismatch_explanation": "Esta categoría de peñera nun s'aplica al contestu nel qu'accediesti a esti artículu. Si tamién quies que se peñere l'artículu nesti contestu, tienes d'editar la peñera.",
|
"filter_modal.added.context_mismatch_explanation": "Esta categoría de peñera nun s'aplica al contestu nel qu'accediesti a esta publicación. Si tamién quies que se peñere la publicación nesti contestu, tienes d'editar la peñera.",
|
||||||
"filter_modal.added.context_mismatch_title": "¡El contestu nun coincide!",
|
"filter_modal.added.context_mismatch_title": "¡El contestu nun coincide!",
|
||||||
"filter_modal.added.expired_explanation": "Esta categoría de peñera caducó, tienes de camudar la so data de caducidá p'aplicala.",
|
"filter_modal.added.expired_explanation": "Esta categoría de peñera caducó, tienes de camudar la so data de caducidá p'aplicala.",
|
||||||
"filter_modal.added.expired_title": "¡La peñera caducó!",
|
"filter_modal.added.expired_title": "¡La peñera caducó!",
|
||||||
"filter_modal.added.review_and_configure": "Pa revisar y configurar a fondu esta categoría de peñera, vete a la {settings_link}.",
|
"filter_modal.added.review_and_configure": "Pa revisar y configurar a fondu esta categoría de peñera, vete a la {settings_link}.",
|
||||||
"filter_modal.added.review_and_configure_title": "Configuración de la peñera",
|
"filter_modal.added.review_and_configure_title": "Configuración de la peñera",
|
||||||
"filter_modal.added.settings_link": "páxina de configuración",
|
"filter_modal.added.settings_link": "páxina de configuración",
|
||||||
"filter_modal.added.short_explanation": "Esti artículu amestóse a la categoría de peñera siguiente: {title}.",
|
"filter_modal.added.short_explanation": "Esta publicación amestóse a la categoría de peñera siguiente: {title}.",
|
||||||
"filter_modal.added.title": "¡Amestóse la peñera!",
|
"filter_modal.added.title": "¡Amestóse la peñera!",
|
||||||
"filter_modal.select_filter.expired": "caducó",
|
"filter_modal.select_filter.expired": "caducó",
|
||||||
"filter_modal.select_filter.prompt_new": "Categoría nueva: {name}",
|
"filter_modal.select_filter.prompt_new": "Categoría nueva: {name}",
|
||||||
"filter_modal.select_filter.search": "Buscar o crear",
|
"filter_modal.select_filter.search": "Buscar o crear",
|
||||||
"filter_modal.select_filter.subtitle": "Usa una categoría esistente o créala",
|
"filter_modal.select_filter.subtitle": "Usa una categoría esistente o créala",
|
||||||
"filter_modal.select_filter.title": "Peñerar esti artículu",
|
"filter_modal.select_filter.title": "Peñerar esta publicación",
|
||||||
"filter_modal.title.status": "Peñera d'un artículu",
|
"filter_modal.title.status": "Peñera d'una publicación",
|
||||||
"firehose.all": "Tolos sirvidores",
|
"firehose.all": "Tolos sirvidores",
|
||||||
"firehose.local": "Esti sirvidor",
|
"firehose.local": "Esti sirvidor",
|
||||||
"firehose.remote": "Otros sirvidores",
|
"firehose.remote": "Otros sirvidores",
|
||||||
|
@ -285,20 +286,20 @@
|
||||||
"interaction_modal.on_another_server": "N'otru sirvidor",
|
"interaction_modal.on_another_server": "N'otru sirvidor",
|
||||||
"interaction_modal.on_this_server": "Nesti sirvidor",
|
"interaction_modal.on_this_server": "Nesti sirvidor",
|
||||||
"interaction_modal.title.follow": "Siguir a {name}",
|
"interaction_modal.title.follow": "Siguir a {name}",
|
||||||
"interaction_modal.title.reply": "Rempuesta al artículu de: {name}",
|
"interaction_modal.title.reply": "Rempuesta a la publicación de: {name}",
|
||||||
"interaction_modal.title.vote": "Vota na encuesta de {name}",
|
"interaction_modal.title.vote": "Vota na encuesta de {name}",
|
||||||
"intervals.full.days": "{number, plural, one {# día} other {# díes}}",
|
"intervals.full.days": "{number, plural, one {# día} other {# díes}}",
|
||||||
"intervals.full.hours": "{number, plural, one {# hora} other {# hores}}",
|
"intervals.full.hours": "{number, plural, one {# hora} other {# hores}}",
|
||||||
"intervals.full.minutes": "{number, plural, one {# minutu} other {# minutos}}",
|
"intervals.full.minutes": "{number, plural, one {# minutu} other {# minutos}}",
|
||||||
"keyboard_shortcuts.back": "Dir p'atrás",
|
"keyboard_shortcuts.back": "Dir p'atrás",
|
||||||
"keyboard_shortcuts.blocked": "Abrir la llista de perfiles bloquiaos",
|
"keyboard_shortcuts.blocked": "Abrir la llista de perfiles bloquiaos",
|
||||||
"keyboard_shortcuts.boost": "Compartir un artículu",
|
"keyboard_shortcuts.boost": "Compartir una publicación",
|
||||||
"keyboard_shortcuts.column": "Enfocar una columna",
|
"keyboard_shortcuts.column": "Enfocar una columna",
|
||||||
"keyboard_shortcuts.compose": "Enfocar l'área de composición",
|
"keyboard_shortcuts.compose": "Enfocar l'área de composición",
|
||||||
"keyboard_shortcuts.description": "Descripción",
|
"keyboard_shortcuts.description": "Descripción",
|
||||||
"keyboard_shortcuts.direct": "p'abrir la columna de les menciones privaes",
|
"keyboard_shortcuts.direct": "p'abrir la columna de les menciones privaes",
|
||||||
"keyboard_shortcuts.down": "Baxar na llista",
|
"keyboard_shortcuts.down": "Baxar na llista",
|
||||||
"keyboard_shortcuts.enter": "Abrir un artículu",
|
"keyboard_shortcuts.enter": "Abrir una publicación",
|
||||||
"keyboard_shortcuts.federated": "Abrir la llinia de tiempu federada",
|
"keyboard_shortcuts.federated": "Abrir la llinia de tiempu federada",
|
||||||
"keyboard_shortcuts.heading": "Atayos del tecláu",
|
"keyboard_shortcuts.heading": "Atayos del tecláu",
|
||||||
"keyboard_shortcuts.home": "Abrir la llinia de tiempu del aniciu",
|
"keyboard_shortcuts.home": "Abrir la llinia de tiempu del aniciu",
|
||||||
|
@ -312,12 +313,12 @@
|
||||||
"keyboard_shortcuts.open_media": "Abrir el conteníu mutimedia",
|
"keyboard_shortcuts.open_media": "Abrir el conteníu mutimedia",
|
||||||
"keyboard_shortcuts.pinned": "Abrir la llista d'artículos fixaos",
|
"keyboard_shortcuts.pinned": "Abrir la llista d'artículos fixaos",
|
||||||
"keyboard_shortcuts.profile": "Abrir el perfil del autor/a",
|
"keyboard_shortcuts.profile": "Abrir el perfil del autor/a",
|
||||||
"keyboard_shortcuts.reply": "Responder a un artículu",
|
"keyboard_shortcuts.reply": "Responder a una publicación",
|
||||||
"keyboard_shortcuts.requests": "Abrir la llista de solicitúes de siguimientu",
|
"keyboard_shortcuts.requests": "Abrir la llista de solicitúes de siguimientu",
|
||||||
"keyboard_shortcuts.search": "Enfocar la barra de busca",
|
"keyboard_shortcuts.search": "Enfocar la barra de busca",
|
||||||
"keyboard_shortcuts.start": "Abrir la columna «Entamar»",
|
"keyboard_shortcuts.start": "Abrir la columna «Entamar»",
|
||||||
"keyboard_shortcuts.toggle_sensitivity": "Amosar/esconder el conteníu multimedia",
|
"keyboard_shortcuts.toggle_sensitivity": "Amosar/esconder el conteníu multimedia",
|
||||||
"keyboard_shortcuts.toot": "Comenzar un artículu nuevu",
|
"keyboard_shortcuts.toot": "Escribir una publicación nueva",
|
||||||
"keyboard_shortcuts.unfocus": "Desenfocar l'área de composición/busca",
|
"keyboard_shortcuts.unfocus": "Desenfocar l'área de composición/busca",
|
||||||
"keyboard_shortcuts.up": "Xubir na llista",
|
"keyboard_shortcuts.up": "Xubir na llista",
|
||||||
"lightbox.close": "Zarrar",
|
"lightbox.close": "Zarrar",
|
||||||
|
@ -377,9 +378,9 @@
|
||||||
"notification.mentioned_you": "{name} mentóte",
|
"notification.mentioned_you": "{name} mentóte",
|
||||||
"notification.moderation-warning.learn_more": "Deprender más",
|
"notification.moderation-warning.learn_more": "Deprender más",
|
||||||
"notification.poll": "Finó una encuesta na que votesti",
|
"notification.poll": "Finó una encuesta na que votesti",
|
||||||
"notification.reblog": "{name} compartió'l to artículu",
|
"notification.reblog": "{name} compartió la to publicación",
|
||||||
"notification.status": "{name} ta acabante d'espublizar",
|
"notification.status": "{name} ta acabante d'espublizar",
|
||||||
"notification.update": "{name} editó un artículu",
|
"notification.update": "{name} editó una publicación",
|
||||||
"notification_requests.edit_selection": "Editar",
|
"notification_requests.edit_selection": "Editar",
|
||||||
"notification_requests.exit_selection": "Fecho",
|
"notification_requests.exit_selection": "Fecho",
|
||||||
"notifications.clear": "Borrar los avisos",
|
"notifications.clear": "Borrar los avisos",
|
||||||
|
@ -421,10 +422,10 @@
|
||||||
"poll.votes": "{votes, plural, one {# votu} other {# votos}}",
|
"poll.votes": "{votes, plural, one {# votu} other {# votos}}",
|
||||||
"poll_button.add_poll": "Amestar una encuesta",
|
"poll_button.add_poll": "Amestar una encuesta",
|
||||||
"poll_button.remove_poll": "Quitar la encuesta",
|
"poll_button.remove_poll": "Quitar la encuesta",
|
||||||
"privacy.change": "Configurar la privacidá del artículu",
|
"privacy.change": "Configurar la privacidá de la publicación",
|
||||||
"privacy.direct.short": "Perfiles específicos",
|
"privacy.direct.short": "Mención privada",
|
||||||
"privacy.private.short": "Siguidores",
|
"privacy.private.short": "Siguidores",
|
||||||
"privacy.public.short": "Artículu públicu",
|
"privacy.public.short": "Publicación pública",
|
||||||
"privacy_policy.last_updated": "Data del últimu anovamientu: {date}",
|
"privacy_policy.last_updated": "Data del últimu anovamientu: {date}",
|
||||||
"privacy_policy.title": "Política de privacidá",
|
"privacy_policy.title": "Política de privacidá",
|
||||||
"refresh": "Anovar",
|
"refresh": "Anovar",
|
||||||
|
@ -448,7 +449,7 @@
|
||||||
"report.category.subtitle": "Escueyi la meyor opción",
|
"report.category.subtitle": "Escueyi la meyor opción",
|
||||||
"report.category.title": "Dinos qué pasa con esti {type}",
|
"report.category.title": "Dinos qué pasa con esti {type}",
|
||||||
"report.category.title_account": "perfil",
|
"report.category.title_account": "perfil",
|
||||||
"report.category.title_status": "artículu",
|
"report.category.title_status": "publicación",
|
||||||
"report.close": "Fecho",
|
"report.close": "Fecho",
|
||||||
"report.comment.title": "¿Hai daqué más qu'habríemos saber?",
|
"report.comment.title": "¿Hai daqué más qu'habríemos saber?",
|
||||||
"report.forward": "Reunviar a {target}",
|
"report.forward": "Reunviar a {target}",
|
||||||
|
@ -468,7 +469,7 @@
|
||||||
"report.rules.subtitle": "Seleiciona tolo que s'axuste",
|
"report.rules.subtitle": "Seleiciona tolo que s'axuste",
|
||||||
"report.rules.title": "¿Qué normes s'incumplen?",
|
"report.rules.title": "¿Qué normes s'incumplen?",
|
||||||
"report.statuses.subtitle": "Seleiciona tolo que s'axuste",
|
"report.statuses.subtitle": "Seleiciona tolo que s'axuste",
|
||||||
"report.statuses.title": "¿Hai dalgún artículu qu'apoye esti informe?",
|
"report.statuses.title": "¿Hai dalguna publicación qu'apoye esti informe?",
|
||||||
"report.submit": "Unviar",
|
"report.submit": "Unviar",
|
||||||
"report.target": "Informe de: {target}",
|
"report.target": "Informe de: {target}",
|
||||||
"report.thanks.take_action": "Equí tienes les opciones pa controlar qué ves en Mastodon:",
|
"report.thanks.take_action": "Equí tienes les opciones pa controlar qué ves en Mastodon:",
|
||||||
|
@ -477,7 +478,7 @@
|
||||||
"report.thanks.title_actionable": "Gracies pol informe, el casu yá ta n'investigación.",
|
"report.thanks.title_actionable": "Gracies pol informe, el casu yá ta n'investigación.",
|
||||||
"report.unfollow": "Dexar de siguir a @{name}",
|
"report.unfollow": "Dexar de siguir a @{name}",
|
||||||
"report.unfollow_explanation": "Sigues a esta cuenta. Pa dexar de ver los sos artículos nel to feed d'aniciu, dexa de siguila.",
|
"report.unfollow_explanation": "Sigues a esta cuenta. Pa dexar de ver los sos artículos nel to feed d'aniciu, dexa de siguila.",
|
||||||
"report_notification.attached_statuses": "{count, plural, one {Axuntóse {count} artículu} other {Axuntáronse {count} artículos}}",
|
"report_notification.attached_statuses": "{count, plural, one {Axuntóse {count} publicación} other {Axuntáronse {count} publicaciones}}",
|
||||||
"report_notification.categories.legal": "Llegal",
|
"report_notification.categories.legal": "Llegal",
|
||||||
"report_notification.categories.legal_sentence": "conteníu illegal",
|
"report_notification.categories.legal_sentence": "conteníu illegal",
|
||||||
"report_notification.categories.spam": "Spam",
|
"report_notification.categories.spam": "Spam",
|
||||||
|
@ -490,6 +491,7 @@
|
||||||
"search.quick_action.go_to_hashtag": "Dir a la etiqueta {x}",
|
"search.quick_action.go_to_hashtag": "Dir a la etiqueta {x}",
|
||||||
"search.quick_action.status_search": "Artículos que concasen con {x}",
|
"search.quick_action.status_search": "Artículos que concasen con {x}",
|
||||||
"search.search_or_paste": "Busca o apiega una URL",
|
"search.search_or_paste": "Busca o apiega una URL",
|
||||||
|
"search_popout.full_text_search_disabled_message": "Nun ta disponible nel dominiu {domain}.",
|
||||||
"search_popout.language_code": "códigu de llingua ISO",
|
"search_popout.language_code": "códigu de llingua ISO",
|
||||||
"search_popout.options": "Opciones de busca",
|
"search_popout.options": "Opciones de busca",
|
||||||
"search_popout.quick_actions": "Aiciones rápides",
|
"search_popout.quick_actions": "Aiciones rápides",
|
||||||
|
@ -501,22 +503,25 @@
|
||||||
"search_results.hashtags": "Etiquetes",
|
"search_results.hashtags": "Etiquetes",
|
||||||
"search_results.see_all": "Ver too",
|
"search_results.see_all": "Ver too",
|
||||||
"search_results.statuses": "Artículos",
|
"search_results.statuses": "Artículos",
|
||||||
|
"server_banner.is_one_of_many": "{domain} ye unu de los munchos sirvidores independientes de Mastodon que pues usar pa participar nel fediversu.",
|
||||||
"server_banner.server_stats": "Estadístiques del sirvidor:",
|
"server_banner.server_stats": "Estadístiques del sirvidor:",
|
||||||
"sign_in_banner.create_account": "Crear una cuenta",
|
"sign_in_banner.create_account": "Crear una cuenta",
|
||||||
|
"sign_in_banner.mastodon_is": "Mastodon ye la meyor manera de siguir al momentu qué pasa.",
|
||||||
|
"sign_in_banner.sign_in": "Aniciar la sesión",
|
||||||
"sign_in_banner.sso_redirect": "Aniciar la sesión o rexistrase",
|
"sign_in_banner.sso_redirect": "Aniciar la sesión o rexistrase",
|
||||||
"status.admin_account": "Abrir la interfaz de moderación pa @{name}",
|
"status.admin_account": "Abrir la interfaz de moderación pa @{name}",
|
||||||
"status.admin_domain": "Abrir la interfaz de moderación pa «{domain}»",
|
"status.admin_domain": "Abrir la interfaz de moderación pa «{domain}»",
|
||||||
"status.admin_status": "Abrir esti artículu na interfaz de moderación",
|
"status.admin_status": "Abrir esta publicación na interfaz de moderación",
|
||||||
"status.block": "Bloquiar a @{name}",
|
"status.block": "Bloquiar a @{name}",
|
||||||
"status.bookmark": "Meter en Marcadores",
|
"status.bookmark": "Meter en Marcadores",
|
||||||
"status.cannot_reblog": "Esti artículu nun se pue compartir",
|
"status.cannot_reblog": "Esta publicación nun se pue compartir",
|
||||||
"status.copy": "Copiar l'enllaz al artículu",
|
"status.copy": "Copiar l'enllaz a la publicación",
|
||||||
"status.delete": "Desaniciar",
|
"status.delete": "Desaniciar",
|
||||||
"status.direct": "Mentar a @{name} per privao",
|
"status.direct": "Mentar a @{name} per privao",
|
||||||
"status.direct_indicator": "Mención privada",
|
"status.direct_indicator": "Mención privada",
|
||||||
"status.edited_x_times": "Editóse {count, plural, one {{count} vegada} other {{count} vegaes}}",
|
"status.edited_x_times": "Editóse {count, plural, one {{count} vegada} other {{count} vegaes}}",
|
||||||
"status.embed": "Consiguir el códigu pa empotrar",
|
"status.embed": "Consiguir el códigu pa empotrar",
|
||||||
"status.filter": "Peñerar esti artículu",
|
"status.filter": "Peñerar esta publicación",
|
||||||
"status.history.created": "{name} creó {date}",
|
"status.history.created": "{name} creó {date}",
|
||||||
"status.history.edited": "{name} editó {date}",
|
"status.history.edited": "{name} editó {date}",
|
||||||
"status.load_more": "Cargar más",
|
"status.load_more": "Cargar más",
|
||||||
|
@ -525,13 +530,13 @@
|
||||||
"status.more": "Más",
|
"status.more": "Más",
|
||||||
"status.mute": "Desactivar los avisos de @{name}",
|
"status.mute": "Desactivar los avisos de @{name}",
|
||||||
"status.mute_conversation": "Desactivar los avisos de la conversación",
|
"status.mute_conversation": "Desactivar los avisos de la conversación",
|
||||||
"status.open": "Espander esti artículu",
|
"status.open": "Espander esta publicación",
|
||||||
"status.pin": "Fixar nel perfil",
|
"status.pin": "Fixar nel perfil",
|
||||||
"status.pinned": "Artículu fixáu",
|
"status.pinned": "Publicación fixada",
|
||||||
"status.read_more": "Lleer más",
|
"status.read_more": "Lleer más",
|
||||||
"status.reblog": "Compartir",
|
"status.reblog": "Compartir",
|
||||||
"status.reblogged_by": "{name} compartió",
|
"status.reblogged_by": "{name} compartió",
|
||||||
"status.reblogs.empty": "Naide nun compartió esti artículu. Cuando daquién lo faiga, apaez equí.",
|
"status.reblogs.empty": "Naide nun compartió esta publicación. Cuando daquién lo faiga, va apaecer equí.",
|
||||||
"status.redraft": "Desaniciar y reeditar",
|
"status.redraft": "Desaniciar y reeditar",
|
||||||
"status.remove_bookmark": "Desaniciar el marcador",
|
"status.remove_bookmark": "Desaniciar el marcador",
|
||||||
"status.replied_to": "En rempuesta a {name}",
|
"status.replied_to": "En rempuesta a {name}",
|
||||||
|
|
|
@ -642,7 +642,6 @@
|
||||||
"poll_button.remove_poll": "Выдаліць апытанне",
|
"poll_button.remove_poll": "Выдаліць апытанне",
|
||||||
"privacy.change": "Змяніць прыватнасць допісу",
|
"privacy.change": "Змяніць прыватнасць допісу",
|
||||||
"privacy.direct.long": "Усе згаданыя ў допісе",
|
"privacy.direct.long": "Усе згаданыя ў допісе",
|
||||||
"privacy.direct.short": "Канкрэтныя людзі",
|
|
||||||
"privacy.private.long": "Толькі вашыя падпісчыкі",
|
"privacy.private.long": "Толькі вашыя падпісчыкі",
|
||||||
"privacy.private.short": "Падпісчыкі",
|
"privacy.private.short": "Падпісчыкі",
|
||||||
"privacy.public.long": "Усе, хто ёсць і каго няма ў Mastodon",
|
"privacy.public.long": "Усе, хто ёсць і каго няма ў Mastodon",
|
||||||
|
|
|
@ -697,7 +697,7 @@
|
||||||
"poll_button.remove_poll": "Премахване на анкета",
|
"poll_button.remove_poll": "Премахване на анкета",
|
||||||
"privacy.change": "Промяна на поверителността на публикация",
|
"privacy.change": "Промяна на поверителността на публикация",
|
||||||
"privacy.direct.long": "Споменатите в публикацията",
|
"privacy.direct.long": "Споменатите в публикацията",
|
||||||
"privacy.direct.short": "Определени хора",
|
"privacy.direct.short": "Частно споменаване",
|
||||||
"privacy.private.long": "Само последователите ви",
|
"privacy.private.long": "Само последователите ви",
|
||||||
"privacy.private.short": "Последователи",
|
"privacy.private.short": "Последователи",
|
||||||
"privacy.public.long": "Всеки във и извън Mastodon",
|
"privacy.public.long": "Всеки във и извън Mastodon",
|
||||||
|
|
|
@ -436,7 +436,6 @@
|
||||||
"poll_button.add_poll": "Ouzhpennañ ur sontadeg",
|
"poll_button.add_poll": "Ouzhpennañ ur sontadeg",
|
||||||
"poll_button.remove_poll": "Dilemel ar sontadeg",
|
"poll_button.remove_poll": "Dilemel ar sontadeg",
|
||||||
"privacy.change": "Cheñch prevezded an embannadur",
|
"privacy.change": "Cheñch prevezded an embannadur",
|
||||||
"privacy.direct.short": "Tud resis",
|
|
||||||
"privacy.private.short": "Heulierien",
|
"privacy.private.short": "Heulierien",
|
||||||
"privacy.public.short": "Publik",
|
"privacy.public.short": "Publik",
|
||||||
"privacy_policy.last_updated": "Hizivadenn ziwezhañ {date}",
|
"privacy_policy.last_updated": "Hizivadenn ziwezhañ {date}",
|
||||||
|
|
|
@ -696,7 +696,7 @@
|
||||||
"poll_button.remove_poll": "Elimina l'enquesta",
|
"poll_button.remove_poll": "Elimina l'enquesta",
|
||||||
"privacy.change": "Canvia la privacitat del tut",
|
"privacy.change": "Canvia la privacitat del tut",
|
||||||
"privacy.direct.long": "Tothom mencionat a la publicació",
|
"privacy.direct.long": "Tothom mencionat a la publicació",
|
||||||
"privacy.direct.short": "Persones concretes",
|
"privacy.direct.short": "Menció privada",
|
||||||
"privacy.private.long": "Només els vostres seguidors",
|
"privacy.private.long": "Només els vostres seguidors",
|
||||||
"privacy.private.short": "Seguidors",
|
"privacy.private.short": "Seguidors",
|
||||||
"privacy.public.long": "Tothom dins o fora Mastodon",
|
"privacy.public.long": "Tothom dins o fora Mastodon",
|
||||||
|
|
|
@ -697,7 +697,7 @@
|
||||||
"poll_button.remove_poll": "Odebrat anketu",
|
"poll_button.remove_poll": "Odebrat anketu",
|
||||||
"privacy.change": "Změnit soukromí příspěvku",
|
"privacy.change": "Změnit soukromí příspěvku",
|
||||||
"privacy.direct.long": "Všichni zmínění v příspěvku",
|
"privacy.direct.long": "Všichni zmínění v příspěvku",
|
||||||
"privacy.direct.short": "Vybraní lidé",
|
"privacy.direct.short": "Soukromá zmínka",
|
||||||
"privacy.private.long": "Jen vaši sledující",
|
"privacy.private.long": "Jen vaši sledující",
|
||||||
"privacy.private.short": "Sledující",
|
"privacy.private.short": "Sledující",
|
||||||
"privacy.public.long": "Kdokoliv na Mastodonu i mimo něj",
|
"privacy.public.long": "Kdokoliv na Mastodonu i mimo něj",
|
||||||
|
|
|
@ -696,7 +696,6 @@
|
||||||
"poll_button.remove_poll": "Tynnu pleidlais",
|
"poll_button.remove_poll": "Tynnu pleidlais",
|
||||||
"privacy.change": "Addasu preifatrwdd y post",
|
"privacy.change": "Addasu preifatrwdd y post",
|
||||||
"privacy.direct.long": "Pawb sydd â sôn amdanyn nhw yn y postiad",
|
"privacy.direct.long": "Pawb sydd â sôn amdanyn nhw yn y postiad",
|
||||||
"privacy.direct.short": "Pobl benodol",
|
|
||||||
"privacy.private.long": "Eich dilynwyr yn unig",
|
"privacy.private.long": "Eich dilynwyr yn unig",
|
||||||
"privacy.private.short": "Dilynwyr",
|
"privacy.private.short": "Dilynwyr",
|
||||||
"privacy.public.long": "Unrhyw ar ac oddi ar Mastodon",
|
"privacy.public.long": "Unrhyw ar ac oddi ar Mastodon",
|
||||||
|
|
|
@ -697,12 +697,12 @@
|
||||||
"poll_button.remove_poll": "Fjern afstemning",
|
"poll_button.remove_poll": "Fjern afstemning",
|
||||||
"privacy.change": "Tilpas indlægsfortrolighed",
|
"privacy.change": "Tilpas indlægsfortrolighed",
|
||||||
"privacy.direct.long": "Alle omtalt i indlægget",
|
"privacy.direct.long": "Alle omtalt i indlægget",
|
||||||
"privacy.direct.short": "Bestemte personer",
|
"privacy.direct.short": "Privat omtale",
|
||||||
"privacy.private.long": "Kun dine følgere",
|
"privacy.private.long": "Kun dine følgere",
|
||||||
"privacy.private.short": "Følgere",
|
"privacy.private.short": "Følgere",
|
||||||
"privacy.public.long": "Alle på og udenfor Mastodon",
|
"privacy.public.long": "Alle på og udenfor Mastodon",
|
||||||
"privacy.public.short": "Offentlig",
|
"privacy.public.short": "Offentlig",
|
||||||
"privacy.unlisted.additional": "Dette er præcis som offentlig adfærd, dog vises indlægget ikke i realtids-strømme/etiketter, udforsk eller Mastodon-søgning, selv hvis valget gælder hele kontoen.",
|
"privacy.unlisted.additional": "Dette er præcis som offentlig adfærd, dog vises indlægget ikke i tidslinjer, under etiketter, udforsk eller Mastodon-søgning, selv hvis du ellers har sagt at dine opslag godt må være søgbare.",
|
||||||
"privacy.unlisted.long": "Færre algoritmiske fanfarer",
|
"privacy.unlisted.long": "Færre algoritmiske fanfarer",
|
||||||
"privacy.unlisted.short": "Stille offentligt",
|
"privacy.unlisted.short": "Stille offentligt",
|
||||||
"privacy_policy.last_updated": "Senest opdateret {date}",
|
"privacy_policy.last_updated": "Senest opdateret {date}",
|
||||||
|
|
|
@ -697,7 +697,7 @@
|
||||||
"poll_button.remove_poll": "Umfrage entfernen",
|
"poll_button.remove_poll": "Umfrage entfernen",
|
||||||
"privacy.change": "Sichtbarkeit anpassen",
|
"privacy.change": "Sichtbarkeit anpassen",
|
||||||
"privacy.direct.long": "Alle in diesem Beitrag erwähnten Profile",
|
"privacy.direct.long": "Alle in diesem Beitrag erwähnten Profile",
|
||||||
"privacy.direct.short": "Ausgewählte Profile",
|
"privacy.direct.short": "Private Erwähnung",
|
||||||
"privacy.private.long": "Nur deine Follower",
|
"privacy.private.long": "Nur deine Follower",
|
||||||
"privacy.private.short": "Follower",
|
"privacy.private.short": "Follower",
|
||||||
"privacy.public.long": "Alle in und außerhalb von Mastodon",
|
"privacy.public.long": "Alle in und außerhalb von Mastodon",
|
||||||
|
|
|
@ -697,7 +697,7 @@
|
||||||
"poll_button.remove_poll": "Αφαίρεση δημοσκόπησης",
|
"poll_button.remove_poll": "Αφαίρεση δημοσκόπησης",
|
||||||
"privacy.change": "Προσαρμογή ιδιωτικότητας ανάρτησης",
|
"privacy.change": "Προσαρμογή ιδιωτικότητας ανάρτησης",
|
||||||
"privacy.direct.long": "Όλοι όσοι αναφέρθηκαν στη δημοσίευση",
|
"privacy.direct.long": "Όλοι όσοι αναφέρθηκαν στη δημοσίευση",
|
||||||
"privacy.direct.short": "Συγκεκριμένα άτομα",
|
"privacy.direct.short": "Ιδιωτική επισήμανση",
|
||||||
"privacy.private.long": "Μόνο οι ακόλουθοί σας",
|
"privacy.private.long": "Μόνο οι ακόλουθοί σας",
|
||||||
"privacy.private.short": "Ακόλουθοι",
|
"privacy.private.short": "Ακόλουθοι",
|
||||||
"privacy.public.long": "Όλοι εντός και εκτός του Mastodon",
|
"privacy.public.long": "Όλοι εντός και εκτός του Mastodon",
|
||||||
|
|
|
@ -697,7 +697,7 @@
|
||||||
"poll_button.remove_poll": "Remove poll",
|
"poll_button.remove_poll": "Remove poll",
|
||||||
"privacy.change": "Change post privacy",
|
"privacy.change": "Change post privacy",
|
||||||
"privacy.direct.long": "Everyone mentioned in the post",
|
"privacy.direct.long": "Everyone mentioned in the post",
|
||||||
"privacy.direct.short": "Specific people",
|
"privacy.direct.short": "Private mention",
|
||||||
"privacy.private.long": "Only your followers",
|
"privacy.private.long": "Only your followers",
|
||||||
"privacy.private.short": "Followers",
|
"privacy.private.short": "Followers",
|
||||||
"privacy.public.long": "Anyone on and off Mastodon",
|
"privacy.public.long": "Anyone on and off Mastodon",
|
||||||
|
|
|
@ -697,7 +697,7 @@
|
||||||
"poll_button.remove_poll": "Remove poll",
|
"poll_button.remove_poll": "Remove poll",
|
||||||
"privacy.change": "Change post privacy",
|
"privacy.change": "Change post privacy",
|
||||||
"privacy.direct.long": "Everyone mentioned in the post",
|
"privacy.direct.long": "Everyone mentioned in the post",
|
||||||
"privacy.direct.short": "Specific people",
|
"privacy.direct.short": "Private mention",
|
||||||
"privacy.private.long": "Only your followers",
|
"privacy.private.long": "Only your followers",
|
||||||
"privacy.private.short": "Followers",
|
"privacy.private.short": "Followers",
|
||||||
"privacy.public.long": "Anyone on and off Mastodon",
|
"privacy.public.long": "Anyone on and off Mastodon",
|
||||||
|
@ -872,7 +872,9 @@
|
||||||
"subscribed_languages.target": "Change subscribed languages for {target}",
|
"subscribed_languages.target": "Change subscribed languages for {target}",
|
||||||
"tabs_bar.home": "Home",
|
"tabs_bar.home": "Home",
|
||||||
"tabs_bar.notifications": "Notifications",
|
"tabs_bar.notifications": "Notifications",
|
||||||
|
"terms_of_service.effective_as_of": "Effective as of {date}",
|
||||||
"terms_of_service.title": "Terms of Service",
|
"terms_of_service.title": "Terms of Service",
|
||||||
|
"terms_of_service.upcoming_changes_on": "Upcoming changes on {date}",
|
||||||
"time_remaining.days": "{number, plural, one {# day} other {# days}} left",
|
"time_remaining.days": "{number, plural, one {# day} other {# days}} left",
|
||||||
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
|
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
|
||||||
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
|
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
|
||||||
|
|
|
@ -697,7 +697,7 @@
|
||||||
"poll_button.remove_poll": "Forigi balotenketon",
|
"poll_button.remove_poll": "Forigi balotenketon",
|
||||||
"privacy.change": "Ŝanĝu afiŝan privatecon",
|
"privacy.change": "Ŝanĝu afiŝan privatecon",
|
||||||
"privacy.direct.long": "Ĉiuj menciitaj en la afiŝo",
|
"privacy.direct.long": "Ĉiuj menciitaj en la afiŝo",
|
||||||
"privacy.direct.short": "Specifaj homoj",
|
"privacy.direct.short": "Privata mencio",
|
||||||
"privacy.private.long": "Nur viaj sekvantoj",
|
"privacy.private.long": "Nur viaj sekvantoj",
|
||||||
"privacy.private.short": "Sekvantoj",
|
"privacy.private.short": "Sekvantoj",
|
||||||
"privacy.public.long": "Ĉiujn ajn ĉe kaj ekster Mastodon",
|
"privacy.public.long": "Ĉiujn ajn ĉe kaj ekster Mastodon",
|
||||||
|
|
|
@ -697,7 +697,7 @@
|
||||||
"poll_button.remove_poll": "Quitar encuesta",
|
"poll_button.remove_poll": "Quitar encuesta",
|
||||||
"privacy.change": "Configurar privacidad del mensaje",
|
"privacy.change": "Configurar privacidad del mensaje",
|
||||||
"privacy.direct.long": "Todas las cuentas mencionadas en el mensaje",
|
"privacy.direct.long": "Todas las cuentas mencionadas en el mensaje",
|
||||||
"privacy.direct.short": "Cuentas específicas",
|
"privacy.direct.short": "Mención privada",
|
||||||
"privacy.private.long": "Solo tus seguidores",
|
"privacy.private.long": "Solo tus seguidores",
|
||||||
"privacy.private.short": "Seguidores",
|
"privacy.private.short": "Seguidores",
|
||||||
"privacy.public.long": "Cualquier persona dentro y fuera de Mastodon",
|
"privacy.public.long": "Cualquier persona dentro y fuera de Mastodon",
|
||||||
|
|
|
@ -192,10 +192,10 @@
|
||||||
"compose_form.poll.switch_to_multiple": "Cambiar la encuesta para permitir múltiples opciones",
|
"compose_form.poll.switch_to_multiple": "Cambiar la encuesta para permitir múltiples opciones",
|
||||||
"compose_form.poll.switch_to_single": "Cambiar la encuesta para permitir una única opción",
|
"compose_form.poll.switch_to_single": "Cambiar la encuesta para permitir una única opción",
|
||||||
"compose_form.poll.type": "Estilo",
|
"compose_form.poll.type": "Estilo",
|
||||||
"compose_form.publish": "Publicación",
|
"compose_form.publish": "Publicar",
|
||||||
"compose_form.publish_form": "Nueva publicación",
|
"compose_form.publish_form": "Nueva publicación",
|
||||||
"compose_form.reply": "Respuesta",
|
"compose_form.reply": "Respuesta",
|
||||||
"compose_form.save_changes": "Actualización",
|
"compose_form.save_changes": "Actualizar",
|
||||||
"compose_form.spoiler.marked": "Quitar advertencia de contenido",
|
"compose_form.spoiler.marked": "Quitar advertencia de contenido",
|
||||||
"compose_form.spoiler.unmarked": "Añadir advertencia de contenido",
|
"compose_form.spoiler.unmarked": "Añadir advertencia de contenido",
|
||||||
"compose_form.spoiler_placeholder": "Advertencia de contenido (opcional)",
|
"compose_form.spoiler_placeholder": "Advertencia de contenido (opcional)",
|
||||||
|
@ -697,7 +697,7 @@
|
||||||
"poll_button.remove_poll": "Eliminar encuesta",
|
"poll_button.remove_poll": "Eliminar encuesta",
|
||||||
"privacy.change": "Ajustar privacidad",
|
"privacy.change": "Ajustar privacidad",
|
||||||
"privacy.direct.long": "Todos los mencionados en la publicación",
|
"privacy.direct.long": "Todos los mencionados en la publicación",
|
||||||
"privacy.direct.short": "Personas específicas",
|
"privacy.direct.short": "Mención privada",
|
||||||
"privacy.private.long": "Sólo tus seguidores",
|
"privacy.private.long": "Sólo tus seguidores",
|
||||||
"privacy.private.short": "Seguidores",
|
"privacy.private.short": "Seguidores",
|
||||||
"privacy.public.long": "Cualquiera dentro y fuera de Mastodon",
|
"privacy.public.long": "Cualquiera dentro y fuera de Mastodon",
|
||||||
|
|
|
@ -697,7 +697,7 @@
|
||||||
"poll_button.remove_poll": "Eliminar encuesta",
|
"poll_button.remove_poll": "Eliminar encuesta",
|
||||||
"privacy.change": "Ajustar privacidad",
|
"privacy.change": "Ajustar privacidad",
|
||||||
"privacy.direct.long": "Visible únicamente por los mencionados en la publicación",
|
"privacy.direct.long": "Visible únicamente por los mencionados en la publicación",
|
||||||
"privacy.direct.short": "Personas específicas",
|
"privacy.direct.short": "Mención privada",
|
||||||
"privacy.private.long": "Visible únicamente por tus seguidores",
|
"privacy.private.long": "Visible únicamente por tus seguidores",
|
||||||
"privacy.private.short": "Seguidores",
|
"privacy.private.short": "Seguidores",
|
||||||
"privacy.public.long": "Visible por todo el mundo, dentro y fuera de Mastodon",
|
"privacy.public.long": "Visible por todo el mundo, dentro y fuera de Mastodon",
|
||||||
|
|
|
@ -696,7 +696,6 @@
|
||||||
"poll_button.remove_poll": "Eemalda küsitlus",
|
"poll_button.remove_poll": "Eemalda küsitlus",
|
||||||
"privacy.change": "Muuda postituse nähtavust",
|
"privacy.change": "Muuda postituse nähtavust",
|
||||||
"privacy.direct.long": "Kõik postituses mainitud",
|
"privacy.direct.long": "Kõik postituses mainitud",
|
||||||
"privacy.direct.short": "Määratud kasutajad",
|
|
||||||
"privacy.private.long": "Ainult jälgijad",
|
"privacy.private.long": "Ainult jälgijad",
|
||||||
"privacy.private.short": "Jälgijad",
|
"privacy.private.short": "Jälgijad",
|
||||||
"privacy.public.long": "Nii kasutajad kui mittekasutajad",
|
"privacy.public.long": "Nii kasutajad kui mittekasutajad",
|
||||||
|
|
|
@ -644,7 +644,6 @@
|
||||||
"poll_button.remove_poll": "Kendu inkesta",
|
"poll_button.remove_poll": "Kendu inkesta",
|
||||||
"privacy.change": "Aldatu bidalketaren pribatutasuna",
|
"privacy.change": "Aldatu bidalketaren pribatutasuna",
|
||||||
"privacy.direct.long": "Argitalpen honetan aipatutako denak",
|
"privacy.direct.long": "Argitalpen honetan aipatutako denak",
|
||||||
"privacy.direct.short": "Jende jakina",
|
|
||||||
"privacy.private.long": "Soilik jarraitzaileak",
|
"privacy.private.long": "Soilik jarraitzaileak",
|
||||||
"privacy.private.short": "Jarraitzaileak",
|
"privacy.private.short": "Jarraitzaileak",
|
||||||
"privacy.public.long": "Mastodonen dagoen edo ez dagoen edonor",
|
"privacy.public.long": "Mastodonen dagoen edo ez dagoen edonor",
|
||||||
|
|
|
@ -218,6 +218,10 @@
|
||||||
"confirmations.logout.confirm": "خروج از حساب",
|
"confirmations.logout.confirm": "خروج از حساب",
|
||||||
"confirmations.logout.message": "مطمئنید میخواهید خارج شوید؟",
|
"confirmations.logout.message": "مطمئنید میخواهید خارج شوید؟",
|
||||||
"confirmations.logout.title": "خروج؟",
|
"confirmations.logout.title": "خروج؟",
|
||||||
|
"confirmations.missing_alt_text.confirm": "متن جایگزین را اضافه کنید",
|
||||||
|
"confirmations.missing_alt_text.message": "پست شما حاوی رسانه بدون متن جایگزین است. افزودن توضیحات کمک می کند تا محتوای شما برای افراد بیشتری قابل دسترسی باشد.",
|
||||||
|
"confirmations.missing_alt_text.secondary": "به هر حال پست کن",
|
||||||
|
"confirmations.missing_alt_text.title": "متن جایگزین اضافه شود؟",
|
||||||
"confirmations.mute.confirm": "خموش",
|
"confirmations.mute.confirm": "خموش",
|
||||||
"confirmations.redraft.confirm": "حذف و بازنویسی",
|
"confirmations.redraft.confirm": "حذف و بازنویسی",
|
||||||
"confirmations.redraft.message": "مطمئنید که میخواهید این فرسته را حذف کنید و از نو بنویسید؟ با این کار تقویتها و پسندهایش از دست رفته و پاسخها به آن بیمرجع میشود.",
|
"confirmations.redraft.message": "مطمئنید که میخواهید این فرسته را حذف کنید و از نو بنویسید؟ با این کار تقویتها و پسندهایش از دست رفته و پاسخها به آن بیمرجع میشود.",
|
||||||
|
@ -693,7 +697,7 @@
|
||||||
"poll_button.remove_poll": "برداشتن نظرسنجی",
|
"poll_button.remove_poll": "برداشتن نظرسنجی",
|
||||||
"privacy.change": "تغییر محرمانگی فرسته",
|
"privacy.change": "تغییر محرمانگی فرسته",
|
||||||
"privacy.direct.long": "هرکسی که در فرسته نام برده شده",
|
"privacy.direct.long": "هرکسی که در فرسته نام برده شده",
|
||||||
"privacy.direct.short": "افراد مشخّص",
|
"privacy.direct.short": "ذکر خصوصی",
|
||||||
"privacy.private.long": "تنها پیگیرندگانتان",
|
"privacy.private.long": "تنها پیگیرندگانتان",
|
||||||
"privacy.private.short": "پیگیرندگان",
|
"privacy.private.short": "پیگیرندگان",
|
||||||
"privacy.public.long": "هرکسی در و بیرون از ماستودون",
|
"privacy.public.long": "هرکسی در و بیرون از ماستودون",
|
||||||
|
|
|
@ -697,7 +697,7 @@
|
||||||
"poll_button.remove_poll": "Poista äänestys",
|
"poll_button.remove_poll": "Poista äänestys",
|
||||||
"privacy.change": "Muuta julkaisun näkyvyyttä",
|
"privacy.change": "Muuta julkaisun näkyvyyttä",
|
||||||
"privacy.direct.long": "Kaikki tässä julkaisussa mainitut",
|
"privacy.direct.long": "Kaikki tässä julkaisussa mainitut",
|
||||||
"privacy.direct.short": "Tietyt käyttäjät",
|
"privacy.direct.short": "Yksityismaininta",
|
||||||
"privacy.private.long": "Vain seuraajasi",
|
"privacy.private.long": "Vain seuraajasi",
|
||||||
"privacy.private.short": "Seuraajat",
|
"privacy.private.short": "Seuraajat",
|
||||||
"privacy.public.long": "Kuka tahansa Mastodonissa ja sen ulkopuolella",
|
"privacy.public.long": "Kuka tahansa Mastodonissa ja sen ulkopuolella",
|
||||||
|
|
|
@ -697,7 +697,7 @@
|
||||||
"poll_button.remove_poll": "Strika atkvøðugreiðslu",
|
"poll_button.remove_poll": "Strika atkvøðugreiðslu",
|
||||||
"privacy.change": "Broyt privatverju av posti",
|
"privacy.change": "Broyt privatverju av posti",
|
||||||
"privacy.direct.long": "Øll, sum eru nevnd í postinum",
|
"privacy.direct.long": "Øll, sum eru nevnd í postinum",
|
||||||
"privacy.direct.short": "Ávís fólk",
|
"privacy.direct.short": "Privat umrøða",
|
||||||
"privacy.private.long": "Einans tey, ið fylgja tær",
|
"privacy.private.long": "Einans tey, ið fylgja tær",
|
||||||
"privacy.private.short": "Fylgjarar",
|
"privacy.private.short": "Fylgjarar",
|
||||||
"privacy.public.long": "Øll í og uttanfyri Mastodon",
|
"privacy.public.long": "Øll í og uttanfyri Mastodon",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"about.blocks": "Serveurs modérés",
|
"about.blocks": "Serveurs modérés",
|
||||||
"about.contact": "Contact:",
|
"about.contact": "Contact :",
|
||||||
"about.disclaimer": "Mastodon est un logiciel open-source gratuit et une marque déposée de Mastodon gGmbH.",
|
"about.disclaimer": "Mastodon est un logiciel open-source gratuit et une marque déposée de Mastodon gGmbH.",
|
||||||
"about.domain_blocks.no_reason_available": "Raison non disponible",
|
"about.domain_blocks.no_reason_available": "Raison non disponible",
|
||||||
"about.domain_blocks.preamble": "Mastodon vous permet généralement de visualiser le contenu et d'interagir avec des comptes de n'importe quel serveur dans le fediverse. Voici les exceptions qui ont été faites sur ce serveur en particulier.",
|
"about.domain_blocks.preamble": "Mastodon vous permet généralement de visualiser le contenu et d'interagir avec des comptes de n'importe quel serveur dans le fediverse. Voici les exceptions qui ont été faites sur ce serveur en particulier.",
|
||||||
|
@ -697,7 +697,7 @@
|
||||||
"poll_button.remove_poll": "Supprimer le sondage",
|
"poll_button.remove_poll": "Supprimer le sondage",
|
||||||
"privacy.change": "Changer la confidentialité des messages",
|
"privacy.change": "Changer la confidentialité des messages",
|
||||||
"privacy.direct.long": "Toutes les personnes mentionnées dans le post",
|
"privacy.direct.long": "Toutes les personnes mentionnées dans le post",
|
||||||
"privacy.direct.short": "Personnes spécifiques",
|
"privacy.direct.short": "Mention privée",
|
||||||
"privacy.private.long": "Seulement vos abonnés",
|
"privacy.private.long": "Seulement vos abonnés",
|
||||||
"privacy.private.short": "Abonnés",
|
"privacy.private.short": "Abonnés",
|
||||||
"privacy.public.long": "Tout le monde sur et en dehors de Mastodon",
|
"privacy.public.long": "Tout le monde sur et en dehors de Mastodon",
|
||||||
|
|
|
@ -697,7 +697,7 @@
|
||||||
"poll_button.remove_poll": "Supprimer le sondage",
|
"poll_button.remove_poll": "Supprimer le sondage",
|
||||||
"privacy.change": "Ajuster la confidentialité du message",
|
"privacy.change": "Ajuster la confidentialité du message",
|
||||||
"privacy.direct.long": "Toutes les personnes mentionnées dans le post",
|
"privacy.direct.long": "Toutes les personnes mentionnées dans le post",
|
||||||
"privacy.direct.short": "Personnes spécifiques",
|
"privacy.direct.short": "Mention privée",
|
||||||
"privacy.private.long": "Seulement vos abonnés",
|
"privacy.private.long": "Seulement vos abonnés",
|
||||||
"privacy.private.short": "Abonnés",
|
"privacy.private.short": "Abonnés",
|
||||||
"privacy.public.long": "Tout le monde sur et en dehors de Mastodon",
|
"privacy.public.long": "Tout le monde sur et en dehors de Mastodon",
|
||||||
|
|
|
@ -682,7 +682,6 @@
|
||||||
"poll_button.remove_poll": "Enkête fuortsmite",
|
"poll_button.remove_poll": "Enkête fuortsmite",
|
||||||
"privacy.change": "Sichtberheid fan berjocht oanpasse",
|
"privacy.change": "Sichtberheid fan berjocht oanpasse",
|
||||||
"privacy.direct.long": "Elkenien dy’ yn it berjocht fermeld wurdt",
|
"privacy.direct.long": "Elkenien dy’ yn it berjocht fermeld wurdt",
|
||||||
"privacy.direct.short": "Bepaalde minsken",
|
|
||||||
"privacy.private.long": "Allinnich jo folgers",
|
"privacy.private.long": "Allinnich jo folgers",
|
||||||
"privacy.private.short": "Folgers",
|
"privacy.private.short": "Folgers",
|
||||||
"privacy.public.long": "Elkenien op Mastodon en dêrbûten",
|
"privacy.public.long": "Elkenien op Mastodon en dêrbûten",
|
||||||
|
|
|
@ -697,7 +697,7 @@
|
||||||
"poll_button.remove_poll": "Bain suirbhé",
|
"poll_button.remove_poll": "Bain suirbhé",
|
||||||
"privacy.change": "Athraigh príobháideacht postála",
|
"privacy.change": "Athraigh príobháideacht postála",
|
||||||
"privacy.direct.long": "Luaigh gach duine sa phost",
|
"privacy.direct.long": "Luaigh gach duine sa phost",
|
||||||
"privacy.direct.short": "Daoine ar leith",
|
"privacy.direct.short": "Tagairt phríobháideach",
|
||||||
"privacy.private.long": "Do leanúna amháin",
|
"privacy.private.long": "Do leanúna amháin",
|
||||||
"privacy.private.short": "Leantóirí",
|
"privacy.private.short": "Leantóirí",
|
||||||
"privacy.public.long": "Duine ar bith ar agus amach Mastodon",
|
"privacy.public.long": "Duine ar bith ar agus amach Mastodon",
|
||||||
|
|
|
@ -633,7 +633,6 @@
|
||||||
"poll_button.remove_poll": "Thoir air falbh an cunntas-bheachd",
|
"poll_button.remove_poll": "Thoir air falbh an cunntas-bheachd",
|
||||||
"privacy.change": "Cuir gleus air prìobhaideachd a’ phuist",
|
"privacy.change": "Cuir gleus air prìobhaideachd a’ phuist",
|
||||||
"privacy.direct.long": "A h-uile duine air a bheil iomradh sa phost",
|
"privacy.direct.long": "A h-uile duine air a bheil iomradh sa phost",
|
||||||
"privacy.direct.short": "Daoine àraidh",
|
|
||||||
"privacy.private.long": "An luchd-leantainn agad a-mhàin",
|
"privacy.private.long": "An luchd-leantainn agad a-mhàin",
|
||||||
"privacy.private.short": "Luchd-leantainn",
|
"privacy.private.short": "Luchd-leantainn",
|
||||||
"privacy.public.long": "Duine sam bith taobh a-staigh no a-muigh Mhastodon",
|
"privacy.public.long": "Duine sam bith taobh a-staigh no a-muigh Mhastodon",
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user