diff --git a/.codeclimate.yml b/.codeclimate.yml
index 29701a777..47e3e6ab9 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -1,21 +1,36 @@
-engines:
+version: "2"
+checks:
+ argument-count:
+ enabled: false
+ complex-logic:
+ enabled: false
+ file-lines:
+ enabled: false
+ method-complexity:
+ enabled: false
+ method-count:
+ enabled: false
+ method-lines:
+ enabled: false
+ nested-control-flow:
+ enabled: false
+ return-statements:
+ enabled: false
+ similar-code:
+ enabled: false
+ identical-code:
+ enabled: false
+plugins:
brakeman:
enabled: true
bundler-audit:
enabled: true
- duplication:
- enabled: false
eslint:
enabled: true
rubocop:
enabled: true
scss-lint:
enabled: true
-ratings:
- paths:
- - "**.rb"
- - "**.js"
- - "**.scss"
-exclude_paths:
+exclude_patterns:
- spec/
- vendor/asset
diff --git a/.env.nanobox b/.env.nanobox
index 7920c47b9..48204a6bf 100644
--- a/.env.nanobox
+++ b/.env.nanobox
@@ -35,6 +35,17 @@ PAPERCLIP_SECRET=$PAPERCLIP_SECRET
SECRET_KEY_BASE=$SECRET_KEY_BASE
OTP_SECRET=$OTP_SECRET
+# VAPID keys (used for push notifications)
+# You can generate the keys using the following command (first is the private key, second is the public one)
+# You should only generate this once per instance. If you later decide to change it, all push subscription will
+# be invalidated, requiring the users to access the website again to resubscribe.
+#
+# Generate with `rake mastodon:webpush:generate_vapid_key` task (`nanobox run bundle exec rake mastodon:webpush:generate_vapid_key`)
+#
+# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html
+VAPID_PRIVATE_KEY=$VAPID_PRIVATE_KEY
+VAPID_PUBLIC_KEY=$VAPID_PUBLIC_KEY
+
# Registrations
# Single user mode will disable registrations and redirect frontpage to the first profile
# SINGLE_USER_MODE=true
@@ -62,7 +73,7 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
#SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt
#SMTP_OPENSSL_VERIFY_MODE=peer
#SMTP_ENABLE_STARTTLS_AUTO=true
-
+#SMTP_TLS=true
# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files.
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system
@@ -91,6 +102,23 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# S3_ENDPOINT=
# S3_SIGNATURE_VERSION=
+# Swift (optional)
+# SWIFT_ENABLED=true
+# SWIFT_USERNAME=
+# For Keystone V3, the value for SWIFT_TENANT should be the project name
+# SWIFT_TENANT=
+# SWIFT_PASSWORD=
+# Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid
+# issues with token rate-limiting during high load.
+# SWIFT_AUTH_URL=
+# SWIFT_CONTAINER=
+# SWIFT_OBJECT_URL=
+# SWIFT_REGION=
+# Defaults to 'default'
+# SWIFT_DOMAIN_NAME=
+# Defaults to 60 seconds. Set to 0 to disable
+# SWIFT_CACHE_TTL=
+
# Optional alias for S3 if you want to use Cloudfront or Cloudflare in front
# S3_CLOUDFRONT_HOST=
diff --git a/CODEOWNERS b/.github/CODEOWNERS
similarity index 100%
rename from CODEOWNERS
rename to .github/CODEOWNERS
diff --git a/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
similarity index 100%
rename from ISSUE_TEMPLATE.md
rename to .github/ISSUE_TEMPLATE.md
diff --git a/.travis.yml b/.travis.yml
index 5c2c2c889..777ca581c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -27,11 +27,14 @@ addons:
apt:
sources:
- trusty-media
+ - sourceline: deb https://dl.yarnpkg.com/debian/ stable main
+ key_url: https://dl.yarnpkg.com/debian/pubkey.gpg
packages:
- ffmpeg
+ - libicu-dev
- libprotobuf-dev
- protobuf-compiler
- - libicu-dev
+ - yarn
rvm:
- 2.3.4
@@ -42,7 +45,6 @@ services:
install:
- nvm install
- - npm install -g yarn
- bundle install --path=vendor/bundle --without development production --retry=3 --jobs=16
- yarn install
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..7cec57180
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,46 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at eugen@zeonfederated.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/
diff --git a/Dockerfile b/Dockerfile
index c3b38fa8b..7cca02ecf 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -7,8 +7,8 @@ ENV UID=991 GID=991 \
RAILS_SERVE_STATIC_FILES=true \
RAILS_ENV=production NODE_ENV=production
-ARG YARN_VERSION=1.1.0
-ARG YARN_DOWNLOAD_SHA256=171c1f9ee93c488c0d774ac6e9c72649047c3f896277d88d0f805266519430f3
+ARG YARN_VERSION=1.3.2
+ARG YARN_DOWNLOAD_SHA256=6cfe82e530ef0837212f13e45c1565ba53f5199eec2527b85ecbcd88bf26821d
ARG LIBICONV_VERSION=1.15
ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178
@@ -48,7 +48,7 @@ RUN apk -U upgrade \
&& rm yarn.tar.gz \
&& mv /tmp/src/yarn-v$YARN_VERSION /opt/yarn \
&& ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
- && wget -O libiconv.tar.gz "http://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \
+ && wget -O libiconv.tar.gz "https://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \
&& echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \
&& tar -xzf libiconv.tar.gz -C /tmp/src \
&& rm libiconv.tar.gz \
diff --git a/Gemfile b/Gemfile
index 7b359af1d..f6acb431a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -14,8 +14,10 @@ gem 'pg', '~> 0.20'
gem 'pghero', '~> 1.7'
gem 'dotenv-rails', '~> 2.2'
-gem 'aws-sdk', '~> 2.9'
-gem 'fog-openstack', '~> 0.1'
+gem 'aws-sdk', '~> 2.10', require: false
+gem 'fog-core', '~> 1.45'
+gem 'fog-local', '~> 0.4', require: false
+gem 'fog-openstack', '~> 0.1', require: false
gem 'paperclip', '~> 5.1'
gem 'paperclip-av-transcoder', '~> 0.6'
@@ -38,16 +40,15 @@ gem 'http', '~> 2.2'
gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 0.99'
gem 'idn-ruby', require: 'idn'
-gem 'kaminari', '~> 1.0'
+gem 'kaminari', '~> 1.1'
gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.1'
-gem 'nokogiri', '~> 1.7'
+gem 'nokogiri', '~> 1.8'
gem 'nsa', '~> 0.2'
-gem 'oj', '~> 3.0'
+gem 'oj', '~> 3.3'
gem 'ostatus2', '~> 2.0'
-gem 'ox', '~> 2.5'
+gem 'ox', '~> 2.8'
gem 'pundit', '~> 1.1'
-gem 'rabl', '~> 0.13'
gem 'rack-attack', '~> 5.0'
gem 'rack-cors', '~> 0.4', require: 'rack/cors'
gem 'rack-timeout', '~> 0.4'
@@ -75,15 +76,15 @@ gem 'json-ld-preloaded', '~> 2.2.1'
gem 'rdf-normalize', '~> 0.3.1'
group :development, :test do
- gem 'fabrication', '~> 2.16'
+ gem 'fabrication', '~> 2.18'
gem 'fuubar', '~> 2.2'
gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-rails', '~> 0.3'
- gem 'rspec-rails', '~> 3.6'
+ gem 'rspec-rails', '~> 3.7'
end
group :test do
- gem 'capybara', '~> 2.14'
+ gem 'capybara', '~> 2.15'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 1.7'
gem 'microformats', '~> 4.0'
@@ -91,13 +92,13 @@ group :test do
gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.14', require: false
gem 'webmock', '~> 3.0'
- gem 'parallel_tests', '~> 2.14'
+ gem 'parallel_tests', '~> 2.17'
end
group :development do
gem 'active_record_query_trace', '~> 1.5'
gem 'annotate', '~> 2.7'
- gem 'better_errors', '~> 2.1'
+ gem 'better_errors', '~> 2.4'
gem 'binding_of_caller', '~> 0.7'
gem 'bullet', '~> 5.5'
gem 'letter_opener', '~> 1.4'
@@ -105,15 +106,15 @@ group :development do
gem 'rubocop', require: false
gem 'brakeman', '~> 4.0', require: false
gem 'bundler-audit', '~> 0.6', require: false
- gem 'scss_lint', '~> 0.53', require: false
+ gem 'scss_lint', '~> 0.55', require: false
- gem 'capistrano', '~> 3.8'
- gem 'capistrano-rails', '~> 1.2'
+ gem 'capistrano', '~> 3.10'
+ gem 'capistrano-rails', '~> 1.3'
gem 'capistrano-rbenv', '~> 2.1'
gem 'capistrano-yarn', '~> 2.0'
end
group :production do
- gem 'lograge', '~> 0.5'
+ gem 'lograge', '~> 0.7'
gem 'redis-rails', '~> 5.0'
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 14ed0d309..febfb8561 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -24,11 +24,11 @@ GEM
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
- active_model_serializers (0.10.6)
+ active_model_serializers (0.10.7)
actionpack (>= 4.1, < 6)
activemodel (>= 4.1, < 6)
case_transform (>= 0.2)
- jsonapi-renderer (>= 0.1.1.beta1, < 0.2)
+ jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.5.4)
activejob (5.1.4)
activesupport (= 5.1.4)
@@ -57,25 +57,25 @@ GEM
encryptor (~> 3.0.0)
av (0.9.0)
cocaine (~> 0.5.3)
- aws-sdk (2.10.46)
- aws-sdk-resources (= 2.10.46)
- aws-sdk-core (2.10.46)
+ aws-sdk (2.10.100)
+ aws-sdk-resources (= 2.10.100)
+ aws-sdk-core (2.10.100)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
- aws-sdk-resources (2.10.46)
- aws-sdk-core (= 2.10.46)
+ aws-sdk-resources (2.10.100)
+ aws-sdk-core (= 2.10.100)
aws-sigv4 (1.0.2)
bcrypt (3.1.11)
- better_errors (2.3.0)
+ better_errors (2.4.0)
coderay (>= 1.0.0)
erubi (>= 1.0.0)
rack (>= 0.9.0)
- binding_of_caller (0.7.2)
+ binding_of_caller (0.7.3)
debug_inspector (>= 0.0.1)
- bootsnap (1.1.3)
+ bootsnap (1.1.5)
msgpack (~> 1.0)
brakeman (4.0.1)
- browser (2.5.1)
+ browser (2.5.2)
builder (3.2.3)
bullet (5.6.1)
activesupport (>= 3.0.0)
@@ -83,23 +83,23 @@ GEM
bundler-audit (0.6.0)
bundler (~> 1.2)
thor (~> 0.18)
- capistrano (3.9.1)
+ capistrano (3.10.0)
airbrussh (>= 1.0.0)
i18n
rake (>= 10.0.0)
sshkit (>= 1.9.0)
- capistrano-bundler (1.2.0)
+ capistrano-bundler (1.3.0)
capistrano (~> 3.1)
sshkit (~> 1.2)
- capistrano-rails (1.3.0)
+ capistrano-rails (1.3.1)
capistrano (~> 3.1)
capistrano-bundler (~> 1.1)
- capistrano-rbenv (2.1.1)
+ capistrano-rbenv (2.1.3)
capistrano (~> 3.1)
sshkit (~> 1.3)
capistrano-yarn (2.0.2)
capistrano (~> 3.0)
- capybara (2.15.1)
+ capybara (2.16.1)
addressable
mini_mime (>= 0.1.3)
nokogiri (>= 1.3.3)
@@ -110,7 +110,7 @@ GEM
activesupport
charlock_holmes (0.7.5)
chunky_png (1.3.8)
- cld3 (3.2.0)
+ cld3 (3.2.1)
ffi (>= 1.1.0, < 1.10.0)
climate_control (0.2.0)
cocaine (0.5.8)
@@ -121,7 +121,7 @@ GEM
connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
- crass (1.0.2)
+ crass (1.0.3)
debug_inspector (0.0.3)
devise (4.3.0)
bcrypt (~> 3.0)
@@ -129,11 +129,11 @@ GEM
railties (>= 4.1.0, < 5.2)
responders
warden (~> 1.2.3)
- devise-two-factor (3.0.0)
- activesupport
+ devise-two-factor (3.0.2)
+ activesupport (< 5.2)
attr_encrypted (>= 1.3, < 4, != 2)
devise (~> 4.0)
- railties
+ railties (< 5.2)
rotp (~> 2.0)
diff-lcs (1.3)
docile (1.1.5)
@@ -150,12 +150,12 @@ GEM
thread
thread_safe
encryptor (3.0.0)
- erubi (1.6.1)
- et-orbi (1.0.5)
+ erubi (1.7.0)
+ et-orbi (1.0.8)
tzinfo
excon (0.59.0)
execjs (2.7.0)
- fabrication (2.16.3)
+ fabrication (2.18.0)
faker (1.8.4)
i18n (~> 0.5)
fast_blank (1.0.0)
@@ -167,7 +167,9 @@ GEM
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
- fog-openstack (0.1.21)
+ fog-local (0.4.0)
+ fog-core (~> 1.27)
+ fog-openstack (0.1.22)
fog-core (>= 1.40)
fog-json (>= 1.0)
ipaddress (>= 0.8)
@@ -175,14 +177,14 @@ GEM
fuubar (2.2.0)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
- globalid (0.4.0)
+ globalid (0.4.1)
activesupport (>= 4.2.0)
goldfinger (2.0.1)
addressable (~> 2.5)
http (~> 2.2)
nokogiri (~> 1.8)
oj (~> 3.0)
- hamlit (2.8.4)
+ hamlit (2.8.5)
temple (>= 0.8.0)
thor
tilt
@@ -194,7 +196,7 @@ GEM
hamster (3.0.0)
concurrent-ruby (~> 1.0)
hashdiff (0.3.7)
- highline (1.7.8)
+ highline (1.7.10)
hiredis (0.6.1)
hkdf (0.3.0)
htmlentities (4.3.4)
@@ -211,8 +213,9 @@ GEM
httplog (0.99.7)
colorize
rack
- i18n (0.8.6)
- i18n-tasks (0.9.18)
+ i18n (0.9.1)
+ concurrent-ruby (~> 1.0)
+ i18n-tasks (0.9.19)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
easy_translate (>= 0.5.0)
@@ -227,27 +230,27 @@ GEM
iso-639 (0.2.8)
jmespath (1.3.1)
json (2.1.0)
- json-ld (2.1.5)
+ json-ld (2.1.7)
multi_json (~> 1.12)
- rdf (~> 2.2)
+ rdf (~> 2.2, >= 2.2.8)
json-ld-preloaded (2.2.2)
json-ld (~> 2.1, >= 2.1.5)
multi_json (~> 1.11)
rdf (~> 2.2)
- jsonapi-renderer (0.1.3)
- jwt (1.5.6)
- kaminari (1.0.1)
+ jsonapi-renderer (0.2.0)
+ jwt (2.1.0)
+ kaminari (1.1.1)
activesupport (>= 4.1.0)
- kaminari-actionview (= 1.0.1)
- kaminari-activerecord (= 1.0.1)
- kaminari-core (= 1.0.1)
- kaminari-actionview (1.0.1)
+ kaminari-actionview (= 1.1.1)
+ kaminari-activerecord (= 1.1.1)
+ kaminari-core (= 1.1.1)
+ kaminari-actionview (1.1.1)
actionview
- kaminari-core (= 1.0.1)
- kaminari-activerecord (1.0.1)
+ kaminari-core (= 1.1.1)
+ kaminari-activerecord (1.1.1)
activerecord
- kaminari-core (= 1.0.1)
- kaminari-core (1.0.1)
+ kaminari-core (= 1.1.1)
+ kaminari-core (1.1.1)
launchy (2.4.3)
addressable (~> 2.3)
letter_opener (1.4.1)
@@ -257,18 +260,19 @@ GEM
letter_opener (~> 1.0)
railties (>= 3.2)
link_header (0.0.8)
- lograge (0.6.0)
+ lograge (0.7.1)
actionpack (>= 4, < 5.2)
activesupport (>= 4, < 5.2)
railties (>= 4, < 5.2)
request_store (~> 1.0)
- loofah (2.0.3)
+ loofah (2.1.1)
+ crass (~> 1.0.2)
nokogiri (>= 1.5.9)
- mail (2.6.6)
- mime-types (>= 1.16, < 4)
+ mail (2.7.0)
+ mini_mime (>= 0.1.1)
mario-redis-lock (1.2.0)
redis (~> 3, >= 3.0.5)
- method_source (0.8.2)
+ method_source (0.9.0)
microformats (4.0.7)
json
nokogiri
@@ -276,8 +280,8 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mimemagic (0.3.2)
- mini_mime (0.1.4)
- mini_portile2 (2.2.0)
+ mini_mime (1.0.0)
+ mini_portile2 (2.3.0)
minitest (5.10.3)
msgpack (1.1.0)
multi_json (1.12.2)
@@ -285,8 +289,8 @@ GEM
net-ssh (>= 2.6.5)
net-ssh (4.2.0)
nio4r (2.1.0)
- nokogiri (1.8.0)
- mini_portile2 (~> 2.2.0)
+ nokogiri (1.8.1)
+ mini_portile2 (~> 2.3.0)
nokogumbo (1.4.13)
nokogiri
nsa (0.2.4)
@@ -294,15 +298,15 @@ GEM
concurrent-ruby (~> 1.0.0)
sidekiq (>= 3.5.0)
statsd-ruby (~> 1.2.0)
- oj (3.3.5)
- openssl (2.0.5)
+ oj (3.3.9)
+ openssl (2.0.6)
orm_adapter (0.5.0)
ostatus2 (2.0.1)
addressable (~> 2.4)
http (~> 2.0)
nokogiri (~> 1.6)
openssl (~> 2.0)
- ox (2.6.0)
+ ox (2.8.2)
paperclip (5.1.0)
activemodel (>= 4.2.0)
activesupport (>= 4.2.0)
@@ -313,27 +317,24 @@ GEM
av (~> 0.9.0)
paperclip (>= 2.5.2)
parallel (1.12.0)
- parallel_tests (2.15.0)
+ parallel_tests (2.19.0)
parallel
- parser (2.4.0.0)
- ast (~> 2.2)
+ parser (2.4.0.2)
+ ast (~> 2.3)
pg (0.21.0)
pghero (1.7.0)
activerecord
- pkg-config (1.2.7)
+ pkg-config (1.2.8)
powerpack (0.1.1)
- pry (0.10.4)
+ pry (0.11.3)
coderay (~> 1.1.0)
- method_source (~> 0.8.1)
- slop (~> 3.4)
+ method_source (~> 0.9.0)
pry-rails (0.3.6)
pry (>= 0.10.4)
- public_suffix (3.0.0)
- puma (3.10.0)
+ public_suffix (3.0.1)
+ puma (3.11.0)
pundit (1.1.0)
activesupport (>= 3.0.0)
- rabl (0.13.1)
- activesupport (>= 2.3.14)
rack (2.0.3)
rack-attack (5.0.1)
rack
@@ -342,7 +343,7 @@ GEM
rack
rack-proxy (0.6.2)
rack
- rack-test (0.7.0)
+ rack-test (0.8.2)
rack (>= 1.0, < 3)
rack-timeout (0.4.2)
rails (5.1.4)
@@ -379,31 +380,34 @@ GEM
thor (>= 0.18.1, < 2.0)
rainbow (2.2.2)
rake
- rake (12.1.0)
- rdf (2.2.9)
+ rake (12.3.0)
+ rb-fsevent (0.10.2)
+ rb-inotify (0.9.10)
+ ffi (>= 0.5.0, < 2)
+ rdf (2.2.12)
hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.2)
rdf (~> 2.0)
- redis (3.3.3)
- redis-actionpack (5.0.1)
+ redis (3.3.5)
+ redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3)
- redis-store (>= 1.1.0, < 1.4.0)
- redis-activesupport (5.0.3)
+ redis-store (>= 1.1.0, < 2)
+ redis-activesupport (5.0.4)
activesupport (>= 3, < 6)
- redis-store (~> 1.3.0)
- redis-namespace (1.5.3)
- redis (~> 3.0, >= 3.0.4)
- redis-rack (2.0.2)
+ redis-store (>= 1.3, < 2)
+ redis-namespace (1.6.0)
+ redis (>= 3.0.4)
+ redis-rack (2.0.3)
rack (>= 1.5, < 3)
- redis-store (>= 1.2, < 1.4)
+ redis-store (>= 1.2, < 2)
redis-rails (5.0.2)
redis-actionpack (>= 5.0, < 6)
redis-activesupport (>= 5.0, < 6)
redis-store (>= 1.2, < 2)
- redis-store (1.3.0)
- redis (>= 2.2)
+ redis-store (1.4.1)
+ redis (>= 2.2, < 5)
request_store (1.3.2)
responders (2.4.0)
actionpack (>= 4.2.0, < 5.3)
@@ -411,27 +415,27 @@ GEM
rotp (2.1.2)
rqrcode (0.10.1)
chunky_png (~> 1.0)
- rspec-core (3.6.0)
- rspec-support (~> 3.6.0)
- rspec-expectations (3.6.0)
+ rspec-core (3.7.0)
+ rspec-support (~> 3.7.0)
+ rspec-expectations (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.6.0)
- rspec-mocks (3.6.0)
+ rspec-support (~> 3.7.0)
+ rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.6.0)
- rspec-rails (3.6.1)
+ rspec-support (~> 3.7.0)
+ rspec-rails (3.7.2)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
- rspec-core (~> 3.6.0)
- rspec-expectations (~> 3.6.0)
- rspec-mocks (~> 3.6.0)
- rspec-support (~> 3.6.0)
+ rspec-core (~> 3.7.0)
+ rspec-expectations (~> 3.7.0)
+ rspec-mocks (~> 3.7.0)
+ rspec-support (~> 3.7.0)
rspec-sidekiq (3.0.3)
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
- rspec-support (3.6.0)
- rubocop (0.50.0)
+ rspec-support (3.7.0)
+ rubocop (0.51.0)
parallel (~> 1.10)
parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1)
@@ -439,7 +443,7 @@ GEM
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-oembed (0.12.0)
- ruby-progressbar (1.8.3)
+ ruby-progressbar (1.9.0)
rufus-scheduler (3.4.2)
et-orbi (~> 1.0)
safe_yaml (1.0.4)
@@ -447,20 +451,24 @@ GEM
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4.1)
- sass (3.4.25)
- scss_lint (0.54.0)
+ sass (3.5.3)
+ sass-listen (~> 4.0.0)
+ sass-listen (4.0.0)
+ rb-fsevent (~> 0.9, >= 0.9.4)
+ rb-inotify (~> 0.9, >= 0.9.7)
+ scss_lint (0.56.0)
rake (>= 0.9, < 13)
- sass (~> 3.4.20)
- sidekiq (5.0.4)
+ sass (~> 3.5.3)
+ sidekiq (5.0.5)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
- redis (~> 3.3, >= 3.3.3)
+ redis (>= 3.3.4, < 5)
sidekiq-bulk (0.1.1)
activesupport
sidekiq
- sidekiq-scheduler (2.1.9)
- redis (~> 3)
+ sidekiq-scheduler (2.1.10)
+ redis (>= 3, < 5)
rufus-scheduler (~> 3.2)
sidekiq (>= 3)
tilt (>= 1.4.0)
@@ -477,7 +485,6 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
- slop (3.6.0)
sprockets (3.7.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
@@ -485,7 +492,7 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
- sshkit (1.14.0)
+ sshkit (1.15.1)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
statsd-ruby (1.2.1)
@@ -500,9 +507,9 @@ GEM
tilt (2.0.8)
twitter-text (1.14.7)
unf (~> 0.1.0)
- tzinfo (1.2.3)
+ tzinfo (1.2.4)
thread_safe (~> 0.1)
- tzinfo-data (1.2017.2)
+ tzinfo-data (1.2017.3)
tzinfo (>= 1.0.0)
uglifier (3.2.0)
execjs (>= 0.3.0, < 3)
@@ -513,20 +520,20 @@ GEM
uniform_notifier (1.10.0)
warden (1.2.7)
rack (>= 1.0)
- webmock (3.1.0)
+ webmock (3.1.1)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
- webpacker (3.0.1)
+ webpacker (3.0.2)
activesupport (>= 4.2)
rack-proxy (>= 0.6.1)
railties (>= 4.2)
- webpush (0.3.2)
+ webpush (0.3.3)
hkdf (~> 0.2)
- jwt
+ jwt (~> 2.0)
websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0)
- websocket-extensions (0.1.2)
+ websocket-extensions (0.1.3)
xpath (2.1.0)
nokogiri (~> 1.3)
@@ -538,19 +545,19 @@ DEPENDENCIES
active_record_query_trace (~> 1.5)
addressable (~> 2.5)
annotate (~> 2.7)
- aws-sdk (~> 2.9)
- better_errors (~> 2.1)
+ aws-sdk (~> 2.10)
+ better_errors (~> 2.4)
binding_of_caller (~> 0.7)
bootsnap
brakeman (~> 4.0)
browser
bullet (~> 5.5)
bundler-audit (~> 0.6)
- capistrano (~> 3.8)
- capistrano-rails (~> 1.2)
+ capistrano (~> 3.10)
+ capistrano-rails (~> 1.3)
capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0)
- capybara (~> 2.14)
+ capybara (~> 2.15)
charlock_holmes (~> 0.7.5)
cld3 (~> 3.2.0)
climate_control (~> 0.2)
@@ -558,9 +565,11 @@ DEPENDENCIES
devise-two-factor (~> 3.0)
doorkeeper (~> 4.2)
dotenv-rails (~> 2.2)
- fabrication (~> 2.16)
+ fabrication (~> 2.18)
faker (~> 1.7)
fast_blank (~> 1.0)
+ fog-core (~> 1.45)
+ fog-local (~> 0.4)
fog-openstack (~> 0.1)
fuubar (~> 2.2)
goldfinger (~> 2.0)
@@ -574,29 +583,28 @@ DEPENDENCIES
idn-ruby
iso-639
json-ld-preloaded (~> 2.2.1)
- kaminari (~> 1.0)
+ kaminari (~> 1.1)
letter_opener (~> 1.4)
letter_opener_web (~> 1.3)
link_header (~> 0.0)
- lograge (~> 0.5)
+ lograge (~> 0.7)
mario-redis-lock (~> 1.2)
microformats (~> 4.0)
mime-types (~> 3.1)
- nokogiri (~> 1.7)
+ nokogiri (~> 1.8)
nsa (~> 0.2)
- oj (~> 3.0)
+ oj (~> 3.3)
ostatus2 (~> 2.0)
- ox (~> 2.5)
+ ox (~> 2.8)
paperclip (~> 5.1)
paperclip-av-transcoder (~> 0.6)
- parallel_tests (~> 2.14)
+ parallel_tests (~> 2.17)
pg (~> 0.20)
pghero (~> 1.7)
pkg-config (~> 1.2)
pry-rails (~> 0.3)
puma (~> 3.10)
pundit (~> 1.1)
- rabl (~> 0.13)
rack-attack (~> 5.0)
rack-cors (~> 0.4)
rack-timeout (~> 0.4)
@@ -609,12 +617,12 @@ DEPENDENCIES
redis-namespace (~> 1.5)
redis-rails (~> 5.0)
rqrcode (~> 0.10)
- rspec-rails (~> 3.6)
+ rspec-rails (~> 3.7)
rspec-sidekiq (~> 3.0)
rubocop
ruby-oembed (~> 0.12)
sanitize (~> 4.4)
- scss_lint (~> 0.53)
+ scss_lint (~> 0.55)
sidekiq (~> 5.0)
sidekiq-bulk (~> 0.1.1)
sidekiq-scheduler (~> 2.1)
@@ -635,4 +643,4 @@ RUBY VERSION
ruby 2.4.2p198
BUNDLED WITH
- 1.15.4
+ 1.16.0
diff --git a/README.md b/README.md
index fc8296813..5cf91d52c 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@

========
-[][travis]
-[][code_climate]
+[][travis]
+[][code_climate]
[travis]: https://travis-ci.org/tootsuite/mastodon
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon
diff --git a/app/controllers/admin/account_moderation_notes_controller.rb b/app/controllers/admin/account_moderation_notes_controller.rb
index 414a875d0..7d5b9bf52 100644
--- a/app/controllers/admin/account_moderation_notes_controller.rb
+++ b/app/controllers/admin/account_moderation_notes_controller.rb
@@ -1,31 +1,41 @@
# frozen_string_literal: true
-class Admin::AccountModerationNotesController < Admin::BaseController
- def create
- @account_moderation_note = current_account.account_moderation_notes.new(resource_params)
- if @account_moderation_note.save
- @target_account = @account_moderation_note.target_account
- redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.created_msg')
- else
- @account = @account_moderation_note.target_account
- @moderation_notes = @account.targeted_moderation_notes.latest
- render template: 'admin/accounts/show'
+module Admin
+ class AccountModerationNotesController < BaseController
+ before_action :set_account_moderation_note, only: [:destroy]
+
+ def create
+ authorize AccountModerationNote, :create?
+
+ @account_moderation_note = current_account.account_moderation_notes.new(resource_params)
+
+ if @account_moderation_note.save
+ redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.created_msg')
+ else
+ @account = @account_moderation_note.target_account
+ @moderation_notes = @account.targeted_moderation_notes.latest
+
+ render template: 'admin/accounts/show'
+ end
+ end
+
+ def destroy
+ authorize @account_moderation_note, :destroy?
+ @account_moderation_note.destroy!
+ redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg')
+ end
+
+ private
+
+ def resource_params
+ params.require(:account_moderation_note).permit(
+ :content,
+ :target_account_id
+ )
+ end
+
+ def set_account_moderation_note
+ @account_moderation_note = AccountModerationNote.find(params[:id])
end
end
-
- def destroy
- @account_moderation_note = AccountModerationNote.find(params[:id])
- @target_account = @account_moderation_note.target_account
- @account_moderation_note.destroy
- redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg')
- end
-
- private
-
- def resource_params
- params.require(:account_moderation_note).permit(
- :content,
- :target_account_id
- )
- end
end
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index ffa4dc850..7428c3f22 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -2,29 +2,57 @@
module Admin
class AccountsController < BaseController
- before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload]
+ before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :enable, :disable, :memorialize]
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
+ before_action :require_local_account!, only: [:enable, :disable, :memorialize]
def index
+ authorize :account, :index?
@accounts = filtered_accounts.page(params[:page])
end
def show
+ authorize @account, :show?
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
@moderation_notes = @account.targeted_moderation_notes.latest
end
def subscribe
+ authorize @account, :subscribe?
Pubsubhubbub::SubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id)
end
def unsubscribe
+ authorize @account, :unsubscribe?
Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id)
end
+ def memorialize
+ authorize @account, :memorialize?
+ @account.memorialize!
+ log_action :memorialize, @account
+ redirect_to admin_account_path(@account.id)
+ end
+
+ def enable
+ authorize @account.user, :enable?
+ @account.user.enable!
+ log_action :enable, @account.user
+ redirect_to admin_account_path(@account.id)
+ end
+
+ def disable
+ authorize @account.user, :disable?
+ @account.user.disable!
+ log_action :disable, @account.user
+ redirect_to admin_account_path(@account.id)
+ end
+
def redownload
+ authorize @account, :redownload?
+
@account.reset_avatar!
@account.reset_header!
@account.save!
@@ -42,6 +70,10 @@ module Admin
redirect_to admin_account_path(@account.id) if @account.local?
end
+ def require_local_account!
+ redirect_to admin_account_path(@account.id) unless @account.local? && @account.user.present?
+ end
+
def filtered_accounts
AccountFilter.new(filter_params).results
end
@@ -57,7 +89,8 @@ module Admin
:username,
:display_name,
:email,
- :ip
+ :ip,
+ :staff
)
end
end
diff --git a/app/controllers/admin/action_logs_controller.rb b/app/controllers/admin/action_logs_controller.rb
new file mode 100644
index 000000000..e273dfeae
--- /dev/null
+++ b/app/controllers/admin/action_logs_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Admin
+ class ActionLogsController < BaseController
+ def index
+ @action_logs = Admin::ActionLog.page(params[:page])
+ end
+ end
+end
diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb
index 11fe326bc..7fb69d578 100644
--- a/app/controllers/admin/base_controller.rb
+++ b/app/controllers/admin/base_controller.rb
@@ -2,7 +2,10 @@
module Admin
class BaseController < ApplicationController
- before_action :require_admin!
+ include Authorization
+ include AccountableConcern
+
+ before_action :require_staff!
layout 'admin'
end
diff --git a/app/controllers/admin/confirmations_controller.rb b/app/controllers/admin/confirmations_controller.rb
index 2542e21ee..34dfb458e 100644
--- a/app/controllers/admin/confirmations_controller.rb
+++ b/app/controllers/admin/confirmations_controller.rb
@@ -2,15 +2,19 @@
module Admin
class ConfirmationsController < BaseController
+ before_action :set_user
+
def create
- account_user.confirm
+ authorize @user, :confirm?
+ @user.confirm!
+ log_action :confirm, @user
redirect_to admin_accounts_path
end
private
- def account_user
- Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
+ def set_user
+ @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end
end
end
diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb
index 5cce5bce4..ccab03de4 100644
--- a/app/controllers/admin/custom_emojis_controller.rb
+++ b/app/controllers/admin/custom_emojis_controller.rb
@@ -5,47 +5,73 @@ module Admin
before_action :set_custom_emoji, except: [:index, :new, :create]
def index
- @custom_emojis = filtered_custom_emojis.page(params[:page])
+ authorize :custom_emoji, :index?
+ @custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page])
end
def new
+ authorize :custom_emoji, :create?
@custom_emoji = CustomEmoji.new
end
def create
+ authorize :custom_emoji, :create?
+
@custom_emoji = CustomEmoji.new(resource_params)
if @custom_emoji.save
+ log_action :create, @custom_emoji
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg')
else
render :new
end
end
+ def update
+ authorize @custom_emoji, :update?
+
+ if @custom_emoji.update(resource_params)
+ log_action :update, @custom_emoji
+ redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg')
+ else
+ redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg')
+ end
+ end
+
def destroy
- @custom_emoji.destroy
+ authorize @custom_emoji, :destroy?
+ @custom_emoji.destroy!
+ log_action :destroy, @custom_emoji
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
end
def copy
- emoji = CustomEmoji.new(domain: nil, shortcode: @custom_emoji.shortcode, image: @custom_emoji.image)
+ authorize @custom_emoji, :copy?
+
+ emoji = CustomEmoji.find_or_initialize_by(domain: nil, shortcode: @custom_emoji.shortcode)
+ emoji.image = @custom_emoji.image
if emoji.save
+ log_action :create, emoji
flash[:notice] = I18n.t('admin.custom_emojis.copied_msg')
else
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
end
- redirect_to admin_custom_emojis_path(params[:page])
+ redirect_to admin_custom_emojis_path(page: params[:page])
end
def enable
+ authorize @custom_emoji, :enable?
@custom_emoji.update!(disabled: false)
+ log_action :enable, @custom_emoji
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg')
end
def disable
+ authorize @custom_emoji, :disable?
@custom_emoji.update!(disabled: true)
+ log_action :disable, @custom_emoji
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg')
end
@@ -56,7 +82,7 @@ module Admin
end
def resource_params
- params.require(:custom_emoji).permit(:shortcode, :image)
+ params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker)
end
def filtered_custom_emojis
@@ -66,7 +92,9 @@ module Admin
def filter_params
params.permit(
:local,
- :remote
+ :remote,
+ :by_domain,
+ :shortcode
)
end
end
diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb
index 1ab620e03..64de2cbf0 100644
--- a/app/controllers/admin/domain_blocks_controller.rb
+++ b/app/controllers/admin/domain_blocks_controller.rb
@@ -5,28 +5,37 @@ module Admin
before_action :set_domain_block, only: [:show, :destroy]
def index
+ authorize :domain_block, :index?
@domain_blocks = DomainBlock.page(params[:page])
end
def new
+ authorize :domain_block, :create?
@domain_block = DomainBlock.new
end
def create
+ authorize :domain_block, :create?
+
@domain_block = DomainBlock.new(resource_params)
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id)
+ log_action :create, @domain_block
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg')
else
render :new
end
end
- def show; end
+ def show
+ authorize @domain_block, :show?
+ end
def destroy
+ authorize @domain_block, :destroy?
UnblockDomainService.new.call(@domain_block, retroactive_unblock?)
+ log_action :destroy, @domain_block
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg')
end
diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb
index 09275d5dc..9fe85064e 100644
--- a/app/controllers/admin/email_domain_blocks_controller.rb
+++ b/app/controllers/admin/email_domain_blocks_controller.rb
@@ -5,17 +5,22 @@ module Admin
before_action :set_email_domain_block, only: [:show, :destroy]
def index
+ authorize :email_domain_block, :index?
@email_domain_blocks = EmailDomainBlock.page(params[:page])
end
def new
+ authorize :email_domain_block, :create?
@email_domain_block = EmailDomainBlock.new
end
def create
+ authorize :email_domain_block, :create?
+
@email_domain_block = EmailDomainBlock.new(resource_params)
if @email_domain_block.save
+ log_action :create, @email_domain_block
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg')
else
render :new
@@ -23,7 +28,9 @@ module Admin
end
def destroy
- @email_domain_block.destroy
+ authorize @email_domain_block, :destroy?
+ @email_domain_block.destroy!
+ log_action :destroy, @email_domain_block
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg')
end
diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb
index 22f02e5d0..8ed0ea421 100644
--- a/app/controllers/admin/instances_controller.rb
+++ b/app/controllers/admin/instances_controller.rb
@@ -3,10 +3,12 @@
module Admin
class InstancesController < BaseController
def index
+ authorize :instance, :index?
@instances = ordered_instances
end
def resubscribe
+ authorize :instance, :resubscribe?
params.require(:by_domain)
Pubsubhubbub::SubscribeWorker.push_bulk(subscribeable_accounts.pluck(:id))
redirect_to admin_instances_path
diff --git a/app/controllers/admin/invites_controller.rb b/app/controllers/admin/invites_controller.rb
new file mode 100644
index 000000000..faccaa7c8
--- /dev/null
+++ b/app/controllers/admin/invites_controller.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Admin
+ class InvitesController < BaseController
+ def index
+ authorize :invite, :index?
+
+ @invites = filtered_invites.includes(user: :account).page(params[:page])
+ @invite = Invite.new
+ end
+
+ def create
+ authorize :invite, :create?
+
+ @invite = Invite.new(resource_params)
+ @invite.user = current_user
+
+ if @invite.save
+ redirect_to admin_invites_path
+ else
+ @invites = Invite.page(params[:page])
+ render :index
+ end
+ end
+
+ def destroy
+ @invite = Invite.find(params[:id])
+ authorize @invite, :destroy?
+ @invite.expire!
+ redirect_to admin_invites_path
+ end
+
+ private
+
+ def resource_params
+ params.require(:invite).permit(:max_uses, :expires_in)
+ end
+
+ def filtered_invites
+ InviteFilter.new(filter_params).results
+ end
+
+ def filter_params
+ params.permit(:available, :expired)
+ end
+ end
+end
diff --git a/app/controllers/admin/reported_statuses_controller.rb b/app/controllers/admin/reported_statuses_controller.rb
index 5a31adecf..535bd11d4 100644
--- a/app/controllers/admin/reported_statuses_controller.rb
+++ b/app/controllers/admin/reported_statuses_controller.rb
@@ -2,26 +2,29 @@
module Admin
class ReportedStatusesController < BaseController
- include Authorization
-
before_action :set_report
before_action :set_status, only: [:update, :destroy]
def create
- @form = Form::StatusBatch.new(form_status_batch_params)
- flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
+ authorize :status, :update?
+
+ @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account))
+ flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_report_path(@report)
end
def update
- @status.update(status_params)
+ authorize @status, :update?
+ @status.update!(status_params)
+ log_action :update, @status
redirect_to admin_report_path(@report)
end
def destroy
authorize @status, :destroy?
RemovalWorker.perform_async(@status.id)
+ log_action :destroy, @status
render json: @status
end
diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb
index 226467739..75db6b78a 100644
--- a/app/controllers/admin/reports_controller.rb
+++ b/app/controllers/admin/reports_controller.rb
@@ -5,14 +5,17 @@ module Admin
before_action :set_report, except: [:index]
def index
+ authorize :report, :index?
@reports = filtered_reports.page(params[:page])
end
def show
+ authorize @report, :show?
@form = Form::StatusBatch.new
end
def update
+ authorize @report, :update?
process_report
redirect_to admin_report_path(@report)
end
@@ -22,12 +25,17 @@ module Admin
def process_report
case params[:outcome].to_s
when 'resolve'
- @report.update(action_taken_by_current_attributes)
+ @report.update!(action_taken_by_current_attributes)
+ log_action :resolve, @report
when 'suspend'
Admin::SuspensionWorker.perform_async(@report.target_account.id)
+ log_action :resolve, @report
+ log_action :suspend, @report.target_account
resolve_all_target_account_reports
when 'silence'
- @report.target_account.update(silenced: true)
+ @report.target_account.update!(silenced: true)
+ log_action :resolve, @report
+ log_action :silence, @report.target_account
resolve_all_target_account_reports
else
raise ActiveRecord::RecordNotFound
diff --git a/app/controllers/admin/resets_controller.rb b/app/controllers/admin/resets_controller.rb
index 6db648403..3e27d01ac 100644
--- a/app/controllers/admin/resets_controller.rb
+++ b/app/controllers/admin/resets_controller.rb
@@ -2,17 +2,19 @@
module Admin
class ResetsController < BaseController
- before_action :set_account
+ before_action :set_user
def create
- @account.user.send_reset_password_instructions
+ authorize @user, :reset_password?
+ @user.send_reset_password_instructions
+ log_action :reset_password, @user
redirect_to admin_accounts_path
end
private
- def set_account
- @account = Account.find(params[:account_id])
+ def set_user
+ @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end
end
end
diff --git a/app/controllers/admin/roles_controller.rb b/app/controllers/admin/roles_controller.rb
new file mode 100644
index 000000000..af7ec0740
--- /dev/null
+++ b/app/controllers/admin/roles_controller.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Admin
+ class RolesController < BaseController
+ before_action :set_user
+
+ def promote
+ authorize @user, :promote?
+ @user.promote!
+ log_action :promote, @user
+ redirect_to admin_account_path(@user.account_id)
+ end
+
+ def demote
+ authorize @user, :demote?
+ @user.demote!
+ log_action :demote, @user
+ redirect_to admin_account_path(@user.account_id)
+ end
+
+ private
+
+ def set_user
+ @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
+ end
+ end
+end
diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb
index a2f86b8a9..eed5fb6b5 100644
--- a/app/controllers/admin/settings_controller.rb
+++ b/app/controllers/admin/settings_controller.rb
@@ -13,14 +13,17 @@ module Admin
closed_registrations_message
open_deletion
timeline_preview
+ show_staff_badge
bootstrap_timeline_accounts
thumbnail
+ min_invite_role
).freeze
BOOLEAN_SETTINGS = %w(
open_registrations
open_deletion
timeline_preview
+ show_staff_badge
).freeze
UPLOAD_SETTINGS = %w(
@@ -28,10 +31,13 @@ module Admin
).freeze
def edit
+ authorize :settings, :show?
@admin_settings = Form::AdminSettings.new
end
def update
+ authorize :settings, :update?
+
settings_params.each do |key, value|
if UPLOAD_SETTINGS.include?(key)
upload = SiteUpload.where(var: key).first_or_initialize(var: key)
diff --git a/app/controllers/admin/silences_controller.rb b/app/controllers/admin/silences_controller.rb
index 81a3008b9..4c06a9c0c 100644
--- a/app/controllers/admin/silences_controller.rb
+++ b/app/controllers/admin/silences_controller.rb
@@ -5,12 +5,16 @@ module Admin
before_action :set_account
def create
- @account.update(silenced: true)
+ authorize @account, :silence?
+ @account.update!(silenced: true)
+ log_action :silence, @account
redirect_to admin_accounts_path
end
def destroy
- @account.update(silenced: false)
+ authorize @account, :unsilence?
+ @account.update!(silenced: false)
+ log_action :unsilence, @account
redirect_to admin_accounts_path
end
diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb
index b05000b16..5d4325f57 100644
--- a/app/controllers/admin/statuses_controller.rb
+++ b/app/controllers/admin/statuses_controller.rb
@@ -2,8 +2,6 @@
module Admin
class StatusesController < BaseController
- include Authorization
-
helper_method :current_params
before_action :set_account
@@ -12,31 +10,39 @@ module Admin
PER_PAGE = 20
def index
+ authorize :status, :index?
+
@statuses = @account.statuses
+
if params[:media]
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
@statuses.merge!(Status.where(id: account_media_status_ids))
end
- @statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
- @form = Form::StatusBatch.new
+ @statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
+ @form = Form::StatusBatch.new
end
def create
- @form = Form::StatusBatch.new(form_status_batch_params)
- flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
+ authorize :status, :update?
+
+ @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account))
+ flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_account_statuses_path(@account.id, current_params)
end
def update
- @status.update(status_params)
+ authorize @status, :update?
+ @status.update!(status_params)
+ log_action :update, @status
redirect_to admin_account_statuses_path(@account.id, current_params)
end
def destroy
authorize @status, :destroy?
RemovalWorker.perform_async(@status.id)
+ log_action :destroy, @status
render json: @status
end
@@ -60,6 +66,7 @@ module Admin
def current_params
page = (params[:page] || 1).to_i
+
{
media: params[:media],
page: page > 1 && page,
diff --git a/app/controllers/admin/subscriptions_controller.rb b/app/controllers/admin/subscriptions_controller.rb
index 624a475a3..40500ef43 100644
--- a/app/controllers/admin/subscriptions_controller.rb
+++ b/app/controllers/admin/subscriptions_controller.rb
@@ -3,6 +3,7 @@
module Admin
class SubscriptionsController < BaseController
def index
+ authorize :subscription, :index?
@subscriptions = ordered_subscriptions.page(requested_page)
end
diff --git a/app/controllers/admin/suspensions_controller.rb b/app/controllers/admin/suspensions_controller.rb
index 5d9048d94..5f222e125 100644
--- a/app/controllers/admin/suspensions_controller.rb
+++ b/app/controllers/admin/suspensions_controller.rb
@@ -5,12 +5,16 @@ module Admin
before_action :set_account
def create
+ authorize @account, :suspend?
Admin::SuspensionWorker.perform_async(@account.id)
+ log_action :suspend, @account
redirect_to admin_accounts_path
end
def destroy
- @account.update(suspended: false)
+ authorize @account, :unsuspend?
+ @account.unsuspend!
+ log_action :unsuspend, @account
redirect_to admin_accounts_path
end
diff --git a/app/controllers/admin/two_factor_authentications_controller.rb b/app/controllers/admin/two_factor_authentications_controller.rb
index 69c08f605..022107203 100644
--- a/app/controllers/admin/two_factor_authentications_controller.rb
+++ b/app/controllers/admin/two_factor_authentications_controller.rb
@@ -5,7 +5,9 @@ module Admin
before_action :set_user
def destroy
+ authorize @user, :disable_2fa?
@user.disable_two_factor!
+ log_action :disable_2fa, @user
redirect_to admin_accounts_path
end
diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index 7cfe8fe71..5983c0fbe 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -72,19 +72,4 @@ class Api::BaseController < ApplicationController
def render_empty
render json: {}, status: 200
end
-
- def set_maps(statuses) # rubocop:disable Style/AccessorMethodName
- if current_account.nil?
- @reblogs_map = {}
- @favourites_map = {}
- @mutes_map = {}
- return
- end
-
- status_ids = statuses.compact.flat_map { |s| [s.id, s.reblog_of_id] }.uniq
- conversation_ids = statuses.compact.map(&:conversation_id).compact.uniq
- @reblogs_map = Status.reblogs_map(status_ids, current_account)
- @favourites_map = Status.favourites_map(status_ids, current_account)
- @mutes_map = Status.mutes_map(conversation_ids, current_account)
- end
end
diff --git a/app/controllers/api/v1/accounts/lists_controller.rb b/app/controllers/api/v1/accounts/lists_controller.rb
new file mode 100644
index 000000000..a7ba89ce2
--- /dev/null
+++ b/app/controllers/api/v1/accounts/lists_controller.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class Api::V1::Accounts::ListsController < Api::BaseController
+ before_action -> { doorkeeper_authorize! :read }
+ before_action :require_user!
+ before_action :set_account
+
+ respond_to :json
+
+ def index
+ @lists = @account.lists.where(account: current_account)
+ render json: @lists, each_serializer: REST::ListSerializer
+ end
+
+ private
+
+ def set_account
+ @account = Account.find(params[:account_id])
+ end
+end
diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb
index 2a5cac547..11e647c3c 100644
--- a/app/controllers/api/v1/accounts/search_controller.rb
+++ b/app/controllers/api/v1/accounts/search_controller.rb
@@ -17,12 +17,13 @@ class Api::V1::Accounts::SearchController < Api::BaseController
AccountSearchService.new.call(
params[:q],
limit_param(DEFAULT_ACCOUNTS_LIMIT),
- resolving_search?,
- current_account
+ current_account,
+ resolve: truthy_param?(:resolve),
+ following: truthy_param?(:following)
)
end
- def resolving_search?
- params[:resolve] == 'true'
+ def truthy_param?(key)
+ params[key] == 'true'
end
end
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index b3fc4e561..4e73e9e8b 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -13,9 +13,9 @@ class Api::V1::AccountsController < Api::BaseController
end
def follow
- FollowService.new.call(current_user.account, @account.acct)
+ FollowService.new.call(current_user.account, @account.acct, reblogs: params[:reblogs])
- options = @account.locked? ? {} : { following_map: { @account.id => true }, requested_map: { @account.id => false } }
+ options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: params[:reblogs] } }, requested_map: { @account.id => false } }
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
end
@@ -26,7 +26,7 @@ class Api::V1::AccountsController < Api::BaseController
end
def mute
- MuteService.new.call(current_user.account, @account)
+ MuteService.new.call(current_user.account, @account, notifications: params[:notifications])
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
end
@@ -51,7 +51,7 @@ class Api::V1::AccountsController < Api::BaseController
@account = Account.find(params[:id])
end
- def relationships(options = {})
+ def relationships(**options)
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
end
end
diff --git a/app/controllers/api/v1/lists/accounts_controller.rb b/app/controllers/api/v1/lists/accounts_controller.rb
new file mode 100644
index 000000000..c29c73b3e
--- /dev/null
+++ b/app/controllers/api/v1/lists/accounts_controller.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+class Api::V1::Lists::AccountsController < Api::BaseController
+ before_action -> { doorkeeper_authorize! :read }, only: [:show]
+ before_action -> { doorkeeper_authorize! :write }, except: [:show]
+
+ before_action :require_user!
+ before_action :set_list
+
+ after_action :insert_pagination_headers, only: :show
+
+ def show
+ @accounts = load_accounts
+ render json: @accounts, each_serializer: REST::AccountSerializer
+ end
+
+ def create
+ ApplicationRecord.transaction do
+ list_accounts.each do |account|
+ @list.accounts << account
+ end
+ end
+
+ render_empty
+ end
+
+ def destroy
+ ListAccount.where(list: @list, account_id: account_ids).destroy_all
+ render_empty
+ end
+
+ private
+
+ def set_list
+ @list = List.where(account: current_account).find(params[:list_id])
+ end
+
+ def load_accounts
+ if unlimited?
+ @list.accounts.all
+ else
+ @list.accounts.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
+ end
+ end
+
+ def list_accounts
+ Account.find(account_ids)
+ end
+
+ def account_ids
+ Array(resource_params[:account_ids])
+ end
+
+ def resource_params
+ params.permit(account_ids: [])
+ end
+
+ def insert_pagination_headers
+ set_pagination_headers(next_path, prev_path)
+ end
+
+ def next_path
+ return if unlimited?
+
+ if records_continue?
+ api_v1_list_accounts_url pagination_params(max_id: pagination_max_id)
+ end
+ end
+
+ def prev_path
+ return if unlimited?
+
+ unless @accounts.empty?
+ api_v1_list_accounts_url pagination_params(since_id: pagination_since_id)
+ end
+ end
+
+ def pagination_max_id
+ @accounts.last.id
+ end
+
+ def pagination_since_id
+ @accounts.first.id
+ end
+
+ def records_continue?
+ @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
+ end
+
+ def pagination_params(core_params)
+ params.permit(:limit).merge(core_params)
+ end
+
+ def unlimited?
+ params[:limit] == '0'
+ end
+end
diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb
new file mode 100644
index 000000000..180a91d81
--- /dev/null
+++ b/app/controllers/api/v1/lists_controller.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+class Api::V1::ListsController < Api::BaseController
+ before_action -> { doorkeeper_authorize! :read }, only: [:index, :show]
+ before_action -> { doorkeeper_authorize! :write }, except: [:index, :show]
+
+ before_action :require_user!
+ before_action :set_list, except: [:index, :create]
+
+ def index
+ @lists = List.where(account: current_account).all
+ render json: @lists, each_serializer: REST::ListSerializer
+ end
+
+ def show
+ render json: @list, serializer: REST::ListSerializer
+ end
+
+ def create
+ @list = List.create!(list_params.merge(account: current_account))
+ render json: @list, serializer: REST::ListSerializer
+ end
+
+ def update
+ @list.update!(list_params)
+ render json: @list, serializer: REST::ListSerializer
+ end
+
+ def destroy
+ @list.destroy!
+ render_empty
+ end
+
+ private
+
+ def set_list
+ @list = List.where(account: current_account).find(params[:id])
+ end
+
+ def list_params
+ params.permit(:title)
+ end
+end
diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb
index 9592cd4bd..22828217d 100644
--- a/app/controllers/api/v1/reports_controller.rb
+++ b/app/controllers/api/v1/reports_controller.rb
@@ -19,7 +19,7 @@ class Api::V1::ReportsController < Api::BaseController
comment: report_params[:comment]
)
- User.admins.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later }
+ User.staff.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later }
render json: @report, serializer: REST::ReportSerializer
end
diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb
index bc5b8e5d4..997eed6e2 100644
--- a/app/controllers/api/v1/search_controller.rb
+++ b/app/controllers/api/v1/search_controller.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class Api::V1::SearchController < Api::BaseController
+ include Authorization
+
RESULTS_LIMIT = 5
before_action -> { doorkeeper_authorize! :read }
@@ -9,12 +11,24 @@ class Api::V1::SearchController < Api::BaseController
respond_to :json
def index
- @search = Search.new(search_results)
+ @search = Search.new(search)
render json: @search, serializer: REST::SearchSerializer
end
private
+ def search
+ search_results.tap do |search|
+ search[:statuses].keep_if do |status|
+ begin
+ authorize status, :show?
+ rescue Mastodon::NotPermittedError
+ false
+ end
+ end
+ end
+ end
+
def search_results
SearchService.new.call(
params[:q],
diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb
index 3dd27710c..db6cd8568 100644
--- a/app/controllers/api/v1/timelines/home_controller.rb
+++ b/app/controllers/api/v1/timelines/home_controller.rb
@@ -31,7 +31,7 @@ class Api::V1::Timelines::HomeController < Api::BaseController
end
def account_home_feed
- Feed.new(:home, current_account)
+ HomeFeed.new(current_account)
end
def insert_pagination_headers
diff --git a/app/controllers/api/v1/timelines/list_controller.rb b/app/controllers/api/v1/timelines/list_controller.rb
new file mode 100644
index 000000000..f5db71e46
--- /dev/null
+++ b/app/controllers/api/v1/timelines/list_controller.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+class Api::V1::Timelines::ListController < Api::BaseController
+ before_action -> { doorkeeper_authorize! :read }
+ before_action :require_user!
+ before_action :set_list
+ before_action :set_statuses
+
+ after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
+
+ def show
+ render json: @statuses,
+ each_serializer: REST::StatusSerializer,
+ relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id)
+ end
+
+ private
+
+ def set_list
+ @list = List.where(account: current_account).find(params[:id])
+ end
+
+ def set_statuses
+ @statuses = cached_list_statuses
+ end
+
+ def cached_list_statuses
+ cache_collection list_statuses, Status
+ end
+
+ def list_statuses
+ list_feed.get(
+ limit_param(DEFAULT_STATUSES_LIMIT),
+ params[:max_id],
+ params[:since_id]
+ )
+ end
+
+ def list_feed
+ ListFeed.new(@list)
+ end
+
+ def insert_pagination_headers
+ set_pagination_headers(next_path, prev_path)
+ end
+
+ def pagination_params(core_params)
+ params.permit(:limit).merge(core_params)
+ end
+
+ def next_path
+ api_v1_timelines_list_url params[:id], pagination_params(max_id: pagination_max_id)
+ end
+
+ def prev_path
+ api_v1_timelines_list_url params[:id], pagination_params(since_id: pagination_since_id)
+ end
+
+ def pagination_max_id
+ @statuses.last.id
+ end
+
+ def pagination_since_id
+ @statuses.first.id
+ end
+end
diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb
index d66237feb..52e250d02 100644
--- a/app/controllers/api/web/push_subscriptions_controller.rb
+++ b/app/controllers/api/web/push_subscriptions_controller.rb
@@ -28,6 +28,8 @@ class Api::Web::PushSubscriptionsController < Api::BaseController
},
}
+ data.deep_merge!(params[:data]) if params[:data]
+
web_subscription = ::Web::PushSubscription.create!(
endpoint: params[:subscription][:endpoint],
key_p256dh: params[:subscription][:keys][:p256dh],
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index d5eca6ffb..a213302cb 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -18,6 +18,7 @@ class ApplicationController < ActionController::Base
rescue_from ActionController::RoutingError, with: :not_found
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
+ rescue_from Mastodon::NotPermittedError, with: :forbidden
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
before_action :check_suspension, if: :user_signed_in?
@@ -40,6 +41,10 @@ class ApplicationController < ActionController::Base
redirect_to root_path unless current_user&.admin?
end
+ def require_staff!
+ redirect_to root_path unless current_user&.staff?
+ end
+
def check_suspension
forbidden if current_user.account.suspended?
end
@@ -99,7 +104,7 @@ class ApplicationController < ActionController::Base
unless uncached_ids.empty?
uncached = klass.where(id: uncached_ids).with_includes.map { |item| [item.id, item] }.to_h
- uncached.values.each do |item|
+ uncached.each_value do |item|
Rails.cache.write(item.cache_key, item)
end
end
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index 223db96ff..da0b6512f 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -16,13 +16,16 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def build_resource(hash = nil)
super(hash)
- resource.locale = I18n.locale
+
+ resource.locale = I18n.locale
+ resource.invite_code = params[:invite_code] if resource.invite_code.blank?
+
resource.build_account if resource.account.nil?
end
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up) do |u|
- u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation)
+ u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation, :invite_code)
end
end
@@ -35,7 +38,19 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end
def check_enabled_registrations
- redirect_to root_path if single_user_mode? || !Setting.open_registrations
+ redirect_to root_path if single_user_mode? || !allowed_registrations?
+ end
+
+ def allowed_registrations?
+ Setting.open_registrations || (invite_code.present? && Invite.find_by(code: invite_code)&.valid_for_use?)
+ end
+
+ def invite_code
+ if params[:user]
+ params[:user][:invite_code]
+ else
+ params[:invite_code]
+ end
end
private
diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb
index 463a183e4..a5acb6c36 100644
--- a/app/controllers/auth/sessions_controller.rb
+++ b/app/controllers/auth/sessions_controller.rb
@@ -62,7 +62,7 @@ class Auth::SessionsController < Devise::SessionsController
if user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user)
- elsif user && user.valid_password?(user_params[:password])
+ elsif user&.valid_password?(user_params[:password])
prompt_for_two_factor(user)
end
end
diff --git a/app/controllers/concerns/accountable_concern.rb b/app/controllers/concerns/accountable_concern.rb
new file mode 100644
index 000000000..3cdcffc51
--- /dev/null
+++ b/app/controllers/concerns/accountable_concern.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module AccountableConcern
+ extend ActiveSupport::Concern
+
+ def log_action(action, target)
+ Admin::ActionLog.create(account: current_account, action: action, target: target)
+ end
+end
diff --git a/app/controllers/concerns/authorization.rb b/app/controllers/concerns/authorization.rb
index 7828fe48d..95a37e379 100644
--- a/app/controllers/concerns/authorization.rb
+++ b/app/controllers/concerns/authorization.rb
@@ -2,6 +2,7 @@
module Authorization
extend ActiveSupport::Concern
+
include Pundit
def pundit_user
diff --git a/app/controllers/concerns/rate_limit_headers.rb b/app/controllers/concerns/rate_limit_headers.rb
index 36cb91075..b79c558d8 100644
--- a/app/controllers/concerns/rate_limit_headers.rb
+++ b/app/controllers/concerns/rate_limit_headers.rb
@@ -44,7 +44,8 @@ module RateLimitHeaders
end
def api_throttle_data
- request.env['rack.attack.throttle_data']['api']
+ most_limited_type, = request.env['rack.attack.throttle_data'].min_by { |_, v| v[:limit] }
+ request.env['rack.attack.throttle_data'][most_limited_type]
end
def request_time
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
new file mode 100644
index 000000000..38d6c8d73
--- /dev/null
+++ b/app/controllers/invites_controller.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+class InvitesController < ApplicationController
+ include Authorization
+
+ layout 'admin'
+
+ before_action :authenticate_user!
+
+ def index
+ authorize :invite, :create?
+
+ @invites = Invite.where(user: current_user)
+ @invite = Invite.new(expires_in: 1.day.to_i)
+ end
+
+ def create
+ authorize :invite, :create?
+
+ @invite = Invite.new(resource_params)
+ @invite.user = current_user
+
+ if @invite.save
+ redirect_to invites_path
+ else
+ @invites = Invite.where(user: current_user)
+ render :index
+ end
+ end
+
+ def destroy
+ @invite = Invite.where(user: current_user).find(params[:id])
+ authorize @invite, :destroy?
+ @invite.expire!
+ redirect_to invites_path
+ end
+
+ private
+
+ def resource_params
+ params.require(:invite).permit(:max_uses, :expires_in)
+ end
+end
diff --git a/app/controllers/settings/migrations_controller.rb b/app/controllers/settings/migrations_controller.rb
new file mode 100644
index 000000000..bc6436b87
--- /dev/null
+++ b/app/controllers/settings/migrations_controller.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+class Settings::MigrationsController < ApplicationController
+ layout 'admin'
+
+ before_action :authenticate_user!
+
+ def show
+ @migration = Form::Migration.new(account: current_account.moved_to_account)
+ end
+
+ def update
+ @migration = Form::Migration.new(resource_params)
+
+ if @migration.valid? && migration_account_changed?
+ current_account.update!(moved_to_account: @migration.account)
+ ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
+ redirect_to settings_migration_path, notice: I18n.t('migrations.updated_msg')
+ else
+ render :show
+ end
+ end
+
+ private
+
+ def resource_params
+ params.require(:migration).permit(:acct)
+ end
+
+ def migration_account_changed?
+ current_account.moved_to_account_id != @migration.account&.id &&
+ current_account.id != @migration.account&.id
+ end
+end
diff --git a/app/controllers/settings/notifications_controller.rb b/app/controllers/settings/notifications_controller.rb
index 09839f16e..ce2530c54 100644
--- a/app/controllers/settings/notifications_controller.rb
+++ b/app/controllers/settings/notifications_controller.rb
@@ -26,7 +26,7 @@ class Settings::NotificationsController < ApplicationController
def user_settings_params
params.require(:user).permit(
notification_emails: %i(follow follow_request reblog favourite mention digest),
- interactions: %i(must_be_follower must_be_following)
+ interactions: %i(must_be_follower must_be_following must_be_following_dm)
)
end
end
diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb
index 1c27b2b18..5cc606808 100644
--- a/app/controllers/well_known/webfinger_controller.rb
+++ b/app/controllers/well_known/webfinger_controller.rb
@@ -6,12 +6,10 @@ module WellKnown
def show
@account = Account.find_local!(username_from_resource)
- @canonical_account_uri = @account.to_webfinger_s
- @magic_key = pem_to_magic_key(@account.keypair.public_key)
respond_to do |format|
format.any(:json, :html) do
- render formats: :json, content_type: 'application/jrd+json'
+ render json: @account, serializer: WebfingerSerializer, content_type: 'application/jrd+json'
end
format.xml do
@@ -35,21 +33,6 @@ module WellKnown
WebfingerResource.new(resource_user).username
end
- def pem_to_magic_key(public_key)
- modulus, exponent = [public_key.n, public_key.e].map do |component|
- result = []
-
- until component.zero?
- result << [component % 256].pack('C')
- component >>= 8
- end
-
- result.reverse.join
- end
-
- (['RSA'] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.')
- end
-
def resource_param
params.require(:resource)
end
diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb
new file mode 100644
index 000000000..e85243e57
--- /dev/null
+++ b/app/helpers/admin/action_logs_helper.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+module Admin::ActionLogsHelper
+ def log_target(log)
+ if log.target
+ linkable_log_target(log.target)
+ else
+ log_target_from_history(log.target_type, log.recorded_changes)
+ end
+ end
+
+ def linkable_log_target(record)
+ case record.class.name
+ when 'Account'
+ link_to record.acct, admin_account_path(record.id)
+ when 'User'
+ link_to record.account.acct, admin_account_path(record.account_id)
+ when 'CustomEmoji'
+ record.shortcode
+ when 'Report'
+ link_to "##{record.id}", admin_report_path(record)
+ when 'DomainBlock', 'EmailDomainBlock'
+ link_to record.domain, "https://#{record.domain}"
+ when 'Status'
+ link_to record.account.acct, TagManager.instance.url_for(record)
+ end
+ end
+
+ def log_target_from_history(type, attributes)
+ case type
+ when 'CustomEmoji'
+ attributes['shortcode']
+ when 'DomainBlock', 'EmailDomainBlock'
+ link_to attributes['domain'], "https://#{attributes['domain']}"
+ when 'Status'
+ tmp_status = Status.new(attributes)
+ link_to tmp_status.account.acct, TagManager.instance.url_for(tmp_status)
+ end
+ end
+
+ def relevant_log_changes(log)
+ if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action)
+ log.recorded_changes.slice('domain')
+ elsif log.target_type == 'CustomEmoji' && log.action == :update
+ log.recorded_changes.slice('domain', 'visible_in_picker')
+ elsif log.target_type == 'User' && [:promote, :demote].include?(log.action)
+ log.recorded_changes.slice('moderator', 'admin')
+ elsif log.target_type == 'DomainBlock'
+ log.recorded_changes.slice('severity', 'reject_media')
+ elsif log.target_type == 'Status' && log.action == :update
+ log.recorded_changes.slice('sensitive')
+ end
+ end
+
+ def log_extra_attributes(hash)
+ safe_join(hash.to_a.map { |key, value| safe_join([content_tag(:span, key, class: 'diff-key'), '=', log_change(value)]) }, ' ')
+ end
+
+ def log_change(val)
+ return content_tag(:span, val, class: 'diff-neutral') unless val.is_a?(Array)
+ safe_join([content_tag(:span, val.first, class: 'diff-old'), content_tag(:span, val.last, class: 'diff-new')], '→')
+ end
+
+ def icon_for_log(log)
+ case log.target_type
+ when 'Account', 'User'
+ 'user'
+ when 'CustomEmoji'
+ 'file'
+ when 'Report'
+ 'flag'
+ when 'DomainBlock'
+ 'lock'
+ when 'EmailDomainBlock'
+ 'envelope'
+ when 'Status'
+ 'pencil'
+ end
+ end
+
+ def class_for_log_icon(log)
+ case log.action
+ when :enable, :unsuspend, :unsilence, :confirm, :promote, :resolve
+ 'positive'
+ when :create
+ opposite_verbs?(log) ? 'negative' : 'positive'
+ when :update, :reset_password, :disable_2fa, :memorialize
+ 'neutral'
+ when :demote, :silence, :disable, :suspend
+ 'negative'
+ when :destroy
+ opposite_verbs?(log) ? 'positive' : 'negative'
+ else
+ ''
+ end
+ end
+
+ private
+
+ def opposite_verbs?(log)
+ %w(DomainBlock EmailDomainBlock).include?(log.target_type)
+ end
+end
diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb
index 6a57b3d63..359c43d0e 100644
--- a/app/helpers/admin/filter_helper.rb
+++ b/app/helpers/admin/filter_helper.rb
@@ -1,10 +1,12 @@
# frozen_string_literal: true
module Admin::FilterHelper
- ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip).freeze
- REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
+ ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip staff).freeze
+ REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
+ INVITE_FILTER = %i(available expired).freeze
+ CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
- FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS
+ FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS
def filter_link_to(text, link_to_params, link_class_params = link_to_params)
new_url = filtered_url_for(link_to_params)
@@ -12,13 +14,13 @@ module Admin::FilterHelper
link_to text, new_url, class: filter_link_class(new_class)
end
- def table_link_to(icon, text, path, options = {})
+ def table_link_to(icon, text, path, **options)
link_to safe_join([fa_icon(icon), text]), path, options.merge(class: 'table-action-link')
end
def selected?(more_params)
new_url = filtered_url_for(more_params)
- filter_link_class(new_url) == 'selected' ? true : false
+ filter_link_class(new_url) == 'selected'
end
private
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 6d625e7db..8ed5c8bda 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -5,7 +5,7 @@ module ApplicationHelper
current_page?(path) ? 'active' : ''
end
- def active_link_to(label, path, options = {})
+ def active_link_to(label, path, **options)
link_to label, path, options.merge(class: active_nav_class(path))
end
@@ -35,6 +35,11 @@ module ApplicationHelper
Rails.env.production? ? site_title : "#{site_title} (Dev)"
end
+ def can?(action, record)
+ return false if record.nil?
+ policy(record).public_send("#{action}?")
+ end
+
def fa_icon(icon, attributes = {})
class_names = attributes[:class]&.split(' ') || []
class_names << 'fa'
@@ -43,6 +48,10 @@ module ApplicationHelper
content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
end
+ def custom_emoji_tag(custom_emoji)
+ image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:")
+ end
+
def opengraph(property, content)
tag(:meta, content: content, property: property)
end
diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb
index c23a2e095..6c7c38070 100644
--- a/app/helpers/jsonld_helper.rb
+++ b/app/helpers/jsonld_helper.rb
@@ -9,6 +9,28 @@ module JsonLdHelper
value.is_a?(Array) ? value.first : value
end
+ # The url attribute can be a string, an array of strings, or an array of objects.
+ # The objects could include a mimeType. Not-included mimeType means it's text/html.
+ def url_to_href(value, preferred_type = nil)
+ single_value = if value.is_a?(Array) && !value.first.is_a?(String)
+ value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) }
+ elsif value.is_a?(Array)
+ value.first
+ else
+ value
+ end
+
+ if single_value.nil? || single_value.is_a?(String)
+ single_value
+ else
+ single_value['href']
+ end
+ end
+
+ def as_array(value)
+ value.is_a?(Array) ? value : [value]
+ end
+
def value_or_id(value)
value.is_a?(String) || value.nil? ? value : value['id']
end
diff --git a/app/helpers/routing_helper.rb b/app/helpers/routing_helper.rb
index f4693358c..11894a895 100644
--- a/app/helpers/routing_helper.rb
+++ b/app/helpers/routing_helper.rb
@@ -11,7 +11,7 @@ module RoutingHelper
end
end
- def full_asset_url(source, options = {})
+ def full_asset_url(source, **options)
source = ActionController::Base.helpers.asset_url(source, options) unless use_storage?
URI.join(root_url, source).to_s
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index abce85812..1d4cb8a57 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -10,6 +10,7 @@ module SettingsHelper
eo: 'Esperanto',
es: 'Español',
fa: 'فارسی',
+ gl: 'Galego',
fi: 'Suomi',
fr: 'Français',
he: 'עברית',
diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js
index 73d6baace..f63325658 100644
--- a/app/javascript/mastodon/actions/accounts.js
+++ b/app/javascript/mastodon/actions/accounts.js
@@ -105,12 +105,13 @@ export function fetchAccountFail(id, error) {
};
};
-export function followAccount(id) {
+export function followAccount(id, reblogs = true) {
return (dispatch, getState) => {
+ const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
dispatch(followAccountRequest(id));
- api(getState).post(`/api/v1/accounts/${id}/follow`).then(response => {
- dispatch(followAccountSuccess(response.data));
+ api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
+ dispatch(followAccountSuccess(response.data, alreadyFollowing));
}).catch(error => {
dispatch(followAccountFail(error));
});
@@ -136,10 +137,11 @@ export function followAccountRequest(id) {
};
};
-export function followAccountSuccess(relationship) {
+export function followAccountSuccess(relationship, alreadyFollowing) {
return {
type: ACCOUNT_FOLLOW_SUCCESS,
relationship,
+ alreadyFollowing,
};
};
@@ -241,11 +243,11 @@ export function unblockAccountFail(error) {
};
-export function muteAccount(id) {
+export function muteAccount(id, notifications) {
return (dispatch, getState) => {
dispatch(muteAccountRequest(id));
- api(getState).post(`/api/v1/accounts/${id}/mute`).then(response => {
+ api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications }).then(response => {
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
dispatch(muteAccountSuccess(response.data, getState().get('statuses')));
}).catch(error => {
diff --git a/app/javascript/mastodon/actions/favourites.js b/app/javascript/mastodon/actions/favourites.js
index 09ce51fce..93094c526 100644
--- a/app/javascript/mastodon/actions/favourites.js
+++ b/app/javascript/mastodon/actions/favourites.js
@@ -10,6 +10,10 @@ export const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FA
export function fetchFavouritedStatuses() {
return (dispatch, getState) => {
+ if (getState().getIn(['status_lists', 'favourites', 'isLoading'])) {
+ return;
+ }
+
dispatch(fetchFavouritedStatusesRequest());
api(getState).get('/api/v1/favourites').then(response => {
@@ -46,7 +50,7 @@ export function expandFavouritedStatuses() {
return (dispatch, getState) => {
const url = getState().getIn(['status_lists', 'favourites', 'next'], null);
- if (url === null) {
+ if (url === null || getState().getIn(['status_lists', 'favourites', 'isLoading'])) {
return;
}
diff --git a/app/javascript/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js
new file mode 100644
index 000000000..4c8f9b186
--- /dev/null
+++ b/app/javascript/mastodon/actions/lists.js
@@ -0,0 +1,313 @@
+import api from '../api';
+
+export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
+export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
+export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL';
+
+export const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST';
+export const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS';
+export const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL';
+
+export const LIST_EDITOR_TITLE_CHANGE = 'LIST_EDITOR_TITLE_CHANGE';
+export const LIST_EDITOR_RESET = 'LIST_EDITOR_RESET';
+export const LIST_EDITOR_SETUP = 'LIST_EDITOR_SETUP';
+
+export const LIST_CREATE_REQUEST = 'LIST_CREATE_REQUEST';
+export const LIST_CREATE_SUCCESS = 'LIST_CREATE_SUCCESS';
+export const LIST_CREATE_FAIL = 'LIST_CREATE_FAIL';
+
+export const LIST_UPDATE_REQUEST = 'LIST_UPDATE_REQUEST';
+export const LIST_UPDATE_SUCCESS = 'LIST_UPDATE_SUCCESS';
+export const LIST_UPDATE_FAIL = 'LIST_UPDATE_FAIL';
+
+export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST';
+export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS';
+export const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL';
+
+export const LIST_ACCOUNTS_FETCH_REQUEST = 'LIST_ACCOUNTS_FETCH_REQUEST';
+export const LIST_ACCOUNTS_FETCH_SUCCESS = 'LIST_ACCOUNTS_FETCH_SUCCESS';
+export const LIST_ACCOUNTS_FETCH_FAIL = 'LIST_ACCOUNTS_FETCH_FAIL';
+
+export const LIST_EDITOR_SUGGESTIONS_CHANGE = 'LIST_EDITOR_SUGGESTIONS_CHANGE';
+export const LIST_EDITOR_SUGGESTIONS_READY = 'LIST_EDITOR_SUGGESTIONS_READY';
+export const LIST_EDITOR_SUGGESTIONS_CLEAR = 'LIST_EDITOR_SUGGESTIONS_CLEAR';
+
+export const LIST_EDITOR_ADD_REQUEST = 'LIST_EDITOR_ADD_REQUEST';
+export const LIST_EDITOR_ADD_SUCCESS = 'LIST_EDITOR_ADD_SUCCESS';
+export const LIST_EDITOR_ADD_FAIL = 'LIST_EDITOR_ADD_FAIL';
+
+export const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST';
+export const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS';
+export const LIST_EDITOR_REMOVE_FAIL = 'LIST_EDITOR_REMOVE_FAIL';
+
+export const fetchList = id => (dispatch, getState) => {
+ if (getState().getIn(['lists', id])) {
+ return;
+ }
+
+ dispatch(fetchListRequest(id));
+
+ api(getState).get(`/api/v1/lists/${id}`)
+ .then(({ data }) => dispatch(fetchListSuccess(data)))
+ .catch(err => dispatch(fetchListFail(id, err)));
+};
+
+export const fetchListRequest = id => ({
+ type: LIST_FETCH_REQUEST,
+ id,
+});
+
+export const fetchListSuccess = list => ({
+ type: LIST_FETCH_SUCCESS,
+ list,
+});
+
+export const fetchListFail = (id, error) => ({
+ type: LIST_FETCH_FAIL,
+ id,
+ error,
+});
+
+export const fetchLists = () => (dispatch, getState) => {
+ dispatch(fetchListsRequest());
+
+ api(getState).get('/api/v1/lists')
+ .then(({ data }) => dispatch(fetchListsSuccess(data)))
+ .catch(err => dispatch(fetchListsFail(err)));
+};
+
+export const fetchListsRequest = () => ({
+ type: LISTS_FETCH_REQUEST,
+});
+
+export const fetchListsSuccess = lists => ({
+ type: LISTS_FETCH_SUCCESS,
+ lists,
+});
+
+export const fetchListsFail = error => ({
+ type: LISTS_FETCH_FAIL,
+ error,
+});
+
+export const submitListEditor = shouldReset => (dispatch, getState) => {
+ const listId = getState().getIn(['listEditor', 'listId']);
+ const title = getState().getIn(['listEditor', 'title']);
+
+ if (listId === null) {
+ dispatch(createList(title, shouldReset));
+ } else {
+ dispatch(updateList(listId, title, shouldReset));
+ }
+};
+
+export const setupListEditor = listId => (dispatch, getState) => {
+ dispatch({
+ type: LIST_EDITOR_SETUP,
+ list: getState().getIn(['lists', listId]),
+ });
+
+ dispatch(fetchListAccounts(listId));
+};
+
+export const changeListEditorTitle = value => ({
+ type: LIST_EDITOR_TITLE_CHANGE,
+ value,
+});
+
+export const createList = (title, shouldReset) => (dispatch, getState) => {
+ dispatch(createListRequest());
+
+ api(getState).post('/api/v1/lists', { title }).then(({ data }) => {
+ dispatch(createListSuccess(data));
+
+ if (shouldReset) {
+ dispatch(resetListEditor());
+ }
+ }).catch(err => dispatch(createListFail(err)));
+};
+
+export const createListRequest = () => ({
+ type: LIST_CREATE_REQUEST,
+});
+
+export const createListSuccess = list => ({
+ type: LIST_CREATE_SUCCESS,
+ list,
+});
+
+export const createListFail = error => ({
+ type: LIST_CREATE_FAIL,
+ error,
+});
+
+export const updateList = (id, title, shouldReset) => (dispatch, getState) => {
+ dispatch(updateListRequest(id));
+
+ api(getState).put(`/api/v1/lists/${id}`, { title }).then(({ data }) => {
+ dispatch(updateListSuccess(data));
+
+ if (shouldReset) {
+ dispatch(resetListEditor());
+ }
+ }).catch(err => dispatch(updateListFail(id, err)));
+};
+
+export const updateListRequest = id => ({
+ type: LIST_UPDATE_REQUEST,
+ id,
+});
+
+export const updateListSuccess = list => ({
+ type: LIST_UPDATE_SUCCESS,
+ list,
+});
+
+export const updateListFail = (id, error) => ({
+ type: LIST_UPDATE_FAIL,
+ id,
+ error,
+});
+
+export const resetListEditor = () => ({
+ type: LIST_EDITOR_RESET,
+});
+
+export const deleteList = id => (dispatch, getState) => {
+ dispatch(deleteListRequest(id));
+
+ api(getState).delete(`/api/v1/lists/${id}`)
+ .then(() => dispatch(deleteListSuccess(id)))
+ .catch(err => dispatch(deleteListFail(id, err)));
+};
+
+export const deleteListRequest = id => ({
+ type: LIST_DELETE_REQUEST,
+ id,
+});
+
+export const deleteListSuccess = id => ({
+ type: LIST_DELETE_SUCCESS,
+ id,
+});
+
+export const deleteListFail = (id, error) => ({
+ type: LIST_DELETE_FAIL,
+ id,
+ error,
+});
+
+export const fetchListAccounts = listId => (dispatch, getState) => {
+ dispatch(fetchListAccountsRequest(listId));
+
+ api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } })
+ .then(({ data }) => dispatch(fetchListAccountsSuccess(listId, data)))
+ .catch(err => dispatch(fetchListAccountsFail(listId, err)));
+};
+
+export const fetchListAccountsRequest = id => ({
+ type: LIST_ACCOUNTS_FETCH_REQUEST,
+ id,
+});
+
+export const fetchListAccountsSuccess = (id, accounts, next) => ({
+ type: LIST_ACCOUNTS_FETCH_SUCCESS,
+ id,
+ accounts,
+ next,
+});
+
+export const fetchListAccountsFail = (id, error) => ({
+ type: LIST_ACCOUNTS_FETCH_FAIL,
+ id,
+ error,
+});
+
+export const fetchListSuggestions = q => (dispatch, getState) => {
+ const params = {
+ q,
+ resolve: false,
+ limit: 4,
+ following: true,
+ };
+
+ api(getState).get('/api/v1/accounts/search', { params })
+ .then(({ data }) => dispatch(fetchListSuggestionsReady(q, data)));
+};
+
+export const fetchListSuggestionsReady = (query, accounts) => ({
+ type: LIST_EDITOR_SUGGESTIONS_READY,
+ query,
+ accounts,
+});
+
+export const clearListSuggestions = () => ({
+ type: LIST_EDITOR_SUGGESTIONS_CLEAR,
+});
+
+export const changeListSuggestions = value => ({
+ type: LIST_EDITOR_SUGGESTIONS_CHANGE,
+ value,
+});
+
+export const addToListEditor = accountId => (dispatch, getState) => {
+ dispatch(addToList(getState().getIn(['listEditor', 'listId']), accountId));
+};
+
+export const addToList = (listId, accountId) => (dispatch, getState) => {
+ dispatch(addToListRequest(listId, accountId));
+
+ api(getState).post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] })
+ .then(() => dispatch(addToListSuccess(listId, accountId)))
+ .catch(err => dispatch(addToListFail(listId, accountId, err)));
+};
+
+export const addToListRequest = (listId, accountId) => ({
+ type: LIST_EDITOR_ADD_REQUEST,
+ listId,
+ accountId,
+});
+
+export const addToListSuccess = (listId, accountId) => ({
+ type: LIST_EDITOR_ADD_SUCCESS,
+ listId,
+ accountId,
+});
+
+export const addToListFail = (listId, accountId, error) => ({
+ type: LIST_EDITOR_ADD_FAIL,
+ listId,
+ accountId,
+ error,
+});
+
+export const removeFromListEditor = accountId => (dispatch, getState) => {
+ dispatch(removeFromList(getState().getIn(['listEditor', 'listId']), accountId));
+};
+
+export const removeFromList = (listId, accountId) => (dispatch, getState) => {
+ dispatch(removeFromListRequest(listId, accountId));
+
+ api(getState).delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } })
+ .then(() => dispatch(removeFromListSuccess(listId, accountId)))
+ .catch(err => dispatch(removeFromListFail(listId, accountId, err)));
+};
+
+export const removeFromListRequest = (listId, accountId) => ({
+ type: LIST_EDITOR_REMOVE_REQUEST,
+ listId,
+ accountId,
+});
+
+export const removeFromListSuccess = (listId, accountId) => ({
+ type: LIST_EDITOR_REMOVE_SUCCESS,
+ listId,
+ accountId,
+});
+
+export const removeFromListFail = (listId, accountId, error) => ({
+ type: LIST_EDITOR_REMOVE_FAIL,
+ listId,
+ accountId,
+ error,
+});
diff --git a/app/javascript/mastodon/actions/mutes.js b/app/javascript/mastodon/actions/mutes.js
index febda7219..daa76a8f7 100644
--- a/app/javascript/mastodon/actions/mutes.js
+++ b/app/javascript/mastodon/actions/mutes.js
@@ -1,5 +1,6 @@
import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
+import { openModal } from './modal';
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
@@ -9,6 +10,9 @@ export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
+export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
+export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
+
export function fetchMutes() {
return (dispatch, getState) => {
dispatch(fetchMutesRequest());
@@ -80,3 +84,20 @@ export function expandMutesFail(error) {
error,
};
};
+
+export function initMuteModal(account) {
+ return dispatch => {
+ dispatch({
+ type: MUTES_INIT_MODAL,
+ account,
+ });
+
+ dispatch(openModal('MUTE'));
+ };
+}
+
+export function toggleHideNotifications() {
+ return dispatch => {
+ dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
+ };
+}
diff --git a/app/javascript/mastodon/actions/pin_statuses.js b/app/javascript/mastodon/actions/pin_statuses.js
index 01bf8930b..3f40f6c2d 100644
--- a/app/javascript/mastodon/actions/pin_statuses.js
+++ b/app/javascript/mastodon/actions/pin_statuses.js
@@ -4,12 +4,13 @@ export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
+import { me } from '../initial_state';
+
export function fetchPinnedStatuses() {
return (dispatch, getState) => {
dispatch(fetchPinnedStatusesRequest());
- const accountId = getState().getIn(['meta', 'me']);
- api(getState).get(`/api/v1/accounts/${accountId}/statuses`, { params: { pinned: true } }).then(response => {
+ api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => {
dispatch(fetchPinnedStatusesSuccess(response.data, null));
}).catch(error => {
dispatch(fetchPinnedStatusesFail(error));
diff --git a/app/javascript/mastodon/actions/push_notifications.js b/app/javascript/mastodon/actions/push_notifications.js
index 55661d2b0..de06385f9 100644
--- a/app/javascript/mastodon/actions/push_notifications.js
+++ b/app/javascript/mastodon/actions/push_notifications.js
@@ -1,4 +1,5 @@
import axios from 'axios';
+import { pushNotificationsSetting } from '../settings';
export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT';
export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION';
@@ -42,11 +43,15 @@ export function saveSettings() {
const state = getState().get('push_notifications');
const subscription = state.get('subscription');
const alerts = state.get('alerts');
+ const data = { alerts };
axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
- data: {
- alerts,
- },
+ data,
+ }).then(() => {
+ const me = getState().getIn(['meta', 'me']);
+ if (me) {
+ pushNotificationsSetting.set(me, data);
+ }
});
};
}
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index 7802694a3..c22152edd 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -1,4 +1,4 @@
-import createStream from '../stream';
+import { connectStream } from '../stream';
import {
updateTimeline,
deleteFromTimelines,
@@ -12,42 +12,19 @@ import { getLocale } from '../locales';
const { messages } = getLocale();
export function connectTimelineStream (timelineId, path, pollingRefresh = null) {
- return (dispatch, getState) => {
- const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
- const accessToken = getState().getIn(['meta', 'access_token']);
+
+ return connectStream (path, pollingRefresh, (dispatch, getState) => {
const locale = getState().getIn(['meta', 'locale']);
- let polling = null;
-
- const setupPolling = () => {
- polling = setInterval(() => {
- pollingRefresh(dispatch);
- }, 20000);
- };
-
- const clearPolling = () => {
- if (polling) {
- clearInterval(polling);
- polling = null;
- }
- };
-
- const subscription = createStream(streamingAPIBaseURL, accessToken, path, {
-
- connected () {
- if (pollingRefresh) {
- clearPolling();
- }
+ return {
+ onConnect() {
dispatch(connectTimeline(timelineId));
},
- disconnected () {
- if (pollingRefresh) {
- setupPolling();
- }
+ onDisconnect() {
dispatch(disconnectTimeline(timelineId));
},
- received (data) {
+ onReceive (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline(timelineId, JSON.parse(data.payload)));
@@ -60,26 +37,8 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null)
break;
}
},
-
- reconnected () {
- if (pollingRefresh) {
- clearPolling();
- pollingRefresh(dispatch);
- }
- dispatch(connectTimeline(timelineId));
- },
-
- });
-
- const disconnect = () => {
- if (subscription) {
- subscription.close();
- }
- clearPolling();
};
-
- return disconnect;
- };
+ });
}
function refreshHomeTimelineAndNotification (dispatch) {
@@ -92,3 +51,4 @@ export const connectCommunityStream = () => connectTimelineStream('community', '
export const connectMediaStream = () => connectTimelineStream('community', 'public:local');
export const connectPublicStream = () => connectTimelineStream('public', 'public');
export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
+export const connectListStream = (id) => connectTimelineStream(`list:${id}`, `list&list=${id}`);
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index 09abe2702..f8843d1d9 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -118,6 +118,7 @@ export const refreshCommunityTimeline = () => refreshTimeline('community', '/
export const refreshAccountTimeline = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
+export const refreshListTimeline = id => refreshTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
export function refreshTimelineFail(timeline, error, skipLoading) {
return {
@@ -158,6 +159,7 @@ export const expandCommunityTimeline = () => expandTimeline('community', '/ap
export const expandAccountTimeline = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
+export const expandListTimeline = id => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
export function expandTimelineRequest(timeline) {
return {
diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js
index d614a52c9..b0479db4f 100644
--- a/app/javascript/mastodon/components/account.js
+++ b/app/javascript/mastodon/components/account.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { Fragment } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from './avatar';
@@ -7,6 +7,7 @@ import Permalink from './permalink';
import IconButton from './icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { me } from '../initial_state';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
@@ -14,6 +15,8 @@ const messages = defineMessages({
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
+ mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },
+ unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
});
@injectIntl
@@ -21,7 +24,6 @@ export default class Account extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
- me: PropTypes.string.isRequired,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
@@ -41,8 +43,16 @@ export default class Account extends ImmutablePureComponent {
this.props.onMute(this.props.account);
}
+ handleMuteNotifications = () => {
+ this.props.onMuteNotifications(this.props.account, true);
+ }
+
+ handleUnmuteNotifications = () => {
+ this.props.onMuteNotifications(this.props.account, false);
+ }
+
render () {
- const { account, me, intl, hidden } = this.props;
+ const { account, intl, hidden } = this.props;
if (!account) {
return
;
@@ -70,8 +80,19 @@ export default class Account extends ImmutablePureComponent {
} else if (blocking) {
buttons = ;
} else if (muting) {
- buttons = ;
- } else {
+ let hidingNotificationsButton;
+ if (account.getIn(['relationship', 'muting_notifications'])) {
+ hidingNotificationsButton = ;
+ } else {
+ hidingNotificationsButton = ;
+ }
+ buttons = (
+
+
+ {hidingNotificationsButton}
+
+ );
+ } else if (!account.get('moved')) {
buttons = ;
}
}
diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js
index 14a8d4c38..6a16e2fc7 100644
--- a/app/javascript/mastodon/components/autosuggest_textarea.js
+++ b/app/javascript/mastodon/components/autosuggest_textarea.js
@@ -209,6 +209,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
onBlur={this.onBlur}
onPaste={this.onPaste}
style={style}
+ aria-autocomplete='list'
/>
diff --git a/app/javascript/mastodon/components/avatar.js b/app/javascript/mastodon/components/avatar.js
index f7c484ee3..570505833 100644
--- a/app/javascript/mastodon/components/avatar.js
+++ b/app/javascript/mastodon/components/avatar.js
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
+import { autoPlayGif } from '../initial_state';
export default class Avatar extends React.PureComponent {
@@ -8,12 +9,12 @@ export default class Avatar extends React.PureComponent {
account: ImmutablePropTypes.map.isRequired,
size: PropTypes.number.isRequired,
style: PropTypes.object,
- animate: PropTypes.bool,
inline: PropTypes.bool,
+ animate: PropTypes.bool,
};
static defaultProps = {
- animate: false,
+ animate: autoPlayGif,
size: 20,
inline: false,
};
diff --git a/app/javascript/mastodon/components/avatar_overlay.js b/app/javascript/mastodon/components/avatar_overlay.js
index f5d67b34e..3ec1d7730 100644
--- a/app/javascript/mastodon/components/avatar_overlay.js
+++ b/app/javascript/mastodon/components/avatar_overlay.js
@@ -1,22 +1,29 @@
import React from 'react';
+import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
+import { autoPlayGif } from '../initial_state';
export default class AvatarOverlay extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
friend: ImmutablePropTypes.map.isRequired,
+ animate: PropTypes.bool,
+ };
+
+ static defaultProps = {
+ animate: autoPlayGif,
};
render() {
- const { account, friend } = this.props;
+ const { account, friend, animate } = this.props;
const baseStyle = {
- backgroundImage: `url(${account.get('avatar_static')})`,
+ backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
};
const overlayStyle = {
- backgroundImage: `url(${friend.get('avatar_static')})`,
+ backgroundImage: `url(${friend.get(animate ? 'avatar' : 'avatar_static')})`,
};
return (
diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js
index e4fa8fa7a..80a8fbdb3 100644
--- a/app/javascript/mastodon/components/column_header.js
+++ b/app/javascript/mastodon/components/column_header.js
@@ -137,7 +137,9 @@ export default class ColumnHeader extends React.PureComponent {
- {title}
+
+ {title}
+
{backButton}
diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js
index 3a3ebf487..43dc0d6e3 100644
--- a/app/javascript/mastodon/components/dropdown_menu.js
+++ b/app/javascript/mastodon/components/dropdown_menu.js
@@ -110,7 +110,7 @@ export default class Dropdown extends React.PureComponent {
icon: PropTypes.string.isRequired,
items: PropTypes.array.isRequired,
size: PropTypes.number.isRequired,
- ariaLabel: PropTypes.string,
+ title: PropTypes.string,
disabled: PropTypes.bool,
status: ImmutablePropTypes.map,
isUserTouching: PropTypes.func,
@@ -120,7 +120,7 @@ export default class Dropdown extends React.PureComponent {
};
static defaultProps = {
- ariaLabel: 'Menu',
+ title: 'Menu',
};
state = {
@@ -186,14 +186,14 @@ export default class Dropdown extends React.PureComponent {
}
render () {
- const { icon, items, size, ariaLabel, disabled } = this.props;
+ const { icon, items, size, title, disabled } = this.props;
const { expanded } = this.state;
return (
components unless
+ // we actually need to animate.
+ return (
+
+ );
+ }
+
return (
{({ rotate }) =>
diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js
index fb71d8c5c..20febdb16 100644
--- a/app/javascript/mastodon/components/media_gallery.js
+++ b/app/javascript/mastodon/components/media_gallery.js
@@ -6,6 +6,7 @@ import IconButton from './icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { isIOS } from '../is_mobile';
import classNames from 'classnames';
+import { autoPlayGif } from '../initial_state';
const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
@@ -23,11 +24,9 @@ class Item extends React.PureComponent {
index: PropTypes.number.isRequired,
size: PropTypes.number.isRequired,
onClick: PropTypes.func.isRequired,
- autoPlayGif: PropTypes.bool,
};
static defaultProps = {
- autoPlayGif: false,
standalone: false,
index: 0,
size: 1,
@@ -47,7 +46,7 @@ class Item extends React.PureComponent {
}
hoverToPlay () {
- const { attachment, autoPlayGif } = this.props;
+ const { attachment } = this.props;
return !autoPlayGif && attachment.get('type') === 'gifv';
}
@@ -139,7 +138,7 @@ class Item extends React.PureComponent {
);
} else if (attachment.get('type') === 'gifv') {
- const autoPlay = !isIOS() && this.props.autoPlayGif;
+ const autoPlay = !isIOS() && autoPlayGif;
thumbnail = (
@@ -181,11 +180,9 @@ export default class MediaGallery extends React.PureComponent {
height: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
- autoPlayGif: PropTypes.bool,
};
static defaultProps = {
- autoPlayGif: false,
standalone: false,
};
@@ -261,9 +258,9 @@ export default class MediaGallery extends React.PureComponent {
const size = media.take(4).size;
if (this.isStandaloneEligible()) {
- children =
;
+ children =
;
} else {
- children = media.take(4).map((attachment, i) =>
);
+ children = media.take(4).map((attachment, i) =>
);
}
}
diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js
index ab9d48510..71228ca6c 100644
--- a/app/javascript/mastodon/components/scrollable_list.js
+++ b/app/javascript/mastodon/components/scrollable_list.js
@@ -1,5 +1,5 @@
import React, { PureComponent } from 'react';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
import PropTypes from 'prop-types';
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
import LoadMore from './load_more';
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index 70005436b..d23ff87fa 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -36,9 +36,6 @@ export default class Status extends ImmutablePureComponent {
onBlock: PropTypes.func,
onEmbed: PropTypes.func,
onHeightChange: PropTypes.func,
- me: PropTypes.string,
- boostModal: PropTypes.bool,
- autoPlayGif: PropTypes.bool,
muted: PropTypes.bool,
hidden: PropTypes.bool,
onMoveUp: PropTypes.func,
@@ -54,9 +51,6 @@ export default class Status extends ImmutablePureComponent {
updateOnProps = [
'status',
'account',
- 'me',
- 'boostModal',
- 'autoPlayGif',
'muted',
'hidden',
]
@@ -197,7 +191,7 @@ export default class Status extends ImmutablePureComponent {
} else {
media = (
- {Component => }
+ {Component => }
);
}
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
index e952733f3..cd59c7845 100644
--- a/app/javascript/mastodon/components/status_action_bar.js
+++ b/app/javascript/mastodon/components/status_action_bar.js
@@ -5,6 +5,7 @@ import IconButton from './icon_button';
import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { me } from '../initial_state';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -47,7 +48,6 @@ export default class StatusActionBar extends ImmutablePureComponent {
onEmbed: PropTypes.func,
onMuteConversation: PropTypes.func,
onPin: PropTypes.func,
- me: PropTypes.string,
withDismiss: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
@@ -56,7 +56,6 @@ export default class StatusActionBar extends ImmutablePureComponent {
// evaluate to false. See react-immutable-pure-component for usage.
updateOnProps = [
'status',
- 'me',
'withDismiss',
]
@@ -116,7 +115,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
}
render () {
- const { status, me, intl, withDismiss } = this.props;
+ const { status, intl, withDismiss } = this.props;
const mutingConversation = status.get('muted');
const anonymousAccess = !me;
@@ -180,7 +179,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
{shareButton}
-
+
);
diff --git a/app/javascript/mastodon/containers/account_container.js b/app/javascript/mastodon/containers/account_container.js
index 7c77cb764..5a5136dd1 100644
--- a/app/javascript/mastodon/containers/account_container.js
+++ b/app/javascript/mastodon/containers/account_container.js
@@ -12,6 +12,8 @@ import {
unmuteAccount,
} from '../actions/accounts';
import { openModal } from '../actions/modal';
+import { initMuteModal } from '../actions/mutes';
+import { unfollowModal } from '../initial_state';
const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
@@ -22,8 +24,6 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => ({
account: getAccount(state, props.id),
- me: state.getIn(['meta', 'me']),
- unfollowModal: state.getIn(['meta', 'unfollow_modal']),
});
return mapStateToProps;
@@ -33,7 +33,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
- if (this.unfollowModal) {
+ if (unfollowModal) {
dispatch(openModal('CONFIRM', {
message: @{account.get('acct')} }} />,
confirm: intl.formatMessage(messages.unfollowConfirm),
@@ -59,10 +59,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (account.getIn(['relationship', 'muting'])) {
dispatch(unmuteAccount(account.get('id')));
} else {
- dispatch(muteAccount(account.get('id')));
+ dispatch(initMuteModal(account));
}
},
+
+ onMuteNotifications (account, notifications) {
+ dispatch(muteAccount(account.get('id'), notifications));
+ },
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account));
diff --git a/app/javascript/mastodon/containers/compose_container.js b/app/javascript/mastodon/containers/compose_container.js
index db452d03a..5ee1d2f14 100644
--- a/app/javascript/mastodon/containers/compose_container.js
+++ b/app/javascript/mastodon/containers/compose_container.js
@@ -6,15 +6,14 @@ import { hydrateStore } from '../actions/store';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import Compose from '../features/standalone/compose';
+import initialState from '../initial_state';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
const store = configureStore();
-const initialStateContainer = document.getElementById('initial-state');
-if (initialStateContainer !== null) {
- const initialState = JSON.parse(initialStateContainer.textContent);
+if (initialState) {
store.dispatch(hydrateStore(initialState));
}
diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js
index 56b7bda46..d1710445b 100644
--- a/app/javascript/mastodon/containers/mastodon.js
+++ b/app/javascript/mastodon/containers/mastodon.js
@@ -4,18 +4,19 @@ import PropTypes from 'prop-types';
import configureStore from '../store/configureStore';
import { showOnboardingOnce } from '../actions/onboarding';
import { BrowserRouter, Route } from 'react-router-dom';
-import { ScrollContext } from 'react-router-scroll';
+import { ScrollContext } from 'react-router-scroll-4';
import UI from '../features/ui';
import { hydrateStore } from '../actions/store';
import { connectUserStream } from '../actions/streaming';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
+import initialState from '../initial_state';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
export const store = configureStore();
-const hydrateAction = hydrateStore(JSON.parse(document.getElementById('initial-state').textContent));
+const hydrateAction = hydrateStore(initialState);
store.dispatch(hydrateAction);
export default class Mastodon extends React.PureComponent {
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js
index c61b7d00d..b22540204 100644
--- a/app/javascript/mastodon/containers/status_container.js
+++ b/app/javascript/mastodon/containers/status_container.js
@@ -14,20 +14,18 @@ import {
pin,
unpin,
} from '../actions/interactions';
-import {
- blockAccount,
- muteAccount,
-} from '../actions/accounts';
+import { blockAccount } from '../actions/accounts';
import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses';
+import { initMuteModal } from '../actions/mutes';
import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { boostModal, deleteModal } from '../initial_state';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
- muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' },
});
const makeMapStateToProps = () => {
@@ -35,10 +33,6 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => ({
status: getStatus(state, props.id),
- me: state.getIn(['meta', 'me']),
- boostModal: state.getIn(['meta', 'boost_modal']),
- deleteModal: state.getIn(['meta', 'delete_modal']),
- autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
});
return mapStateToProps;
@@ -58,7 +52,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (status.get('reblogged')) {
dispatch(unreblog(status));
} else {
- if (e.shiftKey || !this.boostModal) {
+ if (e.shiftKey || !boostModal) {
this.onModalReblog(status);
} else {
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
@@ -87,7 +81,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
},
onDelete (status) {
- if (!this.deleteModal) {
+ if (!deleteModal) {
dispatch(deleteStatus(status.get('id')));
} else {
dispatch(openModal('CONFIRM', {
@@ -123,11 +117,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
},
onMute (account) {
- dispatch(openModal('CONFIRM', {
- message: @{account.get('acct')} }} />,
- confirm: intl.formatMessage(messages.muteConfirm),
- onConfirm: () => dispatch(muteAccount(account.get('id'))),
- }));
+ dispatch(initMuteModal(account));
},
onMuteConversation (status) {
diff --git a/app/javascript/mastodon/containers/timeline_container.js b/app/javascript/mastodon/containers/timeline_container.js
index 4be037955..e84c921ee 100644
--- a/app/javascript/mastodon/containers/timeline_container.js
+++ b/app/javascript/mastodon/containers/timeline_container.js
@@ -7,15 +7,14 @@ import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import PublicTimeline from '../features/standalone/public_timeline';
import HashtagTimeline from '../features/standalone/hashtag_timeline';
+import initialState from '../initial_state';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
const store = configureStore();
-const initialStateContainer = document.getElementById('initial-state');
-if (initialStateContainer !== null) {
- const initialState = JSON.parse(initialStateContainer.textContent);
+if (initialState) {
store.dispatch(hydrateStore(initialState));
}
diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js
index 2819ae252..cb849fa5d 100644
--- a/app/javascript/mastodon/features/account/components/action_bar.js
+++ b/app/javascript/mastodon/features/account/components/action_bar.js
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { Link } from 'react-router-dom';
import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl';
+import { me } from '../../../initial_state';
const messages = defineMessages({
mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
@@ -19,6 +20,8 @@ const messages = defineMessages({
media: { id: 'account.media', defaultMessage: 'Media' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
+ hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
+ showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
});
@injectIntl
@@ -26,10 +29,10 @@ export default class ActionBar extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
- me: PropTypes.string.isRequired,
onFollow: PropTypes.func,
onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
+ onReblogToggle: PropTypes.func.isRequired,
onReport: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
onBlockDomain: PropTypes.func.isRequired,
@@ -44,7 +47,7 @@ export default class ActionBar extends React.PureComponent {
}
render () {
- const { account, me, intl } = this.props;
+ const { account, intl } = this.props;
let menu = [];
let extraInfo = '';
@@ -60,6 +63,14 @@ export default class ActionBar extends React.PureComponent {
if (account.get('id') === me) {
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
} else {
+ if (account.getIn(['relationship', 'following'])) {
+ 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 });
+ }
+ }
+
if (account.getIn(['relationship', 'muting'])) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
} else {
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 07a6c5dec..b2399ae9b 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -5,8 +5,9 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import IconButton from '../../../components/icon_button';
import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
-import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { autoPlayGif, me } from '../../../initial_state';
+import classNames from 'classnames';
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@@ -14,19 +15,10 @@ const messages = defineMessages({
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
});
-const makeMapStateToProps = () => {
- const mapStateToProps = state => ({
- autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
- });
-
- return mapStateToProps;
-};
-
class Avatar extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
- autoPlayGif: PropTypes.bool.isRequired,
};
state = {
@@ -44,7 +36,7 @@ class Avatar extends ImmutablePureComponent {
}
render () {
- const { account, autoPlayGif } = this.props;
+ const { account } = this.props;
const { isHovered } = this.state;
return (
@@ -71,20 +63,17 @@ class Avatar extends ImmutablePureComponent {
}
-@connect(makeMapStateToProps)
@injectIntl
export default class Header extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
- me: PropTypes.string.isRequired,
onFollow: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
- autoPlayGif: PropTypes.bool.isRequired,
};
render () {
- const { account, me, intl } = this.props;
+ const { account, intl } = this.props;
if (!account) {
return null;
@@ -114,6 +103,10 @@ export default class Header extends ImmutablePureComponent {
}
}
+ if (account.get('moved')) {
+ actionBtn = '';
+ }
+
if (account.get('locked')) {
lockedIcon = ;
}
@@ -122,9 +115,9 @@ export default class Header extends ImmutablePureComponent {
const displayNameHtml = { __html: account.get('display_name_html') };
return (
-
+
-
+
@{account.get('acct')} {lockedIcon}
diff --git a/app/javascript/mastodon/features/account_gallery/index.js b/app/javascript/mastodon/features/account_gallery/index.js
index 2a88addc4..a40722417 100644
--- a/app/javascript/mastodon/features/account_gallery/index.js
+++ b/app/javascript/mastodon/features/account_gallery/index.js
@@ -12,14 +12,13 @@ import { getAccountGallery } from '../../selectors';
import MediaItem from './components/media_item';
import HeaderContainer from '../account_timeline/containers/header_container';
import { FormattedMessage } from 'react-intl';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
import LoadMore from '../../components/load_more';
const mapStateToProps = (state, props) => ({
medias: getAccountGallery(state, props.params.accountId),
isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}:media`, 'next']),
- autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
});
@connect(mapStateToProps)
@@ -31,7 +30,6 @@ export default class AccountGallery extends ImmutablePureComponent {
medias: ImmutablePropTypes.list.isRequired,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
- autoPlayGif: PropTypes.bool,
};
componentDidMount () {
@@ -67,7 +65,7 @@ export default class AccountGallery extends ImmutablePureComponent {
}
render () {
- const { medias, autoPlayGif, isLoading, hasMore } = this.props;
+ const { medias, isLoading, hasMore } = this.props;
let loadMore = null;
@@ -100,7 +98,6 @@ export default class AccountGallery extends ImmutablePureComponent {
)}
{loadMore}
diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js
index edfedb864..0ddb6b6c1 100644
--- a/app/javascript/mastodon/features/account_timeline/components/header.js
+++ b/app/javascript/mastodon/features/account_timeline/components/header.js
@@ -5,15 +5,16 @@ import InnerHeader from '../../account/components/header';
import ActionBar from '../../account/components/action_bar';
import MissingIndicator from '../../../components/missing_indicator';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import MovedNote from './moved_note';
export default class Header extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
- me: PropTypes.string.isRequired,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
+ onReblogToggle: PropTypes.func.isRequired,
onReport: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
onBlockDomain: PropTypes.func.isRequired,
@@ -40,6 +41,10 @@ export default class Header extends ImmutablePureComponent {
this.props.onReport(this.props.account);
}
+ handleReblogToggle = () => {
+ this.props.onReblogToggle(this.props.account);
+ }
+
handleMute = () => {
this.props.onMute(this.props.account);
}
@@ -61,7 +66,7 @@ export default class Header extends ImmutablePureComponent {
}
render () {
- const { account, me } = this.props;
+ const { account } = this.props;
if (account === null) {
return
;
@@ -69,17 +74,18 @@ export default class Header extends ImmutablePureComponent {
return (
+ {account.get('moved') &&
}
+
{
+ if (e.button === 0) {
+ e.preventDefault();
+ this.context.router.history.push(`/accounts/${this.props.to.get('id')}`);
+ }
+
+ e.stopPropagation();
+ }
+
+ render () {
+ const { from, to } = this.props;
+ const displayNameHtml = { __html: from.get('display_name_html') };
+
+ return (
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
index ab75b40de..b5e0e9a3f 100644
--- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js
+++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
@@ -7,19 +7,19 @@ import {
unfollowAccount,
blockAccount,
unblockAccount,
- muteAccount,
unmuteAccount,
} from '../../../actions/accounts';
import { mentionCompose } from '../../../actions/compose';
+import { initMuteModal } from '../../../actions/mutes';
import { initReport } from '../../../actions/reports';
import { openModal } from '../../../actions/modal';
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { unfollowModal } from '../../../initial_state';
const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
- muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
});
@@ -28,8 +28,6 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, { accountId }) => ({
account: getAccount(state, accountId),
- me: state.getIn(['meta', 'me']),
- unfollowModal: state.getIn(['meta', 'unfollow_modal']),
});
return mapStateToProps;
@@ -39,7 +37,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
- if (this.unfollowModal) {
+ if (unfollowModal) {
dispatch(openModal('CONFIRM', {
message: @{account.get('acct')} }} />,
confirm: intl.formatMessage(messages.unfollowConfirm),
@@ -69,6 +67,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(mentionCompose(account, router));
},
+ onReblogToggle (account) {
+ if (account.getIn(['relationship', 'showing_reblogs'])) {
+ dispatch(followAccount(account.get('id'), false));
+ } else {
+ dispatch(followAccount(account.get('id'), true));
+ }
+ },
+
onReport (account) {
dispatch(initReport(account));
},
@@ -77,11 +83,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (account.getIn(['relationship', 'muting'])) {
dispatch(unmuteAccount(account.get('id')));
} else {
- dispatch(openModal('CONFIRM', {
- message: @{account.get('acct')} }} />,
- confirm: intl.formatMessage(messages.muteConfirm),
- onConfirm: () => dispatch(muteAccount(account.get('id'))),
- }));
+ dispatch(initMuteModal(account));
}
},
diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js
index fe92216d5..f8c85c296 100644
--- a/app/javascript/mastodon/features/account_timeline/index.js
+++ b/app/javascript/mastodon/features/account_timeline/index.js
@@ -16,7 +16,6 @@ const mapStateToProps = (state, props) => ({
statusIds: state.getIn(['timelines', `account:${props.params.accountId}`, 'items'], ImmutableList()),
isLoading: state.getIn(['timelines', `account:${props.params.accountId}`, 'isLoading']),
hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}`, 'next']),
- me: state.getIn(['meta', 'me']),
});
@connect(mapStateToProps)
@@ -28,7 +27,6 @@ export default class AccountTimeline extends ImmutablePureComponent {
statusIds: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
- me: PropTypes.string.isRequired,
};
componentWillMount () {
@@ -50,7 +48,7 @@ export default class AccountTimeline extends ImmutablePureComponent {
}
render () {
- const { statusIds, isLoading, hasMore, me } = this.props;
+ const { statusIds, isLoading, hasMore } = this.props;
if (!statusIds && isLoading) {
return (
@@ -70,7 +68,6 @@ export default class AccountTimeline extends ImmutablePureComponent {
statusIds={statusIds}
isLoading={isLoading}
hasMore={hasMore}
- me={me}
onScrollToBottom={this.handleScrollToBottom}
/>
diff --git a/app/javascript/mastodon/features/blocks/index.js b/app/javascript/mastodon/features/blocks/index.js
index b16af4b28..14a512ae8 100644
--- a/app/javascript/mastodon/features/blocks/index.js
+++ b/app/javascript/mastodon/features/blocks/index.js
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import LoadingIndicator from '../../components/loading_indicator';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
import Column from '../ui/components/column';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import AccountContainer from '../../containers/account_container';
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js
index 7d175a912..a876c5197 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.js
+++ b/app/javascript/mastodon/features/compose/components/compose_form.js
@@ -41,7 +41,6 @@ export default class ComposeForm extends ImmutablePureComponent {
preselectDate: PropTypes.instanceOf(Date),
is_submitting: PropTypes.bool,
is_uploading: PropTypes.bool,
- me: PropTypes.string,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onClearSuggestions: PropTypes.func.isRequired,
@@ -157,6 +156,8 @@ export default class ComposeForm extends ImmutablePureComponent {
return (
+
+
-
-
@@ -200,11 +199,11 @@ export default class ComposeForm extends ImmutablePureComponent {
+
+
-
-
-
-
+
+
);
diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
index dffa04ff0..dc8fc02ba 100644
--- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
@@ -157,7 +157,6 @@ class EmojiPickerMenu extends React.PureComponent {
intl: PropTypes.object.isRequired,
skinTone: PropTypes.number.isRequired,
onSkinTone: PropTypes.func.isRequired,
- autoPlay: PropTypes.bool,
};
static defaultProps = {
@@ -235,7 +234,7 @@ class EmojiPickerMenu extends React.PureComponent {
}
render () {
- const { loading, style, intl, custom_emojis, autoPlay, skinTone, frequentlyUsedEmojis } = this.props;
+ const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
if (loading) {
return
;
@@ -250,7 +249,7 @@ class EmojiPickerMenu extends React.PureComponent {
perLine={8}
emojiSize={22}
sheetSize={32}
- custom={buildCustomEmojis(custom_emojis, autoPlay)}
+ custom={buildCustomEmojis(custom_emojis)}
color=''
emoji=''
set='twitter'
@@ -284,7 +283,6 @@ export default class EmojiPickerDropdown extends React.PureComponent {
static propTypes = {
custom_emojis: ImmutablePropTypes.list,
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
- autoPlay: PropTypes.bool,
intl: PropTypes.object.isRequired,
onPickEmoji: PropTypes.func.isRequired,
onSkinTone: PropTypes.func.isRequired,
@@ -346,7 +344,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
}
render () {
- const { intl, onPickEmoji, autoPlay, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
+ const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
const title = intl.formatMessage(messages.emoji);
const { active, loading } = this.state;
@@ -366,7 +364,6 @@ export default class EmojiPickerDropdown extends React.PureComponent {
loading={loading}
onClose={this.onHideDropdown}
onPick={onPickEmoji}
- autoPlay={autoPlay}
onSkinTone={onSkinTone}
skinTone={skinTone}
frequentlyUsedEmojis={frequentlyUsedEmojis}
diff --git a/app/javascript/mastodon/features/compose/components/navigation_bar.js b/app/javascript/mastodon/features/compose/components/navigation_bar.js
index 7f346854c..3014c4033 100644
--- a/app/javascript/mastodon/features/compose/components/navigation_bar.js
+++ b/app/javascript/mastodon/features/compose/components/navigation_bar.js
@@ -11,7 +11,7 @@ export default class NavigationBar extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
- onClose: PropTypes.func.isRequired,
+ onClose: PropTypes.func,
};
render () {
diff --git a/app/javascript/mastodon/features/compose/components/reply_indicator.js b/app/javascript/mastodon/features/compose/components/reply_indicator.js
index 7672440b4..d8cda96f3 100644
--- a/app/javascript/mastodon/features/compose/components/reply_indicator.js
+++ b/app/javascript/mastodon/features/compose/components/reply_indicator.js
@@ -6,6 +6,7 @@ import IconButton from '../../../components/icon_button';
import DisplayName from '../../../components/display_name';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { isRtl } from '../../../rtl';
const messages = defineMessages({
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
@@ -42,7 +43,10 @@ export default class ReplyIndicator extends ImmutablePureComponent {
return null;
}
- const content = { __html: status.get('contentHtml') };
+ const content = { __html: status.get('contentHtml') };
+ const style = {
+ direction: isRtl(status.get('search_index')) ? 'rtl' : 'ltr',
+ };
return (
@@ -55,7 +59,7 @@ export default class ReplyIndicator extends ImmutablePureComponent {
-
+
);
}
diff --git a/app/javascript/mastodon/features/compose/components/upload.js b/app/javascript/mastodon/features/compose/components/upload.js
index 5d8d66cf7..3a3d17710 100644
--- a/app/javascript/mastodon/features/compose/components/upload.js
+++ b/app/javascript/mastodon/features/compose/components/upload.js
@@ -62,13 +62,13 @@ export default class Upload extends ImmutablePureComponent {
render () {
const { intl, media } = this.props;
const active = this.state.hovered || this.state.focused;
- const description = this.state.dirtyDescription || media.get('description') || '';
+ const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || '';
return (
{({ scale }) => (
-
+
diff --git a/app/javascript/mastodon/features/compose/containers/compose_form_container.js b/app/javascript/mastodon/features/compose/containers/compose_form_container.js
index 12d435ded..5f5509dbe 100644
--- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js
+++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js
@@ -22,7 +22,6 @@ const mapStateToProps = state => ({
preselectDate: state.getIn(['compose', 'preselectDate']),
is_submitting: state.getIn(['compose', 'is_submitting']),
is_uploading: state.getIn(['compose', 'is_uploading']),
- me: state.getIn(['compose', 'me']),
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
});
diff --git a/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js
index 71944128c..e6a535a5d 100644
--- a/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js
+++ b/app/javascript/mastodon/features/compose/containers/emoji_picker_dropdown_container.js
@@ -46,7 +46,7 @@ const getFrequentlyUsedEmojis = createSelector([
const getCustomEmojis = createSelector([
state => state.get('custom_emojis'),
-], emojis => emojis.sort((a, b) => {
+], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => {
const aShort = a.get('shortcode').toLowerCase();
const bShort = b.get('shortcode').toLowerCase();
@@ -61,7 +61,6 @@ const getCustomEmojis = createSelector([
const mapStateToProps = state => ({
custom_emojis: getCustomEmojis(state),
- autoPlay: state.getIn(['meta', 'auto_play_gif']),
skinTone: state.getIn(['settings', 'skinTone']),
frequentlyUsedEmojis: getFrequentlyUsedEmojis(state),
});
diff --git a/app/javascript/mastodon/features/compose/containers/navigation_container.js b/app/javascript/mastodon/features/compose/containers/navigation_container.js
index 8cc53c087..eb9f3ea45 100644
--- a/app/javascript/mastodon/features/compose/containers/navigation_container.js
+++ b/app/javascript/mastodon/features/compose/containers/navigation_container.js
@@ -1,9 +1,10 @@
import { connect } from 'react-redux';
import NavigationBar from '../components/navigation_bar';
+import { me } from '../../../initial_state';
const mapStateToProps = state => {
return {
- account: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
+ account: state.getIn(['accounts', me]),
};
};
diff --git a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js
index e4bd5a743..c8e74f5a1 100644
--- a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js
+++ b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js
@@ -47,7 +47,7 @@ class SensitiveButton extends React.PureComponent {
'compose-form__sensitive-button--visible': visible,
});
return (
-
+
({
- needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', state.getIn(['meta', 'me']), 'locked']),
+ needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
});
const WarningWrapper = ({ needsLockWarning }) => {
diff --git a/app/javascript/mastodon/features/compose/util/counter.js b/app/javascript/mastodon/features/compose/util/counter.js
index e6d2487c5..700ba2163 100644
--- a/app/javascript/mastodon/features/compose/util/counter.js
+++ b/app/javascript/mastodon/features/compose/util/counter.js
@@ -5,5 +5,5 @@ const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx';
export function countableText(inputText) {
return inputText
.replace(urlRegex, urlPlaceholder)
- .replace(/(?:^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/ig, '@$2');
+ .replace(/(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/ig, '$1@$3');
};
diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
index 636402172..372459c78 100644
--- a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
+++ b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
@@ -57,5 +57,21 @@ describe('emoji', () => {
it('does an emoji whose filename is irregular', () => {
expect(emojify('↙️')).toEqual('
');
});
+
+ it('avoid emojifying on invisible text', () => {
+ expect(emojify('http://example.com/test😄'))
+ .toEqual('http://example.com/test😄');
+ expect(emojify(':luigi:', { ':luigi:': { static_url: 'luigi.exe' } }))
+ .toEqual(':luigi:');
+ });
+
+ it('avoid emojifying on invisible text with nested tags', () => {
+ expect(emojify('😄bar😴😇'))
+ .toEqual('😄bar😴
');
+ expect(emojify('😄😕😴😇'))
+ .toEqual('😄😕😴
');
+ expect(emojify('😄
😴😇'))
+ .toEqual('😄
😴
');
+ });
});
});
diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js
index b70fc2b37..0f005dd50 100644
--- a/app/javascript/mastodon/features/emoji/emoji.js
+++ b/app/javascript/mastodon/features/emoji/emoji.js
@@ -1,3 +1,4 @@
+import { autoPlayGif } from '../../initial_state';
import unicodeMapping from './emoji_unicode_mapping_light';
import Trie from 'substring-trie';
@@ -5,13 +6,13 @@ const trie = new Trie(Object.keys(unicodeMapping));
const assetHost = process.env.CDN_HOST || '';
-let allowAnimations = false;
-
const emojify = (str, customEmojis = {}) => {
- let rtn = '';
+ const tagCharsWithoutEmojis = '<&';
+ const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
+ let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0;
for (;;) {
let match, i = 0, tag;
- while (i < str.length && (tag = '<&:'.indexOf(str[i])) === -1 && !(match = trie.search(str.slice(i)))) {
+ while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || !(match = trie.search(str.slice(i))))) {
i += str.codePointAt(i) < 65536 ? 1 : 2;
}
let rend, replacement = '';
@@ -27,7 +28,7 @@ const emojify = (str, customEmojis = {}) => {
// now got a replacee as ':shortname:'
// if you want additional emoji handler, add statements below which set replacement and return true.
if (shortname in customEmojis) {
- const filename = allowAnimations ? customEmojis[shortname].url : customEmojis[shortname].static_url;
+ const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url;
replacement = `
`;
return true;
}
@@ -35,7 +36,26 @@ const emojify = (str, customEmojis = {}) => {
})()) rend = ++i;
} else if (tag >= 0) { // <, &
rend = str.indexOf('>;'[tag], i + 1) + 1;
- if (!rend) break;
+ if (!rend) {
+ break;
+ }
+ if (tag === 0) {
+ if (invisible) {
+ if (str[i + 1] === '/') { // closing tag
+ if (!--invisible) {
+ tagChars = tagCharsWithEmojis;
+ }
+ } else if (str[rend - 2] !== '/') { // opening tag
+ invisible++;
+ }
+ } else {
+ if (str.startsWith('', i)) {
+ // avoid emojifying on invisible text
+ invisible = 1;
+ tagChars = tagCharsWithoutEmojis;
+ }
+ }
+ }
i = rend;
} else { // matched to unicode emoji
const { filename, shortCode } = unicodeMapping[match];
@@ -51,14 +71,12 @@ const emojify = (str, customEmojis = {}) => {
export default emojify;
-export const buildCustomEmojis = (customEmojis, overrideAllowAnimations = false) => {
+export const buildCustomEmojis = (customEmojis) => {
const emojis = [];
- allowAnimations = overrideAllowAnimations;
-
customEmojis.forEach(emoji => {
const shortcode = emoji.get('shortcode');
- const url = allowAnimations ? emoji.get('url') : emoji.get('static_url');
+ const url = autoPlayGif ? emoji.get('url') : emoji.get('static_url');
const name = shortcode.replace(':', '');
emojis.push({
diff --git a/app/javascript/mastodon/features/emoji/emoji_compressed.js b/app/javascript/mastodon/features/emoji/emoji_compressed.js
index c0cba952a..e5b834a74 100644
--- a/app/javascript/mastodon/features/emoji/emoji_compressed.js
+++ b/app/javascript/mastodon/features/emoji/emoji_compressed.js
@@ -64,14 +64,14 @@ Object.keys(emojiMap).forEach(key => {
Object.keys(emojiIndex.emojis).forEach(key => {
const { native } = emojiIndex.emojis[key];
- const { short_names, search, unified } = emojiMartData.emojis[key];
+ let { short_names, search, unified } = emojiMartData.emojis[key];
if (short_names[0] !== key) {
throw new Error('The compresser expects the first short_code to be the ' +
'key. It may need to be rewritten if the emoji change such that this ' +
'is no longer the case.');
}
- short_names.splice(0, 1); // first short name can be inferred from the key
+ short_names = short_names.slice(1); // first short name can be inferred from the key
const searchData = [native, short_names, search];
if (unicodeToUnifiedName(native) !== unified) {
diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js
index 1e1f5873c..67b107bc8 100644
--- a/app/javascript/mastodon/features/favourited_statuses/index.js
+++ b/app/javascript/mastodon/features/favourited_statuses/index.js
@@ -9,6 +9,7 @@ import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import StatusList from '../../components/status_list';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { debounce } from 'lodash';
const messages = defineMessages({
heading: { id: 'column.favourites', defaultMessage: 'Favourites' },
@@ -16,6 +17,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
+ isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
});
@@ -30,6 +32,7 @@ export default class Favourites extends ImmutablePureComponent {
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
hasMore: PropTypes.bool,
+ isLoading: PropTypes.bool,
};
componentWillMount () {
@@ -59,12 +62,12 @@ export default class Favourites extends ImmutablePureComponent {
this.column = c;
}
- handleScrollToBottom = () => {
+ handleScrollToBottom = debounce(() => {
this.props.dispatch(expandFavouritedStatuses());
- }
+ }, 300, { leading: true })
render () {
- const { intl, statusIds, columnId, multiColumn, hasMore } = this.props;
+ const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
const pinned = !!columnId;
return (
@@ -85,6 +88,7 @@ export default class Favourites extends ImmutablePureComponent {
statusIds={statusIds}
scrollKey={`favourited_statuses-${columnId}`}
hasMore={hasMore}
+ isLoading={isLoading}
onScrollToBottom={this.handleScrollToBottom}
/>
diff --git a/app/javascript/mastodon/features/favourites/index.js b/app/javascript/mastodon/features/favourites/index.js
index 4dbfefd87..6f113beb4 100644
--- a/app/javascript/mastodon/features/favourites/index.js
+++ b/app/javascript/mastodon/features/favourites/index.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { fetchFavourites } from '../../actions/interactions';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import ColumnBackButton from '../../components/column_back_button';
diff --git a/app/javascript/mastodon/features/follow_requests/index.js b/app/javascript/mastodon/features/follow_requests/index.js
index 4c9e514cb..eae821f92 100644
--- a/app/javascript/mastodon/features/follow_requests/index.js
+++ b/app/javascript/mastodon/features/follow_requests/index.js
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
import Column from '../ui/components/column';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import AccountAuthorizeContainer from './containers/account_authorize_container';
diff --git a/app/javascript/mastodon/features/followers/index.js b/app/javascript/mastodon/features/followers/index.js
index 89445559f..f64ed7948 100644
--- a/app/javascript/mastodon/features/followers/index.js
+++ b/app/javascript/mastodon/features/followers/index.js
@@ -8,7 +8,7 @@ import {
fetchFollowers,
expandFollowers,
} from '../../actions/accounts';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import HeaderContainer from '../account_timeline/containers/header_container';
diff --git a/app/javascript/mastodon/features/following/index.js b/app/javascript/mastodon/features/following/index.js
index c34830276..a0c0fac05 100644
--- a/app/javascript/mastodon/features/following/index.js
+++ b/app/javascript/mastodon/features/following/index.js
@@ -8,7 +8,7 @@ import {
fetchFollowing,
expandFollowing,
} from '../../actions/accounts';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import HeaderContainer from '../account_timeline/containers/header_container';
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index 973c8a4ae..4b4c02bcc 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -7,6 +7,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import { me } from '../../initial_state';
const messages = defineMessages({
heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
@@ -24,10 +25,12 @@ const messages = defineMessages({
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
+ lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
+ keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' },
});
const mapStateToProps = state => ({
- me: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
+ myAccount: state.getIn(['accounts', me]),
columns: state.getIn(['settings', 'columns']),
});
@@ -37,13 +40,13 @@ export default class GettingStarted extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
- me: ImmutablePropTypes.map.isRequired,
+ myAccount: ImmutablePropTypes.map.isRequired,
columns: ImmutablePropTypes.list,
multiColumn: PropTypes.bool,
};
render () {
- const { intl, me, columns, multiColumn } = this.props;
+ const { intl, myAccount, columns, multiColumn } = this.props;
let navItems = [];
@@ -68,15 +71,17 @@ export default class GettingStarted extends ImmutablePureComponent {
navItems = navItems.concat([
,
,
+ ,
]);
- if (me.get('locked')) {
+ if (myAccount.get('locked')) {
navItems.push();
}
navItems = navItems.concat([
,
,
+ ,
]);
return (
diff --git a/app/javascript/mastodon/features/keyboard_shortcuts/index.js b/app/javascript/mastodon/features/keyboard_shortcuts/index.js
new file mode 100644
index 000000000..22991fcba
--- /dev/null
+++ b/app/javascript/mastodon/features/keyboard_shortcuts/index.js
@@ -0,0 +1,98 @@
+import React from 'react';
+import Column from '../ui/components/column';
+import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import PropTypes from 'prop-types';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+
+const messages = defineMessages({
+ heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' },
+});
+
+@injectIntl
+export default class KeyboardShortcuts extends ImmutablePureComponent {
+
+ static propTypes = {
+ intl: PropTypes.object.isRequired,
+ multiColumn: PropTypes.bool,
+ };
+
+ render () {
+ const { intl } = this.props;
+
+ return (
+
+
+
+
+
+
+ |
+ |
+
+
+
+
+ r |
+ |
+
+
+ m |
+ |
+
+
+ f |
+ |
+
+
+ b |
+ |
+
+
+ enter |
+ |
+
+
+ up |
+ |
+
+
+ down |
+ |
+
+
+ 1 -9 |
+ |
+
+
+ n |
+ |
+
+
+ alt +n |
+ |
+
+
+ backspace |
+ |
+
+
+ s |
+ |
+
+
+ esc |
+ |
+
+
+ ? |
+ |
+
+
+
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/list_editor/components/account.js b/app/javascript/mastodon/features/list_editor/components/account.js
new file mode 100644
index 000000000..c78c58e24
--- /dev/null
+++ b/app/javascript/mastodon/features/list_editor/components/account.js
@@ -0,0 +1,77 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { makeGetAccount } from '../../../selectors';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Avatar from '../../../components/avatar';
+import DisplayName from '../../../components/display_name';
+import IconButton from '../../../components/icon_button';
+import { defineMessages, injectIntl } from 'react-intl';
+import { removeFromListEditor, addToListEditor } from '../../../actions/lists';
+
+const messages = defineMessages({
+ remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
+ add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
+});
+
+const makeMapStateToProps = () => {
+ const getAccount = makeGetAccount();
+
+ const mapStateToProps = (state, { accountId, added }) => ({
+ account: getAccount(state, accountId),
+ added: typeof added === 'undefined' ? state.getIn(['listEditor', 'accounts', 'items']).includes(accountId) : added,
+ });
+
+ return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { accountId }) => ({
+ onRemove: () => dispatch(removeFromListEditor(accountId)),
+ onAdd: () => dispatch(addToListEditor(accountId)),
+});
+
+@connect(makeMapStateToProps, mapDispatchToProps)
+@injectIntl
+export default class Account extends ImmutablePureComponent {
+
+ static propTypes = {
+ account: ImmutablePropTypes.map.isRequired,
+ intl: PropTypes.object.isRequired,
+ onRemove: PropTypes.func.isRequired,
+ onAdd: PropTypes.func.isRequired,
+ added: PropTypes.bool,
+ };
+
+ static defaultProps = {
+ added: false,
+ };
+
+ render () {
+ const { account, intl, onRemove, onAdd, added } = this.props;
+
+ let button;
+
+ if (added) {
+ button = ;
+ } else {
+ button = ;
+ }
+
+ return (
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/list_editor/components/search.js b/app/javascript/mastodon/features/list_editor/components/search.js
new file mode 100644
index 000000000..45c4d0f2e
--- /dev/null
+++ b/app/javascript/mastodon/features/list_editor/components/search.js
@@ -0,0 +1,75 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { defineMessages, injectIntl } from 'react-intl';
+import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
+import classNames from 'classnames';
+
+const messages = defineMessages({
+ search: { id: 'lists.search', defaultMessage: 'Search among people you follow' },
+});
+
+const mapStateToProps = state => ({
+ value: state.getIn(['listEditor', 'suggestions', 'value']),
+});
+
+const mapDispatchToProps = dispatch => ({
+ onSubmit: value => dispatch(fetchListSuggestions(value)),
+ onClear: () => dispatch(clearListSuggestions()),
+ onChange: value => dispatch(changeListSuggestions(value)),
+});
+
+@connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
+export default class Search extends React.PureComponent {
+
+ static propTypes = {
+ intl: PropTypes.object.isRequired,
+ value: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ onClear: PropTypes.func.isRequired,
+ };
+
+ handleChange = e => {
+ this.props.onChange(e.target.value);
+ }
+
+ handleKeyUp = e => {
+ if (e.keyCode === 13) {
+ this.props.onSubmit(this.props.value);
+ }
+ }
+
+ handleClear = () => {
+ this.props.onClear();
+ }
+
+ render () {
+ const { value, intl } = this.props;
+ const hasValue = value.length > 0;
+
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/list_editor/index.js b/app/javascript/mastodon/features/list_editor/index.js
new file mode 100644
index 000000000..a3b60e447
--- /dev/null
+++ b/app/javascript/mastodon/features/list_editor/index.js
@@ -0,0 +1,80 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl } from 'react-intl';
+import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
+import Account from './components/account';
+import Search from './components/search';
+import Motion from '../ui/util/optional_motion';
+import spring from 'react-motion/lib/spring';
+
+const mapStateToProps = state => ({
+ title: state.getIn(['listEditor', 'title']),
+ accountIds: state.getIn(['listEditor', 'accounts', 'items']),
+ searchAccountIds: state.getIn(['listEditor', 'suggestions', 'items']),
+});
+
+const mapDispatchToProps = dispatch => ({
+ onInitialize: listId => dispatch(setupListEditor(listId)),
+ onClear: () => dispatch(clearListSuggestions()),
+ onReset: () => dispatch(resetListEditor()),
+});
+
+@connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
+export default class ListEditor extends ImmutablePureComponent {
+
+ static propTypes = {
+ listId: PropTypes.string.isRequired,
+ onClose: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
+ onInitialize: PropTypes.func.isRequired,
+ onClear: PropTypes.func.isRequired,
+ onReset: PropTypes.func.isRequired,
+ title: PropTypes.string.isRequired,
+ accountIds: ImmutablePropTypes.list.isRequired,
+ searchAccountIds: ImmutablePropTypes.list.isRequired,
+ };
+
+ componentDidMount () {
+ const { onInitialize, listId } = this.props;
+ onInitialize(listId);
+ }
+
+ componentWillUnmount () {
+ const { onReset } = this.props;
+ onReset();
+ }
+
+ render () {
+ const { title, accountIds, searchAccountIds, onClear } = this.props;
+ const showSearch = searchAccountIds.size > 0;
+
+ return (
+
+
{title}
+
+
+
+
+
+ {accountIds.map(accountId =>
)}
+
+
+ {showSearch &&
}
+
+
+ {({ x }) =>
+
+ {searchAccountIds.map(accountId =>
)}
+
+ }
+
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js
new file mode 100644
index 000000000..ae136e48f
--- /dev/null
+++ b/app/javascript/mastodon/features/list_timeline/index.js
@@ -0,0 +1,170 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import StatusListContainer from '../ui/containers/status_list_container';
+import Column from '../../components/column';
+import ColumnHeader from '../../components/column_header';
+import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
+import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
+import { connectListStream } from '../../actions/streaming';
+import { refreshListTimeline, expandListTimeline } from '../../actions/timelines';
+import { fetchList, deleteList } from '../../actions/lists';
+import { openModal } from '../../actions/modal';
+import MissingIndicator from '../../components/missing_indicator';
+import LoadingIndicator from '../../components/loading_indicator';
+
+const messages = defineMessages({
+ deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
+ deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
+});
+
+const mapStateToProps = (state, props) => ({
+ list: state.getIn(['lists', props.params.id]),
+ hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0,
+});
+
+@connect(mapStateToProps)
+@injectIntl
+export default class ListTimeline extends React.PureComponent {
+
+ static contextTypes = {
+ router: PropTypes.object,
+ };
+
+ static propTypes = {
+ params: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ columnId: PropTypes.string,
+ hasUnread: PropTypes.bool,
+ multiColumn: PropTypes.bool,
+ list: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
+ intl: PropTypes.object.isRequired,
+ };
+
+ handlePin = () => {
+ const { columnId, dispatch } = this.props;
+
+ if (columnId) {
+ dispatch(removeColumn(columnId));
+ } else {
+ dispatch(addColumn('LIST', { id: this.props.params.id }));
+ this.context.router.history.push('/');
+ }
+ }
+
+ handleMove = (dir) => {
+ const { columnId, dispatch } = this.props;
+ dispatch(moveColumn(columnId, dir));
+ }
+
+ handleHeaderClick = () => {
+ this.column.scrollTop();
+ }
+
+ componentDidMount () {
+ const { dispatch } = this.props;
+ const { id } = this.props.params;
+
+ dispatch(fetchList(id));
+ dispatch(refreshListTimeline(id));
+
+ this.disconnect = dispatch(connectListStream(id));
+ }
+
+ componentWillUnmount () {
+ if (this.disconnect) {
+ this.disconnect();
+ this.disconnect = null;
+ }
+ }
+
+ setRef = c => {
+ this.column = c;
+ }
+
+ handleLoadMore = () => {
+ const { id } = this.props.params;
+ this.props.dispatch(expandListTimeline(id));
+ }
+
+ handleEditClick = () => {
+ this.props.dispatch(openModal('LIST_EDITOR', { listId: this.props.params.id }));
+ }
+
+ handleDeleteClick = () => {
+ const { dispatch, columnId, intl } = this.props;
+ const { id } = this.props.params;
+
+ dispatch(openModal('CONFIRM', {
+ message: intl.formatMessage(messages.deleteMessage),
+ confirm: intl.formatMessage(messages.deleteConfirm),
+ onConfirm: () => {
+ dispatch(deleteList(id));
+
+ if (!!columnId) {
+ dispatch(removeColumn(columnId));
+ } else {
+ this.context.router.history.push('/lists');
+ }
+ },
+ }));
+ }
+
+ render () {
+ const { hasUnread, columnId, multiColumn, list } = this.props;
+ const { id } = this.props.params;
+ const pinned = !!columnId;
+ const title = list ? list.get('title') : id;
+
+ if (typeof list === 'undefined') {
+ return (
+
+
+
+ );
+ } else if (list === false) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/lists/components/new_list_form.js b/app/javascript/mastodon/features/lists/components/new_list_form.js
new file mode 100644
index 000000000..eed6efc25
--- /dev/null
+++ b/app/javascript/mastodon/features/lists/components/new_list_form.js
@@ -0,0 +1,78 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
+import IconButton from '../../../components/icon_button';
+import { defineMessages, injectIntl } from 'react-intl';
+
+const messages = defineMessages({
+ label: { id: 'lists.new.title_placeholder', defaultMessage: 'New list title' },
+ title: { id: 'lists.new.create', defaultMessage: 'Add list' },
+});
+
+const mapStateToProps = state => ({
+ value: state.getIn(['listEditor', 'title']),
+ disabled: state.getIn(['listEditor', 'isSubmitting']),
+});
+
+const mapDispatchToProps = dispatch => ({
+ onChange: value => dispatch(changeListEditorTitle(value)),
+ onSubmit: () => dispatch(submitListEditor(true)),
+});
+
+@connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
+export default class NewListForm extends React.PureComponent {
+
+ static propTypes = {
+ value: PropTypes.string.isRequired,
+ disabled: PropTypes.bool,
+ intl: PropTypes.object.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ };
+
+ handleChange = e => {
+ this.props.onChange(e.target.value);
+ }
+
+ handleSubmit = e => {
+ e.preventDefault();
+ this.props.onSubmit();
+ }
+
+ handleClick = () => {
+ this.props.onSubmit();
+ }
+
+ render () {
+ const { value, disabled, intl } = this.props;
+
+ const label = intl.formatMessage(messages.label);
+ const title = intl.formatMessage(messages.title);
+
+ return (
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/lists/index.js b/app/javascript/mastodon/features/lists/index.js
new file mode 100644
index 000000000..28026c434
--- /dev/null
+++ b/app/javascript/mastodon/features/lists/index.js
@@ -0,0 +1,76 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import LoadingIndicator from '../../components/loading_indicator';
+import Column from '../ui/components/column';
+import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+import { fetchLists } from '../../actions/lists';
+import { defineMessages, injectIntl } from 'react-intl';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import ColumnLink from '../ui/components/column_link';
+import ColumnSubheading from '../ui/components/column_subheading';
+import NewListForm from './components/new_list_form';
+import { createSelector } from 'reselect';
+
+const messages = defineMessages({
+ heading: { id: 'column.lists', defaultMessage: 'Lists' },
+ subheading: { id: 'lists.subheading', defaultMessage: 'Your lists' },
+});
+
+const getOrderedLists = createSelector([state => state.get('lists')], lists => {
+ if (!lists) {
+ return lists;
+ }
+
+ return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
+});
+
+const mapStateToProps = state => ({
+ lists: getOrderedLists(state),
+});
+
+@connect(mapStateToProps)
+@injectIntl
+export default class Lists extends ImmutablePureComponent {
+
+ static propTypes = {
+ params: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ lists: ImmutablePropTypes.list,
+ intl: PropTypes.object.isRequired,
+ };
+
+ componentWillMount () {
+ this.props.dispatch(fetchLists());
+ }
+
+ render () {
+ const { intl, lists } = this.props;
+
+ if (!lists) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {lists.map(list =>
+
+ )}
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/mutes/index.js b/app/javascript/mastodon/features/mutes/index.js
index 25ca921ae..bb351ece2 100644
--- a/app/javascript/mastodon/features/mutes/index.js
+++ b/app/javascript/mastodon/features/mutes/index.js
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
import Column from '../ui/components/column';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import AccountContainer from '../../containers/account_container';
diff --git a/app/javascript/mastodon/features/reblogs/index.js b/app/javascript/mastodon/features/reblogs/index.js
index f1904786a..579d6aaa0 100644
--- a/app/javascript/mastodon/features/reblogs/index.js
+++ b/app/javascript/mastodon/features/reblogs/index.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { fetchReblogs } from '../../actions/interactions';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import ColumnBackButton from '../../components/column_back_button';
diff --git a/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js b/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js
index f15fbb2f4..f14be2aaf 100644
--- a/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js
+++ b/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js
@@ -8,6 +8,7 @@ import {
} from '../../../actions/timelines';
import Column from '../../../components/column';
import ColumnHeader from '../../../components/column_header';
+import { connectHashtagStream } from '../../../actions/streaming';
@connect()
export default class HashtagTimeline extends React.PureComponent {
@@ -29,16 +30,13 @@ export default class HashtagTimeline extends React.PureComponent {
const { dispatch, hashtag } = this.props;
dispatch(refreshHashtagTimeline(hashtag));
-
- this.polling = setInterval(() => {
- dispatch(refreshHashtagTimeline(hashtag));
- }, 10000);
+ this.disconnect = dispatch(connectHashtagStream(hashtag));
}
componentWillUnmount () {
- if (typeof this.polling !== 'undefined') {
- clearInterval(this.polling);
- this.polling = null;
+ if (this.disconnect) {
+ this.disconnect();
+ this.disconnect = null;
}
}
diff --git a/app/javascript/mastodon/features/standalone/public_timeline/index.js b/app/javascript/mastodon/features/standalone/public_timeline/index.js
index de4b5320a..5805d1a10 100644
--- a/app/javascript/mastodon/features/standalone/public_timeline/index.js
+++ b/app/javascript/mastodon/features/standalone/public_timeline/index.js
@@ -9,6 +9,7 @@ import {
import Column from '../../../components/column';
import ColumnHeader from '../../../components/column_header';
import { defineMessages, injectIntl } from 'react-intl';
+import { connectPublicStream } from '../../../actions/streaming';
const messages = defineMessages({
title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' },
@@ -35,16 +36,13 @@ export default class PublicTimeline extends React.PureComponent {
const { dispatch } = this.props;
dispatch(refreshPublicTimeline());
-
- this.polling = setInterval(() => {
- dispatch(refreshPublicTimeline());
- }, 3000);
+ this.disconnect = dispatch(connectPublicStream());
}
componentWillUnmount () {
- if (typeof this.polling !== 'undefined') {
- clearInterval(this.polling);
- this.polling = null;
+ if (this.disconnect) {
+ this.disconnect();
+ this.disconnect = null;
}
}
diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js
index 034cc9854..99834df6c 100644
--- a/app/javascript/mastodon/features/status/components/action_bar.js
+++ b/app/javascript/mastodon/features/status/components/action_bar.js
@@ -4,6 +4,7 @@ import IconButton from '../../../components/icon_button';
import ImmutablePropTypes from 'react-immutable-proptypes';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
+import { me } from '../../../initial_state';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -36,7 +37,6 @@ export default class ActionBar extends React.PureComponent {
onReport: PropTypes.func,
onPin: PropTypes.func,
onEmbed: PropTypes.func,
- me: PropTypes.string.isRequired,
intl: PropTypes.object.isRequired,
};
@@ -80,7 +80,7 @@ export default class ActionBar extends React.PureComponent {
}
render () {
- const { status, me, intl } = this.props;
+ const { status, intl } = this.props;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
@@ -120,7 +120,7 @@ export default class ActionBar extends React.PureComponent {
{shareButton}
-
+
);
diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js
index bb83374b9..2f6a7831e 100644
--- a/app/javascript/mastodon/features/status/components/card.js
+++ b/app/javascript/mastodon/features/status/components/card.js
@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
+import Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import punycode from 'punycode';
import classnames from 'classnames';
@@ -24,6 +25,7 @@ export default class Card extends React.PureComponent {
static propTypes = {
card: ImmutablePropTypes.map,
maxDescription: PropTypes.number,
+ onOpenMedia: PropTypes.func.isRequired,
};
static defaultProps = {
@@ -34,8 +36,31 @@ export default class Card extends React.PureComponent {
width: 0,
};
+ handlePhotoClick = () => {
+ const { card, onOpenMedia } = this.props;
+
+ onOpenMedia(
+ Immutable.fromJS([
+ {
+ type: 'image',
+ url: card.get('embed_url'),
+ description: card.get('title'),
+ meta: {
+ original: {
+ width: card.get('width'),
+ height: card.get('height'),
+ },
+ },
+ },
+ ]),
+ 0
+ );
+ };
+
renderLink () {
const { card, maxDescription } = this.props;
+ const { width } = this.state;
+ const horizontal = card.get('width') > card.get('height') && (card.get('width') + 100 >= width);
let image = '';
let provider = card.get('provider_name');
@@ -52,17 +77,15 @@ export default class Card extends React.PureComponent {
provider = decodeIDNA(getHostname(card.get('url')));
}
- const className = classnames('status-card', {
- 'horizontal': card.get('width') > card.get('height'),
- });
+ const className = classnames('status-card', { horizontal });
return (
-
+
{image}
{card.get('title')}
-
{(card.get('description') || '').substring(0, maxDescription)}
+ {!horizontal &&
{(card.get('description') || '').substring(0, maxDescription)}
}
{provider}
@@ -73,9 +96,16 @@ export default class Card extends React.PureComponent {
const { card } = this.props;
return (
-
-
-
+
})
);
}
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index c10e2c531..abdb9a3f6 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -22,7 +22,6 @@ export default class DetailedStatus extends ImmutablePureComponent {
status: ImmutablePropTypes.map.isRequired,
onOpenMedia: PropTypes.func.isRequired,
onOpenVideo: PropTypes.func.isRequired,
- autoPlayGif: PropTypes.bool,
};
handleAccountClick = (e) => {
@@ -70,12 +69,11 @@ export default class DetailedStatus extends ImmutablePureComponent {
media={status.get('media_attachments')}
height={300}
onOpenMedia={this.props.onOpenMedia}
- autoPlayGif={this.props.autoPlayGif}
/>
);
}
} else if (status.get('spoiler_text').length === 0) {
- media =
;
+ media =
;
}
if (status.get('application')) {
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
index 7ad3a7644..cc28ff5fc 100644
--- a/app/javascript/mastodon/features/status/index.js
+++ b/app/javascript/mastodon/features/status/index.js
@@ -1,6 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
+import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { fetchStatus } from '../../actions/statuses';
import MissingIndicator from '../../components/missing_indicator';
@@ -22,13 +23,15 @@ import {
import { deleteStatus } from '../../actions/statuses';
import { initReport } from '../../actions/reports';
import { makeGetStatus } from '../../selectors';
-import { ScrollContainer } from 'react-router-scroll';
+import { ScrollContainer } from 'react-router-scroll-4';
import ColumnBackButton from '../../components/column_back_button';
import StatusContainer from '../../containers/status_container';
import { openModal } from '../../actions/modal';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys';
+import { boostModal, deleteModal } from '../../initial_state';
+import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../../features/ui/util/fullscreen';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
@@ -42,10 +45,6 @@ const makeMapStateToProps = () => {
status: getStatus(state, props.params.statusId),
ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]),
descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]),
- me: state.getIn(['meta', 'me']),
- boostModal: state.getIn(['meta', 'boost_modal']),
- deleteModal: state.getIn(['meta', 'delete_modal']),
- autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
});
return mapStateToProps;
@@ -65,17 +64,21 @@ export default class Status extends ImmutablePureComponent {
status: ImmutablePropTypes.map,
ancestorsIds: ImmutablePropTypes.list,
descendantsIds: ImmutablePropTypes.list,
- me: PropTypes.string,
- boostModal: PropTypes.bool,
- deleteModal: PropTypes.bool,
- autoPlayGif: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
+ state = {
+ fullscreen: false,
+ };
+
componentWillMount () {
this.props.dispatch(fetchStatus(this.props.params.statusId));
}
+ componentDidMount () {
+ attachFullscreenListener(this.onFullScreenChange);
+ }
+
componentWillReceiveProps (nextProps) {
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
this._scrolledIntoView = false;
@@ -111,7 +114,7 @@ export default class Status extends ImmutablePureComponent {
if (status.get('reblogged')) {
this.props.dispatch(unreblog(status));
} else {
- if (e.shiftKey || !this.props.boostModal) {
+ if (e.shiftKey || !boostModal) {
this.handleModalReblog(status);
} else {
this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
@@ -122,7 +125,7 @@ export default class Status extends ImmutablePureComponent {
handleDeleteClick = (status) => {
const { dispatch, intl } = this.props;
- if (!this.props.deleteModal) {
+ if (!deleteModal) {
dispatch(deleteStatus(status.get('id')));
} else {
dispatch(openModal('CONFIRM', {
@@ -255,9 +258,18 @@ export default class Status extends ImmutablePureComponent {
}
}
+ componentWillUnmount () {
+ detachFullscreenListener(this.onFullScreenChange);
+ }
+
+ onFullScreenChange = () => {
+ this.setState({ fullscreen: isFullscreen() });
+ }
+
render () {
let ancestors, descendants;
- const { status, ancestorsIds, descendantsIds, me, autoPlayGif } = this.props;
+ const { status, ancestorsIds, descendantsIds } = this.props;
+ const { fullscreen } = this.state;
if (status === null) {
return (
@@ -291,22 +303,19 @@ export default class Status extends ImmutablePureComponent {
-
+
{ancestors}
injectIntl(component, { withRef: true })
@@ -52,7 +53,10 @@ export default class ColumnsArea extends ImmutablePureComponent {
if (!this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
}
- this.lastIndex = getIndex(this.context.router.history.location.pathname);
+
+ this.lastIndex = getIndex(this.context.router.history.location.pathname);
+ this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
+
this.setState({ shouldAnimate: true });
}
@@ -78,7 +82,8 @@ export default class ColumnsArea extends ImmutablePureComponent {
handleChildrenContentChange() {
if (!this.props.singleColumn) {
- this._interruptScrollAnimation = scrollRight(this.node, this.node.scrollWidth - window.innerWidth);
+ const modifier = this.isRtlLayout ? -1 : 1;
+ this._interruptScrollAnimation = scrollRight(this.node, (this.node.scrollWidth - window.innerWidth) * modifier);
}
}
diff --git a/app/javascript/mastodon/features/ui/components/image_loader.js b/app/javascript/mastodon/features/ui/components/image_loader.js
index aad594380..e3e7197c5 100644
--- a/app/javascript/mastodon/features/ui/components/image_loader.js
+++ b/app/javascript/mastodon/features/ui/components/image_loader.js
@@ -7,7 +7,7 @@ export default class ImageLoader extends React.PureComponent {
static propTypes = {
alt: PropTypes.string,
src: PropTypes.string.isRequired,
- previewSrc: PropTypes.string.isRequired,
+ previewSrc: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
}
@@ -47,7 +47,7 @@ export default class ImageLoader extends React.PureComponent {
this.removeEventListeners();
this.setState({ loading: true, error: false });
Promise.all([
- this.loadPreviewCanvas(props),
+ props.previewSrc && this.loadPreviewCanvas(props),
this.hasSize() && this.loadOriginalImage(props),
].filter(Boolean))
.then(() => {
diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js
index f41a83089..02591a51f 100644
--- a/app/javascript/mastodon/features/ui/components/media_modal.js
+++ b/app/javascript/mastodon/features/ui/components/media_modal.js
@@ -92,7 +92,7 @@ export default class MediaModal extends ImmutablePureComponent {
const height = image.getIn(['meta', 'original', 'height']) || null;
if (image.get('type') === 'image') {
- return ;
+ return ;
} else if (image.get('type') === 'gifv') {
return ;
}
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js
index f420f0abf..ebbff6b5a 100644
--- a/app/javascript/mastodon/features/ui/components/modal_root.js
+++ b/app/javascript/mastodon/features/ui/components/modal_root.js
@@ -10,8 +10,10 @@ import BoostModal from './boost_modal';
import ConfirmationModal from './confirmation_modal';
import {
OnboardingModal,
+ MuteModal,
ReportModal,
EmbedModal,
+ ListEditor,
} from '../../../features/ui/util/async-components';
const MODAL_COMPONENTS = {
@@ -20,9 +22,11 @@ const MODAL_COMPONENTS = {
'VIDEO': () => Promise.resolve({ default: VideoModal }),
'BOOST': () => Promise.resolve({ default: BoostModal }),
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
+ 'MUTE': MuteModal,
'REPORT': ReportModal,
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
'EMBED': EmbedModal,
+ 'LIST_EDITOR': ListEditor,
};
export default class ModalRoot extends React.PureComponent {
diff --git a/app/javascript/mastodon/features/ui/components/mute_modal.js b/app/javascript/mastodon/features/ui/components/mute_modal.js
new file mode 100644
index 000000000..73e48cf09
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/mute_modal.js
@@ -0,0 +1,105 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import { injectIntl, FormattedMessage } from 'react-intl';
+import Toggle from 'react-toggle';
+import Button from '../../../components/button';
+import { closeModal } from '../../../actions/modal';
+import { muteAccount } from '../../../actions/accounts';
+import { toggleHideNotifications } from '../../../actions/mutes';
+
+
+const mapStateToProps = state => {
+ return {
+ isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
+ account: state.getIn(['mutes', 'new', 'account']),
+ notifications: state.getIn(['mutes', 'new', 'notifications']),
+ };
+};
+
+const mapDispatchToProps = dispatch => {
+ return {
+ onConfirm(account, notifications) {
+ dispatch(muteAccount(account.get('id'), notifications));
+ },
+
+ onClose() {
+ dispatch(closeModal());
+ },
+
+ onToggleNotifications() {
+ dispatch(toggleHideNotifications());
+ },
+ };
+};
+
+@connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
+export default class MuteModal extends React.PureComponent {
+
+ static propTypes = {
+ isSubmitting: PropTypes.bool.isRequired,
+ account: PropTypes.object.isRequired,
+ notifications: PropTypes.bool.isRequired,
+ onClose: PropTypes.func.isRequired,
+ onConfirm: PropTypes.func.isRequired,
+ onToggleNotifications: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
+ };
+
+ componentDidMount() {
+ this.button.focus();
+ }
+
+ handleClick = () => {
+ this.props.onClose();
+ this.props.onConfirm(this.props.account, this.props.notifications);
+ }
+
+ handleCancel = () => {
+ this.props.onClose();
+ }
+
+ setRef = (c) => {
+ this.button = c;
+ }
+
+ toggleNotifications = () => {
+ this.props.onToggleNotifications();
+ }
+
+ render () {
+ const { account, notifications } = this.props;
+
+ return (
+
+
+
+ @{account.get('acct')} }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/ui/components/onboarding_modal.js b/app/javascript/mastodon/features/ui/components/onboarding_modal.js
index 7905bca2e..54673e223 100644
--- a/app/javascript/mastodon/features/ui/components/onboarding_modal.js
+++ b/app/javascript/mastodon/features/ui/components/onboarding_modal.js
@@ -11,6 +11,7 @@ import Search from '../../compose/components/search';
import NavigationBar from '../../compose/components/navigation_bar';
import ColumnHeader from './column_header';
import { List as ImmutableList } from 'immutable';
+import { me } from '../../../initial_state';
const noop = () => { };
@@ -40,11 +41,11 @@ PageOne.propTypes = {
domain: PropTypes.string.isRequired,
};
-const PageTwo = ({ me }) => (
+const PageTwo = ({ myAccount }) => (
-
+
(
);
PageTwo.propTypes = {
- me: ImmutablePropTypes.map.isRequired,
+ myAccount: ImmutablePropTypes.map.isRequired,
};
-const PageThree = ({ me }) => (
+const PageThree = ({ myAccount }) => (
@@ -93,7 +94,7 @@ const PageThree = ({ me }) => (
);
PageThree.propTypes = {
- me: ImmutablePropTypes.map.isRequired,
+ myAccount: ImmutablePropTypes.map.isRequired,
};
const PageFour = ({ domain, intl }) => (
@@ -161,7 +162,7 @@ PageSix.propTypes = {
};
const mapStateToProps = state => ({
- me: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
+ myAccount: state.getIn(['accounts', me]),
admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]),
domain: state.getIn(['meta', 'domain']),
});
@@ -173,7 +174,7 @@ export default class OnboardingModal extends React.PureComponent {
static propTypes = {
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
- me: ImmutablePropTypes.map.isRequired,
+ myAccount: ImmutablePropTypes.map.isRequired,
domain: PropTypes.string.isRequired,
admin: ImmutablePropTypes.map,
};
@@ -183,11 +184,11 @@ export default class OnboardingModal extends React.PureComponent {
};
componentWillMount() {
- const { me, admin, domain, intl } = this.props;
+ const { myAccount, admin, domain, intl } = this.props;
this.pages = [
-
,
-
,
-
,
+
,
+
,
+
,
,
,
];
diff --git a/app/javascript/mastodon/features/ui/components/upload_area.js b/app/javascript/mastodon/features/ui/components/upload_area.js
index c19065be6..8b9a26270 100644
--- a/app/javascript/mastodon/features/ui/components/upload_area.js
+++ b/app/javascript/mastodon/features/ui/components/upload_area.js
@@ -40,7 +40,7 @@ export default class UploadArea extends React.PureComponent {
{({ backgroundOpacity, backgroundScale }) =>
diff --git a/app/javascript/mastodon/features/ui/components/video_modal.js b/app/javascript/mastodon/features/ui/components/video_modal.js
index 1437deeb0..6a883759f 100644
--- a/app/javascript/mastodon/features/ui/components/video_modal.js
+++ b/app/javascript/mastodon/features/ui/components/video_modal.js
@@ -23,6 +23,7 @@ export default class VideoModal extends ImmutablePureComponent {
src={media.get('url')}
startTime={time}
onCloseVideo={onClose}
+ detailed
description={media.get('description')}
/>
diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js
index ff29bfdd4..a0aec4403 100644
--- a/app/javascript/mastodon/features/ui/containers/status_list_container.js
+++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js
@@ -4,13 +4,13 @@ import { scrollTopTimeline } from '../../../actions/timelines';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { createSelector } from 'reselect';
import { debounce } from 'lodash';
+import { me } from '../../../initial_state';
const makeGetStatusIds = () => createSelector([
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
(state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()),
(state) => state.get('statuses'),
- (state) => state.getIn(['meta', 'me']),
-], (columnSettings, statusIds, statuses, me) => {
+], (columnSettings, statusIds, statuses) => {
const rawRegex = columnSettings.getIn(['regex', 'body'], '').trim();
let regex = null;
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 70e451373..5b0d7246a 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -19,6 +19,7 @@ import {
Compose,
Status,
GettingStarted,
+ KeyboardShortcuts,
PublicTimeline,
CommunityTimeline,
AccountTimeline,
@@ -33,22 +34,31 @@ import {
FollowRequests,
GenericNotFound,
FavouritedStatuses,
+ ListTimeline,
Blocks,
Mutes,
PinnedStatuses,
+ Lists,
} from './util/async-components';
import { HotKeys } from 'react-hotkeys';
+import { me } from '../../initial_state';
+import { defineMessages, injectIntl } from 'react-intl';
// Dummy import, to make sure that ends up in the application bundle.
// Without this it ends up in ~8 very commonly used bundles.
import '../../components/status';
+const messages = defineMessages({
+ beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave Mastodon.' },
+});
+
const mapStateToProps = state => ({
- me: state.getIn(['meta', 'me']),
isComposing: state.getIn(['compose', 'is_composing']),
+ hasComposingText: state.getIn(['compose', 'text']) !== '',
});
const keyMap = {
+ help: '?',
new: 'n',
search: 's',
forceNew: 'option+n',
@@ -75,6 +85,7 @@ const keyMap = {
};
@connect(mapStateToProps)
+@injectIntl
@withRouter
export default class UI extends React.Component {
@@ -86,8 +97,9 @@ export default class UI extends React.Component {
dispatch: PropTypes.func.isRequired,
children: PropTypes.node,
isComposing: PropTypes.bool,
- me: PropTypes.string,
+ hasComposingText: PropTypes.bool,
location: PropTypes.object,
+ intl: PropTypes.object.isRequired,
};
state = {
@@ -95,6 +107,17 @@ export default class UI extends React.Component {
draggingOver: false,
};
+ handleBeforeUnload = (e) => {
+ const { intl, isComposing, hasComposingText } = this.props;
+
+ if (isComposing && hasComposingText) {
+ // Setting returnValue to any string causes confirmation dialog.
+ // Many browsers no longer display this text to users,
+ // but we set user-friendly message for other browsers, e.g. Edge.
+ e.returnValue = intl.formatMessage(messages.beforeUnload);
+ }
+ }
+
handleResize = debounce(() => {
// The cached heights are no longer accurate, invalidate
this.props.dispatch(clearHeight());
@@ -169,6 +192,7 @@ export default class UI extends React.Component {
}
componentWillMount () {
+ window.addEventListener('beforeunload', this.handleBeforeUnload, false);
window.addEventListener('resize', this.handleResize, { passive: true });
document.addEventListener('dragenter', this.handleDragEnter, false);
document.addEventListener('dragover', this.handleDragOver, false);
@@ -210,6 +234,7 @@ export default class UI extends React.Component {
}
componentWillUnmount () {
+ window.removeEventListener('beforeunload', this.handleBeforeUnload);
window.removeEventListener('resize', this.handleResize);
document.removeEventListener('dragenter', this.handleDragEnter);
document.removeEventListener('dragover', this.handleDragOver);
@@ -276,6 +301,14 @@ export default class UI extends React.Component {
this.hotkeys = c;
}
+ handleHotkeyToggleHelp = () => {
+ if (this.props.location.pathname === '/keyboard-shortcuts') {
+ this.context.router.history.goBack();
+ } else {
+ this.context.router.history.push('/keyboard-shortcuts');
+ }
+ }
+
handleHotkeyGoToHome = () => {
this.context.router.history.push('/timelines/home');
}
@@ -305,7 +338,7 @@ export default class UI extends React.Component {
}
handleHotkeyGoToProfile = () => {
- this.context.router.history.push(`/accounts/${this.props.me}`);
+ this.context.router.history.push(`/accounts/${me}`);
}
handleHotkeyGoToBlocked = () => {
@@ -321,6 +354,7 @@ export default class UI extends React.Component {
const { children } = this.props;
const handlers = {
+ help: this.handleHotkeyToggleHelp,
new: this.handleHotkeyNew,
search: this.handleHotkeySearch,
forceNew: this.handleHotkeyForceNew,
@@ -347,10 +381,12 @@ export default class UI extends React.Component {
+
+
@@ -369,6 +405,7 @@ export default class UI extends React.Component {
+
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 8f7b91d21..d6586680b 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -26,6 +26,14 @@ export function HashtagTimeline () {
return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
}
+export function ListTimeline () {
+ return import(/* webpackChunkName: "features/list_timeline" */'../../list_timeline');
+}
+
+export function Lists () {
+ return import(/* webpackChunkName: "features/lists" */'../../lists');
+}
+
export function Status () {
return import(/* webpackChunkName: "features/status" */'../../status');
}
@@ -34,6 +42,10 @@ export function GettingStarted () {
return import(/* webpackChunkName: "features/getting_started" */'../../getting_started');
}
+export function KeyboardShortcuts () {
+ return import(/* webpackChunkName: "features/keyboard_shortcuts" */'../../keyboard_shortcuts');
+}
+
export function PinnedStatuses () {
return import(/* webpackChunkName: "features/pinned_statuses" */'../../pinned_statuses');
}
@@ -86,6 +98,10 @@ export function OnboardingModal () {
return import(/* webpackChunkName: "modals/onboarding_modal" */'../components/onboarding_modal');
}
+export function MuteModal () {
+ return import(/* webpackChunkName: "modals/mute_modal" */'../components/mute_modal');
+}
+
export function ReportModal () {
return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal');
}
@@ -101,3 +117,7 @@ export function Video () {
export function EmbedModal () {
return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal');
}
+
+export function ListEditor () {
+ return import(/* webpackChunkName: "features/list_editor" */'../../list_editor');
+}
diff --git a/app/javascript/mastodon/features/ui/util/optional_motion.js b/app/javascript/mastodon/features/ui/util/optional_motion.js
index af6368738..df3a8b54a 100644
--- a/app/javascript/mastodon/features/ui/util/optional_motion.js
+++ b/app/javascript/mastodon/features/ui/util/optional_motion.js
@@ -1,56 +1,5 @@
-// Like react-motion's Motion, but checks to see if the user prefers
-// reduced motion and uses a cross-fade in those cases.
-
-import React from 'react';
+import { reduceMotion } from '../../../initial_state';
+import ReducedMotion from './reduced_motion';
import Motion from 'react-motion/lib/Motion';
-import PropTypes from 'prop-types';
-const stylesToKeep = ['opacity', 'backgroundOpacity'];
-
-let reduceMotion;
-
-const extractValue = (value) => {
- // This is either an object with a "val" property or it's a number
- return (typeof value === 'object' && value && 'val' in value) ? value.val : value;
-};
-
-class OptionalMotion extends React.Component {
-
- static propTypes = {
- defaultStyle: PropTypes.object,
- style: PropTypes.object,
- children: PropTypes.func,
- }
-
- render() {
-
- const { style, defaultStyle, children } = this.props;
-
- if (typeof reduceMotion !== 'boolean') {
- // This never changes without a page reload, so we can just grab it
- // once from the body classes as opposed to using Redux's connect(),
- // which would unnecessarily update every state change
- reduceMotion = document.body.classList.contains('reduce-motion');
- }
- if (reduceMotion) {
- Object.keys(style).forEach(key => {
- if (stylesToKeep.includes(key)) {
- return;
- }
- // If it's setting an x or height or scale or some other value, we need
- // to preserve the end-state value without actually animating it
- style[key] = defaultStyle[key] = extractValue(style[key]);
- });
- }
-
- return (
-
- {children}
-
- );
- }
-
-}
-
-
-export default OptionalMotion;
+export default reduceMotion ? ReducedMotion : Motion;
diff --git a/app/javascript/mastodon/features/ui/util/react_router_helpers.js b/app/javascript/mastodon/features/ui/util/react_router_helpers.js
index 86b30d488..43007ddc3 100644
--- a/app/javascript/mastodon/features/ui/util/react_router_helpers.js
+++ b/app/javascript/mastodon/features/ui/util/react_router_helpers.js
@@ -7,11 +7,19 @@ import BundleColumnError from '../components/bundle_column_error';
import BundleContainer from '../containers/bundle_container';
// Small wrapper to pass multiColumn to the route components
-export const WrappedSwitch = ({ multiColumn, children }) => (
-
- {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))}
-
-);
+export class WrappedSwitch extends React.PureComponent {
+
+ render () {
+ const { multiColumn, children } = this.props;
+
+ return (
+
+ {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))}
+
+ );
+ }
+
+}
WrappedSwitch.propTypes = {
multiColumn: PropTypes.bool,
diff --git a/app/javascript/mastodon/features/ui/util/reduced_motion.js b/app/javascript/mastodon/features/ui/util/reduced_motion.js
new file mode 100644
index 000000000..95519042b
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/util/reduced_motion.js
@@ -0,0 +1,44 @@
+// Like react-motion's Motion, but reduces all animations to cross-fades
+// for the benefit of users with motion sickness.
+import React from 'react';
+import Motion from 'react-motion/lib/Motion';
+import PropTypes from 'prop-types';
+
+const stylesToKeep = ['opacity', 'backgroundOpacity'];
+
+const extractValue = (value) => {
+ // This is either an object with a "val" property or it's a number
+ return (typeof value === 'object' && value && 'val' in value) ? value.val : value;
+};
+
+class ReducedMotion extends React.Component {
+
+ static propTypes = {
+ defaultStyle: PropTypes.object,
+ style: PropTypes.object,
+ children: PropTypes.func,
+ }
+
+ render() {
+
+ const { style, defaultStyle, children } = this.props;
+
+ Object.keys(style).forEach(key => {
+ if (stylesToKeep.includes(key)) {
+ return;
+ }
+ // If it's setting an x or height or scale or some other value, we need
+ // to preserve the end-state value without actually animating it
+ style[key] = defaultStyle[key] = extractValue(style[key]);
+ });
+
+ return (
+
+ {children}
+
+ );
+ }
+
+}
+
+export default ReducedMotion;
diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js
index 003bf23a8..0ee8bb6c8 100644
--- a/app/javascript/mastodon/features/video/index.js
+++ b/app/javascript/mastodon/features/video/index.js
@@ -17,6 +17,18 @@ const messages = defineMessages({
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
});
+const formatTime = secondsNum => {
+ let hours = Math.floor(secondsNum / 3600);
+ let minutes = Math.floor((secondsNum - (hours * 3600)) / 60);
+ let seconds = secondsNum - (hours * 3600) - (minutes * 60);
+
+ if (hours < 10) hours = '0' + hours;
+ if (minutes < 10) minutes = '0' + minutes;
+ if (seconds < 10) seconds = '0' + seconds;
+
+ return (hours === '00' ? '' : `${hours}:`) + `${minutes}:${seconds}`;
+};
+
const findElementPosition = el => {
let box;
@@ -83,11 +95,13 @@ export default class Video extends React.PureComponent {
startTime: PropTypes.number,
onOpenVideo: PropTypes.func,
onCloseVideo: PropTypes.func,
+ detailed: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
state = {
- progress: 0,
+ currentTime: 0,
+ duration: 0,
paused: true,
dragging: false,
fullscreen: false,
@@ -117,7 +131,10 @@ export default class Video extends React.PureComponent {
}
handleTimeUpdate = () => {
- this.setState({ progress: 100 * (this.video.currentTime / this.video.duration) });
+ this.setState({
+ currentTime: Math.floor(this.video.currentTime),
+ duration: Math.floor(this.video.duration),
+ });
}
handleMouseDown = e => {
@@ -143,8 +160,10 @@ export default class Video extends React.PureComponent {
handleMouseMove = throttle(e => {
const { x } = getPointerPosition(this.seek, e);
- this.video.currentTime = this.video.duration * x;
- this.setState({ progress: x * 100 });
+ const currentTime = Math.floor(this.video.duration * x);
+
+ this.video.currentTime = currentTime;
+ this.setState({ currentTime });
}, 60);
togglePlay = () => {
@@ -226,11 +245,12 @@ export default class Video extends React.PureComponent {
}
render () {
- const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl, alt } = this.props;
- const { progress, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
+ const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed } = this.props;
+ const { currentTime, duration, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
+ const progress = (currentTime / duration) * 100;
return (
-
+
-
-
-
- {!onCloseVideo && }
-
+
+
+
+
-
- {(!fullscreen && onOpenVideo) && }
- {onCloseVideo && }
-
+ {!onCloseVideo && }
+
+ {(detailed || fullscreen) &&
+
+ {formatTime(currentTime)}
+ /
+ {formatTime(duration)}
+
+ }
+
+
+
+ {(!fullscreen && onOpenVideo) && }
+ {onCloseVideo && }
+
+
diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js
new file mode 100644
index 000000000..3fc45077d
--- /dev/null
+++ b/app/javascript/mastodon/initial_state.js
@@ -0,0 +1,13 @@
+const element = document.getElementById('initial-state');
+const initialState = element && JSON.parse(element.textContent);
+
+const getMeta = (prop) => initialState && initialState.meta && initialState.meta[prop];
+
+export const reduceMotion = getMeta('reduce_motion');
+export const autoPlayGif = getMeta('auto_play_gif');
+export const unfollowModal = getMeta('unfollow_modal');
+export const boostModal = getMeta('boost_modal');
+export const deleteModal = getMeta('delete_modal');
+export const me = getMeta('me');
+
+export default initialState;
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 7cc8ea237..d699a69df 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -7,17 +7,22 @@
"account.followers": "المتابعون",
"account.follows": "يتبع",
"account.follows_you": "يتابعك",
+ "account.hide_reblogs": "إخفاء ترقيات @{name}",
"account.media": "وسائط",
"account.mention": "أُذكُر @{name}",
+ "account.moved_to": "{name} إنتقل إلى :",
"account.mute": "أكتم @{name}",
+ "account.mute_notifications": "كتم إخطارات @{name}",
"account.posts": "المشاركات",
"account.report": "أبلغ عن @{name}",
"account.requested": "في انتظار الموافقة",
"account.share": "مشاركة @{name}'s profile",
+ "account.show_reblogs": "عرض ترقيات @{name}",
"account.unblock": "إلغاء الحظر عن @{name}",
"account.unblock_domain": "فك حظر {domain}",
"account.unfollow": "إلغاء المتابعة",
"account.unmute": "إلغاء الكتم عن @{name}",
+ "account.unmute_notifications": "إلغاء كتم إخطارات @{name}",
"account.view_full_profile": "عرض الملف الشخصي كاملا",
"boost_modal.combo": "يمكنك ضغط {combo} لتخطّي هذه في المرّة القادمة",
"bundle_column_error.body": "لقد وقع هناك خطأ أثناء عملية تحميل هذا العنصر.",
@@ -31,8 +36,9 @@
"column.favourites": "المفضلة",
"column.follow_requests": "طلبات المتابعة",
"column.home": "الرئيسية",
+ "column.lists": "القوائم",
"column.mutes": "الحسابات المكتومة",
- "column.notifications": "الإشعارات",
+ "column.notifications": "الإخطارات",
"column.pins": "التبويقات المثبتة",
"column.public": "الخيط العام الموحد",
"column_back_button.label": "العودة",
@@ -57,6 +63,8 @@
"confirmations.block.message": "هل أنت متأكد أنك تريد حجب {name} ؟",
"confirmations.delete.confirm": "حذف",
"confirmations.delete.message": "هل أنت متأكد أنك تريد حذف هذا المنشور ؟",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "هل تود حقا حذف هذه القائمة ؟",
"confirmations.domain_block.confirm": "إخفاء إسم النطاق كاملا",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "أكتم",
@@ -71,7 +79,7 @@
"emoji_button.food": "الطعام والشراب",
"emoji_button.label": "أدرج إيموجي",
"emoji_button.nature": "الطبيعة",
- "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+ "emoji_button.not_found": "لا إيموجو !! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "أشياء",
"emoji_button.people": "الناس",
"emoji_button.recent": "الشائعة الإستخدام",
@@ -79,12 +87,13 @@
"emoji_button.search_results": "نتائج البحث",
"emoji_button.symbols": "رموز",
"emoji_button.travel": "أماكن و أسفار",
- "empty_column.community": "الخط الزمني المحلي فارغ. اكتب شيئا ما للعامة كبداية.",
+ "empty_column.community": "الخط الزمني المحلي فارغ. أكتب شيئا ما للعامة كبداية !",
"empty_column.hashtag": "ليس هناك بعدُ أي محتوى ذو علاقة بهذا الوسم.",
"empty_column.home": "إنك لا تتبع بعد أي شخص إلى حد الآن. زر {public} أو استخدام حقل البحث لكي تبدأ على التعرف على مستخدمين آخرين.",
"empty_column.home.public_timeline": "الخيط العام",
+ "empty_column.list": "هذه القائمة فارغة.",
"empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.",
- "empty_column.public": "لا يوجد شيء هنا ! قم بتحرير شيء ما بشكل عام، أو اتبع مستخدمين آخرين في الخوادم المثيلة الأخرى لملء خيط المحادثات العام.",
+ "empty_column.public": "لا يوجد أي شيء هنا ! قم بنشر شيء ما للعامة، أو إتبع مستخدمين آخرين في الخوادم المثيلة الأخرى لملء خيط المحادثات العام",
"follow_request.authorize": "ترخيص",
"follow_request.reject": "رفض",
"getting_started.appsshort": "تطبيقات",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "عرض الترقيات",
"home.column_settings.show_replies": "عرض الردود",
"home.settings": "إعدادات العمود",
+ "keyboard_shortcuts.back": "للعودة",
+ "keyboard_shortcuts.boost": "للترقية",
+ "keyboard_shortcuts.column": "للتركيز على منشور على أحد الأعمدة",
+ "keyboard_shortcuts.compose": "للتركيز على نافذة تحرير النصوص",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "للإنتقال إلى أسفل القائمة",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "للإضافة إلى المفضلة",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "مفتاح الإختصار",
+ "keyboard_shortcuts.legend": "لعرض هذا المفتاح",
+ "keyboard_shortcuts.mention": "لذِكر الناشر",
+ "keyboard_shortcuts.reply": "للردّ",
+ "keyboard_shortcuts.search": "للتركيز على البحث",
+ "keyboard_shortcuts.toot": "لتحرير تبويق جديد",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "للإنتقال إلى أعلى القائمة",
"lightbox.close": "إغلاق",
"lightbox.next": "التالي",
"lightbox.previous": "العودة",
+ "lists.account.add": "أضف إلى القائمة",
+ "lists.account.remove": "إحذف من القائمة",
+ "lists.delete": "Delete list",
+ "lists.edit": "تعديل القائمة",
+ "lists.new.create": "إنشاء قائمة",
+ "lists.new.title_placeholder": "عنوان القائمة الجديدة",
+ "lists.search": "إبحث في قائمة الحسابات التي تُتابِعها",
+ "lists.subheading": "قوائمك",
"loading_indicator.label": "تحميل ...",
"media_gallery.toggle_visible": "عرض / إخفاء",
"missing_indicator.label": "تعذر العثور عليه",
+ "mute_modal.hide_notifications": "هل تود إخفاء الإخطارات القادمة من هذا المستخدم ؟",
"navigation_bar.blocks": "الحسابات المحجوبة",
"navigation_bar.community_timeline": "الخيط العام المحلي",
"navigation_bar.edit_profile": "تعديل الملف الشخصي",
"navigation_bar.favourites": "المفضلة",
"navigation_bar.follow_requests": "طلبات المتابعة",
"navigation_bar.info": "معلومات إضافية",
+ "navigation_bar.keyboard_shortcuts": "إختصارات لوحة المفاتيح",
+ "navigation_bar.lists": "القوائم",
"navigation_bar.logout": "خروج",
"navigation_bar.mutes": "الحسابات المكتومة",
"navigation_bar.pins": "التبويقات المثبتة",
@@ -161,7 +198,7 @@
"privacy.unlisted.short": "غير مدرج",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
- "relative_time.just_now": "now",
+ "relative_time.just_now": "الآن",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "إلغاء",
@@ -172,9 +209,9 @@
"search_popout.search_format": "نمط البحث المتقدم",
"search_popout.tips.hashtag": "وسم",
"search_popout.tips.status": "حالة",
- "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
+ "search_popout.tips.text": "جملة قصيرة تُمكّنُك من عرض أسماء و حسابات و كلمات رمزية",
"search_popout.tips.user": "مستخدِم",
- "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
+ "search_results.total": "{count, number} {count, plural, one {result} و {results}}",
"standalone.public_title": "نظرة على ...",
"status.cannot_reblog": "تعذرت ترقية هذا المنشور",
"status.delete": "إحذف",
@@ -183,7 +220,7 @@
"status.load_more": "حمّل المزيد",
"status.media_hidden": "الصورة مستترة",
"status.mention": "أذكُر @{name}",
- "status.more": "More",
+ "status.more": "المزيد",
"status.mute_conversation": "كتم المحادثة",
"status.open": "وسع هذه المشاركة",
"status.pin": "تدبيس على الملف الشخصي",
@@ -204,6 +241,7 @@
"tabs_bar.home": "الرئيسية",
"tabs_bar.local_timeline": "المحلي",
"tabs_bar.notifications": "الإخطارات",
+ "ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.",
"upload_area.title": "إسحب ثم أفلت للرفع",
"upload_button.label": "إضافة وسائط",
"upload_form.description": "وصف للمعاقين بصريا",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index da2372cff..1c04b3bfa 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -7,17 +7,22 @@
"account.followers": "Последователи",
"account.follows": "Следвам",
"account.follows_you": "Твой последовател",
+ "account.hide_reblogs": "Hide boosts from @{name}",
"account.media": "Media",
"account.mention": "Споменаване",
+ "account.moved_to": "{name} has moved to:",
"account.mute": "Mute @{name}",
+ "account.mute_notifications": "Mute notifications from @{name}",
"account.posts": "Публикации",
"account.report": "Report @{name}",
"account.requested": "В очакване на одобрение",
"account.share": "Share @{name}'s profile",
+ "account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "Не блокирай",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Не следвай",
"account.unmute": "Unmute @{name}",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "View full profile",
"boost_modal.combo": "You can press {combo} to skip this next time",
"bundle_column_error.body": "Something went wrong while loading this component.",
@@ -31,6 +36,7 @@
"column.favourites": "Favourites",
"column.follow_requests": "Follow requests",
"column.home": "Начало",
+ "column.lists": "Lists",
"column.mutes": "Muted users",
"column.notifications": "Известия",
"column.pins": "Pinned toot",
@@ -57,6 +63,8 @@
"confirmations.block.message": "Are you sure you want to block {name}?",
"confirmations.delete.confirm": "Delete",
"confirmations.delete.message": "Are you sure you want to delete this status?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Mute",
@@ -83,6 +91,7 @@
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
"empty_column.home.public_timeline": "the public timeline",
+ "empty_column.list": "There is nothing in this list yet.",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
"follow_request.authorize": "Authorize",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Затвори",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "Зареждане...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.blocks": "Blocked users",
"navigation_bar.community_timeline": "Local timeline",
"navigation_bar.edit_profile": "Редактирай профил",
"navigation_bar.favourites": "Favourites",
"navigation_bar.follow_requests": "Follow requests",
"navigation_bar.info": "Extended information",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "Излизане",
"navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
@@ -204,6 +241,7 @@
"tabs_bar.home": "Начало",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Известия",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Drag & drop to upload",
"upload_button.label": "Добави медия",
"upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index af732921d..62d85a5e1 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -7,17 +7,22 @@
"account.followers": "Seguidors",
"account.follows": "Seguint",
"account.follows_you": "et segueix",
+ "account.hide_reblogs": "Amaga els impulsos de @{name}",
"account.media": "Media",
"account.mention": "Esmentar @{name}",
+ "account.moved_to": "{name} s'ha mogut a:",
"account.mute": "Silenciar @{name}",
+ "account.mute_notifications": "Notificacions desactivades de @{name}",
"account.posts": "Publicacions",
"account.report": "Informe @{name}",
- "account.requested": "Esperant aprovació",
+ "account.requested": "Esperant aprovació. Clic per a cancel·lar la petició de seguiment",
"account.share": "Compartir el perfil de @{name}",
+ "account.show_reblogs": "Mostra els impulsos de @{name}",
"account.unblock": "Desbloquejar @{name}",
"account.unblock_domain": "Mostra {domain}",
"account.unfollow": "Deixar de seguir",
"account.unmute": "Treure silenci de @{name}",
+ "account.unmute_notifications": "Activar notificacions de @{name}",
"account.view_full_profile": "Veure el perfil complet",
"boost_modal.combo": "Pots premer {combo} per saltar-te això el proper cop",
"bundle_column_error.body": "S'ha produït un error en carregar aquest component.",
@@ -31,6 +36,7 @@
"column.favourites": "Favorits",
"column.follow_requests": "Peticions per seguir-te",
"column.home": "Inici",
+ "column.lists": "Llistes",
"column.mutes": "Usuaris silenciats",
"column.notifications": "Notificacions",
"column.pins": "Toot fixat",
@@ -51,12 +57,14 @@
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marcar multimèdia com a sensible",
"compose_form.spoiler": "Amagar text darrera l'advertència",
- "compose_form.spoiler_placeholder": "Advertència de contingut",
+ "compose_form.spoiler_placeholder": "Escriu l'advertència aquí",
"confirmation_modal.cancel": "Cancel·lar",
"confirmations.block.confirm": "Bloquejar",
"confirmations.block.message": "Estàs segur que vols bloquejar {name}?",
"confirmations.delete.confirm": "Esborrar",
"confirmations.delete.message": "Estàs segur que vols esborrar aquest estat?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Estàs segur que vols esborrar permanenment aquesta llista?",
"confirmations.domain_block.confirm": "Amagar tot el domini",
"confirmations.domain_block.message": "Estàs realment, realment segur que vols bloquejar totalment {domain}? En la majoria dels casos bloquejar o silenciar és suficient i preferible.",
"confirmations.mute.confirm": "Silenciar",
@@ -64,10 +72,10 @@
"confirmations.unfollow.confirm": "Deixar de seguir",
"confirmations.unfollow.message": "Estàs segur que vols deixar de seguir {name}?",
"embed.instructions": "Incrusta aquest estat al lloc web copiant el codi a continuació.",
- "embed.preview": "A continuació s'explica com:",
+ "embed.preview": "Aquí tenim quin aspecte tindrá:",
"emoji_button.activity": "Activitat",
"emoji_button.custom": "Personalitzat",
- "emoji_button.flags": "Flags",
+ "emoji_button.flags": "Marques",
"emoji_button.food": "Menjar i Beure",
"emoji_button.label": "Inserir emoji",
"emoji_button.nature": "Natura",
@@ -83,6 +91,7 @@
"empty_column.hashtag": "Encara no hi ha res amb aquesta etiqueta.",
"empty_column.home": "Encara no segueixes ningú. Visita {public} o fes cerca per començar i conèixer altres usuaris.",
"empty_column.home.public_timeline": "la línia de temps pública",
+ "empty_column.list": "Encara no hi ha res en aquesta llista.",
"empty_column.notifications": "Encara no tens notificacions. Interactua amb altres per iniciar la conversa.",
"empty_column.public": "No hi ha res aquí! Escriu alguna cosa públicament o segueix manualment usuaris d'altres instàncies per omplir-ho",
"follow_request.authorize": "Autoritzar",
@@ -95,21 +104,49 @@
"home.column_settings.advanced": "Avançat",
"home.column_settings.basic": "Bàsic",
"home.column_settings.filter_regex": "Filtrar per expressió regular",
- "home.column_settings.show_reblogs": "Mostrar 'boosts'",
+ "home.column_settings.show_reblogs": "Mostrar impulsos",
"home.column_settings.show_replies": "Mostrar respostes",
"home.settings": "Ajustos de columna",
+ "keyboard_shortcuts.back": "navegar enrera",
+ "keyboard_shortcuts.boost": "impulsar",
+ "keyboard_shortcuts.column": "per centrar un estat en una de les columnes",
+ "keyboard_shortcuts.compose": "per centrar l'area de composició de text",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "per baixar en la llista",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "afavorir",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Tecla d'accés directe",
+ "keyboard_shortcuts.legend": "per a mostrar aquesta llegenda",
+ "keyboard_shortcuts.mention": "per esmentar l'autor",
+ "keyboard_shortcuts.reply": "respondre",
+ "keyboard_shortcuts.search": "per centrar la cerca",
+ "keyboard_shortcuts.toot": "per a començar un toot nou de trinca",
+ "keyboard_shortcuts.unfocus": "descentrar l'area de composició de text/cerca",
+ "keyboard_shortcuts.up": "moure amunt en la llista",
"lightbox.close": "Tancar",
"lightbox.next": "Següent",
"lightbox.previous": "Anterior",
+ "lists.account.add": "Afegir a la llista",
+ "lists.account.remove": "Treure de la llista",
+ "lists.delete": "Delete list",
+ "lists.edit": "Editar llista",
+ "lists.new.create": "Afegir llista",
+ "lists.new.title_placeholder": "Nou títol de llista",
+ "lists.search": "Cercar entre les persones que segueixes",
+ "lists.subheading": "Les teves llistes",
"loading_indicator.label": "Carregant...",
"media_gallery.toggle_visible": "Alternar visibilitat",
"missing_indicator.label": "No trobat",
+ "mute_modal.hide_notifications": "Amagar notificacions d'aquest usuari?",
"navigation_bar.blocks": "Usuaris bloquejats",
"navigation_bar.community_timeline": "Línia de temps Local",
"navigation_bar.edit_profile": "Editar perfil",
"navigation_bar.favourites": "Favorits",
"navigation_bar.follow_requests": "Sol·licituds de seguiment",
"navigation_bar.info": "Informació addicional",
+ "navigation_bar.keyboard_shortcuts": "Dreceres de teclat",
+ "navigation_bar.lists": "Llistes",
"navigation_bar.logout": "Tancar sessió",
"navigation_bar.mutes": "Usuaris silenciats",
"navigation_bar.pins": "Toots fixats",
@@ -127,7 +164,7 @@
"notifications.column_settings.mention": "Mencions:",
"notifications.column_settings.push": "Push notificacions",
"notifications.column_settings.push_meta": "Aquest dispositiu",
- "notifications.column_settings.reblog": "Boosts:",
+ "notifications.column_settings.reblog": "Impulsos:",
"notifications.column_settings.show": "Mostrar en la columna",
"notifications.column_settings.sound": "Reproduïr so",
"onboarding.done": "Fet",
@@ -159,18 +196,18 @@
"privacy.public.short": "Públic",
"privacy.unlisted.long": "No publicar en línies de temps públiques",
"privacy.unlisted.short": "No llistat",
- "relative_time.days": "fa {number} jorns",
+ "relative_time.days": "fa {number} dies",
"relative_time.hours": "fa {number} hores",
"relative_time.just_now": "ara",
- "relative_time.minutes": "fa {number} minutes",
- "relative_time.seconds": "fa {number} segondes",
+ "relative_time.minutes": "fa {number} minuts",
+ "relative_time.seconds": "fa {number} segons",
"reply_indicator.cancel": "Cancel·lar",
"report.placeholder": "Comentaris addicionals",
"report.submit": "Enviar",
"report.target": "Informes",
"search.placeholder": "Cercar",
"search_popout.search_format": "Format de cerca avançada",
- "search_popout.tips.hashtag": "hashtag",
+ "search_popout.tips.hashtag": "etiqueta",
"search_popout.tips.status": "status",
"search_popout.tips.text": "El text simple retorna coincidències amb els noms de visualització, els noms d'usuari i els hashtags",
"search_popout.tips.user": "usuari",
@@ -187,7 +224,7 @@
"status.mute_conversation": "Silenciar conversació",
"status.open": "Ampliar aquest estat",
"status.pin": "Fixat en el perfil",
- "status.reblog": "Boost",
+ "status.reblog": "Impuls",
"status.reblogged_by": "{name} ha retootejat",
"status.reply": "Respondre",
"status.replyAll": "Respondre al tema",
@@ -204,6 +241,7 @@
"tabs_bar.home": "Inici",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notificacions",
+ "ui.beforeunload": "El vostre esborrany es perdrà si sortiu de Mastodon.",
"upload_area.title": "Arrossega i deixa anar per carregar",
"upload_button.label": "Afegir multimèdia",
"upload_form.description": "Descriure els problemes visuals",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 283a2946f..6354f18b6 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -7,17 +7,22 @@
"account.followers": "Folgende",
"account.follows": "Folgt",
"account.follows_you": "Folgt dir",
+ "account.hide_reblogs": "Geteilte Beiträge von @{name} verbergen",
"account.media": "Medien",
"account.mention": "@{name} erwähnen",
+ "account.moved_to": "{name} ist umgezogen auf:",
"account.mute": "@{name} stummschalten",
+ "account.mute_notifications": "Benachrichtigungen von @{name} verbergen",
"account.posts": "Beiträge",
"account.report": "@{name} melden",
"account.requested": "Warte auf Erlaubnis. Klicke zum Abbrechen",
"account.share": "Profil von @{name} teilen",
+ "account.show_reblogs": "Von @{name} geteilte Beiträge anzeigen",
"account.unblock": "@{name} entblocken",
"account.unblock_domain": "{domain} wieder anzeigen",
"account.unfollow": "Entfolgen",
"account.unmute": "@{name} nicht mehr stummschalten",
+ "account.unmute_notifications": "Benachrichtigungen von @{name} einschalten",
"account.view_full_profile": "Vollständiges Profil anzeigen",
"boost_modal.combo": "Du kannst {combo} drücken, um dies beim nächsten Mal zu überspringen",
"bundle_column_error.body": "Etwas ist beim Laden schiefgelaufen.",
@@ -31,6 +36,7 @@
"column.favourites": "Favoriten",
"column.follow_requests": "Folgeanfragen",
"column.home": "Startseite",
+ "column.lists": "Lists",
"column.mutes": "Stummgeschaltete Profile",
"column.notifications": "Mitteilungen",
"column.pins": "Angeheftete Beiträge",
@@ -57,6 +63,8 @@
"confirmations.block.message": "Bist du dir sicher, dass du {name} blockieren möchtest?",
"confirmations.delete.confirm": "Löschen",
"confirmations.delete.message": "Bist du dir sicher, dass du diesen Beitrag löschen möchtest?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Die ganze Domain verbergen",
"confirmations.domain_block.message": "Bist du dir wirklich sicher, dass du die ganze Domain {domain} verbergen willst? In den meisten Fällen reichen ein paar gezielte Blocks aus.",
"confirmations.mute.confirm": "Stummschalten",
@@ -75,7 +83,7 @@
"emoji_button.objects": "Gegenstände",
"emoji_button.people": "Personen",
"emoji_button.recent": "Häufig benutzt",
- "emoji_button.search": "Suchen",
+ "emoji_button.search": "Suchen…",
"emoji_button.search_results": "Suchergebnisse",
"emoji_button.symbols": "Symbole",
"emoji_button.travel": "Reisen und Orte",
@@ -83,6 +91,7 @@
"empty_column.hashtag": "Unter diesem Hashtag gibt es noch nichts.",
"empty_column.home": "Deine Startseite ist leer! Besuche {public} oder nutze die Suche, um loszulegen und andere Leute zu finden.",
"empty_column.home.public_timeline": "die öffentliche Zeitleiste",
+ "empty_column.list": "Diese Liste ist derzeit leer.",
"empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um ins Gespräch zu kommen.",
"empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Profilen von anderen Instanzen, um die Zeitleiste aufzufüllen",
"follow_request.authorize": "Erlauben",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen",
"home.column_settings.show_replies": "Antworten anzeigen",
"home.settings": "Spalteneinstellungen",
+ "keyboard_shortcuts.back": "zurück navigieren",
+ "keyboard_shortcuts.boost": "boosten",
+ "keyboard_shortcuts.column": "einen Status in einer der Spalten fokussieren",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "sich in der Liste hinunter bewegen",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "favorisieren",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "diese Übersicht anzeigen",
+ "keyboard_shortcuts.mention": "Autor_in erwähnen",
+ "keyboard_shortcuts.reply": "antworten",
+ "keyboard_shortcuts.search": "die Suche fokussieren",
+ "keyboard_shortcuts.toot": "einen neuen Toot beginnen",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "sich in der Liste hinauf bewegen",
"lightbox.close": "Schließen",
"lightbox.next": "Weiter",
"lightbox.previous": "Zurück",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "Wird geladen …",
"media_gallery.toggle_visible": "Sichtbarkeit umschalten",
"missing_indicator.label": "Nicht gefunden",
+ "mute_modal.hide_notifications": "Benachrichtigungen von diesem Account verbergen?",
"navigation_bar.blocks": "Blockierte Profile",
"navigation_bar.community_timeline": "Lokale Zeitleiste",
"navigation_bar.edit_profile": "Profil bearbeiten",
"navigation_bar.favourites": "Favoriten",
"navigation_bar.follow_requests": "Folgeanfragen",
"navigation_bar.info": "Über diese Instanz",
+ "navigation_bar.keyboard_shortcuts": "Tastenkombinationen",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "Abmelden",
"navigation_bar.mutes": "Stummgeschaltete Profile",
"navigation_bar.pins": "Angeheftete Beiträge",
@@ -144,7 +181,7 @@
"onboarding.page_six.apps_available": "Es gibt verschiedene {apps} für iOS, Android und weitere Plattformen.",
"onboarding.page_six.github": "Mastodon ist freie, quelloffene Software. Du kannst auf {github} dazu beitragen, Probleme melden und Wünsche äußern.",
"onboarding.page_six.guidelines": "Richtlinien",
- "onboarding.page_six.read_guidelines": "Bitte mach dich mit den {guidelines} von {domain} vertraut.",
+ "onboarding.page_six.read_guidelines": "Bitte mach dich mit den {guidelines} von {domain} vertraut!",
"onboarding.page_six.various_app": "Apps",
"onboarding.page_three.profile": "Bearbeite dein Profil, um dein Bild, deinen Namen und deine Beschreibung anzupassen. Dort findest du auch weitere Einstellungen.",
"onboarding.page_three.search": "Benutze die Suchfunktion, um Leute zu finden und mit Hashtags wie {illustration} oder {introductions} nach Beiträgen zu suchen. Um eine Person zu finden, die auf einer anderen Instanz ist, benutze den vollständigen Profilnamen.",
@@ -204,6 +241,7 @@
"tabs_bar.home": "Startseite",
"tabs_bar.local_timeline": "Lokal",
"tabs_bar.notifications": "Mitteilungen",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Zum Hochladen hereinziehen",
"upload_button.label": "Mediendatei hinzufügen",
"upload_form.description": "Für Menschen mit Sehbehinderung beschreiben",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index f400b283f..bb82cf5f5 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -29,6 +29,14 @@
{
"defaultMessage": "Unmute @{name}",
"id": "account.unmute"
+ },
+ {
+ "defaultMessage": "Mute notifications from @{name}",
+ "id": "account.mute_notifications"
+ },
+ {
+ "defaultMessage": "Unmute notifications from @{name}",
+ "id": "account.unmute_notifications"
}
],
"path": "app/javascript/mastodon/components/account.json"
@@ -283,17 +291,9 @@
"defaultMessage": "Block",
"id": "confirmations.block.confirm"
},
- {
- "defaultMessage": "Mute",
- "id": "confirmations.mute.confirm"
- },
{
"defaultMessage": "Are you sure you want to block {name}?",
"id": "confirmations.block.message"
- },
- {
- "defaultMessage": "Are you sure you want to mute {name}?",
- "id": "confirmations.mute.message"
}
],
"path": "app/javascript/mastodon/containers/status_container.json"
@@ -307,6 +307,15 @@
],
"path": "app/javascript/mastodon/features/account_gallery/index.json"
},
+ {
+ "descriptors": [
+ {
+ "defaultMessage": "{name} has moved to:",
+ "id": "account.moved_to"
+ }
+ ],
+ "path": "app/javascript/mastodon/features/account_timeline/components/moved_note.json"
+ },
{
"descriptors": [
{
@@ -317,10 +326,6 @@
"defaultMessage": "Block",
"id": "confirmations.block.confirm"
},
- {
- "defaultMessage": "Mute",
- "id": "confirmations.mute.confirm"
- },
{
"defaultMessage": "Hide entire domain",
"id": "confirmations.domain_block.confirm"
@@ -333,10 +338,6 @@
"defaultMessage": "Are you sure you want to block {name}?",
"id": "confirmations.block.message"
},
- {
- "defaultMessage": "Are you sure you want to mute {name}?",
- "id": "confirmations.mute.message"
- },
{
"defaultMessage": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"id": "confirmations.domain_block.message"
@@ -398,6 +399,14 @@
"defaultMessage": "Unhide {domain}",
"id": "account.unblock_domain"
},
+ {
+ "defaultMessage": "Hide boosts from @{name}",
+ "id": "account.hide_reblogs"
+ },
+ {
+ "defaultMessage": "Show boosts from @{name}",
+ "id": "account.show_reblogs"
+ },
{
"defaultMessage": "Information below may reflect the user's profile incompletely.",
"id": "account.disclaimer_full"
@@ -848,6 +857,14 @@
"defaultMessage": "Pinned toots",
"id": "navigation_bar.pins"
},
+ {
+ "defaultMessage": "Lists",
+ "id": "navigation_bar.lists"
+ },
+ {
+ "defaultMessage": "Keyboard shortcuts",
+ "id": "navigation_bar.keyboard_shortcuts"
+ },
{
"defaultMessage": "FAQ",
"id": "getting_started.faq"
@@ -922,6 +939,152 @@
],
"path": "app/javascript/mastodon/features/home_timeline/index.json"
},
+ {
+ "descriptors": [
+ {
+ "defaultMessage": "Keyboard Shortcuts",
+ "id": "keyboard_shortcuts.heading"
+ },
+ {
+ "defaultMessage": "Hotkey",
+ "id": "keyboard_shortcuts.hotkey"
+ },
+ {
+ "defaultMessage": "Description",
+ "id": "keyboard_shortcuts.description"
+ },
+ {
+ "defaultMessage": "to reply",
+ "id": "keyboard_shortcuts.reply"
+ },
+ {
+ "defaultMessage": "to mention author",
+ "id": "keyboard_shortcuts.mention"
+ },
+ {
+ "defaultMessage": "to favourite",
+ "id": "keyboard_shortcuts.favourite"
+ },
+ {
+ "defaultMessage": "to boost",
+ "id": "keyboard_shortcuts.boost"
+ },
+ {
+ "defaultMessage": "to open status",
+ "id": "keyboard_shortcuts.enter"
+ },
+ {
+ "defaultMessage": "to move up in the list",
+ "id": "keyboard_shortcuts.up"
+ },
+ {
+ "defaultMessage": "to move down in the list",
+ "id": "keyboard_shortcuts.down"
+ },
+ {
+ "defaultMessage": "to focus a status in one of the columns",
+ "id": "keyboard_shortcuts.column"
+ },
+ {
+ "defaultMessage": "to focus the compose textarea",
+ "id": "keyboard_shortcuts.compose"
+ },
+ {
+ "defaultMessage": "to start a brand new toot",
+ "id": "keyboard_shortcuts.toot"
+ },
+ {
+ "defaultMessage": "to navigate back",
+ "id": "keyboard_shortcuts.back"
+ },
+ {
+ "defaultMessage": "to focus search",
+ "id": "keyboard_shortcuts.search"
+ },
+ {
+ "defaultMessage": "to un-focus compose textarea/search",
+ "id": "keyboard_shortcuts.unfocus"
+ },
+ {
+ "defaultMessage": "to display this legend",
+ "id": "keyboard_shortcuts.legend"
+ }
+ ],
+ "path": "app/javascript/mastodon/features/keyboard_shortcuts/index.json"
+ },
+ {
+ "descriptors": [
+ {
+ "defaultMessage": "Remove from list",
+ "id": "lists.account.remove"
+ },
+ {
+ "defaultMessage": "Add to list",
+ "id": "lists.account.add"
+ }
+ ],
+ "path": "app/javascript/mastodon/features/list_editor/components/account.json"
+ },
+ {
+ "descriptors": [
+ {
+ "defaultMessage": "Search among people you follow",
+ "id": "lists.search"
+ }
+ ],
+ "path": "app/javascript/mastodon/features/list_editor/components/search.json"
+ },
+ {
+ "descriptors": [
+ {
+ "defaultMessage": "Are you sure you want to permanently delete this list?",
+ "id": "confirmations.delete_list.message"
+ },
+ {
+ "defaultMessage": "Delete",
+ "id": "confirmations.delete_list.confirm"
+ },
+ {
+ "defaultMessage": "Edit list",
+ "id": "lists.edit"
+ },
+ {
+ "defaultMessage": "Delete list",
+ "id": "lists.delete"
+ },
+ {
+ "defaultMessage": "There is nothing in this list yet.",
+ "id": "empty_column.list"
+ }
+ ],
+ "path": "app/javascript/mastodon/features/list_timeline/index.json"
+ },
+ {
+ "descriptors": [
+ {
+ "defaultMessage": "New list title",
+ "id": "lists.new.title_placeholder"
+ },
+ {
+ "defaultMessage": "Add list",
+ "id": "lists.new.create"
+ }
+ ],
+ "path": "app/javascript/mastodon/features/lists/components/new_list_form.json"
+ },
+ {
+ "descriptors": [
+ {
+ "defaultMessage": "Lists",
+ "id": "column.lists"
+ },
+ {
+ "defaultMessage": "Your lists",
+ "id": "lists.subheading"
+ }
+ ],
+ "path": "app/javascript/mastodon/features/lists/index.json"
+ },
{
"descriptors": [
{
@@ -1207,6 +1370,27 @@
],
"path": "app/javascript/mastodon/features/ui/components/media_modal.json"
},
+ {
+ "descriptors": [
+ {
+ "defaultMessage": "Are you sure you want to mute {name}?",
+ "id": "confirmations.mute.message"
+ },
+ {
+ "defaultMessage": "Hide notifications from this user?",
+ "id": "mute_modal.hide_notifications"
+ },
+ {
+ "defaultMessage": "Cancel",
+ "id": "confirmation_modal.cancel"
+ },
+ {
+ "defaultMessage": "Mute",
+ "id": "confirmations.mute.confirm"
+ }
+ ],
+ "path": "app/javascript/mastodon/features/ui/components/mute_modal.json"
+ },
{
"descriptors": [
{
@@ -1359,6 +1543,15 @@
],
"path": "app/javascript/mastodon/features/ui/components/upload_area.json"
},
+ {
+ "descriptors": [
+ {
+ "defaultMessage": "Your draft will be lost if you leave Mastodon.",
+ "id": "ui.beforeunload"
+ }
+ ],
+ "path": "app/javascript/mastodon/features/ui/index.json"
+ },
{
"descriptors": [
{
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 1d0bbcee5..5c39bd682 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -7,17 +7,22 @@
"account.followers": "Followers",
"account.follows": "Follows",
"account.follows_you": "Follows you",
+ "account.hide_reblogs": "Hide boosts from @{name}",
"account.media": "Media",
"account.mention": "Mention @{name}",
+ "account.moved_to": "{name} has moved to:",
"account.mute": "Mute @{name}",
+ "account.mute_notifications": "Mute notifications from @{name}",
"account.posts": "Posts",
"account.report": "Report @{name}",
"account.requested": "Awaiting approval. Click to cancel follow request",
"account.share": "Share @{name}'s profile",
+ "account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "Unblock @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Unfollow",
"account.unmute": "Unmute @{name}",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "View full profile",
"boost_modal.combo": "You can press {combo} to skip this next time",
"bundle_column_error.body": "Something went wrong while loading this component.",
@@ -31,6 +36,7 @@
"column.favourites": "Favourites",
"column.follow_requests": "Follow requests",
"column.home": "Home",
+ "column.lists": "Lists",
"column.mutes": "Muted users",
"column.notifications": "Notifications",
"column.pins": "Pinned toots",
@@ -57,6 +63,8 @@
"confirmations.block.message": "Are you sure you want to block {name}?",
"confirmations.delete.confirm": "Delete",
"confirmations.delete.message": "Are you sure you want to delete this status?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Mute",
@@ -83,6 +91,7 @@
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
"empty_column.home.public_timeline": "the public timeline",
+ "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
"follow_request.authorize": "Authorize",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Close",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.blocks": "Blocked users",
"navigation_bar.community_timeline": "Local timeline",
"navigation_bar.edit_profile": "Edit profile",
"navigation_bar.favourites": "Favourites",
"navigation_bar.follow_requests": "Follow requests",
"navigation_bar.info": "About this instance",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "Logout",
"navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
@@ -204,6 +241,7 @@
"tabs_bar.home": "Home",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notifications",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Drag & drop to upload",
"upload_button.label": "Add media",
"upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 22639f6f9..9e66c379f 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -1,221 +1,259 @@
{
"account.block": "Bloki @{name}",
- "account.block_domain": "Hide everything from {domain}",
- "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+ "account.block_domain": "Kaŝi ĉion el {domain}",
+ "account.disclaimer_full": "La ĉi-subaj informoj povas ne plene reflekti la profilon de la uzanto.",
"account.edit_profile": "Redakti la profilon",
"account.follow": "Sekvi",
"account.followers": "Sekvantoj",
"account.follows": "Sekvatoj",
"account.follows_you": "Sekvas vin",
- "account.media": "Media",
+ "account.hide_reblogs": "Hide boosts from @{name}",
+ "account.media": "Sonbildaĵoj",
"account.mention": "Mencii @{name}",
- "account.mute": "Mute @{name}",
+ "account.moved_to": "{name} has moved to:",
+ "account.mute": "Silentigi @{name}",
+ "account.mute_notifications": "Mute notifications from @{name}",
"account.posts": "Mesaĝoj",
- "account.report": "Report @{name}",
+ "account.report": "Signali @{name}",
"account.requested": "Atendas aprobon",
- "account.share": "Share @{name}'s profile",
+ "account.share": "Diskonigi la profilon de @{name}",
+ "account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "Malbloki @{name}",
- "account.unblock_domain": "Unhide {domain}",
- "account.unfollow": "Malsekvi",
- "account.unmute": "Unmute @{name}",
- "account.view_full_profile": "View full profile",
- "boost_modal.combo": "You can press {combo} to skip this next time",
- "bundle_column_error.body": "Something went wrong while loading this component.",
- "bundle_column_error.retry": "Try again",
- "bundle_column_error.title": "Network error",
- "bundle_modal_error.close": "Close",
- "bundle_modal_error.message": "Something went wrong while loading this component.",
- "bundle_modal_error.retry": "Try again",
- "column.blocks": "Blocked users",
+ "account.unblock_domain": "Malkaŝi {domain}",
+ "account.unfollow": "Ne plus sekvi",
+ "account.unmute": "Malsilentigi @{name}",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
+ "account.view_full_profile": "Vidi plenan profilon",
+ "boost_modal.combo": "La proksiman fojon, premu {combo} por pasigi",
+ "bundle_column_error.body": "Io malfunkciis ŝargante tiun ĉi komponanton.",
+ "bundle_column_error.retry": "Bonvolu reprovi",
+ "bundle_column_error.title": "Reta eraro",
+ "bundle_modal_error.close": "Fermi",
+ "bundle_modal_error.message": "Io malfunkciis ŝargante tiun ĉi komponanton.",
+ "bundle_modal_error.retry": "Bonvolu reprovi",
+ "column.blocks": "Blokitaj uzantoj",
"column.community": "Loka tempolinio",
- "column.favourites": "Favourites",
- "column.follow_requests": "Follow requests",
+ "column.favourites": "Favoritoj",
+ "column.follow_requests": "Abonpetoj",
"column.home": "Hejmo",
- "column.mutes": "Muted users",
+ "column.lists": "Lists",
+ "column.mutes": "Silentigitaj uzantoj",
"column.notifications": "Sciigoj",
- "column.pins": "Pinned toot",
+ "column.pins": "Alpinglitaj pepoj",
"column.public": "Fratara tempolinio",
"column_back_button.label": "Reveni",
- "column_header.hide_settings": "Hide settings",
- "column_header.moveLeft_settings": "Move column to the left",
- "column_header.moveRight_settings": "Move column to the right",
- "column_header.pin": "Pin",
- "column_header.show_settings": "Show settings",
- "column_header.unpin": "Unpin",
- "column_subheading.navigation": "Navigation",
- "column_subheading.settings": "Settings",
- "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
- "compose_form.lock_disclaimer.lock": "locked",
+ "column_header.hide_settings": "Kaŝi agordojn",
+ "column_header.moveLeft_settings": "Movi kolumnon maldekstren",
+ "column_header.moveRight_settings": "Movi kolumnon dekstren",
+ "column_header.pin": "Alpingli",
+ "column_header.show_settings": "Malkaŝi agordojn",
+ "column_header.unpin": "Depingli",
+ "column_subheading.navigation": "Navigado",
+ "column_subheading.settings": "Agordoj",
+ "compose_form.lock_disclaimer": "Via konta ne estas ŝlosita. Iu ajn povas sekvi vin por vidi viajn privatajn pepojn.",
+ "compose_form.lock_disclaimer.lock": "ŝlosita",
"compose_form.placeholder": "Pri kio vi pensas?",
"compose_form.publish": "Hup",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marki ke la enhavo estas tikla",
"compose_form.spoiler": "Kaŝi la tekston malantaŭ averto",
- "compose_form.spoiler_placeholder": "Content warning",
- "confirmation_modal.cancel": "Cancel",
- "confirmations.block.confirm": "Block",
- "confirmations.block.message": "Are you sure you want to block {name}?",
- "confirmations.delete.confirm": "Delete",
- "confirmations.delete.message": "Are you sure you want to delete this status?",
- "confirmations.domain_block.confirm": "Hide entire domain",
- "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
- "confirmations.mute.confirm": "Mute",
- "confirmations.mute.message": "Are you sure you want to mute {name}?",
- "confirmations.unfollow.confirm": "Unfollow",
- "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
- "embed.instructions": "Embed this status on your website by copying the code below.",
- "embed.preview": "Here is what it will look like:",
- "emoji_button.activity": "Activity",
- "emoji_button.custom": "Custom",
- "emoji_button.flags": "Flags",
- "emoji_button.food": "Food & Drink",
- "emoji_button.label": "Insert emoji",
- "emoji_button.nature": "Nature",
- "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
- "emoji_button.objects": "Objects",
- "emoji_button.people": "People",
- "emoji_button.recent": "Frequently used",
- "emoji_button.search": "Search...",
- "emoji_button.search_results": "Search results",
- "emoji_button.symbols": "Symbols",
- "emoji_button.travel": "Travel & Places",
- "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
- "empty_column.hashtag": "There is nothing in this hashtag yet.",
- "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
- "empty_column.home.public_timeline": "the public timeline",
- "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
- "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
- "follow_request.authorize": "Authorize",
- "follow_request.reject": "Reject",
- "getting_started.appsshort": "Apps",
- "getting_started.faq": "FAQ",
+ "compose_form.spoiler_placeholder": "Skribu tie vian averton",
+ "confirmation_modal.cancel": "Malfari",
+ "confirmations.block.confirm": "Bloki",
+ "confirmations.block.message": "Ĉu vi konfirmas la blokadon de {name}?",
+ "confirmations.delete.confirm": "Malaperigi",
+ "confirmations.delete.message": "Ĉu vi konfirmas la malaperigon de tiun pepon?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+ "confirmations.domain_block.confirm": "Kaŝi la tutan reton",
+ "confirmations.domain_block.message": "Ĉu vi vere, vere certas, ke vi volas bloki {domain} tute? Plej ofte, kelkaj celitaj blokadoj aŭ silentigoj estas sufiĉaj kaj preferindaj.",
+ "confirmations.mute.confirm": "Silentigi",
+ "confirmations.mute.message": "Ĉu vi konfirmas la silentigon de {name}?",
+ "confirmations.unfollow.confirm": "Ne plu sekvi",
+ "confirmations.unfollow.message": "Ĉu vi volas ĉesi sekvi {name}?",
+ "embed.instructions": "Enmetu tiun statkonigon ĉe vian retejon kopiante la ĉi-suban kodon.",
+ "embed.preview": "Ĝi aperos tiel:",
+ "emoji_button.activity": "Aktivecoj",
+ "emoji_button.custom": "Personaj",
+ "emoji_button.flags": "Flagoj",
+ "emoji_button.food": "Manĝi kaj trinki",
+ "emoji_button.label": "Enmeti mieneton",
+ "emoji_button.nature": "Naturo",
+ "emoji_button.not_found": "Neniuj mienetoj!! (╯°□°)╯︵ ┻━┻",
+ "emoji_button.objects": "Objektoj",
+ "emoji_button.people": "Homoj",
+ "emoji_button.recent": "Ofte uzataj",
+ "emoji_button.search": "Serĉo…",
+ "emoji_button.search_results": "Rezultatoj de serĉo",
+ "emoji_button.symbols": "Simboloj",
+ "emoji_button.travel": "Vojaĝoj & lokoj",
+ "empty_column.community": "La loka tempolinio estas malplena. Skribu ion por plenigi ĝin!",
+ "empty_column.hashtag": "Ĝise, neniu enhavo estas asociita kun tiu kradvorto.",
+ "empty_column.home": "Via hejma tempolinio estas malplena! Vizitu {public} aŭ uzu la serĉilon por renkonti aliajn uzantojn.",
+ "empty_column.home.public_timeline": "la publika tempolinio",
+ "empty_column.list": "There is nothing in this list yet.",
+ "empty_column.notifications": "Vi dume ne havas sciigojn. Interagi kun aliajn uzantojn por komenci la konversacion.",
+ "empty_column.public": "Estas nenio ĉi tie! Publike skribu ion, aŭ mane sekvu uzantojn de aliaj instancoj por plenigi la publikan tempolinion.",
+ "follow_request.authorize": "Akcepti",
+ "follow_request.reject": "Rifuzi",
+ "getting_started.appsshort": "Aplikaĵoj",
+ "getting_started.faq": "Oftaj demandoj",
"getting_started.heading": "Por komenci",
- "getting_started.open_source_notice": "Mastodon estas malfermitkoda programo. Vi povas kontribui aŭ raporti problemojn en github je {github}.",
- "getting_started.userguide": "User Guide",
- "home.column_settings.advanced": "Advanced",
- "home.column_settings.basic": "Basic",
- "home.column_settings.filter_regex": "Filter out by regular expressions",
- "home.column_settings.show_reblogs": "Show boosts",
- "home.column_settings.show_replies": "Show replies",
- "home.settings": "Column settings",
+ "getting_started.open_source_notice": "Mastodono estas malfermkoda programo. Vi povas kontribui aŭ raporti problemojn en GitHub je {github}.",
+ "getting_started.userguide": "Gvidilo de uzo",
+ "home.column_settings.advanced": "Precizaj agordoj",
+ "home.column_settings.basic": "Bazaj agordoj",
+ "home.column_settings.filter_regex": "Forfiltri per regulesprimo",
+ "home.column_settings.show_reblogs": "Montri diskonigojn",
+ "home.column_settings.show_replies": "Montri respondojn",
+ "home.settings": "Agordoj de la kolumno",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Fermi",
- "lightbox.next": "Next",
- "lightbox.previous": "Previous",
- "loading_indicator.label": "Ŝarĝanta...",
- "media_gallery.toggle_visible": "Toggle visibility",
- "missing_indicator.label": "Not found",
- "navigation_bar.blocks": "Blocked users",
+ "lightbox.next": "Malantaŭa",
+ "lightbox.previous": "Antaŭa",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
+ "loading_indicator.label": "Ŝarganta…",
+ "media_gallery.toggle_visible": "Baskuli videblecon",
+ "missing_indicator.label": "Ne trovita",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
+ "navigation_bar.blocks": "Blokitaj uzantoj",
"navigation_bar.community_timeline": "Loka tempolinio",
"navigation_bar.edit_profile": "Redakti la profilon",
- "navigation_bar.favourites": "Favourites",
- "navigation_bar.follow_requests": "Follow requests",
- "navigation_bar.info": "Extended information",
+ "navigation_bar.favourites": "Favoritaj",
+ "navigation_bar.follow_requests": "Abonpetoj",
+ "navigation_bar.info": "Plia informo",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "Elsaluti",
- "navigation_bar.mutes": "Muted users",
- "navigation_bar.pins": "Pinned toots",
+ "navigation_bar.mutes": "Silentigitaj uzantoj",
+ "navigation_bar.pins": "Alpinglitaj pepoj",
"navigation_bar.preferences": "Preferoj",
"navigation_bar.public_timeline": "Fratara tempolinio",
"notification.favourite": "{name} favoris vian mesaĝon",
"notification.follow": "{name} sekvis vin",
"notification.mention": "{name} menciis vin",
"notification.reblog": "{name} diskonigis vian mesaĝon",
- "notifications.clear": "Clear notifications",
- "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
+ "notifications.clear": "Forviŝi la sciigojn",
+ "notifications.clear_confirmation": "Ĉu vi certe volas malaperigi ĉiujn viajn sciigojn?",
"notifications.column_settings.alert": "Retumilaj atentigoj",
- "notifications.column_settings.favourite": "Favoroj:",
+ "notifications.column_settings.favourite": "Favoritoj:",
"notifications.column_settings.follow": "Novaj sekvantoj:",
"notifications.column_settings.mention": "Mencioj:",
- "notifications.column_settings.push": "Push notifications",
- "notifications.column_settings.push_meta": "This device",
+ "notifications.column_settings.push": "Puŝsciigoj",
+ "notifications.column_settings.push_meta": "Tiu ĉi aparato",
"notifications.column_settings.reblog": "Diskonigoj:",
"notifications.column_settings.show": "Montri en kolono",
- "notifications.column_settings.sound": "Play sound",
- "onboarding.done": "Done",
- "onboarding.next": "Next",
- "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
- "onboarding.page_four.home": "The home timeline shows posts from people you follow.",
- "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
- "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
- "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}",
- "onboarding.page_one.welcome": "Welcome to Mastodon!",
- "onboarding.page_six.admin": "Your instance's admin is {admin}.",
- "onboarding.page_six.almost_done": "Almost done...",
- "onboarding.page_six.appetoot": "Bon Appetoot!",
- "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
- "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
- "onboarding.page_six.guidelines": "community guidelines",
- "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
- "onboarding.page_six.various_app": "mobile apps",
- "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
- "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
- "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
- "onboarding.skip": "Skip",
- "privacy.change": "Adjust status privacy",
- "privacy.direct.long": "Post to mentioned users only",
- "privacy.direct.short": "Direct",
- "privacy.private.long": "Post to followers only",
- "privacy.private.short": "Followers-only",
- "privacy.public.long": "Post to public timelines",
- "privacy.public.short": "Public",
- "privacy.unlisted.long": "Do not show in public timelines",
- "privacy.unlisted.short": "Unlisted",
- "relative_time.days": "{number}d",
+ "notifications.column_settings.sound": "Eligi sonon",
+ "onboarding.done": "Farita",
+ "onboarding.next": "Malantaŭa",
+ "onboarding.page_five.public_timelines": "La loka tempolinio enhavas mesaĝojn de ĉiuj ĉe {domain}. La federacia tempolinio enhavas ĉiujn mesaĝojn de uzantoj, kiujn iu ĉe {domain} sekvas. Ambaŭ tre utilas por trovi novajn kunparolantojn.",
+ "onboarding.page_four.home": "La hejma tempolinio enhavas la mesaĝojn de ĉiuj uzantoj, kiuj vi sekvas.",
+ "onboarding.page_four.notifications": "La sciiga kolumno informas vin kiam iu interagas kun vi.",
+ "onboarding.page_one.federation": "Mastodono estas reto de nedependaj serviloj, unuiĝintaj por krei pligrandan socian retejon. Ni nomas tiujn servilojn instancoj.",
+ "onboarding.page_one.handle": "Vi estas ĉe {domain}, unu el la multaj instancoj de Mastodono. Via kompleta uznomo do estas {handle}",
+ "onboarding.page_one.welcome": "Bonvenon al Mastodono!",
+ "onboarding.page_six.admin": "Via instancestro estas {admin}.",
+ "onboarding.page_six.almost_done": "Estas preskaŭ finita…",
+ "onboarding.page_six.appetoot": "Bonan a‘pepi’ton!",
+ "onboarding.page_six.apps_available": "{apps} estas elŝuteblaj por iOS, Androido kaj alioj. Kaj nun… bonan a‘pepi’ton!",
+ "onboarding.page_six.github": "Mastodono estas libera, senpaga kaj malfermkoda programaro. Vi povas signali cimojn, proponi funkciojn aŭ kontribui al gîa kreskado ĉe {github}.",
+ "onboarding.page_six.guidelines": "komunreguloj",
+ "onboarding.page_six.read_guidelines": "Ni petas vin: ne forgesu legi la {guidelines}n de {domain}!",
+ "onboarding.page_six.various_app": "telefon-aplikaĵoj",
+ "onboarding.page_three.profile": "Redaktu vian profilon por ŝanĝi vian avataron, priskribon kaj vian nomon. Vi tie trovos ankoraŭ aliajn agordojn.",
+ "onboarding.page_three.search": "Uzu la serĉokampo por trovi uzantojn kaj esplori kradvortojn tiel ke {illustration} kaj {introductions}. Por trovi iun, kiu ne estas ĉe ĉi tiu instanco, uzu ĝian kompletan uznomon.",
+ "onboarding.page_two.compose": "Skribu pepojn en la verkkolumno. Vi povas aldoni bildojn, ŝanĝi la agordojn de privateco kaj aldoni tiklavertojn (« content warning ») dank' al la piktogramoj malsupre.",
+ "onboarding.skip": "Pasigi",
+ "privacy.change": "Alĝustigi la privateco de la mesaĝo",
+ "privacy.direct.long": "Vidigi nur al la menciitaj personoj",
+ "privacy.direct.short": "Rekta",
+ "privacy.private.long": "Vidigi nur al viaj sekvantoj",
+ "privacy.private.short": "Nursekvanta",
+ "privacy.public.long": "Vidigi en publikaj tempolinioj",
+ "privacy.public.short": "Publika",
+ "privacy.unlisted.long": "Ne vidigi en publikaj tempolinioj",
+ "privacy.unlisted.short": "Nelistigita",
+ "relative_time.days": "{number}t",
"relative_time.hours": "{number}h",
- "relative_time.just_now": "now",
+ "relative_time.just_now": "nun",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
- "reply_indicator.cancel": "Rezigni",
- "report.placeholder": "Additional comments",
- "report.submit": "Submit",
- "report.target": "Reporting",
+ "reply_indicator.cancel": "Malfari",
+ "report.placeholder": "Pliaj komentoj",
+ "report.submit": "Sendi",
+ "report.target": "Signalaĵo",
"search.placeholder": "Serĉi",
- "search_popout.search_format": "Advanced search format",
- "search_popout.tips.hashtag": "hashtag",
- "search_popout.tips.status": "status",
- "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
- "search_popout.tips.user": "user",
- "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
- "standalone.public_title": "A look inside...",
- "status.cannot_reblog": "This post cannot be boosted",
+ "search_popout.search_format": "Detala serĉo",
+ "search_popout.tips.hashtag": "kradvorto",
+ "search_popout.tips.status": "statkonigo",
+ "search_popout.tips.text": "Simpla teksto eligas la kongruajn afiŝnomojn, uznomojn kaj kradvortojn.",
+ "search_popout.tips.user": "uzanto",
+ "search_results.total": "{count, number} {count, plural, one {rezultato} other {rezultatoj}}",
+ "standalone.public_title": "Rigardeti…",
+ "status.cannot_reblog": "Tiun publikaĵon oni ne povas diskonigi",
"status.delete": "Forigi",
- "status.embed": "Embed",
+ "status.embed": "Enmeti",
"status.favourite": "Favori",
- "status.load_more": "Load more",
- "status.media_hidden": "Media hidden",
+ "status.load_more": "Ŝargi plie",
+ "status.media_hidden": "Sonbildaĵo kaŝita",
"status.mention": "Mencii @{name}",
- "status.more": "More",
- "status.mute_conversation": "Mute conversation",
- "status.open": "Expand this status",
- "status.pin": "Pin on profile",
+ "status.more": "Pli",
+ "status.mute_conversation": "Silentigi konversacion",
+ "status.open": "Disfaldi statkonigon",
+ "status.pin": "Pingli al la profilo",
"status.reblog": "Diskonigi",
- "status.reblogged_by": "{name} diskonigita",
+ "status.reblogged_by": "{name} diskonigis",
"status.reply": "Respondi",
- "status.replyAll": "Reply to thread",
- "status.report": "Report @{name}",
+ "status.replyAll": "Respondi al la fadeno",
+ "status.report": "Signali @{name}",
"status.sensitive_toggle": "Alklaki por vidi",
"status.sensitive_warning": "Tikla enhavo",
- "status.share": "Share",
- "status.show_less": "Show less",
- "status.show_more": "Show more",
- "status.unmute_conversation": "Unmute conversation",
- "status.unpin": "Unpin from profile",
+ "status.share": "Diskonigi",
+ "status.show_less": "Refaldi",
+ "status.show_more": "Disfaldi",
+ "status.unmute_conversation": "Malsilentigi konversacion",
+ "status.unpin": "Depingli de profilo",
"tabs_bar.compose": "Ekskribi",
- "tabs_bar.federated_timeline": "Federated",
+ "tabs_bar.federated_timeline": "Federacia tempolinio",
"tabs_bar.home": "Hejmo",
- "tabs_bar.local_timeline": "Local",
+ "tabs_bar.local_timeline": "Loka tempolinio",
"tabs_bar.notifications": "Sciigoj",
- "upload_area.title": "Drag & drop to upload",
- "upload_button.label": "Aldoni enhavaĵon",
- "upload_form.description": "Describe for the visually impaired",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+ "upload_area.title": "Algliti por alŝuti",
+ "upload_button.label": "Aldoni sonbildaĵon",
+ "upload_form.description": "Priskribi por la misvidantaj",
"upload_form.undo": "Malfari",
- "upload_progress.label": "Uploading...",
- "video.close": "Close video",
- "video.exit_fullscreen": "Exit full screen",
- "video.expand": "Expand video",
- "video.fullscreen": "Full screen",
- "video.hide": "Hide video",
- "video.mute": "Mute sound",
- "video.pause": "Pause",
- "video.play": "Play",
- "video.unmute": "Unmute sound"
+ "upload_progress.label": "Alŝutanta…",
+ "video.close": "Fermi videon",
+ "video.exit_fullscreen": "Eliri el plenekrano",
+ "video.expand": "Vastigi videon",
+ "video.fullscreen": "Igi plenekrane",
+ "video.hide": "Kaŝi videon",
+ "video.mute": "Silentigi",
+ "video.pause": "Paŭzi",
+ "video.play": "Legi",
+ "video.unmute": "Malsilentigi"
}
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 6e8e94700..6122a79ab 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -7,17 +7,22 @@
"account.followers": "Seguidores",
"account.follows": "Sigue",
"account.follows_you": "Te sigue",
+ "account.hide_reblogs": "Ocultar retoots de @{name}",
"account.media": "Media",
"account.mention": "Mencionar a @{name}",
+ "account.moved_to": "{name} se ha mudado a:",
"account.mute": "Silenciar a @{name}",
+ "account.mute_notifications": "Silenciar notificaciones de @{name}",
"account.posts": "Publicaciones",
"account.report": "Reportar a @{name}",
"account.requested": "Esperando aprobación",
"account.share": "Compartir el perfil de @{name}",
+ "account.show_reblogs": "Mostrar retoots de @{name}",
"account.unblock": "Desbloquear a @{name}",
"account.unblock_domain": "Mostrar a {domain}",
"account.unfollow": "Dejar de seguir",
"account.unmute": "Dejar de silenciar a @{name}",
+ "account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}",
"account.view_full_profile": "Ver perfil completo",
"boost_modal.combo": "Puedes presionar {combo} para saltear este aviso la próxima vez",
"bundle_column_error.body": "Algo salió mal al cargar este componente.",
@@ -31,6 +36,7 @@
"column.favourites": "Favoritos",
"column.follow_requests": "Solicitudes de seguimiento",
"column.home": "Inicio",
+ "column.lists": "Lists",
"column.mutes": "Usuarios silenciados",
"column.notifications": "Notificaciones",
"column.pins": "Toot fijado",
@@ -57,6 +63,8 @@
"confirmations.block.message": "¿Estás seguro de que quieres bloquear a {name}?",
"confirmations.delete.confirm": "Eliminar",
"confirmations.delete.message": "¿Estás seguro de que quieres borrar este toot?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Ocultar dominio entero",
"confirmations.domain_block.message": "¿Seguro de que quieres bloquear al dominio entero? En algunos casos es preferible bloquear o silenciar objetivos determinados.",
"confirmations.mute.confirm": "Silenciar",
@@ -66,25 +74,26 @@
"embed.instructions": "Añade este toot a tu sitio web con el siguiente código.",
"embed.preview": "Así es como se verá:",
"emoji_button.activity": "Actividad",
- "emoji_button.custom": "Custom",
+ "emoji_button.custom": "Personalizado",
"emoji_button.flags": "Marcas",
"emoji_button.food": "Comida y bebida",
"emoji_button.label": "Insertar emoji",
"emoji_button.nature": "Naturaleza",
- "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+ "emoji_button.not_found": "No hay emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objetos",
"emoji_button.people": "Gente",
- "emoji_button.recent": "Frequently used",
+ "emoji_button.recent": "Usados frecuentemente",
"emoji_button.search": "Buscar…",
- "emoji_button.search_results": "Search results",
+ "emoji_button.search_results": "Resultados de búsqueda",
"emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viajes y lugares",
"empty_column.community": "La línea de tiempo local está vacía. ¡Escribe algo para empezar la fiesta!",
"empty_column.hashtag": "No hay nada en este hashtag aún.",
"empty_column.home": "No estás siguiendo a nadie aún. Visita {public} o haz búsquedas para empezar y conocer gente nueva.",
"empty_column.home.public_timeline": "la línea de tiempo pública",
+ "empty_column.list": "No hay nada en esta lista aún.",
"empty_column.notifications": "No tienes ninguna notificación aún. Interactúa con otros para empezar una conversación.",
- "empty_column.public": "¡No hay nada aquí! Escribe algo públicamente, o sigue usuarios de otras instancias manualmente para llenarlo.",
+ "empty_column.public": "¡No hay nada aquí! Escribe algo públicamente, o sigue usuarios de otras instancias manualmente para llenarlo",
"follow_request.authorize": "Autorizar",
"follow_request.reject": "Rechazar",
"getting_started.appsshort": "Aplicaciones",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "Mostrar retoots",
"home.column_settings.show_replies": "Mostrar respuestas",
"home.settings": "Ajustes de columna",
+ "keyboard_shortcuts.back": "volver atrás",
+ "keyboard_shortcuts.boost": "retootear",
+ "keyboard_shortcuts.column": "enfocar un estado en una de las columnas",
+ "keyboard_shortcuts.compose": "enfocar el área de texto de redacción",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "mover hacia abajo en la lista",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "añadir a favoritos",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Tecla caliente",
+ "keyboard_shortcuts.legend": "para mostrar esta leyenda",
+ "keyboard_shortcuts.mention": "para mencionar al autor",
+ "keyboard_shortcuts.reply": "para responder",
+ "keyboard_shortcuts.search": "para poner el foco en la búsqueda",
+ "keyboard_shortcuts.toot": "para comenzar un nuevo toot",
+ "keyboard_shortcuts.unfocus": "para retirar el foco de la caja de redacción/búsqueda",
+ "keyboard_shortcuts.up": "para ir hacia arriba en la lista",
"lightbox.close": "Cerrar",
"lightbox.next": "Siguiente",
"lightbox.previous": "Anterior",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "Cargando…",
"media_gallery.toggle_visible": "Cambiar visibilidad",
"missing_indicator.label": "No encontrado",
+ "mute_modal.hide_notifications": "Ocultar notificaciones de este usuario?",
"navigation_bar.blocks": "Usuarios bloqueados",
"navigation_bar.community_timeline": "Historia local",
"navigation_bar.edit_profile": "Editar perfil",
"navigation_bar.favourites": "Favoritos",
"navigation_bar.follow_requests": "Solicitudes para seguirte",
"navigation_bar.info": "Información adicional",
+ "navigation_bar.keyboard_shortcuts": "Atajos de teclado",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "Cerrar sesión",
"navigation_bar.mutes": "Usuarios silenciados",
"navigation_bar.pins": "Toots fijados",
@@ -125,8 +162,8 @@
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.follow": "Nuevos seguidores:",
"notifications.column_settings.mention": "Menciones:",
- "notifications.column_settings.push": "Notificaciones push:",
- "notifications.column_settings.push_meta": "Este dispositivo:",
+ "notifications.column_settings.push": "Notificaciones push",
+ "notifications.column_settings.push_meta": "Este dispositivo",
"notifications.column_settings.reblog": "Retoots:",
"notifications.column_settings.show": "Mostrar en columna",
"notifications.column_settings.sound": "Reproducir sonido",
@@ -169,11 +206,11 @@
"report.submit": "Publicar",
"report.target": "Reportando",
"search.placeholder": "Buscar",
- "search_popout.search_format": "Advanced search format",
- "search_popout.tips.hashtag": "hashtag",
+ "search_popout.search_format": "Formato de búsqueda avanzada",
+ "search_popout.tips.hashtag": "etiqueta",
"search_popout.tips.status": "status",
- "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
- "search_popout.tips.user": "user",
+ "search_popout.tips.text": "El texto simple devuelve correspondencias de nombre, usuario y hashtag",
+ "search_popout.tips.user": "usuario",
"search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
"standalone.public_title": "Un pequeño vistazo...",
"status.cannot_reblog": "Este toot no puede retootearse",
@@ -204,9 +241,10 @@
"tabs_bar.home": "Inicio",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notificaciones",
+ "ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.",
"upload_area.title": "Arrastra y suelta para subir",
"upload_button.label": "Subir multimedia",
- "upload_form.description": "Describe for the visually impaired",
+ "upload_form.description": "Describir para los usuarios con dificultad visual",
"upload_form.undo": "Deshacer",
"upload_progress.label": "Subiendo…",
"video.close": "Cerrar video",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 995d1b5ae..75057a7dd 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -7,17 +7,22 @@
"account.followers": "پیگیران",
"account.follows": "پی میگیرد",
"account.follows_you": "پیگیر شماست",
+ "account.hide_reblogs": "Hide boosts from @{name}",
"account.media": "رسانه",
"account.mention": "نامبردن از @{name}",
+ "account.moved_to": "{name} has moved to:",
"account.mute": "بیصدا کردن @{name}",
+ "account.mute_notifications": "Mute notifications from @{name}",
"account.posts": "نوشتهها",
"account.report": "گزارش @{name}",
"account.requested": "در انتظار پذیرش",
"account.share": "همرسانی نمایهٔ @{name}",
+ "account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "رفع انسداد @{name}",
"account.unblock_domain": "رفع پنهانسازی از {domain}",
"account.unfollow": "پایان پیگیری",
"account.unmute": "باصدا کردن @{name}",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "نمایش نمایهٔ کامل",
"boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید",
"bundle_column_error.body": "هنگام بازکردن این بخش خطایی رخ داد.",
@@ -31,6 +36,7 @@
"column.favourites": "پسندیدهها",
"column.follow_requests": "درخواستهای پیگیری",
"column.home": "خانه",
+ "column.lists": "Lists",
"column.mutes": "کاربران بیصداشده",
"column.notifications": "اعلانها",
"column.pins": "نوشتههای ثابت",
@@ -57,6 +63,8 @@
"confirmations.block.message": "آیا واقعاً میخواهید {name} را مسدود کنید؟",
"confirmations.delete.confirm": "پاک کن",
"confirmations.delete.message": "آیا واقعاً میخواهید این نوشته را پاک کنید؟",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "پنهانسازی کل دامین",
"confirmations.domain_block.message": "آیا جدی جدی میخواهید کل دامین {domain} را مسدود کنید؟ بیشتر وقتها مسدودکردن یا بیصداکردن چند حساب کاربری خاص کافی است و توصیه میشود.",
"confirmations.mute.confirm": "بیصدا کن",
@@ -83,6 +91,7 @@
"empty_column.hashtag": "هنوز هیچ چیزی با این هشتگ نیست.",
"empty_column.home": "شما هنوز پیگیر کسی نیستید. {public} را ببینید یا چیزی را جستجو کنید تا کاربران دیگر را ببینید.",
"empty_column.home.public_timeline": "فهرست نوشتههای همهجا",
+ "empty_column.list": "There is nothing in this list yet.",
"empty_column.notifications": "هنوز هیچ اعلانی ندارید. به نوشتههای دیگران واکنش نشان دهید تا گفتگو آغاز شود.",
"empty_column.public": "اینجا هنوز چیزی نیست! خودتان چیزی بنویسید یا کاربران دیگر را پی بگیرید تا اینجا پر شود",
"follow_request.authorize": "اجازه دهید",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "نمایش بازبوقها",
"home.column_settings.show_replies": "نمایش پاسخها",
"home.settings": "تنظیمات ستون",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "بستن",
"lightbox.next": "بعدی",
"lightbox.previous": "قبلی",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "بارگیری...",
"media_gallery.toggle_visible": "تغییر پیدایی",
"missing_indicator.label": "پیدا نشد",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.blocks": "کاربران مسدودشده",
"navigation_bar.community_timeline": "نوشتههای محلی",
"navigation_bar.edit_profile": "ویرایش نمایه",
"navigation_bar.favourites": "پسندیدهها",
"navigation_bar.follow_requests": "درخواستهای پیگیری",
"navigation_bar.info": "اطلاعات تکمیلی",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "خروج",
"navigation_bar.mutes": "کاربران بیصداشده",
"navigation_bar.pins": "نوشتههای ثابت",
@@ -204,6 +241,7 @@
"tabs_bar.home": "خانه",
"tabs_bar.local_timeline": "محلی",
"tabs_bar.notifications": "اعلانها",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "برای بارگذاری به اینجا بکشید",
"upload_button.label": "افزودن تصویر",
"upload_form.description": "نوشتهٔ توضیحی برای کمبینایان و نابینایان",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index af08be5d1..4ddc1cca7 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -7,17 +7,22 @@
"account.followers": "Seuraajia",
"account.follows": "Seuraa",
"account.follows_you": "Seuraa sinua",
+ "account.hide_reblogs": "Hide boosts from @{name}",
"account.media": "Media",
"account.mention": "Mainitse @{name}",
+ "account.moved_to": "{name} has moved to:",
"account.mute": "Mute @{name}",
+ "account.mute_notifications": "Mute notifications from @{name}",
"account.posts": "Postit",
"account.report": "Report @{name}",
"account.requested": "Odottaa hyväksyntää",
"account.share": "Share @{name}'s profile",
+ "account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "Salli @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Lopeta seuraaminen",
"account.unmute": "Unmute @{name}",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "View full profile",
"boost_modal.combo": "You can press {combo} to skip this next time",
"bundle_column_error.body": "Something went wrong while loading this component.",
@@ -31,6 +36,7 @@
"column.favourites": "Favourites",
"column.follow_requests": "Follow requests",
"column.home": "Koti",
+ "column.lists": "Lists",
"column.mutes": "Muted users",
"column.notifications": "Ilmoitukset",
"column.pins": "Pinned toot",
@@ -57,6 +63,8 @@
"confirmations.block.message": "Are you sure you want to block {name}?",
"confirmations.delete.confirm": "Delete",
"confirmations.delete.message": "Are you sure you want to delete this status?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Mute",
@@ -83,6 +91,7 @@
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
"empty_column.home.public_timeline": "the public timeline",
+ "empty_column.list": "There is nothing in this list yet.",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
"follow_request.authorize": "Authorize",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Sulje",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "Ladataan...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.blocks": "Blocked users",
"navigation_bar.community_timeline": "Paikallinen aikajana",
"navigation_bar.edit_profile": "Muokkaa profiilia",
"navigation_bar.favourites": "Favourites",
"navigation_bar.follow_requests": "Follow requests",
"navigation_bar.info": "Extended information",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "Kirjaudu ulos",
"navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
@@ -204,6 +241,7 @@
"tabs_bar.home": "Koti",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Ilmoitukset",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Drag & drop to upload",
"upload_button.label": "Lisää mediaa",
"upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 219bf4da1..ecfff87c8 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -7,17 +7,22 @@
"account.followers": "Abonné⋅e⋅s",
"account.follows": "Abonnements",
"account.follows_you": "Vous suit",
+ "account.hide_reblogs": "Masquer les partages de @{name}",
"account.media": "Média",
"account.mention": "Mentionner",
+ "account.moved_to": "{name} a déménagé vers :",
"account.mute": "Masquer",
+ "account.mute_notifications": "Ignorer les notifications de @{name}",
"account.posts": "Statuts",
"account.report": "Signaler",
"account.requested": "Invitation envoyée",
"account.share": "Partager le profil de @{name}",
+ "account.show_reblogs": "Afficher les partages de @{name}",
"account.unblock": "Débloquer",
"account.unblock_domain": "Ne plus masquer {domain}",
"account.unfollow": "Ne plus suivre",
"account.unmute": "Ne plus masquer",
+ "account.unmute_notifications": "Réactiver les notifications de @{name}",
"account.view_full_profile": "Afficher le profil complet",
"boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois",
"bundle_column_error.body": "Une erreur s’est produite lors du chargement de ce composant.",
@@ -31,6 +36,7 @@
"column.favourites": "Favoris",
"column.follow_requests": "Demandes de suivi",
"column.home": "Accueil",
+ "column.lists": "Lists",
"column.mutes": "Comptes masqués",
"column.notifications": "Notifications",
"column.pins": "Pouets épinglés",
@@ -47,7 +53,7 @@
"compose_form.lock_disclaimer": "Votre compte n’est pas {locked}. Tout le monde peut vous suivre et voir vos pouets privés.",
"compose_form.lock_disclaimer.lock": "verrouillé",
"compose_form.placeholder": "Qu’avez-vous en tête ?",
- "compose_form.publish": "Pouet ",
+ "compose_form.publish": "Pouet",
"compose_form.publish_loud": "{publish} !",
"compose_form.sensitive": "Marquer le média comme sensible",
"compose_form.spoiler": "Masquer le texte derrière un avertissement",
@@ -57,6 +63,8 @@
"confirmations.block.message": "Confirmez-vous le blocage de {name} ?",
"confirmations.delete.confirm": "Supprimer",
"confirmations.delete.message": "Confirmez-vous la suppression de ce pouet ?",
+ "confirmations.delete_list.confirm": "Supprimer",
+ "confirmations.delete_list.message": "Êtes-vous sûr de vouloir supprimer définitivement cette liste ?",
"confirmations.domain_block.confirm": "Masquer le domaine entier",
"confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier ? Dans la plupart des cas, quelques blocages ou masquages ciblés sont suffisants et préférables.",
"confirmations.mute.confirm": "Masquer",
@@ -64,14 +72,14 @@
"confirmations.unfollow.confirm": "Ne plus suivre",
"confirmations.unfollow.message": "Voulez-vous arrêter de suivre {name} ?",
"embed.instructions": "Intégrez ce statut à votre site en copiant le code ci-dessous.",
- "embed.preview": "Il apparaîtra comme cela : ",
+ "embed.preview": "Il apparaîtra comme cela :",
"emoji_button.activity": "Activités",
"emoji_button.custom": "Personnalisés",
"emoji_button.flags": "Drapeaux",
"emoji_button.food": "Boire et manger",
"emoji_button.label": "Insérer un émoji",
"emoji_button.nature": "Nature",
- "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+ "emoji_button.not_found": "Pas d'emojis !! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objets",
"emoji_button.people": "Personnages",
"emoji_button.recent": "Fréquemment utilisés",
@@ -83,6 +91,7 @@
"empty_column.hashtag": "Il n’y a encore aucun contenu associé à ce hashtag",
"empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres utilisateur⋅ice⋅s.",
"empty_column.home.public_timeline": "le fil public",
+ "empty_column.list": "Il n'y a rien dans cette liste pour l'instant. Dès que des personnes de cette liste publierons de nouveaux statuts, ils apparaîtront ici.",
"empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateur⋅ice⋅s pour débuter la conversation.",
"empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateur⋅ice⋅s d’autres instances pour remplir le fil public.",
"follow_request.authorize": "Accepter",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "Afficher les partages",
"home.column_settings.show_replies": "Afficher les réponses",
"home.settings": "Paramètres de la colonne",
+ "keyboard_shortcuts.back": "revenir en arrière",
+ "keyboard_shortcuts.boost": "partager",
+ "keyboard_shortcuts.column": "focaliser un statut dans l'une des colonnes",
+ "keyboard_shortcuts.compose": "pour centrer la zone de redaction",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "descendre dans la liste",
+ "keyboard_shortcuts.enter": "pour ouvrir le statut",
+ "keyboard_shortcuts.favourite": "vers les favoris",
+ "keyboard_shortcuts.heading": "Raccourcis clavier",
+ "keyboard_shortcuts.hotkey": "Raccourci",
+ "keyboard_shortcuts.legend": "pour afficher cette légende",
+ "keyboard_shortcuts.mention": "pour mentionner l'auteur",
+ "keyboard_shortcuts.reply": "pour répondre",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "pour démarrer un tout nouveau pouet",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Fermer",
"lightbox.next": "Suivant",
"lightbox.previous": "Précédent",
+ "lists.account.add": "Ajouter à la liste",
+ "lists.account.remove": "Supprimer de la liste",
+ "lists.delete": "Effacer la liste",
+ "lists.edit": "Éditer la liste",
+ "lists.new.create": "Ajouter une liste",
+ "lists.new.title_placeholder": "Titre de la nouvelle liste",
+ "lists.search": "Rechercher parmi les gens que vous suivez",
+ "lists.subheading": "Vos listes",
"loading_indicator.label": "Chargement…",
"media_gallery.toggle_visible": "Modifier la visibilité",
"missing_indicator.label": "Non trouvé",
+ "mute_modal.hide_notifications": "Masquer les notifications de cet utilisateur ?",
"navigation_bar.blocks": "Comptes bloqués",
"navigation_bar.community_timeline": "Fil public local",
"navigation_bar.edit_profile": "Modifier le profil",
"navigation_bar.favourites": "Favoris",
"navigation_bar.follow_requests": "Demandes de suivi",
"navigation_bar.info": "Plus d’informations",
+ "navigation_bar.keyboard_shortcuts": "Raccourcis clavier",
+ "navigation_bar.lists": "Listes",
"navigation_bar.logout": "Déconnexion",
"navigation_bar.mutes": "Comptes masqués",
"navigation_bar.pins": "Pouets épinglés",
@@ -122,7 +159,7 @@
"notifications.clear": "Nettoyer",
"notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?",
"notifications.column_settings.alert": "Notifications locales",
- "notifications.column_settings.favourite": "Favoris :",
+ "notifications.column_settings.favourite": "Favoris :",
"notifications.column_settings.follow": "Nouveaux⋅elles abonné⋅e⋅s :",
"notifications.column_settings.mention": "Mentions :",
"notifications.column_settings.push": "Notifications push",
@@ -204,6 +241,7 @@
"tabs_bar.home": "Accueil",
"tabs_bar.local_timeline": "Fil public local",
"tabs_bar.notifications": "Notifications",
+ "ui.beforeunload": "Votre brouillon sera perdu si vous quittez Mastodon.",
"upload_area.title": "Glissez et déposez pour envoyer",
"upload_button.label": "Joindre un média",
"upload_form.description": "Décrire pour les malvoyants",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
new file mode 100644
index 000000000..6398daa11
--- /dev/null
+++ b/app/javascript/mastodon/locales/gl.json
@@ -0,0 +1,259 @@
+{
+ "account.block": "Bloquear @{name}",
+ "account.block_domain": "Ocultar calquer contido de {domain}",
+ "account.disclaimer_full": "A información inferior podería mostrar un perfil incompleto da usuaria.",
+ "account.edit_profile": "Editar perfil",
+ "account.follow": "Seguir",
+ "account.followers": "Seguidoras",
+ "account.follows": "Seguindo",
+ "account.follows_you": "Séguena",
+ "account.hide_reblogs": "Ocultar repeticións de @{name}",
+ "account.media": "Medios",
+ "account.mention": "Mencionar @{name}",
+ "account.moved_to": "{name} marchou a:",
+ "account.mute": "Acalar @{name}",
+ "account.mute_notifications": "Acalar as notificacións de @{name}",
+ "account.posts": "Publicacións",
+ "account.report": "Informar sobre @{name}",
+ "account.requested": "Agardando aceptación. Pulse para cancelar a solicitude de seguimento",
+ "account.share": "Compartir o perfil de @{name}",
+ "account.show_reblogs": "Mostrar repeticións de @{name}",
+ "account.unblock": "Desbloquear @{name}",
+ "account.unblock_domain": "Non ocultar {domain}",
+ "account.unfollow": "Non seguir",
+ "account.unmute": "Non acalar @{name}",
+ "account.unmute_notifications": "Desbloquear as notificacións de @{name}",
+ "account.view_full_profile": "Ver o perfil completo",
+ "boost_modal.combo": "Pulse {combo} para saltar esto a próxima vez",
+ "bundle_column_error.body": "Houbo un fallo mentras se cargaba este compoñente.",
+ "bundle_column_error.retry": "Inténteo de novo",
+ "bundle_column_error.title": "Fallo na rede",
+ "bundle_modal_error.close": "Pechar",
+ "bundle_modal_error.message": "Algo fallou mentras se cargaba este compoñente.",
+ "bundle_modal_error.retry": "Inténteo de novo",
+ "column.blocks": "Usuarias bloqueadas",
+ "column.community": "Liña temporal local",
+ "column.favourites": "Favoritas",
+ "column.follow_requests": "Peticións de seguimento",
+ "column.home": "Inicio",
+ "column.lists": "Listas",
+ "column.mutes": "Usuarias acaladas",
+ "column.notifications": "Notificacións",
+ "column.pins": "Mensaxes fixadas",
+ "column.public": "Liña temporal federada",
+ "column_back_button.label": "Atrás",
+ "column_header.hide_settings": "Agochar axustes",
+ "column_header.moveLeft_settings": "Mover a columna hacia a esquerda",
+ "column_header.moveRight_settings": "Mover a columna hacia a dereita",
+ "column_header.pin": "Fixar",
+ "column_header.show_settings": "Mostras axustes",
+ "column_header.unpin": "Soltar",
+ "column_subheading.navigation": "Navegación",
+ "column_subheading.settings": "Axustes",
+ "compose_form.lock_disclaimer": "A súa conta non está {locked}. Calquera pode seguila para ver as súas mensaxes só-para-seguidoras.",
+ "compose_form.lock_disclaimer.lock": "bloqueado",
+ "compose_form.placeholder": "A qué andas?",
+ "compose_form.publish": "Toot",
+ "compose_form.publish_loud": "{publish}!",
+ "compose_form.sensitive": "Marcar medios como sensibles",
+ "compose_form.spoiler": "Agochar texto detrás de un aviso",
+ "compose_form.spoiler_placeholder": "Escriba o aviso aquí",
+ "confirmation_modal.cancel": "Cancelar",
+ "confirmations.block.confirm": "Bloquear",
+ "confirmations.block.message": "Está segura de querer bloquear a {name}?",
+ "confirmations.delete.confirm": "Borrar",
+ "confirmations.delete.message": "Está segura de que quere eliminar este estado?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Estás seguro de que queres eliminar permanentemente esta lista?",
+ "confirmations.domain_block.confirm": "Agochar un dominio completo",
+ "confirmations.domain_block.message": "Realmente está segura de que quere bloquear por completo o dominio {domain}? Normalmente é suficiente, e preferible, bloquear de xeito selectivo varios elementos.",
+ "confirmations.mute.confirm": "Acalar",
+ "confirmations.mute.message": "Está segura de que quere acalar a {name}?",
+ "confirmations.unfollow.confirm": "Deixar de seguir",
+ "confirmations.unfollow.message": "Quere deixar de seguir a {name}?",
+ "embed.instructions": "Copie o código inferior para incrustar no seu sitio web este estado.",
+ "embed.preview": "Así será mostrado:",
+ "emoji_button.activity": "Actividade",
+ "emoji_button.custom": "Personalizado",
+ "emoji_button.flags": "Marcas",
+ "emoji_button.food": "Comida e Bebida",
+ "emoji_button.label": "Insertar emoji",
+ "emoji_button.nature": "Natureza",
+ "emoji_button.not_found": "Sen emojos!! (╯°□°)╯︵ ┻━┻",
+ "emoji_button.objects": "Obxetos",
+ "emoji_button.people": "Xente",
+ "emoji_button.recent": "Utilizadas con frecuencia",
+ "emoji_button.search": "Buscar...",
+ "emoji_button.search_results": "Resultados da busca",
+ "emoji_button.symbols": "Símbolos",
+ "emoji_button.travel": "Viaxes e Lugares",
+ "empty_column.community": "A liña temporal local está baldeira. Escriba algo de xeito público para que rule!",
+ "empty_column.hashtag": "Aínda non hai nada con esta etiqueta.",
+ "empty_column.home": "A súa liña temporal de inicio está baldeira! Visite {public} ou utilice a busca para atopar outras usuarias.",
+ "empty_column.home.public_timeline": "a liña temporal pública",
+ "empty_column.list": "Aínda non hai nada en esta lista.",
+ "empty_column.notifications": "Aínda non ten notificacións. Interactúe con outras para iniciar unha conversa.",
+ "empty_column.public": "Nada por aquí! Escriba algo de xeito público, ou siga manualmente usuarias de outras instancias para ir enchéndoa",
+ "follow_request.authorize": "Autorizar",
+ "follow_request.reject": "Rexeitar",
+ "getting_started.appsshort": "Aplicacións",
+ "getting_started.faq": "PMF",
+ "getting_started.heading": "Comezando",
+ "getting_started.open_source_notice": "Mastodon é software de código aberto. Pode contribuír ou informar de fallos en GitHub en {github}.",
+ "getting_started.userguide": "Guía de usuaria",
+ "home.column_settings.advanced": "Avanzado",
+ "home.column_settings.basic": "Básico",
+ "home.column_settings.filter_regex": "Filtrar expresións regulares",
+ "home.column_settings.show_reblogs": "Mostrar repeticións",
+ "home.column_settings.show_replies": "Mostrar respostas",
+ "home.settings": "Axustes da columna",
+ "keyboard_shortcuts.back": "voltar atrás",
+ "keyboard_shortcuts.boost": "repetir",
+ "keyboard_shortcuts.column": "destacar un estado en unha das columnas",
+ "keyboard_shortcuts.compose": "Foco no área de escritura",
+ "keyboard_shortcuts.description": "Descrición",
+ "keyboard_shortcuts.down": "ir hacia abaixo na lista",
+ "keyboard_shortcuts.enter": "abrir estado",
+ "keyboard_shortcuts.favourite": "marcar como favorito",
+ "keyboard_shortcuts.heading": "Atallos do teclado",
+ "keyboard_shortcuts.hotkey": "Tecla de acceso directo",
+ "keyboard_shortcuts.legend": "para mostrar esta lenda",
+ "keyboard_shortcuts.mention": "para mencionar o autor",
+ "keyboard_shortcuts.reply": "para responder",
+ "keyboard_shortcuts.search": "para centrar a busca",
+ "keyboard_shortcuts.toot": "escribir un toot novo",
+ "keyboard_shortcuts.unfocus": "quitar o foco do área de escritura/busca",
+ "keyboard_shortcuts.up": "ir hacia arriba na lista",
+ "lightbox.close": "Fechar",
+ "lightbox.next": "Seguinte",
+ "lightbox.previous": "Anterior",
+ "lists.account.add": "Engadir á lista",
+ "lists.account.remove": "Eliminar da lista",
+ "lists.delete": "Delete list",
+ "lists.edit": "Editar lista",
+ "lists.new.create": "Engadir lista",
+ "lists.new.title_placeholder": "Novo título da lista",
+ "lists.search": "Procurar entre a xente que segues",
+ "lists.subheading": "As túas listas",
+ "loading_indicator.label": "Cargando...",
+ "media_gallery.toggle_visible": "Dar visibilidade",
+ "missing_indicator.label": "Non atopado",
+ "mute_modal.hide_notifications": "Esconder notificacións deste usuario?",
+ "navigation_bar.blocks": "Usuarios bloqueados",
+ "navigation_bar.community_timeline": "Liña temporal local",
+ "navigation_bar.edit_profile": "Editar perfil",
+ "navigation_bar.favourites": "Favoritas",
+ "navigation_bar.follow_requests": "Peticións de seguimento",
+ "navigation_bar.info": "Sobre esta instancia",
+ "navigation_bar.keyboard_shortcuts": "Atallos do teclado",
+ "navigation_bar.lists": "Listas",
+ "navigation_bar.logout": "Sair",
+ "navigation_bar.mutes": "Usuarias acaladas",
+ "navigation_bar.pins": "Mensaxes fixadas",
+ "navigation_bar.preferences": "Preferencias",
+ "navigation_bar.public_timeline": "Liña temporal federada",
+ "notification.favourite": "{name} marcou como favorito o seu estado",
+ "notification.follow": "{name} está a seguila",
+ "notification.mention": "{name} mencionoute",
+ "notification.reblog": "{name} promocionou o seu estado",
+ "notifications.clear": "Limpar notificacións",
+ "notifications.clear_confirmation": "Estás seguro de que queres limpar permanentemente todas as túas notificacións?",
+ "notifications.column_settings.alert": "Notificacións de escritorio",
+ "notifications.column_settings.favourite": "Favoritas:",
+ "notifications.column_settings.follow": "Novos seguidores:",
+ "notifications.column_settings.mention": "Mencións:",
+ "notifications.column_settings.push": "Enviar notificacións",
+ "notifications.column_settings.push_meta": "Este aparello",
+ "notifications.column_settings.reblog": "Promocións:",
+ "notifications.column_settings.show": "Mostrar en columna",
+ "notifications.column_settings.sound": "Reproducir son",
+ "onboarding.done": "Feito",
+ "onboarding.next": "Seguinte",
+ "onboarding.page_five.public_timelines": "A liña de tempo local mostra as publicacións públicas de todos en {domain}. A liña de tempo federada mostra as publicacións públicas de todos os que as persoas en {domain} seguen. Estas son as Liñas de tempo públicas, unha boa forma de descubrir novas persoas.",
+ "onboarding.page_four.home": "A liña de tempo local mostra as publicacións das persoas que segues.",
+ "onboarding.page_four.notifications": "A columna de notificacións mostra cando alguén interactúa contigo.",
+ "onboarding.page_one.federation": "Mastodon é unha rede de servidores independentes que se unen para facer unha rede social máis grande. Chamamos instancias a estes servidores.",
+ "onboarding.page_one.handle": "Estás en {domain}, polo que o teu nome de usuario completo é {handle}",
+ "onboarding.page_one.welcome": "Benvido a Mastodon!",
+ "onboarding.page_six.admin": "O administrador da túa instancia é {admin}.",
+ "onboarding.page_six.almost_done": "Case feito...",
+ "onboarding.page_six.appetoot": "Que tootes ben!",
+ "onboarding.page_six.apps_available": "Hai {apps} dispoñíbeis para iOS, Android e outras plataformas.",
+ "onboarding.page_six.github": "Mastodon é un software gratuito e de código aberto. Pode informar de erros, solicitar novas funcionalidades ou contribuír ao código en {github}.",
+ "onboarding.page_six.guidelines": "directrices da comunidade",
+ "onboarding.page_six.read_guidelines": "Por favor, le as {guidelines} do {domain}!",
+ "onboarding.page_six.various_app": "aplicacións móbiles",
+ "onboarding.page_three.profile": "Edita o teu perfil para cambiar o teu avatar, bio e nome. Alí, tamén atoparás outras preferencias.",
+ "onboarding.page_three.search": "Utilice a barra de busca para atopar xente e descubrir etiquetas, como {illustration} e {introductions}. Para atopar unha usuaria que non está en esta instancia utilice o seu enderezo completo.",
+ "onboarding.page_two.compose": "Escriba mensaxes desde a columna de composición. Pode subir imaxes, mudar as opcións de intimidade e engadir avisos sobre o contido coas iconas inferiores.",
+ "onboarding.skip": "Saltar",
+ "privacy.change": "Axustar a intimidade do estado",
+ "privacy.direct.long": "Enviar exclusivamente as usuarias mencionadas",
+ "privacy.direct.short": "Directa",
+ "privacy.private.long": "Enviar só as seguidoras",
+ "privacy.private.short": "Só-seguidoras",
+ "privacy.public.long": "Publicar na liña temporal pública",
+ "privacy.public.short": "Pública",
+ "privacy.unlisted.long": "Non publicar en liñas temporais públicas",
+ "privacy.unlisted.short": "Non listada",
+ "relative_time.days": "{number}d",
+ "relative_time.hours": "{number}h",
+ "relative_time.just_now": "agora",
+ "relative_time.minutes": "{number}m",
+ "relative_time.seconds": "{number}s",
+ "reply_indicator.cancel": "Cancelar",
+ "report.placeholder": "Comentarios adicionais",
+ "report.submit": "Enviar",
+ "report.target": "Informar {target}",
+ "search.placeholder": "Buscar",
+ "search_popout.search_format": "Formato de busca avanzada",
+ "search_popout.tips.hashtag": "etiqueta",
+ "search_popout.tips.status": "estado",
+ "search_popout.tips.text": "Texto simple devolve coincidencias con nomes públicos, nomes de usuaria e etiquetas",
+ "search_popout.tips.user": "usuaria",
+ "search_results.total": "{count, number} {count,plural,one {result} outros {results}}",
+ "standalone.public_title": "Ollada dentro...",
+ "status.cannot_reblog": "Esta mensaxe non pode ser promocionada",
+ "status.delete": "Eliminar",
+ "status.embed": "Incrustar",
+ "status.favourite": "Favorita",
+ "status.load_more": "Cargar máis",
+ "status.media_hidden": "Medios ocultos",
+ "status.mention": "Mencionar @{name}",
+ "status.more": "Máis",
+ "status.mute_conversation": "Acalar conversa",
+ "status.open": "Expandir este estado",
+ "status.pin": "Fixar no perfil",
+ "status.reblog": "Promocionar",
+ "status.reblogged_by": "{name} promocionado",
+ "status.reply": "Resposta",
+ "status.replyAll": "Resposta a conversa",
+ "status.report": "Informar @{name}",
+ "status.sensitive_toggle": "Pulse para ver",
+ "status.sensitive_warning": "Contido sensible",
+ "status.share": "Compartir",
+ "status.show_less": "Mostrar menos",
+ "status.show_more": "Mostrar máis",
+ "status.unmute_conversation": "Non acalar a conversa",
+ "status.unpin": "Despegar do perfil",
+ "tabs_bar.compose": "Compoñer",
+ "tabs_bar.federated_timeline": "Federado",
+ "tabs_bar.home": "Inicio",
+ "tabs_bar.local_timeline": "Local",
+ "tabs_bar.notifications": "Notificacións",
+ "ui.beforeunload": "O borrador perderase se sae de Mastodon.",
+ "upload_area.title": "Arrastre e solte para subir",
+ "upload_button.label": "Engadir medios",
+ "upload_form.description": "Describa para deficientes visuais",
+ "upload_form.undo": "Desfacer",
+ "upload_progress.label": "Subindo...",
+ "video.close": "Pechar video",
+ "video.exit_fullscreen": "Saír da pantalla completa",
+ "video.expand": "Expandir vídeo",
+ "video.fullscreen": "Pantalla completa",
+ "video.hide": "Agochar vídeo",
+ "video.mute": "Acalar son",
+ "video.pause": "Pausar",
+ "video.play": "Reproducir",
+ "video.unmute": "Permitir son"
+}
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index a260f0968..5444c8e34 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -1,47 +1,53 @@
{
"account.block": "חסימת @{name}",
"account.block_domain": "להסתיר הכל מהקהילה {domain}",
- "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+ "account.disclaimer_full": "המידע להלן עשוי להיות לא עדכני או לא שלם.",
"account.edit_profile": "עריכת פרופיל",
"account.follow": "מעקב",
"account.followers": "עוקבים",
"account.follows": "נעקבים",
"account.follows_you": "במעקב אחריך",
+ "account.hide_reblogs": "להסתיר הידהודים מאת @{name}",
"account.media": "מדיה",
"account.mention": "אזכור של @{name}",
+ "account.moved_to": "החשבון {name} הועבר אל:",
"account.mute": "להשתיק את @{name}",
+ "account.mute_notifications": "להסתיר התראות מאת @{name}",
"account.posts": "הודעות",
"account.report": "לדווח על @{name}",
"account.requested": "בהמתנה לאישור",
- "account.share": "Share @{name}'s profile",
+ "account.share": "לשתף את אודות @{name}",
+ "account.show_reblogs": "להראות הדהודים מאת @{name}",
"account.unblock": "הסרת חסימה מעל @{name}",
"account.unblock_domain": "הסר חסימה מקהילת {domain}",
"account.unfollow": "הפסקת מעקב",
"account.unmute": "הפסקת השתקת @{name}",
- "account.view_full_profile": "View full profile",
+ "account.unmute_notifications": "להפסיק הסתרת הודעות מעם @{name}",
+ "account.view_full_profile": "הראה אודות מלאות",
"boost_modal.combo": "ניתן להקיש {combo} כדי לדלג בפעם הבאה",
- "bundle_column_error.body": "Something went wrong while loading this component.",
- "bundle_column_error.retry": "Try again",
+ "bundle_column_error.body": "משהו השתבש בעת הצגת הרכיב הזה.",
+ "bundle_column_error.retry": "לנסות שוב",
"bundle_column_error.title": "Network error",
- "bundle_modal_error.close": "Close",
- "bundle_modal_error.message": "Something went wrong while loading this component.",
- "bundle_modal_error.retry": "Try again",
+ "bundle_modal_error.close": "לסגור",
+ "bundle_modal_error.message": "משהו השתבש בעת טעינת הרכיב הזה.",
+ "bundle_modal_error.retry": "לנסות שוב",
"column.blocks": "חסימות",
"column.community": "ציר זמן מקומי",
"column.favourites": "חיבובים",
"column.follow_requests": "בקשות מעקב",
"column.home": "בבית",
+ "column.lists": "Lists",
"column.mutes": "השתקות",
"column.notifications": "התראות",
"column.pins": "Pinned toot",
"column.public": "בפרהסיה",
"column_back_button.label": "חזרה",
- "column_header.hide_settings": "Hide settings",
- "column_header.moveLeft_settings": "Move column to the left",
- "column_header.moveRight_settings": "Move column to the right",
- "column_header.pin": "Pin",
- "column_header.show_settings": "Show settings",
- "column_header.unpin": "Unpin",
+ "column_header.hide_settings": "הסתרת העדפות",
+ "column_header.moveLeft_settings": "הזחת טור לשמאל",
+ "column_header.moveRight_settings": "הזחת טור לימין",
+ "column_header.pin": "קיבוע",
+ "column_header.show_settings": "הצגת העדפות",
+ "column_header.unpin": "שחרור קיבוע",
"column_subheading.navigation": "ניווט",
"column_subheading.settings": "אפשרויות",
"compose_form.lock_disclaimer": "חשבונך אינו {locked}. כל אחד יוכל לעקוב אחריך כדי לקרוא את הודעותיך המיועדות לעוקבים בלבד.",
@@ -57,34 +63,37 @@
"confirmations.block.message": "לחסום את {name}?",
"confirmations.delete.confirm": "למחוק",
"confirmations.delete.message": "למחוק את ההודעה?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "הסתר קהילה שלמה",
"confirmations.domain_block.message": "באמת באמת לחסום את כל קהילת {domain}? ברב המקרים השתקות נבחרות של מספר משתמשים מסויימים צריכה להספיק.",
"confirmations.mute.confirm": "להשתיק",
"confirmations.mute.message": "להשתיק את {name}?",
- "confirmations.unfollow.confirm": "Unfollow",
- "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
- "embed.instructions": "Embed this status on your website by copying the code below.",
- "embed.preview": "Here is what it will look like:",
+ "confirmations.unfollow.confirm": "להפסיק מעקב",
+ "confirmations.unfollow.message": "להפסיק מעקב אחרי {name}?",
+ "embed.instructions": "ניתן להטמיע את ההודעה באתרך ע\"י העתקת הקוד שלהלן.",
+ "embed.preview": "דוגמא כיצד זה יראה:",
"emoji_button.activity": "פעילות",
- "emoji_button.custom": "Custom",
+ "emoji_button.custom": "מיוחדים",
"emoji_button.flags": "דגלים",
"emoji_button.food": "אוכל ושתיה",
"emoji_button.label": "הוספת אמוג'י",
"emoji_button.nature": "טבע",
- "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+ "emoji_button.not_found": "רגישון לא נמצא!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "חפצים",
"emoji_button.people": "אנשים",
- "emoji_button.recent": "Frequently used",
+ "emoji_button.recent": "בשימוש תדיר",
"emoji_button.search": "חיפוש...",
- "emoji_button.search_results": "Search results",
+ "emoji_button.search_results": "תוצאות חיפוש",
"emoji_button.symbols": "סמלים",
"emoji_button.travel": "טיולים ואתרים",
"empty_column.community": "טור הסביבה ריק. יש לפרסם משהו כדי שדברים יתרחילו להתגלגל!",
"empty_column.hashtag": "אין כלום בהאשתג הזה עדיין.",
"empty_column.home": "אף אחד לא במעקב עדיין. אפשר לבקר ב{public} או להשתמש בחיפוש כדי להתחיל ולהכיר חצוצרנים אחרים.",
"empty_column.home.public_timeline": "ציר זמן בין-קהילתי",
- "empty_column.notifications": "אין התראות עדיין. יאללה, הגיע הזמן להתחיל להתערבב!",
- "empty_column.public": "אין פה כלום! כדי למלא את הטור הזה אפשר לכתוב משהו, או להתחיל לעקוב אחרי אנשים מקהילות אחרות.",
+ "empty_column.list": "אין עדיין מאום ברשימה.",
+ "empty_column.notifications": "אין התראות עדיין. יאללה, הגיע הזמן להתחיל להתערבב.",
+ "empty_column.public": "אין פה כלום! כדי למלא את הטור הזה אפשר לכתוב משהו, או להתחיל לעקוב אחרי אנשים מקהילות אחרות",
"follow_request.authorize": "קבלה",
"follow_request.reject": "דחיה",
"getting_started.appsshort": "יישומונים לניידים",
@@ -98,21 +107,49 @@
"home.column_settings.show_reblogs": "הצגת הדהודים",
"home.column_settings.show_replies": "הצגת תגובות",
"home.settings": "הגדרות טור",
+ "keyboard_shortcuts.back": "ניווט חזרה",
+ "keyboard_shortcuts.boost": "להדהד",
+ "keyboard_shortcuts.column": "להתמקד בהודעה באחד מהטורים",
+ "keyboard_shortcuts.compose": "להתמקד בתיבת חיבור ההודעות",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "לנוע במורד הרשימה",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "לחבב",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "מקש קיצור",
+ "keyboard_shortcuts.legend": "להציג את הפירוש",
+ "keyboard_shortcuts.mention": "לאזכר את המחבר(ת)",
+ "keyboard_shortcuts.reply": "לענות",
+ "keyboard_shortcuts.search": "להתמקד בחלון החיפוש",
+ "keyboard_shortcuts.toot": "להתחיל חיצרוץ חדש",
+ "keyboard_shortcuts.unfocus": "לצאת מתיבת חיבור/חיפוש",
+ "keyboard_shortcuts.up": "לנוע במעלה הרשימה",
"lightbox.close": "סגירה",
- "lightbox.next": "Next",
- "lightbox.previous": "Previous",
+ "lightbox.next": "הלאה",
+ "lightbox.previous": "הקודם",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "טוען...",
"media_gallery.toggle_visible": "נראה\\בלתי נראה",
"missing_indicator.label": "לא נמצא",
+ "mute_modal.hide_notifications": "להסתיר הודעות מחשבון זה?",
"navigation_bar.blocks": "חסימות",
"navigation_bar.community_timeline": "ציר זמן מקומי",
"navigation_bar.edit_profile": "עריכת פרופיל",
"navigation_bar.favourites": "חיבובים",
"navigation_bar.follow_requests": "בקשות מעקב",
"navigation_bar.info": "מידע נוסף",
+ "navigation_bar.keyboard_shortcuts": "קיצורי מקלדת",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "יציאה",
"navigation_bar.mutes": "השתקות",
- "navigation_bar.pins": "Pinned toots",
+ "navigation_bar.pins": "חיצרוצים מקובעים",
"navigation_bar.preferences": "העדפות",
"navigation_bar.public_timeline": "ציר זמן בין-קהילתי",
"notification.favourite": "חצרוצך חובב על ידי {name}",
@@ -125,8 +162,8 @@
"notifications.column_settings.favourite": "מחובבים:",
"notifications.column_settings.follow": "עוקבים חדשים:",
"notifications.column_settings.mention": "פניות:",
- "notifications.column_settings.push": "Push notifications",
- "notifications.column_settings.push_meta": "This device",
+ "notifications.column_settings.push": "הודעות בדחיפה",
+ "notifications.column_settings.push_meta": "מכשיר זה",
"notifications.column_settings.reblog": "הדהודים:",
"notifications.column_settings.show": "הצגה בטור",
"notifications.column_settings.sound": "שמע מופעל",
@@ -135,7 +172,7 @@
"onboarding.page_five.public_timelines": "ציר הזמן המקומי מראה הודעות פומביות מכל באי קהילת {domain}. ציר הזמן העולמי מראה הודעות פומביות מאת כי מי שבאי קהילת {domain} עוקבים אחריו. אלו צירי הזמן הפומביים, דרך נהדרת לגלות אנשים חדשים.",
"onboarding.page_four.home": "ציר זמן הבית מראה הודעות מהנעקבים שלך.",
"onboarding.page_four.notifications": "טור ההתראות מראה כשמישהו מתייחס להודעות שלך.",
- "onboarding.page_one.federation": "מסטודון היא רשת של שרתים עצמאיים מצורפים ביחד לכדי רשת חברתית אחת גדולה. אנחנו מכנים את השרתים האלו: קהילות",
+ "onboarding.page_one.federation": "מסטודון היא רשת של שרתים עצמאיים מצורפים ביחד לכדי רשת חברתית אחת גדולה. אנחנו מכנים את השרתים האלו קהילות.",
"onboarding.page_one.handle": "אתם בקהילה {domain}, ולכן מזהה המשתמש המלא שלכם הוא {handle}",
"onboarding.page_one.welcome": "ברוכים הבאים למסטודון!",
"onboarding.page_six.admin": "הקהילה מנוהלת בידי {admin}.",
@@ -161,7 +198,7 @@
"privacy.unlisted.short": "לא לפיד הכללי",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
- "relative_time.just_now": "now",
+ "relative_time.just_now": "כרגע",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "ביטול",
@@ -169,24 +206,24 @@
"report.submit": "שליחה",
"report.target": "דיווח",
"search.placeholder": "חיפוש",
- "search_popout.search_format": "Advanced search format",
- "search_popout.tips.hashtag": "hashtag",
+ "search_popout.search_format": "מבנה חיפוש מתקדם",
+ "search_popout.tips.hashtag": "האשתג",
"search_popout.tips.status": "status",
- "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
- "search_popout.tips.user": "user",
+ "search_popout.tips.text": "טקסט פשוט מחזיר כינויים, שמות משתמש והאשתגים",
+ "search_popout.tips.user": "משתמש(ת)",
"search_results.total": "{count, number} {count, plural, one {תוצאה} other {תוצאות}}",
- "standalone.public_title": "A look inside...",
+ "standalone.public_title": "הצצה פנימה...",
"status.cannot_reblog": "לא ניתן להדהד הודעה זו",
"status.delete": "מחיקה",
- "status.embed": "Embed",
+ "status.embed": "הטמעה",
"status.favourite": "חיבוב",
"status.load_more": "עוד",
"status.media_hidden": "מדיה מוסתרת",
"status.mention": "פניה אל @{name}",
- "status.more": "More",
+ "status.more": "עוד",
"status.mute_conversation": "השתקת שיחה",
"status.open": "הרחבת הודעה",
- "status.pin": "Pin on profile",
+ "status.pin": "לקבע באודות",
"status.reblog": "הדהוד",
"status.reblogged_by": "הודהד על ידי {name}",
"status.reply": "תגובה",
@@ -194,28 +231,29 @@
"status.report": "דיווח על @{name}",
"status.sensitive_toggle": "לחצו כדי לראות",
"status.sensitive_warning": "תוכן רגיש",
- "status.share": "Share",
+ "status.share": "שיתוף",
"status.show_less": "הראה פחות",
"status.show_more": "הראה יותר",
"status.unmute_conversation": "הסרת השתקת שיחה",
- "status.unpin": "Unpin from profile",
+ "status.unpin": "לשחרר מקיבוע באודות",
"tabs_bar.compose": "חיבור",
"tabs_bar.federated_timeline": "ציר זמן בין-קהילתי",
"tabs_bar.home": "בבית",
"tabs_bar.local_timeline": "ציר זמן מקומי",
"tabs_bar.notifications": "התראות",
+ "ui.beforeunload": "הטיוטא תאבד אם תעזבו את מסטודון.",
"upload_area.title": "ניתן להעלות על ידי Drag & drop",
"upload_button.label": "הוספת מדיה",
- "upload_form.description": "Describe for the visually impaired",
+ "upload_form.description": "תיאור לכבדי ראיה",
"upload_form.undo": "ביטול",
"upload_progress.label": "עולה...",
- "video.close": "Close video",
- "video.exit_fullscreen": "Exit full screen",
- "video.expand": "Expand video",
+ "video.close": "סגירת וידאו",
+ "video.exit_fullscreen": "יציאה ממסך מלא",
+ "video.expand": "להרחיב וידאו",
"video.fullscreen": "Full screen",
- "video.hide": "Hide video",
- "video.mute": "Mute sound",
+ "video.hide": "להסתיר וידאו",
+ "video.mute": "השתקת צליל",
"video.pause": "Pause",
- "video.play": "Play",
- "video.unmute": "Unmute sound"
+ "video.play": "ניגון",
+ "video.unmute": "החזרת צליל"
}
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index 6ac7fc3b4..f70c66223 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -7,17 +7,22 @@
"account.followers": "Sljedbenici",
"account.follows": "Slijedi",
"account.follows_you": "te slijedi",
+ "account.hide_reblogs": "Hide boosts from @{name}",
"account.media": "Media",
"account.mention": "Spomeni @{name}",
+ "account.moved_to": "{name} has moved to:",
"account.mute": "Utišaj @{name}",
+ "account.mute_notifications": "Mute notifications from @{name}",
"account.posts": "Postovi",
"account.report": "Prijavi @{name}",
"account.requested": "Čeka pristanak",
"account.share": "Share @{name}'s profile",
+ "account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "Deblokiraj @{name}",
"account.unblock_domain": "Poništi sakrivanje {domain}",
"account.unfollow": "Prestani slijediti",
"account.unmute": "Poništi utišavanje @{name}",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "View full profile",
"boost_modal.combo": "Možeš pritisnuti {combo} kako bi ovo preskočio sljedeći put",
"bundle_column_error.body": "Something went wrong while loading this component.",
@@ -31,6 +36,7 @@
"column.favourites": "Favoriti",
"column.follow_requests": "Zahtjevi za slijeđenje",
"column.home": "Dom",
+ "column.lists": "Lists",
"column.mutes": "Utišani korisnici",
"column.notifications": "Notifikacije",
"column.pins": "Pinned toot",
@@ -57,6 +63,8 @@
"confirmations.block.message": "Želiš li sigurno blokirati {name}?",
"confirmations.delete.confirm": "Obriši",
"confirmations.delete.message": "Želiš li stvarno obrisati ovaj status?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Sakrij cijelu domenu",
"confirmations.domain_block.message": "Jesi li zaista, zaista siguran da želiš potpuno blokirati {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Utišaj",
@@ -83,6 +91,7 @@
"empty_column.hashtag": "Još ne postoji ništa s ovim hashtagom.",
"empty_column.home": "Još ne slijediš nikoga. Posjeti {public} ili koristi tražilicu kako bi počeo i upoznao druge korisnike.",
"empty_column.home.public_timeline": "javni timeline",
+ "empty_column.list": "There is nothing in this list yet.",
"empty_column.notifications": "Još nemaš notifikacija. Komuniciraj sa drugima kako bi započeo razgovor.",
"empty_column.public": "Ovdje nema ništa! Napiši nešto javno, ili ručno slijedi korisnike sa drugih instanci kako bi popunio",
"follow_request.authorize": "Autoriziraj",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "Pokaži boostove",
"home.column_settings.show_replies": "Pokaži odgovore",
"home.settings": "Postavke Stupca",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Zatvori",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "Učitavam...",
"media_gallery.toggle_visible": "Preklopi vidljivost",
"missing_indicator.label": "Nije nađen",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.blocks": "Blokirani korisnici",
"navigation_bar.community_timeline": "Lokalni timeline",
"navigation_bar.edit_profile": "Uredi profil",
"navigation_bar.favourites": "Favoriti",
"navigation_bar.follow_requests": "Zahtjevi za slijeđenje",
"navigation_bar.info": "Više informacija",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "Odjavi se",
"navigation_bar.mutes": "Utišani korisnici",
"navigation_bar.pins": "Pinned toots",
@@ -204,6 +241,7 @@
"tabs_bar.home": "Dom",
"tabs_bar.local_timeline": "Lokalno",
"tabs_bar.notifications": "Notifikacije",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Povuci i spusti kako bi uploadao",
"upload_button.label": "Dodaj media",
"upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 5892e606e..7cb816fe9 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -7,17 +7,22 @@
"account.followers": "Követők",
"account.follows": "Követve",
"account.follows_you": "Követnek téged",
+ "account.hide_reblogs": "Hide boosts from @{name}",
"account.media": "Media",
"account.mention": "Említés",
+ "account.moved_to": "{name} has moved to:",
"account.mute": "Mute @{name}",
+ "account.mute_notifications": "Mute notifications from @{name}",
"account.posts": "Posts",
"account.report": "Report @{name}",
"account.requested": "Awaiting approval",
"account.share": "Share @{name}'s profile",
+ "account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "Blokkolás levétele",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Követés abbahagyása",
"account.unmute": "Unmute @{name}",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "View full profile",
"boost_modal.combo": "You can press {combo} to skip this next time",
"bundle_column_error.body": "Something went wrong while loading this component.",
@@ -31,6 +36,7 @@
"column.favourites": "Favourites",
"column.follow_requests": "Follow requests",
"column.home": "Kezdőlap",
+ "column.lists": "Lists",
"column.mutes": "Muted users",
"column.notifications": "Értesítések",
"column.pins": "Pinned toot",
@@ -57,6 +63,8 @@
"confirmations.block.message": "Are you sure you want to block {name}?",
"confirmations.delete.confirm": "Delete",
"confirmations.delete.message": "Are you sure you want to delete this status?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Mute",
@@ -83,6 +91,7 @@
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
"empty_column.home.public_timeline": "the public timeline",
+ "empty_column.list": "There is nothing in this list yet.",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
"follow_request.authorize": "Authorize",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Bezárás",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "Betöltés...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.blocks": "Blocked users",
"navigation_bar.community_timeline": "Local timeline",
"navigation_bar.edit_profile": "Profil szerkesztése",
"navigation_bar.favourites": "Favourites",
"navigation_bar.follow_requests": "Follow requests",
"navigation_bar.info": "Extended information",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "Kijelentkezés",
"navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
@@ -204,6 +241,7 @@
"tabs_bar.home": "Kezdőlap",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notifications",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Drag & drop to upload",
"upload_button.label": "Média hozzáadása",
"upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index f73ef0e19..429b77182 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -7,17 +7,22 @@
"account.followers": "Pengikut",
"account.follows": "Mengikuti",
"account.follows_you": "Mengikuti anda",
+ "account.hide_reblogs": "Hide boosts from @{name}",
"account.media": "Media",
"account.mention": "Balasan @{name}",
+ "account.moved_to": "{name} has moved to:",
"account.mute": "Bisukan @{name}",
+ "account.mute_notifications": "Mute notifications from @{name}",
"account.posts": "Postingan",
"account.report": "Laporkan @{name}",
"account.requested": "Menunggu persetujuan",
"account.share": "Share @{name}'s profile",
+ "account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "Hapus blokir @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Berhenti mengikuti",
"account.unmute": "Berhenti membisukan @{name}",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "View full profile",
"boost_modal.combo": "Anda dapat menekan {combo} untuk melewati ini",
"bundle_column_error.body": "Something went wrong while loading this component.",
@@ -31,6 +36,7 @@
"column.favourites": "Favorit",
"column.follow_requests": "Permintaan mengikuti",
"column.home": "Beranda",
+ "column.lists": "Lists",
"column.mutes": "Pengguna dibisukan",
"column.notifications": "Notifikasi",
"column.pins": "Pinned toot",
@@ -57,6 +63,8 @@
"confirmations.block.message": "Apa anda yakin ingin memblokir {name}?",
"confirmations.delete.confirm": "Hapus",
"confirmations.delete.message": "Apa anda yakin akan menghapus status ini?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Bisukan",
@@ -83,6 +91,7 @@
"empty_column.hashtag": "Tidak ada apapun dalam hashtag ini.",
"empty_column.home": "Anda sedang tidak mengikuti siapapun. Kunjungi {public} atau gunakan pencarian untuk memulai dan bertemu pengguna lain.",
"empty_column.home.public_timeline": "linimasa publik",
+ "empty_column.list": "There is nothing in this list yet.",
"empty_column.notifications": "Anda tidak memiliki notifikasi apapun. Berinteraksi dengan orang lain untuk memulai percakapan.",
"empty_column.public": "Tidak ada apapun disini! Tulis sesuatu, atau ikuti pengguna lain dari server lain untuk mengisinya secara manual",
"follow_request.authorize": "Izinkan",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "Tampilkan Boost",
"home.column_settings.show_replies": "Tampilkan balasan",
"home.settings": "Pengaturan kolom",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Tutup",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "Tunggu sebentar...",
"media_gallery.toggle_visible": "Tampil/Sembunyikan",
"missing_indicator.label": "Tidak ditemukan",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.blocks": "Pengguna diblokir",
"navigation_bar.community_timeline": "Linimasa lokal",
"navigation_bar.edit_profile": "Ubah profil",
"navigation_bar.favourites": "Favorit",
"navigation_bar.follow_requests": "Permintaan mengikuti",
"navigation_bar.info": "Informasi selengkapnya",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "Keluar",
"navigation_bar.mutes": "Pengguna dibisukan",
"navigation_bar.pins": "Pinned toots",
@@ -204,6 +241,7 @@
"tabs_bar.home": "Beranda",
"tabs_bar.local_timeline": "Lokal",
"tabs_bar.notifications": "Notifikasi",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Seret & lepaskan untuk mengunggah",
"upload_button.label": "Tambahkan media",
"upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index 53371bece..3e5c8edb9 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -7,17 +7,22 @@
"account.followers": "Sequanti",
"account.follows": "Sequas",
"account.follows_you": "Sequas tu",
+ "account.hide_reblogs": "Hide boosts from @{name}",
"account.media": "Media",
"account.mention": "Mencionar @{name}",
+ "account.moved_to": "{name} has moved to:",
"account.mute": "Celar @{name}",
+ "account.mute_notifications": "Mute notifications from @{name}",
"account.posts": "Mesaji",
"account.report": "Denuncar @{name}",
"account.requested": "Vartante aprobo",
"account.share": "Share @{name}'s profile",
+ "account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "Desblokusar @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Ne plus sequar",
"account.unmute": "Ne plus celar @{name}",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "View full profile",
"boost_modal.combo": "Tu povas presar sur {combo} por omisar co en la venonta foyo",
"bundle_column_error.body": "Something went wrong while loading this component.",
@@ -31,6 +36,7 @@
"column.favourites": "Favorati",
"column.follow_requests": "Demandi di sequado",
"column.home": "Hemo",
+ "column.lists": "Lists",
"column.mutes": "Celita uzeri",
"column.notifications": "Savigi",
"column.pins": "Pinned toot",
@@ -57,6 +63,8 @@
"confirmations.block.message": "Are you sure you want to block {name}?",
"confirmations.delete.confirm": "Delete",
"confirmations.delete.message": "Are you sure you want to delete this status?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Mute",
@@ -83,6 +91,7 @@
"empty_column.hashtag": "Esas ankore nulo en ta gretovorto.",
"empty_column.home": "Tu sequas ankore nulu. Vizitez {public} od uzez la serchilo por komencar e renkontrar altra uzeri.",
"empty_column.home.public_timeline": "la publika tempolineo",
+ "empty_column.list": "There is nothing in this list yet.",
"empty_column.notifications": "Tu havas ankore nula savigo. Komunikez kun altri por debutar la konverso.",
"empty_column.public": "Esas nulo hike! Skribez ulo publike, o manuale sequez uzeri de altra instaluri por plenigar ol.",
"follow_request.authorize": "Yurizar",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "Montrar repeti",
"home.column_settings.show_replies": "Montrar respondi",
"home.settings": "Aranji di la kolumno",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Klozar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "Kargante...",
"media_gallery.toggle_visible": "Chanjar videbleso",
"missing_indicator.label": "Ne trovita",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.blocks": "Blokusita uzeri",
"navigation_bar.community_timeline": "Lokala tempolineo",
"navigation_bar.edit_profile": "Modifikar profilo",
"navigation_bar.favourites": "Favorati",
"navigation_bar.follow_requests": "Demandi di sequado",
"navigation_bar.info": "Detaloza informi",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "Ekirar",
"navigation_bar.mutes": "Celita uzeri",
"navigation_bar.pins": "Pinned toots",
@@ -204,6 +241,7 @@
"tabs_bar.home": "Hemo",
"tabs_bar.local_timeline": "Lokala",
"tabs_bar.notifications": "Savigi",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Tranar faligar por kargar",
"upload_button.label": "Adjuntar kontenajo",
"upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 3873d797e..e2ad1632a 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -7,17 +7,22 @@
"account.followers": "Seguaci",
"account.follows": "Segue",
"account.follows_you": "Ti segue",
+ "account.hide_reblogs": "Hide boosts from @{name}",
"account.media": "Media",
"account.mention": "Menziona @{name}",
+ "account.moved_to": "{name} has moved to:",
"account.mute": "Silenzia @{name}",
+ "account.mute_notifications": "Mute notifications from @{name}",
"account.posts": "Posts",
"account.report": "Segnala @{name}",
"account.requested": "In attesa di approvazione",
"account.share": "Share @{name}'s profile",
+ "account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "Sblocca @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Non seguire",
"account.unmute": "Non silenziare @{name}",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "View full profile",
"boost_modal.combo": "Puoi premere {combo} per saltare questo passaggio la prossima volta",
"bundle_column_error.body": "Something went wrong while loading this component.",
@@ -31,6 +36,7 @@
"column.favourites": "Apprezzati",
"column.follow_requests": "Richieste di amicizia",
"column.home": "Home",
+ "column.lists": "Lists",
"column.mutes": "Utenti silenziati",
"column.notifications": "Notifiche",
"column.pins": "Pinned toot",
@@ -57,6 +63,8 @@
"confirmations.block.message": "Are you sure you want to block {name}?",
"confirmations.delete.confirm": "Delete",
"confirmations.delete.message": "Are you sure you want to delete this status?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Mute",
@@ -83,6 +91,7 @@
"empty_column.hashtag": "Non c'è ancora nessun post con questo hashtag.",
"empty_column.home": "Non stai ancora seguendo nessuno. Visita {public} o usa la ricerca per incontrare nuove persone.",
"empty_column.home.public_timeline": "la timeline pubblica",
+ "empty_column.list": "There is nothing in this list yet.",
"empty_column.notifications": "Non hai ancora nessuna notifica. Interagisci con altri per iniziare conversazioni.",
"empty_column.public": "Qui non c'è nulla! Scrivi qualcosa pubblicamente, o aggiungi utenti da altri server per riempire questo spazio.",
"follow_request.authorize": "Autorizza",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "Mostra post condivisi",
"home.column_settings.show_replies": "Mostra risposte",
"home.settings": "Impostazioni colonna",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Chiudi",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "Carico...",
"media_gallery.toggle_visible": "Imposta visibilità",
"missing_indicator.label": "Non trovato",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.blocks": "Utenti bloccati",
"navigation_bar.community_timeline": "Timeline locale",
"navigation_bar.edit_profile": "Modifica profilo",
"navigation_bar.favourites": "Apprezzati",
"navigation_bar.follow_requests": "Richieste di amicizia",
"navigation_bar.info": "Informazioni estese",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "Logout",
"navigation_bar.mutes": "Utenti silenziati",
"navigation_bar.pins": "Pinned toots",
@@ -204,6 +241,7 @@
"tabs_bar.home": "Home",
"tabs_bar.local_timeline": "Locale",
"tabs_bar.notifications": "Notifiche",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Trascina per caricare",
"upload_button.label": "Aggiungi file multimediale",
"upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index fb6d11ebe..2d4ec531c 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -7,19 +7,24 @@
"account.followers": "フォロワー",
"account.follows": "フォロー",
"account.follows_you": "フォローされています",
+ "account.hide_reblogs": "@{name}さんからのブーストを非表示",
"account.media": "メディア",
"account.mention": "返信",
+ "account.moved_to": "{name}さんは引っ越しました:",
"account.mute": "ミュート",
+ "account.mute_notifications": "@{name}さんからの通知を受け取る",
"account.posts": "投稿",
"account.report": "通報",
"account.requested": "承認待ち",
- "account.share": "@{name} のプロフィールを共有する",
+ "account.share": "@{name}さんのプロフィールを共有する",
+ "account.show_reblogs": "@{name}さんからのブーストを表示",
"account.unblock": "ブロック解除",
"account.unblock_domain": "{domain}を表示",
"account.unfollow": "フォロー解除",
"account.unmute": "ミュート解除",
+ "account.unmute_notifications": "@{name}さんからの通知を受け取らない",
"account.view_full_profile": "全ての情報を見る",
- "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。",
+ "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます",
"bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。",
"bundle_column_error.retry": "再試行",
"bundle_column_error.title": "ネットワークエラー",
@@ -31,6 +36,7 @@
"column.favourites": "お気に入り",
"column.follow_requests": "フォローリクエスト",
"column.home": "ホーム",
+ "column.lists": "リスト",
"column.mutes": "ミュートしたユーザー",
"column.notifications": "通知",
"column.pins": "固定されたトゥート",
@@ -54,15 +60,17 @@
"compose_form.spoiler_placeholder": "ここに警告を書いてください",
"confirmation_modal.cancel": "キャンセル",
"confirmations.block.confirm": "ブロック",
- "confirmations.block.message": "本当に{name}をブロックしますか?",
+ "confirmations.block.message": "本当に{name}さんをブロックしますか?",
"confirmations.delete.confirm": "削除",
"confirmations.delete.message": "本当に削除しますか?",
+ "confirmations.delete_list.confirm": "削除",
+ "confirmations.delete_list.message": "本当に削除しますか?",
"confirmations.domain_block.confirm": "ドメイン全体を非表示",
"confirmations.domain_block.message": "本当に{domain}全体を非表示にしますか? 多くの場合は個別にブロックやミュートするだけで充分であり、また好ましいです。",
"confirmations.mute.confirm": "ミュート",
- "confirmations.mute.message": "本当に{name}をミュートしますか?",
+ "confirmations.mute.message": "本当に{name}さんをミュートしますか?",
"confirmations.unfollow.confirm": "フォロー解除",
- "confirmations.unfollow.message": "本当に{name}をフォロー解除しますか?",
+ "confirmations.unfollow.message": "本当に{name}さんをフォロー解除しますか?",
"embed.instructions": "下記のコードをコピーしてウェブサイトに埋め込みます。",
"embed.preview": "表示例:",
"emoji_button.activity": "活動",
@@ -83,8 +91,9 @@
"empty_column.hashtag": "このハッシュタグはまだ使われていません。",
"empty_column.home": "まだ誰もフォローしていません。{public}を見に行くか、検索を使って他のユーザーを見つけましょう。",
"empty_column.home.public_timeline": "連合タイムライン",
+ "empty_column.list": "このリストにはまだなにもありません。",
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
- "empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!",
+ "empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう",
"follow_request.authorize": "許可",
"follow_request.reject": "拒否",
"getting_started.appsshort": "アプリ",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "ブースト表示",
"home.column_settings.show_replies": "返信表示",
"home.settings": "カラム設定",
+ "keyboard_shortcuts.back": "戻る",
+ "keyboard_shortcuts.boost": "ブースト",
+ "keyboard_shortcuts.column": "左からn番目のカラム内最新トゥートに移動",
+ "keyboard_shortcuts.compose": "トゥート入力欄に移動",
+ "keyboard_shortcuts.description": "説明",
+ "keyboard_shortcuts.down": "カラム内一つ下に移動",
+ "keyboard_shortcuts.enter": "トゥートの詳細を表示",
+ "keyboard_shortcuts.favourite": "お気に入り",
+ "keyboard_shortcuts.heading": "キーボードショートカット",
+ "keyboard_shortcuts.hotkey": "ホットキー",
+ "keyboard_shortcuts.legend": "この一覧を表示",
+ "keyboard_shortcuts.mention": "メンション",
+ "keyboard_shortcuts.reply": "返信",
+ "keyboard_shortcuts.search": "検索欄に移動",
+ "keyboard_shortcuts.toot": "新規トゥート",
+ "keyboard_shortcuts.unfocus": "トゥート入力欄・検索欄から離れる",
+ "keyboard_shortcuts.up": "カラム内一つ上に移動",
"lightbox.close": "閉じる",
"lightbox.next": "次",
"lightbox.previous": "前",
+ "lists.account.add": "リストに追加",
+ "lists.account.remove": "リストから外す",
+ "lists.delete": "リストを削除",
+ "lists.edit": "リストを編集",
+ "lists.new.create": "リストを作成",
+ "lists.new.title_placeholder": "新規リスト名",
+ "lists.search": "フォローしている人の中から検索",
+ "lists.subheading": "あなたのリスト",
"loading_indicator.label": "読み込み中...",
"media_gallery.toggle_visible": "表示切り替え",
"missing_indicator.label": "見つかりません",
+ "mute_modal.hide_notifications": "このユーザーからの通知を隠しますか?",
"navigation_bar.blocks": "ブロックしたユーザー",
"navigation_bar.community_timeline": "ローカルタイムライン",
"navigation_bar.edit_profile": "プロフィールを編集",
"navigation_bar.favourites": "お気に入り",
"navigation_bar.follow_requests": "フォローリクエスト",
"navigation_bar.info": "このインスタンスについて",
+ "navigation_bar.keyboard_shortcuts": "キーボードショートカット",
+ "navigation_bar.lists": "リスト",
"navigation_bar.logout": "ログアウト",
"navigation_bar.mutes": "ミュートしたユーザー",
"navigation_bar.pins": "固定されたトゥート",
@@ -122,12 +159,12 @@
"notifications.clear": "通知を消去",
"notifications.clear_confirmation": "本当に通知を消去しますか?",
"notifications.column_settings.alert": "デスクトップ通知",
- "notifications.column_settings.favourite": "お気に入り",
- "notifications.column_settings.follow": "新しいフォロワー",
- "notifications.column_settings.mention": "返信",
+ "notifications.column_settings.favourite": "お気に入り:",
+ "notifications.column_settings.follow": "新しいフォロワー:",
+ "notifications.column_settings.mention": "返信:",
"notifications.column_settings.push": "プッシュ通知",
"notifications.column_settings.push_meta": "このデバイス",
- "notifications.column_settings.reblog": "ブースト",
+ "notifications.column_settings.reblog": "ブースト:",
"notifications.column_settings.show": "カラムに表示",
"notifications.column_settings.sound": "通知音を再生",
"onboarding.done": "完了",
@@ -136,7 +173,7 @@
"onboarding.page_four.home": "「ホーム」タイムラインではあなたがフォローしている人の投稿を表示します。",
"onboarding.page_four.notifications": "「通知」ではあなたへの他の人からの関わりを表示します。",
"onboarding.page_one.federation": "Mastodonは誰でも参加できるSNSです。",
- "onboarding.page_one.handle": "あなたは今数あるMastodonインスタンスの1つである{domain}にいます。あなたのフルハンドルは{handle}です。",
+ "onboarding.page_one.handle": "今あなたは数あるMastodonインスタンスの1つである{domain}にいます。あなたのフルハンドルは{handle}です",
"onboarding.page_one.welcome": "Mastodonへようこそ!",
"onboarding.page_six.admin": "あなたのインスタンスの管理者は{admin}です。",
"onboarding.page_six.almost_done": "以上です。",
@@ -144,7 +181,7 @@
"onboarding.page_six.apps_available": "iOS、Androidあるいは他のプラットフォームで使える{apps}があります。",
"onboarding.page_six.github": "MastodonはOSSです。バグ報告や機能要望あるいは貢献を{github}から行なえます。",
"onboarding.page_six.guidelines": "コミュニティガイドライン",
- "onboarding.page_six.read_guidelines": "{guidelines}を読むことを忘れないようにしてください。",
+ "onboarding.page_six.read_guidelines": "{guidelines}を読むことを忘れないようにしてください!",
"onboarding.page_six.various_app": "様々なモバイルアプリ",
"onboarding.page_three.profile": "「プロフィールを編集」から、あなたの自己紹介や表示名を変更できます。またそこでは他の設定ができます。",
"onboarding.page_three.search": "検索バーで、{illustration}や{introductions}のように特定のハッシュタグの投稿を見たり、ユーザーを探したりできます。",
@@ -175,7 +212,7 @@
"search_popout.tips.text": "表示名やユーザー名、ハッシュタグに一致する単純なテキスト",
"search_popout.tips.user": "ユーザー",
"search_results.total": "{count, number}件の結果",
- "standalone.public_title": "今こんな話をしています",
+ "standalone.public_title": "今こんな話をしています...",
"status.cannot_reblog": "この投稿はブーストできません",
"status.delete": "削除",
"status.embed": "埋め込み",
@@ -204,6 +241,7 @@
"tabs_bar.home": "ホーム",
"tabs_bar.local_timeline": "ローカル",
"tabs_bar.notifications": "通知",
+ "ui.beforeunload": "Mastodonから離れるとあなたのドラフトは失われます。",
"upload_area.title": "ドラッグ&ドロップでアップロード",
"upload_button.label": "メディアを追加",
"upload_form.description": "視覚障害者のための説明",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index d99dacd59..472a52a99 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -7,17 +7,22 @@
"account.followers": "팔로워",
"account.follows": "팔로우",
"account.follows_you": "날 팔로우합니다",
+ "account.hide_reblogs": "Hide boosts from @{name}",
"account.media": "미디어",
"account.mention": "답장",
+ "account.moved_to": "{name} has moved to:",
"account.mute": "뮤트",
+ "account.mute_notifications": "Mute notifications from @{name}",
"account.posts": "포스트",
"account.report": "신고",
"account.requested": "승인 대기 중",
"account.share": "Share @{name}'s profile",
+ "account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "차단 해제",
"account.unblock_domain": "{domain} 숨김 해제",
"account.unfollow": "팔로우 해제",
"account.unmute": "뮤트 해제",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "전체 프로필 보기",
"boost_modal.combo": "다음부터 {combo}를 누르면 이 과정을 건너뛸 수 있습니다.",
"bundle_column_error.body": "Something went wrong while loading this component.",
@@ -31,6 +36,7 @@
"column.favourites": "즐겨찾기",
"column.follow_requests": "팔로우 요청",
"column.home": "홈",
+ "column.lists": "Lists",
"column.mutes": "뮤트 중인 사용자",
"column.notifications": "알림",
"column.pins": "고정된 툿",
@@ -57,13 +63,15 @@
"confirmations.block.message": "정말로 {name}를 차단하시겠습니까?",
"confirmations.delete.confirm": "삭제",
"confirmations.delete.message": "정말로 삭제하시겠습니까?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "도메인 전체를 숨김",
"confirmations.domain_block.message": "정말로 {domain} 전체를 숨기시겠습니까? 대부분의 경우 개별 차단이나 뮤트로 충분합니다.",
"confirmations.mute.confirm": "뮤트",
"confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?",
"confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
- "embed.instructions": "아래의 코드를 복사하여 대화를 원하는 곳으로 퍼가세요.",
+ "embed.instructions": "아래의 코드를 복사하여 대화를 원하는 곳으로 공유하세요.",
"embed.preview": "다음과 같이 표시됩니다:",
"emoji_button.activity": "활동",
"emoji_button.custom": "Custom",
@@ -83,11 +91,12 @@
"empty_column.hashtag": "이 해시태그는 아직 사용되지 않았습니다.",
"empty_column.home": "아직 아무도 팔로우 하고 있지 않습니다. {public}를 보러 가거나, 검색하여 다른 사용자를 찾아 보세요.",
"empty_column.home.public_timeline": "연합 타임라인",
+ "empty_column.list": "There is nothing in this list yet.",
"empty_column.notifications": "아직 알림이 없습니다. 다른 사람과 대화를 시작해 보세요!",
"empty_column.public": "여기엔 아직 아무 것도 없습니다! 공개적으로 무언가 포스팅하거나, 다른 인스턴스 유저를 팔로우 해서 가득 채워보세요!",
"follow_request.authorize": "허가",
"follow_request.reject": "거부",
- "getting_started.appsshort": "어플리케이션",
+ "getting_started.appsshort": "애플리케이션",
"getting_started.faq": "자주 있는 질문",
"getting_started.heading": "시작",
"getting_started.open_source_notice": "Mastodon은 오픈 소스 소프트웨어입니다. 누구나 GitHub({github})에서 개발에 참여하거나, 문제를 보고할 수 있습니다.",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "부스트 표시",
"home.column_settings.show_replies": "답글 표시",
"home.settings": "컬럼 설정",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "닫기",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "불러오는 중...",
"media_gallery.toggle_visible": "표시 전환",
"missing_indicator.label": "찾을 수 없습니다",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.blocks": "차단한 사용자",
"navigation_bar.community_timeline": "로컬 타임라인",
"navigation_bar.edit_profile": "프로필 편집",
"navigation_bar.favourites": "즐겨찾기",
"navigation_bar.follow_requests": "팔로우 요청",
"navigation_bar.info": "이 인스턴스에 대해서",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "로그아웃",
"navigation_bar.mutes": "뮤트 중인 사용자",
"navigation_bar.pins": "고정된 툿",
@@ -145,7 +182,7 @@
"onboarding.page_six.github": "Mastodon는 오픈 소스 소프트웨어입니다. 버그 보고나 기능 추가 요청, 기여는 {github}에서 할 수 있습니다.",
"onboarding.page_six.guidelines": "커뮤니티 가이드라인",
"onboarding.page_six.read_guidelines": "{guidelines}을 확인하는 것을 잊지 마세요.",
- "onboarding.page_six.various_app": "다양한 모바일 어플리케이션",
+ "onboarding.page_six.various_app": "다양한 모바일 애플리케이션",
"onboarding.page_three.profile": "[프로필 편집] 에서 자기 소개나 이름을 변경할 수 있습니다. 또한 다른 설정도 변경할 수 있습니다.",
"onboarding.page_three.search": "검색 바에서 {illustration} 나 {introductions} 와 같이 특정 해시태그가 달린 포스트를 보거나, 사용자를 찾을 수 있습니다.",
"onboarding.page_two.compose": "이 폼에서 포스팅 할 수 있습니다. 이미지나 공개 범위 설정, 스포일러 경고 설정은 아래 아이콘으로 설정할 수 있습니다.",
@@ -204,6 +241,7 @@
"tabs_bar.home": "홈",
"tabs_bar.local_timeline": "로컬",
"tabs_bar.notifications": "알림",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "드래그 & 드롭으로 업로드",
"upload_button.label": "미디어 추가",
"upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index d0727a24d..e154d1ab2 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -7,17 +7,22 @@
"account.followers": "Volgers",
"account.follows": "Volgt",
"account.follows_you": "Volgt jou",
+ "account.hide_reblogs": "Verberg boosts van @{name}",
"account.media": "Media",
"account.mention": "Vermeld @{name}",
+ "account.moved_to": "{name} is verhuisd naar:",
"account.mute": "Negeer @{name}",
+ "account.mute_notifications": "Negeer meldingen van @{name}",
"account.posts": "Toots",
"account.report": "Rapporteer @{name}",
- "account.requested": "Wacht op goedkeuring. Klik om volgverzoek te annuleren.",
+ "account.requested": "Wacht op goedkeuring. Klik om het volgverzoek te annuleren",
"account.share": "Profiel van @{name} delen",
+ "account.show_reblogs": "Toon boosts van @{name}",
"account.unblock": "Deblokkeer @{name}",
"account.unblock_domain": "{domain} niet meer negeren",
"account.unfollow": "Ontvolgen",
"account.unmute": "@{name} niet meer negeren",
+ "account.unmute_notifications": "@{name} meldingen niet meer negeren",
"account.view_full_profile": "Volledig profiel tonen",
"boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan",
"bundle_column_error.body": "Tijdens het laden van dit onderdeel is er iets fout gegaan.",
@@ -31,6 +36,7 @@
"column.favourites": "Favorieten",
"column.follow_requests": "Volgverzoeken",
"column.home": "Start",
+ "column.lists": "Lijsten",
"column.mutes": "Genegeerde gebruikers",
"column.notifications": "Meldingen",
"column.pins": "Vastgezette toots",
@@ -57,6 +63,8 @@
"confirmations.block.message": "Weet je het zeker dat je {name} wilt blokkeren?",
"confirmations.delete.confirm": "Verwijderen",
"confirmations.delete.message": "Weet je het zeker dat je deze toot wilt verwijderen?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Weet je zeker dat je deze lijst definitief wilt verwijderen?",
"confirmations.domain_block.confirm": "Negeer alles van deze server",
"confirmations.domain_block.message": "Weet je het echt, echt zeker dat je alles van {domain} wil negeren? In de meeste gevallen is het blokkeren of negeren van een paar specifieke personen voldoende en gewenst.",
"confirmations.mute.confirm": "Negeren",
@@ -83,8 +91,9 @@
"empty_column.hashtag": "Er is nog niks te vinden onder deze hashtag.",
"empty_column.home": "Jij volgt nog niemand. Bezoek {public} of gebruik het zoekvenster om andere mensen te ontmoeten.",
"empty_column.home.public_timeline": "de globale tijdlijn",
+ "empty_column.list": "Er is nog niks in deze lijst. Wanneer lijstleden nieuwe toots publiceren, zijn deze hier te zien.",
"empty_column.notifications": "Je hebt nog geen meldingen. Heb interactie met andere mensen om het gesprek aan te gaan.",
- "empty_column.public": "Er is hier helemaal niks! Toot iets in het openbaar of volg mensen van andere Mastodon-servers om het te vullen.",
+ "empty_column.public": "Er is hier helemaal niks! Toot iets in het openbaar of volg mensen van andere servers om het te vullen",
"follow_request.authorize": "Goedkeuren",
"follow_request.reject": "Afkeuren",
"getting_started.appsshort": "Apps",
@@ -93,23 +102,51 @@
"getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}.",
"getting_started.userguide": "Gebruikersgids",
"home.column_settings.advanced": "Geavanceerd",
- "home.column_settings.basic": "Basic",
+ "home.column_settings.basic": "Basis",
"home.column_settings.filter_regex": "Wegfilteren met reguliere expressies",
"home.column_settings.show_reblogs": "Boosts tonen",
"home.column_settings.show_replies": "Reacties tonen",
"home.settings": "Kolom-instellingen",
+ "keyboard_shortcuts.back": "om terug te gaan",
+ "keyboard_shortcuts.boost": "om te boosten",
+ "keyboard_shortcuts.column": "om op een toot te focussen in één van de kolommen",
+ "keyboard_shortcuts.compose": "om het tekstvak voor toots te focussen",
+ "keyboard_shortcuts.description": "Omschrijving",
+ "keyboard_shortcuts.down": "om naar beneden door de lijst te bewegen",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "om als favoriet te markeren",
+ "keyboard_shortcuts.heading": "Sneltoetsen",
+ "keyboard_shortcuts.hotkey": "Sneltoets",
+ "keyboard_shortcuts.legend": "om deze legenda weer te geven",
+ "keyboard_shortcuts.mention": "om de auteur te vermelden",
+ "keyboard_shortcuts.reply": "om te reageren",
+ "keyboard_shortcuts.search": "om het zoekvak te focussen",
+ "keyboard_shortcuts.toot": "om een nieuwe toot te starten",
+ "keyboard_shortcuts.unfocus": "om het tekst- en zoekvak te ontfocussen",
+ "keyboard_shortcuts.up": "om omhoog te bewegen in de lijst",
"lightbox.close": "Sluiten",
"lightbox.next": "Volgende",
"lightbox.previous": "Vorige",
+ "lists.account.add": "Aan lijst toevoegen",
+ "lists.account.remove": "Uit lijst verwijderen",
+ "lists.delete": "Lijst verwijderen",
+ "lists.edit": "Lijst bewerken",
+ "lists.new.create": "Lijst toevoegen",
+ "lists.new.title_placeholder": "Naam nieuwe lijst",
+ "lists.search": "Zoek naar mensen die je volgt",
+ "lists.subheading": "Jouw lijsten",
"loading_indicator.label": "Laden…",
"media_gallery.toggle_visible": "Media wel/niet tonen",
"missing_indicator.label": "Niet gevonden",
+ "mute_modal.hide_notifications": "Verberg meldingen van deze persoon?",
"navigation_bar.blocks": "Geblokkeerde gebruikers",
"navigation_bar.community_timeline": "Lokale tijdlijn",
"navigation_bar.edit_profile": "Profiel bewerken",
"navigation_bar.favourites": "Favorieten",
"navigation_bar.follow_requests": "Volgverzoeken",
"navigation_bar.info": "Uitgebreide informatie",
+ "navigation_bar.keyboard_shortcuts": "Toetsenbord sneltoetsen",
+ "navigation_bar.lists": "Lijsten",
"navigation_bar.logout": "Afmelden",
"navigation_bar.mutes": "Genegeerde gebruikers",
"navigation_bar.pins": "Vastgezette toots",
@@ -161,18 +198,18 @@
"privacy.unlisted.short": "Minder openbaar",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
- "relative_time.just_now": "now",
+ "relative_time.just_now": "nu",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Annuleren",
"report.placeholder": "Extra opmerkingen",
"report.submit": "Verzenden",
- "report.target": "Rapporteren van",
+ "report.target": "Rapporteer {target}",
"search.placeholder": "Zoeken",
"search_popout.search_format": "Geavanceerd zoeken",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "toot",
- "search_popout.tips.text": "Gebruik gewone tekst om te zoeken op weergavenamen, gebruikersnamen en hashtags.",
+ "search_popout.tips.text": "Gebruik gewone tekst om te zoeken op weergavenamen, gebruikersnamen en hashtags",
"search_popout.tips.user": "gebruiker",
"search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}",
"standalone.public_title": "Een kijkje binnenin...",
@@ -204,6 +241,7 @@
"tabs_bar.home": "Start",
"tabs_bar.local_timeline": "Lokaal",
"tabs_bar.notifications": "Meldingen",
+ "ui.beforeunload": "Je concept zal verloren gaan als je Mastodon verlaat.",
"upload_area.title": "Hierin slepen om te uploaden",
"upload_button.label": "Media toevoegen",
"upload_form.description": "Omschrijf dit voor mensen met een visuele beperking",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index d74ae0091..bf2b6259a 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -1,47 +1,53 @@
{
"account.block": "Blokkér @{name}",
"account.block_domain": "Skjul alt fra {domain}",
- "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+ "account.disclaimer_full": "Informasjonen nedenfor kan gi et ufullstendig bilde av brukerens profil.",
"account.edit_profile": "Rediger profil",
"account.follow": "Følg",
"account.followers": "Følgere",
"account.follows": "Følger",
"account.follows_you": "Følger deg",
+ "account.hide_reblogs": "Hide boosts from @{name}",
"account.media": "Media",
"account.mention": "Nevn @{name}",
+ "account.moved_to": "{name} har flyttet til:",
"account.mute": "Demp @{name}",
+ "account.mute_notifications": "Ignorer varsler fra @{name}",
"account.posts": "Innlegg",
"account.report": "Rapportér @{name}",
"account.requested": "Venter på godkjennelse",
- "account.share": "Share @{name}'s profile",
+ "account.share": "Del @{name}s profil",
+ "account.show_reblogs": "Vis boosts fra @{name}",
"account.unblock": "Avblokker @{name}",
"account.unblock_domain": "Vis {domain}",
"account.unfollow": "Avfølg",
"account.unmute": "Avdemp @{name}",
- "account.view_full_profile": "View full profile",
+ "account.unmute_notifications": "Vis varsler fra @{name}",
+ "account.view_full_profile": "Vis full profil",
"boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang",
- "bundle_column_error.body": "Something went wrong while loading this component.",
- "bundle_column_error.retry": "Try again",
+ "bundle_column_error.body": "Noe gikk galt mens denne komponenten lastet.",
+ "bundle_column_error.retry": "Prøv igjen",
"bundle_column_error.title": "Network error",
- "bundle_modal_error.close": "Close",
- "bundle_modal_error.message": "Something went wrong while loading this component.",
- "bundle_modal_error.retry": "Try again",
+ "bundle_modal_error.close": "Lukk",
+ "bundle_modal_error.message": "Noe gikk galt da denne komponenten lastet.",
+ "bundle_modal_error.retry": "Prøv igjen",
"column.blocks": "Blokkerte brukere",
"column.community": "Lokal tidslinje",
"column.favourites": "Likt",
"column.follow_requests": "Følgeforespørsler",
"column.home": "Hjem",
+ "column.lists": "Lists",
"column.mutes": "Dempede brukere",
"column.notifications": "Varsler",
"column.pins": "Pinned toot",
"column.public": "Felles tidslinje",
"column_back_button.label": "Tilbake",
- "column_header.hide_settings": "Hide settings",
- "column_header.moveLeft_settings": "Move column to the left",
- "column_header.moveRight_settings": "Move column to the right",
- "column_header.pin": "Pin",
- "column_header.show_settings": "Show settings",
- "column_header.unpin": "Unpin",
+ "column_header.hide_settings": "Gjem innstillinger",
+ "column_header.moveLeft_settings": "Flytt feltet til venstre",
+ "column_header.moveRight_settings": "Flytt feltet til høyre",
+ "column_header.pin": "Fest",
+ "column_header.show_settings": "Vis innstillinger",
+ "column_header.unpin": "Løsne",
"column_subheading.navigation": "Navigasjon",
"column_subheading.settings": "Innstillinger",
"compose_form.lock_disclaimer": "Din konto er ikke {locked}. Hvem som helst kan følge deg og se dine private poster.",
@@ -57,32 +63,35 @@
"confirmations.block.message": "Er du sikker på at du vil blokkere {name}?",
"confirmations.delete.confirm": "Slett",
"confirmations.delete.message": "Er du sikker på at du vil slette denne statusen?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Skjul alt fra domenet",
"confirmations.domain_block.message": "Er du sikker på at du vil skjule hele domenet {domain}? I de fleste tilfeller er det bedre med målrettet blokkering eller demping.",
"confirmations.mute.confirm": "Demp",
"confirmations.mute.message": "Er du sikker på at du vil dempe {name}?",
- "confirmations.unfollow.confirm": "Unfollow",
- "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
- "embed.instructions": "Embed this status on your website by copying the code below.",
- "embed.preview": "Here is what it will look like:",
+ "confirmations.unfollow.confirm": "Slutt å følge",
+ "confirmations.unfollow.message": "Er du sikker på at du vil slutte å følge {name}?",
+ "embed.instructions": "Kopier koden under for å bygge inn denne statusen på hjemmesiden din.",
+ "embed.preview": "Slik kommer det til å se ut:",
"emoji_button.activity": "Aktivitet",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Flagg",
"emoji_button.food": "Mat og drikke",
"emoji_button.label": "Sett inn emoji",
"emoji_button.nature": "Natur",
- "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+ "emoji_button.not_found": "Ingen emojojoer!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objekter",
"emoji_button.people": "Mennesker",
- "emoji_button.recent": "Frequently used",
+ "emoji_button.recent": "Hyppig brukt",
"emoji_button.search": "Søk...",
- "emoji_button.search_results": "Search results",
+ "emoji_button.search_results": "Søkeresultat",
"emoji_button.symbols": "Symboler",
"emoji_button.travel": "Reise & steder",
"empty_column.community": "Den lokale tidslinjen er tom. Skriv noe offentlig for å få snøballen til å rulle!",
"empty_column.hashtag": "Det er ingenting i denne hashtagen ennå.",
"empty_column.home": "Du har ikke fulgt noen ennå. Besøk {publlic} eller bruk søk for å komme i gang og møte andre brukere.",
"empty_column.home.public_timeline": "en offentlig tidslinje",
+ "empty_column.list": "Det er ikke noe i denne listen ennå.",
"empty_column.notifications": "Du har ingen varsler ennå. Kommuniser med andre for å begynne samtalen.",
"empty_column.public": "Det er ingenting her! Skriv noe offentlig, eller følg brukere manuelt fra andre instanser for å fylle den opp",
"follow_request.authorize": "Autorisér",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "Vis fremhevinger",
"home.column_settings.show_replies": "Vis svar",
"home.settings": "Kolonneinnstillinger",
+ "keyboard_shortcuts.back": "for å navigere tilbake",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "for å flytte ned i listen",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "for å favorittmarkere",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Lyntast",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "for å svare",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Lukk",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "Laster...",
"media_gallery.toggle_visible": "Veksle synlighet",
"missing_indicator.label": "Ikke funnet",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.blocks": "Blokkerte brukere",
"navigation_bar.community_timeline": "Lokal tidslinje",
"navigation_bar.edit_profile": "Rediger profil",
"navigation_bar.favourites": "Likt",
"navigation_bar.follow_requests": "Følgeforespørsler",
"navigation_bar.info": "Utvidet informasjon",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "Logg ut",
"navigation_bar.mutes": "Dempede brukere",
"navigation_bar.pins": "Pinned toots",
@@ -204,6 +241,7 @@
"tabs_bar.home": "Hjem",
"tabs_bar.local_timeline": "Lokal",
"tabs_bar.notifications": "Varslinger",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Dra og slipp for å laste opp",
"upload_button.label": "Legg til media",
"upload_form.description": "Describe for the visually impaired",
@@ -211,11 +249,11 @@
"upload_progress.label": "Laster opp...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
- "video.expand": "Expand video",
+ "video.expand": "Utvid video",
"video.fullscreen": "Full screen",
- "video.hide": "Hide video",
- "video.mute": "Mute sound",
+ "video.hide": "Skjul video",
+ "video.mute": "Skru av lyd",
"video.pause": "Pause",
- "video.play": "Play",
- "video.unmute": "Unmute sound"
+ "video.play": "Spill av",
+ "video.unmute": "Skru på lyd"
}
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 1e0849d95..0d1f7c971 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -7,20 +7,25 @@
"account.followers": "Seguidors",
"account.follows": "Abonaments",
"account.follows_you": "Vos sèc",
+ "account.hide_reblogs": "Rescondre los partages de @{name}",
"account.media": "Mèdias",
"account.mention": "Mencionar @{name}",
+ "account.moved_to": "{name} a mudat los catons a :",
"account.mute": "Rescondre @{name}",
+ "account.mute_notifications": "Rescondre las notificacions de @{name}",
"account.posts": "Estatuts",
"account.report": "Senhalar @{name}",
- "account.requested": "Invitacion mandada. Clicatz per anullar.",
+ "account.requested": "Invitacion mandada. Clicatz per anullar",
"account.share": "Partejar lo perfil a @{name}",
+ "account.show_reblogs": "Mostrar los partages de @{name}",
"account.unblock": "Desblocar @{name}",
"account.unblock_domain": "Desblocar {domain}",
"account.unfollow": "Quitar de sègre",
"account.unmute": "Quitar de rescondre @{name}",
+ "account.unmute_notifications": "Mostrar las notificacions de @{name}",
"account.view_full_profile": "Veire lo perfil complet",
"boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven",
- "bundle_column_error.body": "Quicòm a fach meuca pendent lo cargament d’aqueste compausant.",
+ "bundle_column_error.body": "Quicòm a fach mèuca pendent lo cargament d’aqueste compausant.",
"bundle_column_error.retry": "Tornar ensajar",
"bundle_column_error.title": "Error de ret",
"bundle_modal_error.close": "Tampar",
@@ -31,7 +36,8 @@
"column.favourites": "Favorits",
"column.follow_requests": "Demandas d’abonament",
"column.home": "Acuèlh",
- "column.mutes": "Personas en silenci",
+ "column.lists": "Listas",
+ "column.mutes": "Personas rescondudas",
"column.notifications": "Notificacions",
"column.pins": "Tuts penjats",
"column.public": "Flux public global",
@@ -55,16 +61,18 @@
"confirmation_modal.cancel": "Anullar",
"confirmations.block.confirm": "Blocar",
"confirmations.block.message": "Sètz segur de voler blocar {name} ?",
- "confirmations.delete.confirm": "Suprimir",
- "confirmations.delete.message": "Sètz segur de voler suprimir l’estatut ?",
+ "confirmations.delete.confirm": "Escafar",
+ "confirmations.delete.message": "Sètz segur de voler escafar l’estatut ?",
+ "confirmations.delete_list.confirm": "Suprimir",
+ "confirmations.delete_list.message": "Sètz segur de voler suprimir aquesta lista per totjorn ?",
"confirmations.domain_block.confirm": "Amagar tot lo domeni",
"confirmations.domain_block.message": "Sètz segur segur de voler blocar completament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
- "confirmations.mute.confirm": "Metre en silenci",
- "confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?",
+ "confirmations.mute.confirm": "Rescondre",
+ "confirmations.mute.message": "Sètz segur de voler rescondre {name} ?",
"confirmations.unfollow.confirm": "Quitar de sègre",
"confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?",
"embed.instructions": "Embarcar aqueste estatut per lo far veire sus un site Internet en copiar lo còdi çai-jos.",
- "embed.preview": "Semblarà aquò : ",
+ "embed.preview": "Semblarà aquò :",
"emoji_button.activity": "Activitats",
"emoji_button.custom": "Personalizats",
"emoji_button.flags": "Drapèus",
@@ -76,15 +84,16 @@
"emoji_button.people": "Gents",
"emoji_button.recent": "Sovent utilizats",
"emoji_button.search": "Cercar…",
- "emoji_button.search_results": "Resultat de recèrca",
+ "emoji_button.search_results": "Resultats de recèrca",
"emoji_button.symbols": "Simbòls",
"emoji_button.travel": "Viatges & lòcs",
"empty_column.community": "Lo flux public local es void. Escrivètz quicòm per lo garnir !",
- "empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag",
+ "empty_column.hashtag": "I a pas encara de contengut ligat a aquesta etiqueta.",
"empty_column.home": "Vòstre flux d’acuèlh es void. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.",
"empty_column.home.public_timeline": "lo flux public",
+ "empty_column.list": "I a pas res dins la lista pel moment. Quand de membres d’aquesta lista publiquen de novèls estatuts los veiretz aquí.",
"empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualqu’un per començar una conversacion.",
- "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public.",
+ "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public",
"follow_request.authorize": "Autorizar",
"follow_request.reject": "Regetar",
"getting_started.appsshort": "Apps",
@@ -98,27 +107,55 @@
"home.column_settings.show_reblogs": "Mostrar los partatges",
"home.column_settings.show_replies": "Mostrar las responsas",
"home.settings": "Paramètres de la colomna",
+ "keyboard_shortcuts.back": "per anar enrèire",
+ "keyboard_shortcuts.boost": "per partejar",
+ "keyboard_shortcuts.column": "per centrar un estatut a una colomna",
+ "keyboard_shortcuts.compose": "per anar al camp tèxte",
+ "keyboard_shortcuts.description": "Descripcion",
+ "keyboard_shortcuts.down": "per far davalar dins la lista",
+ "keyboard_shortcuts.enter": "per dobrir los estatuts",
+ "keyboard_shortcuts.favourite": "per apondre als favorits",
+ "keyboard_shortcuts.heading": "Acorchis clavièr",
+ "keyboard_shortcuts.hotkey": "Acorchis",
+ "keyboard_shortcuts.legend": "per mostrar aquesta legenda",
+ "keyboard_shortcuts.mention": "per mencionar l’autor",
+ "keyboard_shortcuts.reply": "per respondre",
+ "keyboard_shortcuts.search": "per anar a la recèrca",
+ "keyboard_shortcuts.toot": "per començar un estatut tot novèl",
+ "keyboard_shortcuts.unfocus": "per quitar lo camp tèxte/de recèrca",
+ "keyboard_shortcuts.up": "per far montar dins la lista",
"lightbox.close": "Tampar",
"lightbox.next": "Seguent",
"lightbox.previous": "Precedent",
+ "lists.account.add": "Ajustar a la lista",
+ "lists.account.remove": "Levar de la lista",
+ "lists.delete": "Suprimir la lista",
+ "lists.edit": "Modificar la lista",
+ "lists.new.create": "Ajustar una lista",
+ "lists.new.title_placeholder": "Títol de la nòva lista",
+ "lists.search": "Cercar demest lo monde que seguètz",
+ "lists.subheading": "Vòstras listas",
"loading_indicator.label": "Cargament…",
"media_gallery.toggle_visible": "Modificar la visibilitat",
"missing_indicator.label": "Pas trobat",
+ "mute_modal.hide_notifications": "Rescondre las notificacions d’aquesta persona ?",
"navigation_bar.blocks": "Personas blocadas",
"navigation_bar.community_timeline": "Flux public local",
"navigation_bar.edit_profile": "Modificar lo perfil",
"navigation_bar.favourites": "Favorits",
- "navigation_bar.follow_requests": "Demandas d'abonament",
+ "navigation_bar.follow_requests": "Demandas d’abonament",
"navigation_bar.info": "Mai informacions",
+ "navigation_bar.keyboard_shortcuts": "Acorchis clavièr",
+ "navigation_bar.lists": "Listas",
"navigation_bar.logout": "Desconnexion",
"navigation_bar.mutes": "Personas rescondudas",
"navigation_bar.pins": "Tuts penjats",
"navigation_bar.preferences": "Preferéncias",
"navigation_bar.public_timeline": "Flux public global",
- "notification.favourite": "{name} a ajustat a sos favorits :",
+ "notification.favourite": "{name} a ajustat a sos favorits",
"notification.follow": "{name} vos sèc",
- "notification.mention": "{name} vos a mencionat :",
- "notification.reblog": "{name} a partejat vòstre estatut :",
+ "notification.mention": "{name} vos a mencionat",
+ "notification.reblog": "{name} a partejat vòstre estatut",
"notifications.clear": "Escafar",
"notifications.clear_confirmation": "Volètz vertadièrament escafar totas vòstras las notificacions ?",
"notifications.column_settings.alert": "Notificacions localas",
@@ -134,8 +171,8 @@
"onboarding.next": "Seguent",
"onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de la gent que los de {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.",
"onboarding.page_four.home": "Lo flux d’acuèlh mòstra los estatuts del mond que seguètz.",
- "onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualqu’un interagís amb vos",
- "onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per bastir un malhum mai larg. Òm los apèla instàncias.",
+ "onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualqu’un interagís amb vos.",
+ "onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per construire un malhum mai larg. Òm los apèla instàncias.",
"onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}",
"onboarding.page_one.welcome": "Benvengut a Mastodon !",
"onboarding.page_six.admin": "Vòstre administrator d’instància es {admin}.",
@@ -159,11 +196,11 @@
"privacy.public.short": "Public",
"privacy.unlisted.long": "Mostrar pas dins los fluxes publics",
"privacy.unlisted.short": "Pas-listat",
- "relative_time.days": "fa {number}j",
- "relative_time.hours": "fa {number}h",
+ "relative_time.days": "fa {number} d",
+ "relative_time.hours": "fa {number} h",
"relative_time.just_now": "ara",
- "relative_time.minutes": "fa {number} minutas",
- "relative_time.seconds": "fa {number} segondas",
+ "relative_time.minutes": "fa {number} min",
+ "relative_time.seconds": "fa {number} s",
"reply_indicator.cancel": "Anullar",
"report.placeholder": "Comentaris addicionals",
"report.submit": "Mandar",
@@ -172,7 +209,7 @@
"search_popout.search_format": "Format recèrca avançada",
"search_popout.tips.hashtag": "etiqueta",
"search_popout.tips.status": "estatut",
- "search_popout.tips.text": "Tèxt brut tòrna escais, noms d’utilizaire e etiquetas correspondents",
+ "search_popout.tips.text": "Lo tèxt brut tòrna escais, noms d’utilizaire e etiquetas correspondents",
"search_popout.tips.user": "utilizaire",
"search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}",
"standalone.public_title": "Una ulhada dedins…",
@@ -188,7 +225,7 @@
"status.open": "Desplegar aqueste estatut",
"status.pin": "Penjar al perfil",
"status.reblog": "Partejar",
- "status.reblogged_by": "{name} a partejat :",
+ "status.reblogged_by": "{name} a partejat",
"status.reply": "Respondre",
"status.replyAll": "Respondre a la conversacion",
"status.report": "Senhalar @{name}",
@@ -197,13 +234,14 @@
"status.share": "Partejar",
"status.show_less": "Tornar plegar",
"status.show_more": "Desplegar",
- "status.unmute_conversation": "Conversacions amb silenci levat",
+ "status.unmute_conversation": "Tornar mostrar la conversacion",
"status.unpin": "Tirar del perfil",
"tabs_bar.compose": "Compausar",
"tabs_bar.federated_timeline": "Flux public global",
"tabs_bar.home": "Acuèlh",
"tabs_bar.local_timeline": "Flux public local",
"tabs_bar.notifications": "Notificacions",
+ "ui.beforeunload": "Vòstre brolhon serà perdut se quitatz Mastodon.",
"upload_area.title": "Lisatz e depausatz per mandar",
"upload_button.label": "Ajustar un mèdia",
"upload_form.description": "Descripcion pels mal vesents",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index c0776dfc9..9295ab937 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -7,17 +7,22 @@
"account.followers": "Śledzący",
"account.follows": "Śledzeni",
"account.follows_you": "Śledzi Cię",
+ "account.hide_reblogs": "Ukryj podbicia od @{name}",
"account.media": "Media",
"account.mention": "Wspomnij o @{name}",
+ "account.moved_to": "{name} przeniósł się do:",
"account.mute": "Wycisz @{name}",
+ "account.mute_notifications": "Wycisz powiadomienia o @{name}",
"account.posts": "Wpisy",
"account.report": "Zgłoś @{name}",
"account.requested": "Oczekująca prośba, kliknij aby anulować",
"account.share": "Udostępnij profil @{name}",
+ "account.show_reblogs": "Pokazuj podbicia od @{name}",
"account.unblock": "Odblokuj @{name}",
"account.unblock_domain": "Odblokuj domenę {domain}",
"account.unfollow": "Przestań śledzić",
"account.unmute": "Cofnij wyciszenie @{name}",
+ "account.unmute_notifications": "Cofnij wyciszenie powiadomień od @{name}",
"account.view_full_profile": "Wyświetl pełny profil",
"boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem",
"bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.",
@@ -31,6 +36,7 @@
"column.favourites": "Ulubione",
"column.follow_requests": "Prośby o śledzenie",
"column.home": "Strona główna",
+ "column.lists": "Listy",
"column.mutes": "Wyciszeni użytkownicy",
"column.notifications": "Powiadomienia",
"column.pins": "Przypięte wpisy",
@@ -57,6 +63,8 @@
"confirmations.block.message": "Czy na pewno chcesz zablokować {name}?",
"confirmations.delete.confirm": "Usuń",
"confirmations.delete.message": "Czy na pewno chcesz usunąć ten wpis?",
+ "confirmations.delete_list.confirm": "Usuń",
+ "confirmations.delete_list.message": "Czy na pewno chcesz bezpowrotnie usunąć tą listę?",
"confirmations.domain_block.confirm": "Ukryj wszysyko z domeny",
"confirmations.domain_block.message": "Czy na pewno chcesz zablokować całą domenę {domain}? Zwykle lepszym rozwiązaniem jest blokada lub wyciszenie kilku użytkowników.",
"confirmations.mute.confirm": "Wycisz",
@@ -83,13 +91,14 @@
"empty_column.hashtag": "Nie ma wpisów oznaczonych tym hashtagiem. Możesz napisać pierwszy!",
"empty_column.home": "Nie śledzisz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.",
"empty_column.home.public_timeline": "publiczna oś czasu",
+ "empty_column.list": "Nie ma nic na tej liście.",
"empty_column.notifications": "Nie masz żadnych powiadomień. Rozpocznij interakcje z innymi użytkownikami.",
"empty_column.public": "Tu nic nie ma! Napisz coś publicznie, lub dodaj ludzi z innych instancji, aby to wyświetlić.",
"follow_request.authorize": "Autoryzuj",
"follow_request.reject": "Odrzuć",
"getting_started.appsshort": "Aplikacje",
"getting_started.faq": "FAQ",
- "getting_started.heading": "Naucz się korzystać",
+ "getting_started.heading": "Rozpocznij",
"getting_started.open_source_notice": "Mastodon jest oprogramowaniem o otwartym źródle. Możesz pomóc w rozwoju lub zgłaszać błędy na GitHubie tutaj: {github}.",
"getting_started.userguide": "Podręcznik użytkownika",
"home.column_settings.advanced": "Zaawansowane",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "Pokazuj podbicia",
"home.column_settings.show_replies": "Pokazuj odpowiedzi",
"home.settings": "Ustawienia kolumny",
+ "keyboard_shortcuts.back": "aby cofnąć się",
+ "keyboard_shortcuts.boost": "aby podbić wpis",
+ "keyboard_shortcuts.column": "aby przejść do wpisu z jednej z kolumn",
+ "keyboard_shortcuts.compose": "aby przejść do pola tworzenia wpisu",
+ "keyboard_shortcuts.description": "Opis",
+ "keyboard_shortcuts.down": "aby przejść na dół listy",
+ "keyboard_shortcuts.enter": "aby otworzyć wpis",
+ "keyboard_shortcuts.favourite": "aby dodać do ulubionych",
+ "keyboard_shortcuts.heading": "Skróty klawiszowe",
+ "keyboard_shortcuts.hotkey": "Klawisz",
+ "keyboard_shortcuts.legend": "aby wyświetlić tą legendę",
+ "keyboard_shortcuts.mention": "aby wspomnieć o autorze",
+ "keyboard_shortcuts.reply": "aby odpowiedzieć",
+ "keyboard_shortcuts.search": "aby przejść do pola wyszukiwania",
+ "keyboard_shortcuts.toot": "aby utworzyć nowy wpis",
+ "keyboard_shortcuts.unfocus": "aby opuścić pole wyszukiwania/pisania",
+ "keyboard_shortcuts.up": "aby przejść na górę listy",
"lightbox.close": "Zamknij",
"lightbox.next": "Następne",
"lightbox.previous": "Poprzednie",
+ "lists.account.add": "Dodaj do listy",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Usuń listę",
+ "lists.edit": "Edytuj listę",
+ "lists.new.create": "Utwórz listę",
+ "lists.new.title_placeholder": "Wprowadź tytuł listy…",
+ "lists.search": "Szukaj wśród osób które śledzisz",
+ "lists.subheading": "Twoje listy",
"loading_indicator.label": "Ładowanie…",
"media_gallery.toggle_visible": "Przełącz widoczność",
"missing_indicator.label": "Nie znaleziono",
+ "mute_modal.hide_notifications": "Chcesz ukryć powiadomienia od tego użytkownika?",
"navigation_bar.blocks": "Zablokowani użytkownicy",
"navigation_bar.community_timeline": "Lokalna oś czasu",
"navigation_bar.edit_profile": "Edytuj profil",
"navigation_bar.favourites": "Ulubione",
"navigation_bar.follow_requests": "Prośby o śledzenie",
"navigation_bar.info": "Szczegółowe informacje",
+ "navigation_bar.keyboard_shortcuts": "Skróty klawiszowe",
+ "navigation_bar.lists": "Listy",
"navigation_bar.logout": "Wyloguj",
"navigation_bar.mutes": "Wyciszeni użytkownicy",
"navigation_bar.pins": "Przypięte wpisy",
@@ -159,11 +196,11 @@
"privacy.public.short": "Publiczny",
"privacy.unlisted.long": "Niewidoczny na publicznych osiach czasu",
"privacy.unlisted.short": "Niewidoczny",
- "relative_time.days": "{number}d",
- "relative_time.hours": "{number}h",
- "relative_time.just_now": "now",
- "relative_time.minutes": "{number}m",
- "relative_time.seconds": "{number}s",
+ "relative_time.days": "{number} dni",
+ "relative_time.hours": "{number} godz.",
+ "relative_time.just_now": "teraz",
+ "relative_time.minutes": "{number} min.",
+ "relative_time.seconds": "{number} s.",
"reply_indicator.cancel": "Anuluj",
"report.placeholder": "Dodatkowe komentarze",
"report.submit": "Wyślij",
@@ -174,7 +211,7 @@
"search_popout.tips.status": "wpis",
"search_popout.tips.text": "Proste wyszukiwanie pasujących pseudonimów, nazw użytkowników i hashtagów",
"search_popout.tips.user": "użytkownik",
- "search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}",
+ "search_results.total": "{count, number} {count, plural, one {wynik} few {wyniki} many {wyników} more {wyników}}",
"standalone.public_title": "Spojrzenie w głąb…",
"status.cannot_reblog": "Ten wpis nie może zostać podbity",
"status.delete": "Usuń",
@@ -204,6 +241,7 @@
"tabs_bar.home": "Strona główna",
"tabs_bar.local_timeline": "Lokalne",
"tabs_bar.notifications": "Powiadomienia",
+ "ui.beforeunload": "Utracisz tworzony wpis, jeżeli opuścisz Mastodona.",
"upload_area.title": "Przeciągnij i upuść aby wysłać",
"upload_button.label": "Dodaj zawartość multimedialną",
"upload_form.description": "Wprowadź opis dla niewidomych i niedowidzących",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index ddb8b83f5..70632846c 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -7,17 +7,22 @@
"account.followers": "Seguidores",
"account.follows": "Segue",
"account.follows_you": "Segue você",
+ "account.hide_reblogs": "Esconder compartilhamentos de @{name}",
"account.media": "Mídia",
"account.mention": "Mencionar @{name}",
+ "account.moved_to": "{name} se mudou para:",
"account.mute": "Silenciar @{name}",
+ "account.mute_notifications": "Silenciar notificações de @{name}",
"account.posts": "Posts",
"account.report": "Denunciar @{name}",
- "account.requested": "Aguardando aprovação. Clique para cancelar a solicitação.",
+ "account.requested": "Aguardando aprovação. Clique para cancelar a solicitação",
"account.share": "Compartilhar perfil de @{name}",
+ "account.show_reblogs": "Mostra compartilhamentos de @{name}",
"account.unblock": "Desbloquear @{name}",
"account.unblock_domain": "Desbloquear {domain}",
"account.unfollow": "Deixar de seguir",
"account.unmute": "Não silenciar @{name}",
+ "account.unmute_notifications": "Retirar silêncio das notificações vindas de @{name}",
"account.view_full_profile": "Ver perfil completo",
"boost_modal.combo": "Você pode pressionar {combo} para ignorar este diálogo na próxima vez",
"bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.",
@@ -31,6 +36,7 @@
"column.favourites": "Favoritos",
"column.follow_requests": "Seguidores pendentes",
"column.home": "Página inicial",
+ "column.lists": "Listas",
"column.mutes": "Usuários silenciados",
"column.notifications": "Notificações",
"column.pins": "Postagens fixadas",
@@ -57,13 +63,15 @@
"confirmations.block.message": "Você tem certeza de que quer bloquear {name}?",
"confirmations.delete.confirm": "Excluir",
"confirmations.delete.message": "Você tem certeza de que quer excluir esta postagem?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Você tem certeza que quer deletar permanentemente a lista?",
"confirmations.domain_block.confirm": "Esconder o domínio inteiro",
"confirmations.domain_block.message": "Você quer mesmo bloquear {domain} inteiro? Na maioria dos casos, silenciar ou bloquear alguns usuários é o suficiente e o recomendado.",
"confirmations.mute.confirm": "Silenciar",
"confirmations.mute.message": "Você tem certeza de que quer silenciar {name}?",
"confirmations.unfollow.confirm": "Deixar de seguir",
"confirmations.unfollow.message": "Você tem certeza de que quer deixar de seguir {name}?",
- "embed.instructions": "Incorpore esta postagem em seu site copiando o código abaixo:",
+ "embed.instructions": "Incorpore esta postagem em seu site copiando o código abaixo.",
"embed.preview": "Aqui está uma previsão de como ficará:",
"emoji_button.activity": "Atividades",
"emoji_button.custom": "Customizados",
@@ -80,11 +88,12 @@
"emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viagens & Lugares",
"empty_column.community": "A timeline local está vazia. Escreva algo publicamente para começar!",
- "empty_column.hashtag": "Ainda não há qualquer conteúdo com essa hashtag",
+ "empty_column.hashtag": "Ainda não há qualquer conteúdo com essa hashtag.",
"empty_column.home": "Você ainda não segue usuário algo. Visite a timeline {public} ou use o buscador para procurar e conhecer outros usuários.",
"empty_column.home.public_timeline": "global",
- "empty_column.notifications": "Você ainda não possui notificações. Interaja com outros usuários para começar a conversar!",
- "empty_column.public": "Não há nada aqui! Escreva algo publicamente ou siga manualmente usuários de outras instâncias.",
+ "empty_column.list": "Ainda não há nada nesta lista.",
+ "empty_column.notifications": "Você ainda não possui notificações. Interaja com outros usuários para começar a conversar.",
+ "empty_column.public": "Não há nada aqui! Escreva algo publicamente ou siga manualmente usuários de outras instâncias",
"follow_request.authorize": "Autorizar",
"follow_request.reject": "Rejeitar",
"getting_started.appsshort": "Apps",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "Mostrar compartilhamentos",
"home.column_settings.show_replies": "Mostrar as respostas",
"home.settings": "Configurações de colunas",
+ "keyboard_shortcuts.back": "para navegar de volta",
+ "keyboard_shortcuts.boost": "para compartilhar",
+ "keyboard_shortcuts.column": "Focar um status em uma das colunas",
+ "keyboard_shortcuts.compose": "para focar a área de redação",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "para mover para baixo na lista",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "para adicionar aos favoritos",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Atalho",
+ "keyboard_shortcuts.legend": "para mostrar essa legenda",
+ "keyboard_shortcuts.mention": "para mencionar o autor",
+ "keyboard_shortcuts.reply": "para responder",
+ "keyboard_shortcuts.search": "para focar a pesquisa",
+ "keyboard_shortcuts.toot": "para compor um novo toot",
+ "keyboard_shortcuts.unfocus": "para remover o foco da área de composição/pesquisa",
+ "keyboard_shortcuts.up": "para mover para cima na lista",
"lightbox.close": "Fechar",
"lightbox.next": "Próximo",
"lightbox.previous": "Anterior",
+ "lists.account.add": "Adicionar a listas",
+ "lists.account.remove": "Remover da lista",
+ "lists.delete": "Delete list",
+ "lists.edit": "Editar lista",
+ "lists.new.create": "Adicionar lista",
+ "lists.new.title_placeholder": "Novo título da lista",
+ "lists.search": "Procurar entre as pessoas que você segue",
+ "lists.subheading": "Suas listas",
"loading_indicator.label": "Carregando...",
"media_gallery.toggle_visible": "Esconder/Mostrar",
"missing_indicator.label": "Não encontrado",
+ "mute_modal.hide_notifications": "Esconder notificações deste usuário?",
"navigation_bar.blocks": "Usuários bloqueados",
"navigation_bar.community_timeline": "Local",
"navigation_bar.edit_profile": "Editar perfil",
"navigation_bar.favourites": "Favoritos",
"navigation_bar.follow_requests": "Seguidores pendentes",
"navigation_bar.info": "Mais informações",
+ "navigation_bar.keyboard_shortcuts": "Atalhos de teclado",
+ "navigation_bar.lists": "Listas",
"navigation_bar.logout": "Sair",
"navigation_bar.mutes": "Usuários silenciados",
"navigation_bar.pins": "Postagens fixadas",
@@ -140,7 +177,7 @@
"onboarding.page_one.welcome": "Seja bem-vindo(a) ao Mastodon!",
"onboarding.page_six.admin": "O administrador de sua instância é {admin}.",
"onboarding.page_six.almost_done": "Quase acabando...",
- "onboarding.page_six.appetoot": "Bon Appetoot!",
+ "onboarding.page_six.appetoot": "Bom Apetoot!",
"onboarding.page_six.apps_available": "Há {apps} disponíveis para iOS, Android e outras plataformas.",
"onboarding.page_six.github": "Mastodon é um software gratuito e de código aberto. Você pode reportar bugs, prequisitar novas funções ou contribuir para o código no {github}.",
"onboarding.page_six.guidelines": "diretrizes da comunidade",
@@ -161,7 +198,7 @@
"privacy.unlisted.short": "Não listada",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
- "relative_time.just_now": "now",
+ "relative_time.just_now": "agora",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Cancelar",
@@ -183,7 +220,7 @@
"status.load_more": "Carregar mais",
"status.media_hidden": "Mídia escondida",
"status.mention": "Mencionar @{name}",
- "status.more": "More",
+ "status.more": "Mais",
"status.mute_conversation": "Silenciar conversa",
"status.open": "Expandir",
"status.pin": "Fixar no perfil",
@@ -204,6 +241,7 @@
"tabs_bar.home": "Página inicial",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notificações",
+ "ui.beforeunload": "Seu rascunho será perdido se você sair do Mastodon.",
"upload_area.title": "Arraste e solte para enviar",
"upload_button.label": "Adicionar mídia",
"upload_form.description": "Descreva a imagem para deficientes visuais",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index 9ae140df9..15d5deb93 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -1,118 +1,155 @@
{
"account.block": "Bloquear @{name}",
- "account.block_domain": "Hide everything from {domain}",
- "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+ "account.block_domain": "Esconder tudo do domínio {domain}",
+ "account.disclaimer_full": "As informações abaixo podem refletir o perfil do usuário de forma incompleta.",
"account.edit_profile": "Editar perfil",
"account.follow": "Seguir",
"account.followers": "Seguidores",
"account.follows": "Segue",
"account.follows_you": "É teu seguidor",
+ "account.hide_reblogs": "Esconder partilhas de @{name}",
"account.media": "Media",
"account.mention": "Mencionar @{name}",
+ "account.moved_to": "{name} mudou a sua conta para:",
"account.mute": "Silenciar @{name}",
+ "account.mute_notifications": "Silenciar notificações de @{name}",
"account.posts": "Posts",
"account.report": "Denunciar @{name}",
"account.requested": "A aguardar aprovação",
- "account.share": "Share @{name}'s profile",
+ "account.share": "Partilhar o perfil @{name}",
+ "account.show_reblogs": "Mostrar partilhas de @{name}",
"account.unblock": "Não bloquear @{name}",
- "account.unblock_domain": "Unhide {domain}",
+ "account.unblock_domain": "Mostrar {domain}",
"account.unfollow": "Deixar de seguir",
"account.unmute": "Não silenciar @{name}",
- "account.view_full_profile": "View full profile",
+ "account.unmute_notifications": "Deixar de silenciar @{name}",
+ "account.view_full_profile": "Ver perfil completo",
"boost_modal.combo": "Pode clicar {combo} para não voltar a ver",
- "bundle_column_error.body": "Something went wrong while loading this component.",
- "bundle_column_error.retry": "Try again",
- "bundle_column_error.title": "Network error",
- "bundle_modal_error.close": "Close",
- "bundle_modal_error.message": "Something went wrong while loading this component.",
- "bundle_modal_error.retry": "Try again",
+ "bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.",
+ "bundle_column_error.retry": "Tente de novo",
+ "bundle_column_error.title": "Erro de rede",
+ "bundle_modal_error.close": "Fechar",
+ "bundle_modal_error.message": "Algo de errado aconteceu enquanto este componente era carregado.",
+ "bundle_modal_error.retry": "Tente de novo",
"column.blocks": "Utilizadores Bloqueados",
"column.community": "Local",
"column.favourites": "Favoritos",
"column.follow_requests": "Seguidores Pendentes",
"column.home": "Home",
+ "column.lists": "Listas",
"column.mutes": "Utilizadores silenciados",
"column.notifications": "Notificações",
"column.pins": "Pinned toot",
"column.public": "Global",
"column_back_button.label": "Voltar",
- "column_header.hide_settings": "Hide settings",
- "column_header.moveLeft_settings": "Move column to the left",
- "column_header.moveRight_settings": "Move column to the right",
- "column_header.pin": "Pin",
- "column_header.show_settings": "Show settings",
- "column_header.unpin": "Unpin",
- "column_subheading.navigation": "Navigation",
- "column_subheading.settings": "Settings",
- "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
- "compose_form.lock_disclaimer.lock": "locked",
+ "column_header.hide_settings": "Esconder preferências",
+ "column_header.moveLeft_settings": "Mover coluna para a esquerda",
+ "column_header.moveRight_settings": "Mover coluna para a direita",
+ "column_header.pin": "Fixar",
+ "column_header.show_settings": "Mostrar preferências",
+ "column_header.unpin": "Remover fixar",
+ "column_subheading.navigation": "Navegação",
+ "column_subheading.settings": "Preferências",
+ "compose_form.lock_disclaimer": "A tua conta não está {locked}. Qualquer pessoa pode seguir-te e ver as publicações direcionadas apenas a seguidores.",
+ "compose_form.lock_disclaimer.lock": "bloqueada",
"compose_form.placeholder": "Em que estás a pensar?",
"compose_form.publish": "Publicar",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marcar media como conteúdo sensível",
"compose_form.spoiler": "Esconder texto com aviso",
"compose_form.spoiler_placeholder": "Aviso de conteúdo",
- "confirmation_modal.cancel": "Cancel",
+ "confirmation_modal.cancel": "Cancelar",
"confirmations.block.confirm": "Block",
- "confirmations.block.message": "Are you sure you want to block {name}?",
- "confirmations.delete.confirm": "Delete",
- "confirmations.delete.message": "Are you sure you want to delete this status?",
- "confirmations.domain_block.confirm": "Hide entire domain",
- "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
- "confirmations.mute.confirm": "Mute",
- "confirmations.mute.message": "Are you sure you want to mute {name}?",
- "confirmations.unfollow.confirm": "Unfollow",
- "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
- "embed.instructions": "Embed this status on your website by copying the code below.",
- "embed.preview": "Here is what it will look like:",
- "emoji_button.activity": "Activity",
- "emoji_button.custom": "Custom",
- "emoji_button.flags": "Flags",
- "emoji_button.food": "Food & Drink",
+ "confirmations.block.message": "De certeza que queres bloquear {name}?",
+ "confirmations.delete.confirm": "Eliminar",
+ "confirmations.delete.message": "De certeza que queres eliminar esta publicação?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Tens a certeza de que desejas apagar permanentemente esta lista?",
+ "confirmations.domain_block.confirm": "Esconder tudo deste domínio",
+ "confirmations.domain_block.message": "De certeza que queres bloquear por completo o domínio {domain}? Na maioria dos casos, silenciar ou bloquear alguns utilizadores é o suficiente e o recomendado.",
+ "confirmations.mute.confirm": "Silenciar",
+ "confirmations.mute.message": "De certeza que queres silenciar {name}?",
+ "confirmations.unfollow.confirm": "Deixar de seguir",
+ "confirmations.unfollow.message": "De certeza que queres deixar de seguir {name}?",
+ "embed.instructions": "Publicar este post num outro site copiando o código abaixo.",
+ "embed.preview": "Podes ver aqui como irá ficar:",
+ "emoji_button.activity": "Actividade",
+ "emoji_button.custom": "Especiais",
+ "emoji_button.flags": "Bandeiras",
+ "emoji_button.food": "Comida & Bebida",
"emoji_button.label": "Inserir Emoji",
- "emoji_button.nature": "Nature",
- "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
- "emoji_button.objects": "Objects",
- "emoji_button.people": "People",
- "emoji_button.recent": "Frequently used",
- "emoji_button.search": "Search...",
- "emoji_button.search_results": "Search results",
- "emoji_button.symbols": "Symbols",
- "emoji_button.travel": "Travel & Places",
- "empty_column.community": "Ainda não existem conteúdo local para mostrar!",
- "empty_column.hashtag": "Ainda não existe qualquer conteúdo com essa hashtag",
+ "emoji_button.nature": "Natureza",
+ "emoji_button.not_found": "Não tem emojos!! (╯°□°)╯︵ ┻━┻",
+ "emoji_button.objects": "Objectos",
+ "emoji_button.people": "Pessoas",
+ "emoji_button.recent": "Regularmente utilizados",
+ "emoji_button.search": "Procurar...",
+ "emoji_button.search_results": "Resultados da pesquisa",
+ "emoji_button.symbols": "Símbolos",
+ "emoji_button.travel": "Viagens & Lugares",
+ "empty_column.community": "Ainda não existe conteúdo local para mostrar!",
+ "empty_column.hashtag": "Não foram encontradas publicações com essa hashtag.",
"empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.",
"empty_column.home.public_timeline": "global",
+ "empty_column.list": "Ainda não existem publicações nesta lista.",
"empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.",
- "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.",
+ "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos",
"follow_request.authorize": "Autorizar",
"follow_request.reject": "Rejeitar",
- "getting_started.appsshort": "Apps",
+ "getting_started.appsshort": "Aplicações",
"getting_started.faq": "FAQ",
"getting_started.heading": "Primeiros passos",
"getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}.",
- "getting_started.userguide": "User Guide",
+ "getting_started.userguide": "Guia do utilizador",
"home.column_settings.advanced": "Avançado",
"home.column_settings.basic": "Básico",
"home.column_settings.filter_regex": "Filtrar com uma expressão regular",
"home.column_settings.show_reblogs": "Mostrar as partilhas",
"home.column_settings.show_replies": "Mostrar as respostas",
"home.settings": "Parâmetros da listagem",
+ "keyboard_shortcuts.back": "para navegar de volta",
+ "keyboard_shortcuts.boost": "para partilhar",
+ "keyboard_shortcuts.column": "para focar uma publicação numa das colunas",
+ "keyboard_shortcuts.compose": "para focar na área de publicação",
+ "keyboard_shortcuts.description": "Descrição",
+ "keyboard_shortcuts.down": "para mover para baixo na lista",
+ "keyboard_shortcuts.enter": "para expandir uma publicação",
+ "keyboard_shortcuts.favourite": "para adicionar aos favoritos",
+ "keyboard_shortcuts.heading": "Atalhos do teclado",
+ "keyboard_shortcuts.hotkey": "Atalho",
+ "keyboard_shortcuts.legend": "para mostrar esta legenda",
+ "keyboard_shortcuts.mention": "para mencionar o autor",
+ "keyboard_shortcuts.reply": "para responder",
+ "keyboard_shortcuts.search": "para focar na pesquisa",
+ "keyboard_shortcuts.toot": "para compor um novo post",
+ "keyboard_shortcuts.unfocus": "para remover o foco da área de publicação/pesquisa",
+ "keyboard_shortcuts.up": "para mover para cima na lista",
"lightbox.close": "Fechar",
- "lightbox.next": "Next",
- "lightbox.previous": "Previous",
- "loading_indicator.label": "Carregando...",
+ "lightbox.next": "Próximo",
+ "lightbox.previous": "Anterior",
+ "lists.account.add": "Adicionar à lista",
+ "lists.account.remove": "Remover da lista",
+ "lists.delete": "Delete list",
+ "lists.edit": "Editar lista",
+ "lists.new.create": "Adicionar lista",
+ "lists.new.title_placeholder": "Novo título da lista",
+ "lists.search": "Pesquisa entre as pessoas que segues",
+ "lists.subheading": "As tuas listas",
+ "loading_indicator.label": "A carregar...",
"media_gallery.toggle_visible": "Esconder/Mostrar",
"missing_indicator.label": "Não encontrado",
+ "mute_modal.hide_notifications": "Esconder notificações deste utilizador?",
"navigation_bar.blocks": "Utilizadores bloqueados",
"navigation_bar.community_timeline": "Local",
"navigation_bar.edit_profile": "Editar perfil",
"navigation_bar.favourites": "Favoritos",
"navigation_bar.follow_requests": "Seguidores pendentes",
"navigation_bar.info": "Mais informações",
+ "navigation_bar.keyboard_shortcuts": "Atalhos de teclado",
+ "navigation_bar.lists": "Listas",
"navigation_bar.logout": "Sair",
"navigation_bar.mutes": "Utilizadores silenciados",
- "navigation_bar.pins": "Pinned toots",
+ "navigation_bar.pins": "Posts fixos",
"navigation_bar.preferences": "Preferências",
"navigation_bar.public_timeline": "Global",
"notification.favourite": "{name} adicionou o teu post aos favoritos",
@@ -125,31 +162,31 @@
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.follow": "Novos seguidores:",
"notifications.column_settings.mention": "Menções:",
- "notifications.column_settings.push": "Push notifications",
- "notifications.column_settings.push_meta": "This device",
+ "notifications.column_settings.push": "Notificações Push",
+ "notifications.column_settings.push_meta": "Este dispositivo",
"notifications.column_settings.reblog": "Partilhas:",
"notifications.column_settings.show": "Mostrar nas colunas",
"notifications.column_settings.sound": "Reproduzir som",
- "onboarding.done": "Done",
- "onboarding.next": "Next",
- "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
- "onboarding.page_four.home": "The home timeline shows posts from people you follow.",
- "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
- "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
- "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}",
- "onboarding.page_one.welcome": "Welcome to Mastodon!",
- "onboarding.page_six.admin": "Your instance's admin is {admin}.",
- "onboarding.page_six.almost_done": "Almost done...",
+ "onboarding.done": "Pronto",
+ "onboarding.next": "Próximo",
+ "onboarding.page_five.public_timelines": "A timeline local mostra as publicações de todos os utilizadores em {domain}. A timeline global mostra as publicações de todas as pessoas que pessoas em {domain} seguem. Estas são as timelines públicas, uma óptima forma de conhecer novas pessoas.",
+ "onboarding.page_four.home": "A timeline home mostra as publicações de pessoas que tu segues.",
+ "onboarding.page_four.notifications": "A coluna de notificações mostra-te quando alguém interage contigo.",
+ "onboarding.page_one.federation": "Mastodon é uma rede de servidores independentes ligados entre si para fazer uma grande rede social. Nós chamamos instâncias a estes servidores.",
+ "onboarding.page_one.handle": "Tu estás em {domain}, por isso o teu endereço completo de utilizador é {handle}",
+ "onboarding.page_one.welcome": "Bem-vindo(a) ao Mastodon!",
+ "onboarding.page_six.admin": "O administrador da tua instância é {admin}.",
+ "onboarding.page_six.almost_done": "Quase pronto...",
"onboarding.page_six.appetoot": "Bon Appetoot!",
- "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
- "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
- "onboarding.page_six.guidelines": "community guidelines",
- "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
- "onboarding.page_six.various_app": "mobile apps",
- "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
- "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
- "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
- "onboarding.skip": "Skip",
+ "onboarding.page_six.apps_available": "Existem {apps} disponíveis para iOS, Android e outras plataformas.",
+ "onboarding.page_six.github": "Mastodon é um software gratuito e de código aberto. Podes reportar bugs, solicitar novas funcionalidades e contribuir para o código em {github}.",
+ "onboarding.page_six.guidelines": "termos de utilização da comunidade",
+ "onboarding.page_six.read_guidelines": "Por favor, lê os {guidelines} de {domain}!",
+ "onboarding.page_six.various_app": "aplicações de telemóvel",
+ "onboarding.page_three.profile": "Edita o teu perfil para mudar a tua imagem, biografia e nome. Lá encontrarás também outras preferências que podes personalizar.",
+ "onboarding.page_three.search": "Utiliza a caixa de pesquisa para procurar pessoas ou hashtags, exemplo {illustration} / {introductions}. Para procurar uma pessoa que não está nesta instância, utiliza o endereço completo.",
+ "onboarding.page_two.compose": "Escreve posts na coluna de publicações. Podes publicar imagens, alterar a privacidade e adicionar alertas de conteúdo usando os ícones abaixo da caixa de composição.",
+ "onboarding.skip": "Saltar",
"privacy.change": "Ajustar a privacidade da mensagem",
"privacy.direct.long": "Apenas para utilizadores mencionados",
"privacy.direct.short": "Directo",
@@ -161,7 +198,7 @@
"privacy.unlisted.short": "Não listar",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
- "relative_time.just_now": "now",
+ "relative_time.just_now": "agora",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Cancelar",
@@ -169,53 +206,54 @@
"report.submit": "Enviar",
"report.target": "Denunciar",
"search.placeholder": "Pesquisar",
- "search_popout.search_format": "Advanced search format",
+ "search_popout.search_format": "Formato avançado de pesquisa",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
- "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
- "search_popout.tips.user": "user",
+ "search_popout.tips.text": "O texto simples retorna a correspondência de nomes, utilizadores e hashtags",
+ "search_popout.tips.user": "utilizador",
"search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
- "standalone.public_title": "A look inside...",
- "status.cannot_reblog": "This post cannot be boosted",
+ "standalone.public_title": "Espreitar lá dentro...",
+ "status.cannot_reblog": "Este post não pode ser partilhado",
"status.delete": "Eliminar",
- "status.embed": "Embed",
+ "status.embed": "Incorporar",
"status.favourite": "Adicionar aos favoritos",
"status.load_more": "Carregar mais",
"status.media_hidden": "Media escondida",
"status.mention": "Mencionar @{name}",
- "status.more": "More",
- "status.mute_conversation": "Mute conversation",
+ "status.more": "Mais",
+ "status.mute_conversation": "Silenciar conversa",
"status.open": "Expandir",
"status.pin": "Pin on profile",
"status.reblog": "Partilhar",
"status.reblogged_by": "{name} partilhou",
"status.reply": "Responder",
- "status.replyAll": "Reply to thread",
- "status.report": "Denúnciar @{name}",
+ "status.replyAll": "Responder à conversa",
+ "status.report": "Denunciar @{name}",
"status.sensitive_toggle": "Clique para ver",
"status.sensitive_warning": "Conteúdo sensível",
"status.share": "Share",
"status.show_less": "Mostrar menos",
"status.show_more": "Mostrar mais",
- "status.unmute_conversation": "Unmute conversation",
- "status.unpin": "Unpin from profile",
+ "status.unmute_conversation": "Deixar de silenciar esta conversa",
+ "status.unpin": "Não fixar no perfil",
"tabs_bar.compose": "Criar",
"tabs_bar.federated_timeline": "Global",
"tabs_bar.home": "Home",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notificações",
+ "ui.beforeunload": "O teu rascunho vai ser perdido se abandonares o Mastodon.",
"upload_area.title": "Arraste e solte para enviar",
"upload_button.label": "Adicionar media",
- "upload_form.description": "Describe for the visually impaired",
+ "upload_form.description": "Descrição da imagem para pessoas com dificuldades visuais",
"upload_form.undo": "Anular",
"upload_progress.label": "A gravar...",
- "video.close": "Close video",
- "video.exit_fullscreen": "Exit full screen",
- "video.expand": "Expand video",
+ "video.close": "Fechar vídeo",
+ "video.exit_fullscreen": "Sair de full screen",
+ "video.expand": "Expandir vídeo",
"video.fullscreen": "Full screen",
- "video.hide": "Hide video",
- "video.mute": "Mute sound",
- "video.pause": "Pause",
- "video.play": "Play",
- "video.unmute": "Unmute sound"
+ "video.hide": "Esconder vídeo",
+ "video.mute": "Silenciar",
+ "video.pause": "Pausar",
+ "video.play": "Reproduzir",
+ "video.unmute": "Remover de silêncio"
}
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 104b063f5..e9925b675 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -7,17 +7,22 @@
"account.followers": "Подписаны",
"account.follows": "Подписки",
"account.follows_you": "Подписан(а) на Вас",
+ "account.hide_reblogs": "Hide boosts from @{name}",
"account.media": "Медиаконтент",
"account.mention": "Упомянуть",
+ "account.moved_to": "{name} has moved to:",
"account.mute": "Заглушить",
+ "account.mute_notifications": "Mute notifications from @{name}",
"account.posts": "Посты",
"account.report": "Пожаловаться",
"account.requested": "Ожидает подтверждения",
"account.share": "Поделиться профилем @{name}",
+ "account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "Разблокировать",
"account.unblock_domain": "Разблокировать {domain}",
"account.unfollow": "Отписаться",
"account.unmute": "Снять глушение",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "Показать полный профиль",
"boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз",
"bundle_column_error.body": "Что-то пошло не так при загрузке этого компонента.",
@@ -31,6 +36,7 @@
"column.favourites": "Понравившееся",
"column.follow_requests": "Запросы на подписку",
"column.home": "Главная",
+ "column.lists": "Lists",
"column.mutes": "Список глушения",
"column.notifications": "Уведомления",
"column.pins": "Pinned toot",
@@ -57,32 +63,35 @@
"confirmations.block.message": "Вы уверены, что хотите заблокировать {name}?",
"confirmations.delete.confirm": "Удалить",
"confirmations.delete.message": "Вы уверены, что хотите удалить этот статус?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Блокировать весь домен",
"confirmations.domain_block.message": "Вы на самом деле уверены, что хотите блокировать весь {domain}? В большинстве случаев нескольких отдельных блокировок или глушений достаточно.",
"confirmations.mute.confirm": "Заглушить",
"confirmations.mute.message": "Вы уверены, что хотите заглушить {name}?",
"confirmations.unfollow.confirm": "Отписаться",
"confirmations.unfollow.message": "Вы уверены, что хотите отписаться от {name}?",
- "embed.instructions": "Embed this status on your website by copying the code below.",
- "embed.preview": "Here is what it will look like:",
+ "embed.instructions": "Встройте этот статус на Вашем сайте, скопировав код внизу.",
+ "embed.preview": "Так это будет выглядеть:",
"emoji_button.activity": "Занятия",
- "emoji_button.custom": "Custom",
+ "emoji_button.custom": "Собственные",
"emoji_button.flags": "Флаги",
"emoji_button.food": "Еда и напитки",
"emoji_button.label": "Вставить эмодзи",
"emoji_button.nature": "Природа",
- "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+ "emoji_button.not_found": "Нет эмодзи!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Предметы",
"emoji_button.people": "Люди",
- "emoji_button.recent": "Frequently used",
+ "emoji_button.recent": "Последние",
"emoji_button.search": "Найти...",
- "emoji_button.search_results": "Search results",
+ "emoji_button.search_results": "Результаты поиска",
"emoji_button.symbols": "Символы",
"emoji_button.travel": "Путешествия",
"empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!",
"empty_column.hashtag": "Статусов с таким хэштегом еще не существует.",
"empty_column.home": "Пока Вы ни на кого не подписаны. Полистайте {public} или используйте поиск, чтобы освоиться и завести новые знакомства.",
"empty_column.home.public_timeline": "публичные ленты",
+ "empty_column.list": "There is nothing in this list yet.",
"empty_column.notifications": "У Вас еще нет уведомлений. Заведите знакомство с другими пользователями, чтобы начать разговор.",
"empty_column.public": "Здесь ничего нет! Опубликуйте что-нибудь или подпишитесь на пользователей с других узлов, чтобы заполнить ленту.",
"follow_request.authorize": "Авторизовать",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "Показывать продвижения",
"home.column_settings.show_replies": "Показывать ответы",
"home.settings": "Настройки колонки",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Закрыть",
"lightbox.next": "Далее",
"lightbox.previous": "Назад",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "Загрузка...",
"media_gallery.toggle_visible": "Показать/скрыть",
"missing_indicator.label": "Не найдено",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.blocks": "Список блокировки",
"navigation_bar.community_timeline": "Локальная лента",
"navigation_bar.edit_profile": "Изменить профиль",
"navigation_bar.favourites": "Понравившееся",
"navigation_bar.follow_requests": "Запросы на подписку",
"navigation_bar.info": "Об узле",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "Выйти",
"navigation_bar.mutes": "Список глушения",
"navigation_bar.pins": "Pinned toots",
@@ -159,34 +196,34 @@
"privacy.public.short": "Публичный",
"privacy.unlisted.long": "Не показывать в лентах",
"privacy.unlisted.short": "Скрытый",
- "relative_time.days": "{number}d",
- "relative_time.hours": "{number}h",
- "relative_time.just_now": "now",
- "relative_time.minutes": "{number}m",
- "relative_time.seconds": "{number}s",
+ "relative_time.days": "{number}д",
+ "relative_time.hours": "{number}ч",
+ "relative_time.just_now": "только что",
+ "relative_time.minutes": "{number}м",
+ "relative_time.seconds": "{number}с",
"reply_indicator.cancel": "Отмена",
"report.placeholder": "Комментарий",
"report.submit": "Отправить",
"report.target": "Жалуемся на",
"search.placeholder": "Поиск",
- "search_popout.search_format": "Advanced search format",
- "search_popout.tips.hashtag": "hashtag",
- "search_popout.tips.status": "status",
- "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
- "search_popout.tips.user": "user",
+ "search_popout.search_format": "Продвинутый формат поиска",
+ "search_popout.tips.hashtag": "хэштег",
+ "search_popout.tips.status": "статус",
+ "search_popout.tips.text": "Простой ввод текста покажет совпадающие имена пользователей, отображаемые имена и хэштеги",
+ "search_popout.tips.user": "пользователь",
"search_results.total": "{count, number} {count, plural, one {результат} few {результата} many {результатов} other {результатов}}",
- "standalone.public_title": "A look inside...",
+ "standalone.public_title": "Прямо сейчас",
"status.cannot_reblog": "Этот статус не может быть продвинут",
"status.delete": "Удалить",
- "status.embed": "Embed",
+ "status.embed": "Встроить",
"status.favourite": "Нравится",
"status.load_more": "Показать еще",
"status.media_hidden": "Медиаконтент скрыт",
"status.mention": "Упомянуть @{name}",
- "status.more": "More",
+ "status.more": "Больше",
"status.mute_conversation": "Заглушить тред",
"status.open": "Развернуть статус",
- "status.pin": "Pin on profile",
+ "status.pin": "Закрепить в профиле",
"status.reblog": "Продвинуть",
"status.reblogged_by": "{name} продвинул(а)",
"status.reply": "Ответить",
@@ -194,28 +231,29 @@
"status.report": "Пожаловаться",
"status.sensitive_toggle": "Нажмите для просмотра",
"status.sensitive_warning": "Чувствительный контент",
- "status.share": "Share",
+ "status.share": "Поделиться",
"status.show_less": "Свернуть",
"status.show_more": "Развернуть",
"status.unmute_conversation": "Снять глушение с треда",
- "status.unpin": "Unpin from profile",
+ "status.unpin": "Открепить от профиля",
"tabs_bar.compose": "Написать",
"tabs_bar.federated_timeline": "Глобальная",
"tabs_bar.home": "Главная",
"tabs_bar.local_timeline": "Локальная",
"tabs_bar.notifications": "Уведомления",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Перетащите сюда, чтобы загрузить",
"upload_button.label": "Добавить медиаконтент",
- "upload_form.description": "Describe for the visually impaired",
+ "upload_form.description": "Описать для людей с нарушениями зрения",
"upload_form.undo": "Отменить",
"upload_progress.label": "Загрузка...",
- "video.close": "Close video",
- "video.exit_fullscreen": "Exit full screen",
- "video.expand": "Expand video",
- "video.fullscreen": "Full screen",
- "video.hide": "Hide video",
- "video.mute": "Mute sound",
- "video.pause": "Pause",
- "video.play": "Play",
- "video.unmute": "Unmute sound"
+ "video.close": "Закрыть видео",
+ "video.exit_fullscreen": "Покинуть полноэкранный режим",
+ "video.expand": "Развернуть видео",
+ "video.fullscreen": "Полноэкранный режим",
+ "video.hide": "Скрыть видео",
+ "video.mute": "Заглушить звук",
+ "video.pause": "Пауза",
+ "video.play": "Пуск",
+ "video.unmute": "Включить звук"
}
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index 70beb70f7..9d9646509 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -7,17 +7,22 @@
"account.followers": "Följare",
"account.follows": "Följer",
"account.follows_you": "Följer dig",
+ "account.hide_reblogs": "Hide boosts from @{name}",
"account.media": "Media",
"account.mention": "Nämna @{name}",
+ "account.moved_to": "{name} has moved to:",
"account.mute": "Tysta @{name}",
+ "account.mute_notifications": "Mute notifications from @{name}",
"account.posts": "Inlägg",
"account.report": "Rapportera @{name}",
"account.requested": "Inväntar godkännande. Klicka för att avbryta följförfrågan",
"account.share": "Dela @{name}'s profil",
+ "account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "Avblockera @{name}",
"account.unblock_domain": "Ta fram {domain}",
"account.unfollow": "Sluta följa",
"account.unmute": "Ta bort tystad @{name}",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "Visa hela profilen",
"boost_modal.combo": "Du kan trycka {combo} för att slippa denna nästa gång",
"bundle_column_error.body": "Något gick fel när du laddade denna komponent.",
@@ -31,6 +36,7 @@
"column.favourites": "Favoriter",
"column.follow_requests": "Följ förfrågningar",
"column.home": "Hem",
+ "column.lists": "Lists",
"column.mutes": "Tystade användare",
"column.notifications": "Meddelanden",
"column.pins": "Nålade toots",
@@ -57,6 +63,8 @@
"confirmations.block.message": "Är du säker att du vill blockera {name}?",
"confirmations.delete.confirm": "Ta bort",
"confirmations.delete.message": "Är du säker att du vill ta bort denna status?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Blockera hela domänen",
"confirmations.domain_block.message": "Är du verkligen, verkligen säker på att du vill blockera hela {domain}? I de flesta fall är några riktade blockeringar eller nedtystade tillräckligt och föredras.",
"confirmations.mute.confirm": "Tysta",
@@ -82,8 +90,8 @@
"empty_column.community": "Den lokala tidslinjen är tom. Skriv något offentligt för att få bollen att rulla!",
"empty_column.hashtag": "Det finns inget i denna hashtag ännu.",
"empty_column.home": "Din hemma-tidslinje är tom! Besök {public} eller använd sökning för att komma igång och träffa andra användare.",
- "empty_column.home.inactivity": "Ditt hemmafeed är tomt. Om du har varit inaktiv ett tag kommer det att regenereras för dig snart.",
"empty_column.home.public_timeline": "den publika tidslinjen",
+ "empty_column.list": "There is nothing in this list yet.",
"empty_column.notifications": "Du har inga meddelanden än. Interagera med andra för att starta konversationen.",
"empty_column.public": "Det finns inget här! Skriv något offentligt, eller följ manuellt användarna från andra instanser för att fylla på det",
"follow_request.authorize": "Godkänn",
@@ -99,22 +107,49 @@
"home.column_settings.show_reblogs": "Visa knuffar",
"home.column_settings.show_replies": "Visa svar",
"home.settings": "Kolumninställningar",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Stäng",
"lightbox.next": "Nästa",
"lightbox.previous": "Tidigare",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "Laddar...",
"media_gallery.toggle_visible": "Växla synlighet",
"missing_indicator.label": "Hittades inte",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.blocks": "Blockerade användare",
"navigation_bar.community_timeline": "Lokal tidslinje",
"navigation_bar.edit_profile": "Redigera profil",
"navigation_bar.favourites": "Favoriter",
"navigation_bar.follow_requests": "Följförfrågningar",
"navigation_bar.info": "Om denna instans",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "Logga ut",
"navigation_bar.mutes": "Tystade användare",
"navigation_bar.pins": "Nålade inlägg (toots)",
-
"navigation_bar.preferences": "Inställningar",
"navigation_bar.public_timeline": "Förenad tidslinje",
"notification.favourite": "{name} favoriserade din status",
@@ -161,6 +196,11 @@
"privacy.public.short": "Publik",
"privacy.unlisted.long": "Skicka inte till publik tidslinje",
"privacy.unlisted.short": "Olistad",
+ "relative_time.days": "{number}d",
+ "relative_time.hours": "{number}h",
+ "relative_time.just_now": "now",
+ "relative_time.minutes": "{number}m",
+ "relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Ångra",
"report.placeholder": "Ytterligare kommentarer",
"report.submit": "Skicka",
@@ -180,6 +220,7 @@
"status.load_more": "Ladda fler",
"status.media_hidden": "Media dold",
"status.mention": "Omnämn @{name}",
+ "status.more": "More",
"status.mute_conversation": "Tysta konversation",
"status.open": "Utvidga denna status",
"status.pin": "Fäst i profil",
@@ -200,6 +241,7 @@
"tabs_bar.home": "Hem",
"tabs_bar.local_timeline": "Lokal",
"tabs_bar.notifications": "Meddelanden",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Dra & släpp för att ladda upp",
"upload_button.label": "Lägg till media",
"upload_form.description": "Beskriv för synskadade",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index db3f3dd0d..cc18a6096 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -7,17 +7,22 @@
"account.followers": "Followers",
"account.follows": "Follows",
"account.follows_you": "Follows you",
+ "account.hide_reblogs": "Hide boosts from @{name}",
"account.media": "Media",
"account.mention": "Mention @{name}",
+ "account.moved_to": "{name} has moved to:",
"account.mute": "Mute @{name}",
+ "account.mute_notifications": "Mute notifications from @{name}",
"account.posts": "Posts",
"account.report": "Report @{name}",
"account.requested": "Awaiting approval",
"account.share": "Share @{name}'s profile",
+ "account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "Unblock @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Unfollow",
"account.unmute": "Unmute @{name}",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "View full profile",
"boost_modal.combo": "You can press {combo} to skip this next time",
"bundle_column_error.body": "Something went wrong while loading this component.",
@@ -31,6 +36,7 @@
"column.favourites": "Favourites",
"column.follow_requests": "Follow requests",
"column.home": "Home",
+ "column.lists": "Lists",
"column.mutes": "Muted users",
"column.notifications": "Notifications",
"column.pins": "Pinned toot",
@@ -57,6 +63,8 @@
"confirmations.block.message": "Are you sure you want to block {name}?",
"confirmations.delete.confirm": "Delete",
"confirmations.delete.message": "Are you sure you want to delete this status?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Mute",
@@ -83,6 +91,7 @@
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
"empty_column.home.public_timeline": "the public timeline",
+ "empty_column.list": "There is nothing in this list yet.",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
"follow_request.authorize": "Authorize",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Close",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.blocks": "Blocked users",
"navigation_bar.community_timeline": "Local timeline",
"navigation_bar.edit_profile": "Edit profile",
"navigation_bar.favourites": "Favourites",
"navigation_bar.follow_requests": "Follow requests",
"navigation_bar.info": "About this instance",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "Logout",
"navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
@@ -204,6 +241,7 @@
"tabs_bar.home": "Home",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notifications",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Drag & drop to upload",
"upload_button.label": "Add media",
"upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index cdd3581da..c51f3e417 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -7,17 +7,22 @@
"account.followers": "Takipçiler",
"account.follows": "Takip ettikleri",
"account.follows_you": "Seni takip ediyor",
+ "account.hide_reblogs": "Hide boosts from @{name}",
"account.media": "Media",
"account.mention": "Bahset @{name}",
+ "account.moved_to": "{name} has moved to:",
"account.mute": "Sustur @{name}",
+ "account.mute_notifications": "Mute notifications from @{name}",
"account.posts": "Gönderiler",
"account.report": "Rapor et @{name}",
"account.requested": "Onay bekleniyor",
"account.share": "Share @{name}'s profile",
+ "account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "Engeli kaldır @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unfollow": "Takipten vazgeç",
"account.unmute": "Sesi aç @{name}",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "View full profile",
"boost_modal.combo": "Bir dahaki sefere {combo} tuşuna basabilirsiniz",
"bundle_column_error.body": "Something went wrong while loading this component.",
@@ -31,6 +36,7 @@
"column.favourites": "Favoriler",
"column.follow_requests": "Takip istekleri",
"column.home": "Anasayfa",
+ "column.lists": "Lists",
"column.mutes": "Susturulmuş kullanıcılar",
"column.notifications": "Bildirimler",
"column.pins": "Pinned toot",
@@ -57,6 +63,8 @@
"confirmations.block.message": "{name} kullanıcısını engellemek istiyor musunuz?",
"confirmations.delete.confirm": "Sil",
"confirmations.delete.message": "Bu gönderiyi silmek istiyor musunuz?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Sessize al",
@@ -83,6 +91,7 @@
"empty_column.hashtag": "Henüz bu hashtag’e sahip hiçbir gönderi yok.",
"empty_column.home": "Henüz kimseyi takip etmiyorsunuz. {public} ziyaret edebilir veya arama kısmını kullanarak diğer kullanıcılarla iletişime geçebilirsiniz.",
"empty_column.home.public_timeline": "herkese açık zaman tüneli",
+ "empty_column.list": "There is nothing in this list yet.",
"empty_column.notifications": "Henüz hiçbir bildiriminiz yok. Diğer insanlarla sobhet edebilmek için etkileşime geçebilirsiniz.",
"empty_column.public": "Burada hiçbir gönderi yok! Herkese açık bir şeyler yazın, veya diğer sunucudaki insanları takip ederek bu alanın dolmasını sağlayın",
"follow_request.authorize": "Yetkilendir",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "Boost edilenleri göster",
"home.column_settings.show_replies": "Cevapları göster",
"home.settings": "Kolon ayarları",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Kapat",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "Yükleniyor...",
"media_gallery.toggle_visible": "Görünürlüğü değiştir",
"missing_indicator.label": "Bulunamadı",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.blocks": "Engellenen kullanıcılar",
"navigation_bar.community_timeline": "Yerel zaman tüneli",
"navigation_bar.edit_profile": "Profili düzenle",
"navigation_bar.favourites": "Favoriler",
"navigation_bar.follow_requests": "Takip istekleri",
"navigation_bar.info": "Genişletilmiş bilgi",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "Çıkış",
"navigation_bar.mutes": "Sessize alınmış kullanıcılar",
"navigation_bar.pins": "Pinned toots",
@@ -204,6 +241,7 @@
"tabs_bar.home": "Ana sayfa",
"tabs_bar.local_timeline": "Yerel",
"tabs_bar.notifications": "Bildirimler",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Upload için sürükle bırak yapınız",
"upload_button.label": "Görsel ekle",
"upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 930529f15..86c0ce76d 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -7,17 +7,22 @@
"account.followers": "Підписники",
"account.follows": "Підписки",
"account.follows_you": "Підписаний(-а) на Вас",
+ "account.hide_reblogs": "Hide boosts from @{name}",
"account.media": "Медія",
"account.mention": "Згадати",
+ "account.moved_to": "{name} has moved to:",
"account.mute": "Заглушити",
+ "account.mute_notifications": "Mute notifications from @{name}",
"account.posts": "Пости",
"account.report": "Поскаржитися",
"account.requested": "Очікує підтвердження",
"account.share": "Share @{name}'s profile",
+ "account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "Розблокувати",
"account.unblock_domain": "Розблокувати {domain}",
"account.unfollow": "Відписатися",
"account.unmute": "Зняти глушення",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "View full profile",
"boost_modal.combo": "Ви можете натиснути {combo}, щоб пропустити це наступного разу",
"bundle_column_error.body": "Something went wrong while loading this component.",
@@ -31,6 +36,7 @@
"column.favourites": "Вподобане",
"column.follow_requests": "Запити на підписку",
"column.home": "Головна",
+ "column.lists": "Lists",
"column.mutes": "Заглушені користувачі",
"column.notifications": "Сповіщення",
"column.pins": "Pinned toot",
@@ -57,6 +63,8 @@
"confirmations.block.message": "Ви впевнені, що хочете заблокувати {name}?",
"confirmations.delete.confirm": "Видалити",
"confirmations.delete.message": "Ви впевнені, що хочете видалити цей допис?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Сховати весь домен",
"confirmations.domain_block.message": "Ви точно, точно впевнені, що хочете заблокувати весь домен {domain}? У більшості випадків для нормальної роботи краще заблокувати/заглушити лише деяких користувачів.",
"confirmations.mute.confirm": "Заглушити",
@@ -83,6 +91,7 @@
"empty_column.hashtag": "Дописів з цим хештегом поки не існує.",
"empty_column.home": "Ви поки ні на кого не підписані. Погортайте {public}, або скористуйтесь пошуком, щоб освоїтися та познайомитися з іншими користувачами.",
"empty_column.home.public_timeline": "публічні стрічки",
+ "empty_column.list": "There is nothing in this list yet.",
"empty_column.notifications": "У вас ще немає сповіщень. Переписуйтесь з іншими користувачами, щоб почати розмову.",
"empty_column.public": "Тут поки нічого немає! Опублікуйте щось, або вручну підпишіться на користувачів інших інстанцій, щоб заповнити стрічку.",
"follow_request.authorize": "Авторизувати",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "Показувати передмухи",
"home.column_settings.show_replies": "Показувати відповіді",
"home.settings": "Налаштування колонок",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Закрити",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "Завантаження...",
"media_gallery.toggle_visible": "Показати/приховати",
"missing_indicator.label": "Не знайдено",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.blocks": "Заблоковані користувачі",
"navigation_bar.community_timeline": "Локальна стрічка",
"navigation_bar.edit_profile": "Редагувати профіль",
"navigation_bar.favourites": "Вподобане",
"navigation_bar.follow_requests": "Запити на підписку",
"navigation_bar.info": "Про інстанцію",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "Вийти",
"navigation_bar.mutes": "Заглушені користувачі",
"navigation_bar.pins": "Pinned toots",
@@ -204,6 +241,7 @@
"tabs_bar.home": "Головна",
"tabs_bar.local_timeline": "Локальна",
"tabs_bar.notifications": "Сповіщення",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Перетягніть сюди, щоб завантажити",
"upload_button.label": "Додати медіаконтент",
"upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/whitelist_gl.json b/app/javascript/mastodon/locales/whitelist_gl.json
new file mode 100644
index 000000000..0d4f101c7
--- /dev/null
+++ b/app/javascript/mastodon/locales/whitelist_gl.json
@@ -0,0 +1,2 @@
+[
+]
diff --git a/app/javascript/mastodon/locales/whitelist_sv.json b/app/javascript/mastodon/locales/whitelist_sv.json
new file mode 100644
index 000000000..0d4f101c7
--- /dev/null
+++ b/app/javascript/mastodon/locales/whitelist_sv.json
@@ -0,0 +1,2 @@
+[
+]
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 827c815cf..9be6a9f73 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -1,117 +1,154 @@
{
"account.block": "屏蔽 @{name}",
- "account.block_domain": "隐藏一切来自 {domain} 的嘟文",
- "account.disclaimer_full": "下列资料不一定完整。",
+ "account.block_domain": "隐藏来自 {domain} 的内容",
+ "account.disclaimer_full": "此处显示的信息可能不是全部内容。",
"account.edit_profile": "修改个人资料",
"account.follow": "关注",
"account.followers": "关注者",
- "account.follows": "正关注",
- "account.follows_you": "关注你",
+ "account.follows": "正在关注",
+ "account.follows_you": "关注了你",
+ "account.hide_reblogs": "隐藏来自 @{name} 的转嘟",
"account.media": "媒体",
"account.mention": "提及 @{name}",
- "account.mute": "将 @{name} 静音",
+ "account.moved_to": "{name} 已经迁移到:",
+ "account.mute": "隐藏 @{name}",
+ "account.mute_notifications": "隐藏来自 @{name} 的通知",
"account.posts": "嘟文",
"account.report": "举报 @{name}",
- "account.requested": "等待审批",
- "account.share": "分享 @{name}的个人资料",
- "account.unblock": "解除对 @{name} 的屏蔽",
- "account.unblock_domain": "不再隐藏 {domain}",
+ "account.requested": "正在等待对方同意。点击以取消发送关注请求",
+ "account.share": "分享 @{name} 的个人资料",
+ "account.show_reblogs": "显示来自 @{name} 的转嘟",
+ "account.unblock": "不再屏蔽 @{name}",
+ "account.unblock_domain": "不再隐藏来自 {domain} 的内容",
"account.unfollow": "取消关注",
- "account.unmute": "取消 @{name} 的静音",
+ "account.unmute": "不再隐藏 @{name}",
+ "account.unmute_notifications": "不再隐藏来自 @{name} 的通知",
"account.view_full_profile": "查看完整资料",
- "boost_modal.combo": "如你想在下次路过时显示,请按{combo},",
- "bundle_column_error.body": "载入组件出错。",
+ "boost_modal.combo": "下次按住 {combo} 即可跳过此提示",
+ "bundle_column_error.body": "载入这个组件时发生了错误。",
"bundle_column_error.retry": "重试",
"bundle_column_error.title": "网络错误",
"bundle_modal_error.close": "关闭",
- "bundle_modal_error.message": "载入组件出错。",
+ "bundle_modal_error.message": "载入这个组件时发生了错误。",
"bundle_modal_error.retry": "重试",
"column.blocks": "屏蔽用户",
"column.community": "本站时间轴",
"column.favourites": "收藏过的嘟文",
"column.follow_requests": "关注请求",
"column.home": "主页",
- "column.mutes": "被静音的用户",
+ "column.lists": "列表",
+ "column.mutes": "被隐藏的用户",
"column.notifications": "通知",
"column.pins": "置顶嘟文",
"column.public": "跨站公共时间轴",
"column_back_button.label": "返回",
"column_header.hide_settings": "隐藏设置",
- "column_header.moveLeft_settings": "将栏左移",
- "column_header.moveRight_settings": "将栏右移",
+ "column_header.moveLeft_settings": "将此栏左移",
+ "column_header.moveRight_settings": "将此栏右移",
"column_header.pin": "固定",
"column_header.show_settings": "显示设置",
- "column_header.unpin": "取下",
+ "column_header.unpin": "取消固定",
"column_subheading.navigation": "导航",
"column_subheading.settings": "设置",
- "compose_form.lock_disclaimer": "你的帐户没 {locked}. 任何人可以通过关注你来查看只有关注者可见的嘟文.",
- "compose_form.lock_disclaimer.lock": "被保护",
+ "compose_form.lock_disclaimer": "你的帐户没有{locked}。任何人都可以在关注你后立即查看仅关注者可见的嘟文。",
+ "compose_form.lock_disclaimer.lock": "开启保护",
"compose_form.placeholder": "在想啥?",
"compose_form.publish": "嘟嘟",
"compose_form.publish_loud": "{publish}!",
- "compose_form.sensitive": "将媒体文件标示为“敏感内容”",
- "compose_form.spoiler": "将部分文本藏于警告消息之后",
- "compose_form.spoiler_placeholder": "敏感内容的警告消息",
+ "compose_form.sensitive": "将媒体文件标记为敏感内容",
+ "compose_form.spoiler": "折叠嘟文内容",
+ "compose_form.spoiler_placeholder": "折叠部分的警告消息",
"confirmation_modal.cancel": "取消",
"confirmations.block.confirm": "屏蔽",
- "confirmations.block.message": "想好了,真的要屏蔽 {name}?",
+ "confirmations.block.message": "你确定要屏蔽 {name} 吗?",
"confirmations.delete.confirm": "删除",
- "confirmations.delete.message": "想好了,真的要删除这条嘟文?",
- "confirmations.domain_block.confirm": "隐藏整个网站",
- "confirmations.domain_block.message": "你真的真的确定要隐藏整个 {domain} ?多数情况下,封锁或静音几个特定目标就好。",
- "confirmations.mute.confirm": "静音",
- "confirmations.mute.message": "想好了,真的要静音 {name}?",
+ "confirmations.delete.message": "你确定要删除这条嘟文吗?",
+ "confirmations.delete_list.confirm": "删除",
+ "confirmations.delete_list.message": "你确定要永久删除这个列表吗?",
+ "confirmations.domain_block.confirm": "隐藏整个网站的内容",
+ "confirmations.domain_block.message": "你真的确定要隐藏所有来自 {domain} 的内容吗?多数情况下,屏蔽或隐藏几个特定的用户应该就能满足你的需要了。",
+ "confirmations.mute.confirm": "隐藏",
+ "confirmations.mute.message": "你确定要隐藏 {name} 吗?",
"confirmations.unfollow.confirm": "取消关注",
- "confirmations.unfollow.message": "确定要取消关注 {name}吗?",
- "embed.instructions": "要内嵌此嘟文,请将以下代码贴进你的网站。",
- "embed.preview": "到时大概长这样:",
+ "confirmations.unfollow.message": "你确定要取消关注 {name} 吗?",
+ "embed.instructions": "要在你的网站上嵌入这条嘟文,请复制以下代码。",
+ "embed.preview": "它会像这样显示出来:",
"emoji_button.activity": "活动",
- "emoji_button.custom": "Custom",
+ "emoji_button.custom": "自定义",
"emoji_button.flags": "旗帜",
"emoji_button.food": "食物和饮料",
"emoji_button.label": "加入表情符号",
"emoji_button.nature": "自然",
- "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+ "emoji_button.not_found": "木有这个表情符号!(╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "物体",
"emoji_button.people": "人物",
- "emoji_button.recent": "Frequently used",
+ "emoji_button.recent": "常用",
"emoji_button.search": "搜索…",
- "emoji_button.search_results": "Search results",
+ "emoji_button.search_results": "搜索结果",
"emoji_button.symbols": "符号",
- "emoji_button.travel": "旅途和地点",
- "empty_column.community": "本站时间轴暂时未有内容,快嘟几个来抢头香啊!",
- "empty_column.hashtag": "这个标签暂时未有内容。",
+ "emoji_button.travel": "旅行和地点",
+ "empty_column.community": "本站时间轴暂时没有内容,快嘟几个来抢头香啊!",
+ "empty_column.hashtag": "这个话题标签下暂时没有内容。",
"empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。",
"empty_column.home.public_timeline": "公共时间轴",
- "empty_column.notifications": "你没有任何通知纪录,快向其他用户搭讪吧。",
- "empty_column.public": "跨站公共时间轴暂时没有内容!快写一些公共的嘟文,或者关注另一些服务器实例的用户吧!你和本站、友站的交流,将决定这里出现的内容。",
- "follow_request.authorize": "批准",
+ "empty_column.list": "这个列表中暂时没有内容。列表中用户所发送的的新嘟文将会在这里显示。",
+ "empty_column.notifications": "你还没有收到过任何通知,快向其他用户搭讪吧。",
+ "empty_column.public": "这里神马都没有!写一些公开的嘟文,或者关注其他实例的用户后,这里就会有嘟文出现了哦!",
+ "follow_request.authorize": "同意",
"follow_request.reject": "拒绝",
- "getting_started.appsshort": "Apps",
- "getting_started.faq": "FAQ",
+ "getting_started.appsshort": "应用",
+ "getting_started.faq": "常见问题",
"getting_started.heading": "开始使用",
- "getting_started.open_source_notice": "Mastodon 是一个开放源码的软件。你可以在官方 GitHub ({github}) 贡献或者回报问题。",
+ "getting_started.open_source_notice": "Mastodon 是一个开源软件。欢迎前往 GitHub({github})贡献代码或反馈问题。",
"getting_started.userguide": "用户指南",
- "home.column_settings.advanced": "高端",
- "home.column_settings.basic": "基本",
- "home.column_settings.filter_regex": "使用正则表达式 (regex) 过滤",
- "home.column_settings.show_reblogs": "显示被转的嘟文",
- "home.column_settings.show_replies": "显示回应嘟文",
- "home.settings": "字段设置",
+ "home.column_settings.advanced": "高级设置",
+ "home.column_settings.basic": "基本设置",
+ "home.column_settings.filter_regex": "使用正则表达式(regex)过滤",
+ "home.column_settings.show_reblogs": "显示转嘟",
+ "home.column_settings.show_replies": "显示回复",
+ "home.settings": "栏目设置",
+ "keyboard_shortcuts.back": "返回上一页",
+ "keyboard_shortcuts.boost": "转嘟",
+ "keyboard_shortcuts.column": "选择第 X 栏中的嘟文",
+ "keyboard_shortcuts.compose": "选择嘟文撰写框",
+ "keyboard_shortcuts.description": "说明",
+ "keyboard_shortcuts.down": "在列表中让光标下移",
+ "keyboard_shortcuts.enter": "展开嘟文",
+ "keyboard_shortcuts.favourite": "收藏嘟文",
+ "keyboard_shortcuts.heading": "快捷键列表",
+ "keyboard_shortcuts.hotkey": "快捷键",
+ "keyboard_shortcuts.legend": "显示此列表",
+ "keyboard_shortcuts.mention": "提及嘟文作者",
+ "keyboard_shortcuts.reply": "回复嘟文",
+ "keyboard_shortcuts.search": "选择搜索框",
+ "keyboard_shortcuts.toot": "发送新嘟文",
+ "keyboard_shortcuts.unfocus": "取消输入",
+ "keyboard_shortcuts.up": "在列表中让光标上移",
"lightbox.close": "关闭",
"lightbox.next": "下一步",
"lightbox.previous": "上一步",
+ "lists.account.add": "添加到列表",
+ "lists.account.remove": "从列表中删除",
+ "lists.delete": "删除列表",
+ "lists.edit": "编辑列表",
+ "lists.new.create": "新建列表",
+ "lists.new.title_placeholder": "新列表的标题",
+ "lists.search": "搜索你关注的人",
+ "lists.subheading": "你的列表",
"loading_indicator.label": "加载中……",
- "media_gallery.toggle_visible": "打开或关上",
+ "media_gallery.toggle_visible": "切换显示/隐藏",
"missing_indicator.label": "找不到内容",
+ "mute_modal.hide_notifications": "同时隐藏来自这个用户的通知",
"navigation_bar.blocks": "被屏蔽的用户",
"navigation_bar.community_timeline": "本站时间轴",
"navigation_bar.edit_profile": "修改个人资料",
"navigation_bar.favourites": "收藏的内容",
"navigation_bar.follow_requests": "关注请求",
"navigation_bar.info": "关于本站",
+ "navigation_bar.keyboard_shortcuts": "快捷键列表",
+ "navigation_bar.lists": "列表",
"navigation_bar.logout": "注销",
- "navigation_bar.mutes": "被静音的用户",
+ "navigation_bar.mutes": "被隐藏的用户",
"navigation_bar.pins": "置顶嘟文",
"navigation_bar.preferences": "首选项",
"navigation_bar.public_timeline": "跨站公共时间轴",
@@ -119,103 +156,104 @@
"notification.follow": "{name} 开始关注你",
"notification.mention": "{name} 提及你",
"notification.reblog": "{name} 转嘟了你的嘟文",
- "notifications.clear": "清空通知纪录",
- "notifications.clear_confirmation": "你确定要清空通知纪录吗?",
- "notifications.column_settings.alert": "显示桌面通知",
- "notifications.column_settings.favourite": "你的嘟文被收藏:",
- "notifications.column_settings.follow": "关注你:",
- "notifications.column_settings.mention": "提及你:",
+ "notifications.clear": "清空通知列表",
+ "notifications.clear_confirmation": "你确定要永久清空通知列表吗?",
+ "notifications.column_settings.alert": "桌面通知",
+ "notifications.column_settings.favourite": "当你的嘟文被收藏时:",
+ "notifications.column_settings.follow": "当有人关注你时:",
+ "notifications.column_settings.mention": "当有人在嘟文中提及你时:",
"notifications.column_settings.push": "推送通知",
"notifications.column_settings.push_meta": "此设备",
- "notifications.column_settings.reblog": "你的嘟文被转嘟:",
+ "notifications.column_settings.reblog": "当有人转嘟了你的嘟文时:",
"notifications.column_settings.show": "在通知栏显示",
"notifications.column_settings.sound": "播放音效",
"onboarding.done": "出发!",
"onboarding.next": "下一步",
- "onboarding.page_five.public_timelines": "本站时间轴显示来自 {domain} 的所有人的公共嘟文。 跨站公共时间轴显示 {domain} 上的各位关注的来自所有Mastodon服务器实例上的人发表的公共嘟文。这些就是寻人好去处的公共时间轴啦。",
- "onboarding.page_four.home": "你的主时间轴上是你关注的用户的嘟文.",
- "onboarding.page_four.notifications": "如果你和他人产生了互动,便会出现在通知列上啦~",
- "onboarding.page_one.federation": "Mastodon是由一系列独立的服务器共同打造的强大的社交网络,我们将这些独立但又相互连接的服务器叫做服务器实例。",
- "onboarding.page_one.handle": "你在 {domain}, {handle} 就是你的完整帐户名称。",
- "onboarding.page_one.welcome": "欢迎来到 Mastodon!",
- "onboarding.page_six.admin": "{admin} 是你所在服务器实例的管理员.",
- "onboarding.page_six.almost_done": "差不多了…",
- "onboarding.page_six.appetoot": "嗷呜~",
- "onboarding.page_six.apps_available": "也有适用于 iOS, Android 和其它平台的 {apps} 咯~",
- "onboarding.page_six.github": "Mastodon 是自由的开放源代码软件。欢迎来 {github} 报告问题,提交功能请求,或者贡献代码 :-)",
+ "onboarding.page_five.public_timelines": "本站时间轴显示的是由本站({domain})用户发布的所有公开嘟文。跨站公共时间轴显示的的是由本站用户关注对象所发布的所有公开嘟文。这些就是寻人好去处的公共时间轴啦。",
+ "onboarding.page_four.home": "你的主页时间轴上显示的是你的关注对象所发布的嘟文。",
+ "onboarding.page_four.notifications": "如果有人与你互动了,他们就会出现在通知栏中哦~",
+ "onboarding.page_one.federation": "Mastodon 是由一系列独立的服务器共同打造的强大的社交网络,我们将这些各自独立而又相互连接的服务器叫做实例。",
+ "onboarding.page_one.handle": "你是在 {domain} 上注册的,所以你的完整用户地址是 {handle}。",
+ "onboarding.page_one.welcome": "欢迎来到 Mastodon!",
+ "onboarding.page_six.admin": "{admin} 是你所在服务器实例的管理员。",
+ "onboarding.page_six.almost_done": "差不多了……",
+ "onboarding.page_six.appetoot": "嗷呜~",
+ "onboarding.page_six.apps_available": "我们还有适用于 iOS、Android 和其它平台的{apps}哦~",
+ "onboarding.page_six.github": "Mastodon 是自由的开源软件。欢迎前往 {github} 反馈问题、提出对新功能的建议或贡献代码 :-)",
"onboarding.page_six.guidelines": "社区指南",
- "onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的 {guidelines}!",
- "onboarding.page_six.various_app": "移动应用程序",
- "onboarding.page_three.profile": "修改你的个人资料,比如头像、简介、和昵称等等。在那还可以找到其它首选项。",
- "onboarding.page_three.search": "用搜索来找人和标签吧,比如 {illustration} 或者 {introductions}。想找其它服务器实例上的人,用完整帐户名称(用户名@域名)啦。",
- "onboarding.page_two.compose": "从这里开始嘟!上面的按钮提供了上传图片,修改隐私设置和提示敏感内容等多种功能。.",
- "onboarding.skip": "好啦好啦我知道啦",
- "privacy.change": "调整隐私设置",
- "privacy.direct.long": "只有提及的用户能看到",
- "privacy.direct.short": "私人消息",
- "privacy.private.long": "只有关注你用户能看到",
- "privacy.private.short": "关注者",
- "privacy.public.long": "在公共时间轴显示",
- "privacy.public.short": "公共",
- "privacy.unlisted.long": "公开,但不在公共时间轴显示",
- "privacy.unlisted.short": "公开",
- "relative_time.days": "{number}d",
- "relative_time.hours": "{number}h",
- "relative_time.just_now": "now",
- "relative_time.minutes": "{number}m",
- "relative_time.seconds": "{number}s",
+ "onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的{guidelines}!",
+ "onboarding.page_six.various_app": "移动设备应用",
+ "onboarding.page_three.profile": "你可以修改你的个人资料,比如头像、简介和昵称等偏好设置。",
+ "onboarding.page_three.search": "你可以通过搜索功能寻找用户和话题标签,比如{illustration}或者{introductions}。如果你想搜索其他实例上的用户,就需要输入完整用户地址(@用户名@域名)哦。",
+ "onboarding.page_two.compose": "在撰写栏中开始嘟嘟吧!下方的按钮分别可以用来上传图片、修改嘟文可见范围,以及添加警告信息。",
+ "onboarding.skip": "跳过",
+ "privacy.change": "设置嘟文可见范围",
+ "privacy.direct.long": "只有被提及的用户能看到",
+ "privacy.direct.short": "私信",
+ "privacy.private.long": "只有关注你的用户能看到",
+ "privacy.private.short": "仅关注者",
+ "privacy.public.long": "所有人可见,并会出现在公共时间轴上",
+ "privacy.public.short": "公开",
+ "privacy.unlisted.long": "所有人可见,但不会出现在公共时间轴上",
+ "privacy.unlisted.short": "不公开",
+ "relative_time.days": "{number}天",
+ "relative_time.hours": "{number}时",
+ "relative_time.just_now": "刚刚",
+ "relative_time.minutes": "{number}分",
+ "relative_time.seconds": "{number}秒",
"reply_indicator.cancel": "取消",
- "report.placeholder": "额外消息",
+ "report.placeholder": "附言",
"report.submit": "提交",
- "report.target": "Reporting",
+ "report.target": "举报 {target}",
"search.placeholder": "搜索",
- "search_popout.search_format": "Advanced search format",
- "search_popout.tips.hashtag": "hashtag",
- "search_popout.tips.status": "status",
- "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
- "search_popout.tips.user": "user",
- "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
+ "search_popout.search_format": "高级搜索格式",
+ "search_popout.tips.hashtag": "话题标签",
+ "search_popout.tips.status": "嘟文",
+ "search_popout.tips.text": "使用普通字符进行搜索将会返回昵称、用户名和话题标签",
+ "search_popout.tips.user": "用户",
+ "search_results.total": "共 {count, number} 个结果",
"standalone.public_title": "大家都在干啥?",
- "status.cannot_reblog": "没法转嘟这条嘟文啦……",
+ "status.cannot_reblog": "无法转嘟这条嘟文",
"status.delete": "删除",
"status.embed": "嵌入",
"status.favourite": "收藏",
"status.load_more": "加载更多",
"status.media_hidden": "隐藏媒体内容",
"status.mention": "提及 @{name}",
- "status.more": "More",
- "status.mute_conversation": "静音对话",
+ "status.more": "更多",
+ "status.mute_conversation": "隐藏此对话",
"status.open": "展开嘟文",
- "status.pin": "置顶到资料",
+ "status.pin": "在个人资料页面置顶",
"status.reblog": "转嘟",
- "status.reblogged_by": "{name} 转嘟",
- "status.reply": "回应",
- "status.replyAll": "回应整串",
+ "status.reblogged_by": "{name} 转嘟了",
+ "status.reply": "回复",
+ "status.replyAll": "回复所有人",
"status.report": "举报 @{name}",
"status.sensitive_toggle": "点击显示",
"status.sensitive_warning": "敏感内容",
- "status.share": "Share",
- "status.show_less": "减少显示",
- "status.show_more": "显示更多",
- "status.unmute_conversation": "解禁对话",
- "status.unpin": "解除置顶",
+ "status.share": "分享",
+ "status.show_less": "隐藏内容",
+ "status.show_more": "显示内容",
+ "status.unmute_conversation": "不再隐藏此对话",
+ "status.unpin": "在个人资料页面取消置顶",
"tabs_bar.compose": "撰写",
"tabs_bar.federated_timeline": "跨站",
"tabs_bar.home": "主页",
"tabs_bar.local_timeline": "本站",
"tabs_bar.notifications": "通知",
- "upload_area.title": "将文件拖放至此上传",
+ "ui.beforeunload": "如果你现在离开 Mastodon,你的草稿内容将会被丢弃。",
+ "upload_area.title": "将文件拖放到此处开始上传",
"upload_button.label": "上传媒体文件",
- "upload_form.description": "Describe for the visually impaired",
- "upload_form.undo": "还原",
- "upload_progress.label": "上传中……",
- "video.close": "关闭影片",
+ "upload_form.description": "为视觉障碍人士添加文字说明",
+ "upload_form.undo": "取消上传",
+ "upload_progress.label": "上传中…",
+ "video.close": "关闭视频",
"video.exit_fullscreen": "退出全屏",
- "video.expand": "展开影片",
+ "video.expand": "展开视频",
"video.fullscreen": "全屏",
- "video.hide": "隐藏影片",
+ "video.hide": "隐藏视频",
"video.mute": "静音",
"video.pause": "暂停",
"video.play": "播放",
- "video.unmute": "解除静音"
+ "video.unmute": "取消静音"
}
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index f6f56fee1..15a68c915 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -7,17 +7,22 @@
"account.followers": "關注的人",
"account.follows": "正關注",
"account.follows_you": "關注你",
+ "account.hide_reblogs": "Hide boosts from @{name}",
"account.media": "媒體",
"account.mention": "提及 @{name}",
+ "account.moved_to": "{name} has moved to:",
"account.mute": "將 @{name} 靜音",
+ "account.mute_notifications": "Mute notifications from @{name}",
"account.posts": "文章",
"account.report": "舉報 @{name}",
"account.requested": "等候審批",
"account.share": "分享 @{name} 的個人資料",
+ "account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "解除對 @{name} 的封鎖",
"account.unblock_domain": "不再隱藏 {domain}",
"account.unfollow": "取消關注",
"account.unmute": "取消 @{name} 的靜音",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "查看完整資料",
"boost_modal.combo": "如你想在下次路過這顯示,請按{combo},",
"bundle_column_error.body": "加載本組件出錯。",
@@ -31,6 +36,7 @@
"column.favourites": "最愛的文章",
"column.follow_requests": "關注請求",
"column.home": "主頁",
+ "column.lists": "Lists",
"column.mutes": "靜音名單",
"column.notifications": "通知",
"column.pins": "置頂文章",
@@ -57,6 +63,8 @@
"confirmations.block.message": "你確定要封鎖{name}嗎?",
"confirmations.delete.confirm": "刪除",
"confirmations.delete.message": "你確定要刪除{name}嗎?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "隱藏整個網站",
"confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或靜音幾個特定目標就好。",
"confirmations.mute.confirm": "靜音",
@@ -83,6 +91,7 @@
"empty_column.hashtag": "這個標籤暫時未有內容。",
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
"empty_column.home.public_timeline": "公共時間軸",
+ "empty_column.list": "There is nothing in this list yet.",
"empty_column.notifications": "你沒有任何通知紀錄,快向其他用戶搭訕吧。",
"empty_column.public": "跨站時間軸暫時沒有內容!快寫一些公共的文章,或者關注另一些服務站的用戶吧!你和本站、友站的交流,將決定這裏出現的內容。",
"follow_request.authorize": "批准",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "顯示被轉推的文章",
"home.column_settings.show_replies": "顯示回應文章",
"home.settings": "欄位設定",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "關閉",
"lightbox.next": "繼續",
"lightbox.previous": "回退",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "載入中...",
"media_gallery.toggle_visible": "打開或關上",
"missing_indicator.label": "找不到內容",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.blocks": "被你封鎖的用戶",
"navigation_bar.community_timeline": "本站時間軸",
"navigation_bar.edit_profile": "修改個人資料",
"navigation_bar.favourites": "最愛的內容",
"navigation_bar.follow_requests": "關注請求",
"navigation_bar.info": "關於本服務站",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "登出",
"navigation_bar.mutes": "被你靜音的用戶",
"navigation_bar.pins": "置頂文章",
@@ -204,6 +241,7 @@
"tabs_bar.home": "主頁",
"tabs_bar.local_timeline": "本站",
"tabs_bar.notifications": "通知",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "將檔案拖放至此上載",
"upload_button.label": "上載媒體檔案",
"upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 1f43c6a20..1bdc883a8 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -7,17 +7,22 @@
"account.followers": "專注者",
"account.follows": "正關注",
"account.follows_you": "關注你",
+ "account.hide_reblogs": "Hide boosts from @{name}",
"account.media": "媒體",
"account.mention": "提到 @{name}",
+ "account.moved_to": "{name} has moved to:",
"account.mute": "消音 @{name}",
+ "account.mute_notifications": "Mute notifications from @{name}",
"account.posts": "貼文",
"account.report": "檢舉 @{name}",
"account.requested": "正在等待許可",
"account.share": "分享 @{name} 的用者資訊",
+ "account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "取消封鎖 @{name}",
"account.unblock_domain": "不再隱藏 {domain}",
"account.unfollow": "取消關注",
"account.unmute": "不再消音 @{name}",
+ "account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "查看完整資訊",
"boost_modal.combo": "下次你可以按 {combo} 來跳過",
"bundle_column_error.body": "加載本組件出錯。",
@@ -31,6 +36,7 @@
"column.favourites": "最愛",
"column.follow_requests": "關注請求",
"column.home": "家",
+ "column.lists": "Lists",
"column.mutes": "消音的使用者",
"column.notifications": "通知",
"column.pins": "置頂貼文",
@@ -57,6 +63,8 @@
"confirmations.block.message": "你確定要封鎖 {name} ?",
"confirmations.delete.confirm": "刪除",
"confirmations.delete.message": "你確定要刪除這個狀態?",
+ "confirmations.delete_list.confirm": "Delete",
+ "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "隱藏整個網域",
"confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。",
"confirmations.mute.confirm": "消音",
@@ -83,6 +91,7 @@
"empty_column.hashtag": "這個主題標籤下什麼都沒有。",
"empty_column.home": "你還沒關注任何人。造訪{public}或利用搜尋功能找到其他用者。",
"empty_column.home.public_timeline": "公開時間軸",
+ "empty_column.list": "There is nothing in this list yet.",
"empty_column.notifications": "還沒有任何通知。和別的使用者互動來開始對話。",
"empty_column.public": "這裡什麼都沒有!公開寫些什麼,或是關注其他副本的使用者。",
"follow_request.authorize": "授權",
@@ -98,18 +107,46 @@
"home.column_settings.show_reblogs": "顯示轉推",
"home.column_settings.show_replies": "顯示回應",
"home.settings": "欄位設定",
+ "keyboard_shortcuts.back": "to navigate back",
+ "keyboard_shortcuts.boost": "to boost",
+ "keyboard_shortcuts.column": "to focus a status in one of the columns",
+ "keyboard_shortcuts.compose": "to focus the compose textarea",
+ "keyboard_shortcuts.description": "Description",
+ "keyboard_shortcuts.down": "to move down in the list",
+ "keyboard_shortcuts.enter": "to open status",
+ "keyboard_shortcuts.favourite": "to favourite",
+ "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+ "keyboard_shortcuts.hotkey": "Hotkey",
+ "keyboard_shortcuts.legend": "to display this legend",
+ "keyboard_shortcuts.mention": "to mention author",
+ "keyboard_shortcuts.reply": "to reply",
+ "keyboard_shortcuts.search": "to focus search",
+ "keyboard_shortcuts.toot": "to start a brand new toot",
+ "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
+ "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "關閉",
"lightbox.next": "繼續",
"lightbox.previous": "回退",
+ "lists.account.add": "Add to list",
+ "lists.account.remove": "Remove from list",
+ "lists.delete": "Delete list",
+ "lists.edit": "Edit list",
+ "lists.new.create": "Add list",
+ "lists.new.title_placeholder": "New list title",
+ "lists.search": "Search among people you follow",
+ "lists.subheading": "Your lists",
"loading_indicator.label": "讀取中...",
"media_gallery.toggle_visible": "切換可見性",
"missing_indicator.label": "找不到",
+ "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.blocks": "封鎖的使用者",
"navigation_bar.community_timeline": "本地時間軸",
"navigation_bar.edit_profile": "編輯用者資訊",
"navigation_bar.favourites": "最愛",
"navigation_bar.follow_requests": "關注請求",
"navigation_bar.info": "關於本站",
+ "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+ "navigation_bar.lists": "Lists",
"navigation_bar.logout": "登出",
"navigation_bar.mutes": "消音的使用者",
"navigation_bar.pins": "置頂貼文",
@@ -204,6 +241,7 @@
"tabs_bar.home": "家",
"tabs_bar.local_timeline": "本地",
"tabs_bar.notifications": "通知",
+ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "拖放來上傳",
"upload_button.label": "增加媒體",
"upload_form.description": "Describe for the visually impaired",
diff --git a/app/javascript/mastodon/middleware/sounds.js b/app/javascript/mastodon/middleware/sounds.js
index 3d1e3eaba..9f1bc02b9 100644
--- a/app/javascript/mastodon/middleware/sounds.js
+++ b/app/javascript/mastodon/middleware/sounds.js
@@ -15,7 +15,7 @@ const play = audio => {
if (typeof audio.fastSeek === 'function') {
audio.fastSeek(0);
} else {
- audio.seek(0);
+ audio.currentTime = 0;
}
}
diff --git a/app/javascript/mastodon/reducers/accounts.js b/app/javascript/mastodon/reducers/accounts.js
index 8a4d69f26..f77061dfa 100644
--- a/app/javascript/mastodon/reducers/accounts.js
+++ b/app/javascript/mastodon/reducers/accounts.js
@@ -43,6 +43,10 @@ import {
FAVOURITED_STATUSES_FETCH_SUCCESS,
FAVOURITED_STATUSES_EXPAND_SUCCESS,
} from '../actions/favourites';
+import {
+ LIST_ACCOUNTS_FETCH_SUCCESS,
+ LIST_EDITOR_SUGGESTIONS_READY,
+} from '../actions/lists';
import { STORE_HYDRATE } from '../actions/store';
import emojify from '../features/emoji/emoji';
import { Map as ImmutableMap, fromJS } from 'immutable';
@@ -59,6 +63,11 @@ const normalizeAccount = (state, account) => {
account.display_name_html = emojify(escapeTextContentForBrowser(displayName));
account.note_emojified = emojify(account.note);
+ if (account.moved) {
+ state = normalizeAccount(state, account.moved);
+ account.moved = account.moved.id;
+ }
+
return state.set(account.id, fromJS(account));
};
@@ -110,6 +119,8 @@ export default function accounts(state = initialState, action) {
case BLOCKS_EXPAND_SUCCESS:
case MUTES_FETCH_SUCCESS:
case MUTES_EXPAND_SUCCESS:
+ case LIST_ACCOUNTS_FETCH_SUCCESS:
+ case LIST_EDITOR_SUGGESTIONS_READY:
return action.accounts ? normalizeAccounts(state, action.accounts) : state;
case NOTIFICATIONS_REFRESH_SUCCESS:
case NOTIFICATIONS_EXPAND_SUCCESS:
diff --git a/app/javascript/mastodon/reducers/accounts_counters.js b/app/javascript/mastodon/reducers/accounts_counters.js
index 1ed0fe3e3..a93fa4245 100644
--- a/app/javascript/mastodon/reducers/accounts_counters.js
+++ b/app/javascript/mastodon/reducers/accounts_counters.js
@@ -45,6 +45,10 @@ import {
FAVOURITED_STATUSES_FETCH_SUCCESS,
FAVOURITED_STATUSES_EXPAND_SUCCESS,
} from '../actions/favourites';
+import {
+ LIST_ACCOUNTS_FETCH_SUCCESS,
+ LIST_EDITOR_SUGGESTIONS_READY,
+} from '../actions/lists';
import { STORE_HYDRATE } from '../actions/store';
import { Map as ImmutableMap, fromJS } from 'immutable';
@@ -106,6 +110,8 @@ export default function accountsCounters(state = initialState, action) {
case BLOCKS_EXPAND_SUCCESS:
case MUTES_FETCH_SUCCESS:
case MUTES_EXPAND_SUCCESS:
+ case LIST_ACCOUNTS_FETCH_SUCCESS:
+ case LIST_EDITOR_SUGGESTIONS_READY:
return action.accounts ? normalizeAccounts(state, action.accounts) : state;
case NOTIFICATIONS_REFRESH_SUCCESS:
case NOTIFICATIONS_EXPAND_SUCCESS:
@@ -126,7 +132,8 @@ export default function accountsCounters(state = initialState, action) {
case STATUS_FETCH_SUCCESS:
return normalizeAccountFromStatus(state, action.status);
case ACCOUNT_FOLLOW_SUCCESS:
- return state.updateIn([action.relationship.id, 'followers_count'], num => num + 1);
+ return action.alreadyFollowing ? state :
+ state.updateIn([action.relationship.id, 'followers_count'], num => num + 1);
case ACCOUNT_UNFOLLOW_SUCCESS:
return state.updateIn([action.relationship.id, 'followers_count'], num => Math.max(0, num - 1));
default:
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 3e9310f16..c709fb88c 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -31,6 +31,7 @@ import { TIMELINE_DELETE } from '../actions/timelines';
import { STORE_HYDRATE } from '../actions/store';
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import uuid from '../uuid';
+import { me } from '../initial_state';
const initialState = ImmutableMap({
mounted: false,
@@ -49,7 +50,6 @@ const initialState = ImmutableMap({
media_attachments: ImmutableList(),
suggestion_token: null,
suggestions: ImmutableList(),
- me: null,
default_privacy: 'public',
default_sensitive: false,
resetFileKey: Math.floor((Math.random() * 0x10000)),
@@ -58,7 +58,6 @@ const initialState = ImmutableMap({
function statusToTextMentions(state, status) {
let set = ImmutableOrderedSet([]);
- let me = state.get('me');
if (status.getIn(['account', 'id']) !== me) {
set = set.add(`@${status.getIn(['account', 'acct'])} `);
diff --git a/app/javascript/mastodon/reducers/contexts.js b/app/javascript/mastodon/reducers/contexts.js
index 64d584a01..fe8308d0c 100644
--- a/app/javascript/mastodon/reducers/contexts.js
+++ b/app/javascript/mastodon/reducers/contexts.js
@@ -1,3 +1,7 @@
+import {
+ ACCOUNT_BLOCK_SUCCESS,
+ ACCOUNT_MUTE_SUCCESS,
+} from '../actions/accounts';
import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses';
import { TIMELINE_DELETE, TIMELINE_CONTEXT_UPDATE } from '../actions/timelines';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
@@ -31,6 +35,12 @@ const deleteFromContexts = (state, id) => {
return state;
};
+const filterContexts = (state, relationship) => {
+ return state.map(
+ statuses => statuses.filter(
+ status => status.get('account') !== relationship.id));
+};
+
const updateContext = (state, status, references) => {
return state.update('descendants', map => {
references.forEach(parentId => {
@@ -49,6 +59,9 @@ const updateContext = (state, status, references) => {
export default function contexts(state = initialState, action) {
switch(action.type) {
+ case ACCOUNT_BLOCK_SUCCESS:
+ case ACCOUNT_MUTE_SUCCESS:
+ return filterContexts(state, action.relationship);
case CONTEXT_FETCH_SUCCESS:
return normalizeContext(state, action.id, action.ancestors, action.descendants);
case TIMELINE_DELETE:
diff --git a/app/javascript/mastodon/reducers/custom_emojis.js b/app/javascript/mastodon/reducers/custom_emojis.js
index f2a8ca5d2..307bcc7dc 100644
--- a/app/javascript/mastodon/reducers/custom_emojis.js
+++ b/app/javascript/mastodon/reducers/custom_emojis.js
@@ -8,7 +8,7 @@ const initialState = ImmutableList();
export default function custom_emojis(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE:
- emojiSearch('', { custom: buildCustomEmojis(action.state.get('custom_emojis', []), action.state.getIn(['meta', 'auto_play_gif'], false)) });
+ emojiSearch('', { custom: buildCustomEmojis(action.state.get('custom_emojis', [])) });
return action.state.get('custom_emojis');
default:
return state;
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index e65144871..a028e989c 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -13,6 +13,7 @@ import settings from './settings';
import push_notifications from './push_notifications';
import status_lists from './status_lists';
import cards from './cards';
+import mutes from './mutes';
import reports from './reports';
import contexts from './contexts';
import compose from './compose';
@@ -21,6 +22,8 @@ import media_attachments from './media_attachments';
import notifications from './notifications';
import height_cache from './height_cache';
import custom_emojis from './custom_emojis';
+import lists from './lists';
+import listEditor from './list_editor';
const reducers = {
timelines,
@@ -37,6 +40,7 @@ const reducers = {
settings,
push_notifications,
cards,
+ mutes,
reports,
contexts,
compose,
@@ -45,6 +49,8 @@ const reducers = {
notifications,
height_cache,
custom_emojis,
+ lists,
+ listEditor,
};
export default combineReducers(reducers);
diff --git a/app/javascript/mastodon/reducers/list_editor.js b/app/javascript/mastodon/reducers/list_editor.js
new file mode 100644
index 000000000..02a0dabb1
--- /dev/null
+++ b/app/javascript/mastodon/reducers/list_editor.js
@@ -0,0 +1,89 @@
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+import {
+ LIST_CREATE_REQUEST,
+ LIST_CREATE_FAIL,
+ LIST_CREATE_SUCCESS,
+ LIST_UPDATE_REQUEST,
+ LIST_UPDATE_FAIL,
+ LIST_UPDATE_SUCCESS,
+ LIST_EDITOR_RESET,
+ LIST_EDITOR_SETUP,
+ LIST_EDITOR_TITLE_CHANGE,
+ LIST_ACCOUNTS_FETCH_REQUEST,
+ LIST_ACCOUNTS_FETCH_SUCCESS,
+ LIST_ACCOUNTS_FETCH_FAIL,
+ LIST_EDITOR_SUGGESTIONS_READY,
+ LIST_EDITOR_SUGGESTIONS_CLEAR,
+ LIST_EDITOR_SUGGESTIONS_CHANGE,
+ LIST_EDITOR_ADD_SUCCESS,
+ LIST_EDITOR_REMOVE_SUCCESS,
+} from '../actions/lists';
+
+const initialState = ImmutableMap({
+ listId: null,
+ isSubmitting: false,
+ title: '',
+
+ accounts: ImmutableMap({
+ items: ImmutableList(),
+ loaded: false,
+ isLoading: false,
+ }),
+
+ suggestions: ImmutableMap({
+ value: '',
+ items: ImmutableList(),
+ }),
+});
+
+export default function listEditorReducer(state = initialState, action) {
+ switch(action.type) {
+ case LIST_EDITOR_RESET:
+ return initialState;
+ case LIST_EDITOR_SETUP:
+ return state.withMutations(map => {
+ map.set('listId', action.list.get('id'));
+ map.set('title', action.list.get('title'));
+ map.set('isSubmitting', false);
+ });
+ case LIST_EDITOR_TITLE_CHANGE:
+ return state.set('title', action.value);
+ case LIST_CREATE_REQUEST:
+ case LIST_UPDATE_REQUEST:
+ return state.set('isSubmitting', true);
+ case LIST_CREATE_FAIL:
+ case LIST_UPDATE_FAIL:
+ return state.set('isSubmitting', false);
+ case LIST_CREATE_SUCCESS:
+ case LIST_UPDATE_SUCCESS:
+ return state.withMutations(map => {
+ map.set('isSubmitting', false);
+ map.set('listId', action.list.id);
+ });
+ case LIST_ACCOUNTS_FETCH_REQUEST:
+ return state.setIn(['accounts', 'isLoading'], true);
+ case LIST_ACCOUNTS_FETCH_FAIL:
+ return state.setIn(['accounts', 'isLoading'], false);
+ case LIST_ACCOUNTS_FETCH_SUCCESS:
+ return state.update('accounts', accounts => accounts.withMutations(map => {
+ map.set('isLoading', false);
+ map.set('loaded', true);
+ map.set('items', ImmutableList(action.accounts.map(item => item.id)));
+ }));
+ case LIST_EDITOR_SUGGESTIONS_CHANGE:
+ return state.setIn(['suggestions', 'value'], action.value);
+ case LIST_EDITOR_SUGGESTIONS_READY:
+ return state.setIn(['suggestions', 'items'], ImmutableList(action.accounts.map(item => item.id)));
+ case LIST_EDITOR_SUGGESTIONS_CLEAR:
+ return state.update('suggestions', suggestions => suggestions.withMutations(map => {
+ map.set('items', ImmutableList());
+ map.set('value', '');
+ }));
+ case LIST_EDITOR_ADD_SUCCESS:
+ return state.updateIn(['accounts', 'items'], list => list.unshift(action.accountId));
+ case LIST_EDITOR_REMOVE_SUCCESS:
+ return state.updateIn(['accounts', 'items'], list => list.filterNot(item => item === action.accountId));
+ default:
+ return state;
+ }
+};
diff --git a/app/javascript/mastodon/reducers/lists.js b/app/javascript/mastodon/reducers/lists.js
new file mode 100644
index 000000000..f30ffbcbd
--- /dev/null
+++ b/app/javascript/mastodon/reducers/lists.js
@@ -0,0 +1,37 @@
+import {
+ LIST_FETCH_SUCCESS,
+ LIST_FETCH_FAIL,
+ LISTS_FETCH_SUCCESS,
+ LIST_CREATE_SUCCESS,
+ LIST_UPDATE_SUCCESS,
+ LIST_DELETE_SUCCESS,
+} from '../actions/lists';
+import { Map as ImmutableMap, fromJS } from 'immutable';
+
+const initialState = ImmutableMap();
+
+const normalizeList = (state, list) => state.set(list.id, fromJS(list));
+
+const normalizeLists = (state, lists) => {
+ lists.forEach(list => {
+ state = normalizeList(state, list);
+ });
+
+ return state;
+};
+
+export default function lists(state = initialState, action) {
+ switch(action.type) {
+ case LIST_FETCH_SUCCESS:
+ case LIST_CREATE_SUCCESS:
+ case LIST_UPDATE_SUCCESS:
+ return normalizeList(state, action.list);
+ case LISTS_FETCH_SUCCESS:
+ return normalizeLists(state, action.lists);
+ case LIST_DELETE_SUCCESS:
+ case LIST_FETCH_FAIL:
+ return state.set(action.id, false);
+ default:
+ return state;
+ }
+};
diff --git a/app/javascript/mastodon/reducers/meta.js b/app/javascript/mastodon/reducers/meta.js
index 119ef9d8f..36a5a1c35 100644
--- a/app/javascript/mastodon/reducers/meta.js
+++ b/app/javascript/mastodon/reducers/meta.js
@@ -4,7 +4,6 @@ import { Map as ImmutableMap } from 'immutable';
const initialState = ImmutableMap({
streaming_api_base_url: null,
access_token: null,
- me: null,
});
export default function meta(state = initialState, action) {
diff --git a/app/javascript/mastodon/reducers/mutes.js b/app/javascript/mastodon/reducers/mutes.js
new file mode 100644
index 000000000..a96232dbd
--- /dev/null
+++ b/app/javascript/mastodon/reducers/mutes.js
@@ -0,0 +1,29 @@
+import Immutable from 'immutable';
+
+import {
+ MUTES_INIT_MODAL,
+ MUTES_TOGGLE_HIDE_NOTIFICATIONS,
+} from '../actions/mutes';
+
+const initialState = Immutable.Map({
+ new: Immutable.Map({
+ isSubmitting: false,
+ account: null,
+ notifications: true,
+ }),
+});
+
+export default function mutes(state = initialState, action) {
+ switch (action.type) {
+ case MUTES_INIT_MODAL:
+ return state.withMutations((state) => {
+ state.setIn(['new', 'isSubmitting'], false);
+ state.setIn(['new', 'account'], action.account);
+ state.setIn(['new', 'notifications'], true);
+ });
+ case MUTES_TOGGLE_HIDE_NOTIFICATIONS:
+ return state.updateIn(['new', 'notifications'], (old) => !old);
+ default:
+ return state;
+ }
+}
diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js
index cccf00a1f..264db4f55 100644
--- a/app/javascript/mastodon/reducers/notifications.js
+++ b/app/javascript/mastodon/reducers/notifications.js
@@ -99,9 +99,10 @@ export default function notifications(state = initialState, action) {
switch(action.type) {
case NOTIFICATIONS_REFRESH_REQUEST:
case NOTIFICATIONS_EXPAND_REQUEST:
+ return state.set('isLoading', true);
case NOTIFICATIONS_REFRESH_FAIL:
case NOTIFICATIONS_EXPAND_FAIL:
- return state.set('isLoading', true);
+ return state.set('isLoading', false);
case NOTIFICATIONS_SCROLL_TOP:
return updateTop(state, action.top);
case NOTIFICATIONS_UPDATE:
diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js
index a9f3f9529..5817cf49b 100644
--- a/app/javascript/mastodon/reducers/settings.js
+++ b/app/javascript/mastodon/reducers/settings.js
@@ -2,6 +2,7 @@ import { SETTING_CHANGE, SETTING_SAVE } from '../actions/settings';
import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE } from '../actions/columns';
import { STORE_HYDRATE } from '../actions/store';
import { EMOJI_USE } from '../actions/emojis';
+import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists';
import { Map as ImmutableMap, fromJS } from 'immutable';
import uuid from '../uuid';
@@ -84,6 +85,8 @@ const moveColumn = (state, uuid, direction) => {
const updateFrequentEmojis = (state, emoji) => state.update('frequentlyUsedEmojis', ImmutableMap(), map => map.update(emoji.id, 0, count => count + 1)).set('saved', false);
+const filterDeadListColumns = (state, listId) => state.update('columns', columns => columns.filterNot(column => column.get('id') === 'LIST' && column.get('params').get('id') === listId));
+
export default function settings(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE:
@@ -106,6 +109,10 @@ export default function settings(state = initialState, action) {
return updateFrequentEmojis(state, action.emoji);
case SETTING_SAVE:
return state.set('saved', true);
+ case LIST_FETCH_FAIL:
+ return action.error.response.status === 404 ? filterDeadListColumns(state, action.id) : state;
+ case LIST_DELETE_SUCCESS:
+ return filterDeadListColumns(state, action.id);
default:
return state;
}
diff --git a/app/javascript/mastodon/reducers/status_lists.js b/app/javascript/mastodon/reducers/status_lists.js
index c4aeb338f..6c5f33557 100644
--- a/app/javascript/mastodon/reducers/status_lists.js
+++ b/app/javascript/mastodon/reducers/status_lists.js
@@ -1,6 +1,10 @@
import {
+ FAVOURITED_STATUSES_FETCH_REQUEST,
FAVOURITED_STATUSES_FETCH_SUCCESS,
+ FAVOURITED_STATUSES_FETCH_FAIL,
+ FAVOURITED_STATUSES_EXPAND_REQUEST,
FAVOURITED_STATUSES_EXPAND_SUCCESS,
+ FAVOURITED_STATUSES_EXPAND_FAIL,
} from '../actions/favourites';
import {
PINNED_STATUSES_FETCH_SUCCESS,
@@ -30,6 +34,7 @@ const normalizeList = (state, listType, statuses, next) => {
return state.update(listType, listMap => listMap.withMutations(map => {
map.set('next', next);
map.set('loaded', true);
+ map.set('isLoading', false);
map.set('items', ImmutableList(statuses.map(item => item.id)));
}));
};
@@ -37,6 +42,7 @@ const normalizeList = (state, listType, statuses, next) => {
const appendToList = (state, listType, statuses, next) => {
return state.update(listType, listMap => listMap.withMutations(map => {
map.set('next', next);
+ map.set('isLoading', false);
map.set('items', map.get('items').concat(statuses.map(item => item.id)));
}));
};
@@ -55,6 +61,12 @@ const removeOneFromList = (state, listType, status) => {
export default function statusLists(state = initialState, action) {
switch(action.type) {
+ case FAVOURITED_STATUSES_FETCH_REQUEST:
+ case FAVOURITED_STATUSES_EXPAND_REQUEST:
+ return state.setIn(['favourites', 'isLoading'], true);
+ case FAVOURITED_STATUSES_FETCH_FAIL:
+ case FAVOURITED_STATUSES_EXPAND_FAIL:
+ return state.setIn(['favourites', 'isLoading'], false);
case FAVOURITED_STATUSES_FETCH_SUCCESS:
return normalizeList(state, 'favourites', action.statuses, action.next);
case FAVOURITED_STATUSES_EXPAND_SUCCESS:
diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js
index b1fb4c5da..5120b2b67 100644
--- a/app/javascript/mastodon/reducers/statuses.js
+++ b/app/javascript/mastodon/reducers/statuses.js
@@ -22,10 +22,6 @@ import {
TIMELINE_DELETE,
TIMELINE_EXPAND_SUCCESS,
} from '../actions/timelines';
-import {
- ACCOUNT_BLOCK_SUCCESS,
- ACCOUNT_MUTE_SUCCESS,
-} from '../actions/accounts';
import {
NOTIFICATIONS_UPDATE,
NOTIFICATIONS_REFRESH_SUCCESS,
@@ -88,18 +84,6 @@ const deleteStatus = (state, id, references) => {
return state.delete(id);
};
-const filterStatuses = (state, relationship) => {
- state.forEach(status => {
- if (status.get('account') !== relationship.id) {
- return;
- }
-
- state = deleteStatus(state, status.get('id'), state.filter(item => item.get('reblog') === status.get('id')));
- });
-
- return state;
-};
-
const initialState = ImmutableMap();
export default function statuses(state = initialState, action) {
@@ -139,9 +123,6 @@ export default function statuses(state = initialState, action) {
return normalizeStatuses(state, action.statuses);
case TIMELINE_DELETE:
return deleteStatus(state, action.id, action.references);
- case ACCOUNT_BLOCK_SUCCESS:
- case ACCOUNT_MUTE_SUCCESS:
- return filterStatuses(state, action.relationship);
default:
return state;
}
diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js
index bee4c4ef9..9984c3b5d 100644
--- a/app/javascript/mastodon/reducers/timelines.js
+++ b/app/javascript/mastodon/reducers/timelines.js
@@ -55,7 +55,7 @@ const appendNormalizedTimeline = (state, timeline, statuses, next) => {
}));
};
-const updateTimeline = (state, timeline, status, references) => {
+const updateTimeline = (state, timeline, status) => {
const top = state.getIn([timeline, 'top']);
const ids = state.getIn([timeline, 'items'], ImmutableList());
const includesId = ids.includes(status.get('id'));
@@ -70,7 +70,6 @@ const updateTimeline = (state, timeline, status, references) => {
return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
if (!top) mMap.set('unread', unread + 1);
if (top && ids.size > 40) newIds = newIds.take(20);
- if (status.getIn(['reblog', 'id'], null) !== null) newIds = newIds.filterNot(item => references.includes(item));
mMap.set('items', newIds.unshift(status.get('id')));
}));
};
@@ -129,7 +128,7 @@ export default function timelines(state = initialState, action) {
case TIMELINE_EXPAND_SUCCESS:
return appendNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next);
case TIMELINE_UPDATE:
- return updateTimeline(state, action.timeline, fromJS(action.status), action.references);
+ return updateTimeline(state, action.timeline, fromJS(action.status));
case TIMELINE_DELETE:
return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
case ACCOUNT_BLOCK_SUCCESS:
diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js
index d26d1b727..e47ec5183 100644
--- a/app/javascript/mastodon/selectors/index.js
+++ b/app/javascript/mastodon/selectors/index.js
@@ -4,14 +4,18 @@ import { List as ImmutableList } from 'immutable';
const getAccountBase = (state, id) => state.getIn(['accounts', id], null);
const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null);
const getAccountRelationship = (state, id) => state.getIn(['relationships', id], null);
+const getAccountMoved = (state, id) => state.getIn(['accounts', state.getIn(['accounts', id, 'moved'])]);
export const makeGetAccount = () => {
- return createSelector([getAccountBase, getAccountCounters, getAccountRelationship], (base, counters, relationship) => {
+ return createSelector([getAccountBase, getAccountCounters, getAccountRelationship, getAccountMoved], (base, counters, relationship, moved) => {
if (base === null) {
return null;
}
- return base.merge(counters).set('relationship', relationship);
+ return base.merge(counters).withMutations(map => {
+ map.set('relationship', relationship);
+ map.set('moved', moved);
+ });
});
};
diff --git a/app/javascript/mastodon/settings.js b/app/javascript/mastodon/settings.js
new file mode 100644
index 000000000..dbd969cb1
--- /dev/null
+++ b/app/javascript/mastodon/settings.js
@@ -0,0 +1,46 @@
+export default class Settings {
+
+ constructor(keyBase = null) {
+ this.keyBase = keyBase;
+ }
+
+ generateKey(id) {
+ return this.keyBase ? [this.keyBase, `id${id}`].join('.') : id;
+ }
+
+ set(id, data) {
+ const key = this.generateKey(id);
+ try {
+ const encodedData = JSON.stringify(data);
+ localStorage.setItem(key, encodedData);
+ return data;
+ } catch (e) {
+ return null;
+ }
+ }
+
+ get(id) {
+ const key = this.generateKey(id);
+ try {
+ const rawData = localStorage.getItem(key);
+ return JSON.parse(rawData);
+ } catch (e) {
+ return null;
+ }
+ }
+
+ remove(id) {
+ const data = this.get(id);
+ if (data) {
+ const key = this.generateKey(id);
+ try {
+ localStorage.removeItem(key);
+ } catch (e) {
+ }
+ }
+ return data;
+ }
+
+}
+
+export const pushNotificationsSetting = new Settings('mastodon_push_notification_data');
diff --git a/app/javascript/mastodon/stream.js b/app/javascript/mastodon/stream.js
index 4b36082b2..9a6f4f26d 100644
--- a/app/javascript/mastodon/stream.js
+++ b/app/javascript/mastodon/stream.js
@@ -1,7 +1,74 @@
import WebSocketClient from 'websocket.js';
+export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onConnect() {}, onDisconnect() {}, onReceive() {} })) {
+ return (dispatch, getState) => {
+ const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
+ const accessToken = getState().getIn(['meta', 'access_token']);
+ const { onConnect, onDisconnect, onReceive } = callbacks(dispatch, getState);
+ let polling = null;
+
+ const setupPolling = () => {
+ polling = setInterval(() => {
+ pollingRefresh(dispatch);
+ }, 20000);
+ };
+
+ const clearPolling = () => {
+ if (polling) {
+ clearInterval(polling);
+ polling = null;
+ }
+ };
+
+ const subscription = getStream(streamingAPIBaseURL, accessToken, path, {
+ connected () {
+ if (pollingRefresh) {
+ clearPolling();
+ }
+ onConnect();
+ },
+
+ disconnected () {
+ if (pollingRefresh) {
+ setupPolling();
+ }
+ onDisconnect();
+ },
+
+ received (data) {
+ onReceive(data);
+ },
+
+ reconnected () {
+ if (pollingRefresh) {
+ clearPolling();
+ pollingRefresh(dispatch);
+ }
+ onConnect();
+ },
+
+ });
+
+ const disconnect = () => {
+ if (subscription) {
+ subscription.close();
+ }
+ clearPolling();
+ };
+
+ return disconnect;
+ };
+}
+
+
export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) {
- const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`);
+ const params = [ `stream=${stream}` ];
+
+ if (accessToken !== null) {
+ params.push(`access_token=${accessToken}`);
+ }
+
+ const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`);
ws.onopen = connected;
ws.onmessage = e => received(JSON.parse(e.data));
diff --git a/app/javascript/mastodon/web_push_subscription.js b/app/javascript/mastodon/web_push_subscription.js
index 3dbed09ea..17aca4060 100644
--- a/app/javascript/mastodon/web_push_subscription.js
+++ b/app/javascript/mastodon/web_push_subscription.js
@@ -1,6 +1,7 @@
import axios from 'axios';
import { store } from './containers/mastodon';
import { setBrowserSupport, setSubscription, clearSubscription } from './actions/push_notifications';
+import { pushNotificationsSetting } from './settings';
// Taken from https://www.npmjs.com/package/web-push
const urlBase64ToUint8Array = (base64String) => {
@@ -35,16 +36,33 @@ const subscribe = (registration) =>
const unsubscribe = ({ registration, subscription }) =>
subscription ? subscription.unsubscribe().then(() => registration) : registration;
-const sendSubscriptionToBackend = (subscription) =>
- axios.post('/api/web/push_subscriptions', {
- subscription,
- }).then(response => response.data);
+const sendSubscriptionToBackend = (subscription) => {
+ const params = { subscription };
+
+ const me = store.getState().getIn(['meta', 'me']);
+ if (me) {
+ const data = pushNotificationsSetting.get(me);
+ if (data) {
+ params.data = data;
+ }
+ }
+
+ return axios.post('/api/web/push_subscriptions', params).then(response => response.data);
+};
// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
export function register () {
store.dispatch(setBrowserSupport(supportsPushNotifications));
+ const me = store.getState().getIn(['meta', 'me']);
+
+ if (me && !pushNotificationsSetting.get(me)) {
+ const alerts = store.getState().getIn(['push_notifications', 'alerts']);
+ if (alerts) {
+ pushNotificationsSetting.set(me, { alerts: alerts });
+ }
+ }
if (supportsPushNotifications) {
if (!getApplicationServerKey()) {
@@ -79,6 +97,9 @@ export function register () {
// it means that the backend subscription is valid (and was set during hydration)
if (!(subscription instanceof PushSubscription)) {
store.dispatch(setSubscription(subscription));
+ if (me) {
+ pushNotificationsSetting.set(me, { alerts: subscription.alerts });
+ }
}
})
.catch(error => {
@@ -90,6 +111,9 @@ export function register () {
// Clear alerts and hide UI settings
store.dispatch(clearSubscription());
+ if (me) {
+ pushNotificationsSetting.remove(me);
+ }
try {
getRegistration()
diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss
index 358d86eec..e45fc03d3 100644
--- a/app/javascript/styles/mastodon/about.scss
+++ b/app/javascript/styles/mastodon/about.scss
@@ -19,7 +19,7 @@
display: inline;
margin: 0;
padding: 0;
- font-weight: 500;
+ font-weight: 700;
background: transparent;
font-family: inherit;
font-size: inherit;
diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss
index 30adf8cdc..9015d04cb 100644
--- a/app/javascript/styles/mastodon/accounts.scss
+++ b/app/javascript/styles/mastodon/accounts.scss
@@ -473,6 +473,12 @@
strong {
font-weight: 500;
color: $ui-base-color;
+
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
}
span {
@@ -531,7 +537,19 @@
font-size: 12px;
line-height: 12px;
font-weight: 500;
- color: $success-green;
- background-color: rgba($success-green, 0.1);
- border: 1px solid rgba($success-green, 0.5);
+ color: $ui-secondary-color;
+ background-color: rgba($ui-secondary-color, 0.1);
+ border: 1px solid rgba($ui-secondary-color, 0.5);
+
+ &.moderator {
+ color: $success-green;
+ background-color: rgba($success-green, 0.1);
+ border-color: rgba($success-green, 0.5);
+ }
+
+ &.admin {
+ color: lighten($error-red, 12%);
+ background-color: rgba(lighten($error-red, 12%), 0.1);
+ border-color: rgba(lighten($error-red, 12%), 0.5);
+ }
}
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 87bc710af..bddea557b 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -121,6 +121,12 @@
strong {
color: $primary-text-color;
font-weight: 500;
+
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
}
}
@@ -222,6 +228,12 @@
font-weight: 500;
text-transform: uppercase;
font-size: 12px;
+
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
}
a {
@@ -265,6 +277,12 @@
font-size: 14px;
line-height: 18px;
color: $ui-secondary-color;
+
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
}
.account-card {
@@ -347,3 +365,120 @@
}
}
}
+
+.spacer {
+ flex: 1 1 auto;
+}
+
+.log-entry {
+ margin-bottom: 8px;
+ line-height: 20px;
+
+ &__header {
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ padding: 10px;
+ background: $ui-base-color;
+ color: $ui-primary-color;
+ border-radius: 4px 4px 0 0;
+ font-size: 14px;
+ position: relative;
+ }
+
+ &__avatar {
+ margin-right: 10px;
+
+ .avatar {
+ display: block;
+ margin: 0;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ }
+ }
+
+ &__title {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ &__timestamp {
+ color: lighten($ui-base-color, 34%);
+ }
+
+ &__extras {
+ background: lighten($ui-base-color, 6%);
+ border-radius: 0 0 4px 4px;
+ padding: 10px;
+ color: $ui-primary-color;
+ font-family: 'mastodon-font-monospace', monospace;
+ font-size: 12px;
+ white-space: nowrap;
+ min-height: 20px;
+ }
+
+ &__icon {
+ font-size: 28px;
+ margin-right: 10px;
+ color: lighten($ui-base-color, 34%);
+ }
+
+ &__icon__overlay {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+
+ &.positive {
+ background: $success-green;
+ }
+
+ &.negative {
+ background: $error-red;
+ }
+
+ &.neutral {
+ background: $ui-highlight-color;
+ }
+ }
+
+ a,
+ .username,
+ .target {
+ color: $ui-secondary-color;
+ text-decoration: none;
+ font-weight: 500;
+ }
+
+ .diff-old {
+ color: $error-red;
+ }
+
+ .diff-neutral {
+ color: $ui-secondary-color;
+ }
+
+ .diff-new {
+ color: $success-green;
+ }
+}
+
+.name-tag {
+ display: flex;
+ align-items: center;
+
+ .avatar {
+ display: block;
+ margin: 0;
+ margin-right: 5px;
+ border-radius: 50%;
+ }
+
+ .username {
+ font-weight: 500;
+ }
+}
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 5211489f7..db07c3dfa 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1,5 +1,3 @@
-@import 'variables';
-
.app-body {
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
@@ -244,6 +242,15 @@
width: 0;
height: 0;
position: absolute;
+
+ img,
+ svg {
+ margin: 0 !important;
+ border: 0 !important;
+ padding: 0 !important;
+ width: 0 !important;
+ height: 0 !important;
+ }
}
.ellipsis {
@@ -258,197 +265,286 @@
.compose-form {
padding: 10px;
-}
-.compose-form__warning {
- color: darken($ui-secondary-color, 65%);
- margin-bottom: 15px;
- background: $ui-primary-color;
- box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3);
- padding: 8px 10px;
- border-radius: 4px;
- font-size: 13px;
- font-weight: 400;
-
- strong {
+ .compose-form__warning {
color: darken($ui-secondary-color, 65%);
- font-weight: 500;
- }
+ margin-bottom: 15px;
+ background: $ui-primary-color;
+ box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3);
+ padding: 8px 10px;
+ border-radius: 4px;
+ font-size: 13px;
+ font-weight: 400;
- a {
- color: darken($ui-primary-color, 33%);
- font-weight: 500;
- text-decoration: underline;
-
- &:hover,
- &:active,
- &:focus {
- text-decoration: none;
- }
- }
-}
-
-.compose-form__modifiers {
- color: $ui-base-color;
- font-family: inherit;
- font-size: 14px;
- background: $simple-background-color;
- border-radius: 0 0 4px;
-}
-
-.compose-form__buttons-wrapper {
- display: flex;
- justify-content: space-between;
-}
-
-.compose-form__buttons {
- padding: 10px;
- background: darken($simple-background-color, 8%);
- box-shadow: inset 0 5px 5px rgba($base-shadow-color, 0.05);
- border-radius: 0 0 4px 4px;
- display: flex;
-
- .icon-button {
- box-sizing: content-box;
- padding: 0 3px;
- }
-}
-
-.compose-form__upload-button-icon {
- line-height: 27px;
-}
-
-.compose-form__sensitive-button {
- display: none;
-
- &.compose-form__sensitive-button--visible {
- display: block;
- }
-
- .compose-form__sensitive-button__icon {
- line-height: 27px;
- }
-}
-
-.compose-form__upload-wrapper {
- overflow: hidden;
-}
-
-.compose-form__uploads-wrapper {
- display: flex;
- flex-direction: row;
- padding: 5px;
- flex-wrap: wrap;
-}
-
-.compose-form__upload {
- flex: 1 1 0;
- min-width: 40%;
- margin: 5px;
-
- &-description {
- position: absolute;
- z-index: 2;
- bottom: 0;
- left: 0;
- right: 0;
- box-sizing: border-box;
- background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent);
- padding: 10px;
- opacity: 0;
- transition: opacity .1s ease;
-
- input {
- background: transparent;
- color: $ui-secondary-color;
- border: 0;
- padding: 0;
- margin: 0;
- width: 100%;
- font-family: inherit;
- font-size: 14px;
+ strong {
+ color: darken($ui-secondary-color, 65%);
font-weight: 500;
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
+ }
+
+ a {
+ color: darken($ui-primary-color, 33%);
+ font-weight: 500;
+ text-decoration: underline;
+
+ &:hover,
+ &:active,
&:focus {
- color: $white;
- }
-
- &::placeholder {
- opacity: 0.54;
- color: $ui-secondary-color;
+ text-decoration: none;
}
}
+ }
- &.active {
- opacity: 1;
+ .compose-form__autosuggest-wrapper {
+ position: relative;
+
+ .emoji-picker-dropdown {
+ position: absolute;
+ right: 5px;
+ top: 5px;
}
}
- .icon-button {
- mix-blend-mode: difference;
- }
-}
-
-.compose-form__upload-thumbnail {
- border-radius: 4px;
- background-position: center;
- background-size: cover;
- background-repeat: no-repeat;
- height: 100px;
- width: 100%;
-}
-
-.compose-form__label {
- display: block;
- line-height: 24px;
- vertical-align: middle;
-
- &.with-border {
- border-top: 1px solid $ui-base-color;
- padding-top: 10px;
+ .autosuggest-textarea,
+ .spoiler-input {
+ position: relative;
}
- .compose-form__label__text {
- display: inline-block;
- vertical-align: middle;
- margin-bottom: 14px;
- margin-left: 8px;
- color: $ui-primary-color;
+ .autosuggest-textarea__textarea,
+ .spoiler-input__input {
+ display: block;
+ box-sizing: border-box;
+ width: 100%;
+ margin: 0;
+ color: $ui-base-color;
+ background: $simple-background-color;
+ padding: 10px;
+ font-family: inherit;
+ font-size: 14px;
+ resize: vertical;
+ border: 0;
+ outline: 0;
+
+ &:focus {
+ outline: 0;
+ }
+
+ @media screen and (max-width: 600px) {
+ font-size: 16px;
+ }
}
-}
-.compose-form__textarea,
-.follow-form__input {
- background: $simple-background-color;
-
- &:disabled {
- background: $ui-secondary-color;
+ .spoiler-input__input {
+ border-radius: 4px;
}
-}
-.compose-form__autosuggest-wrapper {
- position: relative;
+ .autosuggest-textarea__textarea {
+ min-height: 100px;
+ border-radius: 4px 4px 0 0;
+ padding-bottom: 0;
+ padding-right: 10px + 22px;
+ resize: none;
- .emoji-picker-dropdown {
+ @media screen and (max-width: 600px) {
+ height: 100px !important; // prevent auto-resize textarea
+ resize: vertical;
+ }
+ }
+
+ .autosuggest-textarea__suggestions {
+ box-sizing: border-box;
+ display: none;
position: absolute;
- right: 5px;
- top: 5px;
+ top: 100%;
+ width: 100%;
+ z-index: 99;
+ box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
+ background: $ui-secondary-color;
+ border-radius: 0 0 4px 4px;
+ color: $ui-base-color;
+ font-size: 14px;
+ padding: 6px;
- ::-webkit-scrollbar-track:hover,
- ::-webkit-scrollbar-track:active {
- background-color: rgba($base-overlay-background, 0.3);
+ &.autosuggest-textarea__suggestions--visible {
+ display: block;
}
}
-}
-.compose-form__publish {
- display: flex;
- min-width: 0;
-}
+ .autosuggest-textarea__suggestions__item {
+ padding: 10px;
+ cursor: pointer;
+ border-radius: 4px;
-.compose-form__publish-button-wrapper {
- overflow: hidden;
- padding-top: 10px;
+ &:hover,
+ &:focus,
+ &:active,
+ &.selected {
+ background: darken($ui-secondary-color, 10%);
+ }
+ }
+
+ .autosuggest-account,
+ .autosuggest-emoji {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: flex-start;
+ line-height: 18px;
+ font-size: 14px;
+ }
+
+ .autosuggest-account-icon,
+ .autosuggest-emoji img {
+ display: block;
+ margin-right: 8px;
+ width: 16px;
+ height: 16px;
+ }
+
+ .autosuggest-account .display-name__account {
+ color: lighten($ui-base-color, 36%);
+ }
+
+ .compose-form__modifiers {
+ color: $ui-base-color;
+ font-family: inherit;
+ font-size: 14px;
+ background: $simple-background-color;
+
+ .compose-form__upload-wrapper {
+ overflow: hidden;
+ }
+
+ .compose-form__uploads-wrapper {
+ display: flex;
+ flex-direction: row;
+ padding: 5px;
+ flex-wrap: wrap;
+ }
+
+ .compose-form__upload {
+ flex: 1 1 0;
+ min-width: 40%;
+ margin: 5px;
+
+ &-description {
+ position: absolute;
+ z-index: 2;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ box-sizing: border-box;
+ background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent);
+ padding: 10px;
+ opacity: 0;
+ transition: opacity .1s ease;
+
+ input {
+ background: transparent;
+ color: $ui-secondary-color;
+ border: 0;
+ padding: 0;
+ margin: 0;
+ width: 100%;
+ font-family: inherit;
+ font-size: 14px;
+ font-weight: 500;
+
+ &:focus {
+ color: $white;
+ }
+
+ &::placeholder {
+ opacity: 0.54;
+ color: $ui-secondary-color;
+ }
+ }
+
+ &.active {
+ opacity: 1;
+ }
+ }
+
+ .icon-button {
+ mix-blend-mode: difference;
+ }
+ }
+
+ .compose-form__upload-thumbnail {
+ border-radius: 4px;
+ background-position: center;
+ background-size: cover;
+ background-repeat: no-repeat;
+ height: 100px;
+ width: 100%;
+ }
+ }
+
+ .compose-form__buttons-wrapper {
+ padding: 10px;
+ background: darken($simple-background-color, 8%);
+ border-radius: 0 0 4px 4px;
+ display: flex;
+ justify-content: space-between;
+
+ .compose-form__buttons {
+ display: flex;
+
+ .compose-form__upload-button-icon {
+ line-height: 27px;
+ }
+
+ .compose-form__sensitive-button {
+ display: none;
+
+ &.compose-form__sensitive-button--visible {
+ display: block;
+ }
+
+ .compose-form__sensitive-button__icon {
+ line-height: 27px;
+ }
+ }
+ }
+
+ .icon-button {
+ box-sizing: content-box;
+ padding: 0 3px;
+ }
+
+ .character-counter__wrapper {
+ align-self: center;
+ margin-right: 4px;
+
+ .character-counter {
+ cursor: default;
+ font-family: 'mastodon-font-sans-serif', sans-serif;
+ font-size: 14px;
+ font-weight: 600;
+ color: lighten($ui-base-color, 12%);
+
+ &.character-counter--over {
+ color: $warning-red;
+ }
+ }
+ }
+ }
+
+ .compose-form__publish {
+ display: flex;
+ justify-content: flex-end;
+ min-width: 0;
+
+ .compose-form__publish-button-wrapper {
+ overflow: hidden;
+ padding-top: 10px;
+ }
+ }
}
.emojione {
@@ -510,6 +606,11 @@
font-weight: 400;
overflow: hidden;
white-space: pre-wrap;
+ padding-top: 2px;
+
+ &:focus {
+ outline: 0;
+ }
&.status__content--with-spoiler {
white-space: normal;
@@ -520,8 +621,9 @@
}
.emojione {
- width: 18px;
- height: 18px;
+ width: 20px;
+ height: 20px;
+ margin: -3px 0 0;
}
p {
@@ -601,8 +703,12 @@
outline: 0;
background: lighten($ui-base-color, 4%);
- &.status-direct {
+ .status.status-direct {
background: lighten($ui-base-color, 12%);
+
+ &.muted {
+ background: transparent;
+ }
}
.detailed-status,
@@ -620,6 +726,12 @@
border-bottom: 1px solid lighten($ui-base-color, 8%);
cursor: default;
+ @supports (-ms-overflow-style: -ms-autohiding-scrollbar) {
+ // Add margin to avoid Edge auto-hiding scrollbar appearing over content.
+ // On Edge 16 this is 16px and Edge <=15 it's 12px, so aim for 16px.
+ padding-right: 26px; // 10px + 16px
+ }
+
@keyframes fade {
0% { opacity: 0; }
100% { opacity: 1; }
@@ -751,7 +863,7 @@
.status__action-bar {
align-items: center;
display: flex;
- margin-top: 10px;
+ margin-top: 8px;
}
.status__action-bar-button {
@@ -782,8 +894,9 @@
line-height: 24px;
.emojione {
- width: 22px;
- height: 22px;
+ width: 24px;
+ height: 24px;
+ margin: -1px 0 0;
}
}
@@ -888,6 +1001,7 @@
.account__relationship {
height: 18px;
padding: 10px;
+ white-space: nowrap;
}
.account__header {
@@ -898,6 +1012,18 @@
background-position: center;
position: relative;
+ &.inactive {
+ opacity: 0.5;
+
+ .account__header__avatar {
+ filter: grayscale(100%);
+ }
+
+ .account__header__username {
+ color: $ui-primary-color;
+ }
+ }
+
& > div {
background: rgba(lighten($ui-base-color, 4%), 0.9);
padding: 20px 10px;
@@ -936,6 +1062,12 @@
strong {
font-weight: 500;
+
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
}
a {
@@ -1037,6 +1169,12 @@
font-size: 15px;
font-weight: 500;
color: $primary-text-color;
+
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
}
abbr {
@@ -1928,121 +2066,6 @@
cursor: default;
}
-.autosuggest-textarea,
-.spoiler-input {
- position: relative;
-}
-
-.autosuggest-textarea__textarea,
-.spoiler-input__input {
- display: block;
- box-sizing: border-box;
- width: 100%;
- margin: 0;
- color: $ui-base-color;
- background: $simple-background-color;
- padding: 10px;
- font-family: inherit;
- font-size: 14px;
- resize: vertical;
- border: 0;
- outline: 0;
-
- &:focus {
- outline: 0;
- }
-
- @media screen and (max-width: 600px) {
- font-size: 16px;
- }
-}
-
-.spoiler-input__input {
- border-radius: 4px;
-}
-
-.autosuggest-textarea__textarea {
- min-height: 100px;
- border-radius: 4px 4px 0 0;
- padding-bottom: 0;
- padding-right: 10px + 22px;
- resize: none;
-
- @media screen and (max-width: 600px) {
- height: 100px !important; // prevent auto-resize textarea
- resize: vertical;
- }
-}
-
-.autosuggest-textarea__suggestions {
- box-sizing: border-box;
- display: none;
- position: absolute;
- top: 100%;
- width: 100%;
- z-index: 99;
- box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
- background: $ui-secondary-color;
- border-radius: 0 0 4px 4px;
- color: $ui-base-color;
- font-size: 14px;
- padding: 6px;
-
- &.autosuggest-textarea__suggestions--visible {
- display: block;
- }
-}
-
-.autosuggest-textarea__suggestions__item {
- padding: 10px;
- cursor: pointer;
- border-radius: 4px;
-
- &:hover,
- &:focus,
- &:active,
- &.selected {
- background: darken($ui-secondary-color, 10%);
- }
-}
-
-.autosuggest-account,
-.autosuggest-emoji {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: flex-start;
- line-height: 18px;
- font-size: 14px;
-}
-
-.autosuggest-account-icon,
-.autosuggest-emoji img {
- display: block;
- margin-right: 8px;
- width: 16px;
- height: 16px;
-}
-
-.autosuggest-account .display-name__account {
- color: lighten($ui-base-color, 36%);
-}
-
-.character-counter__wrapper {
- line-height: 36px;
- margin: 0 16px 0 8px;
- padding-top: 10px;
-}
-
-.character-counter {
- cursor: default;
- font-size: 16px;
-}
-
-.character-counter--over {
- color: $warning-red;
-}
-
.getting-started__wrapper {
position: relative;
overflow-y: auto;
@@ -2068,6 +2091,27 @@
}
}
+.keyboard-shortcuts {
+ padding: 8px 0 0;
+ overflow: hidden;
+
+ thead {
+ position: absolute;
+ left: -9999px;
+ }
+
+ td {
+ padding: 0 10px 8px;
+ }
+
+ code {
+ display: inline-block;
+ padding: 3px 5px;
+ background-color: lighten($ui-base-color, 8%);
+ border: 1px solid darken($ui-base-color, 4%);
+ }
+}
+
.setting-text {
color: $ui-primary-color;
background: transparent;
@@ -2151,15 +2195,12 @@ button.icon-button.active i.fa-retweet {
}
.status-card-photo {
+ cursor: zoom-in;
display: block;
text-decoration: none;
-
- img {
- display: block;
- width: 100%;
- height: auto;
- margin: 0;
- }
+ width: 100%;
+ height: auto;
+ margin: 0;
}
.status-card-video {
@@ -2210,14 +2251,19 @@ button.icon-button.active i.fa-retweet {
.status-card__image-image {
border-radius: 4px 4px 0 0;
}
+
+ .status-card__title {
+ white-space: inherit;
+ }
}
.status-card__image-image {
border-radius: 4px 0 0 4px;
display: block;
- height: auto;
margin: 0;
width: 100%;
+ height: 100%;
+ object-fit: cover;
}
.load-more {
@@ -2280,6 +2326,7 @@ button.icon-button.active i.fa-retweet {
}
.column-header {
+ display: flex;
padding: 15px;
font-size: 16px;
background: lighten($ui-base-color, 4%);
@@ -2305,12 +2352,14 @@ button.icon-button.active i.fa-retweet {
}
.column-header__buttons {
- position: absolute;
- right: 0;
- top: 0;
- height: 100%;
- display: flex;
height: 48px;
+ display: flex;
+ margin: -15px;
+ margin-left: 0;
+}
+
+.column-header__links .text-btn {
+ margin-right: 10px;
}
.column-header__button {
@@ -2352,6 +2401,14 @@ button.icon-button.active i.fa-retweet {
&.animating {
overflow-y: hidden;
}
+
+ hr {
+ height: 0;
+ background: transparent;
+ border: 0;
+ border-top: 1px solid lighten($ui-base-color, 12%);
+ margin: 10px 0;
+ }
}
.column-header__collapsible-inner {
@@ -2378,6 +2435,14 @@ button.icon-button.active i.fa-retweet {
}
}
+.column-header__title {
+ display: inline-block;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ flex: 1;
+}
+
.text-btn {
display: inline-block;
padding: 0;
@@ -2581,7 +2646,7 @@ button.icon-button.active i.fa-retweet {
color: $primary-text-color;
position: absolute;
top: 10px;
- right: 10px;
+ left: 10px;
opacity: 0.7;
display: inline-block;
vertical-align: top;
@@ -2596,7 +2661,7 @@ button.icon-button.active i.fa-retweet {
.account--action-button {
position: absolute;
top: 10px;
- left: 20px;
+ right: 20px;
}
.setting-toggle {
@@ -2920,6 +2985,12 @@ button.icon-button.active i.fa-retweet {
font-weight: 500;
display: block;
color: $ui-base-color;
+
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
}
}
@@ -3008,21 +3079,21 @@ button.icon-button.active i.fa-retweet {
}
.fa-search {
- transform: translateZ(0) rotate(90deg);
+ transform: rotate(90deg);
&.active {
pointer-events: none;
- transform: translateZ(0) rotate(0deg);
+ transform: rotate(0deg);
}
}
.fa-times-circle {
top: 11px;
- transform: translateZ(0) rotate(0deg);
+ transform: rotate(0deg);
cursor: pointer;
&.active {
- transform: translateZ(0) rotate(90deg);
+ transform: rotate(90deg);
}
&:hover {
@@ -3067,7 +3138,6 @@ button.icon-button.active i.fa-retweet {
right: 0;
bottom: 0;
background: rgba($base-overlay-background, 0.7);
- transform: translateZ(0);
}
.modal-root__container {
@@ -3355,6 +3425,12 @@ button.icon-button.active i.fa-retweet {
border-radius: 4px;
font-size: 14px;
padding: 3px 6px;
+
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
}
}
}
@@ -3491,7 +3567,8 @@ button.icon-button.active i.fa-retweet {
.boost-modal,
.confirmation-modal,
.report-modal,
-.actions-modal {
+.actions-modal,
+.mute-modal {
background: lighten($ui-secondary-color, 8%);
color: $ui-base-color;
border-radius: 8px;
@@ -3541,6 +3618,7 @@ button.icon-button.active i.fa-retweet {
.boost-modal__action-bar,
.confirmation-modal__action-bar,
+.mute-modal__action-bar,
.report-modal__action-bar {
display: flex;
justify-content: space-between;
@@ -3577,6 +3655,14 @@ button.icon-button.active i.fa-retweet {
}
}
+.mute-modal {
+ line-height: 24px;
+}
+
+.mute-modal .react-toggle {
+ vertical-align: middle;
+}
+
.report-modal__statuses,
.report-modal__comment {
padding: 10px;
@@ -3649,8 +3735,10 @@ button.icon-button.active i.fa-retweet {
}
}
-.confirmation-modal__action-bar {
- .confirmation-modal__cancel-button {
+.confirmation-modal__action-bar,
+.mute-modal__action-bar {
+ .confirmation-modal__cancel-button,
+ .mute-modal__cancel-button {
background-color: transparent;
color: darken($ui-secondary-color, 34%);
font-size: 14px;
@@ -3665,6 +3753,7 @@ button.icon-button.active i.fa-retweet {
}
.confirmation-modal__container,
+.mute-modal__container,
.report-modal__target {
padding: 30px;
font-size: 16px;
@@ -3672,6 +3761,12 @@ button.icon-button.active i.fa-retweet {
strong {
font-weight: 500;
+
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
}
}
@@ -3886,6 +3981,7 @@ button.icon-button.active i.fa-retweet {
position: relative;
background: $base-shadow-color;
max-width: 100%;
+ border-radius: 4px;
video {
height: 100%;
@@ -3920,8 +4016,8 @@ button.icon-button.active i.fa-retweet {
left: 0;
right: 0;
box-sizing: border-box;
- background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 60%, transparent);
- padding: 0 10px;
+ background: linear-gradient(0deg, rgba($base-shadow-color, 0.85) 0, rgba($base-shadow-color, 0.45) 60%, transparent);
+ padding: 0 15px;
opacity: 0;
transition: opacity .1s ease;
@@ -3974,40 +4070,67 @@ button.icon-button.active i.fa-retweet {
}
}
- &__buttons {
+ &__buttons-bar {
+ display: flex;
+ justify-content: space-between;
padding-bottom: 10px;
+ }
+
+ &__buttons {
font-size: 16px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
&.left {
- float: left;
-
button {
- padding-right: 10px;
+ padding-left: 0;
}
}
&.right {
- float: right;
-
button {
- padding-left: 10px;
+ padding-right: 0;
}
}
button {
background: transparent;
- padding: 0;
+ padding: 2px 10px;
+ font-size: 16px;
border: 0;
- color: $white;
+ color: rgba($white, 0.75);
&:active,
&:hover,
&:focus {
- color: $ui-highlight-color;
+ color: $white;
}
}
}
+ &__time-sep,
+ &__time-total,
+ &__time-current {
+ font-size: 14px;
+ font-weight: 500;
+ }
+
+ &__time-current {
+ color: $white;
+ margin-left: 10px;
+ }
+
+ &__time-sep {
+ display: inline-block;
+ margin: 0 6px;
+ }
+
+ &__time-sep,
+ &__time-total {
+ color: $white;
+ }
+
&__seek {
cursor: pointer;
height: 24px;
@@ -4017,6 +4140,7 @@ button.icon-button.active i.fa-retweet {
content: "";
width: 100%;
background: rgba($white, 0.35);
+ border-radius: 4px;
display: block;
position: absolute;
height: 4px;
@@ -4028,8 +4152,9 @@ button.icon-button.active i.fa-retweet {
display: block;
position: absolute;
height: 4px;
+ border-radius: 4px;
top: 10px;
- background: $ui-highlight-color;
+ background: lighten($ui-highlight-color, 8%);
}
&__buffer {
@@ -4046,7 +4171,8 @@ button.icon-button.active i.fa-retweet {
top: 6px;
margin-left: -6px;
transition: opacity .1s ease;
- background: $ui-highlight-color;
+ background: lighten($ui-highlight-color, 8%);
+ box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
pointer-events: none;
&.active {
@@ -4060,6 +4186,16 @@ button.icon-button.active i.fa-retweet {
}
}
}
+
+ &.detailed,
+ &.fullscreen {
+ .video-player__buttons {
+ button {
+ padding-top: 10px;
+ padding-bottom: 10px;
+ }
+ }
+ }
}
.media-spoiler-video {
@@ -4337,3 +4473,131 @@ noscript {
}
}
}
+
+.account__moved-note {
+ padding: 14px 10px;
+ padding-bottom: 16px;
+ background: lighten($ui-base-color, 4%);
+ border-top: 1px solid lighten($ui-base-color, 8%);
+ border-bottom: 1px solid lighten($ui-base-color, 8%);
+
+ &__message {
+ position: relative;
+ margin-left: 58px;
+ color: $ui-base-lighter-color;
+ padding: 8px 0;
+ padding-top: 0;
+ padding-bottom: 4px;
+ font-size: 14px;
+
+ > span {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+
+ &__icon-wrapper {
+ left: -26px;
+ position: absolute;
+ }
+
+ .detailed-status__display-avatar {
+ position: relative;
+ }
+
+ .detailed-status__display-name {
+ margin-bottom: 0;
+ }
+}
+
+.column-inline-form {
+ padding: 7px 15px;
+ padding-right: 5px;
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ background: lighten($ui-base-color, 4%);
+
+ label {
+ flex: 1 1 auto;
+
+ input {
+ width: 100%;
+ margin-bottom: 6px;
+
+ &:focus {
+ outline: 0;
+ }
+ }
+ }
+
+ .icon-button {
+ flex: 0 0 auto;
+ margin-left: 5px;
+ }
+}
+
+.drawer__backdrop {
+ cursor: pointer;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba($base-overlay-background, 0.5);
+}
+
+.list-editor {
+ background: $ui-base-color;
+ flex-direction: column;
+ border-radius: 8px;
+ box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
+ width: 380px;
+ overflow: hidden;
+
+ @media screen and (max-width: 420px) {
+ width: 90%;
+ }
+
+ h4 {
+ padding: 15px 0;
+ background: lighten($ui-base-color, 13%);
+ font-weight: 500;
+ font-size: 16px;
+ text-align: center;
+ border-radius: 8px 8px 0 0;
+ }
+
+ .drawer__pager {
+ height: 50vh;
+ }
+
+ .drawer__inner {
+ border-radius: 0 0 8px 8px;
+
+ &.backdrop {
+ width: calc(100% - 60px);
+ box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
+ border-radius: 0 0 0 8px;
+ }
+ }
+
+ &__accounts {
+ overflow-y: auto;
+ }
+
+ .account__display-name {
+ &:hover strong {
+ text-decoration: none;
+ }
+ }
+
+ .account__avatar {
+ cursor: default;
+ }
+
+ .search {
+ margin-bottom: 0;
+ }
+}
diff --git a/app/javascript/styles/mastodon/emoji_picker.scss b/app/javascript/styles/mastodon/emoji_picker.scss
index 2b46d30fc..4161cc045 100644
--- a/app/javascript/styles/mastodon/emoji_picker.scss
+++ b/app/javascript/styles/mastodon/emoji_picker.scss
@@ -95,6 +95,11 @@
padding: 0 6px 6px;
background: $simple-background-color;
will-change: transform;
+
+ &::-webkit-scrollbar-track:hover,
+ &::-webkit-scrollbar-track:active {
+ background-color: rgba($base-overlay-background, 0.3);
+ }
}
.emoji-mart-search {
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 61fcf286f..2bef53cff 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -56,6 +56,12 @@ code {
strong {
font-weight: 500;
+
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
}
.label_input {
@@ -395,6 +401,12 @@ code {
strong {
font-weight: 500;
+
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
}
@media screen and (max-width: 740px) and (min-width: 441px) {
@@ -430,6 +442,12 @@ code {
strong {
color: $ui-secondary-color;
font-weight: 500;
+
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
}
@media screen and (max-width: 740px) and (min-width: 441px) {
@@ -474,6 +492,12 @@ code {
strong {
font-weight: 500;
+
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
}
}
}
@@ -506,6 +530,12 @@ code {
display: block;
margin-bottom: 5px;
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
+
.fa {
font-weight: 400;
}
diff --git a/app/javascript/styles/mastodon/landing_strip.scss b/app/javascript/styles/mastodon/landing_strip.scss
index 15ff84912..ffa1e149d 100644
--- a/app/javascript/styles/mastodon/landing_strip.scss
+++ b/app/javascript/styles/mastodon/landing_strip.scss
@@ -1,4 +1,5 @@
-.landing-strip {
+.landing-strip,
+.memoriam-strip {
background: rgba(darken($ui-base-color, 7%), 0.8);
color: $ui-primary-color;
font-weight: 400;
@@ -11,6 +12,12 @@
strong,
a {
font-weight: 500;
+
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
}
a {
@@ -29,3 +36,76 @@
margin-bottom: 0;
}
}
+
+.memoriam-strip {
+ background: rgba($base-shadow-color, 0.7);
+}
+
+.moved-strip {
+ padding: 14px;
+ border-radius: 4px;
+ background: rgba(darken($ui-base-color, 7%), 0.8);
+ color: $ui-secondary-color;
+ font-weight: 400;
+ margin-bottom: 20px;
+
+ strong,
+ a {
+ font-weight: 500;
+
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
+ }
+
+ a {
+ color: inherit;
+ text-decoration: underline;
+
+ &.mention {
+ text-decoration: none;
+
+ span {
+ text-decoration: none;
+ }
+
+ &:focus,
+ &:hover,
+ &:active {
+ text-decoration: none;
+
+ span {
+ text-decoration: underline;
+ }
+ }
+ }
+ }
+
+ &__message {
+ margin-bottom: 15px;
+
+ .fa {
+ margin-right: 5px;
+ color: $ui-primary-color;
+ }
+ }
+
+ &__card {
+ .detailed-status__display-avatar {
+ position: relative;
+ cursor: pointer;
+ }
+
+ .detailed-status__display-name {
+ margin-bottom: 0;
+ text-decoration: none;
+
+ span {
+ color: $ui-highlight-color;
+ font-weight: 400;
+ }
+ }
+ }
+}
diff --git a/app/javascript/styles/mastodon/rtl.scss b/app/javascript/styles/mastodon/rtl.scss
index 67bfa8a38..77420c84b 100644
--- a/app/javascript/styles/mastodon/rtl.scss
+++ b/app/javascript/styles/mastodon/rtl.scss
@@ -7,9 +7,9 @@ body.rtl {
margin-left: 5px;
}
- .character-counter__wrapper {
- margin-right: 8px;
- margin-left: 16px;
+ .compose-form .compose-form__buttons-wrapper .character-counter__wrapper {
+ margin-right: 0;
+ margin-left: 4px;
}
.navigation-bar__profile {
@@ -30,6 +30,22 @@ body.rtl {
.column-header__buttons {
left: 0;
right: auto;
+ margin-left: -15px;
+ margin-right: 0;
+ }
+
+ .column-inline-form .icon-button {
+ margin-left: 0;
+ margin-right: 5px;
+ }
+
+ .column-header__links .text-btn {
+ margin-left: 10px;
+ margin-right: 0;
+ }
+
+ .account__avatar-wrapper {
+ float: right;
}
.column-header__back-button {
@@ -41,10 +57,6 @@ body.rtl {
float: left;
}
- .compose-form__modifiers {
- border-radius: 0 0 0 4px;
- }
-
.setting-toggle {
margin-left: 0;
margin-right: 8px;
diff --git a/app/javascript/styles/mastodon/stream_entries.scss b/app/javascript/styles/mastodon/stream_entries.scss
index 4f323a378..a35bbee79 100644
--- a/app/javascript/styles/mastodon/stream_entries.scss
+++ b/app/javascript/styles/mastodon/stream_entries.scss
@@ -119,6 +119,12 @@
strong {
font-weight: 500;
color: $ui-base-color;
+
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
}
span {
@@ -170,6 +176,12 @@
strong {
font-weight: 500;
color: $ui-base-color;
+
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
}
span {
diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss
index ad46f5f9f..92870e679 100644
--- a/app/javascript/styles/mastodon/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
@@ -40,6 +40,12 @@
strong {
font-weight: 500;
+
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
}
&.inline-table > tbody > tr:nth-child(odd) > td,
diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/variables.scss
index 52c8cd1cf..dcc2857ff 100644
--- a/app/javascript/styles/mastodon/variables.scss
+++ b/app/javascript/styles/mastodon/variables.scss
@@ -27,3 +27,6 @@ $ui-base-lighter-color: lighten($ui-base-color, 26%) !default; // Lighter darkes
$ui-primary-color: $classic-primary-color !default; // Lighter
$ui-secondary-color: $classic-secondary-color !default; // Lightest
$ui-highlight-color: $classic-highlight-color !default; // Vibrant
+
+// Language codes that uses CJK fonts
+$cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index 01144f595..820189d29 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -3,7 +3,7 @@
class ActivityPub::Activity
include JsonLdHelper
- def initialize(json, account, options = {})
+ def initialize(json, account, **options)
@json = json
@account = account
@object = @json['object']
@@ -15,7 +15,7 @@ class ActivityPub::Activity
end
class << self
- def factory(json, account, options = {})
+ def factory(json, account, **options)
@json = json
klass&.new(json, account, options)
end
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index d6e9bc1de..3a985c19b 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -1,6 +1,9 @@
# frozen_string_literal: true
class ActivityPub::Activity::Create < ActivityPub::Activity
+ SUPPORTED_TYPES = %w(Article Note).freeze
+ CONVERTED_TYPES = %w(Image Video).freeze
+
def perform
return if delete_arrived_first?(object_uri) || unsupported_object_type?
@@ -17,11 +20,13 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
private
def process_status
+ media_attachments = process_attachments
+
ApplicationRecord.transaction do
@status = Status.create!(status_params)
process_tags(@status)
- process_attachments(@status)
+ attach_media(@status, media_attachments)
end
resolve_thread(@status)
@@ -41,7 +46,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
url: object_url || @object['id'],
account: @account,
text: text_from_content || '',
- language: language_from_content,
+ language: detected_language,
spoiler_text: @object['summary'] || '',
created_at: @options[:override_timestamps] ? nil : @object['published'],
reply: @object['inReplyTo'].present?,
@@ -53,9 +58,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end
def process_tags(status)
- return unless @object['tag'].is_a?(Array)
+ return if @object['tag'].nil?
- @object['tag'].each do |tag|
+ as_array(@object['tag']).each do |tag|
case tag['type']
when 'Hashtag'
process_hashtag tag, status
@@ -102,22 +107,36 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
emoji.save
end
- def process_attachments(status)
- return unless @object['attachment'].is_a?(Array)
+ def process_attachments
+ return if @object['attachment'].nil?
- @object['attachment'].each do |attachment|
+ media_attachments = []
+
+ as_array(@object['attachment']).each do |attachment|
next if unsupported_media_type?(attachment['mediaType']) || attachment['url'].blank?
href = Addressable::URI.parse(attachment['url']).normalize.to_s
- media_attachment = MediaAttachment.create(status: status, account: status.account, remote_url: href, description: attachment['name'].presence)
+ media_attachment = MediaAttachment.create(account: @account, remote_url: href, description: attachment['name'].presence)
+ media_attachments << media_attachment
next if skip_download?
media_attachment.file_remote_url = href
media_attachment.save
end
+
+ media_attachments
rescue Addressable::URI::InvalidURIError => e
Rails.logger.debug e
+
+ media_attachments
+ end
+
+ def attach_media(status, media_attachments)
+ return if media_attachments.blank?
+
+ media = MediaAttachment.where(status_id: nil, id: media_attachments.take(4).map(&:id))
+ media.update(status_id: status.id)
end
def resolve_thread(status)
@@ -165,40 +184,62 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end
def text_from_content
+ return Formatter.instance.linkify([text_from_name, object_url || @object['id']].join(' ')) if converted_object_type?
+
if @object['content'].present?
@object['content']
- elsif language_map?
+ elsif content_language_map?
@object['contentMap'].values.first
end
end
- def language_from_content
- return nil unless language_map?
- @object['contentMap'].keys.first
+ def text_from_name
+ if @object['name'].present?
+ @object['name']
+ elsif name_language_map?
+ @object['nameMap'].values.first
+ end
+ end
+
+ def detected_language
+ if content_language_map?
+ @object['contentMap'].keys.first
+ elsif name_language_map?
+ @object['nameMap'].keys.first
+ elsif supported_object_type?
+ LanguageDetector.instance.detect(text_from_content, @account)
+ end
end
def object_url
return if @object['url'].blank?
-
- value = first_of_value(@object['url'])
-
- return value if value.is_a?(String)
-
- value['href']
+ url_to_href(@object['url'], 'text/html')
end
- def language_map?
+ def content_language_map?
@object['contentMap'].is_a?(Hash) && !@object['contentMap'].empty?
end
+ def name_language_map?
+ @object['nameMap'].is_a?(Hash) && !@object['nameMap'].empty?
+ end
+
def unsupported_object_type?
- @object.is_a?(String) || !%w(Article Note).include?(@object['type'])
+ @object.is_a?(String) || !(supported_object_type? || converted_object_type?)
end
def unsupported_media_type?(mime_type)
mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type)
end
+ def supported_object_type?
+ SUPPORTED_TYPES.include?(@object['type'])
+ end
+
+ def converted_object_type?
+ CONVERTED_TYPES.include?(@object['type'])
+ end
+
def skip_download?
return @skip_download if defined?(@skip_download)
@skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
@@ -210,7 +251,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def forward_for_reply
return unless @json['signature'].present? && reply_to_local?
- ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id)
+ ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url])
end
def lock_options
diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb
index 4c6afb090..d0fb49342 100644
--- a/app/lib/activitypub/activity/delete.rb
+++ b/app/lib/activitypub/activity/delete.rb
@@ -30,8 +30,11 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
def forward_for_reblogs(status)
return if @json['signature'].blank?
- ActivityPub::RawDistributionWorker.push_bulk(status.reblogs.includes(:account).references(:account).merge(Account.local).pluck(:account_id)) do |account_id|
- [payload, account_id]
+ rebloggers_ids = status.reblogs.includes(:account).references(:account).merge(Account.local).pluck(:account_id)
+ inboxes = Account.where(id: ::Follow.where(target_account_id: rebloggers_ids).select(:account_id)).inboxes - [@account.preferred_inbox_url]
+
+ ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url|
+ [payload, rebloggers_ids.first, inbox_url]
end
end
diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb
index 790d2025c..90d589d90 100644
--- a/app/lib/activitypub/adapter.rb
+++ b/app/lib/activitypub/adapter.rb
@@ -9,6 +9,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
{
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
'sensitive' => 'as:sensitive',
+ 'movedTo' => 'as:movedTo',
'Hashtag' => 'as:Hashtag',
'ostatus' => 'http://ostatus.org#',
'atomUri' => 'ostatus:atomUri',
diff --git a/app/lib/extractor.rb b/app/lib/extractor.rb
index 957364293..479689d60 100644
--- a/app/lib/extractor.rb
+++ b/app/lib/extractor.rb
@@ -5,7 +5,8 @@ module Extractor
module_function
- def extract_mentions_or_lists_with_indices(text) # :yields: username, list_slug, start, end
+ # :yields: username, list_slug, start, end
+ def extract_mentions_or_lists_with_indices(text)
return [] unless text =~ Twitter::Regex[:at_signs]
possible_entries = []
@@ -31,7 +32,7 @@ module Extractor
possible_entries
end
- def extract_hashtags_with_indices(text, _options = {})
+ def extract_hashtags_with_indices(text, **)
return [] unless text =~ /#/
tags = []
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 58650efb6..20b10e113 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -26,34 +26,42 @@ class FeedManager
end
end
- def push(timeline_type, account, status)
- return false unless add_to_feed(timeline_type, account, status)
-
- trim(timeline_type, account.id)
-
- PushUpdateWorker.perform_async(account.id, status.id) if push_update_required?(timeline_type, account.id)
-
+ def push_to_home(account, status)
+ return false unless add_to_feed(:home, account.id, status)
+ trim(:home, account.id)
+ PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}") if push_update_required?("timeline:#{account.id}")
true
end
- def unpush(timeline_type, account, status)
- return false unless remove_from_feed(timeline_type, account, status)
+ def unpush_from_home(account, status)
+ return false unless remove_from_feed(:home, account.id, status)
+ Redis.current.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s))
+ true
+ end
- payload = Oj.dump(event: :delete, payload: status.id.to_s)
- Redis.current.publish("timeline:#{account.id}", payload)
+ def push_to_list(list, status)
+ return false unless add_to_feed(:list, list.id, status)
+ trim(:list, list.id)
+ PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}") if push_update_required?("timeline:list:#{list.id}")
+ true
+ end
+ def unpush_from_list(list, status)
+ return false unless remove_from_feed(:list, list.id, status)
+ Redis.current.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s))
true
end
def trim(type, account_id)
timeline_key = key(type, account_id)
- reblog_key = key(type, account_id, 'reblogs')
+ reblog_key = key(type, account_id, 'reblogs')
+
# Remove any items past the MAX_ITEMS'th entry in our feed
redis.zremrangebyrank(timeline_key, '0', (-(FeedManager::MAX_ITEMS + 1)).to_s)
# Get the score of the REBLOG_FALLOFF'th item in our feed, and stop
# tracking anything after it for deduplication purposes.
- falloff_rank = FeedManager::REBLOG_FALLOFF - 1
+ falloff_rank = FeedManager::REBLOG_FALLOFF - 1
falloff_range = redis.zrevrange(timeline_key, falloff_rank, falloff_rank, with_scores: true)
falloff_score = falloff_range&.first&.last&.to_i || 0
@@ -69,10 +77,6 @@ class FeedManager
end
end
- def push_update_required?(timeline_type, account_id)
- timeline_type != :home || redis.get("subscribed:timeline:#{account_id}").present?
- end
-
def merge_into_timeline(from_account, into_account)
timeline_key = key(:home, into_account.id)
query = from_account.statuses.limit(FeedManager::MAX_ITEMS / 4)
@@ -84,28 +88,28 @@ class FeedManager
query.each do |status|
next if status.direct_visibility? || filter?(:home, status, into_account)
- add_to_feed(:home, into_account, status)
+ add_to_feed(:home, into_account.id, status)
end
trim(:home, into_account.id)
end
def unmerge_from_timeline(from_account, into_account)
- timeline_key = key(:home, into_account.id)
+ timeline_key = key(:home, into_account.id)
oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
from_account.statuses.select('id, reblog_of_id').where('id > ?', oldest_home_score).reorder(nil).find_each do |status|
- remove_from_feed(:home, into_account, status)
+ remove_from_feed(:home, into_account.id, status)
end
end
def clear_from_timeline(account, target_account)
- timeline_key = key(:home, account.id)
+ timeline_key = key(:home, account.id)
timeline_status_ids = redis.zrange(timeline_key, 0, -1)
- target_statuses = Status.where(id: timeline_status_ids, account: target_account)
+ target_statuses = Status.where(id: timeline_status_ids, account: target_account)
target_statuses.each do |status|
- unpush(:home, account, status)
+ unpush_from_home(account, status)
end
end
@@ -122,7 +126,7 @@ class FeedManager
statuses.each do |status|
next if filter_from_home?(status, account)
- added += 1 if add_to_feed(:home, account, status)
+ added += 1 if add_to_feed(:home, account.id, status)
end
break unless added.zero?
@@ -137,6 +141,10 @@ class FeedManager
Redis.current
end
+ def push_update_required?(timeline_id)
+ redis.exists("subscribed:#{timeline_id}")
+ end
+
def filter_from_home?(status, receiver_id)
return false if receiver_id == status.account_id
return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
@@ -151,14 +159,15 @@ class FeedManager
return true if Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any?
- if status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply
- should_filter = !Follow.where(account_id: receiver_id, target_account_id: status.in_reply_to_account_id).exists? # and I'm not following the person it's a reply to
- should_filter &&= receiver_id != status.in_reply_to_account_id # and it's not a reply to me
- should_filter &&= status.account_id != status.in_reply_to_account_id # and it's not a self-reply
+ if status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply
+ should_filter = !Follow.where(account_id: receiver_id, target_account_id: status.in_reply_to_account_id).exists? # and I'm not following the person it's a reply to
+ should_filter &&= receiver_id != status.in_reply_to_account_id # and it's not a reply to me
+ should_filter &&= status.account_id != status.in_reply_to_account_id # and it's not a self-reply
return should_filter
- elsif status.reblog? # Filter out a reblog
- should_filter = Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists? # or if the author of the reblogged status is blocking me
- should_filter ||= AccountDomainBlock.where(account_id: receiver_id, domain: status.reblog.account.domain).exists? # or the author's domain is blocked
+ elsif status.reblog? # Filter out a reblog
+ should_filter = Follow.where(account_id: receiver_id, target_account_id: status.account_id, show_reblogs: false).exists? # if the reblogger's reblogs are suppressed
+ should_filter ||= Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists? # or if the author of the reblogged status is blocking me
+ should_filter ||= AccountDomainBlock.where(account_id: receiver_id, domain: status.reblog.account.domain).exists? # or the author's domain is blocked
return should_filter
end
@@ -182,9 +191,9 @@ class FeedManager
# added, and false if it was not added to the feed. Note that this is
# an internal helper: callers must call trim or push updates if
# either action is appropriate.
- def add_to_feed(timeline_type, account, status)
- timeline_key = key(timeline_type, account.id)
- reblog_key = key(timeline_type, account.id, 'reblogs')
+ def add_to_feed(timeline_type, account_id, status)
+ timeline_key = key(timeline_type, account_id)
+ reblog_key = key(timeline_type, account_id, 'reblogs')
if status.reblog?
# If the original status or a reblog of it is within
@@ -195,6 +204,7 @@ class FeedManager
return false if !rank.nil? && rank < FeedManager::REBLOG_FALLOFF
reblog_rank = redis.zrevrank(reblog_key, status.reblog_of_id)
+
if reblog_rank.nil?
# This is not something we've already seen reblogged, so we
# can just add it to the feed (and note that we're
@@ -205,7 +215,7 @@ class FeedManager
# Another reblog of the same status was already in the
# REBLOG_FALLOFF most recent statuses, so we note that this
# is an "extra" reblog, by storing it in reblog_set_key.
- reblog_set_key = key(timeline_type, account.id, "reblogs:#{status.reblog_of_id}")
+ reblog_set_key = key(timeline_type, account_id, "reblogs:#{status.reblog_of_id}")
redis.sadd(reblog_set_key, status.id)
return false
end
@@ -220,8 +230,8 @@ class FeedManager
# with reblogs, and returning true if a status was removed. As with
# `add_to_feed`, this does not trigger push updates, so callers must
# do so if appropriate.
- def remove_from_feed(timeline_type, account, status)
- timeline_key = key(timeline_type, account.id)
+ def remove_from_feed(timeline_type, account_id, status)
+ timeline_key = key(timeline_type, account_id)
if status.reblog?
# 1. If the reblogging status is not in the feed, stop.
@@ -229,7 +239,7 @@ class FeedManager
return false if status_rank.nil?
# 2. Remove reblog from set of this status's reblogs.
- reblog_set_key = key(timeline_type, account.id, "reblogs:#{status.reblog_of_id}")
+ reblog_set_key = key(timeline_type, account_id, "reblogs:#{status.reblog_of_id}")
redis.srem(reblog_set_key, status.id)
# 3. Re-insert another reblog or original into the feed if one
@@ -244,7 +254,7 @@ class FeedManager
# (outside conditional)
else
# If the original is getting deleted, no use for reblog references
- redis.del(key(timeline_type, account.id, "reblogs:#{status.id}"))
+ redis.del(key(timeline_type, account_id, "reblogs:#{status.id}"))
end
redis.zrem(timeline_key, status.id)
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index 57f105da7..f5bf64cc7 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -9,7 +9,7 @@ class Formatter
include ActionView::Helpers::TextHelper
- def format(status, options = {})
+ def format(status, **options)
if status.reblog?
prepend_reblog = status.reblog.account.acct
status = status.proper
@@ -51,12 +51,7 @@ class Formatter
def simplified_format(account)
return reformat(account.note).html_safe unless account.local? # rubocop:disable Rails/OutputSafety
-
- html = encode_and_link_urls(account.note)
- html = simple_format(html, {}, sanitize: false)
- html = html.delete("\n")
-
- html.html_safe # rubocop:disable Rails/OutputSafety
+ linkify(account.note)
end
def sanitize(html, config)
@@ -69,6 +64,14 @@ class Formatter
html.html_safe # rubocop:disable Rails/OutputSafety
end
+ def linkify(text)
+ html = encode_and_link_urls(text)
+ html = simple_format(html, {}, sanitize: false)
+ html = html.delete("\n")
+
+ html.html_safe # rubocop:disable Rails/OutputSafety
+ end
+
private
def encode(html)
@@ -89,20 +92,28 @@ class Formatter
end
end
+ def count_tag_nesting(tag)
+ if tag[1] == '/' then -1
+ elsif tag[-2] == '/' then 0
+ else 1
+ end
+ end
+
def encode_custom_emojis(html, emojis)
return html if emojis.empty?
emoji_map = emojis.map { |e| [e.shortcode, full_asset_url(e.image.url(:static))] }.to_h
i = -1
- inside_tag = false
+ tag_open_index = nil
inside_shortname = false
shortname_start_index = -1
+ invisible_depth = 0
while i + 1 < html.size
i += 1
- if inside_shortname && html[i] == ':'
+ if invisible_depth.zero? && inside_shortname && html[i] == ':'
shortcode = html[shortname_start_index + 1..i - 1]
emoji = emoji_map[shortcode]
@@ -116,12 +127,18 @@ class Formatter
end
inside_shortname = false
- elsif inside_tag && html[i] == '>'
- inside_tag = false
+ elsif tag_open_index && html[i] == '>'
+ tag = html[tag_open_index..i]
+ tag_open_index = nil
+ if invisible_depth.positive?
+ invisible_depth += count_tag_nesting(tag)
+ elsif tag == ''
+ invisible_depth = 1
+ end
elsif html[i] == '<'
- inside_tag = true
+ tag_open_index = i
inside_shortname = false
- elsif !inside_tag && html[i] == ':'
+ elsif !tag_open_index && html[i] == ':'
inside_shortname = true
shortname_start_index = i
end
diff --git a/app/lib/language_detector.rb b/app/lib/language_detector.rb
index a42460e10..c6f52f0c7 100644
--- a/app/lib/language_detector.rb
+++ b/app/lib/language_detector.rb
@@ -38,12 +38,31 @@ class LanguageDetector
end
def simplify_text(text)
- text.dup.tap do |new_text|
- new_text.gsub!(FetchLinkCardService::URL_PATTERN, '')
- new_text.gsub!(Account::MENTION_RE, '')
- new_text.gsub!(Tag::HASHTAG_RE, '')
- new_text.gsub!(/\s+/, ' ')
- end
+ new_text = remove_html(text)
+ new_text.gsub!(FetchLinkCardService::URL_PATTERN, '')
+ new_text.gsub!(Account::MENTION_RE, '')
+ new_text.gsub!(Tag::HASHTAG_RE, '')
+ new_text.gsub!(/:#{CustomEmoji::SHORTCODE_RE_FRAGMENT}:/, '')
+ new_text.gsub!(/\s+/, ' ')
+ new_text
+ end
+
+ def new_scrubber
+ scrubber = Rails::Html::PermitScrubber.new
+ scrubber.tags = %w(br p)
+ scrubber
+ end
+
+ def scrubber
+ @scrubber ||= new_scrubber
+ end
+
+ def remove_html(text)
+ text = Loofah.fragment(text).scrub!(scrubber).to_s
+ text.gsub!('
', "\n")
+ text.gsub!('', "\n\n")
+ text.gsub!(/(^
|<\/p>$)/, '')
+ text
end
def default_locale(account)
diff --git a/app/lib/ostatus/activity/base.rb b/app/lib/ostatus/activity/base.rb
index 8b27b124f..c5933f3ad 100644
--- a/app/lib/ostatus/activity/base.rb
+++ b/app/lib/ostatus/activity/base.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class OStatus::Activity::Base
- def initialize(xml, account = nil, options = {})
+ def initialize(xml, account = nil, **options)
@xml = xml
@account = account
@options = options
diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb
index 3418e2420..f210e134a 100644
--- a/app/lib/ostatus/activity/creation.rb
+++ b/app/lib/ostatus/activity/creation.rb
@@ -26,6 +26,8 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
cached_reblog = reblog
status = nil
+ media_attachments = save_media
+
ApplicationRecord.transaction do
status = Status.create!(
uri: id,
@@ -44,7 +46,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
save_mentions(status)
save_hashtags(status)
- save_media(status)
+ attach_media(status, media_attachments)
save_emojis(status)
end
@@ -126,18 +128,20 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
ProcessHashtagsService.new.call(parent, tags)
end
- def save_media(parent)
- do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media?
+ def save_media
+ do_not_download = DomainBlock.find_by(domain: @account.domain)&.reject_media?
+ media_attachments = []
@xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: OStatus::TagManager::XMLNS).each do |link|
next unless link['href']
- media = MediaAttachment.where(status: parent, remote_url: link['href']).first_or_initialize(account: parent.account, status: parent, remote_url: link['href'])
+ media = MediaAttachment.where(status: nil, remote_url: link['href']).first_or_initialize(account: @account, status: nil, remote_url: link['href'])
parsed_url = Addressable::URI.parse(link['href']).normalize
next if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty?
media.save
+ media_attachments << media
next if do_not_download
@@ -148,6 +152,15 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
next
end
end
+
+ media_attachments
+ end
+
+ def attach_media(parent, media_attachments)
+ return if media_attachments.blank?
+
+ media = MediaAttachment.where(status_id: nil, id: media_attachments.take(4).map(&:id))
+ media.update(status_id: parent.id)
end
def save_emojis(parent)
diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb
index 3ca6c5943..656e45822 100644
--- a/app/lib/ostatus/atom_serializer.rb
+++ b/app/lib/ostatus/atom_serializer.rb
@@ -319,7 +319,7 @@ class OStatus::AtomSerializer
private
- def append_element(parent, name, content = nil, attributes = {})
+ def append_element(parent, name, content = nil, **attributes)
element = Ox::Element.new(name)
attributes.each { |k, v| element[k] = sanitize_str(v) }
element << sanitize_str(content) unless content.nil?
diff --git a/app/lib/provider_discovery.rb b/app/lib/provider_discovery.rb
index 5e02e6806..04ba38101 100644
--- a/app/lib/provider_discovery.rb
+++ b/app/lib/provider_discovery.rb
@@ -2,13 +2,26 @@
class ProviderDiscovery < OEmbed::ProviderDiscovery
class << self
- def discover_provider(url, options = {})
- res = Request.new(:get, url).perform
+ def get(url, **options)
+ provider = discover_provider(url, options)
+
+ options.delete(:html)
+
+ provider.get(url, options)
+ end
+
+ def discover_provider(url, **options)
format = options[:format]
- raise OEmbed::NotFound, url if res.code != 200 || res.mime_type != 'text/html'
+ if options[:html]
+ html = Nokogiri::HTML(options[:html])
+ else
+ res = Request.new(:get, url).perform
- html = Nokogiri::HTML(res.to_s)
+ raise OEmbed::NotFound, url if res.code != 200 || res.mime_type != 'text/html'
+
+ html = Nokogiri::HTML(res.to_s)
+ end
if format.nil? || format == :json
provider_endpoint ||= html.at_xpath('//link[@type="application/json+oembed"]')&.attribute('href')&.value
diff --git a/app/lib/request.rb b/app/lib/request.rb
index 30ea0e7ee..7671f4ffc 100644
--- a/app/lib/request.rb
+++ b/app/lib/request.rb
@@ -5,7 +5,7 @@ class Request
include RoutingHelper
- def initialize(verb, url, options = {})
+ def initialize(verb, url, **options)
@verb = verb
@url = Addressable::URI.parse(url).normalize
@options = options
diff --git a/app/lib/status_filter.rb b/app/lib/status_filter.rb
index 27e1f9d30..a6a050ce1 100644
--- a/app/lib/status_filter.rb
+++ b/app/lib/status_filter.rb
@@ -35,7 +35,7 @@ class StatusFilter
end
def silenced_account?
- status_account_silenced? && !account_following_status_account?
+ !account&.silenced? && status_account_silenced? && !account_following_status_account?
end
def status_account_silenced?
diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb
index 80c9d8ccf..fd2b0649a 100644
--- a/app/mailers/notification_mailer.rb
+++ b/app/mailers/notification_mailer.rb
@@ -7,6 +7,8 @@ class NotificationMailer < ApplicationMailer
@me = recipient
@status = notification.target_status
+ return if @me.user.disabled?
+
locale_for_account(@me) do
thread_by_conversation(@status.conversation)
mail to: @me.user.email, subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct)
@@ -17,6 +19,8 @@ class NotificationMailer < ApplicationMailer
@me = recipient
@account = notification.from_account
+ return if @me.user.disabled?
+
locale_for_account(@me) do
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
end
@@ -27,6 +31,8 @@ class NotificationMailer < ApplicationMailer
@account = notification.from_account
@status = notification.target_status
+ return if @me.user.disabled?
+
locale_for_account(@me) do
thread_by_conversation(@status.conversation)
mail to: @me.user.email, subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct)
@@ -38,6 +44,8 @@ class NotificationMailer < ApplicationMailer
@account = notification.from_account
@status = notification.target_status
+ return if @me.user.disabled?
+
locale_for_account(@me) do
thread_by_conversation(@status.conversation)
mail to: @me.user.email, subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct)
@@ -48,26 +56,24 @@ class NotificationMailer < ApplicationMailer
@me = recipient
@account = notification.from_account
+ return if @me.user.disabled?
+
locale_for_account(@me) do
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct)
end
end
- def digest(recipient, opts = {})
+ def digest(recipient, **opts)
@me = recipient
@since = opts[:since] || @me.user.last_emailed_at || @me.user.current_sign_in_at
@notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since)
@follows_since = Notification.where(account: @me, activity_type: 'Follow').where('created_at > ?', @since).count
- return if @notifications.empty?
+ return if @me.user.disabled? || @notifications.empty?
locale_for_account(@me) do
mail to: @me.user.email,
- subject: I18n.t(
- :subject,
- scope: [:notification_mailer, :digest],
- count: @notifications.size
- )
+ subject: I18n.t(:subject, scope: [:notification_mailer, :digest], count: @notifications.size)
end
end
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index c475a9911..5a062dc25 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -5,30 +5,36 @@ class UserMailer < Devise::Mailer
helper :instance
- def confirmation_instructions(user, token, _opts = {})
+ def confirmation_instructions(user, token, **)
@resource = user
@token = token
@instance = Rails.configuration.x.local_domain
+ return if @resource.disabled?
+
I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.unconfirmed_email.blank? ? @resource.email : @resource.unconfirmed_email, subject: I18n.t('devise.mailer.confirmation_instructions.subject', instance: @instance)
end
end
- def reset_password_instructions(user, token, _opts = {})
+ def reset_password_instructions(user, token, **)
@resource = user
@token = token
@instance = Rails.configuration.x.local_domain
+ return if @resource.disabled?
+
I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.reset_password_instructions.subject')
end
end
- def password_change(user, _opts = {})
+ def password_change(user, **)
@resource = user
@instance = Rails.configuration.x.local_domain
+ return if @resource.disabled?
+
I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email, subject: I18n.t('devise.mailer.password_change.subject')
end
diff --git a/app/models/account.rb b/app/models/account.rb
index 3dc2a95ab..686e74044 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -41,10 +41,12 @@
# shared_inbox_url :string default(""), not null
# followers_url :string default(""), not null
# protocol :integer default("ostatus"), not null
+# memorial :boolean default(FALSE), not null
+# moved_to_account_id :integer
#
class Account < ApplicationRecord
- MENTION_RE = /(?:^|[^\/[:word:]])@(([a-z0-9_]+)(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
+ MENTION_RE = /(?<=^|[^\/[:word:]])@(([a-z0-9_]+)(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
include AccountAvatar
include AccountFinderConcern
@@ -52,6 +54,7 @@ class Account < ApplicationRecord
include AccountInteractions
include Attachmentable
include Remotable
+ include Paginable
enum protocol: [:ostatus, :activitypub]
@@ -94,6 +97,13 @@ class Account < ApplicationRecord
has_many :account_moderation_notes, dependent: :destroy
has_many :targeted_moderation_notes, class_name: 'AccountModerationNote', foreign_key: :target_account_id, dependent: :destroy
+ # Lists
+ has_many :list_accounts, inverse_of: :account, dependent: :destroy
+ has_many :lists, through: :list_accounts
+
+ # Account migrations
+ belongs_to :moved_to_account, class_name: 'Account'
+
scope :remote, -> { where.not(domain: nil) }
scope :local, -> { where(domain: nil) }
scope :without_followers, -> { where(followers_count: 0) }
@@ -114,6 +124,8 @@ class Account < ApplicationRecord
:current_sign_in_at,
:confirmed?,
:admin?,
+ :moderator?,
+ :staff?,
:locale,
to: :user,
prefix: true,
@@ -125,6 +137,10 @@ class Account < ApplicationRecord
domain.nil?
end
+ def moved?
+ moved_to_account_id.present?
+ end
+
def acct
local? ? username : "#{username}@#{domain}"
end
@@ -150,10 +166,39 @@ class Account < ApplicationRecord
ResolveRemoteAccountService.new.call(acct)
end
+ def unsuspend!
+ transaction do
+ user&.enable! if local?
+ update!(suspended: false)
+ end
+ end
+
+ def memorialize!
+ transaction do
+ user&.disable! if local?
+ update!(memorial: true)
+ end
+ end
+
def keypair
@keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
end
+ def magic_key
+ modulus, exponent = [keypair.public_key.n, keypair.public_key.e].map do |component|
+ result = []
+
+ until component.zero?
+ result << [component % 256].pack('C')
+ component >>= 8
+ end
+
+ result.reverse.join
+ end
+
+ (['RSA'] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.')
+ end
+
def subscription(webhook_url)
@subscription ||= OStatus2::Subscription.new(remote_url, secret: secret, webhook: webhook_url, hub: hub_url)
end
@@ -184,6 +229,10 @@ class Account < ApplicationRecord
Rails.cache.fetch("exclude_domains_for:#{id}") { domain_blocks.pluck(:domain) }
end
+ def preferred_inbox_url
+ shared_inbox_url.presence || inbox_url
+ end
+
class << self
def readonly_attributes
super - %w(statuses_count following_count followers_count)
@@ -236,6 +285,7 @@ class Account < ApplicationRecord
FROM accounts
WHERE #{query} @@ #{textsearch}
AND accounts.suspended = false
+ AND accounts.moved_to_account_id IS NULL
ORDER BY rank DESC
LIMIT ?
SQL
@@ -243,23 +293,48 @@ class Account < ApplicationRecord
find_by_sql([sql, limit])
end
- def advanced_search_for(terms, account, limit = 10)
+ def advanced_search_for(terms, account, limit = 10, following = false)
textsearch, query = generate_query_for_search(terms)
- sql = <<-SQL.squish
- SELECT
- accounts.*,
- (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
- FROM accounts
- LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
- WHERE #{query} @@ #{textsearch}
- AND accounts.suspended = false
- GROUP BY accounts.id
- ORDER BY rank DESC
- LIMIT ?
- SQL
+ if following
+ sql = <<-SQL.squish
+ WITH first_degree AS (
+ SELECT target_account_id
+ FROM follows
+ WHERE account_id = ?
+ )
+ SELECT
+ accounts.*,
+ (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
+ FROM accounts
+ LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
+ WHERE accounts.id IN (SELECT * FROM first_degree)
+ AND #{query} @@ #{textsearch}
+ AND accounts.suspended = false
+ AND accounts.moved_to_account_id IS NULL
+ GROUP BY accounts.id
+ ORDER BY rank DESC
+ LIMIT ?
+ SQL
- find_by_sql([sql, account.id, account.id, limit])
+ find_by_sql([sql, account.id, account.id, account.id, limit])
+ else
+ sql = <<-SQL.squish
+ SELECT
+ accounts.*,
+ (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
+ FROM accounts
+ LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
+ WHERE #{query} @@ #{textsearch}
+ AND accounts.suspended = false
+ AND accounts.moved_to_account_id IS NULL
+ GROUP BY accounts.id
+ ORDER BY rank DESC
+ LIMIT ?
+ SQL
+
+ find_by_sql([sql, account.id, account.id, limit])
+ end
end
private
diff --git a/app/models/account_domain_block.rb b/app/models/account_domain_block.rb
index fb695e473..35810b6c2 100644
--- a/app/models/account_domain_block.rb
+++ b/app/models/account_domain_block.rb
@@ -3,11 +3,11 @@
#
# Table name: account_domain_blocks
#
+# id :integer not null, primary key
# domain :string
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer
-# id :integer not null, primary key
#
class AccountDomainBlock < ApplicationRecord
diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb
index 189872368..dc7a03039 100644
--- a/app/models/account_filter.rb
+++ b/app/models/account_filter.rb
@@ -45,6 +45,8 @@ class AccountFilter
else
Account.default_scoped
end
+ when 'staff'
+ accounts_with_users.merge User.staff
else
raise "Unknown filter: #{key}"
end
diff --git a/app/models/admin.rb b/app/models/admin.rb
new file mode 100644
index 000000000..d41d18449
--- /dev/null
+++ b/app/models/admin.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Admin
+ def self.table_name_prefix
+ 'admin_'
+ end
+end
diff --git a/app/models/admin/action_log.rb b/app/models/admin/action_log.rb
new file mode 100644
index 000000000..4e950fbf7
--- /dev/null
+++ b/app/models/admin/action_log.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: admin_action_logs
+#
+# id :integer not null, primary key
+# account_id :integer
+# action :string default(""), not null
+# target_type :string
+# target_id :integer
+# recorded_changes :text default(""), not null
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+
+class Admin::ActionLog < ApplicationRecord
+ serialize :recorded_changes
+
+ belongs_to :account, required: true
+ belongs_to :target, required: true, polymorphic: true
+
+ default_scope -> { order('id desc') }
+
+ def action
+ super.to_sym
+ end
+
+ before_validation :set_changes
+
+ private
+
+ def set_changes
+ case action
+ when :destroy, :create
+ self.recorded_changes = target.attributes
+ when :update, :promote, :demote
+ self.recorded_changes = target.previous_changes
+ end
+ end
+end
diff --git a/app/models/block.rb b/app/models/block.rb
index a913782ed..284abfe4c 100644
--- a/app/models/block.rb
+++ b/app/models/block.rb
@@ -3,10 +3,10 @@
#
# Table name: blocks
#
+# id :integer not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer not null
-# id :integer not null, primary key
# target_account_id :integer not null
#
diff --git a/app/models/concerns/account_finder_concern.rb b/app/models/concerns/account_finder_concern.rb
index 561c7ab9f..2e8a7fb37 100644
--- a/app/models/concerns/account_finder_concern.rb
+++ b/app/models/concerns/account_finder_concern.rb
@@ -44,7 +44,7 @@ module AccountFinderConcern
end
def with_usernames
- Account.where.not(username: [nil, ''])
+ Account.where.not(username: '')
end
def matching_username
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index b26520f5b..fdf35a4e3 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -5,7 +5,11 @@ module AccountInteractions
class_methods do
def following_map(target_account_ids, account_id)
- follow_mapping(Follow.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
+ Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping|
+ mapping[follow.target_account_id] = {
+ reblogs: follow.show_reblogs?,
+ }
+ end
end
def followed_by_map(target_account_ids, account_id)
@@ -17,11 +21,19 @@ module AccountInteractions
end
def muting_map(target_account_ids, account_id)
- follow_mapping(Mute.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
+ Mute.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |mute, mapping|
+ mapping[mute.target_account_id] = {
+ notifications: mute.hide_notifications?,
+ }
+ end
end
def requested_map(target_account_ids, account_id)
- follow_mapping(FollowRequest.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
+ FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping|
+ mapping[follow_request.target_account_id] = {
+ reblogs: follow_request.show_reblogs?,
+ }
+ end
end
def domain_blocking_map(target_account_ids, account_id)
@@ -62,16 +74,25 @@ module AccountInteractions
has_many :domain_blocks, class_name: 'AccountDomainBlock', dependent: :destroy
end
- def follow!(other_account)
- active_relationships.find_or_create_by!(target_account: other_account)
+ def follow!(other_account, reblogs: nil)
+ reblogs = true if reblogs.nil?
+ rel = active_relationships.create_with(show_reblogs: reblogs).find_or_create_by!(target_account: other_account)
+ rel.update!(show_reblogs: reblogs)
+
+ rel
end
def block!(other_account)
block_relationships.find_or_create_by!(target_account: other_account)
end
- def mute!(other_account)
- mute_relationships.find_or_create_by!(target_account: other_account)
+ def mute!(other_account, notifications: nil)
+ notifications = true if notifications.nil?
+ mute = mute_relationships.create_with(hide_notifications: notifications).find_or_create_by!(target_account: other_account)
+ # When toggling a mute between hiding and allowing notifications, the mute will already exist, so the find_or_create_by! call will return the existing Mute without updating the hide_notifications attribute. Therefore, we check that hide_notifications? is what we want and set it if it isn't.
+ if mute.hide_notifications? != notifications
+ mute.update!(hide_notifications: notifications)
+ end
end
def mute_conversation!(conversation)
@@ -127,6 +148,14 @@ module AccountInteractions
conversation_mutes.where(conversation: conversation).exists?
end
+ def muting_notifications?(other_account)
+ mute_relationships.where(target_account: other_account, hide_notifications: true).exists?
+ end
+
+ def muting_reblogs?(other_account)
+ active_relationships.where(target_account: other_account, show_reblogs: false).exists?
+ end
+
def requested?(other_account)
follow_requests.where(target_account: other_account).exists?
end
diff --git a/app/models/conversation_mute.rb b/app/models/conversation_mute.rb
index 8d2399adf..248cdfe6e 100644
--- a/app/models/conversation_mute.rb
+++ b/app/models/conversation_mute.rb
@@ -3,9 +3,9 @@
#
# Table name: conversation_mutes
#
+# id :integer not null, primary key
# conversation_id :integer not null
# account_id :integer not null
-# id :integer not null, primary key
#
class ConversationMute < ApplicationRecord
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
index 65d9840d5..a77b53c98 100644
--- a/app/models/custom_emoji.rb
+++ b/app/models/custom_emoji.rb
@@ -15,6 +15,7 @@
# disabled :boolean default(FALSE), not null
# uri :string
# image_remote_url :string
+# visible_in_picker :boolean default(TRUE), not null
#
class CustomEmoji < ApplicationRecord
@@ -24,6 +25,8 @@ class CustomEmoji < ApplicationRecord
:(#{SHORTCODE_RE_FRAGMENT}):
(?=[^[:alnum:]:]|$)/x
+ has_one :local_counterpart, -> { where(domain: nil) }, class_name: 'CustomEmoji', primary_key: :shortcode, foreign_key: :shortcode
+
has_attached_file :image, styles: { static: { format: 'png', convert_options: '-coalesce -strip' } }
validates_attachment :image, content_type: { content_type: 'image/png' }, presence: true, size: { in: 0..50.kilobytes }
diff --git a/app/models/custom_emoji_filter.rb b/app/models/custom_emoji_filter.rb
index 2d1394a59..2c09ed65c 100644
--- a/app/models/custom_emoji_filter.rb
+++ b/app/models/custom_emoji_filter.rb
@@ -27,6 +27,8 @@ class CustomEmojiFilter
CustomEmoji.remote
when 'by_domain'
CustomEmoji.where(domain: value)
+ when 'shortcode'
+ CustomEmoji.where(shortcode: value)
else
raise "Unknown filter: #{key}"
end
diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb
index 1268290bc..aea8919af 100644
--- a/app/models/domain_block.rb
+++ b/app/models/domain_block.rb
@@ -3,12 +3,12 @@
#
# Table name: domain_blocks
#
+# id :integer not null, primary key
# domain :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# severity :integer default("silence")
# reject_media :boolean default(FALSE), not null
-# id :integer not null, primary key
#
class DomainBlock < ApplicationRecord
diff --git a/app/models/email_domain_block.rb b/app/models/email_domain_block.rb
index 839038bea..a104810d1 100644
--- a/app/models/email_domain_block.rb
+++ b/app/models/email_domain_block.rb
@@ -4,14 +4,33 @@
# Table name: email_domain_blocks
#
# id :integer not null, primary key
-# domain :string not null
+# domain :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class EmailDomainBlock < ApplicationRecord
+ before_validation :normalize_domain
+
+ validates :domain, presence: true, uniqueness: true
+
def self.block?(email)
- domain = email.gsub(/.+@([^.]+)/, '\1')
+ _, domain = email.split('@', 2)
+
+ return true if domain.nil?
+
+ begin
+ domain = TagManager.instance.normalize_domain(domain)
+ rescue Addressable::URI::InvalidURIError
+ return true
+ end
+
where(domain: domain).exists?
end
+
+ private
+
+ def normalize_domain
+ self.domain = TagManager.instance.normalize_domain(domain)
+ end
end
diff --git a/app/models/favourite.rb b/app/models/favourite.rb
index d28d5c05b..c38838f2a 100644
--- a/app/models/favourite.rb
+++ b/app/models/favourite.rb
@@ -3,10 +3,10 @@
#
# Table name: favourites
#
+# id :integer not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer not null
-# id :integer not null, primary key
# status_id :integer not null
#
diff --git a/app/models/feed.rb b/app/models/feed.rb
index 5f7b7877a..d99f1ffb2 100644
--- a/app/models/feed.rb
+++ b/app/models/feed.rb
@@ -1,36 +1,27 @@
# frozen_string_literal: true
class Feed
- def initialize(type, account)
- @type = type
- @account = account
+ def initialize(type, id)
+ @type = type
+ @id = id
end
def get(limit, max_id = nil, since_id = nil)
- if redis.exists("account:#{@account.id}:regeneration")
- from_database(limit, max_id, since_id)
- else
- from_redis(limit, max_id, since_id)
- end
+ from_redis(limit, max_id, since_id)
end
- private
+ protected
def from_redis(limit, max_id, since_id)
max_id = '+inf' if max_id.blank?
since_id = '-inf' if since_id.blank?
unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i)
+
Status.where(id: unhydrated).cache_ids
end
- def from_database(limit, max_id, since_id)
- Status.as_home_timeline(@account)
- .paginate_by_max_id(limit, max_id, since_id)
- .reject { |status| FeedManager.instance.filter?(:home, status, @account.id) }
- end
-
def key
- FeedManager.instance.key(@type, @account.id)
+ FeedManager.instance.key(@type, @id)
end
def redis
diff --git a/app/models/follow.rb b/app/models/follow.rb
index 667720a88..3fb665afc 100644
--- a/app/models/follow.rb
+++ b/app/models/follow.rb
@@ -3,11 +3,12 @@
#
# Table name: follows
#
+# id :integer not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer not null
-# id :integer not null, primary key
# target_account_id :integer not null
+# show_reblogs :boolean default(TRUE), not null
#
class Follow < ApplicationRecord
diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb
index 60036d903..ebf6959ce 100644
--- a/app/models/follow_request.rb
+++ b/app/models/follow_request.rb
@@ -3,11 +3,12 @@
#
# Table name: follow_requests
#
+# id :integer not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer not null
-# id :integer not null, primary key
# target_account_id :integer not null
+# show_reblogs :boolean default(TRUE), not null
#
class FollowRequest < ApplicationRecord
@@ -21,13 +22,11 @@ class FollowRequest < ApplicationRecord
validates :account_id, uniqueness: { scope: :target_account_id }
def authorize!
- account.follow!(target_account)
+ account.follow!(target_account, reblogs: show_reblogs)
MergeWorker.perform_async(target_account.id, account.id)
destroy!
end
- def reject!
- destroy!
- end
+ alias reject! destroy!
end
diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb
index 2b148c82b..c1d2cf420 100644
--- a/app/models/form/admin_settings.rb
+++ b/app/models/form/admin_settings.rb
@@ -24,8 +24,12 @@ class Form::AdminSettings
:open_deletion=,
:timeline_preview,
:timeline_preview=,
+ :show_staff_badge,
+ :show_staff_badge=,
:bootstrap_timeline_accounts,
:bootstrap_timeline_accounts=,
+ :min_invite_role,
+ :min_invite_role=,
to: Setting
)
end
diff --git a/app/models/form/migration.rb b/app/models/form/migration.rb
new file mode 100644
index 000000000..b74987337
--- /dev/null
+++ b/app/models/form/migration.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class Form::Migration
+ include ActiveModel::Validations
+
+ attr_accessor :acct, :account
+
+ def initialize(attrs = {})
+ @account = attrs[:account]
+ @acct = attrs[:account].acct unless @account.nil?
+ @acct = attrs[:acct].gsub(/\A@/, '').strip unless attrs[:acct].nil?
+ end
+
+ def valid?
+ return false unless super
+ set_account
+ errors.empty?
+ end
+
+ private
+
+ def set_account
+ self.account = (ResolveRemoteAccountService.new.call(acct) if account.nil? && acct.present?)
+ end
+end
diff --git a/app/models/form/status_batch.rb b/app/models/form/status_batch.rb
index a97b4aa28..4f08a3049 100644
--- a/app/models/form/status_batch.rb
+++ b/app/models/form/status_batch.rb
@@ -2,8 +2,9 @@
class Form::StatusBatch
include ActiveModel::Model
+ include AccountableConcern
- attr_accessor :status_ids, :action
+ attr_accessor :status_ids, :action, :current_account
ACTION_TYPE = %w(nsfw_on nsfw_off delete).freeze
@@ -20,11 +21,14 @@ class Form::StatusBatch
def change_sensitive(sensitive)
media_attached_status_ids = MediaAttachment.where(status_id: status_ids).pluck(:status_id)
+
ApplicationRecord.transaction do
Status.where(id: media_attached_status_ids).find_each do |status|
status.update!(sensitive: sensitive)
+ log_action :update, status
end
end
+
true
rescue ActiveRecord::RecordInvalid
false
@@ -33,7 +37,9 @@ class Form::StatusBatch
def delete_statuses
Status.where(id: status_ids).find_each do |status|
RemovalWorker.perform_async(status.id)
+ log_action :destroy, status
end
+
true
end
end
diff --git a/app/models/home_feed.rb b/app/models/home_feed.rb
new file mode 100644
index 000000000..b943a34ce
--- /dev/null
+++ b/app/models/home_feed.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class HomeFeed < Feed
+ def initialize(account)
+ @type = :home
+ @id = account.id
+ @account = account
+ end
+
+ def get(limit, max_id = nil, since_id = nil)
+ if redis.exists("account:#{@account.id}:regeneration")
+ from_database(limit, max_id, since_id)
+ else
+ super
+ end
+ end
+
+ private
+
+ def from_database(limit, max_id, since_id)
+ Status.as_home_timeline(@account)
+ .paginate_by_max_id(limit, max_id, since_id)
+ .reject { |status| FeedManager.instance.filter?(:home, status, @account.id) }
+ end
+end
diff --git a/app/models/import.rb b/app/models/import.rb
index 8ae7e3a46..091fb3044 100644
--- a/app/models/import.rb
+++ b/app/models/import.rb
@@ -3,6 +3,7 @@
#
# Table name: imports
#
+# id :integer not null, primary key
# type :integer not null
# approved :boolean default(FALSE), not null
# created_at :datetime not null
@@ -12,7 +13,6 @@
# data_file_size :integer
# data_updated_at :datetime
# account_id :integer not null
-# id :integer not null, primary key
#
class Import < ApplicationRecord
diff --git a/app/models/invite.rb b/app/models/invite.rb
new file mode 100644
index 000000000..6907c1f1d
--- /dev/null
+++ b/app/models/invite.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: invites
+#
+# id :integer not null, primary key
+# user_id :integer
+# code :string default(""), not null
+# expires_at :datetime
+# max_uses :integer
+# uses :integer default(0), not null
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+
+class Invite < ApplicationRecord
+ belongs_to :user, required: true
+ has_many :users, inverse_of: :invite
+
+ scope :available, -> { where(expires_at: nil).or(where('expires_at >= ?', Time.now.utc)) }
+ scope :expired, -> { where.not(expires_at: nil).where('expires_at < ?', Time.now.utc) }
+
+ before_validation :set_code
+
+ attr_reader :expires_in
+
+ def expires_in=(interval)
+ self.expires_at = interval.to_i.seconds.from_now unless interval.blank?
+ @expires_in = interval
+ end
+
+ def valid_for_use?
+ (max_uses.nil? || uses < max_uses) && !expired?
+ end
+
+ def expire!
+ touch(:expires_at)
+ end
+
+ def expired?
+ !expires_at.nil? && expires_at < Time.now.utc
+ end
+
+ private
+
+ def set_code
+ loop do
+ self.code = ([*('a'..'z'), *('A'..'Z'), *('0'..'9')] - %w(0 1 I l O)).sample(8).join
+ break if Invite.find_by(code: code).nil?
+ end
+ end
+end
diff --git a/app/models/invite_filter.rb b/app/models/invite_filter.rb
new file mode 100644
index 000000000..7d89bad4a
--- /dev/null
+++ b/app/models/invite_filter.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class InviteFilter
+ attr_reader :params
+
+ def initialize(params)
+ @params = params
+ end
+
+ def results
+ scope = Invite.order(created_at: :desc)
+
+ params.each do |key, value|
+ scope.merge!(scope_for(key, value)) if value.present?
+ end
+
+ scope
+ end
+
+ private
+
+ def scope_for(key, _value)
+ case key.to_s
+ when 'available'
+ Invite.available
+ when 'expired'
+ Invite.expired
+ else
+ raise "Unknown filter: #{key}"
+ end
+ end
+end
diff --git a/app/models/list.rb b/app/models/list.rb
new file mode 100644
index 000000000..be85c3b87
--- /dev/null
+++ b/app/models/list.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: lists
+#
+# id :integer not null, primary key
+# account_id :integer not null
+# title :string default(""), not null
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+
+class List < ApplicationRecord
+ include Paginable
+
+ PER_ACCOUNT_LIMIT = 50
+
+ belongs_to :account
+
+ has_many :list_accounts, inverse_of: :list, dependent: :destroy
+ has_many :accounts, through: :list_accounts
+
+ validates :title, presence: true
+
+ validates_each :account_id, on: :create do |record, _attr, value|
+ record.errors.add(:base, I18n.t('lists.errors.limit')) if List.where(account_id: value).count >= PER_ACCOUNT_LIMIT
+ end
+
+ before_destroy :clean_feed_manager
+
+ private
+
+ def clean_feed_manager
+ reblog_key = FeedManager.instance.key(:list, id, 'reblogs')
+ reblogged_id_set = Redis.current.zrange(reblog_key, 0, -1)
+
+ Redis.current.pipelined do
+ Redis.current.del(FeedManager.instance.key(:list, id))
+ Redis.current.del(reblog_key)
+
+ reblogged_id_set.each do |reblogged_id|
+ reblog_set_key = FeedManager.instance.key(:list, id, "reblogs:#{reblogged_id}")
+ Redis.current.del(reblog_set_key)
+ end
+ end
+ end
+end
diff --git a/app/models/list_account.rb b/app/models/list_account.rb
new file mode 100644
index 000000000..253932590
--- /dev/null
+++ b/app/models/list_account.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: list_accounts
+#
+# id :integer not null, primary key
+# list_id :integer not null
+# account_id :integer not null
+# follow_id :integer not null
+#
+
+class ListAccount < ApplicationRecord
+ belongs_to :list, required: true
+ belongs_to :account, required: true
+ belongs_to :follow, required: true
+
+ validates :account_id, uniqueness: { scope: :list_id }
+
+ before_validation :set_follow
+
+ private
+
+ def set_follow
+ self.follow = Follow.find_by(account_id: list.account_id, target_account_id: account.id)
+ end
+end
diff --git a/app/models/list_feed.rb b/app/models/list_feed.rb
new file mode 100644
index 000000000..f371e4ed9
--- /dev/null
+++ b/app/models/list_feed.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class ListFeed < Feed
+ def initialize(list)
+ @type = :list
+ @id = list.id
+ end
+end
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 60380198b..abc5ab854 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -10,12 +10,12 @@
# file_file_size :integer
# file_updated_at :datetime
# remote_url :string default(""), not null
-# account_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# shortcode :string
# type :integer default("image"), not null
# file_meta :json
+# account_id :integer
# description :text
#
diff --git a/app/models/mention.rb b/app/models/mention.rb
index 3700c781c..14533e6a9 100644
--- a/app/models/mention.rb
+++ b/app/models/mention.rb
@@ -3,11 +3,11 @@
#
# Table name: mentions
#
+# id :integer not null, primary key
# status_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer
-# id :integer not null, primary key
#
class Mention < ApplicationRecord
diff --git a/app/models/mute.rb b/app/models/mute.rb
index 6e64848c7..105696da6 100644
--- a/app/models/mute.rb
+++ b/app/models/mute.rb
@@ -3,11 +3,12 @@
#
# Table name: mutes
#
-# created_at :datetime not null
-# updated_at :datetime not null
-# account_id :integer not null
-# id :integer not null, primary key
-# target_account_id :integer not null
+# id :integer not null, primary key
+# created_at :datetime not null
+# updated_at :datetime not null
+# account_id :integer not null
+# target_account_id :integer not null
+# hide_notifications :boolean default(TRUE), not null
#
class Mute < ApplicationRecord
diff --git a/app/models/notification.rb b/app/models/notification.rb
index 0a5d987cf..976963528 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -4,11 +4,11 @@
# Table name: notifications
#
# id :integer not null, primary key
-# account_id :integer
# activity_id :integer
# activity_type :string
# created_at :datetime not null
# updated_at :datetime not null
+# account_id :integer
# from_account_id :integer
#
@@ -24,7 +24,7 @@ class Notification < ApplicationRecord
favourite: 'Favourite',
}.freeze
- STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :media_attachments, :tags, mentions: :account]].freeze
+ STATUS_INCLUDES = [:account, :application, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :application, :media_attachments, :tags, mentions: :account]].freeze
belongs_to :account
belongs_to :from_account, class_name: 'Account'
@@ -55,9 +55,11 @@ class Notification < ApplicationRecord
def target_status
case type
when :reblog
- activity&.reblog
- when :favourite, :mention
- activity&.status
+ status&.reblog
+ when :favourite
+ favourite&.status
+ when :mention
+ mention&.status
end
end
diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb
index e2bf65d94..716b82243 100644
--- a/app/models/preview_card.rb
+++ b/app/models/preview_card.rb
@@ -21,6 +21,7 @@
# height :integer default(0), not null
# created_at :datetime not null
# updated_at :datetime not null
+# embed_url :string default(""), not null
#
class PreviewCard < ApplicationRecord
@@ -32,7 +33,7 @@ class PreviewCard < ApplicationRecord
has_and_belongs_to_many :statuses
- has_attached_file :image, styles: { original: '280x280>' }, convert_options: { all: '-quality 80 -strip' }
+ has_attached_file :image, styles: { original: '400x400>' }, convert_options: { all: '-quality 80 -strip' }
include Attachmentable
include Remotable
diff --git a/app/models/remote_follow.rb b/app/models/remote_follow.rb
index c3f867743..070144e2d 100644
--- a/app/models/remote_follow.rb
+++ b/app/models/remote_follow.rb
@@ -7,8 +7,8 @@ class RemoteFollow
validates :acct, presence: true
- def initialize(attrs = {})
- @acct = attrs[:acct].gsub(/\A@/, '').strip unless attrs[:acct].nil?
+ def initialize(attrs = nil)
+ @acct = attrs[:acct].gsub(/\A@/, '').strip if !attrs.nil? && !attrs[:acct].nil?
end
def valid?
diff --git a/app/models/report.rb b/app/models/report.rb
index bffb42b48..c36f8db0a 100644
--- a/app/models/report.rb
+++ b/app/models/report.rb
@@ -3,6 +3,7 @@
#
# Table name: reports
#
+# id :integer not null, primary key
# status_ids :integer default([]), not null, is an Array
# comment :text default(""), not null
# action_taken :boolean default(FALSE), not null
@@ -10,7 +11,6 @@
# updated_at :datetime not null
# account_id :integer not null
# action_taken_by_account_id :integer
-# id :integer not null, primary key
# target_account_id :integer not null
#
diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb
index c1645223b..1d4ebca02 100644
--- a/app/models/session_activation.rb
+++ b/app/models/session_activation.rb
@@ -4,26 +4,16 @@
# Table name: session_activations
#
# id :integer not null, primary key
-# user_id :integer not null
# session_id :string not null
# created_at :datetime not null
# updated_at :datetime not null
# user_agent :string default(""), not null
# ip :inet
# access_token_id :integer
+# user_id :integer not null
# web_push_subscription_id :integer
#
-# id :integer not null, primary key
-# user_id :integer not null
-# session_id :string not null
-# created_at :datetime not null
-# updated_at :datetime not null
-# user_agent :string default(""), not null
-# ip :inet
-# access_token_id :integer
-#
-
class SessionActivation < ApplicationRecord
belongs_to :user, inverse_of: :session_activations, required: true
belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', dependent: :destroy
@@ -53,7 +43,7 @@ class SessionActivation < ApplicationRecord
id && where(session_id: id).exists?
end
- def activate(options = {})
+ def activate(**options)
activation = create!(options)
purge_old
activation
diff --git a/app/models/setting.rb b/app/models/setting.rb
index a14f156a1..df93590ce 100644
--- a/app/models/setting.rb
+++ b/app/models/setting.rb
@@ -3,12 +3,12 @@
#
# Table name: settings
#
+# id :integer not null, primary key
# var :string not null
# value :text
# thing_type :string
# created_at :datetime
# updated_at :datetime
-# id :integer not null, primary key
# thing_id :integer
#
diff --git a/app/models/status.rb b/app/models/status.rb
index 5a7245613..8579ff9e4 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -5,7 +5,6 @@
#
# id :integer not null, primary key
# uri :string
-# account_id :integer not null
# text :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
@@ -14,8 +13,6 @@
# url :string
# sensitive :boolean default(FALSE), not null
# visibility :integer default("public"), not null
-# in_reply_to_account_id :integer
-# application_id :integer
# spoiler_text :text default(""), not null
# reply :boolean default(FALSE), not null
# favourites_count :integer default(0), not null
@@ -23,6 +20,9 @@
# language :string
# conversation_id :integer
# local :boolean
+# account_id :integer not null
+# application_id :integer
+# in_reply_to_account_id :integer
#
class Status < ApplicationRecord
@@ -175,7 +175,7 @@ class Status < ApplicationRecord
end
def reblogs_map(status_ids, account_id)
- select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).map { |s| [s.reblog_of_id, true] }.to_h
+ select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).reorder(nil).map { |s| [s.reblog_of_id, true] }.to_h
end
def mutes_map(conversation_ids, account_id)
@@ -278,6 +278,7 @@ class Status < ApplicationRecord
def set_visibility
self.visibility = (account.locked? ? :private : :public) if visibility.nil?
+ self.visibility = reblog.visibility if reblog?
self.sensitive = false if sensitive.nil?
end
diff --git a/app/models/stream_entry.rb b/app/models/stream_entry.rb
index b51fe9ad7..2ae034d93 100644
--- a/app/models/stream_entry.rb
+++ b/app/models/stream_entry.rb
@@ -3,13 +3,13 @@
#
# Table name: stream_entries
#
+# id :integer not null, primary key
# activity_id :integer
# activity_type :string
# created_at :datetime not null
# updated_at :datetime not null
# hidden :boolean default(FALSE), not null
# account_id :integer
-# id :integer not null, primary key
#
class StreamEntry < ApplicationRecord
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
index 39860196b..7f2eeab91 100644
--- a/app/models/subscription.rb
+++ b/app/models/subscription.rb
@@ -3,6 +3,7 @@
#
# Table name: subscriptions
#
+# id :integer not null, primary key
# callback_url :string default(""), not null
# secret :string
# expires_at :datetime
@@ -12,7 +13,6 @@
# last_successful_delivery_at :datetime
# domain :string
# account_id :integer not null
-# id :integer not null, primary key
#
class Subscription < ApplicationRecord
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 0fa08e157..dc2c8d129 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -23,7 +23,7 @@ class Tag < ApplicationRecord
class << self
def search_for(term, limit = 5)
- pattern = sanitize_sql_like(term) + '%'
+ pattern = sanitize_sql_like(term.strip) + '%'
Tag.where('lower(name) like lower(?)', pattern).order(:name).limit(limit)
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 325e27f44..578622fdf 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -5,7 +5,6 @@
#
# id :integer not null, primary key
# email :string default(""), not null
-# account_id :integer not null
# created_at :datetime not null
# updated_at :datetime not null
# encrypted_password :string default(""), not null
@@ -31,10 +30,15 @@
# last_emailed_at :datetime
# otp_backup_codes :string is an Array
# filtered_languages :string default([]), not null, is an Array
+# account_id :integer not null
+# disabled :boolean default(FALSE), not null
+# moderator :boolean default(FALSE), not null
+# invite_id :integer
#
class User < ApplicationRecord
include Settings::Extend
+
ACTIVE_DURATION = 14.days
devise :registerable, :recoverable,
@@ -44,6 +48,7 @@ class User < ApplicationRecord
otp_number_of_backup_codes: 10
belongs_to :account, inverse_of: :user, required: true
+ belongs_to :invite, counter_cache: :uses
accepts_nested_attributes_for :account
has_many :applications, class_name: 'Doorkeeper::Application', as: :owner
@@ -51,8 +56,10 @@ class User < ApplicationRecord
validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
validates_with BlacklistedEmailValidator, if: :email_changed?
- scope :recent, -> { order(id: :desc) }
- scope :admins, -> { where(admin: true) }
+ scope :recent, -> { order(id: :desc) }
+ scope :admins, -> { where(admin: true) }
+ scope :moderators, -> { where(moderator: true) }
+ scope :staff, -> { admins.or(moderators) }
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) }
scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended: false }) }
@@ -68,56 +75,88 @@ class User < ApplicationRecord
has_many :session_activations, dependent: :destroy
+ delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal,
+ :reduce_motion, :system_font_ui, :noindex, :theme,
+ to: :settings, prefix: :setting, allow_nil: false
+
+ attr_accessor :invite_code
+
def confirmed?
confirmed_at.present?
end
+ def staff?
+ admin? || moderator?
+ end
+
+ def role
+ if admin?
+ 'admin'
+ elsif moderator?
+ 'moderator'
+ else
+ 'user'
+ end
+ end
+
+ def role?(role)
+ case role
+ when 'user'
+ true
+ when 'moderator'
+ staff?
+ when 'admin'
+ admin?
+ else
+ false
+ end
+ end
+
+ def disable!
+ update!(disabled: true,
+ last_sign_in_at: current_sign_in_at,
+ current_sign_in_at: nil)
+ end
+
+ def enable!
+ update!(disabled: false)
+ end
+
+ def confirm!
+ skip_confirmation!
+ save!
+ end
+
+ def promote!
+ if moderator?
+ update!(moderator: false, admin: true)
+ elsif !admin?
+ update!(moderator: true)
+ end
+ end
+
+ def demote!
+ if admin?
+ update!(admin: false, moderator: true)
+ elsif moderator?
+ update!(moderator: false)
+ end
+ end
+
def disable_two_factor!
self.otp_required_for_login = false
otp_backup_codes&.clear
save!
end
+ def active_for_authentication?
+ super && !disabled?
+ end
+
def setting_default_privacy
settings.default_privacy || (account.locked? ? 'private' : 'public')
end
- def setting_default_sensitive
- settings.default_sensitive
- end
-
- def setting_unfollow_modal
- settings.unfollow_modal
- end
-
- def setting_boost_modal
- settings.boost_modal
- end
-
- def setting_delete_modal
- settings.delete_modal
- end
-
- def setting_auto_play_gif
- settings.auto_play_gif
- end
-
- def setting_reduce_motion
- settings.reduce_motion
- end
-
- def setting_system_font_ui
- settings.system_font_ui
- end
-
- def setting_noindex
- settings.noindex
- end
-
- def setting_theme
- settings.theme
- end
-
def token_for_app(a)
return nil if a.nil? || a.owner != self
Doorkeeper::AccessToken
@@ -147,6 +186,11 @@ class User < ApplicationRecord
session.web_push_subscription.nil? ? nil : session.web_push_subscription.as_payload
end
+ def invite_code=(code)
+ self.invite = Invite.find_by(code: code) unless code.blank?
+ @invite_code = code
+ end
+
protected
def send_devise_notification(notification, *args)
diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb
index cb15dfa37..5aee92d27 100644
--- a/app/models/web/push_subscription.rb
+++ b/app/models/web/push_subscription.rb
@@ -24,12 +24,12 @@ class Web::PushSubscription < ApplicationRecord
end
def pushable?(notification)
- data && data.key?('alerts') && data['alerts'][notification.type.to_s]
+ data&.key?('alerts') && data['alerts'][notification.type.to_s]
end
def as_payload
payload = { id: id, endpoint: endpoint }
- payload[:alerts] = data['alerts'] if data && data.key?('alerts')
+ payload[:alerts] = data['alerts'] if data&.key?('alerts')
payload
end
diff --git a/app/models/web/setting.rb b/app/models/web/setting.rb
index 1b0bfb2b7..12b9d1226 100644
--- a/app/models/web/setting.rb
+++ b/app/models/web/setting.rb
@@ -3,10 +3,10 @@
#
# Table name: web_settings
#
+# id :integer not null, primary key
# data :json
# created_at :datetime not null
# updated_at :datetime not null
-# id :integer not null, primary key
# user_id :integer
#
diff --git a/app/policies/account_moderation_note_policy.rb b/app/policies/account_moderation_note_policy.rb
new file mode 100644
index 000000000..885411a5b
--- /dev/null
+++ b/app/policies/account_moderation_note_policy.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AccountModerationNotePolicy < ApplicationPolicy
+ def create?
+ staff?
+ end
+
+ def destroy?
+ admin? || owner?
+ end
+
+ private
+
+ def owner?
+ record.account_id == current_account&.id
+ end
+end
diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb
new file mode 100644
index 000000000..85e2c8419
--- /dev/null
+++ b/app/policies/account_policy.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+class AccountPolicy < ApplicationPolicy
+ def index?
+ staff?
+ end
+
+ def show?
+ staff?
+ end
+
+ def suspend?
+ staff? && !record.user&.staff?
+ end
+
+ def unsuspend?
+ staff?
+ end
+
+ def silence?
+ staff? && !record.user&.staff?
+ end
+
+ def unsilence?
+ staff?
+ end
+
+ def redownload?
+ admin?
+ end
+
+ def subscribe?
+ admin?
+ end
+
+ def unsubscribe?
+ admin?
+ end
+
+ def memorialize?
+ admin? && !record.user&.admin?
+ end
+end
diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb
new file mode 100644
index 000000000..3e617001f
--- /dev/null
+++ b/app/policies/application_policy.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class ApplicationPolicy
+ attr_reader :current_account, :record
+
+ def initialize(current_account, record)
+ @current_account = current_account
+ @record = record
+ end
+
+ delegate :admin?, :moderator?, :staff?, to: :current_user, allow_nil: true
+
+ private
+
+ def current_user
+ current_account&.user
+ end
+end
diff --git a/app/policies/custom_emoji_policy.rb b/app/policies/custom_emoji_policy.rb
new file mode 100644
index 000000000..a8c3cbc73
--- /dev/null
+++ b/app/policies/custom_emoji_policy.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class CustomEmojiPolicy < ApplicationPolicy
+ def index?
+ staff?
+ end
+
+ def create?
+ admin?
+ end
+
+ def update?
+ admin?
+ end
+
+ def copy?
+ admin?
+ end
+
+ def enable?
+ staff?
+ end
+
+ def disable?
+ staff?
+ end
+
+ def destroy?
+ admin?
+ end
+end
diff --git a/app/policies/domain_block_policy.rb b/app/policies/domain_block_policy.rb
new file mode 100644
index 000000000..47c0a81af
--- /dev/null
+++ b/app/policies/domain_block_policy.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class DomainBlockPolicy < ApplicationPolicy
+ def index?
+ admin?
+ end
+
+ def show?
+ admin?
+ end
+
+ def create?
+ admin?
+ end
+
+ def destroy?
+ admin?
+ end
+end
diff --git a/app/policies/email_domain_block_policy.rb b/app/policies/email_domain_block_policy.rb
new file mode 100644
index 000000000..5a75ee183
--- /dev/null
+++ b/app/policies/email_domain_block_policy.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class EmailDomainBlockPolicy < ApplicationPolicy
+ def index?
+ admin?
+ end
+
+ def create?
+ admin?
+ end
+
+ def destroy?
+ admin?
+ end
+end
diff --git a/app/policies/instance_policy.rb b/app/policies/instance_policy.rb
new file mode 100644
index 000000000..d1956e2de
--- /dev/null
+++ b/app/policies/instance_policy.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class InstancePolicy < ApplicationPolicy
+ def index?
+ admin?
+ end
+
+ def resubscribe?
+ admin?
+ end
+end
diff --git a/app/policies/invite_policy.rb b/app/policies/invite_policy.rb
new file mode 100644
index 000000000..a2a65f934
--- /dev/null
+++ b/app/policies/invite_policy.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class InvitePolicy < ApplicationPolicy
+ def index?
+ staff?
+ end
+
+ def create?
+ min_required_role?
+ end
+
+ def destroy?
+ owner? || (Setting.min_invite_role == 'admin' ? admin? : staff?)
+ end
+
+ private
+
+ def owner?
+ record.user_id == current_user&.id
+ end
+
+ def min_required_role?
+ current_user&.role?(Setting.min_invite_role)
+ end
+end
diff --git a/app/policies/report_policy.rb b/app/policies/report_policy.rb
new file mode 100644
index 000000000..95b5c30c8
--- /dev/null
+++ b/app/policies/report_policy.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class ReportPolicy < ApplicationPolicy
+ def update?
+ staff?
+ end
+
+ def index?
+ staff?
+ end
+
+ def show?
+ staff?
+ end
+end
diff --git a/app/policies/settings_policy.rb b/app/policies/settings_policy.rb
new file mode 100644
index 000000000..2dcb79f51
--- /dev/null
+++ b/app/policies/settings_policy.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class SettingsPolicy < ApplicationPolicy
+ def update?
+ admin?
+ end
+
+ def show?
+ admin?
+ end
+end
diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb
index 2ded61850..0373fdf04 100644
--- a/app/policies/status_policy.rb
+++ b/app/policies/status_policy.rb
@@ -1,20 +1,17 @@
# frozen_string_literal: true
-class StatusPolicy
- attr_reader :account, :status
-
- def initialize(account, status)
- @account = account
- @status = status
+class StatusPolicy < ApplicationPolicy
+ def index?
+ staff?
end
def show?
if direct?
- owned? || status.mentions.where(account: account).exists?
+ owned? || record.mentions.where(account: current_account).exists?
elsif private?
- owned? || account&.following?(status.account) || status.mentions.where(account: account).exists?
+ owned? || current_account&.following?(author) || record.mentions.where(account: current_account).exists?
else
- account.nil? || !status.account.blocking?(account)
+ current_account.nil? || !author.blocking?(current_account)
end
end
@@ -23,26 +20,30 @@ class StatusPolicy
end
def destroy?
- admin? || owned?
+ staff? || owned?
end
alias unreblog? destroy?
- private
-
- def admin?
- account&.user&.admin?
+ def update?
+ staff?
end
+ private
+
def direct?
- status.direct_visibility?
+ record.direct_visibility?
end
def owned?
- status.account.id == account&.id
+ author.id == current_account&.id
end
def private?
- status.private_visibility?
+ record.private_visibility?
+ end
+
+ def author
+ record.account
end
end
diff --git a/app/policies/subscription_policy.rb b/app/policies/subscription_policy.rb
new file mode 100644
index 000000000..ac9a8a6c4
--- /dev/null
+++ b/app/policies/subscription_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class SubscriptionPolicy < ApplicationPolicy
+ def index?
+ admin?
+ end
+end
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
new file mode 100644
index 000000000..aae207d06
--- /dev/null
+++ b/app/policies/user_policy.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+class UserPolicy < ApplicationPolicy
+ def reset_password?
+ staff? && !record.staff?
+ end
+
+ def disable_2fa?
+ admin? && !record.staff?
+ end
+
+ def confirm?
+ staff? && !record.confirmed?
+ end
+
+ def enable?
+ admin?
+ end
+
+ def disable?
+ admin? && !record.admin?
+ end
+
+ def promote?
+ admin? && promoteable?
+ end
+
+ def demote?
+ admin? && !record.admin? && demoteable?
+ end
+
+ private
+
+ def promoteable?
+ !record.staff? || !record.admin?
+ end
+
+ def demoteable?
+ record.staff?
+ end
+end
diff --git a/app/presenters/account_relationships_presenter.rb b/app/presenters/account_relationships_presenter.rb
index a30558bac..bf1ba3716 100644
--- a/app/presenters/account_relationships_presenter.rb
+++ b/app/presenters/account_relationships_presenter.rb
@@ -4,7 +4,7 @@ class AccountRelationshipsPresenter
attr_reader :following, :followed_by, :blocking,
:muting, :requested, :domain_blocking
- def initialize(account_ids, current_account_id, options = {})
+ def initialize(account_ids, current_account_id, **options)
@following = Account.following_map(account_ids, current_account_id).merge(options[:following_map] || {})
@followed_by = Account.followed_by_map(account_ids, current_account_id).merge(options[:followed_by_map] || {})
@blocking = Account.blocking_map(account_ids, current_account_id).merge(options[:blocking_map] || {})
diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb
index bc3887a44..b04e10e2f 100644
--- a/app/presenters/status_relationships_presenter.rb
+++ b/app/presenters/status_relationships_presenter.rb
@@ -3,7 +3,7 @@
class StatusRelationshipsPresenter
attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map
- def initialize(statuses, current_account_id = nil, options = {})
+ def initialize(statuses, current_account_id = nil, **options)
if current_account_id.nil?
@reblogs_map = {}
@favourites_map = {}
diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb
index 896d67115..622bdde0c 100644
--- a/app/serializers/activitypub/actor_serializer.rb
+++ b/app/serializers/activitypub/actor_serializer.rb
@@ -10,6 +10,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
has_one :public_key, serializer: ActivityPub::PublicKeySerializer
+ attribute :moved_to, if: :moved?
+
class EndpointsSerializer < ActiveModel::Serializer
include RoutingHelper
@@ -25,6 +27,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
has_one :icon, serializer: ActivityPub::ImageSerializer, if: :avatar_exists?
has_one :image, serializer: ActivityPub::ImageSerializer, if: :header_exists?
+ delegate :moved?, to: :object
+
def id
account_url(object)
end
@@ -92,4 +96,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
def manually_approves_followers
object.locked
end
+
+ def moved_to
+ ActivityPub::TagManager.instance.uri_for(object.moved_to_account)
+ end
end
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index a8db73161..4fa1981ed 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -7,7 +7,7 @@ class InitialStateSerializer < ActiveModel::Serializer
has_many :custom_emojis, serializer: REST::CustomEmojiSerializer
def custom_emojis
- CustomEmoji.local
+ CustomEmoji.local.where(disabled: false)
end
def meta
diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb
index 65fdb0308..19b746520 100644
--- a/app/serializers/rest/account_serializer.rb
+++ b/app/serializers/rest/account_serializer.rb
@@ -7,6 +7,8 @@ class REST::AccountSerializer < ActiveModel::Serializer
:note, :url, :avatar, :avatar_static, :header, :header_static,
:followers_count, :following_count, :statuses_count
+ has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?
+
def id
object.id.to_s
end
@@ -34,4 +36,8 @@ class REST::AccountSerializer < ActiveModel::Serializer
def header_static
full_asset_url(object.header_static_url)
end
+
+ def moved_and_not_nested?
+ object.moved? && object.moved_to_account.moved_to_account_id.nil?
+ end
end
diff --git a/app/serializers/rest/custom_emoji_serializer.rb b/app/serializers/rest/custom_emoji_serializer.rb
index b958e6a5d..65686a866 100644
--- a/app/serializers/rest/custom_emoji_serializer.rb
+++ b/app/serializers/rest/custom_emoji_serializer.rb
@@ -3,7 +3,7 @@
class REST::CustomEmojiSerializer < ActiveModel::Serializer
include RoutingHelper
- attributes :shortcode, :url, :static_url
+ attributes :shortcode, :url, :static_url, :visible_in_picker
def url
full_asset_url(object.image.url)
diff --git a/app/serializers/rest/list_serializer.rb b/app/serializers/rest/list_serializer.rb
new file mode 100644
index 000000000..977da7439
--- /dev/null
+++ b/app/serializers/rest/list_serializer.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class REST::ListSerializer < ActiveModel::Serializer
+ attributes :id, :title
+
+ def id
+ object.id.to_s
+ end
+end
diff --git a/app/serializers/rest/preview_card_serializer.rb b/app/serializers/rest/preview_card_serializer.rb
index 9c460332c..2df9d07a7 100644
--- a/app/serializers/rest/preview_card_serializer.rb
+++ b/app/serializers/rest/preview_card_serializer.rb
@@ -6,7 +6,7 @@ class REST::PreviewCardSerializer < ActiveModel::Serializer
attributes :url, :title, :description, :type,
:author_name, :author_url, :provider_name,
:provider_url, :html, :width, :height,
- :image
+ :image, :embed_url
def image
object.image? ? full_asset_url(object.image.url(:original)) : nil
diff --git a/app/serializers/rest/relationship_serializer.rb b/app/serializers/rest/relationship_serializer.rb
index 998727e37..45bfd4d6e 100644
--- a/app/serializers/rest/relationship_serializer.rb
+++ b/app/serializers/rest/relationship_serializer.rb
@@ -1,15 +1,21 @@
# frozen_string_literal: true
class REST::RelationshipSerializer < ActiveModel::Serializer
- attributes :id, :following, :followed_by, :blocking,
- :muting, :requested, :domain_blocking
+ attributes :id, :following, :showing_reblogs, :followed_by, :blocking,
+ :muting, :muting_notifications, :requested, :domain_blocking
def id
object.id.to_s
end
def following
- instance_options[:relationships].following[object.id] || false
+ instance_options[:relationships].following[object.id] ? true : false
+ end
+
+ def showing_reblogs
+ (instance_options[:relationships].following[object.id] || {})[:reblogs] ||
+ (instance_options[:relationships].requested[object.id] || {})[:reblogs] ||
+ false
end
def followed_by
@@ -21,11 +27,15 @@ class REST::RelationshipSerializer < ActiveModel::Serializer
end
def muting
- instance_options[:relationships].muting[object.id] || false
+ instance_options[:relationships].muting[object.id] ? true : false
+ end
+
+ def muting_notifications
+ (instance_options[:relationships].muting[object.id] || {})[:notifications] || false
end
def requested
- instance_options[:relationships].requested[object.id] || false
+ instance_options[:relationships].requested[object.id] ? true : false
end
def domain_blocking
diff --git a/app/serializers/webfinger_serializer.rb b/app/serializers/webfinger_serializer.rb
new file mode 100644
index 000000000..f80d12c02
--- /dev/null
+++ b/app/serializers/webfinger_serializer.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class WebfingerSerializer < ActiveModel::Serializer
+ include RoutingHelper
+
+ attributes :subject, :aliases, :links
+
+ def subject
+ object.to_webfinger_s
+ end
+
+ def aliases
+ [short_account_url(object), account_url(object)]
+ end
+
+ def links
+ [
+ { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) },
+ { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(object, format: 'atom') },
+ { rel: 'self', type: 'application/activity+json', href: account_url(object) },
+ { rel: 'salmon', href: api_salmon_url(object.id) },
+ { rel: 'magic-public-key', href: "data:application/magic-public-key,#{object.magic_key}" },
+ { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_follow_url}?acct={uri}" },
+ ]
+ end
+end
diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb
index b0c663d02..3be110665 100644
--- a/app/services/account_search_service.rb
+++ b/app/services/account_search_service.rb
@@ -1,12 +1,12 @@
# frozen_string_literal: true
class AccountSearchService < BaseService
- attr_reader :query, :limit, :resolve, :account
+ attr_reader :query, :limit, :options, :account
- def call(query, limit, resolve = false, account = nil)
- @query = query
- @limit = limit
- @resolve = resolve
+ def call(query, limit, account = nil, options = {})
+ @query = query.strip
+ @limit = limit
+ @options = options
@account = account
search_service_results
@@ -25,7 +25,7 @@ class AccountSearchService < BaseService
end
def resolving_non_matching_remote_account?
- resolve && !exact_match && !domain_is_local?
+ options[:resolve] && !exact_match && !domain_is_local?
end
def search_results_and_exact_match
@@ -58,12 +58,16 @@ class AccountSearchService < BaseService
@_domain_is_local ||= TagManager.instance.local_domain?(query_domain)
end
+ def search_from
+ options[:following] && account ? account.following : Account
+ end
+
def exact_match
@_exact_match ||= begin
if domain_is_local?
- Account.find_local(query_username)
+ search_from.find_local(query_username)
else
- Account.find_remote(query_username, query_domain)
+ search_from.find_remote(query_username, query_domain)
end
end
end
@@ -79,7 +83,7 @@ class AccountSearchService < BaseService
end
def advanced_search_results
- Account.advanced_search_for(terms_for_query, account, limit)
+ Account.advanced_search_for(terms_for_query, account, limit, options[:following])
end
def simple_search_results
diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb
index e2a89a87c..7649bceca 100644
--- a/app/services/activitypub/fetch_remote_status_service.rb
+++ b/app/services/activitypub/fetch_remote_status_service.rb
@@ -16,9 +16,9 @@ class ActivityPub::FetchRemoteStatusService < BaseService
return if actor_id.nil? || !trustworthy_attribution?(@json['id'], actor_id)
actor = ActivityPub::TagManager.instance.uri_to_resource(actor_id, Account)
- actor = ActivityPub::FetchRemoteAccountService.new.call(actor_id, id: true) if actor.nil?
+ actor = ActivityPub::FetchRemoteAccountService.new.call(actor_id, id: true) if actor.nil? || needs_update(actor)
- return if actor.suspended?
+ return if actor.nil? || actor.suspended?
ActivityPub::Activity.factory(activity_json, actor).perform
end
@@ -42,6 +42,10 @@ class ActivityPub::FetchRemoteStatusService < BaseService
end
def expected_type?
- %w(Note Article).include? @json['type']
+ (ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES).include? @json['type']
+ end
+
+ def needs_update(actor)
+ actor.possibly_stale?
end
end
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index f93baf4b5..06ca75563 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -74,6 +74,7 @@ class ActivityPub::ProcessAccountService < BaseService
@account.statuses_count = outbox_total_items if outbox_total_items.present?
@account.following_count = following_total_items if following_total_items.present?
@account.followers_count = followers_total_items if followers_total_items.present?
+ @account.moved_to_account = moved_account if @json['movedTo'].present?
end
def after_protocol_change!
@@ -106,12 +107,7 @@ class ActivityPub::ProcessAccountService < BaseService
def url
return if @json['url'].blank?
-
- value = first_of_value(@json['url'])
-
- return value if value.is_a?(String)
-
- value['href']
+ url_to_href(@json['url'], 'text/html')
end
def outbox_total_items
@@ -137,6 +133,12 @@ class ActivityPub::ProcessAccountService < BaseService
@collections[type] = nil
end
+ def moved_account
+ account = ActivityPub::TagManager.instance.uri_to_resource(@json['movedTo'], Account)
+ account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], id: true)
+ account
+ end
+
def skip_download?
@account.suspended? || domain_block&.reject_media?
end
diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb
index db4d1b4bc..eb93329e9 100644
--- a/app/services/activitypub/process_collection_service.rb
+++ b/app/services/activitypub/process_collection_service.rb
@@ -3,7 +3,7 @@
class ActivityPub::ProcessCollectionService < BaseService
include JsonLdHelper
- def call(body, account, options = {})
+ def call(body, account, **options)
@account = account
@json = Oj.load(body, mode: :strict)
@options = options
diff --git a/app/services/authorize_follow_service.rb b/app/services/authorize_follow_service.rb
index b1bff8962..f47d488f1 100644
--- a/app/services/authorize_follow_service.rb
+++ b/app/services/authorize_follow_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class AuthorizeFollowService < BaseService
- def call(source_account, target_account, options = {})
+ def call(source_account, target_account, **options)
if options[:skip_follow_request]
follow_request = FollowRequest.new(account: source_account, target_account: target_account)
else
diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb
index 5d83771c9..6b6b0c418 100644
--- a/app/services/batched_remove_status_service.rb
+++ b/app/services/batched_remove_status_service.rb
@@ -26,10 +26,11 @@ class BatchedRemoveStatusService < BaseService
statuses.each(&:destroy)
# Batch by source account
- statuses.group_by(&:account_id).each do |_, account_statuses|
+ statuses.group_by(&:account_id).each_value do |account_statuses|
account = account_statuses.first.account
unpush_from_home_timelines(account, account_statuses)
+ unpush_from_list_timelines(account, account_statuses)
if account.local?
batch_stream_entries(account, account_statuses)
@@ -79,7 +80,15 @@ class BatchedRemoveStatusService < BaseService
recipients.each do |follower|
statuses.each do |status|
- FeedManager.instance.unpush(:home, follower, status)
+ FeedManager.instance.unpush_from_home(follower, status)
+ end
+ end
+ end
+
+ def unpush_from_list_timelines(account, statuses)
+ account.lists.select(:id, :account_id).each do |list|
+ statuses.each do |status|
+ FeedManager.instance.unpush_from_list(list, status)
end
end
end
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index 47a47a735..bbaf3094b 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -14,6 +14,7 @@ class FanOutOnWriteService < BaseService
deliver_to_mentioned_followers(status)
else
deliver_to_followers(status)
+ deliver_to_lists(status)
end
return if status.account.silenced? || !status.public_visibility? || status.reblog?
@@ -30,7 +31,7 @@ class FanOutOnWriteService < BaseService
def deliver_to_self(status)
Rails.logger.debug "Delivering status #{status.id} to author"
- FeedManager.instance.push(:home, status.account, status)
+ FeedManager.instance.push_to_home(status.account, status)
end
def deliver_to_followers(status)
@@ -38,7 +39,17 @@ class FanOutOnWriteService < BaseService
status.account.followers.where(domain: nil).joins(:user).where('users.current_sign_in_at > ?', 14.days.ago).select(:id).reorder(nil).find_in_batches do |followers|
FeedInsertWorker.push_bulk(followers) do |follower|
- [status.id, follower.id]
+ [status.id, follower.id, :home]
+ end
+ end
+ end
+
+ def deliver_to_lists(status)
+ Rails.logger.debug "Delivering status #{status.id} to lists"
+
+ status.account.lists.joins(account: :user).where('users.current_sign_in_at > ?', 14.days.ago).select(:id).reorder(nil).find_in_batches do |lists|
+ FeedInsertWorker.push_bulk(lists) do |list|
+ [status.id, list.id, :list]
end
end
end
@@ -49,7 +60,7 @@ class FanOutOnWriteService < BaseService
status.mentions.includes(:account).each do |mention|
mentioned_account = mention.account
next if !mentioned_account.local? || !mentioned_account.following?(status.account) || FeedManager.instance.filter?(:home, status, mention.account_id)
- FeedManager.instance.push(:home, mentioned_account, status)
+ FeedManager.instance.push_to_home(mentioned_account, status)
end
end
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index 14c21b6cc..d0472a1d7 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -38,7 +38,13 @@ class FetchLinkCardService < BaseService
@card ||= PreviewCard.new(url: @url)
res = Request.new(:head, @url).perform
- return if res.code != 200 || res.mime_type != 'text/html'
+ return if res.code != 405 && (res.code != 200 || res.mime_type != 'text/html')
+
+ @response = Request.new(:get, @url).perform
+
+ return if @response.code != 200 || @response.mime_type != 'text/html'
+
+ @html = @response.to_s
attempt_oembed || attempt_opengraph
end
@@ -70,30 +76,32 @@ class FetchLinkCardService < BaseService
end
def attempt_oembed
- response = OEmbed::Providers.get(@url)
+ embed = OEmbed::Providers.get(@url, html: @html)
- return false unless response.respond_to?(:type)
+ return false unless embed.respond_to?(:type)
- @card.type = response.type
- @card.title = response.respond_to?(:title) ? response.title : ''
- @card.author_name = response.respond_to?(:author_name) ? response.author_name : ''
- @card.author_url = response.respond_to?(:author_url) ? response.author_url : ''
- @card.provider_name = response.respond_to?(:provider_name) ? response.provider_name : ''
- @card.provider_url = response.respond_to?(:provider_url) ? response.provider_url : ''
+ @card.type = embed.type
+ @card.title = embed.respond_to?(:title) ? embed.title : ''
+ @card.author_name = embed.respond_to?(:author_name) ? embed.author_name : ''
+ @card.author_url = embed.respond_to?(:author_url) ? embed.author_url : ''
+ @card.provider_name = embed.respond_to?(:provider_name) ? embed.provider_name : ''
+ @card.provider_url = embed.respond_to?(:provider_url) ? embed.provider_url : ''
@card.width = 0
@card.height = 0
case @card.type
when 'link'
- @card.image = URI.parse(response.thumbnail_url) if response.respond_to?(:thumbnail_url)
+ @card.image = URI.parse(embed.thumbnail_url) if embed.respond_to?(:thumbnail_url)
when 'photo'
- @card.url = response.url
- @card.width = response.width.presence || 0
- @card.height = response.height.presence || 0
+ return false unless embed.respond_to?(:url)
+ @card.embed_url = embed.url
+ @card.image = URI.parse(embed.url)
+ @card.width = embed.width.presence || 0
+ @card.height = embed.height.presence || 0
when 'video'
- @card.width = response.width.presence || 0
- @card.height = response.height.presence || 0
- @card.html = Formatter.instance.sanitize(response.html, Sanitize::Config::MASTODON_OEMBED)
+ @card.width = embed.width.presence || 0
+ @card.height = embed.height.presence || 0
+ @card.html = Formatter.instance.sanitize(embed.html, Sanitize::Config::MASTODON_OEMBED)
when 'rich'
# Most providers rely on
-
-
+
+
中国域名网站
-
+
@@ -472,7 +472,7 @@ Accept-Ranges: bytes