merge with gh master

This commit is contained in:
Baptiste Lemoine 2019-12-02 16:35:37 +01:00
commit 11418ca8a9
1166 changed files with 13287 additions and 6038 deletions

0
.cache/.gitkeep Normal file
View File

View File

@ -183,6 +183,11 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# LDAP_BIND_DN= # LDAP_BIND_DN=
# LDAP_PASSWORD= # LDAP_PASSWORD=
# LDAP_UID=cn # LDAP_UID=cn
# LDAP_MAIL=mail
# LDAP_SEARCH_FILTER=(|(%{uid}=%{email})(%{mail}=%{email}))
# LDAP_UID_CONVERSION_ENABLED=true
# LDAP_UID_CONVERSION_SEARCH=., -
# LDAP_UID_CONVERSION_REPLACE=_
# PAM authentication (optional) # PAM authentication (optional)
# PAM authentication uses for the email generation the "email" pam variable # PAM authentication uses for the email generation the "email" pam variable

View File

@ -178,7 +178,11 @@ STREAMING_CLUSTER_NUM=1
# LDAP_BIND_DN= # LDAP_BIND_DN=
# LDAP_PASSWORD= # LDAP_PASSWORD=
# LDAP_UID=cn # LDAP_UID=cn
# LDAP_SEARCH_FILTER=%{uid}=%{email} # LDAP_MAIL=mail
# LDAP_SEARCH_FILTER=(|(%{uid}=%{email})(%{mail}=%{email}))
# LDAP_UID_CONVERSION_ENABLED=true
# LDAP_UID_CONVERSION_SEARCH=., -
# LDAP_UID_CONVERSION_REPLACE=_
# PAM authentication (optional) # PAM authentication (optional)
# PAM authentication uses for the email generation the "email" pam variable # PAM authentication uses for the email generation the "email" pam variable

View File

@ -1,5 +1,5 @@
# Node.js # Node.js
NODE_ENV=test NODE_ENV=tests
# Federation # Federation
LOCAL_DOMAIN=cb6e6126.ngrok.io LOCAL_DOMAIN=cb6e6126.ngrok.io
LOCAL_HTTPS=true LOCAL_HTTPS=true

View File

@ -1,2 +1,3 @@
VAGRANT=true VAGRANT=true
LOCAL_DOMAIN=mastodon.local LOCAL_DOMAIN=mastodon.local
BIND=0.0.0.0

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Mastodon Meta Discussion Board
url: https://discourse.joinmastodon.org/
about: Please ask and answer questions here.

10
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,10 @@
daysUntilStale: 120
daysUntilClose: 7
exemptLabels:
- security
staleLabel: wontfix
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
only: pulls

3
.gitignore vendored
View File

@ -55,6 +55,9 @@ npm-debug.log
yarn-error.log yarn-error.log
yarn-debug.log yarn-debug.log
# Ignore vagrant log files
ubuntu-xenial-16.04-cloudimg-console.log
# Ignore Docker option files # Ignore Docker option files
docker-compose.override.yml docker-compose.override.yml

View File

@ -3,6 +3,39 @@ Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [3.0.1] - 2019-10-10
### Added
- Add `tootctl media usage` command ([Gargron](https://github.com/tootsuite/mastodon/pull/12115))
- Add admin setting to auto-approve trending hashtags ([Gargron](https://github.com/tootsuite/mastodon/pull/12122), [Gargron](https://github.com/tootsuite/mastodon/pull/12130))
### Changed
- Change `tootctl media refresh` to skip already downloaded attachments ([Gargron](https://github.com/tootsuite/mastodon/pull/12118))
### Removed
- Remove auto-silence behaviour from spam check ([Gargron](https://github.com/tootsuite/mastodon/pull/12117))
- Remove HTML `lang` attribute from individual statuses in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/12124))
- Remove fallback to long description on sidebar and meta description ([Gargron](https://github.com/tootsuite/mastodon/pull/12119))
### Fixed
- Fix preloaded JSON-LD context for identity not being used ([Gargron](https://github.com/tootsuite/mastodon/pull/12138))
- Fix media editing modal changing dimensions once the image loads ([Gargron](https://github.com/tootsuite/mastodon/pull/12131))
- Fix not showing whether a custom emoji has a local counterpart in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/12135))
- Fix attachment not being re-downloaded even if file is not stored ([Gargron](https://github.com/tootsuite/mastodon/pull/12125))
- Fix old migration trying to use new column due to default status scope ([Gargron](https://github.com/tootsuite/mastodon/pull/12095))
- Fix column back button missing for not found accounts ([trwnh](https://github.com/tootsuite/mastodon/pull/12094))
- Fix issues with tootctl's parallelization and progress reporting ([Gargron](https://github.com/tootsuite/mastodon/pull/12093), [Gargron](https://github.com/tootsuite/mastodon/pull/12097))
- Fix existing user records with now-renamed `pt` locale ([Gargron](https://github.com/tootsuite/mastodon/pull/12092))
- Fix hashtag timeline REST API accepting too many hashtags ([Gargron](https://github.com/tootsuite/mastodon/pull/12091))
- Fix `GET /api/v1/instance` REST APIs being unavailable in secure mode ([Gargron](https://github.com/tootsuite/mastodon/pull/12089))
- Fix performance of home feed regeneration and merging ([Gargron](https://github.com/tootsuite/mastodon/pull/12084))
- Fix ffmpeg performance issues due to stdout buffer overflow ([hugogameiro](https://github.com/tootsuite/mastodon/pull/12088))
- Fix S3 adapter retrying failing uploads with exponential backoff ([Gargron](https://github.com/tootsuite/mastodon/pull/12085))
- Fix `tootctl accounts cull` advertising unused option flag ([Kjwon15](https://github.com/tootsuite/mastodon/pull/12074))
## [3.0.0] - 2019-10-03 ## [3.0.0] - 2019-10-03
### Added ### Added

View File

@ -14,13 +14,13 @@ If your contributions are accepted into Mastodon, you can request to be paid thr
## Bug reports ## Bug reports
Bug reports and feature suggestions can be submitted to [GitHub Issues](https://github.com/tootsuite/mastodon/issues). Please make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected in the past using the search function. Please also use descriptive, concise titles. Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/tootsuite/mastodon/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected.
## Translations ## Translations
You can submit translations via [Crowdin](https://crowdin.com/project/mastodon). They are periodically merged into the codebase. You can submit translations via [Crowdin](https://crowdin.com/project/mastodon). They are periodically merged into the codebase.
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin] [![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)](https://crowdin.com/project/mastodon)
## Pull requests ## Pull requests

View File

@ -3,8 +3,8 @@ FROM ubuntu:18.04 as build-dep
# Use bash for the shell # Use bash for the shell
SHELL ["bash", "-c"] SHELL ["bash", "-c"]
# Install Node # Install Node v12 (LTS)
ENV NODE_VER="12.11.1" ENV NODE_VER="12.13.1"
RUN echo "Etc/UTC" > /etc/localtime && \ RUN echo "Etc/UTC" > /etc/localtime && \
apt update && \ apt update && \
apt -y install wget python && \ apt -y install wget python && \
@ -123,3 +123,4 @@ RUN cd ~ && \
# Set the work dir and the container entry point # Set the work dir and the container entry point
WORKDIR /opt/mastodon WORKDIR /opt/mastodon
ENTRYPOINT ["/tini", "--"] ENTRYPOINT ["/tini", "--"]
EXPOSE 3000 4000

42
Gemfile
View File

@ -3,7 +3,7 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '>= 2.4.0', '< 2.7.0' ruby '>= 2.4.0', '< 2.7.0'
gem 'pkg-config', '~> 1.3' gem 'pkg-config', '~> 1.4'
gem 'puma', '~> 4.2' gem 'puma', '~> 4.2'
gem 'rails', '~> 5.2.3' gem 'rails', '~> 5.2.3'
@ -12,10 +12,10 @@ gem 'thor', '~> 0.20'
gem 'hamlit-rails', '~> 0.2' gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.1' gem 'pg', '~> 1.1'
gem 'makara', '~> 0.4' gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.3' gem 'pghero', '~> 2.4'
gem 'dotenv-rails', '~> 2.7' gem 'dotenv-rails', '~> 2.7'
gem 'aws-sdk-s3', '~> 1.48', require: false gem 'aws-sdk-s3', '~> 1.57', require: false
gem 'fog-core', '<= 2.1.0' gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0' gem 'paperclip', '~> 6.0'
@ -27,7 +27,7 @@ gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.7' gem 'addressable', '~> 2.7'
gem 'bootsnap', '~> 1.4', require: false gem 'bootsnap', '~> 1.4', require: false
gem 'browser' gem 'browser'
gem 'charlock_holmes', '~> 0.7.6' gem 'charlock_holmes', '~> 0.7.7'
gem 'iso-639' gem 'iso-639'
gem 'chewy', '~> 5.1' gem 'chewy', '~> 5.1'
gem 'cld3', '~> 3.2.4' gem 'cld3', '~> 3.2.4'
@ -38,7 +38,7 @@ group :pam_authentication, optional: true do
gem 'devise_pam_authenticatable2', '~> 9.2' gem 'devise_pam_authenticatable2', '~> 9.2'
end end
gem 'net-ldap', '~> 0.10' gem 'net-ldap', '~> 0.16'
gem 'omniauth-cas', '~> 1.1' gem 'omniauth-cas', '~> 1.1'
gem 'omniauth-saml', '~> 1.10' gem 'omniauth-saml', '~> 1.10'
gem 'omniauth', '~> 1.9' gem 'omniauth', '~> 1.9'
@ -67,12 +67,12 @@ gem 'oj', '~> 3.9'
gem 'ostatus2', '~> 2.0' gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.11' gem 'ox', '~> 2.11'
gem 'parslet' gem 'parslet'
gem 'parallel', '~> 1.17' gem 'parallel', '~> 1.19'
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c' gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
gem 'pundit', '~> 2.1' gem 'pundit', '~> 2.1'
gem 'premailer-rails' gem 'premailer-rails'
gem 'rack-attack', '~> 6.1' gem 'rack-attack', '~> 6.2'
gem 'rack-cors', '~> 1.0', require: 'rack/cors' gem 'rack-cors', '~> 1.1', require: 'rack/cors'
gem 'rails-i18n', '~> 5.1' gem 'rails-i18n', '~> 5.1'
gem 'rails-settings-cached', '~> 0.6' gem 'rails-settings-cached', '~> 0.6'
gem 'redis', '~> 4.1', require: ['redis', 'redis/connection/hiredis'] gem 'redis', '~> 4.1', require: ['redis', 'redis/connection/hiredis']
@ -85,15 +85,15 @@ gem 'sidekiq-scheduler', '~> 3.0'
gem 'sidekiq-unique-jobs', '~> 6.0' gem 'sidekiq-unique-jobs', '~> 6.0'
gem 'sidekiq-bulk', '~>0.2.0' gem 'sidekiq-bulk', '~>0.2.0'
gem 'simple-navigation', '~> 4.1' gem 'simple-navigation', '~> 4.1'
gem 'simple_form', '~> 4.1' gem 'simple_form', '~> 5.0'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.1.3' gem 'stoplight', '~> 2.2.0'
gem 'strong_migrations', '~> 0.4' gem 'strong_migrations', '~> 0.4'
gem 'tty-command', '~> 0.9', require: false gem 'tty-command', '~> 0.9', require: false
gem 'tty-prompt', '~> 0.19', require: false gem 'tty-prompt', '~> 0.20', require: false
gem 'twitter-text', '~> 1.14' gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2019' gem 'tzinfo-data', '~> 1.2019'
gem 'webpacker', '~> 4.0' gem 'webpacker', '~> 4.2'
gem 'webpush' gem 'webpush'
gem 'json-ld', git: 'https://github.com/ruby-rdf/json-ld.git', ref: 'e742697a0906e74e8bb777ef98137bc3955d981d' gem 'json-ld', git: 'https://github.com/ruby-rdf/json-ld.git', ref: 'e742697a0906e74e8bb777ef98137bc3955d981d'
@ -101,12 +101,12 @@ gem 'json-ld-preloaded', '~> 3.0'
gem 'rdf-normalize', '~> 0.3' gem 'rdf-normalize', '~> 0.3'
group :development, :test do group :development, :test do
gem 'fabrication', '~> 2.20' gem 'fabrication', '~> 2.21'
gem 'fuubar', '~> 2.4' gem 'fuubar', '~> 2.5'
gem 'i18n-tasks', '~> 0.9', require: false gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-byebug', '~> 3.7' gem 'pry-byebug', '~> 3.7'
gem 'pry-rails', '~> 0.3' gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 3.8' gem 'rspec-rails', '~> 3.9'
end end
group :production, :test do group :production, :test do
@ -116,7 +116,7 @@ end
group :test do group :test do
gem 'capybara', '~> 3.29' gem 'capybara', '~> 3.29'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.5' gem 'faker', '~> 2.7'
gem 'microformats', '~> 4.1' gem 'microformats', '~> 4.1'
gem 'rails-controller-testing', '~> 1.0' gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0' gem 'rspec-sidekiq', '~> 3.0'
@ -126,17 +126,17 @@ group :test do
end end
group :development do group :development do
gem 'active_record_query_trace', '~> 1.6' gem 'active_record_query_trace', '~> 1.7'
gem 'annotate', '~> 2.7' gem 'annotate', '~> 3.0'
gem 'better_errors', '~> 2.5' gem 'better_errors', '~> 2.5'
gem 'binding_of_caller', '~> 0.7' gem 'binding_of_caller', '~> 0.7'
gem 'bullet', '~> 6.0' gem 'bullet', '~> 6.0'
gem 'letter_opener', '~> 1.7' gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.3' gem 'letter_opener_web', '~> 1.3'
gem 'memory_profiler' gem 'memory_profiler'
gem 'rubocop', '~> 0.74', require: false gem 'rubocop', '~> 0.76', require: false
gem 'rubocop-rails', '~> 2.3', require: false gem 'rubocop-rails', '~> 2.4', require: false
gem 'brakeman', '~> 4.6', require: false gem 'brakeman', '~> 4.7', require: false
gem 'bundler-audit', '~> 0.6', require: false gem 'bundler-audit', '~> 0.6', require: false
gem 'capistrano', '~> 3.11' gem 'capistrano', '~> 3.11'

View File

@ -72,7 +72,7 @@ GEM
activemodel (>= 4.1, < 6.1) activemodel (>= 4.1, < 6.1)
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.6.2) active_record_query_trace (1.7)
activejob (5.2.3) activejob (5.2.3)
activesupport (= 5.2.3) activesupport (= 5.2.3)
globalid (>= 0.3.6) globalid (>= 0.3.6)
@ -95,9 +95,9 @@ GEM
public_suffix (>= 2.0.2, < 5.0) public_suffix (>= 2.0.2, < 5.0)
airbrussh (1.3.4) airbrussh (1.3.4)
sshkit (>= 1.6.1, != 1.7.0) sshkit (>= 1.6.1, != 1.7.0)
annotate (2.7.5) annotate (3.0.3)
activerecord (>= 3.2, < 7.0) activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 13.0) rake (>= 10.4, < 14.0)
arel (9.0.0) arel (9.0.0)
ast (2.4.0) ast (2.4.0)
attr_encrypted (3.1.0) attr_encrypted (3.1.0)
@ -105,17 +105,17 @@ GEM
av (0.9.0) av (0.9.0)
cocaine (~> 0.5.3) cocaine (~> 0.5.3)
aws-eventstream (1.0.3) aws-eventstream (1.0.3)
aws-partitions (1.207.0) aws-partitions (1.246.0)
aws-sdk-core (3.65.1) aws-sdk-core (3.82.0)
aws-eventstream (~> 1.0, >= 1.0.2) aws-eventstream (~> 1.0, >= 1.0.2)
aws-partitions (~> 1.0) aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-kms (1.24.0) aws-sdk-kms (1.26.0)
aws-sdk-core (~> 3, >= 3.61.1) aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.48.0) aws-sdk-s3 (1.57.0)
aws-sdk-core (~> 3, >= 3.61.1) aws-sdk-core (~> 3, >= 3.77.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.0) aws-sigv4 (1.1.0)
@ -132,8 +132,8 @@ GEM
ffi (~> 1.10.0) ffi (~> 1.10.0)
bootsnap (1.4.5) bootsnap (1.4.5)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (4.6.1) brakeman (4.7.1)
browser (2.6.1) browser (2.7.1)
builder (3.2.3) builder (3.2.3)
bullet (6.0.2) bullet (6.0.2)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -168,7 +168,7 @@ GEM
xpath (~> 3.2) xpath (~> 3.2)
case_transform (0.2) case_transform (0.2)
activesupport activesupport
charlock_holmes (0.7.6) charlock_holmes (0.7.7)
chewy (5.1.0) chewy (5.1.0)
activesupport (>= 4.0) activesupport (>= 4.0)
elasticsearch (>= 2.0.0) elasticsearch (>= 2.0.0)
@ -184,17 +184,17 @@ GEM
connection_pool (2.2.2) connection_pool (2.2.2)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
crass (1.0.4) crass (1.0.5)
css_parser (1.7.0) css_parser (1.7.0)
addressable addressable
debug_inspector (0.0.3) debug_inspector (0.0.3)
derailed_benchmarks (1.4.0) derailed_benchmarks (1.4.2)
benchmark-ips (~> 2) benchmark-ips (~> 2)
get_process_mem (~> 0) get_process_mem (~> 0)
heapy (~> 0) heapy (~> 0)
memory_profiler (~> 0) memory_profiler (~> 0)
rack (>= 1) rack (>= 1)
rake (> 10, < 13) rake (> 10, < 14)
ruby-statistics (>= 2.1) ruby-statistics (>= 2.1)
thor (~> 0.19) thor (~> 0.19)
devise (4.7.1) devise (4.7.1)
@ -218,7 +218,7 @@ GEM
docile (1.3.2) docile (1.3.2)
domain_name (0.5.20180417) domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.2.1) doorkeeper (5.2.2)
railties (>= 5) railties (>= 5)
dotenv (2.7.5) dotenv (2.7.5)
dotenv-rails (2.7.5) dotenv-rails (2.7.5)
@ -235,13 +235,13 @@ GEM
multi_json multi_json
encryptor (3.0.0) encryptor (3.0.0)
equatable (0.6.1) equatable (0.6.1)
erubi (1.8.0) erubi (1.9.0)
et-orbi (1.1.6) et-orbi (1.1.6)
tzinfo tzinfo
excon (0.62.0) excon (0.62.0)
fabrication (2.20.2) fabrication (2.21.0)
faker (2.5.0) faker (2.7.0)
i18n (~> 1.6.0) i18n (>= 1.6, < 1.8)
faraday (0.15.4) faraday (0.15.4)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
fast_blank (1.0.0) fast_blank (1.0.0)
@ -263,10 +263,10 @@ GEM
fugit (1.1.6) fugit (1.1.6)
et-orbi (~> 1.1, >= 1.1.6) et-orbi (~> 1.1, >= 1.1.6)
raabro (~> 1.1) raabro (~> 1.1)
fuubar (2.4.1) fuubar (2.5.0)
rspec-core (~> 3.0) rspec-core (~> 3.0)
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
get_process_mem (0.2.4) get_process_mem (0.2.5)
ffi (~> 1.0) ffi (~> 1.0)
globalid (0.4.2) globalid (0.4.2)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
@ -302,10 +302,10 @@ GEM
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (2.1.1) http-form_data (2.1.1)
http_accept_language (2.1.1) http_accept_language (2.1.1)
httplog (1.3.2) httplog (1.3.3)
rack (>= 1.0) rack (>= 1.0)
rainbow (>= 2.0.0) rainbow (>= 2.0.0)
i18n (1.6.0) i18n (1.7.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
i18n-tasks (0.9.29) i18n-tasks (0.9.29)
activesupport (>= 4.0.2) activesupport (>= 4.0.2)
@ -320,7 +320,7 @@ GEM
idn-ruby (0.1.0) idn-ruby (0.1.0)
ipaddress (0.8.3) ipaddress (0.8.3)
iso-639 (0.2.8) iso-639 (0.2.8)
jaro_winkler (1.5.3) jaro_winkler (1.5.4)
jmespath (1.4.0) jmespath (1.4.0)
json (2.2.0) json (2.2.0)
json-canonicalization (0.1.0) json-canonicalization (0.1.0)
@ -356,7 +356,7 @@ GEM
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
request_store (~> 1.0) request_store (~> 1.0)
loofah (2.2.3) loofah (2.3.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.7.1) mail (2.7.1)
@ -378,17 +378,17 @@ GEM
mimemagic (0.3.3) mimemagic (0.3.3)
mini_mime (1.0.2) mini_mime (1.0.2)
mini_portile2 (2.4.0) mini_portile2 (2.4.0)
minitest (5.12.0) minitest (5.13.0)
msgpack (1.3.1) msgpack (1.3.1)
multi_json (1.13.1) multi_json (1.13.1)
multipart-post (2.1.1) multipart-post (2.1.1)
necromancer (0.5.0) necromancer (0.5.1)
net-ldap (0.16.1) net-ldap (0.16.2)
net-scp (2.0.0) net-scp (2.0.0)
net-ssh (>= 2.6.5, < 6.0.0) net-ssh (>= 2.6.5, < 6.0.0)
net-ssh (5.2.0) net-ssh (5.2.0)
nio4r (2.5.1) nio4r (2.5.1)
nokogiri (1.10.4) nokogiri (1.10.5)
mini_portile2 (~> 2.4.0) mini_portile2 (~> 2.4.0)
nokogumbo (2.0.1) nokogumbo (2.0.1)
nokogiri (~> 1.8, >= 1.8.4) nokogiri (~> 1.8, >= 1.8.4)
@ -397,7 +397,7 @@ GEM
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5) sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0) statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.9.1) oj (3.9.2)
omniauth (1.9.0) omniauth (1.9.0)
hashie (>= 3.4.6, < 3.7.0) hashie (>= 3.4.6, < 3.7.0)
rack (>= 1.6.2, < 3) rack (>= 1.6.2, < 3)
@ -423,19 +423,19 @@ GEM
paperclip-av-transcoder (0.6.4) paperclip-av-transcoder (0.6.4)
av (~> 0.9.0) av (~> 0.9.0)
paperclip (>= 2.5.2) paperclip (>= 2.5.2)
parallel (1.17.0) parallel (1.19.1)
parallel_tests (2.29.2) parallel_tests (2.29.2)
parallel parallel
parser (2.6.4.0) parser (2.6.5.0)
ast (~> 2.4.0) ast (~> 2.4.0)
parslet (1.8.2) parslet (1.8.2)
pastel (0.7.3) pastel (0.7.3)
equatable (~> 0.6) equatable (~> 0.6)
tty-color (~> 0.5) tty-color (~> 0.5)
pg (1.1.4) pg (1.1.4)
pghero (2.3.0) pghero (2.4.1)
activerecord (>= 5) activerecord (>= 5)
pkg-config (1.3.9) pkg-config (1.4.0)
premailer (1.11.1) premailer (1.11.1)
addressable addressable
css_parser (>= 1.6.0) css_parser (>= 1.6.0)
@ -459,10 +459,11 @@ GEM
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.1.6) raabro (1.1.6)
rack (2.0.7) rack (2.0.7)
rack-attack (6.1.0) rack-attack (6.2.1)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-cors (1.0.3) rack-cors (1.1.0)
rack-protection (2.0.5) rack (>= 2.0.0)
rack-protection (2.0.7)
rack rack
rack-proxy (0.6.5) rack-proxy (0.6.5)
rack rack
@ -488,8 +489,8 @@ GEM
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.2.0) rails-html-sanitizer (1.3.0)
loofah (~> 2.2, >= 2.2.2) loofah (~> 2.3)
rails-i18n (5.1.3) rails-i18n (5.1.3)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 5.0, < 6) railties (>= 5.0, < 6)
@ -502,7 +503,7 @@ GEM
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0) thor (>= 0.19.0, < 2.0)
rainbow (3.0.0) rainbow (3.0.0)
rake (12.3.3) rake (13.0.1)
rdf (3.0.12) rdf (3.0.12)
hamster (~> 3.0) hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
@ -537,34 +538,34 @@ GEM
rpam2 (4.0.2) rpam2 (4.0.2)
rqrcode (0.10.1) rqrcode (0.10.1)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rspec-core (3.8.0) rspec-core (3.9.0)
rspec-support (~> 3.8.0) rspec-support (~> 3.9.0)
rspec-expectations (3.8.2) rspec-expectations (3.9.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.8.0) rspec-support (~> 3.9.0)
rspec-mocks (3.8.0) rspec-mocks (3.9.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.8.0) rspec-support (~> 3.9.0)
rspec-rails (3.8.2) rspec-rails (3.9.0)
actionpack (>= 3.0) actionpack (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
railties (>= 3.0) railties (>= 3.0)
rspec-core (~> 3.8.0) rspec-core (~> 3.9.0)
rspec-expectations (~> 3.8.0) rspec-expectations (~> 3.9.0)
rspec-mocks (~> 3.8.0) rspec-mocks (~> 3.9.0)
rspec-support (~> 3.8.0) rspec-support (~> 3.9.0)
rspec-sidekiq (3.0.3) rspec-sidekiq (3.0.3)
rspec-core (~> 3.0, >= 3.0.0) rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0) sidekiq (>= 2.4.0)
rspec-support (3.8.0) rspec-support (3.9.0)
rubocop (0.74.0) rubocop (0.76.0)
jaro_winkler (~> 1.5.1) jaro_winkler (~> 1.5.1)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.6) parser (>= 2.6)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7) unicode-display_width (>= 1.4.0, < 1.7)
rubocop-rails (2.3.2) rubocop-rails (2.4.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 0.72.0) rubocop (>= 0.72.0)
ruby-progressbar (1.10.1) ruby-progressbar (1.10.1)
@ -590,13 +591,13 @@ GEM
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 3) sidekiq (>= 3)
tilt (>= 1.4.0) tilt (>= 1.4.0)
sidekiq-unique-jobs (6.0.13) sidekiq-unique-jobs (6.0.15)
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 4.0, < 7.0) sidekiq (>= 4.0, < 7.0)
thor (~> 0) thor (~> 0)
simple-navigation (4.1.0) simple-navigation (4.1.0)
activesupport (>= 2.3.2) activesupport (>= 2.3.2)
simple_form (4.1.0) simple_form (5.0.1)
actionpack (>= 5.0) actionpack (>= 5.0)
activemodel (>= 5.0) activemodel (>= 5.0)
simplecov (0.17.1) simplecov (0.17.1)
@ -614,12 +615,12 @@ GEM
sshkit (1.20.0) sshkit (1.20.0)
net-scp (>= 1.1.2) net-scp (>= 1.1.2)
net-ssh (>= 2.8.0) net-ssh (>= 2.8.0)
stackprof (0.2.12) stackprof (0.2.13)
statsd-ruby (1.4.0) statsd-ruby (1.4.0)
stoplight (2.1.3) stoplight (2.2.0)
streamio-ffmpeg (3.0.2) streamio-ffmpeg (3.0.2)
multi_json (~> 1.8) multi_json (~> 1.8)
strong_migrations (0.4.1) strong_migrations (0.4.2)
activerecord (>= 5) activerecord (>= 5)
temple (0.8.1) temple (0.8.1)
terminal-table (1.8.0) terminal-table (1.8.0)
@ -633,11 +634,11 @@ GEM
tty-command (0.9.0) tty-command (0.9.0)
pastel (~> 0.7.0) pastel (~> 0.7.0)
tty-cursor (0.7.0) tty-cursor (0.7.0)
tty-prompt (0.19.0) tty-prompt (0.20.0)
necromancer (~> 0.5.0) necromancer (~> 0.5.0)
pastel (~> 0.7.0) pastel (~> 0.7.0)
tty-reader (~> 0.6.0) tty-reader (~> 0.7.0)
tty-reader (0.6.0) tty-reader (0.7.0)
tty-cursor (~> 0.7) tty-cursor (~> 0.7)
tty-screen (~> 0.7) tty-screen (~> 0.7)
wisper (~> 2.0.0) wisper (~> 2.0.0)
@ -659,7 +660,7 @@ GEM
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webpacker (4.0.7) webpacker (4.2.0)
activesupport (>= 4.2) activesupport (>= 4.2)
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 4.2) railties (>= 4.2)
@ -669,7 +670,7 @@ GEM
websocket-driver (0.7.0) websocket-driver (0.7.0)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3) websocket-extensions (0.1.3)
wisper (2.0.0) wisper (2.0.1)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
@ -678,15 +679,15 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
active_model_serializers (~> 0.10) active_model_serializers (~> 0.10)
active_record_query_trace (~> 1.6) active_record_query_trace (~> 1.7)
addressable (~> 2.7) addressable (~> 2.7)
annotate (~> 2.7) annotate (~> 3.0)
aws-sdk-s3 (~> 1.48) aws-sdk-s3 (~> 1.57)
better_errors (~> 2.5) better_errors (~> 2.5)
binding_of_caller (~> 0.7) binding_of_caller (~> 0.7)
blurhash (~> 0.1) blurhash (~> 0.1)
bootsnap (~> 1.4) bootsnap (~> 1.4)
brakeman (~> 4.6) brakeman (~> 4.7)
browser browser
bullet (~> 6.0) bullet (~> 6.0)
bundler-audit (~> 0.6) bundler-audit (~> 0.6)
@ -695,7 +696,7 @@ DEPENDENCIES
capistrano-rbenv (~> 2.1) capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0) capistrano-yarn (~> 2.0)
capybara (~> 3.29) capybara (~> 3.29)
charlock_holmes (~> 0.7.6) charlock_holmes (~> 0.7.7)
chewy (~> 5.1) chewy (~> 5.1)
cld3 (~> 3.2.4) cld3 (~> 3.2.4)
climate_control (~> 0.2) climate_control (~> 0.2)
@ -708,13 +709,13 @@ DEPENDENCIES
discard (~> 1.1) discard (~> 1.1)
doorkeeper (~> 5.2) doorkeeper (~> 5.2)
dotenv-rails (~> 2.7) dotenv-rails (~> 2.7)
fabrication (~> 2.20) fabrication (~> 2.21)
faker (~> 2.5) faker (~> 2.7)
fast_blank (~> 1.0) fast_blank (~> 1.0)
fastimage fastimage
fog-core (<= 2.1.0) fog-core (<= 2.1.0)
fog-openstack (~> 0.3) fog-openstack (~> 0.3)
fuubar (~> 2.4) fuubar (~> 2.5)
goldfinger (~> 2.1) goldfinger (~> 2.1)
hamlit-rails (~> 0.2) hamlit-rails (~> 0.2)
health_check! health_check!
@ -739,7 +740,7 @@ DEPENDENCIES
memory_profiler memory_profiler
microformats (~> 4.1) microformats (~> 4.1)
mime-types (~> 3.3) mime-types (~> 3.3)
net-ldap (~> 0.10) net-ldap (~> 0.16)
nilsimsa! nilsimsa!
nokogiri (~> 1.10) nokogiri (~> 1.10)
nsa (~> 0.2) nsa (~> 0.2)
@ -751,12 +752,12 @@ DEPENDENCIES
ox (~> 2.11) ox (~> 2.11)
paperclip (~> 6.0) paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6) paperclip-av-transcoder (~> 0.6)
parallel (~> 1.17) parallel (~> 1.19)
parallel_tests (~> 2.29) parallel_tests (~> 2.29)
parslet parslet
pg (~> 1.1) pg (~> 1.1)
pghero (~> 2.3) pghero (~> 2.4)
pkg-config (~> 1.3) pkg-config (~> 1.4)
posix-spawn! posix-spawn!
premailer-rails premailer-rails
private_address_check (~> 0.5) private_address_check (~> 0.5)
@ -764,8 +765,8 @@ DEPENDENCIES
pry-rails (~> 0.3) pry-rails (~> 0.3)
puma (~> 4.2) puma (~> 4.2)
pundit (~> 2.1) pundit (~> 2.1)
rack-attack (~> 6.1) rack-attack (~> 6.2)
rack-cors (~> 1.0) rack-cors (~> 1.1)
rails (~> 5.2.3) rails (~> 5.2.3)
rails-controller-testing (~> 1.0) rails-controller-testing (~> 1.0)
rails-i18n (~> 5.1) rails-i18n (~> 5.1)
@ -775,10 +776,10 @@ DEPENDENCIES
redis-namespace (~> 1.5) redis-namespace (~> 1.5)
redis-rails (~> 5.0) redis-rails (~> 5.0)
rqrcode (~> 0.10) rqrcode (~> 0.10)
rspec-rails (~> 3.8) rspec-rails (~> 3.9)
rspec-sidekiq (~> 3.0) rspec-sidekiq (~> 3.0)
rubocop (~> 0.74) rubocop (~> 0.76)
rubocop-rails (~> 2.3) rubocop-rails (~> 2.4)
ruby-progressbar (~> 1.10) ruby-progressbar (~> 1.10)
sanitize (~> 5.1) sanitize (~> 5.1)
sidekiq (~> 5.2) sidekiq (~> 5.2)
@ -786,20 +787,20 @@ DEPENDENCIES
sidekiq-scheduler (~> 3.0) sidekiq-scheduler (~> 3.0)
sidekiq-unique-jobs (~> 6.0) sidekiq-unique-jobs (~> 6.0)
simple-navigation (~> 4.1) simple-navigation (~> 4.1)
simple_form (~> 4.1) simple_form (~> 5.0)
simplecov (~> 0.17) simplecov (~> 0.17)
sprockets-rails (~> 3.2) sprockets-rails (~> 3.2)
stackprof stackprof
stoplight (~> 2.1.3) stoplight (~> 2.2.0)
streamio-ffmpeg (~> 3.0) streamio-ffmpeg (~> 3.0)
strong_migrations (~> 0.4) strong_migrations (~> 0.4)
thor (~> 0.20) thor (~> 0.20)
tty-command (~> 0.9) tty-command (~> 0.9)
tty-prompt (~> 0.19) tty-prompt (~> 0.20)
twitter-text (~> 1.14) twitter-text (~> 1.14)
tzinfo-data (~> 1.2019) tzinfo-data (~> 1.2019)
webmock (~> 3.7) webmock (~> 3.7)
webpacker (~> 4.0) webpacker (~> 4.2)
webpush webpush
RUBY VERSION RUBY VERSION

View File

@ -13,7 +13,7 @@
[crowdin]: https://crowdin.com/project/mastodon [crowdin]: https://crowdin.com/project/mastodon
[docker]: https://hub.docker.com/r/tootsuite/mastodon/ [docker]: https://hub.docker.com/r/tootsuite/mastodon/
Mastodon is a **free, open-source social network server** based on ActivityPub. Follow friends and discover new ones. Publish anything you want: links, pictures, text, video. All servers of Mastodon are interoperable as a federated network, i.e. users on one server can seamlessly communicate with users from another one. This includes non-Mastodon software that also implements ActivityPub! Mastodon is a **free, open-source social network server** based on ActivityPub where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, video. All Mastodon servers are interoperable as a federated network (users on one server can seamlessly communicate with users from another one, including non-Mastodon software that implements ActivityPub)!
Click below to **learn more** in a video: Click below to **learn more** in a video:
@ -78,7 +78,7 @@ A **Vagrant** configuration is included for development purposes.
## Contributing ## Contributing
Mastodon is **free, open source software** licensed under **AGPLv3**. Mastodon is **free, open-source software** licensed under **AGPLv3**.
You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository, or submit translations using Weblate. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md). If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon). You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository, or submit translations using Weblate. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md). If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon).

View File

@ -7,6 +7,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
before_action :skip_unknown_actor_delete before_action :skip_unknown_actor_delete
before_action :require_signature! before_action :require_signature!
skip_before_action :authenticate_user!
def create def create
upgrade_account upgrade_account

View File

@ -55,7 +55,8 @@ module Admin
params.permit( params.permit(
:account_id, :account_id,
:resolved, :resolved,
:target_account_id :target_account_id,
:by_target_domain
) )
end end

View File

@ -3,6 +3,8 @@
class Api::ProofsController < Api::BaseController class Api::ProofsController < Api::BaseController
include AccountOwnedConcern include AccountOwnedConcern
skip_before_action :require_authenticated_user!
before_action :set_provider before_action :set_provider
def index def index

View File

@ -25,7 +25,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
end end
def user_settings_params def user_settings_params
return nil unless params.key?(:source) return nil if params[:source].blank?
source_params = params.require(:source) source_params = params.require(:source)

View File

@ -0,0 +1,66 @@
# frozen_string_literal: true
class Api::V1::BookmarksController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:bookmarks' }
before_action :require_user!
after_action :insert_pagination_headers
respond_to :json
def index
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
end
private
def load_statuses
cached_bookmarks
end
def cached_bookmarks
cache_collection(
Status.reorder(nil).joins(:bookmarks).merge(results),
Status
)
end
def results
@_results ||= account_bookmarks.paginate_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
end
def account_bookmarks
current_account.bookmarks
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_bookmarks_url pagination_params(max_id: pagination_max_id) if records_continue?
end
def prev_path
api_v1_bookmarks_url pagination_params(min_id: pagination_since_id) unless results.empty?
end
def pagination_max_id
results.last.id
end
def pagination_since_id
results.first.id
end
def records_continue?
results.size == limit_param(DEFAULT_STATUSES_LIMIT)
end
def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params)
end
end

View File

@ -51,6 +51,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
def data_params def data_params
return {} if params[:data].blank? return {} if params[:data].blank?
params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention, :poll]) params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
end end
end end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
class Api::V1::Statuses::BookmarksController < Api::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write, :'write:bookmarks' }
before_action :require_user!
respond_to :json
def create
@status = bookmarked_status
render json: @status, serializer: REST::StatusSerializer
end
def destroy
@status = requested_status
@bookmarks_map = { @status.id => false }
bookmark = Bookmark.find_by!(account: current_user.account, status: @status)
bookmark.destroy!
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_user&.account_id, bookmarks_map: @bookmarks_map)
end
private
def bookmarked_status
authorize_with current_user.account, requested_status, :show?
bookmark = Bookmark.find_or_create_by!(account: current_user.account, status: requested_status)
bookmark.status.reload
end
def requested_status
Status.find(params[:status_id])
end
end

View File

@ -5,11 +5,17 @@ class Api::V1::StreamingController < Api::BaseController
def index def index
if Rails.configuration.x.streaming_api_base_url != request.host if Rails.configuration.x.streaming_api_base_url != request.host
uri = URI.parse(request.url) redirect_to streaming_api_url, status: 301
uri.host = URI.parse(Rails.configuration.x.streaming_api_base_url).host
redirect_to uri.to_s, status: 301
else else
raise ActiveRecord::RecordNotFound not_found
end end
end end
private
def streaming_api_url
Addressable::URI.parse(request.url).tap do |uri|
uri.host = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url).host
end.to_s
end
end end

View File

@ -19,6 +19,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
data = { data = {
alerts: { alerts: {
follow: alerts_enabled, follow: alerts_enabled,
follow_request: false,
favourite: alerts_enabled, favourite: alerts_enabled,
reblog: alerts_enabled, reblog: alerts_enabled,
mention: alerts_enabled, mention: alerts_enabled,
@ -58,6 +59,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
end end
def data_params def data_params
@data_params ||= params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention, :poll]) @data_params ||= params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
end end
end end

View File

@ -136,9 +136,6 @@ class ApplicationController < ActionController::Base
end end
def respond_with_error(code) def respond_with_error(code)
respond_to do |format| render "errors/#{code}", layout: 'error', status: code
format.any { head code }
format.html { render "errors/#{code}", layout: 'error', status: code }
end
end end
end end

View File

@ -57,6 +57,7 @@ class Settings::PreferencesController < Settings::BaseController
:setting_use_blurhash, :setting_use_blurhash,
:setting_use_pending_items, :setting_use_pending_items,
:setting_trends, :setting_trends,
:setting_crop_images,
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag), notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag),
interactions: %i(must_be_follower must_be_following must_be_following_dm) interactions: %i(must_be_follower must_be_following must_be_following_dm)
) )

View File

@ -0,0 +1,106 @@
# frozen_string_literal: true
module AccountsHelper
def display_name(account, **options)
if options[:custom_emojify]
Formatter.instance.format_display_name(account, options)
else
account.display_name.presence || account.username
end
end
def acct(account)
if account.local?
"@#{account.acct}@#{Rails.configuration.x.local_domain}"
else
"@#{account.acct}"
end
end
def account_action_button(account)
if user_signed_in?
if account.id == current_user.account_id
link_to settings_profile_url, class: 'button logo-button' do
safe_join([svg_logo, t('settings.edit_profile')])
end
elsif current_account.following?(account) || current_account.requested?(account)
link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do
safe_join([svg_logo, t('accounts.unfollow')])
end
elsif !(account.memorial? || account.moved?)
link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do
safe_join([svg_logo, t('accounts.follow')])
end
end
elsif !(account.memorial? || account.moved?)
link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do
safe_join([svg_logo, t('accounts.follow')])
end
end
end
def minimal_account_action_button(account)
if user_signed_in?
return if account.id == current_user.account_id
if current_account.following?(account) || current_account.requested?(account)
link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do
fa_icon('user-times fw')
end
elsif !(account.memorial? || account.moved?)
link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do
fa_icon('user-plus fw')
end
end
elsif !(account.memorial? || account.moved?)
link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do
fa_icon('user-plus fw')
end
end
end
def account_badge(account, all: false)
if account.bot?
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
elsif (Setting.show_staff_badge && account.user_staff?) || all
content_tag(:div, class: 'roles') do
if all && !account.user_staff?
content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role')
elsif account.user_admin?
content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin')
elsif account.user_moderator?
content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator')
end
end
end
end
def account_description(account)
prepend_str = [
[
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
I18n.t('accounts.posts', count: account.statuses_count),
].join(' '),
[
number_to_human(account.following_count, strip_insignificant_zeros: true),
I18n.t('accounts.following', count: account.following_count),
].join(' '),
[
number_to_human(account.followers_count, strip_insignificant_zeros: true),
I18n.t('accounts.followers', count: account.followers_count),
].join(' '),
].join(', ')
[prepend_str, account.note].join(' · ')
end
def svg_logo
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
end
def svg_logo_full
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678')
end
end

View File

@ -44,6 +44,8 @@ module Admin::ActionLogsHelper
'flag' 'flag'
when 'DomainBlock' when 'DomainBlock'
'lock' 'lock'
when 'DomainAllow'
'plus-circle'
when 'EmailDomainBlock' when 'EmailDomainBlock'
'envelope' 'envelope'
when 'Status' when 'Status'
@ -86,7 +88,7 @@ module Admin::ActionLogsHelper
record.shortcode record.shortcode
when 'Report' when 'Report'
link_to "##{record.id}", admin_report_path(record) link_to "##{record.id}", admin_report_path(record)
when 'DomainBlock', 'EmailDomainBlock' when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock'
link_to record.domain, "https://#{record.domain}" link_to record.domain, "https://#{record.domain}"
when 'Status' when 'Status'
link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record) link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record)
@ -99,7 +101,7 @@ module Admin::ActionLogsHelper
case type case type
when 'CustomEmoji' when 'CustomEmoji'
attributes['shortcode'] attributes['shortcode']
when 'DomainBlock', 'EmailDomainBlock' when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock'
link_to attributes['domain'], "https://#{attributes['domain']}" link_to attributes['domain'], "https://#{attributes['domain']}"
when 'Status' when 'Status'
tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count')) tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count'))

View File

@ -2,7 +2,7 @@
module Admin::FilterHelper module Admin::FilterHelper
ACCOUNT_FILTERS = %i(local remote by_domain active pending silenced suspended username display_name email ip staff).freeze ACCOUNT_FILTERS = %i(local remote by_domain active pending silenced suspended username display_name email ip staff).freeze
REPORT_FILTERS = %i(resolved account_id target_account_id).freeze REPORT_FILTERS = %i(resolved account_id target_account_id by_target_domain).freeze
INVITE_FILTER = %i(available expired).freeze INVITE_FILTER = %i(available expired).freeze
CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
TAGS_FILTERS = %i(directory reviewed unreviewed pending_review popular active name).freeze TAGS_FILTERS = %i(directory reviewed unreviewed pending_review popular active name).freeze

View File

@ -6,7 +6,7 @@ module DomainControlHelper
domain = begin domain = begin
if uri_or_domain.include?('://') if uri_or_domain.include?('://')
Addressable::URI.parse(uri_or_domain).domain Addressable::URI.parse(uri_or_domain).host
else else
uri_or_domain uri_or_domain
end end

View File

@ -36,11 +36,13 @@ module SettingsHelper
ja: '日本語', ja: '日本語',
ka: 'ქართული', ka: 'ქართული',
kk: 'Қазақша', kk: 'Қазақша',
kn: 'ಕನ್ನಡ',
ko: '한국어', ko: '한국어',
lt: 'Lietuvių', lt: 'Lietuvių',
lv: 'Latviešu', lv: 'Latviešu',
mk: 'Македонски', mk: 'Македонски',
ml: 'മലയാളം', ml: 'മലയാളം',
mr: 'मराठी',
ms: 'Bahasa Melayu', ms: 'Bahasa Melayu',
nl: 'Nederlands', nl: 'Nederlands',
nn: 'Nynorsk', nn: 'Nynorsk',
@ -63,6 +65,7 @@ module SettingsHelper
th: 'ไทย', th: 'ไทย',
tr: 'Türkçe', tr: 'Türkçe',
uk: 'Українська', uk: 'Українська',
ur: 'اُردُو',
'zh-CN': '简体中文', 'zh-CN': '简体中文',
'zh-HK': '繁體中文(香港)', 'zh-HK': '繁體中文(香港)',
'zh-TW': '繁體中文(臺灣)', 'zh-TW': '繁體中文(臺灣)',

View File

@ -4,80 +4,6 @@ module StatusesHelper
EMBEDDED_CONTROLLER = 'statuses' EMBEDDED_CONTROLLER = 'statuses'
EMBEDDED_ACTION = 'embed' EMBEDDED_ACTION = 'embed'
def display_name(account, **options)
if options[:custom_emojify]
Formatter.instance.format_display_name(account, options)
else
account.display_name.presence || account.username
end
end
def account_action_button(account)
if user_signed_in?
if account.id == current_user.account_id
link_to settings_profile_url, class: 'button logo-button' do
safe_join([svg_logo, t('settings.edit_profile')])
end
elsif current_account.following?(account) || current_account.requested?(account)
link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do
safe_join([svg_logo, t('accounts.unfollow')])
end
elsif !(account.memorial? || account.moved?)
link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do
safe_join([svg_logo, t('accounts.follow')])
end
end
elsif !(account.memorial? || account.moved?)
link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do
safe_join([svg_logo, t('accounts.follow')])
end
end
end
def minimal_account_action_button(account)
if user_signed_in?
return if account.id == current_user.account_id
if current_account.following?(account) || current_account.requested?(account)
link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do
fa_icon('user-times fw')
end
elsif !(account.memorial? || account.moved?)
link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do
fa_icon('user-plus fw')
end
end
elsif !(account.memorial? || account.moved?)
link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do
fa_icon('user-plus fw')
end
end
end
def svg_logo
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
end
def svg_logo_full
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678')
end
def account_badge(account, all: false)
if account.bot?
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
elsif (Setting.show_staff_badge && account.user_staff?) || all
content_tag(:div, class: 'roles') do
if all && !account.user_staff?
content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role')
elsif account.user_admin?
content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin')
elsif account.user_moderator?
content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator')
end
end
end
end
def link_to_more(url) def link_to_more(url)
link_to t('statuses.show_more'), url, class: 'load-more load-gap' link_to t('statuses.show_more'), url, class: 'load-more load-gap'
end end
@ -88,27 +14,6 @@ module StatusesHelper
end end
end end
def account_description(account)
prepend_str = [
[
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
I18n.t('accounts.posts', count: account.statuses_count),
].join(' '),
[
number_to_human(account.following_count, strip_insignificant_zeros: true),
I18n.t('accounts.following', count: account.following_count),
].join(' '),
[
number_to_human(account.followers_count, strip_insignificant_zeros: true),
I18n.t('accounts.followers', count: account.followers_count),
].join(' '),
].join(', ')
[prepend_str, account.note].join(' · ')
end
def media_summary(status) def media_summary(status)
attachments = { image: 0, video: 0 } attachments = { image: 0, video: 0 }
@ -154,14 +59,6 @@ module StatusesHelper
embedded_view? ? '_blank' : nil embedded_view? ? '_blank' : nil
end end
def acct(account)
if account.local?
"@#{account.acct}@#{Rails.configuration.x.local_domain}"
else
"@#{account.acct}"
end
end
def style_classes(status, is_predecessor, is_successor, include_threads) def style_classes(status, is_predecessor, is_successor, include_threads)
classes = ['entry'] classes = ['entry']
classes << 'entry-predecessor' if is_predecessor classes << 'entry-predecessor' if is_predecessor

View File

@ -0,0 +1,90 @@
import api, { getLinks } from '../api';
import { importFetchedStatuses } from './importer';
export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST';
export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS';
export const BOOKMARKED_STATUSES_FETCH_FAIL = 'BOOKMARKED_STATUSES_FETCH_FAIL';
export const BOOKMARKED_STATUSES_EXPAND_REQUEST = 'BOOKMARKED_STATUSES_EXPAND_REQUEST';
export const BOOKMARKED_STATUSES_EXPAND_SUCCESS = 'BOOKMARKED_STATUSES_EXPAND_SUCCESS';
export const BOOKMARKED_STATUSES_EXPAND_FAIL = 'BOOKMARKED_STATUSES_EXPAND_FAIL';
export function fetchBookmarkedStatuses() {
return (dispatch, getState) => {
if (getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) {
return;
}
dispatch(fetchBookmarkedStatusesRequest());
api(getState).get('/api/v1/bookmarks').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchBookmarkedStatusesFail(error));
});
};
};
export function fetchBookmarkedStatusesRequest() {
return {
type: BOOKMARKED_STATUSES_FETCH_REQUEST,
};
};
export function fetchBookmarkedStatusesSuccess(statuses, next) {
return {
type: BOOKMARKED_STATUSES_FETCH_SUCCESS,
statuses,
next,
};
};
export function fetchBookmarkedStatusesFail(error) {
return {
type: BOOKMARKED_STATUSES_FETCH_FAIL,
error,
};
};
export function expandBookmarkedStatuses() {
return (dispatch, getState) => {
const url = getState().getIn(['status_lists', 'bookmarks', 'next'], null);
if (url === null || getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) {
return;
}
dispatch(expandBookmarkedStatusesRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandBookmarkedStatusesFail(error));
});
};
};
export function expandBookmarkedStatusesRequest() {
return {
type: BOOKMARKED_STATUSES_EXPAND_REQUEST,
};
};
export function expandBookmarkedStatusesSuccess(statuses, next) {
return {
type: BOOKMARKED_STATUSES_EXPAND_SUCCESS,
statuses,
next,
};
};
export function expandBookmarkedStatusesFail(error) {
return {
type: BOOKMARKED_STATUSES_EXPAND_FAIL,
error,
};
};

View File

@ -205,10 +205,11 @@ export function uploadCompose(files) {
return function (dispatch, getState) { return function (dispatch, getState) {
const uploadLimit = 4; const uploadLimit = 4;
const media = getState().getIn(['compose', 'media_attachments']); const media = getState().getIn(['compose', 'media_attachments']);
const pending = getState().getIn(['compose', 'pending_media_attachments']);
const progress = new Array(files.length).fill(0); const progress = new Array(files.length).fill(0);
let total = Array.from(files).reduce((a, v) => a + v.size, 0); let total = Array.from(files).reduce((a, v) => a + v.size, 0);
if (files.length + media.size > uploadLimit) { if (files.length + media.size + pending > uploadLimit) {
dispatch(showAlert(undefined, messages.uploadErrorLimit)); dispatch(showAlert(undefined, messages.uploadErrorLimit));
return; return;
} }

View File

@ -10,6 +10,12 @@ const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
return obj; return obj;
}, {}); }, {});
export function searchTextFromRawStatus (status) {
const spoilerText = status.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
}
export function normalizeAccount(account) { export function normalizeAccount(account) {
account = { ...account }; account = { ...account };

View File

@ -33,6 +33,14 @@ export const UNPIN_REQUEST = 'UNPIN_REQUEST';
export const UNPIN_SUCCESS = 'UNPIN_SUCCESS'; export const UNPIN_SUCCESS = 'UNPIN_SUCCESS';
export const UNPIN_FAIL = 'UNPIN_FAIL'; export const UNPIN_FAIL = 'UNPIN_FAIL';
export const BOOKMARK_REQUEST = 'BOOKMARK_REQUEST';
export const BOOKMARK_SUCCESS = 'BOOKMARKED_SUCCESS';
export const BOOKMARK_FAIL = 'BOOKMARKED_FAIL';
export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
export function reblog(status) { export function reblog(status) {
return function (dispatch, getState) { return function (dispatch, getState) {
dispatch(reblogRequest(status)); dispatch(reblogRequest(status));
@ -187,6 +195,78 @@ export function unfavouriteFail(status, error) {
}; };
}; };
export function bookmark(status) {
return function (dispatch, getState) {
dispatch(bookmarkRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function (response) {
dispatch(importFetchedStatus(response.data));
dispatch(bookmarkSuccess(status, response.data));
}).catch(function (error) {
dispatch(bookmarkFail(status, error));
});
};
};
export function unbookmark(status) {
return (dispatch, getState) => {
dispatch(unbookmarkRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(unbookmarkSuccess(status, response.data));
}).catch(error => {
dispatch(unbookmarkFail(status, error));
});
};
};
export function bookmarkRequest(status) {
return {
type: BOOKMARK_REQUEST,
status: status,
};
};
export function bookmarkSuccess(status, response) {
return {
type: BOOKMARK_SUCCESS,
status: status,
response: response,
};
};
export function bookmarkFail(status, error) {
return {
type: BOOKMARK_FAIL,
status: status,
error: error,
};
};
export function unbookmarkRequest(status) {
return {
type: UNBOOKMARK_REQUEST,
status: status,
};
};
export function unbookmarkSuccess(status, response) {
return {
type: UNBOOKMARK_SUCCESS,
status: status,
response: response,
};
};
export function unbookmarkFail(status, error) {
return {
type: UNBOOKMARK_FAIL,
status: status,
error: error,
};
};
export function fetchReblogs(id) { export function fetchReblogs(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(fetchReblogsRequest(id)); dispatch(fetchReblogsRequest(id));

View File

@ -14,6 +14,7 @@ import { unescapeHTML } from '../utils/html';
import { getFiltersRegex } from '../selectors'; import { getFiltersRegex } from '../selectors';
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
import compareId from 'mastodon/compare_id'; import compareId from 'mastodon/compare_id';
import { searchTextFromRawStatus } from 'mastodon/actions/importer/normalizer';
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
@ -60,7 +61,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
if (notification.type === 'mention') { if (notification.type === 'mention') {
const dropRegex = filters[0]; const dropRegex = filters[0];
const regex = filters[1]; const regex = filters[1];
const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content); const searchIndex = searchTextFromRawStatus(notification.status);
if (dropRegex && dropRegex.test(searchIndex)) { if (dropRegex && dropRegex.test(searchIndex)) {
return; return;
@ -109,7 +110,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
const excludeTypesFromFilter = filter => { const excludeTypesFromFilter = filter => {
const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention', 'poll']); const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll']);
return allTypes.filterNot(item => item === filter).toJS(); return allTypes.filterNot(item => item === filter).toJS();
}; };

View File

@ -25,7 +25,7 @@ export default class AttachmentList extends ImmutablePureComponent {
return ( return (
<li key={attachment.get('id')}> <li key={attachment.get('id')}>
<a href={displayUrl} target='_blank' rel='noopener'><Icon id='link' /> {filename(displayUrl)}</a> <a href={displayUrl} target='_blank' rel='noopener noreferrer'><Icon id='link' /> {filename(displayUrl)}</a>
</li> </li>
); );
})} })}
@ -46,7 +46,7 @@ export default class AttachmentList extends ImmutablePureComponent {
return ( return (
<li key={attachment.get('id')}> <li key={attachment.get('id')}>
<a href={displayUrl} target='_blank' rel='noopener'>{filename(displayUrl)}</a> <a href={displayUrl} target='_blank' rel='noopener noreferrer'>{filename(displayUrl)}</a>
</li> </li>
); );
})} })}

View File

@ -143,7 +143,7 @@ class DropdownMenu extends React.PureComponent {
return ( return (
<li className='dropdown-menu__item' key={`${text}-${i}`}> <li className='dropdown-menu__item' key={`${text}-${i}`}>
<a href={href} target={target} data-method={method} rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}> <a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
{text} {text}
</a> </a>
</li> </li>

View File

@ -58,7 +58,7 @@ export default class ErrorBoundary extends React.PureComponent {
<div> <div>
<p className='error-boundary__error'><FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' /></p> <p className='error-boundary__error'><FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' /></p>
<p><FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' /></p> <p><FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' /></p>
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied && 'copied'}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p> <p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied && 'copied'}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
</div> </div>
</div> </div>
); );

View File

@ -1,63 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
export default class ExtendedVideoPlayer extends React.PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
time: PropTypes.number,
controls: PropTypes.bool.isRequired,
muted: PropTypes.bool.isRequired,
onClick: PropTypes.func,
};
handleLoadedData = () => {
if (this.props.time) {
this.video.currentTime = this.props.time;
}
}
componentDidMount () {
this.video.addEventListener('loadeddata', this.handleLoadedData);
}
componentWillUnmount () {
this.video.removeEventListener('loadeddata', this.handleLoadedData);
}
setRef = (c) => {
this.video = c;
}
handleClick = e => {
e.stopPropagation();
const handler = this.props.onClick;
if (handler) handler();
}
render () {
const { src, muted, controls, alt } = this.props;
return (
<div className='extended-video-player'>
<video
ref={this.setRef}
src={src}
autoPlay
role='button'
tabIndex='0'
aria-label={alt}
title={alt}
muted={muted}
controls={controls}
loop={!controls}
onClick={this.handleClick}
/>
</div>
);
}
}

View File

@ -0,0 +1,75 @@
import React from 'react';
import PropTypes from 'prop-types';
export default class GIFV extends React.PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
onClick: PropTypes.func,
};
state = {
loading: true,
};
handleLoadedData = () => {
this.setState({ loading: false });
}
componentWillReceiveProps (nextProps) {
if (nextProps.src !== this.props.src) {
this.setState({ loading: true });
}
}
handleClick = e => {
const { onClick } = this.props;
if (onClick) {
e.stopPropagation();
onClick();
}
}
render () {
const { src, width, height, alt } = this.props;
const { loading } = this.state;
return (
<div className='gifv' style={{ position: 'relative' }}>
{loading && (
<canvas
width={width}
height={height}
role='button'
tabIndex='0'
aria-label={alt}
title={alt}
onClick={this.handleClick}
/>
)}
<video
src={src}
width={width}
height={height}
role='button'
tabIndex='0'
aria-label={alt}
title={alt}
muted
loop
autoPlay
playsInline
onClick={this.handleClick}
onLoadedData={this.handleLoadedData}
style={{ position: loading ? 'absolute' : 'static', top: 0, left: 0 }}
/>
</div>
);
}
}

View File

@ -1,6 +1,4 @@
import React from 'react'; import React from 'react';
import Motion from '../features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
@ -37,6 +35,21 @@ export default class IconButton extends React.PureComponent {
tabIndex: '0', tabIndex: '0',
}; };
state = {
activate: false,
deactivate: false,
}
componentWillReceiveProps (nextProps) {
if (!nextProps.animate) return;
if (this.props.active && !nextProps.active) {
this.setState({ activate: false, deactivate: true });
} else if (!this.props.active && nextProps.active) {
this.setState({ activate: true, deactivate: false });
}
}
handleClick = (e) => { handleClick = (e) => {
e.preventDefault(); e.preventDefault();
@ -75,7 +88,6 @@ export default class IconButton extends React.PureComponent {
const { const {
active, active,
animate,
className, className,
disabled, disabled,
expanded, expanded,
@ -87,57 +99,37 @@ export default class IconButton extends React.PureComponent {
title, title,
} = this.props; } = this.props;
const {
activate,
deactivate,
} = this.state;
const classes = classNames(className, 'icon-button', { const classes = classNames(className, 'icon-button', {
active, active,
disabled, disabled,
inverted, inverted,
activate,
deactivate,
overlayed: overlay, overlayed: overlay,
}); });
if (!animate) {
// Perf optimization: avoid unnecessary <Motion> components unless
// we actually need to animate.
return (
<button
aria-label={title}
aria-pressed={pressed}
aria-expanded={expanded}
title={title}
className={classes}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleKeyDown}
onKeyPress={this.handleKeyPress}
style={style}
tabIndex={tabIndex}
disabled={disabled}
>
<Icon id={icon} fixedWidth aria-hidden='true' />
</button>
);
}
return ( return (
<Motion defaultStyle={{ rotate: active ? -360 : 0 }} style={{ rotate: animate ? spring(active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}> <button
{({ rotate }) => ( aria-label={title}
<button aria-pressed={pressed}
aria-label={title} aria-expanded={expanded}
aria-pressed={pressed} title={title}
aria-expanded={expanded} className={classes}
title={title} onClick={this.handleClick}
className={classes} onMouseDown={this.handleMouseDown}
onClick={this.handleClick} onKeyDown={this.handleKeyDown}
onMouseDown={this.handleMouseDown} onKeyPress={this.handleKeyPress}
onKeyDown={this.handleKeyDown} style={style}
onKeyPress={this.handleKeyPress} tabIndex={tabIndex}
style={style} disabled={disabled}
tabIndex={tabIndex} >
disabled={disabled} <Icon id={icon} fixedWidth aria-hidden='true' />
> </button>
<Icon id={icon} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />
</button>
)}
</Motion>
); );
} }

View File

@ -6,7 +6,7 @@ import IconButton from './icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { isIOS } from '../is_mobile'; import { isIOS } from '../is_mobile';
import classNames from 'classnames'; import classNames from 'classnames';
import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state'; import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_state';
import { decode } from 'blurhash'; import { decode } from 'blurhash';
const messages = defineMessages({ const messages = defineMessages({
@ -159,7 +159,7 @@ class Item extends React.PureComponent {
if (attachment.get('type') === 'unknown') { if (attachment.get('type') === 'unknown') {
return ( return (
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}> <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} target='_blank' rel='noopener noreferrer'>
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' /> <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
</a> </a>
</div> </div>
@ -187,6 +187,7 @@ class Item extends React.PureComponent {
href={attachment.get('remote_url') || originalUrl} href={attachment.get('remote_url') || originalUrl}
onClick={this.handleClick} onClick={this.handleClick}
target='_blank' target='_blank'
rel='noopener noreferrer'
> >
<img <img
src={previewUrl} src={previewUrl}
@ -280,7 +281,7 @@ class MediaGallery extends React.PureComponent {
} }
handleRef = (node) => { handleRef = (node) => {
if (node /*&& this.isStandaloneEligible()*/) { if (node) {
// offsetWidth triggers a layout, so only calculate when we need to // offsetWidth triggers a layout, so only calculate when we need to
if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth); if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth);
@ -290,13 +291,13 @@ class MediaGallery extends React.PureComponent {
} }
} }
isStandaloneEligible() { isFullSizeEligible() {
const { media, standalone } = this.props; const { media } = this.props;
return standalone && media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']); return media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
} }
render () { render () {
const { media, intl, sensitive, height, defaultWidth } = this.props; const { media, intl, sensitive, height, defaultWidth, standalone } = this.props;
const { visible } = this.state; const { visible } = this.state;
const width = this.state.width || defaultWidth; const width = this.state.width || defaultWidth;
@ -305,7 +306,7 @@ class MediaGallery extends React.PureComponent {
const style = {}; const style = {};
if (this.isStandaloneEligible()) { if (this.isFullSizeEligible() && (standalone || !cropImages)) {
if (width) { if (width) {
style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']); style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']);
} }
@ -318,7 +319,7 @@ class MediaGallery extends React.PureComponent {
const size = media.take(4).size; const size = media.take(4).size;
const uncached = media.every(attachment => attachment.get('type') === 'unknown'); const uncached = media.every(attachment => attachment.get('type') === 'unknown');
if (this.isStandaloneEligible()) { if (standalone && this.isFullSizeEligible()) {
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />; children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />;
} else { } else {
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} visible={visible || uncached} />); children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} visible={visible || uncached} />);

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import 'wicg-inert';
export default class ModalRoot extends React.PureComponent { export default class ModalRoot extends React.PureComponent {
@ -55,15 +56,21 @@ export default class ModalRoot extends React.PureComponent {
} else if (!nextProps.children) { } else if (!nextProps.children) {
this.setState({ revealed: false }); this.setState({ revealed: false });
} }
if (!nextProps.children && !!this.props.children) {
this.activeElement.focus();
this.activeElement = null;
}
} }
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
if (!this.props.children && !!prevProps.children) { if (!this.props.children && !!prevProps.children) {
this.getSiblings().forEach(sibling => sibling.removeAttribute('inert')); this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
// Because of the wicg-inert polyfill, the activeElement may not be
// immediately selectable, we have to wait for observers to run, as
// described in https://github.com/WICG/inert#performance-and-gotchas
Promise.resolve().then(() => {
this.activeElement.focus();
this.activeElement = null;
}).catch((error) => {
console.error(error);
});
} }
if (this.props.children) { if (this.props.children) {
requestAnimationFrame(() => { requestAnimationFrame(() => {

View File

@ -39,7 +39,8 @@ class Poll extends ImmutablePureComponent {
static getDerivedStateFromProps (props, state) { static getDerivedStateFromProps (props, state) {
const { poll, intl } = props; const { poll, intl } = props;
const expired = poll.get('expired') || (new Date(poll.get('expires_at'))).getTime() < intl.now(); const expires_at = poll.get('expires_at');
const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < intl.now();
return (expired === state.expired) ? null : { expired }; return (expired === state.expired) ? null : { expired };
} }

View File

@ -214,6 +214,22 @@ class Status extends ImmutablePureComponent {
this.props.onOpenVideo(media, startTime); this.props.onOpenVideo(media, startTime);
} }
handleHotkeyOpenMedia = e => {
const { status, onOpenMedia, onOpenVideo } = this.props;
e.preventDefault();
if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
// TODO: toggle play/paused?
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
onOpenVideo(status.getIn(['media_attachments', 0]), 0);
} else {
onOpenMedia(status.get('media_attachments'), 0);
}
}
}
handleHotkeyReply = e => { handleHotkeyReply = e => {
e.preventDefault(); e.preventDefault();
this.props.onReply(this._properStatus(), this.context.router.history); this.props.onReply(this._properStatus(), this.context.router.history);
@ -293,6 +309,7 @@ class Status extends ImmutablePureComponent {
moveDown: this.handleHotkeyMoveDown, moveDown: this.handleHotkeyMoveDown,
toggleHidden: this.handleHotkeyToggleHidden, toggleHidden: this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive, toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia,
}; };
if (hidden) { if (hidden) {
@ -437,9 +454,9 @@ class Status extends ImmutablePureComponent {
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}> <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' /> <div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
<div className='status__info'> <div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
<a onClick={this.handleAccountClick} target='_blank' data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name'> <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
<div className='status__avatar'> <div className='status__avatar'>
{statusAvatar} {statusAvatar}
</div> </div>

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import IconButton from './icon_button'; import IconButton from './icon_button';
import DropdownMenuContainer from '../containers/dropdown_menu_container'; import DropdownMenuContainer from '../containers/dropdown_menu_container';
@ -23,6 +24,8 @@ const messages = defineMessages({
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' },
open: { id: 'status.open', defaultMessage: 'Expand this status' }, open: { id: 'status.open', defaultMessage: 'Expand this status' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' }, report: { id: 'status.report', defaultMessage: 'Report @{name}' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
@ -33,6 +36,10 @@ const messages = defineMessages({
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
}); });
const obfuscatedCount = count => { const obfuscatedCount = count => {
@ -45,7 +52,12 @@ const obfuscatedCount = count => {
} }
}; };
export default @injectIntl const mapStateToProps = (state, { status }) => ({
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
});
export default @connect(mapStateToProps)
@injectIntl
class StatusActionBar extends ImmutablePureComponent { class StatusActionBar extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
@ -54,6 +66,7 @@ class StatusActionBar extends ImmutablePureComponent {
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.map,
onReply: PropTypes.func, onReply: PropTypes.func,
onFavourite: PropTypes.func, onFavourite: PropTypes.func,
onReblog: PropTypes.func, onReblog: PropTypes.func,
@ -61,11 +74,16 @@ class StatusActionBar extends ImmutablePureComponent {
onDirect: PropTypes.func, onDirect: PropTypes.func,
onMention: PropTypes.func, onMention: PropTypes.func,
onMute: PropTypes.func, onMute: PropTypes.func,
onUnmute: PropTypes.func,
onBlock: PropTypes.func, onBlock: PropTypes.func,
onUnblock: PropTypes.func,
onBlockDomain: PropTypes.func,
onUnblockDomain: PropTypes.func,
onReport: PropTypes.func, onReport: PropTypes.func,
onEmbed: PropTypes.func, onEmbed: PropTypes.func,
onMuteConversation: PropTypes.func, onMuteConversation: PropTypes.func,
onPin: PropTypes.func, onPin: PropTypes.func,
onBookmark: PropTypes.func,
withDismiss: PropTypes.bool, withDismiss: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -74,6 +92,7 @@ class StatusActionBar extends ImmutablePureComponent {
// evaluate to false. See react-immutable-pure-component for usage. // evaluate to false. See react-immutable-pure-component for usage.
updateOnProps = [ updateOnProps = [
'status', 'status',
'relationship',
'withDismiss', 'withDismiss',
] ]
@ -114,6 +133,10 @@ class StatusActionBar extends ImmutablePureComponent {
window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
} }
handleBookmarkClick = () => {
this.props.onBookmark(this.props.status);
}
handleDeleteClick = () => { handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.context.router.history); this.props.onDelete(this.props.status, this.context.router.history);
} }
@ -135,11 +158,39 @@ class StatusActionBar extends ImmutablePureComponent {
} }
handleMuteClick = () => { handleMuteClick = () => {
this.props.onMute(this.props.status.get('account')); const { status, relationship, onMute, onUnmute } = this.props;
const account = status.get('account');
if (relationship && relationship.get('muting')) {
onUnmute(account);
} else {
onMute(account);
}
} }
handleBlockClick = () => { handleBlockClick = () => {
this.props.onBlock(this.props.status); const { status, relationship, onBlock, onUnblock } = this.props;
const account = status.get('account');
if (relationship && relationship.get('blocking')) {
onBlock(status);
} else {
onUnblock(account);
}
}
handleBlockDomain = () => {
const { status, onBlockDomain } = this.props;
const account = status.get('account');
onBlockDomain(account.get('acct').split('@')[1]);
}
handleUnblockDomain = () => {
const { status, onUnblockDomain } = this.props;
const account = status.get('account');
onUnblockDomain(account.get('acct').split('@')[1]);
} }
handleOpen = () => { handleOpen = () => {
@ -178,11 +229,12 @@ class StatusActionBar extends ImmutablePureComponent {
} }
render () { render () {
const { status, intl, withDismiss } = this.props; const { status, relationship, intl, withDismiss } = this.props;
const mutingConversation = status.get('muted'); const mutingConversation = status.get('muted');
const anonymousAccess = !me; const anonymousAccess = !me;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const account = status.get('account');
let menu = []; let menu = [];
let reblogIcon = 'retweet'; let reblogIcon = 'retweet';
@ -196,6 +248,7 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
} }
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
menu.push(null); menu.push(null);
if (status.getIn(['account', 'id']) === me || withDismiss) { if (status.getIn(['account', 'id']) === me || withDismiss) {
@ -215,16 +268,39 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
} else { } else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); if (relationship && relationship.get('muting')) {
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
} else {
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick });
}
if (relationship && relationship.get('blocking')) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
} else {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick });
}
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport });
if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1];
menu.push(null);
if (relationship && relationship.get('domain_blocking')) {
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
} else {
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain });
}
}
if (isStaff) { if (isStaff) {
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` }); menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
} }
} }

View File

@ -59,7 +59,7 @@ export default class StatusContent extends React.PureComponent {
} }
link.setAttribute('target', '_blank'); link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener'); link.setAttribute('rel', 'noopener noreferrer');
} }
if ( if (
@ -216,14 +216,14 @@ export default class StatusContent extends React.PureComponent {
return ( return (
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}> <div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}> <p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
<span dangerouslySetInnerHTML={spoilerContent} lang={status.get('language')} /> <span dangerouslySetInnerHTML={spoilerContent} />
{' '} {' '}
<button tabIndex='0' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick}>{toggleText}</button> <button tabIndex='0' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick}>{toggleText}</button>
</p> </p>
{mentionsPlaceholder} {mentionsPlaceholder}
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} /> <div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
{!hidden && !!status.get('poll') && <PollContainer pollId={status.get('poll')} />} {!hidden && !!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
</div> </div>
@ -231,7 +231,7 @@ export default class StatusContent extends React.PureComponent {
} else if (this.props.onClick) { } else if (this.props.onClick) {
const output = [ const output = [
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content'> <div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content'>
<div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} /> <div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} />
{!!status.get('poll') && <PollContainer pollId={status.get('poll')} />} {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
</div>, </div>,
@ -245,7 +245,7 @@ export default class StatusContent extends React.PureComponent {
} else { } else {
return ( return (
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle}> <div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle}>
<div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} lang={status.get('language')} /> <div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} />
{!!status.get('poll') && <PollContainer pollId={status.get('poll')} />} {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
</div> </div>

View File

@ -1,4 +1,5 @@
import { openDropdownMenu, closeDropdownMenu } from '../actions/dropdown_menu'; import { openDropdownMenu, closeDropdownMenu } from '../actions/dropdown_menu';
import { fetchRelationships } from 'mastodon/actions/accounts';
import { openModal, closeModal } from '../actions/modal'; import { openModal, closeModal } from '../actions/modal';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import DropdownMenu from '../components/dropdown_menu'; import DropdownMenu from '../components/dropdown_menu';
@ -13,12 +14,17 @@ const mapStateToProps = state => ({
const mapDispatchToProps = (dispatch, { status, items }) => ({ const mapDispatchToProps = (dispatch, { status, items }) => ({
onOpen(id, onItemClick, dropdownPlacement, keyboard) { onOpen(id, onItemClick, dropdownPlacement, keyboard) {
if (status) {
dispatch(fetchRelationships([status.getIn(['account', 'id'])]));
}
dispatch(isUserTouching() ? openModal('ACTIONS', { dispatch(isUserTouching() ? openModal('ACTIONS', {
status, status,
actions: items, actions: items,
onClick: onItemClick, onClick: onItemClick,
}) : openDropdownMenu(id, dropdownPlacement, keyboard)); }) : openDropdownMenu(id, dropdownPlacement, keyboard));
}, },
onClose(id) { onClose(id) {
dispatch(closeModal('ACTIONS')); dispatch(closeModal('ACTIONS'));
dispatch(closeDropdownMenu(id)); dispatch(closeDropdownMenu(id));

View File

@ -1,3 +1,4 @@
import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Status from '../components/status'; import Status from '../components/status';
import { makeGetStatus } from '../selectors'; import { makeGetStatus } from '../selectors';
@ -9,8 +10,10 @@ import {
import { import {
reblog, reblog,
favourite, favourite,
bookmark,
unreblog, unreblog,
unfavourite, unfavourite,
unbookmark,
pin, pin,
unpin, unpin,
} from '../actions/interactions'; } from '../actions/interactions';
@ -21,11 +24,19 @@ import {
hideStatus, hideStatus,
revealStatus, revealStatus,
} from '../actions/statuses'; } from '../actions/statuses';
import {
unmuteAccount,
unblockAccount,
} from '../actions/accounts';
import {
blockDomain,
unblockDomain,
} from '../actions/domain_blocks';
import { initMuteModal } from '../actions/mutes'; import { initMuteModal } from '../actions/mutes';
import { initBlockModal } from '../actions/blocks'; import { initBlockModal } from '../actions/blocks';
import { initReport } from '../actions/reports'; import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal'; import { openModal } from '../actions/modal';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { boostModal, deleteModal } from '../initial_state'; import { boostModal, deleteModal } from '../initial_state';
import { showAlertForError } from '../actions/alerts'; import { showAlertForError } from '../actions/alerts';
@ -36,6 +47,7 @@ const messages = defineMessages({
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
}); });
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
@ -90,6 +102,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
} }
}, },
onBookmark (status) {
if (status.get('bookmarked')) {
dispatch(unbookmark(status));
} else {
dispatch(bookmark(status));
}
},
onPin (status) { onPin (status) {
if (status.get('pinned')) { if (status.get('pinned')) {
dispatch(unpin(status)); dispatch(unpin(status));
@ -138,6 +158,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(initBlockModal(account)); dispatch(initBlockModal(account));
}, },
onUnblock (account) {
dispatch(unblockAccount(account.get('id')));
},
onReport (status) { onReport (status) {
dispatch(initReport(status.get('account'), status)); dispatch(initReport(status.get('account'), status));
}, },
@ -146,6 +170,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(initMuteModal(account)); dispatch(initMuteModal(account));
}, },
onUnmute (account) {
dispatch(unmuteAccount(account.get('id')));
},
onMuteConversation (status) { onMuteConversation (status) {
if (status.get('muted')) { if (status.get('muted')) {
dispatch(unmuteStatus(status.get('id'))); dispatch(unmuteStatus(status.get('id')));
@ -162,6 +190,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
} }
}, },
onBlockDomain (domain) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.domain_block.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. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => dispatch(blockDomain(domain)),
}));
},
onUnblockDomain (domain) {
dispatch(unblockDomain(domain));
},
}); });
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status)); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));

View File

@ -253,7 +253,7 @@ class Header extends ImmutablePureComponent {
<div className='account__header__bar'> <div className='account__header__bar'>
<div className='account__header__tabs'> <div className='account__header__tabs'>
<a className='avatar' href={account.get('url')} rel='noopener' target='_blank'> <a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'>
<Avatar account={account} size={90} /> <Avatar account={account} size={90} />
</a> </a>
@ -282,10 +282,10 @@ class Header extends ImmutablePureComponent {
<dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} /> <dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} />
<dd className='verified'> <dd className='verified'>
<a href={proof.get('proof_url')} target='_blank' rel='noopener'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}> <a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
<Icon id='check' className='verified__mark' /> <Icon id='check' className='verified__mark' />
</span></a> </span></a>
<a href={proof.get('profile_url')} target='_blank' rel='noopener'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a> <a href={proof.get('profile_url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a>
</dd> </dd>
</dl> </dl>
))} ))}

View File

@ -1,12 +1,12 @@
import React from 'react'; import { decode } from 'blurhash';
import PropTypes from 'prop-types'; import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import { autoPlayGif, displayMedia } from 'mastodon/initial_state'; import { autoPlayGif, displayMedia } from 'mastodon/initial_state';
import classNames from 'classnames';
import { decode } from 'blurhash';
import { isIOS } from 'mastodon/is_mobile'; import { isIOS } from 'mastodon/is_mobile';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
export default class MediaItem extends ImmutablePureComponent { export default class MediaItem extends ImmutablePureComponent {
@ -151,7 +151,7 @@ export default class MediaItem extends ImmutablePureComponent {
return ( return (
<div className='account-gallery__item' style={{ width, height }}> <div className='account-gallery__item' style={{ width, height }}>
<a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick} title={title}> <a className='media-gallery__item-thumbnail' href={status.get('url')} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'>
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} /> <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} />
{visible && thumbnail} {visible && thumbnail}
{!visible && icon} {!visible && icon}

View File

@ -12,6 +12,7 @@ const messages = defineMessages({
pause: { id: 'video.pause', defaultMessage: 'Pause' }, pause: { id: 'video.pause', defaultMessage: 'Pause' },
mute: { id: 'video.mute', defaultMessage: 'Mute sound' }, mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' }, unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
download: { id: 'video.download', defaultMessage: 'Download file' },
}); });
export default @injectIntl export default @injectIntl
@ -202,6 +203,7 @@ class Audio extends React.PureComponent {
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}> <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
&nbsp;
<div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} /> <div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
<span <span
@ -217,6 +219,14 @@ class Audio extends React.PureComponent {
<span className='video-player__time-total'>{formatTime(this.state.duration || Math.floor(this.props.duration))}</span> <span className='video-player__time-total'>{formatTime(this.state.duration || Math.floor(this.props.duration))}</span>
</span> </span>
</div> </div>
<div className='video-player__buttons right'>
<button type='button' aria-label={intl.formatMessage(messages.download)}>
<a className='video-player__download__icon' href={this.props.src} download>
<Icon id={'download'} fixedWidth />
</a>
</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,104 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from '../../actions/bookmarks';
import Column from '../ui/components/column';
import ColumnHeader from '../../components/column_header';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import StatusList from '../../components/status_list';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { debounce } from 'lodash';
const messages = defineMessages({
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
});
const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'bookmarks', 'items']),
isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
});
export default @connect(mapStateToProps)
@injectIntl
class Bookmarks extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
statusIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
};
componentWillMount () {
this.props.dispatch(fetchBookmarkedStatuses());
}
handlePin = () => {
const { columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('BOOKMARKS', {}));
}
}
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
handleHeaderClick = () => {
this.column.scrollTop();
}
setRef = c => {
this.column = c;
}
handleLoadMore = debounce(() => {
this.props.dispatch(expandBookmarkedStatuses());
}, 300, { leading: true })
render () {
const { intl, shouldUpdateScroll, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
const pinned = !!columnId;
const emptyMessage = <FormattedMessage id='empty_column.bookmarked_statuses' defaultMessage="You don't have any bookmarked toots yet. When you bookmark one, it will show up here." />;
return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
<ColumnHeader
icon='bookmark'
title={intl.formatMessage(messages.heading)}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
showBackButton
/>
<StatusList
trackScroll={!pinned}
statusIds={statusIds}
scrollKey={`bookmarked_statuses-${columnId}`}
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
/>
</Column>
);
}
}

View File

@ -14,15 +14,16 @@ const messages = defineMessages({
title: { id: 'column.community', defaultMessage: 'Local timeline' }, title: { id: 'column.community', defaultMessage: 'Local timeline' },
}); });
const mapStateToProps = (state, { onlyMedia, columnId }) => { const mapStateToProps = (state, { columnId }) => {
const uuid = columnId; const uuid = columnId;
const columns = state.getIn(['settings', 'columns']); const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid); const index = columns.findIndex(c => c.get('uuid') === uuid);
const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'community', 'other', 'onlyMedia']);
const timelineState = state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`]); const timelineState = state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`]);
return { return {
hasUnread: !!timelineState && (timelineState.get('unread') > 0 || timelineState.get('pendingItems').size > 0), hasUnread: !!timelineState && timelineState.get('unread') > 0,
onlyMedia: (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'community', 'other', 'onlyMedia']), onlyMedia,
}; };
}; };

View File

@ -142,11 +142,9 @@ class PollForm extends ImmutablePureComponent {
</ul> </ul>
<div className='poll__footer'> <div className='poll__footer'>
{options.size < 4 && ( <button disabled={options.size >= 4} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>
<button className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>
)}
<select value={expiresIn} onChange={this.handleSelectDuration}> <select value={expiresIn} onBlur={this.handleSelectDuration}>
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option> <option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option> <option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
<option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option> <option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>

View File

@ -3,7 +3,7 @@ import UploadButton from '../components/upload_button';
import { uploadCompose } from '../../../actions/compose'; import { uploadCompose } from '../../../actions/compose';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')))), disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size + state.getIn(['compose', 'pending_media_attachments']) > 3 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')))),
unavailable: state.getIn(['compose', 'poll']) !== null, unavailable: state.getIn(['compose', 'poll']) !== null,
resetFileKey: state.getIn(['compose', 'resetFileKey']), resetFileKey: state.getIn(['compose', 'resetFileKey']),
}); });

View File

@ -12,6 +12,7 @@ import IconButton from 'mastodon/components/icon_button';
import RelativeTimestamp from 'mastodon/components/relative_timestamp'; import RelativeTimestamp from 'mastodon/components/relative_timestamp';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import { autoPlayGif } from 'mastodon/initial_state'; import { autoPlayGif } from 'mastodon/initial_state';
import classNames from 'classnames';
const messages = defineMessages({ const messages = defineMessages({
more: { id: 'status.more', defaultMessage: 'More' }, more: { id: 'status.more', defaultMessage: 'More' },
@ -158,7 +159,7 @@ class Conversation extends ImmutablePureComponent {
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div className='conversation focusable muted' tabIndex='0'> <div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex='0'>
<div className='conversation__avatar'> <div className='conversation__avatar'>
<AvatarComposite accounts={accounts} size={48} /> <AvatarComposite accounts={accounts} size={48} />
</div> </div>
@ -166,7 +167,7 @@ class Conversation extends ImmutablePureComponent {
<div className='conversation__content'> <div className='conversation__content'>
<div className='conversation__content__info'> <div className='conversation__content__info'>
<div className='conversation__content__relative-time'> <div className='conversation__content__relative-time'>
<RelativeTimestamp timestamp={lastStatus.get('created_at')} /> {unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
</div> </div>
<div className='conversation__content__names' ref={this.setNamesRef}> <div className='conversation__content__names' ref={this.setNamesRef}>

View File

@ -22,6 +22,7 @@ const messages = defineMessages({
settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' }, settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' }, direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' },
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
@ -126,11 +127,12 @@ class GettingStarted extends ImmutablePureComponent {
navItems.push( navItems.push(
<ColumnLink key={i++} icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />, <ColumnLink key={i++} icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />,
<ColumnLink key={i++} icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />,
<ColumnLink key={i++} icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />, <ColumnLink key={i++} icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key={i++} icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' /> <ColumnLink key={i++} icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />
); );
height += 48*3; height += 48*4;
if (myAccount.get('locked') || unreadFollowRequests > 0) { if (myAccount.get('locked') || unreadFollowRequests > 0) {
navItems.push(<ColumnLink key={i++} icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />); navItems.push(<ColumnLink key={i++} icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Toggle from 'react-toggle'; import Toggle from 'react-toggle';
import AsyncSelect from 'react-select/lib/Async'; import AsyncSelect from 'react-select/async';
const messages = defineMessages({ const messages = defineMessages({
placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' }, placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' },

View File

@ -56,6 +56,10 @@ class KeyboardShortcuts extends ImmutablePureComponent {
<td><kbd>enter</kbd>, <kbd>o</kbd></td> <td><kbd>enter</kbd>, <kbd>o</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open status' /></td> <td><FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open status' /></td>
</tr> </tr>
<tr>
<td><kbd>e</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.open_media' defaultMessage='to open media' /></td>
</tr>
<tr> <tr>
<td><kbd>x</kbd></td> <td><kbd>x</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td> <td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td>

View File

@ -57,6 +57,17 @@ export default class ColumnSettings extends React.PureComponent {
</div> </div>
</div> </div>
<div role='group' aria-labelledby='notifications-follow-request'>
<span id='notifications-follow-request' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow_request' defaultMessage='New follow requests:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow_request']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow_request']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow_request']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow_request']} onChange={onChange} label={soundStr} />
</div>
</div>
<div role='group' aria-labelledby='notifications-favourite'> <div role='group' aria-labelledby='notifications-favourite'>
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span> <span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>

View File

@ -0,0 +1,59 @@
import React, { Fragment } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from 'mastodon/components/avatar';
import DisplayName from 'mastodon/components/display_name';
import Permalink from 'mastodon/components/permalink';
import IconButton from 'mastodon/components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
});
export default @injectIntl
class FollowRequest extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
onAuthorize: PropTypes.func.isRequired,
onReject: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
render () {
const { intl, hidden, account, onAuthorize, onReject } = this.props;
if (!account) {
return <div />;
}
if (hidden) {
return (
<Fragment>
{account.get('display_name')}
{account.get('username')}
</Fragment>
);
}
return (
<div className='account'>
<div className='account__wrapper'>
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
<DisplayName account={account} />
</Permalink>
<div className='account__relationship'>
<IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} />
<IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} />
</div>
</div>
</div>
);
}
}

View File

@ -1,13 +1,23 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import StatusContainer from '../../../containers/status_container'; import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
import AccountContainer from '../../../containers/account_container';
import { injectIntl, FormattedMessage } from 'react-intl';
import Permalink from '../../../components/permalink';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from 'mastodon/initial_state';
import StatusContainer from 'mastodon/containers/status_container';
import AccountContainer from 'mastodon/containers/account_container';
import FollowRequestContainer from '../containers/follow_request_container';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import Permalink from 'mastodon/components/permalink';
const messages = defineMessages({
favourite: { id: 'notification.favourite', defaultMessage: '{name} favourited your status' },
follow: { id: 'notification.follow', defaultMessage: '{name} followed you' },
ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
});
const notificationForScreenReader = (intl, message, timestamp) => { const notificationForScreenReader = (intl, message, timestamp) => {
const output = [message]; const output = [message];
@ -107,7 +117,7 @@ class Notification extends ImmutablePureComponent {
return ( return (
<HotKeys handlers={this.getHandlers()}> <HotKeys handlers={this.getHandlers()}>
<div className='notification notification-follow focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow', defaultMessage: '{name} followed you' }, { name: account.get('acct') }), notification.get('created_at'))}> <div className='notification notification-follow focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.follow, { name: account.get('acct') }), notification.get('created_at'))}>
<div className='notification__message'> <div className='notification__message'>
<div className='notification__favourite-icon-wrapper'> <div className='notification__favourite-icon-wrapper'>
<Icon id='user-plus' fixedWidth /> <Icon id='user-plus' fixedWidth />
@ -118,7 +128,29 @@ class Notification extends ImmutablePureComponent {
</span> </span>
</div> </div>
<AccountContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} /> <AccountContainer id={account.get('id')} hidden={this.props.hidden} />
</div>
</HotKeys>
);
}
renderFollowRequest (notification, account, link) {
const { intl } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<div className='notification notification-follow-request focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow_request', defaultMessage: '{name} has requested to follow you' }, { name: account.get('acct') }), notification.get('created_at'))}>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='user' fixedWidth />
</div>
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.follow_request' defaultMessage='{name} has requested to follow you' values={{ name: link }} />
</span>
</div>
<FollowRequestContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} />
</div> </div>
</HotKeys> </HotKeys>
); );
@ -146,7 +178,7 @@ class Notification extends ImmutablePureComponent {
return ( return (
<HotKeys handlers={this.getHandlers()}> <HotKeys handlers={this.getHandlers()}>
<div className='notification notification-favourite focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.favourite', defaultMessage: '{name} favourited your status' }, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}> <div className='notification notification-favourite focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.favourite, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'> <div className='notification__message'>
<div className='notification__favourite-icon-wrapper'> <div className='notification__favourite-icon-wrapper'>
<Icon id='star' className='star-icon' fixedWidth /> <Icon id='star' className='star-icon' fixedWidth />
@ -178,7 +210,7 @@ class Notification extends ImmutablePureComponent {
return ( return (
<HotKeys handlers={this.getHandlers()}> <HotKeys handlers={this.getHandlers()}>
<div className='notification notification-reblog focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.reblog', defaultMessage: '{name} boosted your status' }, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}> <div className='notification notification-reblog focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.reblog, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'> <div className='notification__message'>
<div className='notification__favourite-icon-wrapper'> <div className='notification__favourite-icon-wrapper'>
<Icon id='retweet' fixedWidth /> <Icon id='retweet' fixedWidth />
@ -205,25 +237,31 @@ class Notification extends ImmutablePureComponent {
); );
} }
renderPoll (notification) { renderPoll (notification, account) {
const { intl } = this.props; const { intl } = this.props;
const ownPoll = me === account.get('id');
const message = ownPoll ? intl.formatMessage(messages.ownPoll) : intl.formatMessage(messages.poll);
return ( return (
<HotKeys handlers={this.getHandlers()}> <HotKeys handlers={this.getHandlers()}>
<div className='notification notification-poll focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' }), notification.get('created_at'))}> <div className='notification notification-poll focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, message, notification.get('created_at'))}>
<div className='notification__message'> <div className='notification__message'>
<div className='notification__favourite-icon-wrapper'> <div className='notification__favourite-icon-wrapper'>
<Icon id='tasks' fixedWidth /> <Icon id='tasks' fixedWidth />
</div> </div>
<span title={notification.get('created_at')}> <span title={notification.get('created_at')}>
<FormattedMessage id='notification.poll' defaultMessage='A poll you have voted in has ended' /> {ownPoll ? (
<FormattedMessage id='notification.own_poll' defaultMessage='Your poll has ended' />
) : (
<FormattedMessage id='notification.poll' defaultMessage='A poll you have voted in has ended' />
)}
</span> </span>
</div> </div>
<StatusContainer <StatusContainer
id={notification.get('status')} id={notification.get('status')}
account={notification.get('account')} account={account}
muted muted
withDismiss withDismiss
hidden={this.props.hidden} hidden={this.props.hidden}
@ -246,6 +284,8 @@ class Notification extends ImmutablePureComponent {
switch(notification.get('type')) { switch(notification.get('type')) {
case 'follow': case 'follow':
return this.renderFollow(notification, account, link); return this.renderFollow(notification, account, link);
case 'follow_request':
return this.renderFollowRequest(notification, account, link);
case 'mention': case 'mention':
return this.renderMention(notification); return this.renderMention(notification);
case 'favourite': case 'favourite':
@ -253,7 +293,7 @@ class Notification extends ImmutablePureComponent {
case 'reblog': case 'reblog':
return this.renderReblog(notification, link); return this.renderReblog(notification, link);
case 'poll': case 'poll':
return this.renderPoll(notification); return this.renderPoll(notification, account);
} }
return null; return null;

View File

@ -0,0 +1,26 @@
import { connect } from 'react-redux';
import { makeGetAccount } from 'mastodon/selectors';
import FollowRequest from '../components/follow_request';
import { authorizeFollowRequest, rejectFollowRequest } from 'mastodon/actions/accounts';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, props) => ({
account: getAccount(state, props.id),
});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { id }) => ({
onAuthorize () {
dispatch(authorizeFollowRequest(id));
},
onReject () {
dispatch(rejectFollowRequest(id));
},
});
export default connect(makeMapStateToProps, mapDispatchToProps)(FollowRequest);

View File

@ -14,14 +14,16 @@ const messages = defineMessages({
title: { id: 'column.public', defaultMessage: 'Federated timeline' }, title: { id: 'column.public', defaultMessage: 'Federated timeline' },
}); });
const mapStateToProps = (state, { onlyMedia, columnId }) => { const mapStateToProps = (state, { columnId }) => {
const uuid = columnId; const uuid = columnId;
const columns = state.getIn(['settings', 'columns']); const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid); const index = columns.findIndex(c => c.get('uuid') === uuid);
const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']);
const timelineState = state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`]);
return { return {
hasUnread: state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`, 'unread']) > 0, hasUnread: !!timelineState && timelineState.get('unread') > 0,
onlyMedia: (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']), onlyMedia,
}; };
}; };

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import IconButton from '../../../components/icon_button'; import IconButton from '../../../components/icon_button';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
@ -17,6 +18,7 @@ const messages = defineMessages({
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' }, mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
@ -29,9 +31,18 @@ const messages = defineMessages({
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
}); });
export default @injectIntl const mapStateToProps = (state, { status }) => ({
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
});
export default @connect(mapStateToProps)
@injectIntl
class ActionBar extends React.PureComponent { class ActionBar extends React.PureComponent {
static contextTypes = { static contextTypes = {
@ -40,15 +51,21 @@ class ActionBar extends React.PureComponent {
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.map,
onReply: PropTypes.func.isRequired, onReply: PropTypes.func.isRequired,
onReblog: PropTypes.func.isRequired, onReblog: PropTypes.func.isRequired,
onFavourite: PropTypes.func.isRequired, onFavourite: PropTypes.func.isRequired,
onBookmark: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired, onDirect: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired,
onMute: PropTypes.func, onMute: PropTypes.func,
onMuteConversation: PropTypes.func, onUnmute: PropTypes.func,
onBlock: PropTypes.func, onBlock: PropTypes.func,
onUnblock: PropTypes.func,
onBlockDomain: PropTypes.func,
onUnblockDomain: PropTypes.func,
onMuteConversation: PropTypes.func,
onReport: PropTypes.func, onReport: PropTypes.func,
onPin: PropTypes.func, onPin: PropTypes.func,
onEmbed: PropTypes.func, onEmbed: PropTypes.func,
@ -67,6 +84,10 @@ class ActionBar extends React.PureComponent {
this.props.onFavourite(this.props.status); this.props.onFavourite(this.props.status);
} }
handleBookmarkClick = (e) => {
this.props.onBookmark(this.props.status, e);
}
handleDeleteClick = () => { handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.context.router.history); this.props.onDelete(this.props.status, this.context.router.history);
} }
@ -84,17 +105,45 @@ class ActionBar extends React.PureComponent {
} }
handleMuteClick = () => { handleMuteClick = () => {
this.props.onMute(this.props.status.get('account')); const { status, relationship, onMute, onUnmute } = this.props;
const account = status.get('account');
if (relationship && relationship.get('muting')) {
onUnmute(account);
} else {
onMute(account);
}
}
handleBlockClick = () => {
const { status, relationship, onBlock, onUnblock } = this.props;
const account = status.get('account');
if (relationship && relationship.get('blocking')) {
onBlock(status);
} else {
onUnblock(account);
}
}
handleBlockDomain = () => {
const { status, onBlockDomain } = this.props;
const account = status.get('account');
onBlockDomain(account.get('acct').split('@')[1]);
}
handleUnblockDomain = () => {
const { status, onUnblockDomain } = this.props;
const account = status.get('account');
onUnblockDomain(account.get('acct').split('@')[1]);
} }
handleConversationMuteClick = () => { handleConversationMuteClick = () => {
this.props.onMuteConversation(this.props.status); this.props.onMuteConversation(this.props.status);
} }
handleBlockClick = () => {
this.props.onBlock(this.props.status);
}
handleReport = () => { handleReport = () => {
this.props.onReport(this.props.status); this.props.onReport(this.props.status);
} }
@ -134,10 +183,11 @@ class ActionBar extends React.PureComponent {
} }
render () { render () {
const { status, intl } = this.props; const { status, relationship, intl } = this.props;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const mutingConversation = status.get('muted'); const mutingConversation = status.get('muted');
const account = status.get('account');
let menu = []; let menu = [];
@ -165,9 +215,33 @@ class ActionBar extends React.PureComponent {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); if (relationship && relationship.get('muting')) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
} else {
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick });
}
if (relationship && relationship.get('blocking')) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
} else {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick });
}
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1];
menu.push(null);
if (relationship && relationship.get('domain_blocking')) {
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
} else {
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain });
}
}
if (isStaff) { if (isStaff) {
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
@ -198,9 +272,10 @@ class ActionBar extends React.PureComponent {
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div> <div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div> <div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
{shareButton} {shareButton}
<div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
<div className='detailed-status__action-bar-dropdown'> <div className='detailed-status__action-bar-dropdown'>
<DropdownMenuContainer size={18} icon='ellipsis-h' items={menu} direction='left' title='More' /> <DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title='More' />
</div> </div>
</div> </div>
); );

View File

@ -148,7 +148,7 @@ export default class Card extends React.PureComponent {
const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded; const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded;
const interactive = card.get('type') !== 'link'; const interactive = card.get('type') !== 'link';
const className = classnames('status-card', { horizontal, compact, interactive }); const className = classnames('status-card', { horizontal, compact, interactive });
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>; const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
const ratio = card.get('width') / card.get('height'); const ratio = card.get('width') / card.get('height');
const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio); const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
@ -180,7 +180,7 @@ export default class Card extends React.PureComponent {
<div className='status-card__actions'> <div className='status-card__actions'>
<div> <div>
<button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button> <button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
{horizontal && <a href={card.get('url')} target='_blank' rel='noopener'><Icon id='external-link' /></a>} {horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>}
</div> </div>
</div> </div>
</div> </div>
@ -208,7 +208,7 @@ export default class Card extends React.PureComponent {
} }
return ( return (
<a href={card.get('url')} className={className} target='_blank' rel='noopener' ref={this.setRef}> <a href={card.get('url')} className={className} target='_blank' rel='noopener noreferrer' ref={this.setRef}>
{embed} {embed}
{description} {description}
</a> </a>

View File

@ -156,7 +156,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
} }
if (status.get('application')) { if (status.get('application')) {
applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>; applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></span>;
} }
if (status.get('visibility') === 'direct') { if (status.get('visibility') === 'direct') {
@ -220,7 +220,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
{media} {media}
<div className='detailed-status__meta'> <div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'> <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /> <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
</a>{applicationLink} · {reblogLink} · {favouriteLink} </a>{applicationLink} · {reblogLink} · {favouriteLink}
</div> </div>

View File

@ -13,6 +13,8 @@ import Column from '../ui/components/column';
import { import {
favourite, favourite,
unfavourite, unfavourite,
bookmark,
unbookmark,
reblog, reblog,
unreblog, unreblog,
pin, pin,
@ -30,6 +32,14 @@ import {
hideStatus, hideStatus,
revealStatus, revealStatus,
} from '../../actions/statuses'; } from '../../actions/statuses';
import {
unblockAccount,
unmuteAccount,
} from '../../actions/accounts';
import {
blockDomain,
unblockDomain,
} from '../../actions/domain_blocks';
import { initMuteModal } from '../../actions/mutes'; import { initMuteModal } from '../../actions/mutes';
import { initBlockModal } from '../../actions/blocks'; import { initBlockModal } from '../../actions/blocks';
import { initReport } from '../../actions/reports'; import { initReport } from '../../actions/reports';
@ -39,7 +49,7 @@ import ColumnBackButton from '../../components/column_back_button';
import ColumnHeader from '../../components/column_header'; import ColumnHeader from '../../components/column_header';
import StatusContainer from '../../containers/status_container'; import StatusContainer from '../../containers/status_container';
import { openModal } from '../../actions/modal'; import { openModal } from '../../actions/modal';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import { boostModal, deleteModal } from '../../initial_state'; import { boostModal, deleteModal } from '../../initial_state';
@ -57,6 +67,7 @@ const messages = defineMessages({
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' }, detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
}); });
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
@ -232,6 +243,14 @@ class Status extends ImmutablePureComponent {
} }
} }
handleBookmarkClick = (status) => {
if (status.get('bookmarked')) {
this.props.dispatch(unbookmark(status));
} else {
this.props.dispatch(bookmark(status));
}
}
handleDeleteClick = (status, history, withRedraft = false) => { handleDeleteClick = (status, history, withRedraft = false) => {
const { dispatch, intl } = this.props; const { dispatch, intl } = this.props;
@ -262,6 +281,22 @@ class Status extends ImmutablePureComponent {
this.props.dispatch(openModal('VIDEO', { media, time })); this.props.dispatch(openModal('VIDEO', { media, time }));
} }
handleHotkeyOpenMedia = e => {
const { status } = this.props;
e.preventDefault();
if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
// TODO: toggle play/paused?
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
this.handleOpenVideo(status.getIn(['media_attachments', 0]), 0);
} else {
this.handleOpenMedia(status.get('media_attachments'), 0);
}
}
}
handleMuteClick = (account) => { handleMuteClick = (account) => {
this.props.dispatch(initMuteModal(account)); this.props.dispatch(initMuteModal(account));
} }
@ -307,6 +342,27 @@ class Status extends ImmutablePureComponent {
this.props.dispatch(openModal('EMBED', { url: status.get('url') })); this.props.dispatch(openModal('EMBED', { url: status.get('url') }));
} }
handleUnmuteClick = account => {
this.props.dispatch(unmuteAccount(account.get('id')));
}
handleUnblockClick = account => {
this.props.dispatch(unblockAccount(account.get('id')));
}
handleBlockDomainClick = domain => {
this.props.dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.domain_block.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. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: this.props.intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => this.props.dispatch(blockDomain(domain)),
}));
}
handleUnblockDomainClick = domain => {
this.props.dispatch(unblockDomain(domain));
}
handleHotkeyMoveUp = () => { handleHotkeyMoveUp = () => {
this.handleMoveUp(this.props.status.get('id')); this.handleMoveUp(this.props.status.get('id'));
} }
@ -466,6 +522,7 @@ class Status extends ImmutablePureComponent {
openProfile: this.handleHotkeyOpenProfile, openProfile: this.handleHotkeyOpenProfile,
toggleHidden: this.handleHotkeyToggleHidden, toggleHidden: this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive, toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia,
}; };
return ( return (
@ -499,12 +556,17 @@ class Status extends ImmutablePureComponent {
onReply={this.handleReplyClick} onReply={this.handleReplyClick}
onFavourite={this.handleFavouriteClick} onFavourite={this.handleFavouriteClick}
onReblog={this.handleReblogClick} onReblog={this.handleReblogClick}
onBookmark={this.handleBookmarkClick}
onDelete={this.handleDeleteClick} onDelete={this.handleDeleteClick}
onDirect={this.handleDirectClick} onDirect={this.handleDirectClick}
onMention={this.handleMentionClick} onMention={this.handleMentionClick}
onMute={this.handleMuteClick} onMute={this.handleMuteClick}
onUnmute={this.handleUnmuteClick}
onMuteConversation={this.handleConversationMuteClick} onMuteConversation={this.handleConversationMuteClick}
onBlock={this.handleBlockClick} onBlock={this.handleBlockClick}
onUnblock={this.handleUnblockClick}
onBlockDomain={this.handleBlockDomainClick}
onUnblockDomain={this.handleUnblockDomainClick}
onReport={this.handleReport} onReport={this.handleReport}
onPin={this.handlePin} onPin={this.handlePin}
onEmbed={this.handleEmbed} onEmbed={this.handleEmbed}

View File

@ -26,7 +26,7 @@ export default class ActionsModal extends ImmutablePureComponent {
return ( return (
<li key={`${text}-${i}`}> <li key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener' onClick={this.props.onClick} data-index={i} className={classNames({ active })}> <a href={href} target='_blank' rel='noopener noreferrer' onClick={this.props.onClick} data-index={i} className={classNames({ active })}>
{icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' inverted />} {icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' inverted />}
<div> <div>
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div> <div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
@ -42,7 +42,7 @@ export default class ActionsModal extends ImmutablePureComponent {
<div className='status light'> <div className='status light'>
<div className='boost-modal__status-header'> <div className='boost-modal__status-header'>
<div className='boost-modal__status-time'> <div className='boost-modal__status-time'>
<a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener'> <a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<RelativeTimestamp timestamp={this.props.status.get('created_at')} /> <RelativeTimestamp timestamp={this.props.status.get('created_at')} />
</a> </a>
</div> </div>

View File

@ -61,7 +61,7 @@ class BoostModal extends ImmutablePureComponent {
<div className='status light'> <div className='status light'>
<div className='boost-modal__status-header'> <div className='boost-modal__status-header'>
<div className='boost-modal__status-time'> <div className='boost-modal__status-time'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
</div> </div>
<a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'> <a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'>

View File

@ -21,6 +21,7 @@ import {
HashtagTimeline, HashtagTimeline,
DirectTimeline, DirectTimeline,
FavouritedStatuses, FavouritedStatuses,
BookmarkedStatuses,
ListTimeline, ListTimeline,
Directory, Directory,
} from '../../ui/util/async-components'; } from '../../ui/util/async-components';
@ -40,6 +41,7 @@ const componentMap = {
'HASHTAG': HashtagTimeline, 'HASHTAG': HashtagTimeline,
'DIRECT': DirectTimeline, 'DIRECT': DirectTimeline,
'FAVOURITES': FavouritedStatuses, 'FAVOURITES': FavouritedStatuses,
'BOOKMARKS': BookmarkedStatuses,
'LIST': ListTimeline, 'LIST': ListTimeline,
'DIRECTORY': Directory, 'DIRECTORY': Directory,
}; };
@ -182,7 +184,7 @@ class ColumnsArea extends ImmutablePureComponent {
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>; const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
const content = columnIndex !== -1 ? ( const content = columnIndex !== -1 ? (
<ReactSwipeableViews key='content' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}> <ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
{links.map(this.renderView)} {links.map(this.renderView)}
</ReactSwipeableViews> </ReactSwipeableViews>
) : ( ) : (

View File

@ -16,6 +16,7 @@ import UploadProgress from 'mastodon/features/compose/components/upload_progress
import CharacterCounter from 'mastodon/features/compose/components/character_counter'; import CharacterCounter from 'mastodon/features/compose/components/character_counter';
import { length } from 'stringz'; import { length } from 'stringz';
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components'; import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
import GIFV from 'mastodon/components/gifv';
const messages = defineMessages({ const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' }, close: { id: 'lightbox.close', defaultMessage: 'Close' },
@ -41,6 +42,36 @@ const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******')
const assetHost = process.env.CDN_HOST || ''; const assetHost = process.env.CDN_HOST || '';
class ImageLoader extends React.PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
width: PropTypes.number,
height: PropTypes.number,
};
state = {
loading: true,
};
componentDidMount() {
const image = new Image();
image.addEventListener('load', () => this.setState({ loading: false }));
image.src = this.props.src;
}
render () {
const { loading } = this.state;
if (loading) {
return <canvas width={this.props.width} height={this.props.height} />;
} else {
return <img {...this.props} alt='' />;
}
}
}
export default @connect(mapStateToProps, mapDispatchToProps) export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl @injectIntl
class FocalPointModal extends ImmutablePureComponent { class FocalPointModal extends ImmutablePureComponent {
@ -60,6 +91,7 @@ class FocalPointModal extends ImmutablePureComponent {
description: '', description: '',
dirty: false, dirty: false,
progress: 0, progress: 0,
loading: true,
}; };
componentWillMount () { componentWillMount () {
@ -152,6 +184,15 @@ class FocalPointModal extends ImmutablePureComponent {
this.setState({ description: e.target.value, dirty: true }); this.setState({ description: e.target.value, dirty: true });
} }
handleKeyDown = (e) => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
e.stopPropagation();
this.setState({ description: e.target.value, dirty: true });
this.handleSubmit();
}
}
handleSubmit = () => { handleSubmit = () => {
this.props.onSave(this.state.description, this.state.focusX, this.state.focusY); this.props.onSave(this.state.description, this.state.focusX, this.state.focusY);
this.props.onClose(); this.props.onClose();
@ -173,7 +214,7 @@ class FocalPointModal extends ImmutablePureComponent {
langPath: `${assetHost}/ocr/lang-data`, langPath: `${assetHost}/ocr/lang-data`,
}); });
let media_url = media.get('file'); let media_url = media.get('url');
if (window.URL && URL.createObjectURL) { if (window.URL && URL.createObjectURL) {
try { try {
@ -203,6 +244,16 @@ class FocalPointModal extends ImmutablePureComponent {
const previewWidth = 200; const previewWidth = 200;
const previewHeight = previewWidth / previewRatio; const previewHeight = previewWidth / previewRatio;
let descriptionLabel = null;
if (media.get('type') === 'audio') {
descriptionLabel = <FormattedMessage id='upload_form.audio_description' defaultMessage='Describe for people with hearing loss' />;
} else if (media.get('type') === 'video') {
descriptionLabel = <FormattedMessage id='upload_form.video_description' defaultMessage='Describe for people with hearing loss or visual impairment' />;
} else {
descriptionLabel = <FormattedMessage id='upload_form.description' defaultMessage='Describe for the visually impaired' />;
}
return ( return (
<div className='modal-root__modal report-modal' style={{ maxWidth: 960 }}> <div className='modal-root__modal report-modal' style={{ maxWidth: 960 }}>
<div className='report-modal__target'> <div className='report-modal__target'>
@ -214,7 +265,9 @@ class FocalPointModal extends ImmutablePureComponent {
<div className='report-modal__comment'> <div className='report-modal__comment'>
{focals && <p><FormattedMessage id='upload_modal.hint' defaultMessage='Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.' /></p>} {focals && <p><FormattedMessage id='upload_modal.hint' defaultMessage='Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.' /></p>}
<label className='setting-text-label' htmlFor='upload-modal__description'><FormattedMessage id='upload_form.description' defaultMessage='Describe for the visually impaired' /></label> <label className='setting-text-label' htmlFor='upload-modal__description'>
{descriptionLabel}
</label>
<div className='setting-text__wrapper'> <div className='setting-text__wrapper'>
<Textarea <Textarea
@ -222,6 +275,7 @@ class FocalPointModal extends ImmutablePureComponent {
className='setting-text light' className='setting-text light'
value={detecting ? '…' : description} value={detecting ? '…' : description}
onChange={this.handleChange} onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
disabled={detecting} disabled={detecting}
autoFocus autoFocus
/> />
@ -242,8 +296,8 @@ class FocalPointModal extends ImmutablePureComponent {
<div className='focal-point-modal__content'> <div className='focal-point-modal__content'>
{focals && ( {focals && (
<div className={classNames('focal-point', { dragging })} ref={this.setRef} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart}> <div className={classNames('focal-point', { dragging })} ref={this.setRef} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart}>
{media.get('type') === 'image' && <img src={media.get('url')} width={width} height={height} alt='' />} {media.get('type') === 'image' && <ImageLoader src={media.get('url')} width={width} height={height} alt='' />}
{media.get('type') === 'gifv' && <video src={media.get('url')} width={width} height={height} loop muted autoPlay />} {media.get('type') === 'gifv' && <GIFV src={media.get('url')} width={width} height={height} />}
<div className='focal-point__preview'> <div className='focal-point__preview'>
<strong><FormattedMessage id='upload_modal.preview_label' defaultMessage='Preview ({ratio})' values={{ ratio: '16:9' }} /></strong> <strong><FormattedMessage id='upload_modal.preview_label' defaultMessage='Preview ({ratio})' values={{ ratio: '16:9' }} /></strong>

View File

@ -4,12 +4,10 @@ import { fetchFollowRequests } from 'mastodon/actions/accounts';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { NavLink, withRouter } from 'react-router-dom'; import { NavLink, withRouter } from 'react-router-dom';
import IconWithBadge from 'mastodon/components/icon_with_badge'; import IconWithBadge from 'mastodon/components/icon_with_badge';
import { me } from 'mastodon/initial_state';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
locked: state.getIn(['accounts', me, 'locked']),
count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size, count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
}); });
@ -19,22 +17,19 @@ class FollowRequestsNavLink extends React.Component {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
locked: PropTypes.bool,
count: PropTypes.number.isRequired, count: PropTypes.number.isRequired,
}; };
componentDidMount () { componentDidMount () {
const { dispatch, locked } = this.props; const { dispatch } = this.props;
if (locked) { dispatch(fetchFollowRequests());
dispatch(fetchFollowRequests());
}
} }
render () { render () {
const { locked, count } = this.props; const { count } = this.props;
if (!locked || count === 0) { if (count === 0) {
return null; return null;
} }

View File

@ -62,7 +62,7 @@ class LinkFooter extends React.PureComponent {
<FormattedMessage <FormattedMessage
id='getting_started.open_source_notice' id='getting_started.open_source_notice'
defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.' defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }} values={{ github: <span><a href={source_url} rel='noopener noreferrer' target='_blank'>{repository}</a> (v{version})</span> }}
/> />
</p> </p>
</div> </div>

View File

@ -3,13 +3,13 @@ import ReactSwipeableViews from 'react-swipeable-views';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Video from 'mastodon/features/video'; import Video from 'mastodon/features/video';
import ExtendedVideoPlayer from 'mastodon/components/extended_video_player';
import classNames from 'classnames'; import classNames from 'classnames';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import IconButton from 'mastodon/components/icon_button'; import IconButton from 'mastodon/components/icon_button';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import ImageLoader from './image_loader'; import ImageLoader from './image_loader';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import GIFV from 'mastodon/components/gifv';
const messages = defineMessages({ const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' }, close: { id: 'lightbox.close', defaultMessage: 'Close' },
@ -169,10 +169,8 @@ class MediaModal extends ImmutablePureComponent {
); );
} else if (image.get('type') === 'gifv') { } else if (image.get('type') === 'gifv') {
return ( return (
<ExtendedVideoPlayer <GIFV
src={image.get('url')} src={image.get('url')}
muted
controls={false}
width={width} width={width}
height={height} height={height}
key={image.get('preview_url')} key={image.get('preview_url')}

View File

@ -17,6 +17,7 @@ const NavigationPanel = () => (
<NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink> <NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink> <NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink> <NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink> <NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
{profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>} {profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>}

View File

@ -6,9 +6,9 @@ import { createSelector } from 'reselect';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { me } from '../../../initial_state'; import { me } from '../../../initial_state';
const makeGetStatusIds = () => createSelector([ const makeGetStatusIds = (pending = false) => createSelector([
(state, { type }) => state.getIn(['settings', type], ImmutableMap()), (state, { type }) => state.getIn(['settings', type], ImmutableMap()),
(state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()), (state, { type }) => state.getIn(['timelines', type, pending ? 'pendingItems' : 'items'], ImmutableList()),
(state) => state.get('statuses'), (state) => state.get('statuses'),
], (columnSettings, statusIds, statuses) => { ], (columnSettings, statusIds, statuses) => {
return statusIds.filter(id => { return statusIds.filter(id => {
@ -31,13 +31,14 @@ const makeGetStatusIds = () => createSelector([
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
const getStatusIds = makeGetStatusIds(); const getStatusIds = makeGetStatusIds();
const getPendingStatusIds = makeGetStatusIds(true);
const mapStateToProps = (state, { timelineId }) => ({ const mapStateToProps = (state, { timelineId }) => ({
statusIds: getStatusIds(state, { type: timelineId }), statusIds: getStatusIds(state, { type: timelineId }),
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true), isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false), isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
hasMore: state.getIn(['timelines', timelineId, 'hasMore']), hasMore: state.getIn(['timelines', timelineId, 'hasMore']),
numPending: state.getIn(['timelines', timelineId, 'pendingItems'], ImmutableList()).size, numPending: getPendingStatusIds(state, { type: timelineId }).size,
}); });
return mapStateToProps; return mapStateToProps;

View File

@ -41,6 +41,7 @@ import {
FollowRequests, FollowRequests,
GenericNotFound, GenericNotFound,
FavouritedStatuses, FavouritedStatuses,
BookmarkedStatuses,
ListTimeline, ListTimeline,
Blocks, Blocks,
DomainBlocks, DomainBlocks,
@ -99,6 +100,7 @@ const keyMap = {
goToRequests: 'g r', goToRequests: 'g r',
toggleHidden: 'x', toggleHidden: 'x',
toggleSensitive: 'h', toggleSensitive: 'h',
openMedia: 'e',
}; };
class SwitchingColumnsArea extends React.PureComponent { class SwitchingColumnsArea extends React.PureComponent {
@ -164,7 +166,9 @@ class SwitchingColumnsArea extends React.PureComponent {
} }
setRef = c => { setRef = c => {
this.node = c.getWrappedInstance(); if (c) {
this.node = c.getWrappedInstance();
}
} }
render () { render () {
@ -188,6 +192,7 @@ class SwitchingColumnsArea extends React.PureComponent {
<WrappedRoute path='/notifications' component={Notifications} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> <WrappedRoute path='/notifications' component={Notifications} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/search' component={Search} content={children} /> <WrappedRoute path='/search' component={Search} content={children} />

View File

@ -90,6 +90,10 @@ export function FavouritedStatuses () {
return import(/* webpackChunkName: "features/favourited_statuses" */'../../favourited_statuses'); return import(/* webpackChunkName: "features/favourited_statuses" */'../../favourited_statuses');
} }
export function BookmarkedStatuses () {
return import(/* webpackChunkName: "features/bookmarked_statuses" */'../../bookmarked_statuses');
}
export function Blocks () { export function Blocks () {
return import(/* webpackChunkName: "features/blocks" */'../../blocks'); return import(/* webpackChunkName: "features/blocks" */'../../blocks');
} }

View File

@ -19,6 +19,7 @@ const messages = defineMessages({
close: { id: 'video.close', defaultMessage: 'Close video' }, close: { id: 'video.close', defaultMessage: 'Close video' },
fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' }, fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' }, exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
download: { id: 'video.download', defaultMessage: 'Download file' },
}); });
export const formatTime = secondsNum => { export const formatTime = secondsNum => {
@ -466,10 +467,11 @@ class Video extends React.PureComponent {
<div className='video-player__buttons-bar'> <div className='video-player__buttons-bar'>
<div className='video-player__buttons left'> <div className='video-player__buttons left'>
<button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}> <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
&nbsp;
<div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} /> <div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
<span <span
className={classNames('video-player__volume__handle')} className={classNames('video-player__volume__handle')}
@ -493,7 +495,13 @@ class Video extends React.PureComponent {
{(!onCloseVideo && !editable) && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>} {(!onCloseVideo && !editable) && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
{(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>} {(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
{onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>} {onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
<button type='button' aria-label={intl.formatMessage(messages.download)}>
<a className='video-player__download__icon' href={this.props.src} download>
<Icon id={'download'} fixedWidth />
</a>
</button>
<button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button> <button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -24,5 +24,6 @@ export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items'); export const usePendingItems = getMeta('use_pending_items');
export const showTrends = getMeta('trends'); export const showTrends = getMeta('trends');
export const title = getMeta('title'); export const title = getMeta('title');
export const cropImages = getMeta('crop_images');
export default initialState; export default initialState;

View File

@ -0,0 +1,16 @@
// On KaiOS, we may not be able to use a mouse cursor or navigate using Tab-based focus, so we install
// special left/right focus navigation keyboard listeners, at least on public pages (i.e. so folks
// can at least log in using KaiOS devices).
function importArrowKeyNavigation() {
return import(/* webpackChunkName: "arrow-key-navigation" */ 'arrow-key-navigation');
}
export default function loadKeyboardExtensions() {
if (/KAIOS/.test(navigator.userAgent)) {
return importArrowKeyNavigation().then(arrowKeyNav => {
arrowKeyNav.register();
});
}
return Promise.resolve();
}

View File

@ -10,7 +10,7 @@
"account.edit_profile": "تعديل الملف التعريفي", "account.edit_profile": "تعديل الملف التعريفي",
"account.endorse": "أوصِ به على صفحتك", "account.endorse": "أوصِ به على صفحتك",
"account.follow": "تابِع", "account.follow": "تابِع",
"account.followers": تابعون", "account.followers": ُتابِعون",
"account.followers.empty": "لا أحد يتبع هذا الحساب بعد.", "account.followers.empty": "لا أحد يتبع هذا الحساب بعد.",
"account.follows": "يتبع", "account.follows": "يتبع",
"account.follows.empty": "هذا الحساب لا يتبع أحدًا بعد.", "account.follows.empty": "هذا الحساب لا يتبع أحدًا بعد.",
@ -27,7 +27,7 @@
"account.muted": "مكتوم", "account.muted": "مكتوم",
"account.never_active": "أبدا", "account.never_active": "أبدا",
"account.posts": "تبويقات", "account.posts": "تبويقات",
"account.posts_with_replies": "التبويقات و الردود", "account.posts_with_replies": "التبويقات والردود",
"account.report": "ابلِغ عن @{name}", "account.report": "ابلِغ عن @{name}",
"account.requested": "في انتظار الموافقة. اضْغَطْ/ي لإلغاء طلب المتابعة", "account.requested": "في انتظار الموافقة. اضْغَطْ/ي لإلغاء طلب المتابعة",
"account.share": "شارك ملف تعريف @{name}", "account.share": "شارك ملف تعريف @{name}",
@ -53,7 +53,7 @@
"column.blocks": "الحسابات المحجوبة", "column.blocks": "الحسابات المحجوبة",
"column.community": "الخيط العام المحلي", "column.community": "الخيط العام المحلي",
"column.direct": "الرسائل المباشرة", "column.direct": "الرسائل المباشرة",
"column.directory": "استعرض الملفات التعريفية", "column.directory": "استعراض الملفات التعريفية",
"column.domain_blocks": "النطاقات المخفية", "column.domain_blocks": "النطاقات المخفية",
"column.favourites": "المفضلة", "column.favourites": "المفضلة",
"column.follow_requests": "طلبات المتابعة", "column.follow_requests": "طلبات المتابعة",
@ -106,7 +106,7 @@
"confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.",
"confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟", "confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟",
"confirmations.redraft.confirm": "إزالة و إعادة الصياغة", "confirmations.redraft.confirm": "إزالة و إعادة الصياغة",
"confirmations.redraft.message": "هل أنت متأكد من أنك تريد حذف هذا المنشور و إعادة صياغته ؟ سوف تفقد جميع الإعجابات و الترقيات أما الردود المتصلة به فستُصبِح يتيمة.", "confirmations.redraft.message": "هل أنت متأكد من أنك تريد حذف هذا المنشور و إعادة صياغته؟ سوف تفقد جميع الإعجابات و الترقيات أما الردود المتصلة به فستُصبِح يتيمة.",
"confirmations.reply.confirm": "رد", "confirmations.reply.confirm": "رد",
"confirmations.reply.message": "الرد في الحين سوف يُعيد كتابة الرسالة التي أنت بصدد كتابتها. متأكد من أنك تريد المواصلة؟", "confirmations.reply.message": "الرد في الحين سوف يُعيد كتابة الرسالة التي أنت بصدد كتابتها. متأكد من أنك تريد المواصلة؟",
"confirmations.unfollow.confirm": "إلغاء المتابعة", "confirmations.unfollow.confirm": "إلغاء المتابعة",
@ -115,7 +115,7 @@
"conversation.mark_as_read": "اعتبرها كمقروءة", "conversation.mark_as_read": "اعتبرها كمقروءة",
"conversation.open": "اعرض المحادثة", "conversation.open": "اعرض المحادثة",
"conversation.with": "بـ {names}", "conversation.with": "بـ {names}",
"directory.federated": "From known fediverse", "directory.federated": "مِن الفديفرس المعروف",
"directory.local": "مِن {domain} فقط", "directory.local": "مِن {domain} فقط",
"directory.new_arrivals": "الوافدون الجُدد", "directory.new_arrivals": "الوافدون الجُدد",
"directory.recently_active": "نشط مؤخرا", "directory.recently_active": "نشط مؤخرا",
@ -155,7 +155,7 @@
"error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.", "error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.",
"error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.", "error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard",
"errors.unexpected_crash.report_issue": "Report issue", "errors.unexpected_crash.report_issue": "الإبلاغ عن خلل",
"follow_request.authorize": "ترخيص", "follow_request.authorize": "ترخيص",
"follow_request.reject": "رفض", "follow_request.reject": "رفض",
"getting_started.developers": "المُطوِّرون", "getting_started.developers": "المُطوِّرون",
@ -176,9 +176,8 @@
"hashtag.column_settings.tag_mode.none": "لا شيء مِن هذه", "hashtag.column_settings.tag_mode.none": "لا شيء مِن هذه",
"hashtag.column_settings.tag_toggle": "إدراج الوسوم الإضافية لهذا العمود", "hashtag.column_settings.tag_toggle": "إدراج الوسوم الإضافية لهذا العمود",
"home.column_settings.basic": "الأساسية", "home.column_settings.basic": "الأساسية",
"home.column_settings.show_reblogs": "عرض الترقيات", "home.column_settings.show_reblogs": "اعرض الترقيات",
"home.column_settings.show_replies": "اعرض الردود", "home.column_settings.show_replies": "اعرض الردود",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# يوم} other {# أيام}}", "intervals.full.days": "{number, plural, one {# يوم} other {# أيام}}",
"intervals.full.hours": "{number, plural, one {# ساعة} other {# ساعات}}", "intervals.full.hours": "{number, plural, one {# ساعة} other {# ساعات}}",
"intervals.full.minutes": "{number, plural, one {# دقيقة} other {# دقائق}}", "intervals.full.minutes": "{number, plural, one {# دقيقة} other {# دقائق}}",
@ -244,7 +243,7 @@
"lists.new.title_placeholder": "عنوان القائمة الجديدة", "lists.new.title_placeholder": "عنوان القائمة الجديدة",
"lists.search": "إبحث في قائمة الحسابات التي تُتابِعها", "lists.search": "إبحث في قائمة الحسابات التي تُتابِعها",
"lists.subheading": "قوائمك", "lists.subheading": "قوائمك",
"load_pending": "{count, plural, one {# new item} other {# new items}}", "load_pending": "{count, plural, one {# عنصر جديد} other {# عناصر جديدة}}",
"loading_indicator.label": "تحميل...", "loading_indicator.label": "تحميل...",
"media_gallery.toggle_visible": "عرض / إخفاء", "media_gallery.toggle_visible": "عرض / إخفاء",
"missing_indicator.label": "غير موجود", "missing_indicator.label": "غير موجود",
@ -272,7 +271,6 @@
"navigation_bar.preferences": "التفضيلات", "navigation_bar.preferences": "التفضيلات",
"navigation_bar.public_timeline": "الخيط العام الموحد", "navigation_bar.public_timeline": "الخيط العام الموحد",
"navigation_bar.security": "الأمان", "navigation_bar.security": "الأمان",
"notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
"notification.favourite": "أُعجِب {name} بمنشورك", "notification.favourite": "أُعجِب {name} بمنشورك",
"notification.follow": "{name} يتابعك", "notification.follow": "{name} يتابعك",
"notification.mention": "{name} ذكرك", "notification.mention": "{name} ذكرك",
@ -284,7 +282,7 @@
"notifications.column_settings.favourite": "المُفَضَّلة:", "notifications.column_settings.favourite": "المُفَضَّلة:",
"notifications.column_settings.filter_bar.advanced": "اعرض كافة الفئات", "notifications.column_settings.filter_bar.advanced": "اعرض كافة الفئات",
"notifications.column_settings.filter_bar.category": "شريط الفلترة السريعة", "notifications.column_settings.filter_bar.category": "شريط الفلترة السريعة",
"notifications.column_settings.filter_bar.show": "اعرض", "notifications.column_settings.filter_bar.show": "اظهِره",
"notifications.column_settings.follow": "متابعُون جُدُد:", "notifications.column_settings.follow": "متابعُون جُدُد:",
"notifications.column_settings.mention": "الإشارات:", "notifications.column_settings.mention": "الإشارات:",
"notifications.column_settings.poll": "نتائج استطلاع الرأي:", "notifications.column_settings.poll": "نتائج استطلاع الرأي:",
@ -301,10 +299,10 @@
"notifications.group": "{count} إشعارات", "notifications.group": "{count} إشعارات",
"poll.closed": "انتهى", "poll.closed": "انتهى",
"poll.refresh": "تحديث", "poll.refresh": "تحديث",
"poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_people": "{count, plural, one {# شخص} two {# شخصين} few {# أشخاص} many {# أشخاص} other {# أشخاص}}",
"poll.total_votes": "{count, plural, one {# صوت} other {# أصوات}}", "poll.total_votes": "{count, plural, one {# صوت} other {# أصوات}}",
"poll.vote": "صَوّت", "poll.vote": "صَوّت",
"poll.voted": "You voted for this answer", "poll.voted": "لقد صوّتت على هذه الإجابة",
"poll_button.add_poll": "إضافة استطلاع للرأي", "poll_button.add_poll": "إضافة استطلاع للرأي",
"poll_button.remove_poll": "إزالة استطلاع الرأي", "poll_button.remove_poll": "إزالة استطلاع الرأي",
"privacy.change": "اضبط خصوصية المنشور", "privacy.change": "اضبط خصوصية المنشور",
@ -342,7 +340,7 @@
"search_results.hashtags": "الوُسوم", "search_results.hashtags": "الوُسوم",
"search_results.statuses": "التبويقات", "search_results.statuses": "التبويقات",
"search_results.statuses_fts_disabled": "البحث في التبويقات عن طريق المحتوى ليس مفعل في خادم ماستدون هذا.", "search_results.statuses_fts_disabled": "البحث في التبويقات عن طريق المحتوى ليس مفعل في خادم ماستدون هذا.",
"search_results.total": "{count, number} {count, plural, one {result} و {results}}", "search_results.total": "{count, number} {count, plural, zero {} one {نتيجة} two {نتيجتين} few {نتائج} many {نتائج} other {نتائج}}",
"status.admin_account": "افتح الواجهة الإدارية لـ @{name}", "status.admin_account": "افتح الواجهة الإدارية لـ @{name}",
"status.admin_status": "افتح هذا المنشور على واجهة الإشراف", "status.admin_status": "افتح هذا المنشور على واجهة الإشراف",
"status.block": "احجب @{name}", "status.block": "احجب @{name}",
@ -391,11 +389,11 @@
"tabs_bar.notifications": "الإخطارات", "tabs_bar.notifications": "الإخطارات",
"tabs_bar.search": "البحث", "tabs_bar.search": "البحث",
"time_remaining.days": "{number, plural, one {# يوم} other {# أيام}} متبقية", "time_remaining.days": "{number, plural, one {# يوم} other {# أيام}} متبقية",
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", "time_remaining.hours": "{number, plural, one {# ساعة} other {# ساعات}} متبقية",
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", "time_remaining.minutes": "{number, plural, one {# دقيقة} other {# دقائق}} متبقية",
"time_remaining.moments": "لحظات متبقية", "time_remaining.moments": "لحظات متبقية",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "time_remaining.seconds": "{number, plural, one {# ثانية} other {# ثوانٍ}} متبقية",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} آخرون {people}} يتحدثون", "trends.count_by_accounts": "{count} {rawCount, plural, zero {} one {شخص واحد} two {شخصين} few {أشخاص} many {أشخاص} other {أشخاص}} تتحدّث",
"trends.trending_now": "المتداولة الآن", "trends.trending_now": "المتداولة الآن",
"ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.", "ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.",
"upload_area.title": "اسحب ثم أفلت للرفع", "upload_area.title": "اسحب ثم أفلت للرفع",
@ -408,7 +406,7 @@
"upload_modal.analyzing_picture": "جارٍ فحص الصورة…", "upload_modal.analyzing_picture": "جارٍ فحص الصورة…",
"upload_modal.apply": "طبّق", "upload_modal.apply": "طبّق",
"upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
"upload_modal.detect_text": "Detect text from picture", "upload_modal.detect_text": "اكتشف النص مِن الصورة",
"upload_modal.edit_media": "تعديل الوسائط", "upload_modal.edit_media": "تعديل الوسائط",
"upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.", "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
"upload_modal.preview_label": "معاينة ({ratio})", "upload_modal.preview_label": "معاينة ({ratio})",

View File

@ -178,7 +178,6 @@
"home.column_settings.basic": "Basic", "home.column_settings.basic": "Basic",
"home.column_settings.show_reblogs": "Amosar toots compartíos", "home.column_settings.show_reblogs": "Amosar toots compartíos",
"home.column_settings.show_replies": "Amosar rempuestes", "home.column_settings.show_replies": "Amosar rempuestes",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
@ -272,7 +271,6 @@
"navigation_bar.preferences": "Preferencies", "navigation_bar.preferences": "Preferencies",
"navigation_bar.public_timeline": "Llinia temporal federada", "navigation_bar.public_timeline": "Llinia temporal federada",
"navigation_bar.security": "Seguranza", "navigation_bar.security": "Seguranza",
"notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
"notification.favourite": "{name} favourited your status", "notification.favourite": "{name} favourited your status",
"notification.follow": "{name} siguióte", "notification.follow": "{name} siguióte",
"notification.mention": "{name} mentóte", "notification.mention": "{name} mentóte",

View File

@ -4,19 +4,19 @@
"account.block": "Блокирай", "account.block": "Блокирай",
"account.block_domain": "скрий всичко от (домейн)", "account.block_domain": "скрий всичко от (домейн)",
"account.blocked": "Блокирани", "account.blocked": "Блокирани",
"account.cancel_follow_request": "Cancel follow request", "account.cancel_follow_request": "Откажи искането за следване",
"account.direct": "Direct Message @{name}", "account.direct": "Direct Message @{name}",
"account.domain_blocked": "Скрит домейн", "account.domain_blocked": "Скрит домейн",
"account.edit_profile": "Редактирай профила си", "account.edit_profile": "Редактирай профила си",
"account.endorse": "Feature on profile", "account.endorse": "Характеристика на профила",
"account.follow": "Последвай", "account.follow": "Последвай",
"account.followers": "Последователи", "account.followers": "Последователи",
"account.followers.empty": "No one follows this user yet.", "account.followers.empty": "Все още никой не следва този потребител.",
"account.follows": "Следвам", "account.follows": "Следвам",
"account.follows.empty": "This user doesn't follow anyone yet.", "account.follows.empty": "Този потребител все още не следва никого.",
"account.follows_you": "Твой последовател", "account.follows_you": "Твой последовател",
"account.hide_reblogs": "Hide boosts from @{name}", "account.hide_reblogs": "Hide boosts from @{name}",
"account.last_status": "Last active", "account.last_status": "Последно активен/а",
"account.link_verified_on": "Ownership of this link was checked on {date}", "account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Media", "account.media": "Media",
@ -178,7 +178,6 @@
"home.column_settings.basic": "Basic", "home.column_settings.basic": "Basic",
"home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies", "home.column_settings.show_replies": "Show replies",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
@ -272,7 +271,6 @@
"navigation_bar.preferences": "Предпочитания", "navigation_bar.preferences": "Предпочитания",
"navigation_bar.public_timeline": "Публичен канал", "navigation_bar.public_timeline": "Публичен канал",
"navigation_bar.security": "Security", "navigation_bar.security": "Security",
"notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
"notification.favourite": "{name} хареса твоята публикация", "notification.favourite": "{name} хареса твоята публикация",
"notification.follow": "{name} те последва", "notification.follow": "{name} те последва",
"notification.mention": "{name} те спомена", "notification.mention": "{name} те спомена",
@ -412,7 +410,7 @@
"upload_modal.edit_media": "Edit media", "upload_modal.edit_media": "Edit media",
"upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.", "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
"upload_modal.preview_label": "Preview ({ratio})", "upload_modal.preview_label": "Preview ({ratio})",
"upload_progress.label": "Uploading...", "upload_progress.label": "Uploading",
"video.close": "Close video", "video.close": "Close video",
"video.exit_fullscreen": "Exit full screen", "video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video", "video.expand": "Expand video",

View File

@ -1,31 +1,31 @@
{ {
"account.add_or_remove_from_list": "তালিকাতে আরো যুক্ত বা মুছে ফেলুন", "account.add_or_remove_from_list": "তালিকা হতে মুছুন অথবা যুক্ত করুন",
"account.badges.bot": "রোবট", "account.badges.bot": "বট",
"account.block": "@{name} কে বন্ধ করুন", "account.block": "@{name} কে ব্লক করুন",
"account.block_domain": "{domain} থেকে সব সরিয়ে ফেলুন", "account.block_domain": "{domain} থেকে সব লুকান",
"account.blocked": "বন্ধ করা হয়েছে", "account.blocked": "ব্লককৃত",
"account.cancel_follow_request": "Cancel follow request", "account.cancel_follow_request": "অসুসারণ অনুরোধ বাতিল করুন",
"account.direct": "@{name} এর কাছে সরকারি লেখা পাঠাতে", "account.direct": "@{name} কে সরাসরি বার্তা",
"account.domain_blocked": "ওয়েবসাইট সরিয়ে ফেলা হয়েছে", "account.domain_blocked": "ডোমেন লুকানো আছে",
"account.edit_profile": "নিজের পাতা সম্পাদনা করতে", "account.edit_profile": "প্রোফাইল সম্পাদন করুন",
"account.endorse": "আপনার নিজের পাতায় দেখাতে", "account.endorse": "নিজের পাতায় দেখান",
"account.follow": "অনুসরণ করুন", "account.follow": "অনুসরণ করুন",
"account.followers": "অনুসরণকারক", "account.followers": "অনুসরণকারক",
"account.followers.empty": "এই ব্যবহারকারীকে কেও এখনো অনুসরণ করে না।", "account.followers.empty": "এই ব্যবহারকারীকে কেও এখনো অনুসরণ করে না।",
"account.follows": "যাদেরকে অনুসরণ করেন", "account.follows": "যাদেরকে অনুসরণ করেন",
"account.follows.empty": "এই ব্যবহারকারী কাওকে এখনো অনুসরণ করেন না।", "account.follows.empty": "এই ব্যবহারকারী কাওকে এখনো অনুসরণ করেন না।",
"account.follows_you": "আপনাকে অনুসরণ করে", "account.follows_you": "আপনাকে অনুসরণ করে",
"account.hide_reblogs": "@{name}র সমর্থনগুলি সরিয়ে ফেলুন", "account.hide_reblogs": "@{name}'র সমর্থনগুলি লুকিয়ে ফেলুন",
"account.last_status": "Last active", "account.last_status": "শেষ সক্রিয় ছিল",
"account.link_verified_on": "এই লিংকের মালিকানা চেক করা হয়েছে {date} তারিকে", "account.link_verified_on": "এই লিংকের মালিকানা চেক করা হয়েছে {date} তারিকে",
"account.locked_info": "এই নিবন্ধনের গোপনীয়তার ক্ষেত্র তালা দেওয়া আছে। নিবন্ধনকারী অনুসরণ করার অনুমতি যাদেরকে দেবেন, শুধু তারাই অনুসরণ করতে পারবেন।", "account.locked_info": "এই নিবন্ধনের গোপনীয়তার ক্ষেত্র তালা দেওয়া আছে। নিবন্ধনকারী অনুসরণ করার অনুমতি যাদেরকে দেবেন, শুধু তারাই অনুসরণ করতে পারবেন।",
"account.media": "ছবি বা ভিডিও", "account.media": "মিডিয়া",
"account.mention": "@{name} কে উল্লেখ করতে", "account.mention": "@{name} কে উল্লেখ করুন",
"account.moved_to": "{name} চলে গেছে এখানে:", "account.moved_to": "{name} কে এখানে সরানো হয়েছে:",
"account.mute": "@{name} সব কার্যক্রম আপনার সময়রেখা থেকে সরিয়ে ফেলতে", "account.mute": "@{name} কে নিঃশব্দ করুন",
"account.mute_notifications": "@{name}র প্রজ্ঞাপন আপনার কাছ থেকে সরিয়ে ফেলুন", "account.mute_notifications": "@{name}র প্রজ্ঞাপন আপনার কাছ থেকে সরিয়ে ফেলুন",
"account.muted": "সরানো আছে", "account.muted": "সরানো আছে",
"account.never_active": "Never", "account.never_active": "কখনও নয়",
"account.posts": "টুট", "account.posts": "টুট",
"account.posts_with_replies": "টুট এবং মতামত", "account.posts_with_replies": "টুট এবং মতামত",
"account.report": "@{name} কে রিপোর্ট করতে", "account.report": "@{name} কে রিপোর্ট করতে",
@ -33,16 +33,16 @@
"account.share": "@{name}র পাতা অন্যদের দেখান", "account.share": "@{name}র পাতা অন্যদের দেখান",
"account.show_reblogs": "@{name}র সমর্থনগুলো দেখুন", "account.show_reblogs": "@{name}র সমর্থনগুলো দেখুন",
"account.unblock": "@{name}র কার্যকলাপ আবার দেখুন", "account.unblock": "@{name}র কার্যকলাপ আবার দেখুন",
"account.unblock_domain": "{domain}থেকে আবার দেখুন", "account.unblock_domain": "{domain} থেকে আবার দেখুন",
"account.unendorse": "আপনার নিজের পাতায় এটা না দেখাতে", "account.unendorse": "আপনার নিজের পাতায় এটা না দেখাতে",
"account.unfollow": "অনুসরণ না করতে", "account.unfollow": "অনুসরণ না করতে",
"account.unmute": "@{name}র কার্যকলাপ আবার দেখুন", "account.unmute": "@{name}র কার্যকলাপ আবার দেখুন",
"account.unmute_notifications": "@{name}র প্রজ্ঞাপন দেওয়ার অনুমতি দিন", "account.unmute_notifications": "@{name}র প্রজ্ঞাপন দেওয়ার অনুমতি দিন",
"alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", "alert.rate_limited.message": "{retry_time, time, medium} -এর পরে আবার প্রচেষ্টা করুন।",
"alert.rate_limited.title": "Rate limited", "alert.rate_limited.title": "হার সীমিত",
"alert.unexpected.message": "অপ্রত্যাশিত একটি সমস্যা হয়েছে।", "alert.unexpected.message": "অপ্রত্যাশিত একটি সমস্যা হয়েছে।",
"alert.unexpected.title": "ওহো!", "alert.unexpected.title": "ওহো!",
"autosuggest_hashtag.per_week": "{count} per week", "autosuggest_hashtag.per_week": "প্রতি সপ্তাহে {count}",
"boost_modal.combo": "পরেরবার আপনি {combo} চাপ দিলে এটার শেষে চলে যেতে পারবেন", "boost_modal.combo": "পরেরবার আপনি {combo} চাপ দিলে এটার শেষে চলে যেতে পারবেন",
"bundle_column_error.body": "এই অংশটি দেখতে যেয়ে কোনো সমস্যা হয়েছে।", "bundle_column_error.body": "এই অংশটি দেখতে যেয়ে কোনো সমস্যা হয়েছে।",
"bundle_column_error.retry": "আবার চেষ্টা করুন", "bundle_column_error.retry": "আবার চেষ্টা করুন",
@ -50,11 +50,11 @@
"bundle_modal_error.close": "বন্ধ করুন", "bundle_modal_error.close": "বন্ধ করুন",
"bundle_modal_error.message": "এই অংশটি দেখাতে যেয়ে কোনো সমস্যা হয়েছে।", "bundle_modal_error.message": "এই অংশটি দেখাতে যেয়ে কোনো সমস্যা হয়েছে।",
"bundle_modal_error.retry": "আবার চেষ্টা করুন", "bundle_modal_error.retry": "আবার চেষ্টা করুন",
"column.blocks": "যাদের বন্ধ করে রাখা হয়েছে", "column.blocks": "যাদের ব্লক করে রাখা হয়েছে",
"column.community": "স্থানীয় সময়সারি", "column.community": "স্থানীয় সময়সারি",
"column.direct": "সরাসরি লেখা", "column.direct": "সরাসরি লেখা",
"column.directory": "Browse profiles", "column.directory": "প্রোফাইল ব্রাউজ করুন",
"column.domain_blocks": "সরিয়ে ফেলা ওয়েবসাইট", "column.domain_blocks": "লুকোনো ডোমেনগুলি",
"column.favourites": "পছন্দের গুলো", "column.favourites": "পছন্দের গুলো",
"column.follow_requests": "অনুসরণের অনুমতি চেয়েছে যারা", "column.follow_requests": "অনুসরণের অনুমতি চেয়েছে যারা",
"column.home": "বাড়ি", "column.home": "বাড়ি",
@ -87,23 +87,23 @@
"compose_form.sensitive.hide": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করতে", "compose_form.sensitive.hide": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করতে",
"compose_form.sensitive.marked": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করা হয়েছে", "compose_form.sensitive.marked": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করা হয়েছে",
"compose_form.sensitive.unmarked": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করা হয়নি", "compose_form.sensitive.unmarked": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করা হয়নি",
"compose_form.spoiler.marked": "লেখাটি সাবধানতার পেছনে লুকানো আছে", "compose_form.spoiler.marked": "সতর্কতার পিছনে লেখানটি লুকানো আছে",
"compose_form.spoiler.unmarked": "লেখাটি লুকানো নেই", "compose_form.spoiler.unmarked": "লেখাটি লুকানো নেই",
"compose_form.spoiler_placeholder": "আপনার লেখা দেখার সাবধানবাণী লিখুন", "compose_form.spoiler_placeholder": "আপনার লেখা দেখার সাবধানবাণী লিখুন",
"confirmation_modal.cancel": "বাতিল করুন", "confirmation_modal.cancel": "বাতিল করুন",
"confirmations.block.block_and_report": "বন্ধ করুন এবং রিপোর্ট করুন", "confirmations.block.block_and_report": "ব্লক করুন এবং রিপোর্ট করুন",
"confirmations.block.confirm": "বন্ধ করুন", "confirmations.block.confirm": "ব্লক করুন",
"confirmations.block.message": "আপনি কি নিশ্চিত {name} কে বন্ধ করতে চান ?", "confirmations.block.message": "আপনি কি নিশ্চিত {name} কে ব্লক করতে চান?",
"confirmations.delete.confirm": "মুছে ফেলুন", "confirmations.delete.confirm": "মুছে ফেলুন",
"confirmations.delete.message": "আপনি কি নিশ্চিত যে এই লেখাটি মুছে ফেলতে চান ?", "confirmations.delete.message": "আপনি কি নিশ্চিত যে এই লেখাটি মুছে ফেলতে চান ?",
"confirmations.delete_list.confirm": "মুছে ফেলুন", "confirmations.delete_list.confirm": "মুছে ফেলুন",
"confirmations.delete_list.message": "আপনি কি নিশ্চিত যে আপনি এই তালিকাটি স্থায়িভাবে মুছে ফেলতে চান ?", "confirmations.delete_list.message": "আপনি কি নিশ্চিত যে আপনি এই তালিকাটি স্থায়িভাবে মুছে ফেলতে চান ?",
"confirmations.domain_block.confirm": "এই ওয়েবসাইট থেকে সব সরান", "confirmations.domain_block.confirm": "এই ডোমেন থেকে সব লুকান",
"confirmations.domain_block.message": "আপনি কি সত্যি সত্যি নিশ্চিত যে {domain} ওয়েবসাইট থেকে সব সরাতে চান ? সাধারণত কিছু লক্ষ্যবস্তু বন্ধ এবং সরানোযা যথেষ্ট। নিশ্চিত করলে ওই ওয়েবসাইট থেকে কোনোকিছু কোনখানে দেখবেন না। যারা আপনাকে অনুসরণ করে ওই ওয়েবসাইট থেকে তাদেরকেও মুছে ফেলা হবে।", "confirmations.domain_block.message": "আপনি কি সত্যিই সত্যই নিশ্চিত যে আপনি পুরো {domain}'টি ব্লক করতে চান? বেশিরভাগ ক্ষেত্রে কয়েকটি লক্ষ্যযুক্ত ব্লক বা নীরবতা যথেষ্ট এবং পছন্দসই। আপনি কোনও পাবলিক টাইমলাইন বা আপনার বিজ্ঞপ্তিগুলিতে সেই ডোমেন থেকে সামগ্রী দেখতে পাবেন না। সেই ডোমেন থেকে আপনার অনুসরণকারীদের সরানো হবে।",
"confirmations.logout.confirm": "Log out", "confirmations.logout.confirm": "প্রস্থান",
"confirmations.logout.message": "Are you sure you want to log out?", "confirmations.logout.message": "আপনি লগ আউট করতে চান?",
"confirmations.mute.confirm": "সরিয়ে ফেলুন", "confirmations.mute.confirm": "সরিয়ে ফেলুন",
"confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.explanation": "এটি তাদের কাছ থেকে পোস্ট এবং তাদেরকে মেনশন করা পোস্টগুলি হাইড করবে, তবুও তাদেরকে এটি আপনার পোস্ট গুলো দেখতে দিবে ও তারা আপনাকে অনুসরন করতে পারবে।.",
"confirmations.mute.message": "আপনি কি নিশ্চিত {name} সরিয়ে ফেলতে চান ?", "confirmations.mute.message": "আপনি কি নিশ্চিত {name} সরিয়ে ফেলতে চান ?",
"confirmations.redraft.confirm": "মুছে ফেলুন এবং আবার সম্পাদন করুন", "confirmations.redraft.confirm": "মুছে ফেলুন এবং আবার সম্পাদন করুন",
"confirmations.redraft.message": "আপনি কি নিশ্চিত এটি মুছে ফেলে এবং আবার সম্পাদন করতে চান ? এটাতে যা পছন্দিত, সমর্থন বা মতামত আছে সেগুলো নতুন লেখার সাথে যুক্ত থাকবে না।", "confirmations.redraft.message": "আপনি কি নিশ্চিত এটি মুছে ফেলে এবং আবার সম্পাদন করতে চান ? এটাতে যা পছন্দিত, সমর্থন বা মতামত আছে সেগুলো নতুন লেখার সাথে যুক্ত থাকবে না।",
@ -111,14 +111,14 @@
"confirmations.reply.message": "এখন মতামত লিখতে গেলে আপনার এখন যেটা লিখছেন সেটা মুছে যাবে। আপনি নি নিশ্চিত এটা করতে চান ?", "confirmations.reply.message": "এখন মতামত লিখতে গেলে আপনার এখন যেটা লিখছেন সেটা মুছে যাবে। আপনি নি নিশ্চিত এটা করতে চান ?",
"confirmations.unfollow.confirm": "অনুসরণ করা বাতিল করতে", "confirmations.unfollow.confirm": "অনুসরণ করা বাতিল করতে",
"confirmations.unfollow.message": "আপনি কি নিশ্চিত {name} কে আর অনুসরণ করতে চান না ?", "confirmations.unfollow.message": "আপনি কি নিশ্চিত {name} কে আর অনুসরণ করতে চান না ?",
"conversation.delete": "Delete conversation", "conversation.delete": "কথোপকথন মুছে ফেলুন",
"conversation.mark_as_read": "Mark as read", "conversation.mark_as_read": "পঠিত হিসেবে চিহ্নিত করুন",
"conversation.open": "View conversation", "conversation.open": "কথপোকথন দেখান",
"conversation.with": "With {names}", "conversation.with": "{names} এর সঙ্গে",
"directory.federated": "From known fediverse", "directory.federated": "পরিচিত ফেডিভারসের থেকে",
"directory.local": "From {domain} only", "directory.local": "শুধু {domain} থেকে",
"directory.new_arrivals": "New arrivals", "directory.new_arrivals": "নতুন আগত",
"directory.recently_active": "Recently active", "directory.recently_active": "সম্প্রতি সক্রিয়",
"embed.instructions": "এই লেখাটি আপনার ওয়েবসাইটে যুক্ত করতে নিচের কোডটি বেবহার করুন।", "embed.instructions": "এই লেখাটি আপনার ওয়েবসাইটে যুক্ত করতে নিচের কোডটি বেবহার করুন।",
"embed.preview": "সেটা দেখতে এরকম হবে:", "embed.preview": "সেটা দেখতে এরকম হবে:",
"emoji_button.activity": "কার্যকলাপ", "emoji_button.activity": "কার্যকলাপ",
@ -137,25 +137,25 @@
"emoji_button.travel": "ভ্রমণ এবং স্থান", "emoji_button.travel": "ভ্রমণ এবং স্থান",
"empty_column.account_timeline": "এখানে কোনো টুট নেই!", "empty_column.account_timeline": "এখানে কোনো টুট নেই!",
"empty_column.account_unavailable": "নিজস্ব পাতা নেই", "empty_column.account_unavailable": "নিজস্ব পাতা নেই",
"empty_column.blocks": "আপনি কোনো ব্যবহারকারীদের বন্ধ করেন নি।", "empty_column.blocks": "আপনি কোনো ব্যবহারকারীদের ব্লক করেন নি।",
"empty_column.community": "স্থানীয় সময়রেখাতে কিছু নেই। প্রকাশ্যভাবে কিছু লিখে লেখালেখির উদ্বোধন করে ফেলুন!", "empty_column.community": "স্থানীয় সময়রেখাতে কিছু নেই। প্রকাশ্যভাবে কিছু লিখে লেখালেখির উদ্বোধন করে ফেলুন!",
"empty_column.direct": "আপনার কাছে সরাসরি পাঠানো কোনো লেখা নেই। যদি কেও পাঠায়, সেটা এখানে দেখা যাবে।", "empty_column.direct": "আপনার কাছে সরাসরি পাঠানো কোনো লেখা নেই। যদি কেও পাঠায়, সেটা এখানে দেখা যাবে।",
"empty_column.domain_blocks": "এখনো কোনো সরানো ওয়েবসাইট নেই।", "empty_column.domain_blocks": "এখনও কোনও লুকানো ডোমেন নেই।",
"empty_column.favourited_statuses": "আপনার পছন্দের কোনো টুট এখনো নেই। আপনি কোনো লেখা পছন্দের হিসেবে চিহ্নিত করলে এখানে পাওয়া যাবে।", "empty_column.favourited_statuses": "আপনার পছন্দের কোনো টুট এখনো নেই। আপনি কোনো লেখা পছন্দের হিসেবে চিহ্নিত করলে এখানে পাওয়া যাবে।",
"empty_column.favourites": "কেও এখনো এটাকে পছন্দের টুট হিসেবে চিহ্নিত করেনি। যদি করে, তখন তাদের এখানে পাওয়া যাবে।", "empty_column.favourites": "কেও এখনো এটাকে পছন্দের টুট হিসেবে চিহ্নিত করেনি। যদি করে, তখন তাদের এখানে পাওয়া যাবে।",
"empty_column.follow_requests": "আপনার এখনো কোনো অনুসরণের আবেদন পাঠানো নেই। যদি পাঠায়, এখানে পাওয়া যাবে।", "empty_column.follow_requests": "আপনার এখনো কোনো অনুসরণের আবেদন পাঠানো নেই। যদি পাঠায়, এখানে পাওয়া যাবে।",
"empty_column.hashtag": "এই হেসটাগে এখনো কিছু নেই।", "empty_column.hashtag": "এই হেসটাগে এখনো কিছু নেই।",
"empty_column.home": "আপনার বাড়ির সময়রেখা এখনো খালি! {public}এ ঘুরে আসুন অথবা অনুসন্ধান বেবহার করে শুরু করতে পারেন এবং অন্য ব্যবহারকারীদের সাথে সাক্ষাৎ করতে পারেন।", "empty_column.home": "আপনার বাড়ির সময়রেখা এখনো খালি! {public} এ ঘুরে আসুন অথবা অনুসন্ধান বেবহার করে শুরু করতে পারেন এবং অন্য ব্যবহারকারীদের সাথে সাক্ষাৎ করতে পারেন।",
"empty_column.home.public_timeline": "প্রকাশ্য সময়রেখা", "empty_column.home.public_timeline": "প্রকাশ্য সময়রেখা",
"empty_column.list": "এই তালিকাতে এখনো কিছু নেই. যখন এই তালিকায় থাকা ব্যবহারকারী নতুন কিছু লিখবে, সেগুলো এখানে পাওয়া যাবে।", "empty_column.list": "এই তালিকাতে এখনো কিছু নেই. যখন এই তালিকায় থাকা ব্যবহারকারী নতুন কিছু লিখবে, সেগুলো এখানে পাওয়া যাবে।",
"empty_column.lists": "আপনার এখনো কোনো তালিকা তৈরী নেই। যদি বা যখন তৈরী করেন, সেগুলো এখানে পাওয়া যাবে।", "empty_column.lists": "আপনার এখনো কোনো তালিকা তৈরী নেই। যদি বা যখন তৈরী করেন, সেগুলো এখানে পাওয়া যাবে।",
"empty_column.mutes": "আপনি এখনো কোনো ব্যবহারকারীকে সরাননি।", "empty_column.mutes": "আপনি এখনো কোনো ব্যবহারকারীকে নিঃশব্দ করেননি।",
"empty_column.notifications": "আপনার এখনো কোনো প্রজ্ঞাপন নেই। কথোপকথন শুরু করতে, অন্যদের সাথে মেলামেশা করতে পারেন।", "empty_column.notifications": "আপনার এখনো কোনো প্রজ্ঞাপন নেই। কথোপকথন শুরু করতে, অন্যদের সাথে মেলামেশা করতে পারেন।",
"empty_column.public": "এখানে এখনো কিছু নেই! প্রকাশ্য ভাবে কিছু লিখুন বা অন্য সার্ভার থেকে কাওকে অনুসরণ করে এই জায়গা ভরে ফেলুন", "empty_column.public": "এখানে এখনো কিছু নেই! প্রকাশ্য ভাবে কিছু লিখুন বা অন্য সার্ভার থেকে কাওকে অনুসরণ করে এই জায়গা ভরে ফেলুন",
"error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.", "error.unexpected_crash.explanation": "আমাদের কোড বা ব্রাউজারের সামঞ্জস্য ইস্যুতে একটি বাগের কারণে এই পৃষ্ঠাটি সঠিকভাবে প্রদর্শিত করা যায় নি।",
"error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.", "error.unexpected_crash.next_steps": "পাতাটি রিফ্রেশ করে চেষ্টা করুন। তবুও যদি না হয়, তবে আপনি অন্য একটি ব্রাউজার অথবা আপনার ডিভাইসের জন্যে এপের মাধ্যমে মাস্টডন ব্যাবহার করতে পারবেন।.",
"errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", "errors.unexpected_crash.copy_stacktrace": "স্টেকট্রেস ক্লিপবোর্ডে কপি করুন",
"errors.unexpected_crash.report_issue": "Report issue", "errors.unexpected_crash.report_issue": "সমস্যার প্রতিবেদন করুন",
"follow_request.authorize": "অনুমতি দিন", "follow_request.authorize": "অনুমতি দিন",
"follow_request.reject": "প্রত্যাখ্যান করুন", "follow_request.reject": "প্রত্যাখ্যান করুন",
"getting_started.developers": "তৈরিকারকদের জন্য", "getting_started.developers": "তৈরিকারকদের জন্য",
@ -178,10 +178,9 @@
"home.column_settings.basic": "সাধারণ", "home.column_settings.basic": "সাধারণ",
"home.column_settings.show_reblogs": "সমর্থনগুলো দেখান", "home.column_settings.show_reblogs": "সমর্থনগুলো দেখান",
"home.column_settings.show_replies": "মতামত দেখান", "home.column_settings.show_replies": "মতামত দেখান",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# ঘটা} other {# ঘটা}}", "intervals.full.hours": "{number, plural, one {# ঘটা} other {# ঘটা}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", "intervals.full.minutes": "{number, plural, one {# মিনিট} other {# মিনিট}}",
"introduction.federation.action": "পরবর্তী", "introduction.federation.action": "পরবর্তী",
"introduction.federation.federated.headline": "যুক্তবিশ্ব", "introduction.federation.federated.headline": "যুক্তবিশ্ব",
"introduction.federation.federated.text": "অন্যান্য যুক্তবিশ্বের সার্ভারের লেখাগুলি যুক্তবিশ্বের সময়রেখাতে আসবে ।", "introduction.federation.federated.text": "অন্যান্য যুক্তবিশ্বের সার্ভারের লেখাগুলি যুক্তবিশ্বের সময়রেখাতে আসবে ।",
@ -200,7 +199,7 @@
"introduction.welcome.headline": "প্রথম ধাপ", "introduction.welcome.headline": "প্রথম ধাপ",
"introduction.welcome.text": "যুক্তবিশ্বে স্বাগতম! কিছুক্ষনের মধ্যেই আপনি আপনার লেখা বিভিন্ন সার্ভারে সম্প্রচার করতে পারবেন। কিন্তু মনে রাখবে যে এটা একটা বিশেষ সার্ভার, {domain} কারণ এখানে আপনার নিজেস্ব পাতা রাখা হচ্ছে।", "introduction.welcome.text": "যুক্তবিশ্বে স্বাগতম! কিছুক্ষনের মধ্যেই আপনি আপনার লেখা বিভিন্ন সার্ভারে সম্প্রচার করতে পারবেন। কিন্তু মনে রাখবে যে এটা একটা বিশেষ সার্ভার, {domain} কারণ এখানে আপনার নিজেস্ব পাতা রাখা হচ্ছে।",
"keyboard_shortcuts.back": "পেছনে যেতে", "keyboard_shortcuts.back": "পেছনে যেতে",
"keyboard_shortcuts.blocked": "বন্ধ করা ব্যবহারকারীদের তালিকা দেখতে", "keyboard_shortcuts.blocked": "ব্লক করা ব্যবহারকারীদের তালিকা খুলতে",
"keyboard_shortcuts.boost": "সমর্থন করতে", "keyboard_shortcuts.boost": "সমর্থন করতে",
"keyboard_shortcuts.column": "কোনো কলামএ কোনো লেখা ফোকাস করতে", "keyboard_shortcuts.column": "কোনো কলামএ কোনো লেখা ফোকাস করতে",
"keyboard_shortcuts.compose": "লেখা সম্পদনার জায়গায় ফোকাস করতে", "keyboard_shortcuts.compose": "লেখা সম্পদনার জায়গায় ফোকাস করতে",
@ -244,7 +243,7 @@
"lists.new.title_placeholder": "তালিকার নতুন শিরোনাম দিতে", "lists.new.title_placeholder": "তালিকার নতুন শিরোনাম দিতে",
"lists.search": "যাদের অনুসরণ করেন তাদের ভেতরে খুঁজুন", "lists.search": "যাদের অনুসরণ করেন তাদের ভেতরে খুঁজুন",
"lists.subheading": "আপনার তালিকা", "lists.subheading": "আপনার তালিকা",
"load_pending": "{count, plural, one {# new item} other {# new items}}", "load_pending": "{count, plural, one {# নতুন জিনিস} other {# নতুন জিনিস}}",
"loading_indicator.label": "আসছে...", "loading_indicator.label": "আসছে...",
"media_gallery.toggle_visible": "দৃশ্যতার অবস্থা বদলান", "media_gallery.toggle_visible": "দৃশ্যতার অবস্থা বদলান",
"missing_indicator.label": "খুঁজে পাওয়া যায়নি", "missing_indicator.label": "খুঁজে পাওয়া যায়নি",
@ -256,23 +255,22 @@
"navigation_bar.compose": "নতুন টুট লিখুন", "navigation_bar.compose": "নতুন টুট লিখুন",
"navigation_bar.direct": "সরাসরি লেখাগুলি", "navigation_bar.direct": "সরাসরি লেখাগুলি",
"navigation_bar.discover": "ঘুরে দেখুন", "navigation_bar.discover": "ঘুরে দেখুন",
"navigation_bar.domain_blocks": "বন্ধ করা ওয়েবসাইট", "navigation_bar.domain_blocks": "লুকানো ডোমেনগুলি",
"navigation_bar.edit_profile": "নিজের পাতা সম্পাদনা করতে", "navigation_bar.edit_profile": "নিজের পাতা সম্পাদনা করতে",
"navigation_bar.favourites": "পছন্দের", "navigation_bar.favourites": "পছন্দের",
"navigation_bar.filters": "বন্ধ করা শব্দ", "navigation_bar.filters": "বন্ধ করা শব্দ",
"navigation_bar.follow_requests": "অনুসরণের অনুরোধগুলি", "navigation_bar.follow_requests": "অনুসরণের অনুরোধগুলি",
"navigation_bar.follows_and_followers": "যাদেরকে অনুসরণ করেন এবং যারা তাকে অনুসরণ করে", "navigation_bar.follows_and_followers": "অনুসরণ এবং অনুসরণকারী",
"navigation_bar.info": "এই সার্ভার সম্পর্কে", "navigation_bar.info": "এই সার্ভার সম্পর্কে",
"navigation_bar.keyboard_shortcuts": "হটকীগুলি", "navigation_bar.keyboard_shortcuts": "হটকীগুলি",
"navigation_bar.lists": "তালিকাগুলো", "navigation_bar.lists": "তালিকাগুলো",
"navigation_bar.logout": "বাইরে যান", "navigation_bar.logout": "বাইরে যান",
"navigation_bar.mutes": "যেসব বেভহারকরীদের কার্যক্রম বন্ধ করা আছে", "navigation_bar.mutes": "যাদের কার্যক্রম দেখা বন্ধ আছে",
"navigation_bar.personal": "নিজস্ব", "navigation_bar.personal": "নিজস্ব",
"navigation_bar.pins": "পিন দেওয়া টুট", "navigation_bar.pins": "পিন দেওয়া টুট",
"navigation_bar.preferences": "পছন্দসমূহ", "navigation_bar.preferences": "পছন্দসমূহ",
"navigation_bar.public_timeline": "যুক্তবিশ্বের সময়রেখা", "navigation_bar.public_timeline": "যুক্তবিশ্বের সময়রেখা",
"navigation_bar.security": "নিরাপত্তা", "navigation_bar.security": "নিরাপত্তা",
"notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
"notification.favourite": "{name} আপনার কার্যক্রম পছন্দ করেছেন", "notification.favourite": "{name} আপনার কার্যক্রম পছন্দ করেছেন",
"notification.follow": "{name} আপনাকে অনুসরণ করেছেন", "notification.follow": "{name} আপনাকে অনুসরণ করেছেন",
"notification.mention": "{name} আপনাকে উল্লেখ করেছেন", "notification.mention": "{name} আপনাকে উল্লেখ করেছেন",
@ -301,10 +299,10 @@
"notifications.group": "{count} প্রজ্ঞাপন", "notifications.group": "{count} প্রজ্ঞাপন",
"poll.closed": "বন্ধ", "poll.closed": "বন্ধ",
"poll.refresh": "বদলেছে কিনা দেখতে", "poll.refresh": "বদলেছে কিনা দেখতে",
"poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_people": "{count, plural, one {# ব্যক্তি} other {# ব্যক্তি}}",
"poll.total_votes": "{count, plural, one {# ভোট} other {# ভোট}}", "poll.total_votes": "{count, plural, one {# ভোট} other {# ভোট}}",
"poll.vote": "ভোট", "poll.vote": "ভোট",
"poll.voted": "You voted for this answer", "poll.voted": "আপনি এই উত্তরের পক্ষে ভোট দিয়েছেন",
"poll_button.add_poll": "একটা নির্বাচন যোগ করতে", "poll_button.add_poll": "একটা নির্বাচন যোগ করতে",
"poll_button.remove_poll": "নির্বাচন বাদ দিতে", "poll_button.remove_poll": "নির্বাচন বাদ দিতে",
"privacy.change": "লেখার গোপনীয়তা অবস্থা ঠিক করতে", "privacy.change": "লেখার গোপনীয়তা অবস্থা ঠিক করতে",
@ -316,13 +314,13 @@
"privacy.public.short": "সর্বজনীন প্রকাশ্য", "privacy.public.short": "সর্বজনীন প্রকাশ্য",
"privacy.unlisted.long": "সর্বজনীন প্রকাশ্য সময়রেখাতে না দেখাতে", "privacy.unlisted.long": "সর্বজনীন প্রকাশ্য সময়রেখাতে না দেখাতে",
"privacy.unlisted.short": "প্রকাশ্য নয়", "privacy.unlisted.short": "প্রকাশ্য নয়",
"refresh": "Refresh", "refresh": "সতেজ করা",
"regeneration_indicator.label": "আসছে…", "regeneration_indicator.label": "আসছে…",
"regeneration_indicator.sublabel": "আপনার বাড়ির-সময়রেখা প্রস্তূত করা হচ্ছে!", "regeneration_indicator.sublabel": "আপনার বাড়ির-সময়রেখা প্রস্তূত করা হচ্ছে!",
"relative_time.days": "{number} দিন", "relative_time.days": "{number} দিন",
"relative_time.hours": "{number} ঘন্টা", "relative_time.hours": "{number} ঘন্টা",
"relative_time.just_now": "এখন", "relative_time.just_now": "এখন",
"relative_time.minutes": "{number}ম", "relative_time.minutes": "{number}মিঃ",
"relative_time.seconds": "{number} সেকেন্ড", "relative_time.seconds": "{number} সেকেন্ড",
"reply_indicator.cancel": "বাতিল করতে", "reply_indicator.cancel": "বাতিল করতে",
"report.forward": "এটা আরো পাঠান {target} তে", "report.forward": "এটা আরো পাঠান {target} তে",
@ -331,7 +329,7 @@
"report.placeholder": "অন্য কোনো মন্তব্য", "report.placeholder": "অন্য কোনো মন্তব্য",
"report.submit": "জমা দিন", "report.submit": "জমা দিন",
"report.target": "{target} রিপোর্ট করুন", "report.target": "{target} রিপোর্ট করুন",
"search.placeholder": "খুঁজতে", "search.placeholder": "অনুসন্ধান",
"search_popout.search_format": "বিস্তারিতভাবে খোঁজার পদ্ধতি", "search_popout.search_format": "বিস্তারিতভাবে খোঁজার পদ্ধতি",
"search_popout.tips.full_text": "সাধারণ লেখা দিয়ে খুঁজলে বের হবে সেরকম আপনার লেখা, পছন্দের লেখা, সমর্থন করা লেখা, আপনাকে উল্লেখকরা কোনো লেখা, যা খুঁজছেন সেরকম কোনো ব্যবহারকারীর নাম বা কোনো হ্যাশট্যাগগুলো।", "search_popout.tips.full_text": "সাধারণ লেখা দিয়ে খুঁজলে বের হবে সেরকম আপনার লেখা, পছন্দের লেখা, সমর্থন করা লেখা, আপনাকে উল্লেখকরা কোনো লেখা, যা খুঁজছেন সেরকম কোনো ব্যবহারকারীর নাম বা কোনো হ্যাশট্যাগগুলো।",
"search_popout.tips.hashtag": "হ্যাশট্যাগ", "search_popout.tips.hashtag": "হ্যাশট্যাগ",
@ -341,11 +339,11 @@
"search_results.accounts": "মানুষ", "search_results.accounts": "মানুষ",
"search_results.hashtags": "হ্যাশট্যাগগুলি", "search_results.hashtags": "হ্যাশট্যাগগুলি",
"search_results.statuses": "টুট", "search_results.statuses": "টুট",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.statuses_fts_disabled": "তাদের সামগ্রী দ্বারা টুটগুলি অনুসন্ধান এই মস্তোডন সার্ভারে সক্ষম নয়।",
"search_results.total": "{count, number} {count, plural, one {ফলাফল} other {ফলাফল}}", "search_results.total": "{count, number} {count, plural, one {ফলাফল} other {ফলাফল}}",
"status.admin_account": "@{name} র জন্য পরিচালনার ইন্টারফেসে ঢুকুন", "status.admin_account": "@{name} র জন্য পরিচালনার ইন্টারফেসে ঢুকুন",
"status.admin_status": "যায় লেখাটি পরিচালনার ইন্টারফেসে খুলুন", "status.admin_status": "যায় লেখাটি পরিচালনার ইন্টারফেসে খুলুন",
"status.block": "@{name}কে বন্ধ করুন", "status.block": "@{name} কে ব্লক করুন",
"status.cancel_reblog_private": "সমর্থন বাতিল করতে", "status.cancel_reblog_private": "সমর্থন বাতিল করতে",
"status.cannot_reblog": "এটিতে সমর্থন দেওয়া যাবেনা", "status.cannot_reblog": "এটিতে সমর্থন দেওয়া যাবেনা",
"status.copy": "লেখাটির লিংক কপি করতে", "status.copy": "লেখাটির লিংক কপি করতে",
@ -356,7 +354,7 @@
"status.favourite": "পছন্দের করতে", "status.favourite": "পছন্দের করতে",
"status.filtered": "ছাঁকনিদিত", "status.filtered": "ছাঁকনিদিত",
"status.load_more": "আরো দেখুন", "status.load_more": "আরো দেখুন",
"status.media_hidden": "ছবি বা ভিডিও পেছনে", "status.media_hidden": "মিডিয়া লুকানো আছে",
"status.mention": "@{name}কে উল্লেখ করতে", "status.mention": "@{name}কে উল্লেখ করতে",
"status.more": "আরো", "status.more": "আরো",
"status.mute": "@{name}র কার্যক্রম সরিয়ে ফেলতে", "status.mute": "@{name}র কার্যক্রম সরিয়ে ফেলতে",
@ -380,7 +378,7 @@
"status.show_more": "আরো দেখাতে", "status.show_more": "আরো দেখাতে",
"status.show_more_all": "সবগুলোতে আরো দেখতে", "status.show_more_all": "সবগুলোতে আরো দেখতে",
"status.show_thread": "আলোচনা দেখতে", "status.show_thread": "আলোচনা দেখতে",
"status.uncached_media_warning": "Not available", "status.uncached_media_warning": "পাওয়া যাচ্ছে না",
"status.unmute_conversation": "আলোচনার প্রজ্ঞাপন চালু করতে", "status.unmute_conversation": "আলোচনার প্রজ্ঞাপন চালু করতে",
"status.unpin": "নিজের পাতা থেকে পিন করে রাখাটির পিন খুলতে", "status.unpin": "নিজের পাতা থেকে পিন করে রাখাটির পিন খুলতে",
"suggestions.dismiss": "সাহায্যের পরামর্শগুলো সরাতে", "suggestions.dismiss": "সাহায্যের পরামর্শগুলো সরাতে",
@ -389,29 +387,29 @@
"tabs_bar.home": "বাড়ি", "tabs_bar.home": "বাড়ি",
"tabs_bar.local_timeline": "স্থানীয়", "tabs_bar.local_timeline": "স্থানীয়",
"tabs_bar.notifications": "প্রজ্ঞাপনগুলো", "tabs_bar.notifications": "প্রজ্ঞাপনগুলো",
"tabs_bar.search": "খুঁজতে", "tabs_bar.search": "অনুসন্ধান",
"time_remaining.days": "{number, plural, one {# day} other {# days}} বাকি আছে", "time_remaining.days": "{number, plural, one {# day} other {# days}} বাকি আছে",
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} বাকি আছে", "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} বাকি আছে",
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} বাকি আছে", "time_remaining.minutes": "{number, plural, one {# মিনিট} other {# মিনিট}} বাকি আছে",
"time_remaining.moments": "সময় বাকি আছে", "time_remaining.moments": "সময় বাকি আছে",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} বাকি আছে", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} বাকি আছে",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} কথা বলছে", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} কথা বলছে",
"trends.trending_now": "Trending now", "trends.trending_now": "বর্তমানে জনপ্রিয়",
"ui.beforeunload": "যে পর্যন্ত এটা লেখা হয়েছে, মাস্টাডন থেকে চলে গেলে এটা মুছে যাবে।", "ui.beforeunload": "যে পর্যন্ত এটা লেখা হয়েছে, মাস্টাডন থেকে চলে গেলে এটা মুছে যাবে।",
"upload_area.title": "টেনে এখানে ছেড়ে দিলে এখানে যুক্ত করা যাবে", "upload_area.title": "টেনে এখানে ছেড়ে দিলে এখানে যুক্ত করা যাবে",
"upload_button.label": "ছবি বা ভিডিও যুক্ত করতে (এসব ধরণের: JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_button.label": "ছবি বা ভিডিও যুক্ত করতে (এসব ধরণের: JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_error.limit": "যা যুক্ত করতে চাচ্ছেন সেটি বেশি বড়, এখানকার সর্বাধিকের মেমোরির উপরে চলে গেছে।", "upload_error.limit": "যা যুক্ত করতে চাচ্ছেন সেটি বেশি বড়, এখানকার সর্বাধিকের মেমোরির উপরে চলে গেছে।",
"upload_error.poll": "নির্বাচনক্ষেত্রে কোনো ফাইল যুক্ত করা যাবেনা।", "upload_error.poll": "নির্বাচনক্ষেত্রে কোনো ফাইল যুক্ত করা যাবেনা।",
"upload_form.description": "যারা দেখতে পায়না তাদের জন্য এটা বর্ণনা করতে", "upload_form.description": "যারা দেখতে পায়না তাদের জন্য এটা বর্ণনা করতে",
"upload_form.edit": "Edit", "upload_form.edit": "সম্পাদন",
"upload_form.undo": "মুছে ফেলতে", "upload_form.undo": "মুছে ফেলতে",
"upload_modal.analyzing_picture": "Analyzing picture…", "upload_modal.analyzing_picture": "চিত্র বিশ্লেষণ করা হচ্ছে…",
"upload_modal.apply": "Apply", "upload_modal.apply": "প্রয়োগ করুন",
"upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
"upload_modal.detect_text": "Detect text from picture", "upload_modal.detect_text": "ছবি থেকে পাঠ্য সনাক্ত করুন",
"upload_modal.edit_media": "Edit media", "upload_modal.edit_media": "মিডিয়া সম্পাদনা করুন",
"upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.", "upload_modal.hint": "একটি দৃশ্যমান পয়েন্ট নির্বাচন করুন ক্লিক অথবা টানার মাধ্যমে যেটি সবময় সব থাম্বনেলে দেখা যাবে।",
"upload_modal.preview_label": "Preview ({ratio})", "upload_modal.preview_label": "পূর্বরূপ({ratio})",
"upload_progress.label": "যুক্ত করতে পাঠানো হচ্ছে...", "upload_progress.label": "যুক্ত করতে পাঠানো হচ্ছে...",
"video.close": "ভিডিওটি বন্ধ করতে", "video.close": "ভিডিওটি বন্ধ করতে",
"video.exit_fullscreen": "পূর্ণ পর্দা থেকে বাইরে বের হতে", "video.exit_fullscreen": "পূর্ণ পর্দা থেকে বাইরে বের হতে",

View File

@ -178,7 +178,6 @@
"home.column_settings.basic": "Basic", "home.column_settings.basic": "Basic",
"home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies", "home.column_settings.show_replies": "Show replies",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
@ -272,7 +271,6 @@
"navigation_bar.preferences": "Preferences", "navigation_bar.preferences": "Preferences",
"navigation_bar.public_timeline": "Federated timeline", "navigation_bar.public_timeline": "Federated timeline",
"navigation_bar.security": "Security", "navigation_bar.security": "Security",
"notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
"notification.favourite": "{name} favourited your status", "notification.favourite": "{name} favourited your status",
"notification.follow": "{name} followed you", "notification.follow": "{name} followed you",
"notification.mention": "{name} mentioned you", "notification.mention": "{name} mentioned you",
@ -412,7 +410,7 @@
"upload_modal.edit_media": "Edit media", "upload_modal.edit_media": "Edit media",
"upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.", "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
"upload_modal.preview_label": "Preview ({ratio})", "upload_modal.preview_label": "Preview ({ratio})",
"upload_progress.label": "Uploading...", "upload_progress.label": "Uploading",
"video.close": "Close video", "video.close": "Close video",
"video.exit_fullscreen": "Exit full screen", "video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video", "video.expand": "Expand video",

View File

@ -178,7 +178,6 @@
"home.column_settings.basic": "Bàsic", "home.column_settings.basic": "Bàsic",
"home.column_settings.show_reblogs": "Mostrar impulsos", "home.column_settings.show_reblogs": "Mostrar impulsos",
"home.column_settings.show_replies": "Mostrar respostes", "home.column_settings.show_replies": "Mostrar respostes",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# dia} other {# dies}}", "intervals.full.days": "{number, plural, one {# dia} other {# dies}}",
"intervals.full.hours": "{number, plural, one {# hora} other {# hores}}", "intervals.full.hours": "{number, plural, one {# hora} other {# hores}}",
"intervals.full.minutes": "{number, plural, one {# minut} other {# minuts}}", "intervals.full.minutes": "{number, plural, one {# minut} other {# minuts}}",
@ -272,7 +271,6 @@
"navigation_bar.preferences": "Preferències", "navigation_bar.preferences": "Preferències",
"navigation_bar.public_timeline": "Línia de temps federada", "navigation_bar.public_timeline": "Línia de temps federada",
"navigation_bar.security": "Seguretat", "navigation_bar.security": "Seguretat",
"notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
"notification.favourite": "{name} ha afavorit el teu estat", "notification.favourite": "{name} ha afavorit el teu estat",
"notification.follow": "{name} et segueix", "notification.follow": "{name} et segueix",
"notification.mention": "{name} t'ha esmentat", "notification.mention": "{name} t'ha esmentat",
@ -395,7 +393,7 @@
"time_remaining.minutes": "{number, plural, one {# minut} other {# minuts}} restants", "time_remaining.minutes": "{number, plural, one {# minut} other {# minuts}} restants",
"time_remaining.moments": "Moments restants", "time_remaining.moments": "Moments restants",
"time_remaining.seconds": "{number, plural, one {# segon} other {# segons}} restants", "time_remaining.seconds": "{number, plural, one {# segon} other {# segons}} restants",
"trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {gent}} talking", "trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {persones}} parlant-hi",
"trends.trending_now": "Ara en tendència", "trends.trending_now": "Ara en tendència",
"ui.beforeunload": "El teu esborrany es perdrà si surts de Mastodon.", "ui.beforeunload": "El teu esborrany es perdrà si surts de Mastodon.",
"upload_area.title": "Arrossega i deixa anar per a carregar", "upload_area.title": "Arrossega i deixa anar per a carregar",

View File

@ -178,7 +178,6 @@
"home.column_settings.basic": "Bàsichi", "home.column_settings.basic": "Bàsichi",
"home.column_settings.show_reblogs": "Vede e spartere", "home.column_settings.show_reblogs": "Vede e spartere",
"home.column_settings.show_replies": "Vede e risposte", "home.column_settings.show_replies": "Vede e risposte",
"home.column_settings.update_live": "Attualizà in tempu reale",
"intervals.full.days": "{number, plural, one {# ghjornu} other {# ghjorni}}", "intervals.full.days": "{number, plural, one {# ghjornu} other {# ghjorni}}",
"intervals.full.hours": "{number, plural, one {# ora} other {# ore}}", "intervals.full.hours": "{number, plural, one {# ora} other {# ore}}",
"intervals.full.minutes": "{number, plural, one {# minuta} other {# minute}}", "intervals.full.minutes": "{number, plural, one {# minuta} other {# minute}}",
@ -272,7 +271,6 @@
"navigation_bar.preferences": "Preferenze", "navigation_bar.preferences": "Preferenze",
"navigation_bar.public_timeline": "Linea pubblica glubale", "navigation_bar.public_timeline": "Linea pubblica glubale",
"navigation_bar.security": "Sicurità", "navigation_bar.security": "Sicurità",
"notification.and_n_others": "è {count, plural, one {# altru} other {# altri}}",
"notification.favourite": "{name} hà aghjuntu u vostru statutu à i so favuriti", "notification.favourite": "{name} hà aghjuntu u vostru statutu à i so favuriti",
"notification.follow": "{name} v'hà seguitatu", "notification.follow": "{name} v'hà seguitatu",
"notification.mention": "{name} v'hà mintuvatu", "notification.mention": "{name} v'hà mintuvatu",

View File

@ -43,7 +43,7 @@
"alert.unexpected.message": "Objevila se neočekávaná chyba.", "alert.unexpected.message": "Objevila se neočekávaná chyba.",
"alert.unexpected.title": "Jejda!", "alert.unexpected.title": "Jejda!",
"autosuggest_hashtag.per_week": "{count} za týden", "autosuggest_hashtag.per_week": "{count} za týden",
"boost_modal.combo": "Příště můžete pro přeskočení kliknout na {combo}", "boost_modal.combo": "Příště můžete pro přeskočení stisknout {combo}",
"bundle_column_error.body": "Při načítání tohoto komponentu se něco pokazilo.", "bundle_column_error.body": "Při načítání tohoto komponentu se něco pokazilo.",
"bundle_column_error.retry": "Zkuste to znovu", "bundle_column_error.retry": "Zkuste to znovu",
"bundle_column_error.title": "Chyba sítě", "bundle_column_error.title": "Chyba sítě",
@ -164,7 +164,7 @@
"getting_started.heading": "Začínáme", "getting_started.heading": "Začínáme",
"getting_started.invite": "Pozvat lidi", "getting_started.invite": "Pozvat lidi",
"getting_started.open_source_notice": "Mastodon je otevřený software. Na GitHubu k němu můžete přispět nebo nahlásit chyby: {github}.", "getting_started.open_source_notice": "Mastodon je otevřený software. Na GitHubu k němu můžete přispět nebo nahlásit chyby: {github}.",
"getting_started.security": "Zabezpečení", "getting_started.security": "Nastavení účtu",
"getting_started.terms": "Podmínky používání", "getting_started.terms": "Podmínky používání",
"hashtag.column_header.tag_mode.all": "a {additional}", "hashtag.column_header.tag_mode.all": "a {additional}",
"hashtag.column_header.tag_mode.any": "nebo {additional}", "hashtag.column_header.tag_mode.any": "nebo {additional}",
@ -178,7 +178,6 @@
"home.column_settings.basic": "Základní", "home.column_settings.basic": "Základní",
"home.column_settings.show_reblogs": "Zobrazit boosty", "home.column_settings.show_reblogs": "Zobrazit boosty",
"home.column_settings.show_replies": "Zobrazit odpovědi", "home.column_settings.show_replies": "Zobrazit odpovědi",
"home.column_settings.update_live": "Aktualizovat v reálném čase",
"intervals.full.days": "{number, plural, one {# den} few {# dny} many {# dne} other {# dní}}", "intervals.full.days": "{number, plural, one {# den} few {# dny} many {# dne} other {# dní}}",
"intervals.full.hours": "{number, plural, one {# hodina} few {# hodiny} many {# hodiny} other {# hodin}}", "intervals.full.hours": "{number, plural, one {# hodina} few {# hodiny} many {# hodiny} other {# hodin}}",
"intervals.full.minutes": "{number, plural, one {# minuta} few {# minuty} many {# minuty} other {# minut}}", "intervals.full.minutes": "{number, plural, one {# minuta} few {# minuty} many {# minuty} other {# minut}}",
@ -272,7 +271,6 @@
"navigation_bar.preferences": "Předvolby", "navigation_bar.preferences": "Předvolby",
"navigation_bar.public_timeline": "Federovaná časová osa", "navigation_bar.public_timeline": "Federovaná časová osa",
"navigation_bar.security": "Zabezpečení", "navigation_bar.security": "Zabezpečení",
"notification.and_n_others": "a {count, plural, one {# další} few {# další} many {# dalších} other {# dalších}}",
"notification.favourite": "{name} si oblíbil/a váš toot", "notification.favourite": "{name} si oblíbil/a váš toot",
"notification.follow": "{name} vás začal/a sledovat", "notification.follow": "{name} vás začal/a sledovat",
"notification.mention": "{name} vás zmínil/a", "notification.mention": "{name} vás zmínil/a",

View File

@ -103,7 +103,7 @@
"confirmations.logout.confirm": "Allgofnodi", "confirmations.logout.confirm": "Allgofnodi",
"confirmations.logout.message": "Ydych chi'n siŵr eich bod am allgofnodi?", "confirmations.logout.message": "Ydych chi'n siŵr eich bod am allgofnodi?",
"confirmations.mute.confirm": "Tawelu", "confirmations.mute.confirm": "Tawelu",
"confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.explanation": "Bydd hyn yn cuddio pyst oddi wrthynt a physt sydd yn sôn amdanynt, ond bydd hyn dal yn gadael iddyn nhw gweld eich pyst a'ch dilyn.",
"confirmations.mute.message": "Ydych chi'n sicr eich bod am ddistewi {name}?", "confirmations.mute.message": "Ydych chi'n sicr eich bod am ddistewi {name}?",
"confirmations.redraft.confirm": "Dileu & ailddrafftio", "confirmations.redraft.confirm": "Dileu & ailddrafftio",
"confirmations.redraft.message": "Ydych chi'n siwr eich bod eisiau dileu y tŵt hwn a'i ailddrafftio? Bydd ffefrynnau a bwstiau'n cael ei colli, a bydd ymatebion i'r tŵt gwreiddiol yn cael eu hamddifadu.", "confirmations.redraft.message": "Ydych chi'n siwr eich bod eisiau dileu y tŵt hwn a'i ailddrafftio? Bydd ffefrynnau a bwstiau'n cael ei colli, a bydd ymatebion i'r tŵt gwreiddiol yn cael eu hamddifadu.",
@ -152,10 +152,10 @@
"empty_column.mutes": "Nid ydych wedi tawelu unrhyw ddefnyddwyr eto.", "empty_column.mutes": "Nid ydych wedi tawelu unrhyw ddefnyddwyr eto.",
"empty_column.notifications": "Nid oes gennych unrhyw hysbysiadau eto. Rhyngweithiwch ac eraill i ddechrau'r sgwrs.", "empty_column.notifications": "Nid oes gennych unrhyw hysbysiadau eto. Rhyngweithiwch ac eraill i ddechrau'r sgwrs.",
"empty_column.public": "Does dim byd yma! Ysgrifennwch rhywbeth yn gyhoeddus, neu dilynwch ddefnyddwyr o achosion eraill i'w lenwi", "empty_column.public": "Does dim byd yma! Ysgrifennwch rhywbeth yn gyhoeddus, neu dilynwch ddefnyddwyr o achosion eraill i'w lenwi",
"error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.", "error.unexpected_crash.explanation": "Oherwydd gwall yn ein cod neu oherwydd problem cysondeb porwr, nid oedd y dudalen hon gallu cael ei dangos yn gywir.",
"error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.", "error.unexpected_crash.next_steps": "Ceisiwch ail-lwytho y dudalen. Os nad yw hyn yn eich helpu, efallai gallech defnyddio Mastodon trwy borwr neu ap brodorol gwahanol.",
"errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", "errors.unexpected_crash.copy_stacktrace": "Copïo'r olrhain stac i'r clipfwrdd",
"errors.unexpected_crash.report_issue": "Report issue", "errors.unexpected_crash.report_issue": "Rhoi gwybod am broblem",
"follow_request.authorize": "Caniatau", "follow_request.authorize": "Caniatau",
"follow_request.reject": "Gwrthod", "follow_request.reject": "Gwrthod",
"getting_started.developers": "Datblygwyr", "getting_started.developers": "Datblygwyr",
@ -178,7 +178,6 @@
"home.column_settings.basic": "Syml", "home.column_settings.basic": "Syml",
"home.column_settings.show_reblogs": "Dangos bŵstiau", "home.column_settings.show_reblogs": "Dangos bŵstiau",
"home.column_settings.show_replies": "Dangos ymatebion", "home.column_settings.show_replies": "Dangos ymatebion",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# ddydd} other {# o ddyddiau}}", "intervals.full.days": "{number, plural, one {# ddydd} other {# o ddyddiau}}",
"intervals.full.hours": "{number, plural, one {# awr} other {# o oriau}}", "intervals.full.hours": "{number, plural, one {# awr} other {# o oriau}}",
"intervals.full.minutes": "{number, plural, one {# funud} other {# o funudau}}", "intervals.full.minutes": "{number, plural, one {# funud} other {# o funudau}}",
@ -272,7 +271,6 @@
"navigation_bar.preferences": "Dewisiadau", "navigation_bar.preferences": "Dewisiadau",
"navigation_bar.public_timeline": "Ffrwd y ffederasiwn", "navigation_bar.public_timeline": "Ffrwd y ffederasiwn",
"navigation_bar.security": "Diogelwch", "navigation_bar.security": "Diogelwch",
"notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
"notification.favourite": "hoffodd {name} eich tŵt", "notification.favourite": "hoffodd {name} eich tŵt",
"notification.follow": "dilynodd {name} chi", "notification.follow": "dilynodd {name} chi",
"notification.mention": "Soniodd {name} amdanoch chi", "notification.mention": "Soniodd {name} amdanoch chi",
@ -301,10 +299,10 @@
"notifications.group": "{count} o hysbysiadau", "notifications.group": "{count} o hysbysiadau",
"poll.closed": "Ar gau", "poll.closed": "Ar gau",
"poll.refresh": "Adnewyddu", "poll.refresh": "Adnewyddu",
"poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_people": "{count, plural, one {# berson} other {# o bobl}}",
"poll.total_votes": "{count, plural, one {# bleidlais} other {# o bleidleisiau}}", "poll.total_votes": "{count, plural, one {# bleidlais} other {# o bleidleisiau}}",
"poll.vote": "Pleidleisio", "poll.vote": "Pleidleisio",
"poll.voted": "You voted for this answer", "poll.voted": "Pleidleisioch chi am yr ateb hon",
"poll_button.add_poll": "Ychwanegu pleidlais", "poll_button.add_poll": "Ychwanegu pleidlais",
"poll_button.remove_poll": "Tynnu pleidlais", "poll_button.remove_poll": "Tynnu pleidlais",
"privacy.change": "Addasu preifatrwdd y tŵt", "privacy.change": "Addasu preifatrwdd y tŵt",
@ -316,7 +314,7 @@
"privacy.public.short": "Cyhoeddus", "privacy.public.short": "Cyhoeddus",
"privacy.unlisted.long": "Peidio a chyhoeddi i ffrydiau cyhoeddus", "privacy.unlisted.long": "Peidio a chyhoeddi i ffrydiau cyhoeddus",
"privacy.unlisted.short": "Heb ei restru", "privacy.unlisted.short": "Heb ei restru",
"refresh": "Refresh", "refresh": "Adnewyddu",
"regeneration_indicator.label": "Llwytho…", "regeneration_indicator.label": "Llwytho…",
"regeneration_indicator.sublabel": "Mae eich ffrwd cartref yn cael ei baratoi!", "regeneration_indicator.sublabel": "Mae eich ffrwd cartref yn cael ei baratoi!",
"relative_time.days": "{number}dydd", "relative_time.days": "{number}dydd",

View File

@ -39,7 +39,7 @@
"account.unmute": "Fjern dæmpningen af @{name}", "account.unmute": "Fjern dæmpningen af @{name}",
"account.unmute_notifications": "Fjern dæmpningen af notifikationer fra @{name}", "account.unmute_notifications": "Fjern dæmpningen af notifikationer fra @{name}",
"alert.rate_limited.message": "Prøv venligst igen efter {retry_time, time, medium}.", "alert.rate_limited.message": "Prøv venligst igen efter {retry_time, time, medium}.",
"alert.rate_limited.title": "Rate limited", "alert.rate_limited.title": "Gradsbegrænset",
"alert.unexpected.message": "Der opstod en uventet fejl.", "alert.unexpected.message": "Der opstod en uventet fejl.",
"alert.unexpected.title": "Ups!", "alert.unexpected.title": "Ups!",
"autosuggest_hashtag.per_week": "{count} per uge", "autosuggest_hashtag.per_week": "{count} per uge",
@ -103,7 +103,7 @@
"confirmations.logout.confirm": "Log ud", "confirmations.logout.confirm": "Log ud",
"confirmations.logout.message": "Er du sikker på du vil logge ud?", "confirmations.logout.message": "Er du sikker på du vil logge ud?",
"confirmations.mute.confirm": "Dæmp", "confirmations.mute.confirm": "Dæmp",
"confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.explanation": "Dette vil skjule indlæg fra dem, samt andre indlæg der omtaler dem, men de vil stadig være i stand til at se dine indlæg og følge dig.",
"confirmations.mute.message": "Er du sikker på, du vil dæmpe {name}?", "confirmations.mute.message": "Er du sikker på, du vil dæmpe {name}?",
"confirmations.redraft.confirm": "Slet & omskriv", "confirmations.redraft.confirm": "Slet & omskriv",
"confirmations.redraft.message": "Er du sikker på, du vil slette denne status og omskrive den? Favoritter og fremhævelser vil gå tabt og svar til det oprindelige opslag vil blive forældreløse.", "confirmations.redraft.message": "Er du sikker på, du vil slette denne status og omskrive den? Favoritter og fremhævelser vil gå tabt og svar til det oprindelige opslag vil blive forældreløse.",
@ -111,10 +111,10 @@
"confirmations.reply.message": "Hvis du svarer nu vil du overskrive den besked du er ved at skrive. Er du sikker på, du vil fortsætte?", "confirmations.reply.message": "Hvis du svarer nu vil du overskrive den besked du er ved at skrive. Er du sikker på, du vil fortsætte?",
"confirmations.unfollow.confirm": "Følg ikke længere", "confirmations.unfollow.confirm": "Følg ikke længere",
"confirmations.unfollow.message": "Er du sikker på, du ikke længere vil følge {name}?", "confirmations.unfollow.message": "Er du sikker på, du ikke længere vil følge {name}?",
"conversation.delete": "Delete conversation", "conversation.delete": "Slet samtale",
"conversation.mark_as_read": "Mark as read", "conversation.mark_as_read": "Marker som læst",
"conversation.open": "View conversation", "conversation.open": "Vis samtale",
"conversation.with": "With {names}", "conversation.with": "Med {names}",
"directory.federated": "Fra kendt fedivers", "directory.federated": "Fra kendt fedivers",
"directory.local": "Kun fra {domain}", "directory.local": "Kun fra {domain}",
"directory.new_arrivals": "Nye ankomster", "directory.new_arrivals": "Nye ankomster",
@ -152,10 +152,10 @@
"empty_column.mutes": "Du har endnu ikke dæmpet nogen som helst bruger.", "empty_column.mutes": "Du har endnu ikke dæmpet nogen som helst bruger.",
"empty_column.notifications": "Du har endnu ingen notifikationer. Tag ud og bland dig med folkemængden for at starte samtalen.", "empty_column.notifications": "Du har endnu ingen notifikationer. Tag ud og bland dig med folkemængden for at starte samtalen.",
"empty_column.public": "Der er ikke noget at se her! Skriv noget offentligt eller start ud med manuelt at følge brugere fra andre server for at udfylde tomrummet", "empty_column.public": "Der er ikke noget at se her! Skriv noget offentligt eller start ud med manuelt at følge brugere fra andre server for at udfylde tomrummet",
"error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.", "error.unexpected_crash.explanation": "På grund af en fejl i vores kode, eller en browser kompatibilitetsfejl, så kunne siden ikke vises korrekt.",
"error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.", "error.unexpected_crash.next_steps": "Prøv at genindlæs siden. Hvis dette ikke hjælper, så forsøg venligst, at tilgå Mastodon via en anden browser eller app.",
"errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", "errors.unexpected_crash.copy_stacktrace": "Kopiér stack trace til udklipsholderen",
"errors.unexpected_crash.report_issue": "Report issue", "errors.unexpected_crash.report_issue": "Rapportér problem",
"follow_request.authorize": "Godkend", "follow_request.authorize": "Godkend",
"follow_request.reject": "Afvis", "follow_request.reject": "Afvis",
"getting_started.developers": "Udviklere", "getting_started.developers": "Udviklere",
@ -178,7 +178,6 @@
"home.column_settings.basic": "Grundlæggende", "home.column_settings.basic": "Grundlæggende",
"home.column_settings.show_reblogs": "Vis fremhævelser", "home.column_settings.show_reblogs": "Vis fremhævelser",
"home.column_settings.show_replies": "Vis svar", "home.column_settings.show_replies": "Vis svar",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# dag} other {# dage}}", "intervals.full.days": "{number, plural, one {# dag} other {# dage}}",
"intervals.full.hours": "{number, plural, one {# time} other {# timer}}", "intervals.full.hours": "{number, plural, one {# time} other {# timer}}",
"intervals.full.minutes": "{number, plural, one {# minut} other {# minutter}}", "intervals.full.minutes": "{number, plural, one {# minut} other {# minutter}}",
@ -272,7 +271,6 @@
"navigation_bar.preferences": "Præferencer", "navigation_bar.preferences": "Præferencer",
"navigation_bar.public_timeline": "Fælles tidslinje", "navigation_bar.public_timeline": "Fælles tidslinje",
"navigation_bar.security": "Sikkerhed", "navigation_bar.security": "Sikkerhed",
"notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
"notification.favourite": "{name} favoriserede din status", "notification.favourite": "{name} favoriserede din status",
"notification.follow": "{name} fulgte dig", "notification.follow": "{name} fulgte dig",
"notification.mention": "{name} nævnte dig", "notification.mention": "{name} nævnte dig",
@ -301,10 +299,10 @@
"notifications.group": "{count} notifikationer", "notifications.group": "{count} notifikationer",
"poll.closed": "Lukket", "poll.closed": "Lukket",
"poll.refresh": "Opdatér", "poll.refresh": "Opdatér",
"poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_people": "{count, plural, one {# person} other {# personer}}",
"poll.total_votes": "{count, plural, one {# stemme} other {# stemmer}}", "poll.total_votes": "{count, plural, one {# stemme} other {# stemmer}}",
"poll.vote": "Stem", "poll.vote": "Stem",
"poll.voted": "You voted for this answer", "poll.voted": "Du stemte for denne valgmulighed",
"poll_button.add_poll": "Tilføj en afstemning", "poll_button.add_poll": "Tilføj en afstemning",
"poll_button.remove_poll": "Fjern afstemning", "poll_button.remove_poll": "Fjern afstemning",
"privacy.change": "Skift status visningsindstillinger", "privacy.change": "Skift status visningsindstillinger",
@ -316,7 +314,7 @@
"privacy.public.short": "Offentligt", "privacy.public.short": "Offentligt",
"privacy.unlisted.long": "Udgiv ikke på offentlige tidslinjer", "privacy.unlisted.long": "Udgiv ikke på offentlige tidslinjer",
"privacy.unlisted.short": "Ikke listet", "privacy.unlisted.short": "Ikke listet",
"refresh": "Refresh", "refresh": "Opdatér",
"regeneration_indicator.label": "Indlæser…", "regeneration_indicator.label": "Indlæser…",
"regeneration_indicator.sublabel": "Din startside er ved at blive forberedt!", "regeneration_indicator.sublabel": "Din startside er ved at blive forberedt!",
"relative_time.days": "{number}d", "relative_time.days": "{number}d",

View File

@ -10,7 +10,7 @@
"account.edit_profile": "Profil bearbeiten", "account.edit_profile": "Profil bearbeiten",
"account.endorse": "Auf Profil hervorheben", "account.endorse": "Auf Profil hervorheben",
"account.follow": "Folgen", "account.follow": "Folgen",
"account.followers": "Folger_innen", "account.followers": "Folgende",
"account.followers.empty": "Diesem Profil folgt noch niemand.", "account.followers.empty": "Diesem Profil folgt noch niemand.",
"account.follows": "Folgt", "account.follows": "Folgt",
"account.follows.empty": "Dieses Profil folgt noch niemandem.", "account.follows.empty": "Dieses Profil folgt noch niemandem.",
@ -99,7 +99,7 @@
"confirmations.delete_list.confirm": "Löschen", "confirmations.delete_list.confirm": "Löschen",
"confirmations.delete_list.message": "Bist du dir sicher, dass du diese Liste permanent löschen möchtest?", "confirmations.delete_list.message": "Bist du dir sicher, dass du diese Liste permanent löschen möchtest?",
"confirmations.domain_block.confirm": "Die ganze Domain verbergen", "confirmations.domain_block.confirm": "Die ganze Domain verbergen",
"confirmations.domain_block.message": "Bist du dir wirklich sicher, dass du die ganze Domain {domain} blockieren willst? In den meisten Fällen reichen ein paar gezielte Blockierungen oder Stummschaltungen aus. Nach der Blockierung wirst du nichts mehr von dieser Domain in öffentlichen Zeitleisten oder Benachrichtigungen sehen. Deine Folger_innen von dieser Domain werden auch entfernt.", "confirmations.domain_block.message": "Bist du dir wirklich sicher, dass du die ganze Domain {domain} blockieren willst? In den meisten Fällen reichen ein paar gezielte Blockierungen oder Stummschaltungen aus. Du wirst den Inhalt von dieser Domain nicht in irgendwelchen öffentlichen Timelines oder den Benachrichtigungen finden. Deine Folgenden von dieser Domain werden entfernt.",
"confirmations.logout.confirm": "Abmelden", "confirmations.logout.confirm": "Abmelden",
"confirmations.logout.message": "Bist du sicher, dass du dich abmelden möchtest?", "confirmations.logout.message": "Bist du sicher, dass du dich abmelden möchtest?",
"confirmations.mute.confirm": "Stummschalten", "confirmations.mute.confirm": "Stummschalten",
@ -178,7 +178,6 @@
"home.column_settings.basic": "Einfach", "home.column_settings.basic": "Einfach",
"home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen", "home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen",
"home.column_settings.show_replies": "Antworten anzeigen", "home.column_settings.show_replies": "Antworten anzeigen",
"home.column_settings.update_live": "In Echtzeit aktualisieren",
"intervals.full.days": "{number, plural, one {# Tag} other {# Tage}}", "intervals.full.days": "{number, plural, one {# Tag} other {# Tage}}",
"intervals.full.hours": "{number, plural, one {# Stunde} other {# Stunden}}", "intervals.full.hours": "{number, plural, one {# Stunde} other {# Stunden}}",
"intervals.full.minutes": "{number, plural, one {# Minute} other {# Minuten}}", "intervals.full.minutes": "{number, plural, one {# Minute} other {# Minuten}}",
@ -191,7 +190,7 @@
"introduction.federation.local.text": "Öffentliche Beiträge von Leuten auf demselben Server wie du erscheinen in der lokalen Zeitleiste.", "introduction.federation.local.text": "Öffentliche Beiträge von Leuten auf demselben Server wie du erscheinen in der lokalen Zeitleiste.",
"introduction.interactions.action": "Tutorial beenden!", "introduction.interactions.action": "Tutorial beenden!",
"introduction.interactions.favourite.headline": "Favorisieren", "introduction.interactions.favourite.headline": "Favorisieren",
"introduction.interactions.favourite.text": "Du kannst Beitrage für später speichern und ihre Autor_innen wissen lassen, dass sie dir gefallen haben, indem du sie favorisierst.", "introduction.interactions.favourite.text": "Du kannst Beitrage für später speichern und ihre Autoren wissen lassen, dass sie dir gefallen haben, indem du sie favorisierst.",
"introduction.interactions.reblog.headline": "Teilen", "introduction.interactions.reblog.headline": "Teilen",
"introduction.interactions.reblog.text": "Du kannst Beiträge anderer mit deinen Followern teilen, indem du sie teilst.", "introduction.interactions.reblog.text": "Du kannst Beiträge anderer mit deinen Followern teilen, indem du sie teilst.",
"introduction.interactions.reply.headline": "Antworten", "introduction.interactions.reply.headline": "Antworten",
@ -261,7 +260,7 @@
"navigation_bar.favourites": "Favoriten", "navigation_bar.favourites": "Favoriten",
"navigation_bar.filters": "Stummgeschaltene Wörter", "navigation_bar.filters": "Stummgeschaltene Wörter",
"navigation_bar.follow_requests": "Folgeanfragen", "navigation_bar.follow_requests": "Folgeanfragen",
"navigation_bar.follows_and_followers": "Folger_innen und Gefolgte", "navigation_bar.follows_and_followers": "Folgende und Gefolgte",
"navigation_bar.info": "Über diesen Server", "navigation_bar.info": "Über diesen Server",
"navigation_bar.keyboard_shortcuts": "Tastenkombinationen", "navigation_bar.keyboard_shortcuts": "Tastenkombinationen",
"navigation_bar.lists": "Listen", "navigation_bar.lists": "Listen",
@ -272,7 +271,6 @@
"navigation_bar.preferences": "Einstellungen", "navigation_bar.preferences": "Einstellungen",
"navigation_bar.public_timeline": "Föderierte Zeitleiste", "navigation_bar.public_timeline": "Föderierte Zeitleiste",
"navigation_bar.security": "Sicherheit", "navigation_bar.security": "Sicherheit",
"notification.and_n_others": "und {count, plural, one {# andere Person} other {# andere Personen}}",
"notification.favourite": "{name} hat deinen Beitrag favorisiert", "notification.favourite": "{name} hat deinen Beitrag favorisiert",
"notification.follow": "{name} folgt dir", "notification.follow": "{name} folgt dir",
"notification.mention": "{name} hat dich erwähnt", "notification.mention": "{name} hat dich erwähnt",
@ -285,7 +283,7 @@
"notifications.column_settings.filter_bar.advanced": "Zeige alle Kategorien an", "notifications.column_settings.filter_bar.advanced": "Zeige alle Kategorien an",
"notifications.column_settings.filter_bar.category": "Schnellfilterleiste", "notifications.column_settings.filter_bar.category": "Schnellfilterleiste",
"notifications.column_settings.filter_bar.show": "Anzeigen", "notifications.column_settings.filter_bar.show": "Anzeigen",
"notifications.column_settings.follow": "Neue Folger_innen:", "notifications.column_settings.follow": "Neue Folgende:",
"notifications.column_settings.mention": "Erwähnungen:", "notifications.column_settings.mention": "Erwähnungen:",
"notifications.column_settings.poll": "Ergebnisse von Umfragen:", "notifications.column_settings.poll": "Ergebnisse von Umfragen:",
"notifications.column_settings.push": "Push-Benachrichtigungen", "notifications.column_settings.push": "Push-Benachrichtigungen",
@ -295,7 +293,7 @@
"notifications.filter.all": "Alle", "notifications.filter.all": "Alle",
"notifications.filter.boosts": "Geteilte Beiträge", "notifications.filter.boosts": "Geteilte Beiträge",
"notifications.filter.favourites": "Favorisierungen", "notifications.filter.favourites": "Favorisierungen",
"notifications.filter.follows": "Folger_innen", "notifications.filter.follows": "Folgt",
"notifications.filter.mentions": "Erwähnungen", "notifications.filter.mentions": "Erwähnungen",
"notifications.filter.polls": "Ergebnisse der Umfrage", "notifications.filter.polls": "Ergebnisse der Umfrage",
"notifications.group": "{count} Benachrichtigungen", "notifications.group": "{count} Benachrichtigungen",
@ -310,8 +308,8 @@
"privacy.change": "Sichtbarkeit des Beitrags anpassen", "privacy.change": "Sichtbarkeit des Beitrags anpassen",
"privacy.direct.long": "Wird an erwähnte Profile gesendet", "privacy.direct.long": "Wird an erwähnte Profile gesendet",
"privacy.direct.short": "Direktnachricht", "privacy.direct.short": "Direktnachricht",
"privacy.private.long": "Wird nur für deine Folger_innen sichtbar sein", "privacy.private.long": "Wird nur für deine Folgende sichtbar sein",
"privacy.private.short": "Nur für Folger_innen", "privacy.private.short": "Nur für Folgende",
"privacy.public.long": "Wird in öffentlichen Zeitleisten erscheinen", "privacy.public.long": "Wird in öffentlichen Zeitleisten erscheinen",
"privacy.public.short": "Öffentlich", "privacy.public.short": "Öffentlich",
"privacy.unlisted.long": "Wird in öffentlichen Zeitleisten nicht gezeigt", "privacy.unlisted.long": "Wird in öffentlichen Zeitleisten nicht gezeigt",
@ -407,7 +405,7 @@
"upload_form.undo": "Löschen", "upload_form.undo": "Löschen",
"upload_modal.analyzing_picture": "Analysiere Bild…", "upload_modal.analyzing_picture": "Analysiere Bild…",
"upload_modal.apply": "Übernehmen", "upload_modal.apply": "Übernehmen",
"upload_modal.description_placeholder": "Franz jagt im komplett verwahrlosten Taxi quer durch Bayern", "upload_modal.description_placeholder": "Die heiße Zypernsonne quälte Max und Victoria ja böse auf dem Weg bis zur Küste.",
"upload_modal.detect_text": "Text aus Bild erkennen", "upload_modal.detect_text": "Text aus Bild erkennen",
"upload_modal.edit_media": "Medien bearbeiten", "upload_modal.edit_media": "Medien bearbeiten",
"upload_modal.hint": "Klicke oder ziehe den Kreis auf die Vorschau, um den Brennpunkt auszuwählen, der immer auf allen Vorschaubilder angezeigt wird.", "upload_modal.hint": "Klicke oder ziehe den Kreis auf die Vorschau, um den Brennpunkt auszuwählen, der immer auf allen Vorschaubilder angezeigt wird.",

View File

@ -278,6 +278,19 @@
], ],
"path": "app/javascript/mastodon/components/poll.json" "path": "app/javascript/mastodon/components/poll.json"
}, },
{
"descriptors": [
{
"defaultMessage": "Loading…",
"id": "regeneration_indicator.label"
},
{
"defaultMessage": "Your home feed is being prepared!",
"id": "regeneration_indicator.sublabel"
}
],
"path": "app/javascript/mastodon/components/regeneration_indicator.json"
},
{ {
"descriptors": [ "descriptors": [
{ {
@ -385,6 +398,14 @@
"defaultMessage": "Favourite", "defaultMessage": "Favourite",
"id": "status.favourite" "id": "status.favourite"
}, },
{
"defaultMessage": "Bookmark",
"id": "status.bookmark"
},
{
"defaultMessage": "Remove bookmark",
"id": "status.remove_bookmark"
},
{ {
"defaultMessage": "Expand this status", "defaultMessage": "Expand this status",
"id": "status.open" "id": "status.open"
@ -424,6 +445,22 @@
{ {
"defaultMessage": "Copy link to status", "defaultMessage": "Copy link to status",
"id": "status.copy" "id": "status.copy"
},
{
"defaultMessage": "Hide everything from {domain}",
"id": "account.block_domain"
},
{
"defaultMessage": "Unhide {domain}",
"id": "account.unblock_domain"
},
{
"defaultMessage": "Unmute @{name}",
"id": "account.unmute"
},
{
"defaultMessage": "Unblock @{name}",
"id": "account.unblock"
} }
], ],
"path": "app/javascript/mastodon/components/status_action_bar.json" "path": "app/javascript/mastodon/components/status_action_bar.json"
@ -445,19 +482,6 @@
], ],
"path": "app/javascript/mastodon/components/status_content.json" "path": "app/javascript/mastodon/components/status_content.json"
}, },
{
"descriptors": [
{
"defaultMessage": "Loading…",
"id": "regeneration_indicator.label"
},
{
"defaultMessage": "Your home feed is being prepared!",
"id": "regeneration_indicator.sublabel"
}
],
"path": "app/javascript/mastodon/components/status_list.json"
},
{ {
"descriptors": [ "descriptors": [
{ {
@ -530,6 +554,14 @@
{ {
"defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"id": "confirmations.reply.message" "id": "confirmations.reply.message"
},
{
"defaultMessage": "Hide entire domain",
"id": "confirmations.domain_block.confirm"
},
{
"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. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
"id": "confirmations.domain_block.message"
} }
], ],
"path": "app/javascript/mastodon/containers/status_container.json" "path": "app/javascript/mastodon/containers/status_container.json"
@ -776,6 +808,10 @@
{ {
"defaultMessage": "Unmute sound", "defaultMessage": "Unmute sound",
"id": "video.unmute" "id": "video.unmute"
},
{
"defaultMessage": "Download file",
"id": "video.download"
} }
], ],
"path": "app/javascript/mastodon/features/audio/index.json" "path": "app/javascript/mastodon/features/audio/index.json"
@ -793,6 +829,19 @@
], ],
"path": "app/javascript/mastodon/features/blocks/index.json" "path": "app/javascript/mastodon/features/blocks/index.json"
}, },
{
"descriptors": [
{
"defaultMessage": "Bookmarks",
"id": "column.bookmarks"
},
{
"defaultMessage": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.",
"id": "empty_column.bookmarked_statuses"
}
],
"path": "app/javascript/mastodon/features/bookmarked_statuses/index.json"
},
{ {
"descriptors": [ "descriptors": [
{ {
@ -1135,15 +1184,6 @@
], ],
"path": "app/javascript/mastodon/features/compose/components/upload_form.json" "path": "app/javascript/mastodon/features/compose/components/upload_form.json"
}, },
{
"descriptors": [
{
"defaultMessage": "Uploading...",
"id": "upload_progress.label"
}
],
"path": "app/javascript/mastodon/features/compose/components/upload_progress.json"
},
{ {
"descriptors": [ "descriptors": [
{ {
@ -1435,6 +1475,10 @@
}, },
{ {
"descriptors": [ "descriptors": [
{
"defaultMessage": "Refresh",
"id": "refresh"
},
{ {
"defaultMessage": "No one has favourited this toot yet. When someone does, they will show up here.", "defaultMessage": "No one has favourited this toot yet. When someone does, they will show up here.",
"id": "empty_column.favourites" "id": "empty_column.favourites"
@ -1483,10 +1527,6 @@
}, },
{ {
"descriptors": [ "descriptors": [
{
"defaultMessage": "Refresh",
"id": "refresh"
},
{ {
"defaultMessage": "Profile unavailable", "defaultMessage": "Profile unavailable",
"id": "empty_column.account_unavailable" "id": "empty_column.account_unavailable"
@ -1533,6 +1573,10 @@
"defaultMessage": "Direct messages", "defaultMessage": "Direct messages",
"id": "navigation_bar.direct" "id": "navigation_bar.direct"
}, },
{
"defaultMessage": "Bookmarks",
"id": "navigation_bar.bookmarks"
},
{ {
"defaultMessage": "Preferences", "defaultMessage": "Preferences",
"id": "navigation_bar.preferences" "id": "navigation_bar.preferences"
@ -1640,6 +1684,10 @@
}, },
{ {
"descriptors": [ "descriptors": [
{
"defaultMessage": "Basic",
"id": "home.column_settings.basic"
},
{ {
"defaultMessage": "Show boosts", "defaultMessage": "Show boosts",
"id": "home.column_settings.show_reblogs" "id": "home.column_settings.show_reblogs"
@ -1779,6 +1827,10 @@
"defaultMessage": "to open status", "defaultMessage": "to open status",
"id": "keyboard_shortcuts.enter" "id": "keyboard_shortcuts.enter"
}, },
{
"defaultMessage": "to open media",
"id": "keyboard_shortcuts.open_media"
},
{ {
"defaultMessage": "to show/hide text behind CW", "defaultMessage": "to show/hide text behind CW",
"id": "keyboard_shortcuts.toggle_hidden" "id": "keyboard_shortcuts.toggle_hidden"
@ -2021,14 +2073,6 @@
"defaultMessage": "Push notifications", "defaultMessage": "Push notifications",
"id": "notifications.column_settings.push" "id": "notifications.column_settings.push"
}, },
{
"defaultMessage": "Basic",
"id": "home.column_settings.basic"
},
{
"defaultMessage": "Update in real-time",
"id": "home.column_settings.update_live"
},
{ {
"defaultMessage": "Quick filter bar", "defaultMessage": "Quick filter bar",
"id": "notifications.column_settings.filter_bar.category" "id": "notifications.column_settings.filter_bar.category"
@ -2037,6 +2081,10 @@
"defaultMessage": "New followers:", "defaultMessage": "New followers:",
"id": "notifications.column_settings.follow" "id": "notifications.column_settings.follow"
}, },
{
"defaultMessage": "New follow requests:",
"id": "notifications.column_settings.follow_request"
},
{ {
"defaultMessage": "Favourites:", "defaultMessage": "Favourites:",
"id": "notifications.column_settings.favourite" "id": "notifications.column_settings.favourite"
@ -2088,24 +2136,41 @@
{ {
"descriptors": [ "descriptors": [
{ {
"defaultMessage": "and {count, plural, one {# other} other {# others}}", "defaultMessage": "Authorize",
"id": "notification.and_n_others" "id": "follow_request.authorize"
},
{
"defaultMessage": "Reject",
"id": "follow_request.reject"
}
],
"path": "app/javascript/mastodon/features/notifications/components/follow_request.json"
},
{
"descriptors": [
{
"defaultMessage": "{name} favourited your status",
"id": "notification.favourite"
}, },
{ {
"defaultMessage": "{name} followed you", "defaultMessage": "{name} followed you",
"id": "notification.follow" "id": "notification.follow"
}, },
{ {
"defaultMessage": "{name} favourited your status", "defaultMessage": "Your poll has ended",
"id": "notification.favourite" "id": "notification.own_poll"
},
{
"defaultMessage": "A poll you have voted in has ended",
"id": "notification.poll"
}, },
{ {
"defaultMessage": "{name} boosted your status", "defaultMessage": "{name} boosted your status",
"id": "notification.reblog" "id": "notification.reblog"
}, },
{ {
"defaultMessage": "A poll you have voted in has ended", "defaultMessage": "{name} has requested to follow you",
"id": "notification.poll" "id": "notification.follow_request"
} }
], ],
"path": "app/javascript/mastodon/features/notifications/components/notification.json" "path": "app/javascript/mastodon/features/notifications/components/notification.json"
@ -2213,6 +2278,10 @@
"defaultMessage": "Favourite", "defaultMessage": "Favourite",
"id": "status.favourite" "id": "status.favourite"
}, },
{
"defaultMessage": "Bookmark",
"id": "status.bookmark"
},
{ {
"defaultMessage": "Mute @{name}", "defaultMessage": "Mute @{name}",
"id": "status.mute" "id": "status.mute"
@ -2260,6 +2329,22 @@
{ {
"defaultMessage": "Copy link to status", "defaultMessage": "Copy link to status",
"id": "status.copy" "id": "status.copy"
},
{
"defaultMessage": "Hide everything from {domain}",
"id": "account.block_domain"
},
{
"defaultMessage": "Unhide {domain}",
"id": "account.unblock_domain"
},
{
"defaultMessage": "Unmute @{name}",
"id": "account.unmute"
},
{
"defaultMessage": "Unblock @{name}",
"id": "account.unblock"
} }
], ],
"path": "app/javascript/mastodon/features/status/components/action_bar.json" "path": "app/javascript/mastodon/features/status/components/action_bar.json"
@ -2330,6 +2415,14 @@
{ {
"defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"id": "confirmations.reply.message" "id": "confirmations.reply.message"
},
{
"defaultMessage": "Hide entire domain",
"id": "confirmations.domain_block.confirm"
},
{
"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. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
"id": "confirmations.domain_block.message"
} }
], ],
"path": "app/javascript/mastodon/features/status/index.json" "path": "app/javascript/mastodon/features/status/index.json"
@ -2468,6 +2561,18 @@
"defaultMessage": "A quick brown fox jumps over the lazy dog", "defaultMessage": "A quick brown fox jumps over the lazy dog",
"id": "upload_modal.description_placeholder" "id": "upload_modal.description_placeholder"
}, },
{
"defaultMessage": "Describe for people with hearing loss",
"id": "upload_form.audio_description"
},
{
"defaultMessage": "Describe for people with hearing loss or visual impairment",
"id": "upload_form.video_description"
},
{
"defaultMessage": "Describe for the visually impaired",
"id": "upload_form.description"
},
{ {
"defaultMessage": "Edit media", "defaultMessage": "Edit media",
"id": "upload_modal.edit_media" "id": "upload_modal.edit_media"
@ -2476,10 +2581,6 @@
"defaultMessage": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.", "defaultMessage": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
"id": "upload_modal.hint" "id": "upload_modal.hint"
}, },
{
"defaultMessage": "Describe for the visually impaired",
"id": "upload_form.description"
},
{ {
"defaultMessage": "Analyzing picture…", "defaultMessage": "Analyzing picture…",
"id": "upload_modal.analyzing_picture" "id": "upload_modal.analyzing_picture"
@ -2629,6 +2730,10 @@
"defaultMessage": "Favourites", "defaultMessage": "Favourites",
"id": "navigation_bar.favourites" "id": "navigation_bar.favourites"
}, },
{
"defaultMessage": "Bookmarks",
"id": "navigation_bar.bookmarks"
},
{ {
"defaultMessage": "Lists", "defaultMessage": "Lists",
"id": "navigation_bar.lists" "id": "navigation_bar.lists"
@ -2771,6 +2876,10 @@
"defaultMessage": "Exit full screen", "defaultMessage": "Exit full screen",
"id": "video.exit_fullscreen" "id": "video.exit_fullscreen"
}, },
{
"defaultMessage": "Download file",
"id": "video.download"
},
{ {
"defaultMessage": "Sensitive content", "defaultMessage": "Sensitive content",
"id": "status.sensitive_warning" "id": "status.sensitive_warning"

View File

@ -103,7 +103,7 @@
"confirmations.logout.confirm": "Αποσύνδεση", "confirmations.logout.confirm": "Αποσύνδεση",
"confirmations.logout.message": "Σίγουρα θέλεις να αποσυνδεθείς;", "confirmations.logout.message": "Σίγουρα θέλεις να αποσυνδεθείς;",
"confirmations.mute.confirm": "Αποσιώπηση", "confirmations.mute.confirm": "Αποσιώπηση",
"confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.explanation": "Αυτό θα κρύψει τις δημοσιεύσεις τους και τις δημοσιεύσεις που τους αναφέρουν, αλλά θα συνεχίσουν να μπορούν να βλέπουν τις δημοσιεύσεις σου και να σε ακολουθούν.",
"confirmations.mute.message": "Σίγουρα θες να αποσιωπήσεις {name};", "confirmations.mute.message": "Σίγουρα θες να αποσιωπήσεις {name};",
"confirmations.redraft.confirm": "Διαγραφή & ξαναγράψιμο", "confirmations.redraft.confirm": "Διαγραφή & ξαναγράψιμο",
"confirmations.redraft.message": "Σίγουρα θέλεις να σβήσεις αυτή την κατάσταση και να την ξαναγράψεις; Οι αναφορές και τα αγαπημένα της θα χαθούν ενώ οι απαντήσεις προς αυτή θα μείνουν ορφανές.", "confirmations.redraft.message": "Σίγουρα θέλεις να σβήσεις αυτή την κατάσταση και να την ξαναγράψεις; Οι αναφορές και τα αγαπημένα της θα χαθούν ενώ οι απαντήσεις προς αυτή θα μείνουν ορφανές.",
@ -152,10 +152,10 @@
"empty_column.mutes": "Δεν έχεις αποσιωπήσει κανένα χρήστη ακόμα.", "empty_column.mutes": "Δεν έχεις αποσιωπήσει κανένα χρήστη ακόμα.",
"empty_column.notifications": "Δεν έχεις ειδοποιήσεις ακόμα. Αλληλεπίδρασε με άλλους χρήστες για να ξεκινήσεις την κουβέντα.", "empty_column.notifications": "Δεν έχεις ειδοποιήσεις ακόμα. Αλληλεπίδρασε με άλλους χρήστες για να ξεκινήσεις την κουβέντα.",
"empty_column.public": "Δεν υπάρχει τίποτα εδώ! Γράψε κάτι δημόσιο, ή ακολούθησε χειροκίνητα χρήστες από άλλους κόμβους για να τη γεμίσεις", "empty_column.public": "Δεν υπάρχει τίποτα εδώ! Γράψε κάτι δημόσιο, ή ακολούθησε χειροκίνητα χρήστες από άλλους κόμβους για να τη γεμίσεις",
"error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.", "error.unexpected_crash.explanation": "Είτε λόγω λάθους στον κώδικά μας ή λόγω ασυμβατότητας με τον browser, η σελίδα δε μπόρεσε να εμφανιστεί σωστά.",
"error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.", "error.unexpected_crash.next_steps": "Δοκίμασε να ανανεώσεις τη σελίδα. Αν αυτό δε βοηθήσει, ίσως να μπορέσεις να χρησιμοποιήσεις το Mastodon μέσω διαφορετικού browser ή κάποιας εφαρμογής.",
"errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", "errors.unexpected_crash.copy_stacktrace": "Αντιγραφή μηνυμάτων κώδικα στο πρόχειρο",
"errors.unexpected_crash.report_issue": "Report issue", "errors.unexpected_crash.report_issue": "Αναφορά προβλήματος",
"follow_request.authorize": "Ενέκρινε", "follow_request.authorize": "Ενέκρινε",
"follow_request.reject": "Απέρριψε", "follow_request.reject": "Απέρριψε",
"getting_started.developers": "Ανάπτυξη", "getting_started.developers": "Ανάπτυξη",
@ -178,7 +178,6 @@
"home.column_settings.basic": "Βασικές ρυθμίσεις", "home.column_settings.basic": "Βασικές ρυθμίσεις",
"home.column_settings.show_reblogs": "Εμφάνιση προωθήσεων", "home.column_settings.show_reblogs": "Εμφάνιση προωθήσεων",
"home.column_settings.show_replies": "Εμφάνιση απαντήσεων", "home.column_settings.show_replies": "Εμφάνιση απαντήσεων",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# μέρα} other {# μέρες}}", "intervals.full.days": "{number, plural, one {# μέρα} other {# μέρες}}",
"intervals.full.hours": "{number, plural, one {# ώρα} other {# ώρες}}", "intervals.full.hours": "{number, plural, one {# ώρα} other {# ώρες}}",
"intervals.full.minutes": "{number, plural, one {# λεπτό} other {# λεπτά}}", "intervals.full.minutes": "{number, plural, one {# λεπτό} other {# λεπτά}}",
@ -272,7 +271,6 @@
"navigation_bar.preferences": "Προτιμήσεις", "navigation_bar.preferences": "Προτιμήσεις",
"navigation_bar.public_timeline": "Ομοσπονδιακή ροή", "navigation_bar.public_timeline": "Ομοσπονδιακή ροή",
"navigation_bar.security": "Ασφάλεια", "navigation_bar.security": "Ασφάλεια",
"notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
"notification.favourite": "Ο/Η {name} σημείωσε ως αγαπημένη την κατάστασή σου", "notification.favourite": "Ο/Η {name} σημείωσε ως αγαπημένη την κατάστασή σου",
"notification.follow": "Ο/Η {name} σε ακολούθησε", "notification.follow": "Ο/Η {name} σε ακολούθησε",
"notification.mention": "Ο/Η {name} σε ανέφερε", "notification.mention": "Ο/Η {name} σε ανέφερε",
@ -301,7 +299,7 @@
"notifications.group": "{count} ειδοποιήσεις", "notifications.group": "{count} ειδοποιήσεις",
"poll.closed": "Κλειστή", "poll.closed": "Κλειστή",
"poll.refresh": "Ανανέωση", "poll.refresh": "Ανανέωση",
"poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_people": "{count, plural, one {# άτομο} other {# άτομα}}",
"poll.total_votes": "{count, plural, one {# ψήφος} other {# ψήφοι}}", "poll.total_votes": "{count, plural, one {# ψήφος} other {# ψήφοι}}",
"poll.vote": "Ψήφισε", "poll.vote": "Ψήφισε",
"poll.voted": "Ψηφίσατε αυτήν την απάντηση", "poll.voted": "Ψηφίσατε αυτήν την απάντηση",
@ -316,7 +314,7 @@
"privacy.public.short": "Δημόσιο", "privacy.public.short": "Δημόσιο",
"privacy.unlisted.long": "Μην δημοσιεύσεις στις δημόσιες ροές", "privacy.unlisted.long": "Μην δημοσιεύσεις στις δημόσιες ροές",
"privacy.unlisted.short": "Μη καταχωρημένα", "privacy.unlisted.short": "Μη καταχωρημένα",
"refresh": "Refresh", "refresh": "Ανανέωση",
"regeneration_indicator.label": "Φορτώνει…", "regeneration_indicator.label": "Φορτώνει…",
"regeneration_indicator.sublabel": "Η αρχική σου ροή ετοιμάζεται!", "regeneration_indicator.sublabel": "Η αρχική σου ροή ετοιμάζεται!",
"relative_time.days": "{number}η", "relative_time.days": "{number}η",

View File

@ -51,6 +51,7 @@
"bundle_modal_error.message": "Something went wrong while loading this component.", "bundle_modal_error.message": "Something went wrong while loading this component.",
"bundle_modal_error.retry": "Try again", "bundle_modal_error.retry": "Try again",
"column.blocks": "Blocked users", "column.blocks": "Blocked users",
"column.bookmarks": "Bookmarks",
"column.community": "Local timeline", "column.community": "Local timeline",
"column.direct": "Direct messages", "column.direct": "Direct messages",
"column.directory": "Browse profiles", "column.directory": "Browse profiles",
@ -138,6 +139,7 @@
"empty_column.account_timeline": "No toots here!", "empty_column.account_timeline": "No toots here!",
"empty_column.account_unavailable": "Profile unavailable", "empty_column.account_unavailable": "Profile unavailable",
"empty_column.blocks": "You haven't blocked any users yet.", "empty_column.blocks": "You haven't blocked any users yet.",
"empty_column.bookmarked_statuses": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
"empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
"empty_column.domain_blocks": "There are no hidden domains yet.", "empty_column.domain_blocks": "There are no hidden domains yet.",
@ -178,7 +180,6 @@
"home.column_settings.basic": "Basic", "home.column_settings.basic": "Basic",
"home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies", "home.column_settings.show_replies": "Show replies",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
@ -220,6 +221,7 @@
"keyboard_shortcuts.muted": "to open muted users list", "keyboard_shortcuts.muted": "to open muted users list",
"keyboard_shortcuts.my_profile": "to open your profile", "keyboard_shortcuts.my_profile": "to open your profile",
"keyboard_shortcuts.notifications": "to open notifications column", "keyboard_shortcuts.notifications": "to open notifications column",
"keyboard_shortcuts.open_media": "to open media",
"keyboard_shortcuts.pinned": "to open pinned toots list", "keyboard_shortcuts.pinned": "to open pinned toots list",
"keyboard_shortcuts.profile": "to open author's profile", "keyboard_shortcuts.profile": "to open author's profile",
"keyboard_shortcuts.reply": "to reply", "keyboard_shortcuts.reply": "to reply",
@ -252,6 +254,7 @@
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.apps": "Mobile apps", "navigation_bar.apps": "Mobile apps",
"navigation_bar.blocks": "Blocked users", "navigation_bar.blocks": "Blocked users",
"navigation_bar.bookmarks": "Bookmarks",
"navigation_bar.community_timeline": "Local timeline", "navigation_bar.community_timeline": "Local timeline",
"navigation_bar.compose": "Compose new toot", "navigation_bar.compose": "Compose new toot",
"navigation_bar.direct": "Direct messages", "navigation_bar.direct": "Direct messages",
@ -272,10 +275,11 @@
"navigation_bar.preferences": "Preferences", "navigation_bar.preferences": "Preferences",
"navigation_bar.public_timeline": "Federated timeline", "navigation_bar.public_timeline": "Federated timeline",
"navigation_bar.security": "Security", "navigation_bar.security": "Security",
"notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
"notification.favourite": "{name} favourited your status", "notification.favourite": "{name} favourited your status",
"notification.follow": "{name} followed you", "notification.follow": "{name} followed you",
"notification.follow_request": "{name} has requested to follow you",
"notification.mention": "{name} mentioned you", "notification.mention": "{name} mentioned you",
"notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you have voted in has ended", "notification.poll": "A poll you have voted in has ended",
"notification.reblog": "{name} boosted your status", "notification.reblog": "{name} boosted your status",
"notifications.clear": "Clear notifications", "notifications.clear": "Clear notifications",
@ -286,6 +290,7 @@
"notifications.column_settings.filter_bar.category": "Quick filter bar", "notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show", "notifications.column_settings.filter_bar.show": "Show",
"notifications.column_settings.follow": "New followers:", "notifications.column_settings.follow": "New followers:",
"notifications.column_settings.follow_request": "New follow requests:",
"notifications.column_settings.mention": "Mentions:", "notifications.column_settings.mention": "Mentions:",
"notifications.column_settings.poll": "Poll results:", "notifications.column_settings.poll": "Poll results:",
"notifications.column_settings.push": "Push notifications", "notifications.column_settings.push": "Push notifications",
@ -346,6 +351,7 @@
"status.admin_account": "Open moderation interface for @{name}", "status.admin_account": "Open moderation interface for @{name}",
"status.admin_status": "Open this status in the moderation interface", "status.admin_status": "Open this status in the moderation interface",
"status.block": "Block @{name}", "status.block": "Block @{name}",
"status.bookmark": "Bookmark",
"status.cancel_reblog_private": "Unboost", "status.cancel_reblog_private": "Unboost",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "This post cannot be boosted",
"status.copy": "Copy link to status", "status.copy": "Copy link to status",
@ -370,6 +376,7 @@
"status.reblogged_by": "{name} boosted", "status.reblogged_by": "{name} boosted",
"status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.", "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.",
"status.redraft": "Delete & re-draft", "status.redraft": "Delete & re-draft",
"status.remove_bookmark": "Remove bookmark",
"status.reply": "Reply", "status.reply": "Reply",
"status.replyAll": "Reply to thread", "status.replyAll": "Reply to thread",
"status.report": "Report @{name}", "status.report": "Report @{name}",
@ -402,9 +409,11 @@
"upload_button.label": "Add media ({formats})", "upload_button.label": "Add media ({formats})",
"upload_error.limit": "File upload limit exceeded.", "upload_error.limit": "File upload limit exceeded.",
"upload_error.poll": "File upload not allowed with polls.", "upload_error.poll": "File upload not allowed with polls.",
"upload_form.audio_description": "Describe for people with hearing loss",
"upload_form.description": "Describe for the visually impaired", "upload_form.description": "Describe for the visually impaired",
"upload_form.edit": "Edit", "upload_form.edit": "Edit",
"upload_form.undo": "Delete", "upload_form.undo": "Delete",
"upload_form.video_description": "Describe for people with hearing loss or visual impairment",
"upload_modal.analyzing_picture": "Analyzing picture…", "upload_modal.analyzing_picture": "Analyzing picture…",
"upload_modal.apply": "Apply", "upload_modal.apply": "Apply",
"upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
@ -414,6 +423,7 @@
"upload_modal.preview_label": "Preview ({ratio})", "upload_modal.preview_label": "Preview ({ratio})",
"upload_progress.label": "Uploading...", "upload_progress.label": "Uploading...",
"video.close": "Close video", "video.close": "Close video",
"video.download": "Download file",
"video.exit_fullscreen": "Exit full screen", "video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video", "video.expand": "Expand video",
"video.fullscreen": "Full screen", "video.fullscreen": "Full screen",

View File

@ -178,7 +178,6 @@
"home.column_settings.basic": "Bazaj agordoj", "home.column_settings.basic": "Bazaj agordoj",
"home.column_settings.show_reblogs": "Montri diskonigojn", "home.column_settings.show_reblogs": "Montri diskonigojn",
"home.column_settings.show_replies": "Montri respondojn", "home.column_settings.show_replies": "Montri respondojn",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# tago} other {# tagoj}}", "intervals.full.days": "{number, plural, one {# tago} other {# tagoj}}",
"intervals.full.hours": "{number, plural, one {# horo} other {# horoj}}", "intervals.full.hours": "{number, plural, one {# horo} other {# horoj}}",
"intervals.full.minutes": "{number, plural, one {# minuto} other {# minutoj}}", "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutoj}}",
@ -272,7 +271,6 @@
"navigation_bar.preferences": "Preferoj", "navigation_bar.preferences": "Preferoj",
"navigation_bar.public_timeline": "Fratara tempolinio", "navigation_bar.public_timeline": "Fratara tempolinio",
"navigation_bar.security": "Sekureco", "navigation_bar.security": "Sekureco",
"notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
"notification.favourite": "{name} stelumis vian mesaĝon", "notification.favourite": "{name} stelumis vian mesaĝon",
"notification.follow": "{name} eksekvis vin", "notification.follow": "{name} eksekvis vin",
"notification.mention": "{name} menciis vin", "notification.mention": "{name} menciis vin",
@ -301,7 +299,7 @@
"notifications.group": "{count} sciigoj", "notifications.group": "{count} sciigoj",
"poll.closed": "Finita", "poll.closed": "Finita",
"poll.refresh": "Aktualigi", "poll.refresh": "Aktualigi",
"poll.total_people": "{count, plural, one {# person} other {# people}}", "poll.total_people": "{count, plural, one {# homo} other {# homoj}}",
"poll.total_votes": "{count, plural, one {# voĉdono} other {# voĉdonoj}}", "poll.total_votes": "{count, plural, one {# voĉdono} other {# voĉdonoj}}",
"poll.vote": "Voĉdoni", "poll.vote": "Voĉdoni",
"poll.voted": "Vi elektis por ĉi tiu respondo", "poll.voted": "Vi elektis por ĉi tiu respondo",

View File

@ -40,7 +40,7 @@
"account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}", "account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}",
"alert.rate_limited.message": "Por favor, reintentá después de las {retry_time, time, medium}.", "alert.rate_limited.message": "Por favor, reintentá después de las {retry_time, time, medium}.",
"alert.rate_limited.title": "Tarifa limitada", "alert.rate_limited.title": "Tarifa limitada",
"alert.unexpected.message": "Ocurrió un error inesperado.", "alert.unexpected.message": "Ocurrió un error.",
"alert.unexpected.title": "¡Epa!", "alert.unexpected.title": "¡Epa!",
"autosuggest_hashtag.per_week": "{count} por semana", "autosuggest_hashtag.per_week": "{count} por semana",
"boost_modal.combo": "Podés hacer clic en {combo} para saltar esto la próxima vez", "boost_modal.combo": "Podés hacer clic en {combo} para saltar esto la próxima vez",
@ -103,10 +103,10 @@
"confirmations.logout.confirm": "Cerrar sesión", "confirmations.logout.confirm": "Cerrar sesión",
"confirmations.logout.message": "¿Estás seguro que querés cerrar la sesión?", "confirmations.logout.message": "¿Estás seguro que querés cerrar la sesión?",
"confirmations.mute.confirm": "Silenciar", "confirmations.mute.confirm": "Silenciar",
"confirmations.mute.explanation": "Esto ocultará mensajes de ellos y mensajes que los mencionen, pero todavía les permitirá ver tus mensajes o seguirte.", "confirmations.mute.explanation": "Se ocultarán los mensajes de esta cuenta y los mensajes de otras cuentas que mencionen a ésta, pero todavía esta cuenta podrá ver tus mensajes o seguirte.",
"confirmations.mute.message": "¿Estás seguro que querés silenciar a {name}?", "confirmations.mute.message": "¿Estás seguro que querés silenciar a {name}?",
"confirmations.redraft.confirm": "Eliminar toot original y editarlo", "confirmations.redraft.confirm": "Eliminar toot original y editarlo",
"confirmations.redraft.message": "¿Estás seguro que querés eliminar este estado y volverlo a editarlo? Se perderán las veces marcadas como favoritos y los retoots, y las respuestas a la publicación original quedarán huérfanas.", "confirmations.redraft.message": "¿Estás seguro que querés eliminar este estado y volver a editarlo? Se perderán las veces marcadas como favoritos y los retoots, y las respuestas a la publicación original quedarán huérfanas.",
"confirmations.reply.confirm": "Responder", "confirmations.reply.confirm": "Responder",
"confirmations.reply.message": "Responder ahora sobreescribirá el mensaje que estás redactando actualmente. ¿Estás seguro que querés seguir?", "confirmations.reply.message": "Responder ahora sobreescribirá el mensaje que estás redactando actualmente. ¿Estás seguro que querés seguir?",
"confirmations.unfollow.confirm": "Dejar de seguir", "confirmations.unfollow.confirm": "Dejar de seguir",
@ -178,7 +178,6 @@
"home.column_settings.basic": "Básico", "home.column_settings.basic": "Básico",
"home.column_settings.show_reblogs": "Mostrar retoots", "home.column_settings.show_reblogs": "Mostrar retoots",
"home.column_settings.show_replies": "Mostrar respuestas", "home.column_settings.show_replies": "Mostrar respuestas",
"home.column_settings.update_live": "Actualizar en tiempo real",
"intervals.full.days": "{number, plural, one {# día} other {# días}}", "intervals.full.days": "{number, plural, one {# día} other {# días}}",
"intervals.full.hours": "{number, plural, one {# hora} other {# horas}}", "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}",
"intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}", "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}",
@ -272,7 +271,6 @@
"navigation_bar.preferences": "Configuración", "navigation_bar.preferences": "Configuración",
"navigation_bar.public_timeline": "Línea temporal federada", "navigation_bar.public_timeline": "Línea temporal federada",
"navigation_bar.security": "Seguridad", "navigation_bar.security": "Seguridad",
"notification.and_n_others": "y {count, plural, one {otro} other {otros #}}",
"notification.favourite": "{name} marcó tu estado como favorito", "notification.favourite": "{name} marcó tu estado como favorito",
"notification.follow": "{name} te empezó a seguir", "notification.follow": "{name} te empezó a seguir",
"notification.mention": "{name} te mencionó", "notification.mention": "{name} te mencionó",
@ -308,13 +306,13 @@
"poll_button.add_poll": "Agregar una encuesta", "poll_button.add_poll": "Agregar una encuesta",
"poll_button.remove_poll": "Quitar encuesta", "poll_button.remove_poll": "Quitar encuesta",
"privacy.change": "Configurar privacidad de estado", "privacy.change": "Configurar privacidad de estado",
"privacy.direct.long": "Enviar entrada sólo a los usuarios mencionados", "privacy.direct.long": "Enviar toot sólo a los usuarios mencionados",
"privacy.direct.short": "Directo", "privacy.direct.short": "Directo",
"privacy.private.long": "Enviar entrada sólo a los seguidores", "privacy.private.long": "Enviar toot sólo a los seguidores",
"privacy.private.short": "Sólo a seguidores", "privacy.private.short": "Sólo a seguidores",
"privacy.public.long": "Enviar entrada a las líneas temporales públicas", "privacy.public.long": "Enviar toot a las líneas temporales públicas",
"privacy.public.short": "Público", "privacy.public.short": "Público",
"privacy.unlisted.long": "No enviar entrada a las líneas temporales públicas", "privacy.unlisted.long": "No enviar toot a las líneas temporales públicas",
"privacy.unlisted.short": "No listado", "privacy.unlisted.short": "No listado",
"refresh": "Refrescar", "refresh": "Refrescar",
"regeneration_indicator.label": "Cargando…", "regeneration_indicator.label": "Cargando…",
@ -367,7 +365,7 @@
"status.read_more": "Leer más", "status.read_more": "Leer más",
"status.reblog": "Retootear", "status.reblog": "Retootear",
"status.reblog_private": "Retootear a la audiencia original", "status.reblog_private": "Retootear a la audiencia original",
"status.reblogged_by": "Retooteado por {name}", "status.reblogged_by": "{name} retooteó",
"status.reblogs.empty": "Todavía nadie retooteó este toot. Cuando alguien lo haga, se mostrará acá.", "status.reblogs.empty": "Todavía nadie retooteó este toot. Cuando alguien lo haga, se mostrará acá.",
"status.redraft": "Eliminar toot original y editarlo", "status.redraft": "Eliminar toot original y editarlo",
"status.reply": "Responder", "status.reply": "Responder",

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