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