Merge tag 'v2.8.2' into instance_only_statuses

This commit is contained in:
Renato "Lond" Cerqueira 2019-05-07 21:42:11 +02:00
commit 84c8b1e200
101 changed files with 1602 additions and 490 deletions

View File

@ -3,6 +3,57 @@ 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.
## [2.8.2] - 2019-05-05
### Added
- Add `SOURCE_TAG` environment variable ([ushitora-anqou](https://github.com/tootsuite/mastodon/pull/10698))
### Fixed
- Fix cropped hero image on frontpage ([BaptisteGelez](https://github.com/tootsuite/mastodon/pull/10702))
- Fix blurhash gem not compiling on some operating systems ([Gargron](https://github.com/tootsuite/mastodon/pull/10700))
- Fix unexpected CSS animations in some browsers ([ThibG](https://github.com/tootsuite/mastodon/pull/10699))
- Fix closing video modal scrolling timelines to top ([ThibG](https://github.com/tootsuite/mastodon/pull/10695))
## [2.8.1] - 2019-05-04
### Added
- Add link to existing domain block when trying to block an already-blocked domain ([ThibG](https://github.com/tootsuite/mastodon/pull/10663))
- Add button to view context to media modal when opened from account gallery in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10676))
- Add ability to create multiple-choice polls in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10603))
- Add `GITHUB_REPOSITORY` and `SOURCE_BASE_URL` environment variables ([rosylilly](https://github.com/tootsuite/mastodon/pull/10600))
- Add `/interact/` paths to `robots.txt` ([ThibG](https://github.com/tootsuite/mastodon/pull/10666))
- Add `blurhash` to the Attachment entity in the REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/10630))
### Changed
- Change hidden media to be shown as a blurhash-based colorful gradient instead of a black box in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10630))
- Change rejected media to be shown as a blurhash-based gradient instead of a list of filenames in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10630))
- Change e-mail whitelist/blacklist to not be checked when invited ([Gargron](https://github.com/tootsuite/mastodon/pull/10683))
- Change cache header of REST API results to no-cache ([ThibG](https://github.com/tootsuite/mastodon/pull/10655))
- Change the "mark media as sensitive" button to be more obvious in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10673), [Gargron](https://github.com/tootsuite/mastodon/pull/10682))
- Change account gallery in web UI to display 3 columns, open media modal ([Gargron](https://github.com/tootsuite/mastodon/pull/10667), [Gargron](https://github.com/tootsuite/mastodon/pull/10674))
### Fixed
- Fix LDAP/PAM/SAML/CAS users not being pre-approved ([Gargron](https://github.com/tootsuite/mastodon/pull/10621))
- Fix accounts created through tootctl not being always pre-approved ([Gargron](https://github.com/tootsuite/mastodon/pull/10684))
- Fix Sidekiq retrying ActivityPub processing jobs that fail validation ([ThibG](https://github.com/tootsuite/mastodon/pull/10614))
- Fix toots not being scrolled into view sometimes through keyboard selection ([ThibG](https://github.com/tootsuite/mastodon/pull/10593))
- Fix expired invite links being usable to bypass approval mode ([ThibG](https://github.com/tootsuite/mastodon/pull/10657))
- Fix not being able to save e-mail preference for new pending accounts ([Gargron](https://github.com/tootsuite/mastodon/pull/10622))
- Fix upload progressbar when image resizing is involved ([ThibG](https://github.com/tootsuite/mastodon/pull/10632))
- Fix block action not automatically cancelling pending follow request ([ThibG](https://github.com/tootsuite/mastodon/pull/10633))
- Fix stoplight logging to stderr separate from Rails logger ([Gargron](https://github.com/tootsuite/mastodon/pull/10624))
- Fix sign up button not saying sign up when invite is used ([Gargron](https://github.com/tootsuite/mastodon/pull/10623))
- Fix health checks in Docker Compose configuration ([fabianonline](https://github.com/tootsuite/mastodon/pull/10553))
- Fix modal items not being scrollable on touch devices ([kedamaDQ](https://github.com/tootsuite/mastodon/pull/10605))
- Fix Keybase configuration using wrong domain when a web domain is used ([BenLubar](https://github.com/tootsuite/mastodon/pull/10565))
- Fix avatar GIFs not being animated on-hover on public profiles ([hyenagirl64](https://github.com/tootsuite/mastodon/pull/10549))
- Fix OpenGraph parser not understanding some valid property meta tags ([da2x](https://github.com/tootsuite/mastodon/pull/10604))
- Fix wrong fonts being displayed when Roboto is installed on user's machine ([ThibG](https://github.com/tootsuite/mastodon/pull/10594))
- Fix confirmation modals being too narrow for a secondary action button ([ThibG](https://github.com/tootsuite/mastodon/pull/10586))
## [2.8.0] - 2019-04-10 ## [2.8.0] - 2019-04-10
### Added ### Added

17
Gemfile
View File

@ -21,6 +21,7 @@ gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0' gem 'paperclip', '~> 6.0'
gem 'paperclip-av-transcoder', '~> 0.6' gem 'paperclip-av-transcoder', '~> 0.6'
gem 'streamio-ffmpeg', '~> 3.0' gem 'streamio-ffmpeg', '~> 3.0'
gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10' gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.6' gem 'addressable', '~> 2.6'
@ -29,7 +30,7 @@ gem 'browser'
gem 'charlock_holmes', '~> 0.7.6' gem 'charlock_holmes', '~> 0.7.6'
gem 'iso-639' gem 'iso-639'
gem 'chewy', '~> 5.0' gem 'chewy', '~> 5.0'
gem 'cld3', '~> 3.2.3' gem 'cld3', '~> 3.2.4'
gem 'devise', '~> 4.6' gem 'devise', '~> 4.6'
gem 'devise-two-factor', '~> 3.0' gem 'devise-two-factor', '~> 3.0'
@ -42,7 +43,7 @@ gem 'omniauth-cas', '~> 1.1'
gem 'omniauth-saml', '~> 1.10' gem 'omniauth-saml', '~> 1.10'
gem 'omniauth', '~> 1.9' gem 'omniauth', '~> 1.9'
gem 'doorkeeper', '~> 5.0' gem 'doorkeeper', '~> 5.1'
gem 'fast_blank', '~> 1.0' gem 'fast_blank', '~> 1.0'
gem 'fastimage' gem 'fastimage'
gem 'goldfinger', '~> 2.1' gem 'goldfinger', '~> 2.1'
@ -65,7 +66,7 @@ gem 'ox', '~> 2.10'
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.0' gem 'pundit', '~> 2.0'
gem 'premailer-rails' gem 'premailer-rails'
gem 'rack-attack', '~> 5.4' gem 'rack-attack', '~> 6.0'
gem 'rack-cors', '~> 1.0', require: 'rack/cors' gem 'rack-cors', '~> 1.0', 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'
@ -107,7 +108,7 @@ group :production, :test do
end end
group :test do group :test do
gem 'capybara', '~> 3.16' gem 'capybara', '~> 3.18'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
gem 'faker', '~> 1.9' gem 'faker', '~> 1.9'
gem 'microformats', '~> 4.1' gem 'microformats', '~> 4.1'
@ -123,14 +124,14 @@ group :development do
gem 'annotate', '~> 2.7' gem 'annotate', '~> 2.7'
gem 'better_errors', '~> 2.5' gem 'better_errors', '~> 2.5'
gem 'binding_of_caller', '~> 0.7' gem 'binding_of_caller', '~> 0.7'
gem 'bullet', '~> 5.9' 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.67', require: false gem 'rubocop', '~> 0.68', require: false
gem 'brakeman', '~> 4.5', require: false gem 'brakeman', '~> 4.5', require: false
gem 'bundler-audit', '~> 0.6', require: false gem 'bundler-audit', '~> 0.6', require: false
gem 'scss_lint', '~> 0.57', require: false gem 'scss_lint', '~> 0.58', require: false
gem 'capistrano', '~> 3.11' gem 'capistrano', '~> 3.11'
gem 'capistrano-rails', '~> 1.4' gem 'capistrano-rails', '~> 1.4'
@ -142,7 +143,7 @@ group :development do
end end
group :production do group :production do
gem 'lograge', '~> 0.10' gem 'lograge', '~> 0.11'
gem 'redis-rails', '~> 5.0' gem 'redis-rails', '~> 5.0'
end end

View File

@ -66,8 +66,8 @@ GEM
public_suffix (>= 2.0.2, < 4.0) public_suffix (>= 2.0.2, < 4.0)
airbrussh (1.3.0) airbrussh (1.3.0)
sshkit (>= 1.6.1, != 1.7.0) sshkit (>= 1.6.1, != 1.7.0)
annotate (2.7.4) annotate (2.7.5)
activerecord (>= 3.2, < 6.0) activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 13.0) rake (>= 10.4, < 13.0)
arel (9.0.0) arel (9.0.0)
ast (2.4.0) ast (2.4.0)
@ -76,16 +76,16 @@ GEM
av (0.9.0) av (0.9.0)
cocaine (~> 0.5.3) cocaine (~> 0.5.3)
aws-eventstream (1.0.2) aws-eventstream (1.0.2)
aws-partitions (1.147.0) aws-partitions (1.151.0)
aws-sdk-core (3.48.3) aws-sdk-core (3.48.4)
aws-eventstream (~> 1.0, >= 1.0.2) aws-eventstream (~> 1.0, >= 1.0.2)
aws-partitions (~> 1.0) aws-partitions (~> 1.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-kms (1.16.0) aws-sdk-kms (1.17.0)
aws-sdk-core (~> 3, >= 3.48.2) aws-sdk-core (~> 3, >= 3.48.2)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.36.0) aws-sdk-s3 (1.36.1)
aws-sdk-core (~> 3, >= 3.48.2) aws-sdk-core (~> 3, >= 3.48.2)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.0) aws-sigv4 (~> 1.0)
@ -99,12 +99,14 @@ GEM
rack (>= 0.9.0) rack (>= 0.9.0)
binding_of_caller (0.8.0) binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootsnap (1.4.3) blurhash (0.1.3)
ffi (~> 1.10.0)
bootsnap (1.4.4)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (4.5.0) brakeman (4.5.0)
browser (2.5.3) browser (2.5.3)
builder (3.2.3) builder (3.2.3)
bullet (5.9.0) bullet (6.0.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
bundler-audit (0.6.1) bundler-audit (0.6.1)
@ -127,7 +129,7 @@ GEM
sshkit (~> 1.3) sshkit (~> 1.3)
capistrano-yarn (2.0.2) capistrano-yarn (2.0.2)
capistrano (~> 3.0) capistrano (~> 3.0)
capybara (3.16.1) capybara (3.18.0)
addressable addressable
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (~> 1.8) nokogiri (~> 1.8)
@ -143,8 +145,8 @@ GEM
elasticsearch (>= 2.0.0) elasticsearch (>= 2.0.0)
elasticsearch-dsl elasticsearch-dsl
chunky_png (1.3.10) chunky_png (1.3.10)
cld3 (3.2.3) cld3 (3.2.4)
ffi (>= 1.1.0, < 1.10.0) ffi (>= 1.1.0, < 1.11.0)
climate_control (0.2.0) climate_control (0.2.0)
cocaine (0.5.8) cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
@ -184,8 +186,8 @@ GEM
docile (1.3.0) docile (1.3.0)
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.0.2) doorkeeper (5.1.0)
railties (>= 4.2) railties (>= 5)
dotenv (2.7.2) dotenv (2.7.2)
dotenv-rails (2.7.2) dotenv-rails (2.7.2)
dotenv (= 2.7.2) dotenv (= 2.7.2)
@ -205,14 +207,14 @@ GEM
et-orbi (1.1.6) et-orbi (1.1.6)
tzinfo tzinfo
excon (0.62.0) excon (0.62.0)
fabrication (2.20.1) fabrication (2.20.2)
faker (1.9.3) faker (1.9.3)
i18n (>= 0.7) i18n (>= 0.7)
faraday (0.15.0) faraday (0.15.0)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
fast_blank (1.0.0) fast_blank (1.0.0)
fastimage (2.1.5) fastimage (2.1.5)
ffi (1.9.25) ffi (1.10.0)
fog-core (2.1.0) fog-core (2.1.0)
builder builder
excon (~> 0.58) excon (~> 0.58)
@ -318,7 +320,7 @@ GEM
letter_opener (~> 1.0) letter_opener (~> 1.0)
railties (>= 3.2) railties (>= 3.2)
link_header (0.0.8) link_header (0.0.8)
lograge (0.10.0) lograge (0.11.0)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
@ -346,7 +348,7 @@ GEM
mini_mime (1.0.1) mini_mime (1.0.1)
mini_portile2 (2.4.0) mini_portile2 (2.4.0)
minitest (5.11.3) minitest (5.11.3)
msgpack (1.2.9) msgpack (1.2.10)
multi_json (1.13.1) multi_json (1.13.1)
multipart-post (2.0.0) multipart-post (2.0.0)
necromancer (0.4.0) necromancer (0.4.0)
@ -355,7 +357,7 @@ GEM
net-ssh (>= 2.6.5) net-ssh (>= 2.6.5)
net-ssh (5.0.2) net-ssh (5.0.2)
nio4r (2.3.1) nio4r (2.3.1)
nokogiri (1.10.2) nokogiri (1.10.3)
mini_portile2 (~> 2.4.0) mini_portile2 (~> 2.4.0)
nokogumbo (2.0.0) nokogumbo (2.0.0)
nokogiri (~> 1.8, >= 1.8.4) nokogiri (~> 1.8, >= 1.8.4)
@ -364,7 +366,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.7.11) oj (3.7.12)
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)
@ -393,7 +395,7 @@ GEM
parallel (1.17.0) parallel (1.17.0)
parallel_tests (2.28.0) parallel_tests (2.28.0)
parallel parallel
parser (2.6.2.0) parser (2.6.3.0)
ast (~> 2.4.0) ast (~> 2.4.0)
pastel (0.7.2) pastel (0.7.2)
equatable (~> 0.5.0) equatable (~> 0.5.0)
@ -418,14 +420,13 @@ GEM
pry (~> 0.10) pry (~> 0.10)
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
psych (3.1.0)
public_suffix (3.0.3) public_suffix (3.0.3)
puma (3.12.1) puma (3.12.1)
pundit (2.0.1) pundit (2.0.1)
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 (5.4.2) rack-attack (6.0.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-cors (1.0.3) rack-cors (1.0.3)
rack-protection (2.0.5) rack-protection (2.0.5)
@ -470,8 +471,8 @@ GEM
rainbow (3.0.0) rainbow (3.0.0)
rake (12.3.2) rake (12.3.2)
rb-fsevent (0.10.3) rb-fsevent (0.10.3)
rb-inotify (0.9.10) rb-inotify (0.10.0)
ffi (>= 0.5.0, < 2) ffi (~> 1.0)
rdf (3.0.9) rdf (3.0.9)
hamster (~> 3.0) hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
@ -496,7 +497,7 @@ GEM
redis-store (>= 1.2, < 2) redis-store (>= 1.2, < 2)
redis-store (1.5.0) redis-store (1.5.0)
redis (>= 2.2, < 5) redis (>= 2.2, < 5)
regexp_parser (1.3.0) regexp_parser (1.4.0)
request_store (1.4.1) request_store (1.4.1)
rack (>= 1.4) rack (>= 1.4)
responders (2.4.1) responders (2.4.1)
@ -526,11 +527,10 @@ GEM
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.8.0)
rubocop (0.67.1) rubocop (0.68.1)
jaro_winkler (~> 1.5.1) jaro_winkler (~> 1.5.1)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.5, != 2.5.1.1) parser (>= 2.5, != 2.5.1.1)
psych (>= 3.1.0)
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.6) unicode-display_width (>= 1.4.0, < 1.6)
@ -544,15 +544,15 @@ GEM
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.8.0) nokogiri (>= 1.8.0)
nokogumbo (~> 2.0) nokogumbo (~> 2.0)
sass (3.6.0) sass (3.7.4)
sass-listen (~> 4.0.0) sass-listen (~> 4.0.0)
sass-listen (4.0.0) sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4) rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7) rb-inotify (~> 0.9, >= 0.9.7)
scss_lint (0.57.1) scss_lint (0.58.0)
rake (>= 0.9, < 13) rake (>= 0.9, < 13)
sass (~> 3.5, >= 3.5.5) sass (~> 3.5, >= 3.5.5)
sidekiq (5.2.5) sidekiq (5.2.7)
connection_pool (~> 2.2, >= 2.2.2) connection_pool (~> 2.2, >= 2.2.2)
rack (>= 1.5.0) rack (>= 1.5.0)
rack-protection (>= 1.5.0) rack-protection (>= 1.5.0)
@ -564,7 +564,7 @@ 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.12) sidekiq-unique-jobs (6.0.13)
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)
@ -640,7 +640,7 @@ GEM
activesupport (>= 4.2) activesupport (>= 4.2)
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 4.2) railties (>= 4.2)
webpush (0.3.7) webpush (0.3.8)
hkdf (~> 0.2) hkdf (~> 0.2)
jwt (~> 2.0) jwt (~> 2.0)
websocket-driver (0.7.0) websocket-driver (0.7.0)
@ -661,26 +661,27 @@ DEPENDENCIES
aws-sdk-s3 (~> 1.36) aws-sdk-s3 (~> 1.36)
better_errors (~> 2.5) better_errors (~> 2.5)
binding_of_caller (~> 0.7) binding_of_caller (~> 0.7)
blurhash (~> 0.1)
bootsnap (~> 1.4) bootsnap (~> 1.4)
brakeman (~> 4.5) brakeman (~> 4.5)
browser browser
bullet (~> 5.9) bullet (~> 6.0)
bundler-audit (~> 0.6) bundler-audit (~> 0.6)
capistrano (~> 3.11) capistrano (~> 3.11)
capistrano-rails (~> 1.4) capistrano-rails (~> 1.4)
capistrano-rbenv (~> 2.1) capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0) capistrano-yarn (~> 2.0)
capybara (~> 3.16) capybara (~> 3.18)
charlock_holmes (~> 0.7.6) charlock_holmes (~> 0.7.6)
chewy (~> 5.0) chewy (~> 5.0)
cld3 (~> 3.2.3) cld3 (~> 3.2.4)
climate_control (~> 0.2) climate_control (~> 0.2)
concurrent-ruby concurrent-ruby
derailed_benchmarks derailed_benchmarks
devise (~> 4.6) devise (~> 4.6)
devise-two-factor (~> 3.0) devise-two-factor (~> 3.0)
devise_pam_authenticatable2 (~> 9.2) devise_pam_authenticatable2 (~> 9.2)
doorkeeper (~> 5.0) doorkeeper (~> 5.1)
dotenv-rails (~> 2.7) dotenv-rails (~> 2.7)
fabrication (~> 2.20) fabrication (~> 2.20)
faker (~> 1.9) faker (~> 1.9)
@ -706,7 +707,7 @@ DEPENDENCIES
letter_opener (~> 1.7) letter_opener (~> 1.7)
letter_opener_web (~> 1.3) letter_opener_web (~> 1.3)
link_header (~> 0.0) link_header (~> 0.0)
lograge (~> 0.10) lograge (~> 0.11)
makara (~> 0.4) makara (~> 0.4)
mario-redis-lock (~> 1.2) mario-redis-lock (~> 1.2)
memory_profiler memory_profiler
@ -734,7 +735,7 @@ DEPENDENCIES
pry-rails (~> 0.3) pry-rails (~> 0.3)
puma (~> 3.12) puma (~> 3.12)
pundit (~> 2.0) pundit (~> 2.0)
rack-attack (~> 5.4) rack-attack (~> 6.0)
rack-cors (~> 1.0) rack-cors (~> 1.0)
rails (~> 5.2.3) rails (~> 5.2.3)
rails-controller-testing (~> 1.0) rails-controller-testing (~> 1.0)
@ -747,9 +748,9 @@ DEPENDENCIES
rqrcode (~> 0.10) rqrcode (~> 0.10)
rspec-rails (~> 3.8) rspec-rails (~> 3.8)
rspec-sidekiq (~> 3.0) rspec-sidekiq (~> 3.0)
rubocop (~> 0.67) rubocop (~> 0.68)
sanitize (~> 5.0) sanitize (~> 5.0)
scss_lint (~> 0.57) scss_lint (~> 0.58)
sidekiq (~> 5.2) sidekiq (~> 5.2)
sidekiq-bulk (~> 0.2.0) sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 3.0) sidekiq-scheduler (~> 3.0)

View File

@ -13,13 +13,25 @@ module Admin
authorize :domain_block, :create? authorize :domain_block, :create?
@domain_block = DomainBlock.new(resource_params) @domain_block = DomainBlock.new(resource_params)
existing_domain_block = resource_params[:domain].present? ? DomainBlock.find_by(domain: resource_params[:domain]) : nil
if @domain_block.save if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
DomainBlockWorker.perform_async(@domain_block.id) @domain_block.save
log_action :create, @domain_block flash[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') @domain_block.errors[:domain].clear
else
render :new render :new
else
if existing_domain_block.present?
@domain_block = existing_domain_block
@domain_block.update(resource_params)
end
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id)
log_action :create, @domain_block
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
else
render :new
end
end end
end end

View File

@ -9,6 +9,8 @@ class Api::BaseController < ApplicationController
skip_before_action :store_current_location skip_before_action :store_current_location
skip_before_action :check_user_permissions skip_before_action :check_user_permissions
before_action :set_cache_headers
protect_from_forgery with: :null_session protect_from_forgery with: :null_session
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e| rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
@ -88,4 +90,8 @@ class Api::BaseController < ApplicationController
def authorize_if_got_token!(*scopes) def authorize_if_got_token!(*scopes)
doorkeeper_authorize!(*scopes) if doorkeeper_token doorkeeper_authorize!(*scopes) if doorkeeper_token
end end
def set_cache_headers
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
end
end end

View File

@ -3,6 +3,8 @@
class Api::V1::CustomEmojisController < Api::BaseController class Api::V1::CustomEmojisController < Api::BaseController
respond_to :json respond_to :json
skip_before_action :set_cache_headers
def index def index
render_cached_json('api:v1:custom_emojis', expires_in: 1.minute) do render_cached_json('api:v1:custom_emojis', expires_in: 1.minute) do
ActiveModelSerializers::SerializableResource.new(CustomEmoji.local.where(disabled: false), each_serializer: REST::CustomEmojiSerializer) ActiveModelSerializers::SerializableResource.new(CustomEmoji.local.where(disabled: false), each_serializer: REST::CustomEmojiSerializer)

View File

@ -2,6 +2,7 @@
class Api::V1::Instances::ActivityController < Api::BaseController class Api::V1::Instances::ActivityController < Api::BaseController
before_action :require_enabled_api! before_action :require_enabled_api!
skip_before_action :set_cache_headers
respond_to :json respond_to :json

View File

@ -2,6 +2,7 @@
class Api::V1::Instances::PeersController < Api::BaseController class Api::V1::Instances::PeersController < Api::BaseController
before_action :require_enabled_api! before_action :require_enabled_api!
skip_before_action :set_cache_headers
respond_to :json respond_to :json

View File

@ -2,6 +2,7 @@
class Api::V1::InstancesController < Api::BaseController class Api::V1::InstancesController < Api::BaseController
respond_to :json respond_to :json
skip_before_action :set_cache_headers
def show def show
render_cached_json('api:v1:instances', expires_in: 5.minutes) do render_cached_json('api:v1:instances', expires_in: 5.minutes) do

View File

@ -91,7 +91,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end end
def set_invite def set_invite
@invite = invite_code.present? ? Invite.find_by(code: invite_code) : nil invite = invite_code.present? ? Invite.find_by(code: invite_code) : nil
@invite = invite&.valid_for_use? ? invite : nil
end end
def determine_layout def determine_layout

View File

@ -25,7 +25,7 @@ class Settings::NotificationsController < Settings::BaseController
def user_settings_params def user_settings_params
params.require(:user).permit( params.require(:user).permit(
notification_emails: %i(follow follow_request reblog favourite mention digest report), notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account),
interactions: %i(must_be_follower must_be_following must_be_following_dm) interactions: %i(must_be_follower must_be_following must_be_following_dm)
) )
end end

View File

@ -205,8 +205,8 @@ 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 total = Array.from(files).reduce((a, v) => a + v.size, 0);
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);
if (files.length + media.size > uploadLimit) { if (files.length + media.size > uploadLimit) {
dispatch(showAlert(undefined, messages.uploadErrorLimit)); dispatch(showAlert(undefined, messages.uploadErrorLimit));
@ -226,6 +226,8 @@ export function uploadCompose(files) {
resizeImage(f).then(file => { resizeImage(f).then(file => {
const data = new FormData(); const data = new FormData();
data.append('file', file); data.append('file', file);
// Account for disparity in size of original image and resized data
total += file.size - f.size;
return api(getState).post('/api/v1/media', data, { return api(getState).post('/api/v1/media', data, {
onUploadProgress: function({ loaded }){ onUploadProgress: function({ loaded }){

View File

@ -96,7 +96,7 @@ export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done =
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true }); export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => { export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => {
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {

View File

@ -7,6 +7,7 @@ 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 } from '../initial_state'; import { autoPlayGif, displayMedia } from '../initial_state';
import { decode } from 'blurhash';
const messages = defineMessages({ const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }, toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
@ -21,6 +22,7 @@ class Item extends React.PureComponent {
size: PropTypes.number.isRequired, size: PropTypes.number.isRequired,
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
displayWidth: PropTypes.number, displayWidth: PropTypes.number,
visible: PropTypes.bool.isRequired,
}; };
static defaultProps = { static defaultProps = {
@ -29,6 +31,10 @@ class Item extends React.PureComponent {
size: 1, size: 1,
}; };
state = {
loaded: false,
};
handleMouseEnter = (e) => { handleMouseEnter = (e) => {
if (this.hoverToPlay()) { if (this.hoverToPlay()) {
e.target.play(); e.target.play();
@ -62,8 +68,40 @@ class Item extends React.PureComponent {
e.stopPropagation(); e.stopPropagation();
} }
componentDidMount () {
if (this.props.attachment.get('blurhash')) {
this._decode();
}
}
componentDidUpdate (prevProps) {
if (prevProps.attachment.get('blurhash') !== this.props.attachment.get('blurhash') && this.props.attachment.get('blurhash')) {
this._decode();
}
}
_decode () {
const hash = this.props.attachment.get('blurhash');
const pixels = decode(hash, 32, 32);
if (pixels) {
const ctx = this.canvas.getContext('2d');
const imageData = new ImageData(pixels, 32, 32);
ctx.putImageData(imageData, 0, 0);
}
}
setCanvasRef = c => {
this.canvas = c;
}
handleImageLoad = () => {
this.setState({ loaded: true });
}
render () { render () {
const { attachment, index, size, standalone, displayWidth } = this.props; const { attachment, index, size, standalone, displayWidth, visible } = this.props;
let width = 50; let width = 50;
let height = 100; let height = 100;
@ -116,12 +154,20 @@ class Item extends React.PureComponent {
let thumbnail = ''; let thumbnail = '';
if (attachment.get('type') === 'image') { if (attachment.get('type') === 'unknown') {
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}%` }}>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }}>
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
</a>
</div>
);
} else if (attachment.get('type') === 'image') {
const previewUrl = attachment.get('preview_url'); const previewUrl = attachment.get('preview_url');
const previewWidth = attachment.getIn(['meta', 'small', 'width']); const previewWidth = attachment.getIn(['meta', 'small', 'width']);
const originalUrl = attachment.get('url'); const originalUrl = attachment.get('url');
const originalWidth = attachment.getIn(['meta', 'original', 'width']); const originalWidth = attachment.getIn(['meta', 'original', 'width']);
const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number'; const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
@ -147,6 +193,7 @@ class Item extends React.PureComponent {
alt={attachment.get('description')} alt={attachment.get('description')}
title={attachment.get('description')} title={attachment.get('description')}
style={{ objectPosition: `${x}% ${y}%` }} style={{ objectPosition: `${x}% ${y}%` }}
onLoad={this.handleImageLoad}
/> />
</a> </a>
); );
@ -176,7 +223,8 @@ class Item extends React.PureComponent {
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}%` }}>
{thumbnail} <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && this.state.loaded })} />
{visible && thumbnail}
</div> </div>
); );
} }
@ -225,6 +273,7 @@ class MediaGallery extends React.PureComponent {
if (node /*&& this.isStandaloneEligible()*/) { if (node /*&& this.isStandaloneEligible()*/) {
// 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);
this.setState({ this.setState({
width: node.offsetWidth, width: node.offsetWidth,
}); });
@ -242,7 +291,7 @@ class MediaGallery extends React.PureComponent {
const width = this.state.width || defaultWidth; const width = this.state.width || defaultWidth;
let children; let children, spoilerButton;
const style = {}; const style = {};
@ -256,35 +305,28 @@ class MediaGallery extends React.PureComponent {
style.height = height; style.height = height;
} }
if (!visible) { const size = media.take(4).size;
let warning;
if (sensitive) { if (this.isStandaloneEligible()) {
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />; children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />;
} else { } else {
warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />; 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} />);
} }
children = ( if (visible) {
<button type='button' className='media-spoiler' onClick={this.handleOpen} style={style} ref={this.handleRef}> spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon='eye-slash' overlay onClick={this.handleOpen} />;
<span className='media-spoiler__warning'>{warning}</span> } else {
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> spoilerButton = (
<button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'>
<span className='spoiler-button__overlay__label'>{sensitive ? <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /> : <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />}</span>
</button> </button>
); );
} else {
const size = media.take(4).size;
if (this.isStandaloneEligible()) {
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} />;
} else {
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} />);
}
} }
return ( return (
<div className='media-gallery' style={style} ref={this.handleRef}> <div className='media-gallery' style={style} ref={this.handleRef}>
<div className={classNames('spoiler-button', { 'spoiler-button--visible': visible })}> <div className={classNames('spoiler-button', { 'spoiler-button--minified': visible })}>
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} /> {spoilerButton}
</div> </div>
{children} {children}

View File

@ -274,7 +274,7 @@ class Status extends ImmutablePureComponent {
if (status.get('poll')) { if (status.get('poll')) {
media = <PollContainer pollId={status.get('poll')} />; media = <PollContainer pollId={status.get('poll')} />;
} else if (status.get('media_attachments').size > 0) { } else if (status.get('media_attachments').size > 0) {
if (this.props.muted || status.get('media_attachments').some(item => item.get('type') === 'unknown')) { if (this.props.muted) {
media = ( media = (
<AttachmentList <AttachmentList
compact compact
@ -289,6 +289,7 @@ class Status extends ImmutablePureComponent {
{Component => ( {Component => (
<Component <Component
preview={video.get('preview_url')} preview={video.get('preview_url')}
blurhash={video.get('blurhash')}
src={video.get('url')} src={video.get('url')}
alt={video.get('description')} alt={video.get('description')}
width={this.props.cachedMediaWidth} width={this.props.cachedMediaWidth}

View File

@ -46,22 +46,28 @@ export default class StatusList extends ImmutablePureComponent {
handleMoveUp = (id, featured) => { handleMoveUp = (id, featured) => {
const elementIndex = this.getCurrentStatusIndex(id, featured) - 1; const elementIndex = this.getCurrentStatusIndex(id, featured) - 1;
this._selectChild(elementIndex); this._selectChild(elementIndex, true);
} }
handleMoveDown = (id, featured) => { handleMoveDown = (id, featured) => {
const elementIndex = this.getCurrentStatusIndex(id, featured) + 1; const elementIndex = this.getCurrentStatusIndex(id, featured) + 1;
this._selectChild(elementIndex); this._selectChild(elementIndex, false);
} }
handleLoadOlder = debounce(() => { handleLoadOlder = debounce(() => {
this.props.onLoadMore(this.props.statusIds.size > 0 ? this.props.statusIds.last() : undefined); this.props.onLoadMore(this.props.statusIds.size > 0 ? this.props.statusIds.last() : undefined);
}, 300, { leading: true }) }, 300, { leading: true })
_selectChild (index) { _selectChild (index, align_top) {
const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`); const container = this.node.node;
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
if (element) { if (element) {
if (align_top && container.scrollTop > element.offsetTop) {
element.scrollIntoView(true);
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
element.scrollIntoView(false);
}
element.focus(); element.focus();
} }
} }

View File

@ -1,62 +1,142 @@
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 ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Permalink from '../../../components/permalink'; import { autoPlayGif, displayMedia } from 'mastodon/initial_state';
import { displayMedia } from '../../../initial_state'; import classNames from 'classnames';
import Icon from 'mastodon/components/icon'; import { decode } from 'blurhash';
import { isIOS } from 'mastodon/is_mobile';
export default class MediaItem extends ImmutablePureComponent { export default class MediaItem extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.map.isRequired, attachment: ImmutablePropTypes.map.isRequired,
displayWidth: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired,
}; };
state = { state = {
visible: displayMedia !== 'hide_all' && !this.props.media.getIn(['status', 'sensitive']) || displayMedia === 'show_all', visible: displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || displayMedia === 'show_all',
loaded: false,
}; };
handleClick = () => { componentDidMount () {
if (!this.state.visible) { if (this.props.attachment.get('blurhash')) {
this.setState({ visible: true }); this._decode();
return true;
} }
}
return false; componentDidUpdate (prevProps) {
if (prevProps.attachment.get('blurhash') !== this.props.attachment.get('blurhash') && this.props.attachment.get('blurhash')) {
this._decode();
}
}
_decode () {
const hash = this.props.attachment.get('blurhash');
const pixels = decode(hash, 32, 32);
if (pixels) {
const ctx = this.canvas.getContext('2d');
const imageData = new ImageData(pixels, 32, 32);
ctx.putImageData(imageData, 0, 0);
}
}
setCanvasRef = c => {
this.canvas = c;
}
handleImageLoad = () => {
this.setState({ loaded: true });
}
handleMouseEnter = e => {
if (this.hoverToPlay()) {
e.target.play();
}
}
handleMouseLeave = e => {
if (this.hoverToPlay()) {
e.target.pause();
e.target.currentTime = 0;
}
}
hoverToPlay () {
return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1;
}
handleClick = e => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
if (this.state.visible) {
this.props.onOpenMedia(this.props.attachment);
} else {
this.setState({ visible: true });
}
}
} }
render () { render () {
const { media } = this.props; const { attachment, displayWidth } = this.props;
const { visible } = this.state; const { visible, loaded } = this.state;
const status = media.get('status');
const focusX = media.getIn(['meta', 'focus', 'x']);
const focusY = media.getIn(['meta', 'focus', 'y']);
const x = ((focusX / 2) + .5) * 100;
const y = ((focusY / -2) + .5) * 100;
const style = {};
let label, icon; const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
const height = width;
const status = attachment.get('status');
if (media.get('type') === 'gifv') { let thumbnail = '';
label = <span className='media-gallery__gifv__label'>GIF</span>;
}
if (visible) { if (attachment.get('type') === 'unknown') {
style.backgroundImage = `url(${media.get('preview_url')})`; // Skip
style.backgroundPosition = `${x}% ${y}%`; } else if (attachment.get('type') === 'image') {
} else { const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
icon = ( const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
<span className='account-gallery__item__icons'> const x = ((focusX / 2) + .5) * 100;
<Icon id='eye-slash' /> const y = ((focusY / -2) + .5) * 100;
</span>
thumbnail = (
<img
src={attachment.get('preview_url')}
alt={attachment.get('description')}
title={attachment.get('description')}
style={{ objectPosition: `${x}% ${y}%` }}
onLoad={this.handleImageLoad}
/>
);
} else if (['gifv', 'video'].indexOf(attachment.get('type')) !== -1) {
const autoPlay = !isIOS() && autoPlayGif;
thumbnail = (
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
<video
className='media-gallery__item-gifv-thumbnail'
aria-label={attachment.get('description')}
title={attachment.get('description')}
role='application'
src={attachment.get('url')}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
autoPlay={autoPlay}
loop
muted
/>
<span className='media-gallery__gifv__label'>GIF</span>
</div>
); );
} }
return ( return (
<div className='account-gallery__item'> <div className='account-gallery__item' style={{ width, height }}>
<Permalink to={`/statuses/${status.get('id')}`} href={status.get('url')} style={style} onInterceptClick={this.handleClick}> <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick}>
{icon} <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} />
{label} {visible && thumbnail}
</Permalink> </a>
</div> </div>
); );
} }

View File

@ -2,24 +2,25 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { fetchAccount } from '../../actions/accounts'; import { fetchAccount } from 'mastodon/actions/accounts';
import { expandAccountMediaTimeline } from '../../actions/timelines'; import { expandAccountMediaTimeline } from '../../actions/timelines';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from 'mastodon/components/loading_indicator';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import ColumnBackButton from '../../components/column_back_button'; import ColumnBackButton from 'mastodon/components/column_back_button';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { getAccountGallery } from '../../selectors'; import { getAccountGallery } from 'mastodon/selectors';
import MediaItem from './components/media_item'; import MediaItem from './components/media_item';
import HeaderContainer from '../account_timeline/containers/header_container'; import HeaderContainer from '../account_timeline/containers/header_container';
import { ScrollContainer } from 'react-router-scroll-4'; import { ScrollContainer } from 'react-router-scroll-4';
import LoadMore from '../../components/load_more'; import LoadMore from 'mastodon/components/load_more';
import MissingIndicator from 'mastodon/components/missing_indicator'; import MissingIndicator from 'mastodon/components/missing_indicator';
import { openModal } from 'mastodon/actions/modal';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
isAccount: !!state.getIn(['accounts', props.params.accountId]), isAccount: !!state.getIn(['accounts', props.params.accountId]),
medias: getAccountGallery(state, props.params.accountId), attachments: getAccountGallery(state, props.params.accountId),
isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']), isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']), hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
}); });
class LoadMoreMedia extends ImmutablePureComponent { class LoadMoreMedia extends ImmutablePureComponent {
@ -51,12 +52,16 @@ class AccountGallery extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
medias: ImmutablePropTypes.list.isRequired, attachments: ImmutablePropTypes.list.isRequired,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
isAccount: PropTypes.bool, isAccount: PropTypes.bool,
}; };
state = {
width: 323,
};
componentDidMount () { componentDidMount () {
this.props.dispatch(fetchAccount(this.props.params.accountId)); this.props.dispatch(fetchAccount(this.props.params.accountId));
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId)); this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId));
@ -71,11 +76,11 @@ class AccountGallery extends ImmutablePureComponent {
handleScrollToBottom = () => { handleScrollToBottom = () => {
if (this.props.hasMore) { if (this.props.hasMore) {
this.handleLoadMore(this.props.medias.size > 0 ? this.props.medias.last().getIn(['status', 'id']) : undefined); this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined);
} }
} }
handleScroll = (e) => { handleScroll = e => {
const { scrollTop, scrollHeight, clientHeight } = e.target; const { scrollTop, scrollHeight, clientHeight } = e.target;
const offset = scrollHeight - scrollTop - clientHeight; const offset = scrollHeight - scrollTop - clientHeight;
@ -88,13 +93,31 @@ class AccountGallery extends ImmutablePureComponent {
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId, { maxId })); this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId, { maxId }));
}; };
handleLoadOlder = (e) => { handleLoadOlder = e => {
e.preventDefault(); e.preventDefault();
this.handleScrollToBottom(); this.handleScrollToBottom();
} }
handleOpenMedia = attachment => {
if (attachment.get('type') === 'video') {
this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status') }));
} else {
const media = attachment.getIn(['status', 'media_attachments']);
const index = media.findIndex(x => x.get('id') === attachment.get('id'));
this.props.dispatch(openModal('MEDIA', { media, index, status: attachment.get('status') }));
}
}
handleRef = c => {
if (c) {
this.setState({ width: c.offsetWidth });
}
}
render () { render () {
const { medias, shouldUpdateScroll, isLoading, hasMore, isAccount } = this.props; const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount } = this.props;
const { width } = this.state;
if (!isAccount) { if (!isAccount) {
return ( return (
@ -104,9 +127,7 @@ class AccountGallery extends ImmutablePureComponent {
); );
} }
let loadOlder = null; if (!attachments && isLoading) {
if (!medias && isLoading) {
return ( return (
<Column> <Column>
<LoadingIndicator /> <LoadingIndicator />
@ -114,7 +135,9 @@ class AccountGallery extends ImmutablePureComponent {
); );
} }
if (hasMore && !(isLoading && medias.size === 0)) { let loadOlder = null;
if (hasMore && !(isLoading && attachments.size === 0)) {
loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />; loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
} }
@ -126,23 +149,17 @@ class AccountGallery extends ImmutablePureComponent {
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}> <div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
<HeaderContainer accountId={this.props.params.accountId} /> <HeaderContainer accountId={this.props.params.accountId} />
<div role='feed' className='account-gallery__container'> <div role='feed' className='account-gallery__container' ref={this.handleRef}>
{medias.map((media, index) => media === null ? ( {attachments.map((attachment, index) => attachment === null ? (
<LoadMoreMedia <LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
key={'more:' + medias.getIn(index + 1, 'id')}
maxId={index > 0 ? medias.getIn(index - 1, 'id') : null}
onLoadMore={this.handleLoadMore}
/>
) : ( ) : (
<MediaItem <MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
key={media.get('id')}
media={media}
/>
))} ))}
{loadOlder} {loadOlder}
</div> </div>
{isLoading && medias.size === 0 && ( {isLoading && attachments.size === 0 && (
<div className='scrollable__append'> <div className='scrollable__append'>
<LoadingIndicator /> <LoadingIndicator />
</div> </div>

View File

@ -11,7 +11,6 @@ import { defineMessages, injectIntl } from 'react-intl';
import SpoilerButtonContainer from '../containers/spoiler_button_container'; import SpoilerButtonContainer from '../containers/spoiler_button_container';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
import FederationDropdownContainer from '../containers/federation_dropdown_container'; import FederationDropdownContainer from '../containers/federation_dropdown_container';
import SensitiveButtonContainer from '../containers/sensitive_button_container';
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container'; import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
import PollFormContainer from '../containers/poll_form_container'; import PollFormContainer from '../containers/poll_form_container';
import UploadFormContainer from '../containers/upload_form_container'; import UploadFormContainer from '../containers/upload_form_container';
@ -41,18 +40,17 @@ class ComposeForm extends ImmutablePureComponent {
static propTypes = { static propTypes = {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
text: PropTypes.string.isRequired, text: PropTypes.string.isRequired,
suggestion_token: PropTypes.string,
suggestions: ImmutablePropTypes.list, suggestions: ImmutablePropTypes.list,
spoiler: PropTypes.bool, spoiler: PropTypes.bool,
privacy: PropTypes.string, privacy: PropTypes.string,
federation: PropTypes.bool, federation: PropTypes.bool,
spoiler_text: PropTypes.string, spoilerText: PropTypes.string,
focusDate: PropTypes.instanceOf(Date), focusDate: PropTypes.instanceOf(Date),
caretPosition: PropTypes.number, caretPosition: PropTypes.number,
preselectDate: PropTypes.instanceOf(Date), preselectDate: PropTypes.instanceOf(Date),
is_submitting: PropTypes.bool, isSubmitting: PropTypes.bool,
is_changing_upload: PropTypes.bool, isChangingUpload: PropTypes.bool,
is_uploading: PropTypes.bool, isUploading: PropTypes.bool,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
onClearSuggestions: PropTypes.func.isRequired, onClearSuggestions: PropTypes.func.isRequired,
@ -87,10 +85,10 @@ class ComposeForm extends ImmutablePureComponent {
} }
// Submit disabled: // Submit disabled:
const { is_submitting, is_changing_upload, is_uploading, anyMedia } = this.props; const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props;
const fulltext = [this.props.spoiler_text, countableText(this.props.text)].join(''); const fulltext = [this.props.spoilerText, countableText(this.props.text)].join('');
if (is_submitting || is_uploading || is_changing_upload || length(fulltext) > 500 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) { if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > 500 || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
return; return;
} }
@ -135,7 +133,7 @@ class ComposeForm extends ImmutablePureComponent {
this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd); this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
this.autosuggestTextarea.textarea.focus(); this.autosuggestTextarea.textarea.focus();
} else if(prevProps.is_submitting && !this.props.is_submitting) { } else if(prevProps.isSubmitting && !this.props.isSubmitting) {
this.autosuggestTextarea.textarea.focus(); this.autosuggestTextarea.textarea.focus();
} else if (this.props.spoiler !== prevProps.spoiler) { } else if (this.props.spoiler !== prevProps.spoiler) {
if (this.props.spoiler) { if (this.props.spoiler) {
@ -164,9 +162,9 @@ class ComposeForm extends ImmutablePureComponent {
render () { render () {
const { intl, onPaste, showSearch, anyMedia } = this.props; const { intl, onPaste, showSearch, anyMedia } = this.props;
const disabled = this.props.is_submitting; const disabled = this.props.isSubmitting;
const text = [this.props.spoiler_text, countableText(this.props.text)].join(''); const text = [this.props.spoilerText, countableText(this.props.text)].join('');
const disabledButton = disabled || this.props.is_uploading || this.props.is_changing_upload || length(text) > 500 || (text.length !== 0 && text.trim().length === 0 && !anyMedia); const disabledButton = disabled || this.props.isUploading || this.props.isChangingUpload || length(text) > 500 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
let publishText = ''; let publishText = '';
if (this.props.privacy === 'private' || this.props.privacy === 'direct') { if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
@ -184,7 +182,7 @@ class ComposeForm extends ImmutablePureComponent {
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}> <div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}>
<label> <label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span> <span style={{ display: 'none' }}>{intl.formatMessage(messages.spoiler_placeholder)}</span>
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} tabIndex={this.props.spoiler ? 0 : -1} type='text' className='spoiler-input__input' id='cw-spoiler-input' ref={this.setSpoilerText} /> <input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoilerText} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} tabIndex={this.props.spoiler ? 0 : -1} type='text' className='spoiler-input__input' id='cw-spoiler-input' ref={this.setSpoilerText} />
</label> </label>
</div> </div>
@ -217,7 +215,6 @@ class ComposeForm extends ImmutablePureComponent {
<UploadButtonContainer /> <UploadButtonContainer />
<PollButtonContainer /> <PollButtonContainer />
<PrivacyDropdownContainer /> <PrivacyDropdownContainer />
<SensitiveButtonContainer />
<SpoilerButtonContainer /> <SpoilerButtonContainer />
<FederationDropdownContainer /> <FederationDropdownContainer />
</div> </div>

View File

@ -26,6 +26,7 @@ class Option extends React.PureComponent {
isPollMultiple: PropTypes.bool, isPollMultiple: PropTypes.bool,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired, onRemove: PropTypes.func.isRequired,
onToggleMultiple: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -37,13 +38,24 @@ class Option extends React.PureComponent {
this.props.onRemove(this.props.index); this.props.onRemove(this.props.index);
}; };
handleToggleMultiple = e => {
this.props.onToggleMultiple();
e.preventDefault();
e.stopPropagation();
};
render () { render () {
const { isPollMultiple, title, index, intl } = this.props; const { isPollMultiple, title, index, intl } = this.props;
return ( return (
<li> <li>
<label className='poll__text editable'> <label className='poll__text editable'>
<span className={classNames('poll__input', { checkbox: isPollMultiple })} /> <span
className={classNames('poll__input', { checkbox: isPollMultiple })}
onClick={this.handleToggleMultiple}
role='button'
tabIndex='0'
/>
<input <input
type='text' type='text'
@ -86,6 +98,10 @@ class PollForm extends ImmutablePureComponent {
this.props.onChangeSettings(e.target.value, this.props.isMultiple); this.props.onChangeSettings(e.target.value, this.props.isMultiple);
}; };
handleToggleMultiple = () => {
this.props.onChangeSettings(this.props.expiresIn, !this.props.isMultiple);
};
render () { render () {
const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl } = this.props; const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl } = this.props;
@ -96,7 +112,7 @@ class PollForm extends ImmutablePureComponent {
return ( return (
<div className='compose-form__poll-wrapper'> <div className='compose-form__poll-wrapper'>
<ul> <ul>
{options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} />)} {options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} />)}
</ul> </ul>
<div className='poll__footer'> <div className='poll__footer'>

View File

@ -73,7 +73,7 @@ class Search extends React.PureComponent {
} }
} }
handleKeyDown = (e) => { handleKeyUp = (e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
this.props.onSubmit(); this.props.onSubmit();
@ -82,10 +82,6 @@ class Search extends React.PureComponent {
} }
} }
noop () {
}
handleFocus = () => { handleFocus = () => {
this.setState({ expanded: true }); this.setState({ expanded: true });
this.props.onShow(); this.props.onShow();
@ -110,7 +106,7 @@ class Search extends React.PureComponent {
placeholder={intl.formatMessage(messages.placeholder)} placeholder={intl.formatMessage(messages.placeholder)}
value={value} value={value}
onChange={this.handleChange} onChange={this.handleChange}
onKeyUp={this.handleKeyDown} onKeyUp={this.handleKeyUp}
onFocus={this.handleFocus} onFocus={this.handleFocus}
onBlur={this.handleBlur} onBlur={this.handleBlur}
/> />

View File

@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import UploadProgressContainer from '../containers/upload_progress_container'; import UploadProgressContainer from '../containers/upload_progress_container';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import UploadContainer from '../containers/upload_container'; import UploadContainer from '../containers/upload_container';
import SensitiveButtonContainer from '../containers/sensitive_button_container';
export default class UploadForm extends ImmutablePureComponent { export default class UploadForm extends ImmutablePureComponent {
@ -22,6 +23,8 @@ export default class UploadForm extends ImmutablePureComponent {
<UploadContainer id={id} key={id} /> <UploadContainer id={id} key={id} />
))} ))}
</div> </div>
{!mediaIds.isEmpty() && <SensitiveButtonContainer />}
</div> </div>
); );
} }

View File

@ -1,6 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ComposeForm from '../components/compose_form'; import ComposeForm from '../components/compose_form';
import { uploadCompose } from '../../../actions/compose';
import { import {
changeCompose, changeCompose,
submitCompose, submitCompose,
@ -9,22 +8,22 @@ import {
selectComposeSuggestion, selectComposeSuggestion,
changeComposeSpoilerText, changeComposeSpoilerText,
insertEmojiCompose, insertEmojiCompose,
uploadCompose,
} from '../../../actions/compose'; } from '../../../actions/compose';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
text: state.getIn(['compose', 'text']), text: state.getIn(['compose', 'text']),
suggestion_token: state.getIn(['compose', 'suggestion_token']),
suggestions: state.getIn(['compose', 'suggestions']), suggestions: state.getIn(['compose', 'suggestions']),
spoiler: state.getIn(['compose', 'spoiler']), spoiler: state.getIn(['compose', 'spoiler']),
spoiler_text: state.getIn(['compose', 'spoiler_text']), spoilerText: state.getIn(['compose', 'spoiler_text']),
privacy: state.getIn(['compose', 'privacy']), privacy: state.getIn(['compose', 'privacy']),
federation: state.getIn(['compose', 'federation']), federation: state.getIn(['compose', 'federation']),
focusDate: state.getIn(['compose', 'focusDate']), focusDate: state.getIn(['compose', 'focusDate']),
caretPosition: state.getIn(['compose', 'caretPosition']), caretPosition: state.getIn(['compose', 'caretPosition']),
preselectDate: state.getIn(['compose', 'preselectDate']), preselectDate: state.getIn(['compose', 'preselectDate']),
is_submitting: state.getIn(['compose', 'is_submitting']), is_submitting: state.getIn(['compose', 'is_submitting']),
is_changing_upload: state.getIn(['compose', 'is_changing_upload']), isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
is_uploading: state.getIn(['compose', 'is_uploading']), isUploading: state.getIn(['compose', 'is_uploading']),
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0, anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
}); });
@ -47,8 +46,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(fetchComposeSuggestions(token)); dispatch(fetchComposeSuggestions(token));
}, },
onSuggestionSelected (position, token, accountId) { onSuggestionSelected (position, token, suggestion) {
dispatch(selectComposeSuggestion(position, token, accountId)); dispatch(selectComposeSuggestion(position, token, suggestion));
}, },
onChangeSpoilerText (checked) { onChangeSpoilerText (checked) {

View File

@ -2,11 +2,9 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import IconButton from '../../../components/icon_button'; import { changeComposeSensitivity } from 'mastodon/actions/compose';
import { changeComposeSensitivity } from '../../../actions/compose'; import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
import Motion from '../../ui/util/optional_motion'; import Icon from 'mastodon/components/icon';
import spring from 'react-motion/lib/spring';
import { injectIntl, defineMessages } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages({
marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' }, marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' },
@ -14,7 +12,6 @@ const messages = defineMessages({
}); });
const mapStateToProps = state => ({ const mapStateToProps = state => ({
visible: state.getIn(['compose', 'media_attachments']).size > 0,
active: state.getIn(['compose', 'sensitive']), active: state.getIn(['compose', 'sensitive']),
disabled: state.getIn(['compose', 'spoiler']), disabled: state.getIn(['compose', 'spoiler']),
}); });
@ -30,7 +27,6 @@ const mapDispatchToProps = dispatch => ({
class SensitiveButton extends React.PureComponent { class SensitiveButton extends React.PureComponent {
static propTypes = { static propTypes = {
visible: PropTypes.bool,
active: PropTypes.bool, active: PropTypes.bool,
disabled: PropTypes.bool, disabled: PropTypes.bool,
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
@ -38,32 +34,14 @@ class SensitiveButton extends React.PureComponent {
}; };
render () { render () {
const { visible, active, disabled, onClick, intl } = this.props; const { active, disabled, onClick, intl } = this.props;
return ( return (
<Motion defaultStyle={{ scale: 0.87 }} style={{ scale: spring(visible ? 1 : 0.87, { stiffness: 200, damping: 3 }) }}> <div className='compose-form__sensitive-button'>
{({ scale }) => { <button className={classNames('icon-button', { active })} onClick={onClick} disabled={disabled} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}>
const icon = active ? 'eye-slash' : 'eye'; <Icon id='eye-slash' /> <FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' />
const className = classNames('compose-form__sensitive-button', { </button>
'compose-form__sensitive-button--visible': visible, </div>
});
return (
<div className={className} style={{ transform: `scale(${scale})` }}>
<IconButton
className='compose-form__sensitive-button__icon'
title={intl.formatMessage(active ? messages.marked : messages.unmarked)}
icon={icon}
onClick={onClick}
size={18}
active={active}
disabled={disabled}
style={{ lineHeight: null, height: null }}
inverted
/>
</div>
);
}}
</Motion>
); );
} }

View File

@ -20,18 +20,24 @@ export default class ConversationsList extends ImmutablePureComponent {
handleMoveUp = id => { handleMoveUp = id => {
const elementIndex = this.getCurrentIndex(id) - 1; const elementIndex = this.getCurrentIndex(id) - 1;
this._selectChild(elementIndex); this._selectChild(elementIndex, true);
} }
handleMoveDown = id => { handleMoveDown = id => {
const elementIndex = this.getCurrentIndex(id) + 1; const elementIndex = this.getCurrentIndex(id) + 1;
this._selectChild(elementIndex); this._selectChild(elementIndex, false);
} }
_selectChild (index) { _selectChild (index, align_top) {
const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`); const container = this.node.node;
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
if (element) { if (element) {
if (align_top && container.scrollTop > element.offsetTop) {
element.scrollIntoView(true);
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
element.scrollIntoView(false);
}
element.focus(); element.focus();
} }
} }

View File

@ -7,7 +7,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { me, invitesEnabled, version, profile_directory } from '../../initial_state'; import { me, invitesEnabled, version, profile_directory, repository, source_url } from '../../initial_state';
import { fetchFollowRequests } from '../../actions/accounts'; import { fetchFollowRequests } from '../../actions/accounts';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@ -172,7 +172,7 @@ class GettingStarted extends ImmutablePureComponent {
<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='https://github.com/tootsuite/mastodon' rel='noopener' target='_blank'>tootsuite/mastodon</a> (v{version})</span> }} values={{ github: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
/> />
</p> </p>
</div> </div>

View File

@ -113,18 +113,24 @@ class Notifications extends React.PureComponent {
handleMoveUp = id => { handleMoveUp = id => {
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1; const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
this._selectChild(elementIndex); this._selectChild(elementIndex, true);
} }
handleMoveDown = id => { handleMoveDown = id => {
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1; const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
this._selectChild(elementIndex); this._selectChild(elementIndex, false);
} }
_selectChild (index) { _selectChild (index, align_top) {
const element = this.column.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`); const container = this.column.node;
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
if (element) { if (element) {
if (align_top && container.scrollTop > element.offsetTop) {
element.scrollIntoView(true);
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
element.scrollIntoView(false);
}
element.focus(); element.focus();
} }
} }

View File

@ -35,6 +35,7 @@ export default class StatusCheckBox extends React.PureComponent {
{Component => ( {Component => (
<Component <Component
preview={video.get('preview_url')} preview={video.get('preview_url')}
blurhash={video.get('blurhash')}
src={video.get('url')} src={video.get('url')}
alt={video.get('description')} alt={video.get('description')}
width={239} width={239}

View File

@ -5,7 +5,6 @@ import Avatar from '../../../components/avatar';
import DisplayName from '../../../components/display_name'; import DisplayName from '../../../components/display_name';
import StatusContent from '../../../components/status_content'; import StatusContent from '../../../components/status_content';
import MediaGallery from '../../../components/media_gallery'; import MediaGallery from '../../../components/media_gallery';
import AttachmentList from '../../../components/attachment_list';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { defineMessages, injectIntl, FormattedDate, FormattedNumber } from 'react-intl'; import { defineMessages, injectIntl, FormattedDate, FormattedNumber } from 'react-intl';
import Card from './card'; import Card from './card';
@ -116,14 +115,13 @@ export default class DetailedStatus extends ImmutablePureComponent {
if (status.get('poll')) { if (status.get('poll')) {
media = <PollContainer pollId={status.get('poll')} />; media = <PollContainer pollId={status.get('poll')} />;
} else if (status.get('media_attachments').size > 0) { } else if (status.get('media_attachments').size > 0) {
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
media = <AttachmentList media={status.get('media_attachments')} />;
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const video = status.getIn(['media_attachments', 0]); const video = status.getIn(['media_attachments', 0]);
media = ( media = (
<Video <Video
preview={video.get('preview_url')} preview={video.get('preview_url')}
blurhash={video.get('blurhash')}
src={video.get('url')} src={video.get('url')}
alt={video.get('description')} alt={video.get('description')}
width={300} width={300}

View File

@ -316,15 +316,15 @@ class Status extends ImmutablePureComponent {
const { status, ancestorsIds, descendantsIds } = this.props; const { status, ancestorsIds, descendantsIds } = this.props;
if (id === status.get('id')) { if (id === status.get('id')) {
this._selectChild(ancestorsIds.size - 1); this._selectChild(ancestorsIds.size - 1, true);
} else { } else {
let index = ancestorsIds.indexOf(id); let index = ancestorsIds.indexOf(id);
if (index === -1) { if (index === -1) {
index = descendantsIds.indexOf(id); index = descendantsIds.indexOf(id);
this._selectChild(ancestorsIds.size + index); this._selectChild(ancestorsIds.size + index, true);
} else { } else {
this._selectChild(index - 1); this._selectChild(index - 1, true);
} }
} }
} }
@ -333,23 +333,29 @@ class Status extends ImmutablePureComponent {
const { status, ancestorsIds, descendantsIds } = this.props; const { status, ancestorsIds, descendantsIds } = this.props;
if (id === status.get('id')) { if (id === status.get('id')) {
this._selectChild(ancestorsIds.size + 1); this._selectChild(ancestorsIds.size + 1, false);
} else { } else {
let index = ancestorsIds.indexOf(id); let index = ancestorsIds.indexOf(id);
if (index === -1) { if (index === -1) {
index = descendantsIds.indexOf(id); index = descendantsIds.indexOf(id);
this._selectChild(ancestorsIds.size + index + 2); this._selectChild(ancestorsIds.size + index + 2, false);
} else { } else {
this._selectChild(index + 1); this._selectChild(index + 1, false);
} }
} }
} }
_selectChild (index) { _selectChild (index, align_top) {
const element = this.node.querySelectorAll('.focusable')[index]; const container = this.node;
const element = container.querySelectorAll('.focusable')[index];
if (element) { if (element) {
if (align_top && container.scrollTop > element.offsetTop) {
element.scrollIntoView(true);
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
element.scrollIntoView(false);
}
element.focus(); element.focus();
} }
} }

View File

@ -64,7 +64,7 @@ export default class ActionsModal extends ImmutablePureComponent {
<div className='modal-root__modal actions-modal'> <div className='modal-root__modal actions-modal'>
{status} {status}
<ul> <ul className={classNames({ 'with-status': !!status })}>
{this.props.actions.map(this.renderAction)} {this.props.actions.map(this.renderAction)}
</ul> </ul>
</div> </div>

View File

@ -2,11 +2,11 @@ import React from 'react';
import ReactSwipeableViews from 'react-swipeable-views'; 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 '../../video'; import Video from 'mastodon/features/video';
import ExtendedVideoPlayer from '../../../components/extended_video_player'; import ExtendedVideoPlayer from 'mastodon/components/extended_video_player';
import classNames from 'classnames'; import classNames from 'classnames';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import IconButton from '../../../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';
@ -24,6 +24,7 @@ class MediaModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.list.isRequired, media: ImmutablePropTypes.list.isRequired,
status: ImmutablePropTypes.map,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
@ -72,9 +73,12 @@ class MediaModal extends ImmutablePureComponent {
componentDidMount () { componentDidMount () {
window.addEventListener('keydown', this.handleKeyDown, false); window.addEventListener('keydown', this.handleKeyDown, false);
if (this.context.router) { if (this.context.router) {
const history = this.context.router.history; const history = this.context.router.history;
history.push(history.location.pathname, previewState); history.push(history.location.pathname, previewState);
this.unlistenHistory = history.listen(() => { this.unlistenHistory = history.listen(() => {
this.props.onClose(); this.props.onClose();
}); });
@ -83,6 +87,7 @@ class MediaModal extends ImmutablePureComponent {
componentWillUnmount () { componentWillUnmount () {
window.removeEventListener('keydown', this.handleKeyDown); window.removeEventListener('keydown', this.handleKeyDown);
if (this.context.router) { if (this.context.router) {
this.unlistenHistory(); this.unlistenHistory();
@ -102,8 +107,15 @@ class MediaModal extends ImmutablePureComponent {
})); }));
}; };
handleStatusClick = e => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
}
}
render () { render () {
const { media, intl, onClose } = this.props; const { media, status, intl, onClose } = this.props;
const { navigationHidden } = this.state; const { navigationHidden } = this.state;
const index = this.getIndex(); const index = this.getIndex();
@ -144,6 +156,7 @@ class MediaModal extends ImmutablePureComponent {
return ( return (
<Video <Video
preview={image.get('preview_url')} preview={image.get('preview_url')}
blurhash={image.get('blurhash')}
src={image.get('url')} src={image.get('url')}
width={image.get('width')} width={image.get('width')}
height={image.get('height')} height={image.get('height')}
@ -206,10 +219,19 @@ class MediaModal extends ImmutablePureComponent {
{content} {content}
</ReactSwipeableViews> </ReactSwipeableViews>
</div> </div>
<div className={navigationClassName}> <div className={navigationClassName}>
<IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={40} /> <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={40} />
{leftNav} {leftNav}
{rightNav} {rightNav}
{status && (
<div className={classNames('media-modal__meta', { 'media-modal__meta--shifted': media.size > 1 })}>
<a href={status.get('url')} onClick={this.handleStatusClick}><FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
</div>
)}
<ul className='media-modal__pagination'> <ul className='media-modal__pagination'>
{pagination} {pagination}
</ul> </ul>

View File

@ -1,28 +1,69 @@
import React from 'react'; import React from 'react';
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 '../../video'; import Video from 'mastodon/features/video';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
export const previewState = 'previewVideoModal';
export default class VideoModal extends ImmutablePureComponent { export default class VideoModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.map.isRequired, media: ImmutablePropTypes.map.isRequired,
status: ImmutablePropTypes.map,
time: PropTypes.number, time: PropTypes.number,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
static contextTypes = {
router: PropTypes.object,
};
componentDidMount () {
if (this.context.router) {
const history = this.context.router.history;
history.push(history.location.pathname, previewState);
this.unlistenHistory = history.listen(() => {
this.props.onClose();
});
}
}
componentWillUnmount () {
if (this.context.router) {
this.unlistenHistory();
if (this.context.router.history.location.state === previewState) {
this.context.router.history.goBack();
}
}
}
handleStatusClick = e => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
}
}
render () { render () {
const { media, time, onClose } = this.props; const { media, status, time, onClose } = this.props;
const link = status && <a href={status.get('url')} onClick={this.handleStatusClick}><FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>;
return ( return (
<div className='modal-root__modal video-modal'> <div className='modal-root__modal video-modal'>
<div> <div>
<Video <Video
preview={media.get('preview_url')} preview={media.get('preview_url')}
blurhash={media.get('blurhash')}
src={media.get('url')} src={media.get('url')}
startTime={time} startTime={time}
onCloseVideo={onClose} onCloseVideo={onClose}
link={link}
detailed detailed
alt={media.get('description')} alt={media.get('description')}
/> />

View File

@ -47,7 +47,8 @@ import {
Lists, Lists,
} from './util/async-components'; } from './util/async-components';
import { me } from '../../initial_state'; import { me } from '../../initial_state';
import { previewState } from './components/media_modal'; import { previewState as previewMediaState } from './components/media_modal';
import { previewState as previewVideoState } from './components/video_modal';
// Dummy import, to make sure that <Status /> ends up in the application bundle. // Dummy import, to make sure that <Status /> ends up in the application bundle.
// Without this it ends up in ~8 very commonly used bundles. // Without this it ends up in ~8 very commonly used bundles.
@ -121,7 +122,7 @@ class SwitchingColumnsArea extends React.PureComponent {
} }
shouldUpdateScroll (_, { location }) { shouldUpdateScroll (_, { location }) {
return location.state !== previewState; return location.state !== previewMediaState && location.state !== previewVideoState;
} }
handleResize = debounce(() => { handleResize = debounce(() => {
@ -367,11 +368,16 @@ class UI extends React.PureComponent {
handleHotkeyFocusColumn = e => { handleHotkeyFocusColumn = e => {
const index = (e.key * 1) + 1; // First child is drawer, skip that const index = (e.key * 1) + 1; // First child is drawer, skip that
const column = this.node.querySelector(`.column:nth-child(${index})`); const column = this.node.querySelector(`.column:nth-child(${index})`);
if (!column) return;
const container = column.querySelector('.scrollable');
if (column) { if (container) {
const status = column.querySelector('.focusable'); const status = container.querySelector('.focusable');
if (status) { if (status) {
if (container.scrollTop > status.offsetTop) {
status.scrollIntoView(true);
}
status.focus(); status.focus();
} }
} }

View File

@ -7,6 +7,7 @@ import classNames from 'classnames';
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
import { displayMedia } from '../../initial_state'; import { displayMedia } from '../../initial_state';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import { decode } from 'blurhash';
const messages = defineMessages({ const messages = defineMessages({
play: { id: 'video.play', defaultMessage: 'Play' }, play: { id: 'video.play', defaultMessage: 'Play' },
@ -102,6 +103,8 @@ class Video extends React.PureComponent {
inline: PropTypes.bool, inline: PropTypes.bool,
cacheWidth: PropTypes.func, cacheWidth: PropTypes.func,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
blurhash: PropTypes.string,
link: PropTypes.node,
}; };
state = { state = {
@ -139,6 +142,7 @@ class Video extends React.PureComponent {
setVideoRef = c => { setVideoRef = c => {
this.video = c; this.video = c;
if (this.video) { if (this.video) {
this.setState({ volume: this.video.volume, muted: this.video.muted }); this.setState({ volume: this.video.volume, muted: this.video.muted });
} }
@ -152,6 +156,10 @@ class Video extends React.PureComponent {
this.volume = c; this.volume = c;
} }
setCanvasRef = c => {
this.canvas = c;
}
handleClickRoot = e => e.stopPropagation(); handleClickRoot = e => e.stopPropagation();
handlePlay = () => { handlePlay = () => {
@ -170,7 +178,6 @@ class Video extends React.PureComponent {
} }
handleVolumeMouseDown = e => { handleVolumeMouseDown = e => {
document.addEventListener('mousemove', this.handleMouseVolSlide, true); document.addEventListener('mousemove', this.handleMouseVolSlide, true);
document.addEventListener('mouseup', this.handleVolumeMouseUp, true); document.addEventListener('mouseup', this.handleVolumeMouseUp, true);
document.addEventListener('touchmove', this.handleMouseVolSlide, true); document.addEventListener('touchmove', this.handleMouseVolSlide, true);
@ -190,7 +197,6 @@ class Video extends React.PureComponent {
} }
handleMouseVolSlide = throttle(e => { handleMouseVolSlide = throttle(e => {
const rect = this.volume.getBoundingClientRect(); const rect = this.volume.getBoundingClientRect();
const x = (e.clientX - rect.left) / this.volWidth; //x position within the element. const x = (e.clientX - rect.left) / this.volWidth; //x position within the element.
@ -261,6 +267,10 @@ class Video extends React.PureComponent {
document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true); document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true); document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true); document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
if (this.props.blurhash) {
this._decode();
}
} }
componentWillUnmount () { componentWillUnmount () {
@ -270,6 +280,24 @@ class Video extends React.PureComponent {
document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true); document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
} }
componentDidUpdate (prevProps) {
if (prevProps.blurhash !== this.props.blurhash && this.props.blurhash) {
this._decode();
}
}
_decode () {
const hash = this.props.blurhash;
const pixels = decode(hash, 32, 32);
if (pixels) {
const ctx = this.canvas.getContext('2d');
const imageData = new ImageData(pixels, 32, 32);
ctx.putImageData(imageData, 0, 0);
}
}
handleFullscreenChange = () => { handleFullscreenChange = () => {
this.setState({ fullscreen: isFullscreen() }); this.setState({ fullscreen: isFullscreen() });
} }
@ -314,6 +342,7 @@ class Video extends React.PureComponent {
handleOpenVideo = () => { handleOpenVideo = () => {
const { src, preview, width, height, alt } = this.props; const { src, preview, width, height, alt } = this.props;
const media = fromJS({ const media = fromJS({
type: 'video', type: 'video',
url: src, url: src,
@ -333,7 +362,7 @@ class Video extends React.PureComponent {
} }
render () { render () {
const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive } = this.props; const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link } = this.props;
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
const progress = (currentTime / duration) * 100; const progress = (currentTime / duration) * 100;
@ -351,6 +380,7 @@ class Video extends React.PureComponent {
} }
let preload; let preload;
if (startTime || fullscreen || dragging) { if (startTime || fullscreen || dragging) {
preload = 'auto'; preload = 'auto';
} else if (detailed) { } else if (detailed) {
@ -360,6 +390,7 @@ class Video extends React.PureComponent {
} }
let warning; let warning;
if (sensitive) { if (sensitive) {
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />; warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
} else { } else {
@ -377,7 +408,9 @@ class Video extends React.PureComponent {
onClick={this.handleClickRoot} onClick={this.handleClickRoot}
tabIndex={0} tabIndex={0}
> >
<video <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': revealed })} />
{revealed && <video
ref={this.setVideoRef} ref={this.setVideoRef}
src={src} src={src}
poster={preview} poster={preview}
@ -397,12 +430,13 @@ class Video extends React.PureComponent {
onLoadedData={this.handleLoadedData} onLoadedData={this.handleLoadedData}
onProgress={this.handleProgress} onProgress={this.handleProgress}
onVolumeChange={this.handleVolumeChange} onVolumeChange={this.handleVolumeChange}
/> />}
<button type='button' className={classNames('video-player__spoiler', { active: !revealed })} onClick={this.toggleReveal}> <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed })}>
<span className='video-player__spoiler__title'>{warning}</span> <button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}>
<span className='video-player__spoiler__subtitle'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> <span className='spoiler-button__overlay__label'>{warning}</span>
</button> </button>
</div>
<div className={classNames('video-player__controls', { active: paused || hovered })}> <div className={classNames('video-player__controls', { active: paused || hovered })}>
<div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}> <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
@ -420,6 +454,7 @@ class Video extends React.PureComponent {
<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}><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}>
<div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} /> <div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
<span <span
@ -429,17 +464,19 @@ class Video extends React.PureComponent {
/> />
</div> </div>
{(detailed || fullscreen) && {(detailed || fullscreen) && (
<span> <span>
<span className='video-player__time-current'>{formatTime(currentTime)}</span> <span className='video-player__time-current'>{formatTime(currentTime)}</span>
<span className='video-player__time-sep'>/</span> <span className='video-player__time-sep'>/</span>
<span className='video-player__time-total'>{formatTime(duration)}</span> <span className='video-player__time-total'>{formatTime(duration)}</span>
</span> </span>
} )}
{link && <span className='video-player__link'>{link}</span>}
</div> </div>
<div className='video-player__buttons right'> <div className='video-player__buttons right'>
{!onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye' fixedWidth /></button>} {!onCloseVideo && <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(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>

View File

@ -13,6 +13,8 @@ export const deleteModal = getMeta('delete_modal');
export const me = getMeta('me'); export const me = getMeta('me');
export const searchEnabled = getMeta('search_enabled'); export const searchEnabled = getMeta('search_enabled');
export const invitesEnabled = getMeta('invites_enabled'); export const invitesEnabled = getMeta('invites_enabled');
export const repository = getMeta('repository');
export const source_url = getMeta('source_url');
export const version = getMeta('version'); export const version = getMeta('version');
export const mascot = getMeta('mascot'); export const mascot = getMeta('mascot');
export const profile_directory = getMeta('profile_directory'); export const profile_directory = getMeta('profile_directory');

View File

@ -77,6 +77,7 @@
"compose_form.poll.remove_option": "Odstranit tuto volbu", "compose_form.poll.remove_option": "Odstranit tuto volbu",
"compose_form.publish": "Tootnout", "compose_form.publish": "Tootnout",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.hide": "Označit média jako citlivá",
"compose_form.sensitive.marked": "Média jsou označena jako citlivá", "compose_form.sensitive.marked": "Média jsou označena jako citlivá",
"compose_form.sensitive.unmarked": "Média nejsou označena jako citlivá", "compose_form.sensitive.unmarked": "Média nejsou označena jako citlivá",
"compose_form.spoiler.marked": "Text je skrytý za varováním", "compose_form.spoiler.marked": "Text je skrytý za varováním",
@ -214,6 +215,7 @@
"lightbox.close": "Zavřít", "lightbox.close": "Zavřít",
"lightbox.next": "Další", "lightbox.next": "Další",
"lightbox.previous": "Předchozí", "lightbox.previous": "Předchozí",
"lightbox.view_context": "Zobrazit kontext",
"lists.account.add": "Přidat do seznamu", "lists.account.add": "Přidat do seznamu",
"lists.account.remove": "Odebrat ze seznamu", "lists.account.remove": "Odebrat ze seznamu",
"lists.delete": "Smazat seznam", "lists.delete": "Smazat seznam",

View File

@ -0,0 +1,384 @@
{
"account.add_or_remove_from_list": "Add or Remove from lists",
"account.badges.bot": "Bot",
"account.block": "Block @{name}",
"account.block_domain": "Hide everything from {domain}",
"account.blocked": "Blocked",
"account.direct": "Direct message @{name}",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Edit profile",
"account.endorse": "Feature on profile",
"account.follow": "Follow",
"account.followers": "Followers",
"account.followers.empty": "No one follows this user yet.",
"account.follows": "Follows",
"account.follows.empty": "This user doesn't follow anyone yet.",
"account.follows_you": "Follows you",
"account.hide_reblogs": "Hide boosts from @{name}",
"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.media": "Media",
"account.mention": "Mention @{name}",
"account.moved_to": "{name} has moved to:",
"account.mute": "Mute @{name}",
"account.mute_notifications": "Mute notifications from @{name}",
"account.muted": "Muted",
"account.posts": "Toots",
"account.posts_with_replies": "Toots and replies",
"account.report": "Report @{name}",
"account.requested": "Awaiting approval. Click to cancel follow request",
"account.share": "Share @{name}'s profile",
"account.show_reblogs": "Show boosts from @{name}",
"account.unblock": "Unblock @{name}",
"account.unblock_domain": "Unhide {domain}",
"account.unendorse": "Don't feature on profile",
"account.unfollow": "Unfollow",
"account.unmute": "Unmute @{name}",
"account.unmute_notifications": "Unmute notifications from @{name}",
"alert.unexpected.message": "An unexpected error occurred.",
"alert.unexpected.title": "Oops!",
"boost_modal.combo": "You can press {combo} to skip this next time",
"bundle_column_error.body": "Something went wrong while loading this component.",
"bundle_column_error.retry": "Try again",
"bundle_column_error.title": "Network error",
"bundle_modal_error.close": "Close",
"bundle_modal_error.message": "Something went wrong while loading this component.",
"bundle_modal_error.retry": "Try again",
"column.blocks": "Blocked users",
"column.community": "Local timeline",
"column.direct": "Direct messages",
"column.domain_blocks": "Hidden domains",
"column.favourites": "Favourites",
"column.follow_requests": "Follow requests",
"column.home": "Home",
"column.lists": "Lists",
"column.mutes": "Muted users",
"column.notifications": "Notifications",
"column.pins": "Pinned toot",
"column.public": "Federated timeline",
"column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.settings": "Settings",
"community.column_settings.media_only": "Media Only",
"compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.",
"compose_form.direct_message_warning_learn_more": "Learn more",
"compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
"compose_form.lock_disclaimer.lock": "locked",
"compose_form.placeholder": "What is on your mind?",
"compose_form.poll.add_option": "Add a choice",
"compose_form.poll.duration": "Poll duration",
"compose_form.poll.option_placeholder": "Choice {number}",
"compose_form.poll.remove_option": "Remove this choice",
"compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.marked": "Media is marked as sensitive",
"compose_form.sensitive.unmarked": "Media is not marked as sensitive",
"compose_form.spoiler.marked": "Text is hidden behind warning",
"compose_form.spoiler.unmarked": "Text is not hidden",
"compose_form.spoiler_placeholder": "Write your warning here",
"confirmation_modal.cancel": "Cancel",
"confirmations.block.block_and_report": "Block & Report",
"confirmations.block.confirm": "Block",
"confirmations.block.message": "Are you sure you want to block {name}?",
"confirmations.delete.confirm": "Delete",
"confirmations.delete.message": "Are you sure you want to delete this status?",
"confirmations.delete_list.confirm": "Delete",
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
"confirmations.mute.confirm": "Mute",
"confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.redraft.confirm": "Delete & redraft",
"confirmations.redraft.message": "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.",
"confirmations.reply.confirm": "Reply",
"confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity",
"emoji_button.custom": "Custom",
"emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink",
"emoji_button.label": "Insert emoji",
"emoji_button.nature": "Nature",
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Objects",
"emoji_button.people": "People",
"emoji_button.recent": "Frequently used",
"emoji_button.search": "Search...",
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places",
"empty_column.account_timeline": "No toots here!",
"empty_column.account_unavailable": "Profile unavailable",
"empty_column.blocks": "You haven't blocked any users yet.",
"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.domain_blocks": "There are no hidden domains yet.",
"empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.",
"empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.",
"empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.",
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
"empty_column.home.public_timeline": "the public timeline",
"empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
"empty_column.mutes": "You haven't muted any users yet.",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up",
"follow_request.authorize": "Authorize",
"follow_request.reject": "Reject",
"getting_started.developers": "Developers",
"getting_started.directory": "Profile directory",
"getting_started.documentation": "Documentation",
"getting_started.heading": "Getting started",
"getting_started.invite": "Invite people",
"getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.",
"getting_started.security": "Security",
"getting_started.terms": "Terms of service",
"hashtag.column_header.tag_mode.all": "and {additional}",
"hashtag.column_header.tag_mode.any": "or {additional}",
"hashtag.column_header.tag_mode.none": "without {additional}",
"hashtag.column_settings.select.no_options_message": "No suggestions found",
"hashtag.column_settings.select.placeholder": "Enter hashtags…",
"hashtag.column_settings.tag_mode.all": "All of these",
"hashtag.column_settings.tag_mode.any": "Any of these",
"hashtag.column_settings.tag_mode.none": "None of these",
"hashtag.column_settings.tag_toggle": "Include additional tags in this column",
"home.column_settings.basic": "Basic",
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
"introduction.federation.action": "Next",
"introduction.federation.federated.headline": "Federated",
"introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
"introduction.federation.home.headline": "Home",
"introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
"introduction.federation.local.headline": "Local",
"introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
"introduction.interactions.action": "Finish toot-orial!",
"introduction.interactions.favourite.headline": "Favourite",
"introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
"introduction.interactions.reblog.headline": "Boost",
"introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
"introduction.interactions.reply.headline": "Reply",
"introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
"introduction.welcome.action": "Let's go!",
"introduction.welcome.headline": "First steps",
"introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
"keyboard_shortcuts.back": "to navigate back",
"keyboard_shortcuts.blocked": "to open blocked users list",
"keyboard_shortcuts.boost": "to boost",
"keyboard_shortcuts.column": "to focus a status in one of the columns",
"keyboard_shortcuts.compose": "to focus the compose textarea",
"keyboard_shortcuts.description": "Description",
"keyboard_shortcuts.direct": "to open direct messages column",
"keyboard_shortcuts.down": "to move down in the list",
"keyboard_shortcuts.enter": "to open status",
"keyboard_shortcuts.favourite": "to favourite",
"keyboard_shortcuts.favourites": "to open favourites list",
"keyboard_shortcuts.federated": "to open federated timeline",
"keyboard_shortcuts.heading": "Keyboard Shortcuts",
"keyboard_shortcuts.home": "to open home timeline",
"keyboard_shortcuts.hotkey": "Hotkey",
"keyboard_shortcuts.legend": "to display this legend",
"keyboard_shortcuts.local": "to open local timeline",
"keyboard_shortcuts.mention": "to mention author",
"keyboard_shortcuts.muted": "to open muted users list",
"keyboard_shortcuts.my_profile": "to open your profile",
"keyboard_shortcuts.notifications": "to open notifications column",
"keyboard_shortcuts.pinned": "to open pinned toots list",
"keyboard_shortcuts.profile": "to open author's profile",
"keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.requests": "to open follow requests list",
"keyboard_shortcuts.search": "to focus search",
"keyboard_shortcuts.start": "to open \"get started\" column",
"keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toot": "to start a brand new toot",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Close",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"lists.account.add": "Add to list",
"lists.account.remove": "Remove from list",
"lists.delete": "Delete list",
"lists.edit": "Edit list",
"lists.edit.submit": "Change title",
"lists.new.create": "Add list",
"lists.new.title_placeholder": "New list title",
"lists.search": "Search among people you follow",
"lists.subheading": "Your lists",
"loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
"missing_indicator.sublabel": "This resource could not be found",
"mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.apps": "Mobile apps",
"navigation_bar.blocks": "Blocked users",
"navigation_bar.community_timeline": "Local timeline",
"navigation_bar.compose": "Compose new toot",
"navigation_bar.direct": "Direct messages",
"navigation_bar.discover": "Discover",
"navigation_bar.domain_blocks": "Hidden domains",
"navigation_bar.edit_profile": "Edit profile",
"navigation_bar.favourites": "Favourites",
"navigation_bar.filters": "Muted words",
"navigation_bar.follow_requests": "Follow requests",
"navigation_bar.info": "About this server",
"navigation_bar.keyboard_shortcuts": "Hotkeys",
"navigation_bar.lists": "Lists",
"navigation_bar.logout": "Logout",
"navigation_bar.mutes": "Muted users",
"navigation_bar.personal": "Personal",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferences",
"navigation_bar.public_timeline": "Federated timeline",
"navigation_bar.security": "Security",
"notification.favourite": "{name} favourited your status",
"notification.follow": "{name} followed you",
"notification.mention": "{name} mentioned you",
"notification.poll": "A poll you have voted in has ended",
"notification.reblog": "{name} boosted your status",
"notifications.clear": "Clear notifications",
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
"notifications.column_settings.alert": "Desktop notifications",
"notifications.column_settings.favourite": "Favourites:",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show",
"notifications.column_settings.follow": "New followers:",
"notifications.column_settings.mention": "Mentions:",
"notifications.column_settings.poll": "Poll results:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.reblog": "Boosts:",
"notifications.column_settings.show": "Show in column",
"notifications.column_settings.sound": "Play sound",
"notifications.filter.all": "All",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
"notifications.filter.follows": "Follows",
"notifications.filter.mentions": "Mentions",
"notifications.filter.polls": "Poll results",
"notifications.group": "{count} notifications",
"poll.closed": "Closed",
"poll.refresh": "Refresh",
"poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
"poll.vote": "Vote",
"poll_button.add_poll": "Add a poll",
"poll_button.remove_poll": "Remove poll",
"privacy.change": "Adjust status privacy",
"privacy.direct.long": "Post to mentioned users only",
"privacy.direct.short": "Direct",
"privacy.private.long": "Post to followers only",
"privacy.private.short": "Followers-only",
"privacy.public.long": "Post to public timelines",
"privacy.public.short": "Public",
"privacy.unlisted.long": "Do not show in public timelines",
"privacy.unlisted.short": "Unlisted",
"regeneration_indicator.label": "Loading…",
"regeneration_indicator.sublabel": "Your home feed is being prepared!",
"relative_time.days": "{number}d",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"reply_indicator.cancel": "Cancel",
"report.forward": "Forward to {target}",
"report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
"report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:",
"report.placeholder": "Additional comments",
"report.submit": "Submit",
"report.target": "Report {target}",
"search.placeholder": "Search",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
"search_popout.tips.user": "user",
"search_results.accounts": "People",
"search_results.hashtags": "Hashtags",
"search_results.statuses": "Toots",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"status.admin_account": "Open moderation interface for @{name}",
"status.admin_status": "Open this status in the moderation interface",
"status.block": "Block @{name}",
"status.cancel_reblog_private": "Unboost",
"status.cannot_reblog": "This post cannot be boosted",
"status.copy": "Copy link to status",
"status.delete": "Delete",
"status.detailed_status": "Detailed conversation view",
"status.direct": "Direct message @{name}",
"status.embed": "Embed",
"status.favourite": "Favourite",
"status.filtered": "Filtered",
"status.load_more": "Load more",
"status.media_hidden": "Media hidden",
"status.mention": "Mention @{name}",
"status.more": "More",
"status.mute": "Mute @{name}",
"status.mute_conversation": "Mute conversation",
"status.open": "Expand this status",
"status.pin": "Pin on profile",
"status.pinned": "Pinned toot",
"status.read_more": "Read more",
"status.reblog": "Boost",
"status.reblog_private": "Boost to original audience",
"status.reblogged_by": "{name} boosted",
"status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.",
"status.redraft": "Delete & re-draft",
"status.reply": "Reply",
"status.replyAll": "Reply to thread",
"status.report": "Report @{name}",
"status.sensitive_toggle": "Click to view",
"status.sensitive_warning": "Sensitive content",
"status.share": "Share",
"status.show_less": "Show less",
"status.show_less_all": "Show less for all",
"status.show_more": "Show more",
"status.show_more_all": "Show more for all",
"status.show_thread": "Show thread",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"suggestions.dismiss": "Dismiss suggestion",
"suggestions.header": "You might be interested in…",
"tabs_bar.federated_timeline": "Federated",
"tabs_bar.home": "Home",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notifications",
"tabs_bar.search": "Search",
"time_remaining.days": "{number, plural, one {# day} other {# days}} left",
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
"time_remaining.moments": "Moments remaining",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Drag & drop to upload",
"upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_error.limit": "File upload limit exceeded.",
"upload_error.poll": "File upload not allowed with polls.",
"upload_form.description": "Describe for the visually impaired",
"upload_form.focus": "Crop",
"upload_form.undo": "Delete",
"upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound"
}

View File

@ -244,11 +244,11 @@
"navigation_bar.lists": "Ցանկեր", "navigation_bar.lists": "Ցանկեր",
"navigation_bar.logout": "Դուրս գալ", "navigation_bar.logout": "Դուրս գալ",
"navigation_bar.mutes": "Լռեցրած օգտատերեր", "navigation_bar.mutes": "Լռեցրած օգտատերեր",
"navigation_bar.personal": "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": "Security", "navigation_bar.security": "Անվտանգություն",
"notification.favourite": "{name} հավանեց թութդ", "notification.favourite": "{name} հավանեց թութդ",
"notification.follow": "{name} սկսեց հետեւել քեզ", "notification.follow": "{name} սկսեց հետեւել քեզ",
"notification.mention": "{name} նշեց քեզ", "notification.mention": "{name} նշեց քեզ",
@ -314,7 +314,7 @@
"search_results.accounts": "People", "search_results.accounts": "People",
"search_results.hashtags": "Hashtags", "search_results.hashtags": "Hashtags",
"search_results.statuses": "Toots", "search_results.statuses": "Toots",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}", "search_results.total": "{count, number} {count, plural, one {արդյունք} other {արդյունք}}",
"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": "Արգելափակել @{name}֊ին", "status.block": "Արգելափակել @{name}֊ին",

View File

@ -11,7 +11,7 @@
"account.follow": "Volgen", "account.follow": "Volgen",
"account.followers": "Volgers", "account.followers": "Volgers",
"account.followers.empty": "Niemand volgt nog deze gebruiker.", "account.followers.empty": "Niemand volgt nog deze gebruiker.",
"account.follows": "Volgt", "account.follows": "Volgend",
"account.follows.empty": "Deze gebruiker volgt nog niemand.", "account.follows.empty": "Deze gebruiker volgt nog niemand.",
"account.follows_you": "Volgt jou", "account.follows_you": "Volgt jou",
"account.hide_reblogs": "Verberg boosts van @{name}", "account.hide_reblogs": "Verberg boosts van @{name}",

View File

@ -117,7 +117,7 @@
"emoji_button.symbols": "Símbolos", "emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viagens & Lugares", "emoji_button.travel": "Viagens & Lugares",
"empty_column.account_timeline": "Não há toots aqui!", "empty_column.account_timeline": "Não há toots aqui!",
"empty_column.account_unavailable": "Profile unavailable", "empty_column.account_unavailable": "Perfil indisponível",
"empty_column.blocks": "Você ainda não bloqueou nenhum usuário.", "empty_column.blocks": "Você ainda não bloqueou nenhum usuário.",
"empty_column.community": "A timeline local está vazia. Escreva algo publicamente para começar!", "empty_column.community": "A timeline local está vazia. Escreva algo publicamente para começar!",
"empty_column.direct": "Você não tem nenhuma mensagem direta ainda. Quando você enviar ou receber uma, as mensagens aparecerão por aqui.", "empty_column.direct": "Você não tem nenhuma mensagem direta ainda. Quando você enviar ou receber uma, as mensagens aparecerão por aqui.",

View File

@ -118,7 +118,6 @@
"emoji_button.travel": "Путешествия", "emoji_button.travel": "Путешествия",
"empty_column.account_timeline": "Статусов нет!", "empty_column.account_timeline": "Статусов нет!",
"empty_column.account_unavailable": "Профиль недоступен", "empty_column.account_unavailable": "Профиль недоступен",
"empty_column.account_timeline_blocked": "Вы заблокированы",
"empty_column.blocks": "Вы ещё никого не заблокировали.", "empty_column.blocks": "Вы ещё никого не заблокировали.",
"empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!", "empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!",
"empty_column.direct": "У Вас пока нет личных сообщений. Когда Вы начнёте их отправлять или получать, они появятся здесь.", "empty_column.direct": "У Вас пока нет личных сообщений. Когда Вы начнёте их отправлять или получать, они появятся здесь.",

View File

@ -0,0 +1,2 @@
[
]

View File

@ -173,6 +173,21 @@ function main() {
avatar.src = url; avatar.src = url;
}); });
const getProfileAvatarAnimationHandler = (swapTo) => {
//animate avatar gifs on the profile page when moused over
return ({ target }) => {
const swapSrc = target.getAttribute(swapTo);
//only change the img source if autoplay is off and the image src is actually different
if(target.getAttribute('data-autoplay') === 'false' && target.src !== swapSrc) {
target.src = swapSrc;
}
};
};
delegate(document, 'img#profile_page_avatar', 'mouseover', getProfileAvatarAnimationHandler('data-original'));
delegate(document, 'img#profile_page_avatar', 'mouseout', getProfileAvatarAnimationHandler('data-static'));
delegate(document, '#account_header', 'change', ({ target }) => { delegate(document, '#account_header', 'change', ({ target }) => {
const header = document.querySelector('.card .card__img img'); const header = document.querySelector('.card .card__img img');
const [file] = target.files || []; const [file] = target.files || [];

View File

@ -10,7 +10,7 @@
@font-face { @font-face {
font-family: 'mastodon-font-display'; font-family: 'mastodon-font-display';
src: local('Montserrat'), src: local('Montserrat Medium'),
url('../fonts/montserrat/Montserrat-Medium.ttf') format('truetype'); url('../fonts/montserrat/Montserrat-Medium.ttf') format('truetype');
font-weight: 500; font-weight: 500;
font-style: normal; font-style: normal;

View File

@ -1,6 +1,6 @@
@font-face { @font-face {
font-family: 'mastodon-font-sans-serif'; font-family: 'mastodon-font-sans-serif';
src: local('Roboto'), src: local('Roboto Italic'),
url('../fonts/roboto/roboto-italic-webfont.woff2') format('woff2'), url('../fonts/roboto/roboto-italic-webfont.woff2') format('woff2'),
url('../fonts/roboto/roboto-italic-webfont.woff') format('woff'), url('../fonts/roboto/roboto-italic-webfont.woff') format('woff'),
url('../fonts/roboto/roboto-italic-webfont.ttf') format('truetype'), url('../fonts/roboto/roboto-italic-webfont.ttf') format('truetype'),
@ -11,7 +11,7 @@
@font-face { @font-face {
font-family: 'mastodon-font-sans-serif'; font-family: 'mastodon-font-sans-serif';
src: local('Roboto'), src: local('Roboto Bold'),
url('../fonts/roboto/roboto-bold-webfont.woff2') format('woff2'), url('../fonts/roboto/roboto-bold-webfont.woff2') format('woff2'),
url('../fonts/roboto/roboto-bold-webfont.woff') format('woff'), url('../fonts/roboto/roboto-bold-webfont.woff') format('woff'),
url('../fonts/roboto/roboto-bold-webfont.ttf') format('truetype'), url('../fonts/roboto/roboto-bold-webfont.ttf') format('truetype'),
@ -22,7 +22,7 @@
@font-face { @font-face {
font-family: 'mastodon-font-sans-serif'; font-family: 'mastodon-font-sans-serif';
src: local('Roboto'), src: local('Roboto Medium'),
url('../fonts/roboto/roboto-medium-webfont.woff2') format('woff2'), url('../fonts/roboto/roboto-medium-webfont.woff2') format('woff2'),
url('../fonts/roboto/roboto-medium-webfont.woff') format('woff'), url('../fonts/roboto/roboto-medium-webfont.woff') format('woff'),
url('../fonts/roboto/roboto-medium-webfont.ttf') format('truetype'), url('../fonts/roboto/roboto-medium-webfont.ttf') format('truetype'),

View File

@ -50,6 +50,7 @@ $content-width: 840px;
color: $darker-text-color; color: $darker-text-color;
text-decoration: none; text-decoration: none;
transition: all 200ms linear; transition: all 200ms linear;
transition-property: color, background-color;
border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px;
i.fa { i.fa {
@ -60,6 +61,7 @@ $content-width: 840px;
color: $primary-text-color; color: $primary-text-color;
background-color: darken($ui-base-color, 5%); background-color: darken($ui-base-color, 5%);
transition: all 100ms linear; transition: all 100ms linear;
transition-property: color, background-color;
} }
&.selected { &.selected {

View File

@ -264,6 +264,16 @@
.compose-form { .compose-form {
padding: 10px; padding: 10px;
&__sensitive-button {
padding: 10px;
padding-top: 0;
.icon-button {
font-size: 14px;
font-weight: 500;
}
}
.compose-form__warning { .compose-form__warning {
color: $inverted-text-color; color: $inverted-text-color;
margin-bottom: 10px; margin-bottom: 10px;
@ -1962,6 +1972,7 @@ a.account__display-name {
font-weight: 500; font-weight: 500;
border-bottom: 2px solid lighten($ui-base-color, 8%); border-bottom: 2px solid lighten($ui-base-color, 8%);
transition: all 50ms linear; transition: all 50ms linear;
transition-property: border-bottom, background, color;
.fa { .fa {
font-weight: 400; font-weight: 400;
@ -2127,7 +2138,7 @@ a.account__display-name {
padding: 0; padding: 0;
border-radius: 30px; border-radius: 30px;
background-color: $ui-base-color; background-color: $ui-base-color;
transition: all 0.2s ease; transition: background-color 0.2s ease;
} }
.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track { .react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
@ -2180,7 +2191,6 @@ a.account__display-name {
} }
.react-toggle-thumb { .react-toggle-thumb {
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms;
position: absolute; position: absolute;
top: 1px; top: 1px;
left: 1px; left: 1px;
@ -2191,6 +2201,7 @@ a.account__display-name {
background-color: darken($simple-background-color, 2%); background-color: darken($simple-background-color, 2%);
box-sizing: border-box; box-sizing: border-box;
transition: all 0.25s ease; transition: all 0.25s ease;
transition-property: border-color, left;
} }
.react-toggle--checked .react-toggle-thumb { .react-toggle--checked .react-toggle-thumb {
@ -2412,7 +2423,7 @@ a.account__display-name {
& > div { & > div {
background: rgba($base-shadow-color, 0.6); background: rgba($base-shadow-color, 0.6);
border-radius: 4px; border-radius: 8px;
padding: 12px 9px; padding: 12px 9px;
flex: 0 0 auto; flex: 0 0 auto;
display: flex; display: flex;
@ -2423,19 +2434,18 @@ a.account__display-name {
button, button,
a { a {
display: inline; display: inline;
color: $primary-text-color; color: $secondary-text-color;
background: transparent; background: transparent;
border: 0; border: 0;
padding: 0 5px; padding: 0 8px;
text-decoration: none; text-decoration: none;
opacity: 0.6;
font-size: 18px; font-size: 18px;
line-height: 18px; line-height: 18px;
&:hover, &:hover,
&:active, &:active,
&:focus { &:focus {
opacity: 1; color: $primary-text-color;
} }
} }
@ -2932,15 +2942,49 @@ a.status-card.compact:hover {
} }
.spoiler-button { .spoiler-button {
display: none; top: 0;
left: 4px; left: 0;
width: 100%;
height: 100%;
position: absolute; position: absolute;
text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color;
top: 4px;
z-index: 100; z-index: 100;
&.spoiler-button--visible { &--minified {
display: block; display: block;
left: 4px;
top: 4px;
width: auto;
height: auto;
}
&--hidden {
display: none;
}
&__overlay {
display: block;
background: transparent;
width: 100%;
height: 100%;
border: 0;
&__label {
display: inline-block;
background: rgba($base-overlay-background, 0.5);
border-radius: 8px;
padding: 8px 12px;
color: $primary-text-color;
font-weight: 500;
font-size: 14px;
}
&:hover,
&:focus,
&:active {
.spoiler-button__overlay__label {
background: rgba($base-overlay-background, 0.8);
}
}
} }
} }
@ -3509,6 +3553,7 @@ a.status-card.compact:hover {
display: inline-block; display: inline-block;
opacity: 0; opacity: 0;
transition: all 100ms linear; transition: all 100ms linear;
transition-property: transform, opacity;
font-size: 18px; font-size: 18px;
width: 18px; width: 18px;
height: 18px; height: 18px;
@ -3728,6 +3773,31 @@ a.status-card.compact:hover {
pointer-events: none; pointer-events: none;
} }
.media-modal__meta {
text-align: center;
position: absolute;
left: 0;
bottom: 20px;
width: 100%;
pointer-events: none;
&--shifted {
bottom: 62px;
}
a {
text-decoration: none;
font-weight: 500;
color: $ui-secondary-color;
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
}
.media-modal__page-dot { .media-modal__page-dot {
display: inline-block; display: inline-block;
} }
@ -3961,14 +4031,6 @@ a.status-card.compact:hover {
font-size: 14px; font-size: 14px;
} }
.confirmation-modal {
max-width: 85vw;
@media screen and (min-width: 480px) {
max-width: 380px;
}
}
.mute-modal { .mute-modal {
line-height: 24px; line-height: 24px;
} }
@ -4093,6 +4155,11 @@ a.status-card.compact:hover {
ul { ul {
overflow-y: auto; overflow-y: auto;
flex-shrink: 0; flex-shrink: 0;
max-height: 80vh;
&.with-status {
max-height: calc(80vh - 75px);
}
li:empty { li:empty {
margin: 0; margin: 0;
@ -4147,6 +4214,10 @@ a.status-card.compact:hover {
color: darken($lighter-text-color, 4%); color: darken($lighter-text-color, 4%);
} }
} }
.confirmation-modal__secondary-button {
flex-shrink: 1;
}
} }
.confirmation-modal__container, .confirmation-modal__container,
@ -4199,6 +4270,7 @@ a.status-card.compact:hover {
pointer-events: none; pointer-events: none;
opacity: 0.9; opacity: 0.9;
transition: opacity 0.1s ease; transition: opacity 0.1s ease;
line-height: 18px;
} }
.media-gallery__gifv { .media-gallery__gifv {
@ -4312,6 +4384,8 @@ a.status-card.compact:hover {
text-decoration: none; text-decoration: none;
color: $secondary-text-color; color: $secondary-text-color;
line-height: 0; line-height: 0;
position: relative;
z-index: 1;
&, &,
img { img {
@ -4324,6 +4398,21 @@ a.status-card.compact:hover {
} }
} }
.media-gallery__preview {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
top: 0;
left: 0;
z-index: 0;
background: $base-overlay-background;
&--hidden {
display: none;
}
}
.media-gallery__gifv { .media-gallery__gifv {
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
@ -4619,6 +4708,23 @@ a.status-card.compact:hover {
} }
} }
&__link {
padding: 2px 10px;
a {
text-decoration: none;
font-size: 14px;
font-weight: 500;
color: $white;
&:hover,
&:active,
&:focus {
text-decoration: underline;
}
}
}
&__seek { &__seek {
cursor: pointer; cursor: pointer;
height: 24px; height: 24px;
@ -4711,62 +4817,18 @@ a.status-card.compact:hover {
.account-gallery__container { .account-gallery__container {
display: flex; display: flex;
justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
padding: 2px; padding: 4px 2px;
} }
.account-gallery__item { .account-gallery__item {
flex-grow: 1; border: none;
width: 50%; box-sizing: border-box;
overflow: hidden; display: block;
position: relative; position: relative;
border-radius: 4px;
&::before { overflow: hidden;
content: ""; margin: 2px;
display: block;
padding-top: 100%;
}
a {
display: block;
width: calc(100% - 4px);
height: calc(100% - 4px);
margin: 2px;
top: 0;
left: 0;
background-color: $base-overlay-background;
background-size: cover;
background-position: center;
position: absolute;
color: $darker-text-color;
text-decoration: none;
border-radius: 4px;
&:hover,
&:active,
&:focus {
outline: 0;
color: $secondary-text-color;
&::before {
content: "";
display: block;
width: 100%;
height: 100%;
background: rgba($base-overlay-background, 0.3);
border-radius: 4px;
}
}
}
&__icons {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
}
} }
.notification__filter-bar, .notification__filter-bar,

View File

@ -533,6 +533,17 @@ code {
color: $error-value-color; color: $error-value-color;
} }
a {
display: inline-block;
color: $darker-text-color;
text-decoration: none;
&:hover {
color: $primary-text-color;
text-decoration: underline;
}
}
p { p {
margin-bottom: 15px; margin-bottom: 15px;
} }

View File

@ -4,7 +4,6 @@
&__img { &__img {
width: 100%; width: 100%;
height: 167px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0;

View File

@ -194,7 +194,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
next if attachment['url'].blank? next if attachment['url'].blank?
href = Addressable::URI.parse(attachment['url']).normalize.to_s href = Addressable::URI.parse(attachment['url']).normalize.to_s
media_attachment = MediaAttachment.create(account: @account, remote_url: href, description: attachment['name'].presence, focus: attachment['focalPoint']) media_attachment = MediaAttachment.create(account: @account, remote_url: href, description: attachment['name'].presence, focus: attachment['focalPoint'], blurhash: supported_blurhash?(attachment['blurhash']) ? attachment['blurhash'] : nil)
media_attachments << media_attachment media_attachments << media_attachment
next if unsupported_media_type?(attachment['mediaType']) || skip_download? next if unsupported_media_type?(attachment['mediaType']) || skip_download?
@ -369,6 +369,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type) mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type)
end end
def supported_blurhash?(blurhash)
components = blurhash.blank? ? nil : Blurhash.components(blurhash)
components.present? && components.none? { |comp| comp > 5 }
end
def skip_download? def skip_download?
return @skip_download if defined?(@skip_download) return @skip_download if defined?(@skip_download)
@skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media? @skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?

View File

@ -19,6 +19,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
conversation: { 'ostatus' => 'http://ostatus.org#', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'conversation' => 'ostatus:conversation' }, conversation: { 'ostatus' => 'http://ostatus.org#', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'conversation' => 'ostatus:conversation' },
focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } }, focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } },
identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' }, identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' },
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
}.freeze }.freeze
def self.default_key_transform def self.default_key_transform

View File

@ -2,7 +2,7 @@
class ProofProvider::Keybase class ProofProvider::Keybase
BASE_URL = ENV.fetch('KEYBASE_BASE_URL', 'https://keybase.io') BASE_URL = ENV.fetch('KEYBASE_BASE_URL', 'https://keybase.io')
DOMAIN = ENV.fetch('KEYBASE_DOMAIN', Rails.configuration.x.local_domain) DOMAIN = ENV.fetch('KEYBASE_DOMAIN', Rails.configuration.x.web_domain)
class Error < StandardError; end class Error < StandardError; end

View File

@ -6,6 +6,7 @@ module LdapAuthenticable
def ldap_setup(_attributes) def ldap_setup(_attributes)
self.confirmed_at = Time.now.utc self.confirmed_at = Time.now.utc
self.admin = false self.admin = false
self.external = true
save! save!
end end

View File

@ -66,6 +66,7 @@ module Omniauthable
email: email || "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com", email: email || "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com",
password: Devise.friendly_token[0, 20], password: Devise.friendly_token[0, 20],
agreement: true, agreement: true,
external: true,
account_attributes: { account_attributes: {
username: ensure_unique_username(auth.uid), username: ensure_unique_username(auth.uid),
display_name: display_name, display_name: display_name,

View File

@ -34,6 +34,7 @@ module PamAuthenticable
self.confirmed_at = Time.now.utc self.confirmed_at = Time.now.utc
self.admin = false self.admin = false
self.account = account self.account = account
self.external = true
account.destroy! unless save account.destroy! unless save
end end

View File

@ -29,4 +29,11 @@ class DomainBlock < ApplicationRecord
def self.blocked?(domain) def self.blocked?(domain)
where(domain: domain, severity: :suspend).exists? where(domain: domain, severity: :suspend).exists?
end end
def stricter_than?(other_block)
return true if suspend?
return false if other_block.suspend? && (silence? || noop?)
return false if other_block.silence? && noop?
(reject_media || !other_block.reject_media) && (reject_reports || !other_block.reject_reports)
end
end end

View File

@ -18,6 +18,7 @@
# account_id :bigint(8) # account_id :bigint(8)
# description :text # description :text
# scheduled_status_id :bigint(8) # scheduled_status_id :bigint(8)
# blurhash :string
# #
class MediaAttachment < ApplicationRecord class MediaAttachment < ApplicationRecord
@ -32,6 +33,11 @@ class MediaAttachment < ApplicationRecord
VIDEO_MIME_TYPES = ['video/webm', 'video/mp4', 'video/quicktime'].freeze VIDEO_MIME_TYPES = ['video/webm', 'video/mp4', 'video/quicktime'].freeze
VIDEO_CONVERTIBLE_MIME_TYPES = ['video/webm', 'video/quicktime'].freeze VIDEO_CONVERTIBLE_MIME_TYPES = ['video/webm', 'video/quicktime'].freeze
BLURHASH_OPTIONS = {
x_comp: 4,
y_comp: 4,
}.freeze
IMAGE_STYLES = { IMAGE_STYLES = {
original: { original: {
pixels: 1_638_400, # 1280x1280px pixels: 1_638_400, # 1280x1280px
@ -41,6 +47,7 @@ class MediaAttachment < ApplicationRecord
small: { small: {
pixels: 160_000, # 400x400px pixels: 160_000, # 400x400px
file_geometry_parser: FastGeometryParser, file_geometry_parser: FastGeometryParser,
blurhash: BLURHASH_OPTIONS,
}, },
}.freeze }.freeze
@ -53,6 +60,8 @@ class MediaAttachment < ApplicationRecord
}, },
format: 'png', format: 'png',
time: 0, time: 0,
file_geometry_parser: FastGeometryParser,
blurhash: BLURHASH_OPTIONS,
}, },
}.freeze }.freeze
@ -166,11 +175,11 @@ class MediaAttachment < ApplicationRecord
def file_processors(f) def file_processors(f)
if f.file_content_type == 'image/gif' if f.file_content_type == 'image/gif'
[:gif_transcoder] [:gif_transcoder, :blurhash_transcoder]
elsif VIDEO_MIME_TYPES.include? f.file_content_type elsif VIDEO_MIME_TYPES.include? f.file_content_type
[:video_transcoder] [:video_transcoder, :blurhash_transcoder]
else else
[:lazy_thumbnail] [:lazy_thumbnail, :blurhash_transcoder]
end end
end end
end end

View File

@ -78,7 +78,7 @@ class User < ApplicationRecord
accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? } accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? }
validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale? validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
validates_with BlacklistedEmailValidator, if: :email_changed? validates_with BlacklistedEmailValidator, on: :create
validates_with EmailMxValidator, if: :validate_email_dns? validates_with EmailMxValidator, if: :validate_email_dns?
validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
@ -107,13 +107,14 @@ class User < ApplicationRecord
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application, :default_federation, to: :settings, prefix: :setting, allow_nil: false :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, :default_federation, to: :settings, prefix: :setting, allow_nil: false
attr_reader :invite_code attr_reader :invite_code
attr_writer :external
def confirmed? def confirmed?
confirmed_at.present? confirmed_at.present?
end end
def invited? def invited?
invite_id.present? invite_id.present? && invite.valid_for_use?
end end
def disable! def disable!
@ -273,13 +274,17 @@ class User < ApplicationRecord
private private
def set_approved def set_approved
self.approved = open_registrations? || invited? self.approved = open_registrations? || invited? || external?
end end
def open_registrations? def open_registrations?
Setting.registrations_mode == 'open' Setting.registrations_mode == 'open'
end end
def external?
!!@external
end
def sanitize_languages def sanitize_languages
return if chosen_languages.nil? return if chosen_languages.nil?
chosen_languages.reject!(&:blank?) chosen_languages.reject!(&:blank?)

View File

@ -2,7 +2,7 @@
class ActivityPub::NoteSerializer < ActivityPub::Serializer class ActivityPub::NoteSerializer < ActivityPub::Serializer
context_extensions :atom_uri, :conversation, :sensitive, context_extensions :atom_uri, :conversation, :sensitive,
:hashtag, :emoji, :focal_point :hashtag, :emoji, :focal_point, :blurhash
attributes :id, :type, :summary, attributes :id, :type, :summary,
:in_reply_to, :published, :url, :in_reply_to, :published, :url,
@ -153,7 +153,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
class MediaAttachmentSerializer < ActivityPub::Serializer class MediaAttachmentSerializer < ActivityPub::Serializer
include RoutingHelper include RoutingHelper
attributes :type, :media_type, :url, :name attributes :type, :media_type, :url, :name, :blurhash
attribute :focal_point, if: :focal_point? attribute :focal_point, if: :focal_point?
def type def type

View File

@ -14,6 +14,8 @@ class InitialStateSerializer < ActiveModel::Serializer
domain: Rails.configuration.x.local_domain, domain: Rails.configuration.x.local_domain,
admin: object.admin&.id&.to_s, admin: object.admin&.id&.to_s,
search_enabled: Chewy.enabled?, search_enabled: Chewy.enabled?,
repository: Mastodon::Version.repository,
source_url: Mastodon::Version.source_url,
version: Mastodon::Version.to_s, version: Mastodon::Version.to_s,
invites_enabled: Setting.min_invite_role == 'user', invites_enabled: Setting.min_invite_role == 'user',
mascot: instance_presenter.mascot&.file&.url, mascot: instance_presenter.mascot&.file&.url,

View File

@ -5,7 +5,7 @@ class REST::MediaAttachmentSerializer < ActiveModel::Serializer
attributes :id, :type, :url, :preview_url, attributes :id, :type, :url, :preview_url,
:remote_url, :text_url, :meta, :remote_url, :text_url, :meta,
:description :description, :blurhash
def id def id
object.id.to_s object.id.to_s

View File

@ -6,6 +6,7 @@ class BlockService < BaseService
UnfollowService.new.call(account, target_account) if account.following?(target_account) UnfollowService.new.call(account, target_account) if account.following?(target_account)
UnfollowService.new.call(target_account, account) if target_account.following?(account) UnfollowService.new.call(target_account, account) if target_account.following?(account)
RejectFollowService.new.call(account, target_account) if target_account.requested?(account)
block = account.block!(target_account) block = account.block!(target_account)

View File

@ -165,7 +165,7 @@ class FetchLinkCardService < BaseService
end end
def meta_property(page, property) def meta_property(page, property)
page.at_xpath("//meta[@property=\"#{property}\"]")&.attribute('content')&.value || page.at_xpath("//meta[@name=\"#{property}\"]")&.attribute('content')&.value page.at_xpath("//meta[contains(concat(' ', normalize-space(@property), ' '), ' #{property} ')]")&.attribute('content')&.value || page.at_xpath("//meta[@name=\"#{property}\"]")&.attribute('content')&.value
end end
def lock_options def lock_options

View File

@ -2,7 +2,10 @@
class BlacklistedEmailValidator < ActiveModel::Validator class BlacklistedEmailValidator < ActiveModel::Validator
def validate(user) def validate(user)
return if user.invited?
@email = user.email @email = user.email
user.errors.add(:email, I18n.t('users.invalid_email')) if blocked_email? user.errors.add(:email, I18n.t('users.invalid_email')) if blocked_email?
end end
@ -13,7 +16,7 @@ class BlacklistedEmailValidator < ActiveModel::Validator
end end
def on_blacklist? def on_blacklist?
return true if EmailDomainBlock.block?(@email) return true if EmailDomainBlock.block?(@email)
return false if Rails.configuration.x.email_domains_blacklist.blank? return false if Rails.configuration.x.email_domains_blacklist.blank?
domains = Rails.configuration.x.email_domains_blacklist.gsub('.', '\.') domains = Rails.configuration.x.email_domains_blacklist.gsub('.', '\.')

View File

@ -3,7 +3,7 @@
= image_tag (current_account&.user&.setting_auto_play_gif ? account.header_original_url : account.header_static_url), class: 'parallax' = image_tag (current_account&.user&.setting_auto_play_gif ? account.header_original_url : account.header_static_url), class: 'parallax'
.public-account-header__bar .public-account-header__bar
= link_to short_account_url(account), class: 'avatar' do = link_to short_account_url(account), class: 'avatar' do
= image_tag (current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url) = image_tag (current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url), id: 'profile_page_avatar', data: {original: full_asset_url(account.avatar_original_url), static: full_asset_url(account.avatar_static_url), autoplay: current_account&.user&.setting_auto_play_gif}
.public-account-header__tabs .public-account-header__tabs
.public-account-header__tabs__name .public-account-header__tabs__name
%h1 %h1

View File

@ -36,6 +36,6 @@
= f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path) = f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path)
.actions .actions
= f.button :button, sign_up_message, type: :submit = f.button :button, @invite.present? ? t('auth.register') : sign_up_message, type: :submit
.form-footer= render 'auth/shared/links' .form-footer= render 'auth/shared/links'

View File

@ -28,7 +28,7 @@
- elsif !status.media_attachments.empty? - elsif !status.media_attachments.empty?
- if status.media_attachments.first.video? - if status.media_attachments.first.video?
- video = status.media_attachments.first - video = status.media_attachments.first
= react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do
= render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments } = render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments }
- else - else
= react_component :media_gallery, height: 380, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do = react_component :media_gallery, height: 380, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do

View File

@ -32,7 +32,7 @@
- elsif !status.media_attachments.empty? - elsif !status.media_attachments.empty?
- if status.media_attachments.first.video? - if status.media_attachments.first.video?
- video = status.media_attachments.first - video = status.media_attachments.first
= react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description do = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description do
= render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments } = render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments }
- else - else
= react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do = react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do

View File

@ -7,5 +7,7 @@ class ActivityPub::ProcessingWorker
def perform(account_id, body, delivered_to_account_id = nil) def perform(account_id, body, delivered_to_account_id = nil)
ActivityPub::ProcessCollectionService.new.call(body, Account.find(account_id), override_timestamps: true, delivered_to_account_id: delivered_to_account_id, delivery: true) ActivityPub::ProcessCollectionService.new.call(body, Account.find(account_id), override_timestamps: true, delivered_to_account_id: delivered_to_account_id, delivery: true)
rescue ActiveRecord::RecordInvalid => e
Rails.logger.debug "Error processing incoming ActivityPub object: #{e}"
end end
end end

View File

@ -1,4 +1,6 @@
ActiveSupport::Notifications.subscribe('rack.attack') do |_name, _start, _finish, _request_id, req| ActiveSupport::Notifications.subscribe(/rack_attack/) do |_name, _start, _finish, _request_id, payload|
req = payload[:request]
next unless [:throttle, :blacklist].include? req.env['rack.attack.match_type'] next unless [:throttle, :blacklist].include? req.env['rack.attack.match_type']
Rails.logger.info("Rate limit hit (#{req.env['rack.attack.match_type']}): #{req.ip} #{req.request_method} #{req.fullpath}") Rails.logger.info("Rate limit hit (#{req.env['rack.attack.match_type']}): #{req.ip} #{req.request_method} #{req.fullpath}")
end end

View File

@ -1,3 +1,4 @@
require 'stoplight' require 'stoplight'
Stoplight::Light.default_data_store = Stoplight::DataStore::Redis.new(Redis.current) Stoplight::Light.default_data_store = Stoplight::DataStore::Redis.new(Redis.current)
Stoplight::Light.default_notifiers = [Stoplight::Notifier::Logger.new(Rails.logger)]

View File

@ -2,8 +2,9 @@
pt-BR: pt-BR:
activerecord: activerecord:
attributes: attributes:
status: poll:
owned_poll: enquete expires_at: Expira em
options: Escolhas
errors: errors:
models: models:
account: account:

View File

@ -3,7 +3,7 @@ sk:
activerecord: activerecord:
attributes: attributes:
poll: poll:
expires_at: Uzávierka expires_at: Trvá do
options: Voľby options: Voľby
status: status:
owned_poll: Anketa owned_poll: Anketa

View File

@ -68,6 +68,7 @@ ca:
admin: Administrador admin: Administrador
bot: Bot bot: Bot
moderator: Moderador moderator: Moderador
unavailable: Perfil inaccessible
unfollow: Deixa de seguir unfollow: Deixa de seguir
admin: admin:
account_actions: account_actions:
@ -80,6 +81,7 @@ ca:
destroyed_msg: Nota de moderació destruïda amb èxit! destroyed_msg: Nota de moderació destruïda amb èxit!
accounts: accounts:
approve: Aprova approve: Aprova
approve_all: Aprova'ls tots
are_you_sure: N'estàs segur? are_you_sure: N'estàs segur?
avatar: Avatar avatar: Avatar
by_domain: Domini by_domain: Domini
@ -132,6 +134,7 @@ ca:
moderation_notes: Notes de moderació moderation_notes: Notes de moderació
most_recent_activity: Activitat més recent most_recent_activity: Activitat més recent
most_recent_ip: IP més recent most_recent_ip: IP més recent
no_account_selected: No s'han canviat els comptes perque no s'han seleccionat
no_limits_imposed: Sense límits imposats no_limits_imposed: Sense límits imposats
not_subscribed: No subscrit not_subscribed: No subscrit
outbox_url: URL de la bústia de sortida outbox_url: URL de la bústia de sortida
@ -144,6 +147,7 @@ ca:
push_subscription_expires: La subscripció PuSH expira push_subscription_expires: La subscripció PuSH expira
redownload: Actualitza el perfil redownload: Actualitza el perfil
reject: Rebutja reject: Rebutja
reject_all: Rebutja'ls tots
remove_avatar: Eliminar avatar remove_avatar: Eliminar avatar
remove_header: Treu la capçalera remove_header: Treu la capçalera
resend_confirmation: resend_confirmation:
@ -330,6 +334,8 @@ ca:
expired: Caducat expired: Caducat
title: Filtre title: Filtre
title: Convida title: Convida
pending_accounts:
title: Comptes pendents (%{count})
relays: relays:
add_new: Afegiu un nou relay add_new: Afegiu un nou relay
delete: Esborra delete: Esborra
@ -854,18 +860,23 @@ ca:
revoke_success: S'ha revocat la sessió amb èxit revoke_success: S'ha revocat la sessió amb èxit
title: Sessions title: Sessions
settings: settings:
account: Compte
account_settings: Ajustos del compte
appearance: Aparènça
authorized_apps: Aplicacions autoritzades authorized_apps: Aplicacions autoritzades
back: Torna a l'inici back: Torna a Mastodon
delete: Eliminació del compte delete: Eliminació del compte
development: Desenvolupament development: Desenvolupament
edit_profile: Editar perfil edit_profile: Editar perfil
export: Exportar informació export: Exportar dades
featured_tags: Etiquetes destacades featured_tags: Etiquetes destacades
identity_proofs: Proves d'identitat identity_proofs: Proves d'identitat
import: Importar import: Importar
import_and_export: Importar i exportar
migrate: Migració del compte migrate: Migració del compte
notifications: Notificacions notifications: Notificacions
preferences: Preferències preferences: Preferències
profile: Perfil
relationships: Seguits i seguidors relationships: Seguits i seguidors
two_factor_authentication: Autenticació de dos factors two_factor_authentication: Autenticació de dos factors
statuses: statuses:
@ -1040,7 +1051,7 @@ ca:
welcome: welcome:
edit_profile_action: Configurar perfil edit_profile_action: Configurar perfil
edit_profile_step: Pots personalitzar el teu perfil penjant un avatar, un encapçalament, canviant el teu nom de visualització i molt més. Si prefereixes revisar els seguidors nous abans de que et puguin seguir, pots blocar el teu compte. edit_profile_step: Pots personalitzar el teu perfil penjant un avatar, un encapçalament, canviant el teu nom de visualització i molt més. Si prefereixes revisar els seguidors nous abans de que et puguin seguir, pots blocar el teu compte.
explanation: Aquests són alguns consells per començar explanation: Aquests són alguns consells per a començar
final_action: Comença a publicar final_action: Comença a publicar
final_step: 'Comença a publicar! Fins i tot sense seguidors, els altres poden veure els teus missatges públics, per exemple, a la línia de temps local i a les etiquetes ("hashtags"). És possible que vulguis presentar-te amb l''etiqueta #introductions.' final_step: 'Comença a publicar! Fins i tot sense seguidors, els altres poden veure els teus missatges públics, per exemple, a la línia de temps local i a les etiquetes ("hashtags"). És possible que vulguis presentar-te amb l''etiqueta #introductions.'
full_handle: El teu nom d'usuari sencer full_handle: El teu nom d'usuari sencer

View File

@ -269,6 +269,7 @@ co:
created_msg: U blucchime di u duminiu hè attivu created_msg: U blucchime di u duminiu hè attivu
destroyed_msg: U blucchime di u duminiu ùn hè più attivu destroyed_msg: U blucchime di u duminiu ùn hè più attivu
domain: Duminiu domain: Duminiu
existing_domain_block_html: Avete digià impostu limite più strette nant'à %{name}, duvete <a href="%{unblock_url}">sbluccallu</a> primu.
new: new:
create: Creà un blucchime create: Creà un blucchime
hint: U blucchime di duminiu ùn impedirà micca a creazione di conti indè a database, mà metudi di muderazione specifiche saranu applicati. hint: U blucchime di duminiu ùn impedirà micca a creazione di conti indè a database, mà metudi di muderazione specifiche saranu applicati.
@ -862,6 +863,7 @@ co:
settings: settings:
account: Contu account: Contu
account_settings: Parametri di u contu account_settings: Parametri di u contu
appearance: Apparenza
authorized_apps: Applicazione auturizate authorized_apps: Applicazione auturizate
back: Ritornu nantà Mastodon back: Ritornu nantà Mastodon
delete: Suppressione di u contu delete: Suppressione di u contu

View File

@ -273,6 +273,7 @@ cs:
created_msg: Blokace domény se právě vyřizuje created_msg: Blokace domény se právě vyřizuje
destroyed_msg: Blokace domény byla zrušena destroyed_msg: Blokace domény byla zrušena
domain: Doména domain: Doména
existing_domain_block_html: Pro účet %{name} jste již nastavil/a přísnější omezení, musíte jej nejdříve <a href="%{unblock_url}">odblokovat</a>.
new: new:
create: Vytvořit blokaci create: Vytvořit blokaci
hint: Blokace domény nezakáže vytváření záznamů účtů v databázi, ale bude na tyto účty zpětně a automaticky aplikovat specifické metody moderování. hint: Blokace domény nezakáže vytváření záznamů účtů v databázi, ale bude na tyto účty zpětně a automaticky aplikovat specifické metody moderování.

View File

@ -3,7 +3,7 @@ sk:
devise: devise:
confirmations: confirmations:
confirmed: Tvoja emailová adresa bola úspešne overená. confirmed: Tvoja emailová adresa bola úspešne overená.
send_instructions: O niekoľko minút obdržíš email s inštrukciami ako potvrdiť svoj účet. Prosím, skontroluj si aj zložku spam, ak sa k tebe toto potvrdenie nedostalo. send_instructions: O niekoľko minút obdržíš email s pokynmi ako potvrdiť svoj účet. Prosím, skontroluj si aj zložku spam, ak sa k tebe toto potvrdenie nedostalo.
send_paranoid_instructions: Ak sa tvoja emailová adresa nachádza v našej databázi, o niekoľko minút obdržíš email s pokynmi ako potvrdiť svoj účet. Prosím, skontroluj aj zložku spam, ak sa k tebe toto potvrdenie nedostalo. send_paranoid_instructions: Ak sa tvoja emailová adresa nachádza v našej databázi, o niekoľko minút obdržíš email s pokynmi ako potvrdiť svoj účet. Prosím, skontroluj aj zložku spam, ak sa k tebe toto potvrdenie nedostalo.
failure: failure:
already_authenticated: Už si prihlásený/á. already_authenticated: Už si prihlásený/á.
@ -11,27 +11,27 @@ sk:
invalid: Nesprávny %{authentication_keys}, alebo heslo. invalid: Nesprávny %{authentication_keys}, alebo heslo.
last_attempt: Máš posledný pokus pred zamknutím tvojho účtu. last_attempt: Máš posledný pokus pred zamknutím tvojho účtu.
locked: Tvoj účet je zamknutý. locked: Tvoj účet je zamknutý.
not_found_in_database: Nesprávny %{authentication_keys} alebo heslo. not_found_in_database: Nesprávny %{authentication_keys}, alebo heslo.
pending: Tvoj účet je stále prehodnocovaný. pending: Tvoj účet je stále prehodnocovaný.
timeout: Vaša aktívna sezóna vypršala. Pre pokračovanie sa prosím znovu prihláste. timeout: Tvoja aktívna sezóna vypršala. Pre pokračovanie sa prosím prihlás znovu.
unauthenticated: K pokračovaniu sa musíš zaregistrovať alebo prihlásiť. unauthenticated: K pokračovaniu sa musíš zaregistrovať alebo prihlásiť.
unconfirmed: Pred pokračovaním musíš potvrdiť svoj email. unconfirmed: Pred pokračovaním musíš potvrdiť svoj email.
mailer: mailer:
confirmation_instructions: confirmation_instructions:
action: Potvŕď emailovú adresu action: Potvrď emailovú adresu
action_with_app: Potvrď a vráť sa na %{app} action_with_app: Potvrď a vráť sa na %{app}
explanation: S touto emailovou adresou si si vytvoril/a účet na %{host}. Si iba jeden klik od jeho aktivácie. Pokiaľ si to ale nebol/a ty, prosím ignoruj tento email. explanation: S touto emailovou adresou si si vytvoril/a účet na %{host}. Si iba jeden klik od jeho aktivácie. Pokiaľ si to ale nebol/a ty, prosím ignoruj tento email.
extra_html: Prosím pozri sa aj na <a href="%{terms_path}"> pravidlá tohto servera,</a> a <a href="%{policy_path}"> naše užívaťeľské podiemky</a>. extra_html: Prosím, pozri sa aj na <a href="%{terms_path}"> pravidlá tohto servera,</a> a <a href="%{policy_path}"> naše užívaťeľské podiemky</a>.
subject: 'Mastodon: Potvrdzovacie inštrukcie pre %{instance}' subject: 'Mastodon: Potvrdzovacie pokyny pre %{instance}'
title: Potvrď emailovú adresu title: Potvrď emailovú adresu
email_changed: email_changed:
explanation: 'Emailová adresa tvojho účtu bude zmenená na:' explanation: 'Emailová adresa tvojho účtu bude zmenená na:'
extra: Pokiaľ si nezmenil/a svoj email, je pravdepodobné že niekto iný získal prístup k tvojmu účtu. Naliehavo preto prosím zmeň svoje heslo, alebo kontaktuj administrátora tohto serveru pokiaľ si vymknutý/á zo svojho účtu. extra: Ak si nezmenil/a svoj email, je pravdepodobné, že niekto iný získal prístup k tvojmu účtu. Naliehavo preto prosím zmeň svoje heslo, alebo kontaktuj administrátora tohto serveru, pokiaľ si vymknutý/á zo svojho účtu.
subject: 'Mastodon: Emailová adresa bola zmenená' subject: 'Mastodon: Emailová adresa bola zmenená'
title: Nová emailová adresa title: Nová emailová adresa
password_change: password_change:
explanation: Heslo k tvojmu účtu bolo zmenené. explanation: Heslo k tvojmu účtu bolo zmenené.
extra: Ak si heslo nezmenil/a, je pravdepodobné že niekto iný získal prístup k tvojmu účtu. Naliehavo preto prosím zmeň svoje heslo, alebo kontaktuj administrátora tohto serveru pokiaľ si vymknutý/á zo svojho účtu. extra: Ak si heslo nezmenil/a, je pravdepodobné, že niekto iný získal prístup k tvojmu účtu. Naliehavo preto prosím zmeň svoje heslo, alebo kontaktuj administrátora tohto serveru, pokiaľ si vymknutý/á zo svojho účtu.
subject: 'Mastodon: Heslo bolo zmenené' subject: 'Mastodon: Heslo bolo zmenené'
title: Heslo bolo zmenené title: Heslo bolo zmenené
reconfirmation_instructions: reconfirmation_instructions:
@ -42,17 +42,17 @@ sk:
reset_password_instructions: reset_password_instructions:
action: Zmeň svoje heslo action: Zmeň svoje heslo
explanation: Vyžiadal/a si si nové heslo pre svoj účet. explanation: Vyžiadal/a si si nové heslo pre svoj účet.
extra: Pokiaľ si túto akciu nevyžiadal/a, prosím ignoruj tento email. Tvoje heslo nebude zmenené pokiaľ nepostúpiš na adresu uvedenú vyššie a vytvoríš si nové. extra: Ak si túto akciu nevyžiadal/a, prosím ignoruj tento email. Tvoje heslo nebude zmenené pokiaľ nepostúpiš na adresu uvedenú vyššie a vytvoríš si nové.
subject: 'Mastodon: Inštrukcie pre obnovu hesla' subject: 'Mastodon: Pokyny pre obnovu hesla'
title: Nastav nové heslo title: Nastav nové heslo
unlock_instructions: unlock_instructions:
subject: 'Mastodon: Inštrukcie pre odomknutie účtu' subject: 'Mastodon: Pokyny na odomknutie účtu'
omniauth_callbacks: omniauth_callbacks:
failure: Nebolo možné ťa overiť z dôvodu,%{kind} že "%{reason}". failure: Nebolo možné ťa overiť z %{kind}, lebo "%{reason}".
success: Úspešné overenie z účtu %{kind}. success: Úspešné overenie z účtu %{kind}.
passwords: passwords:
no_token: Túto stránku nemôžete navštíviť pokiaľ neprichádzate z emailu s inštrukciami na obnovu hesla. Pokiaľ prichádzate z tohto emailu, prosím uistite sa že ste použili celú URL z emailu. no_token: Túto stránku nemôžeš navštíviť, ak neprichádzaš z emailu s pokynmi na obnovu hesla. Pokiaľ prichádzaš z tohto emailu, prosím uisti sa že si použil/a celú URL adresu z emailu.
send_instructions: Pokiaľ sa tvoja emailová adresa nachádza v databázi, tak o niekoľko minút obdržíš email s inštrukciami ako nastaviť nové heslo. Ak máš pocit, že si email neobdržal/a, prosím skontroluj aj svoju spam zložku. send_instructions: Ak sa tvoja emailová adresa nachádza v databázi, tak o niekoľko minút obdržíš email s pokynmi ako nastaviť nové heslo. Ak máš pocit, že si email neobdržal/a, prosím skontroluj aj svoju spam zložku.
send_paranoid_instructions: Ak sa tvoja emailová adresa nachádza v databázi, za chvíľu obdržíš odkaz pre obnovu hesla na svoj email. Skontroluj ale prosím aj svoj spam, ak tento email nevidíš. send_paranoid_instructions: Ak sa tvoja emailová adresa nachádza v databázi, za chvíľu obdržíš odkaz pre obnovu hesla na svoj email. Skontroluj ale prosím aj svoj spam, ak tento email nevidíš.
updated: Tvoje heslo bolo úspešne zmenené. Teraz si prihlásený/á. updated: Tvoje heslo bolo úspešne zmenené. Teraz si prihlásený/á.
updated_not_active: Tvoje heslo bolo úspešne zmenené. updated_not_active: Tvoje heslo bolo úspešne zmenené.
@ -66,9 +66,9 @@ sk:
update_needs_confirmation: Účet bol úspešne pozmenený, ale ešte potrebujeme overiť tvoju novú emailovú adresu. Pre overenie prosím klikni na link v správe ktorú si dostal/a na email. Takisto ale skontroluj aj svoju spam zložku, ak sa ti zdá, že si tento email nedostal/a. update_needs_confirmation: Účet bol úspešne pozmenený, ale ešte potrebujeme overiť tvoju novú emailovú adresu. Pre overenie prosím klikni na link v správe ktorú si dostal/a na email. Takisto ale skontroluj aj svoju spam zložku, ak sa ti zdá, že si tento email nedostal/a.
updated: Tvoj účet bol úspešne aktualizovaný. updated: Tvoj účet bol úspešne aktualizovaný.
sessions: sessions:
already_signed_out: Odhlásil/a si sa úspešné. already_signed_out: Už si sa úspešne odhlásil/a.
signed_in: Prihlásil/a si sa úspešné. signed_in: Prihlásil/a si sa úspešne.
signed_out: Odhlásil/a si sa úspešné. signed_out: Odhlásil/a si sa úspešne.
unlocks: unlocks:
send_instructions: O niekoľko minút obdržíš email s pokynmi, ako nastaviť nové heslo. Prosím, skontroluj ale aj svoju spam zložku, pokiaľ sa ti zdá, že si tento email nedostal/a. send_instructions: O niekoľko minút obdržíš email s pokynmi, ako nastaviť nové heslo. Prosím, skontroluj ale aj svoju spam zložku, pokiaľ sa ti zdá, že si tento email nedostal/a.
send_paranoid_instructions: Ak tvoj účet existuje, o niekoľko minút obdržíš email s pokynmi ako si ho odomknúť. Prosím, skontroluj ale aj svoju spam zložku, pokiaľ sa ti zdá, že si tento email nedostal/a. send_paranoid_instructions: Ak tvoj účet existuje, o niekoľko minút obdržíš email s pokynmi ako si ho odomknúť. Prosím, skontroluj ale aj svoju spam zložku, pokiaľ sa ti zdá, že si tento email nedostal/a.
@ -79,8 +79,8 @@ sk:
confirmation_period_expired: musí byť potvrdený do %{period}, prosím požiadaj o nový confirmation_period_expired: musí byť potvrdený do %{period}, prosím požiadaj o nový
expired: vypŕšal, prosím, vyžiadaj si nový expired: vypŕšal, prosím, vyžiadaj si nový
not_found: nenájdený not_found: nenájdený
not_locked: nebol uzamknutý not_locked: nebol zamknutý
not_saved: not_saved:
few: "%{resource} nebol uložený kôli %{count} chybám:" few: "%{resource} nebol uložený kvôli %{count} chybám:"
one: "%{resource} nebol uložený kôli chybe:" one: "%{resource} nebol uložený kvôli chybe:"
other: "%{resource} nebol uložený kôli %{count} chybám:" other: "%{resource} nebol uložený kvôli %{count} chybám:"

View File

@ -269,6 +269,7 @@ en:
created_msg: Domain block is now being processed created_msg: Domain block is now being processed
destroyed_msg: Domain block has been undone destroyed_msg: Domain block has been undone
domain: Domain domain: Domain
existing_domain_block_html: You have already imposed stricter limits on %{name}, you need to <a href="%{unblock_url}">unblock it</a> first.
new: new:
create: Create block create: Create block
hint: The domain block will not prevent creation of account entries in the database, but will retroactively and automatically apply specific moderation methods on those accounts. hint: The domain block will not prevent creation of account entries in the database, but will retroactively and automatically apply specific moderation methods on those accounts.

View File

@ -260,10 +260,10 @@ fr:
title: Nouveau blocage de domaine title: Nouveau blocage de domaine
reject_media: Fichiers média rejetés reject_media: Fichiers média rejetés
reject_media_hint: Supprime localement les fichiers média stockés et refuse den télécharger ultérieurement. Ne concerne pas les suspensions reject_media_hint: Supprime localement les fichiers média stockés et refuse den télécharger ultérieurement. Ne concerne pas les suspensions
reject_reports: Rapports de rejet reject_reports: Rejeter les signalements
reject_reports_hint: Ignorez tous les rapports provenant de ce domaine. Sans objet pour les suspensions reject_reports_hint: Ignorez tous les signalements provenant de ce domaine. Ne concerne pas les suspensions
rejecting_media: rejet des fichiers multimédia rejecting_media: rejet des fichiers multimédia
rejecting_reports: rejet de rapports rejecting_reports: rejet des signalements
severity: severity:
silence: silencié silence: silencié
suspend: suspendu suspend: suspendu

View File

@ -68,6 +68,7 @@ nl:
admin: Beheerder admin: Beheerder
bot: Bot bot: Bot
moderator: Moderator moderator: Moderator
unavailable: Profiel niet beschikbaar
unfollow: Ontvolgen unfollow: Ontvolgen
admin: admin:
account_actions: account_actions:
@ -80,6 +81,7 @@ nl:
destroyed_msg: Verwijderen van opmerking voor moderatoren geslaagd! destroyed_msg: Verwijderen van opmerking voor moderatoren geslaagd!
accounts: accounts:
approve: Goedkeuren approve: Goedkeuren
approve_all: Alles goedkeuren
are_you_sure: Weet je het zeker? are_you_sure: Weet je het zeker?
avatar: Avatar avatar: Avatar
by_domain: Domein by_domain: Domein
@ -132,6 +134,7 @@ nl:
moderation_notes: Opmerkingen voor moderatoren moderation_notes: Opmerkingen voor moderatoren
most_recent_activity: Laatst actief most_recent_activity: Laatst actief
most_recent_ip: Laatst gebruikt IP-adres most_recent_ip: Laatst gebruikt IP-adres
no_account_selected: Er zijn geen accounts veranderd, omdat er geen een was geselecteerd
no_limits_imposed: Geen limieten ingesteld no_limits_imposed: Geen limieten ingesteld
not_subscribed: Niet geabonneerd not_subscribed: Niet geabonneerd
outbox_url: Outbox-URL outbox_url: Outbox-URL
@ -144,6 +147,7 @@ nl:
push_subscription_expires: PuSH-abonnement verloopt op push_subscription_expires: PuSH-abonnement verloopt op
redownload: Profiel vernieuwen redownload: Profiel vernieuwen
reject: Afkeuren reject: Afkeuren
reject_all: Alles afkeuren
remove_avatar: Avatar verwijderen remove_avatar: Avatar verwijderen
remove_header: Omslagfoto verwijderen remove_header: Omslagfoto verwijderen
resend_confirmation: resend_confirmation:
@ -245,6 +249,7 @@ nl:
feature_profile_directory: Gebruikersgids feature_profile_directory: Gebruikersgids
feature_registrations: Registraties feature_registrations: Registraties
feature_relay: Federatierelay feature_relay: Federatierelay
feature_timeline_preview: Voorvertoning van tijdlijn
features: Functies features: Functies
hidden_service: Federatie met verborgen diensten hidden_service: Federatie met verborgen diensten
open_reports: onopgeloste rapportages open_reports: onopgeloste rapportages
@ -329,6 +334,8 @@ nl:
expired: Verlopen expired: Verlopen
title: Filter title: Filter
title: Uitnodigingen title: Uitnodigingen
pending_accounts:
title: Accounts in afwachting (%{count})
relays: relays:
add_new: Nieuwe relayserver toevoegen add_new: Nieuwe relayserver toevoegen
delete: Verwijderen delete: Verwijderen
@ -428,14 +435,14 @@ nl:
desc_html: Medewerkersbadge op profielpagina tonen desc_html: Medewerkersbadge op profielpagina tonen
title: Medewerkersbadge tonen title: Medewerkersbadge tonen
site_description: site_description:
desc_html: Dit wordt als een alinea op de voorpagina getoond. Beschrijf wat er speciaal is aan deze server en andere zaken die van belang zijn. Je kan HTML gebruiken, zoals <code>&lt;a&gt;</code> en <code>&lt;em&gt;</code>. desc_html: Introductie-alinea voor de API. Beschrijf wat er speciaal is aan deze server en andere zaken die van belang zijn. Je kan HTML gebruiken, zoals <code>&lt;a&gt;</code> en <code>&lt;em&gt;</code>.
title: Omschrijving Mastodonserver title: Omschrijving Mastodonserver (API)
site_description_extended: site_description_extended:
desc_html: Een goede plek voor je gedragscode, regels, richtlijnen en andere zaken die jouw server uniek maken. Je kan ook hier HTML gebruiken desc_html: Een goede plek voor je gedragscode, regels, richtlijnen en andere zaken die jouw server uniek maken. Je kan ook hier HTML gebruiken
title: Uitgebreide omschrijving Mastodonserver title: Uitgebreide omschrijving Mastodonserver
site_short_description: site_short_description:
desc_html: Dit wordt in de zijbalk getoond als en als metatag in de paginabron. Beschrijf in één alinea wat Mastodon is en wat deze server speciaal maakt. De (langere) omschrijving van de Mastodonserver wordt gebruikt wanneer dit veld wordt leeg gelaten. desc_html: Dit wordt gebruikt op de voorpagina, in de zijbalk op profielpagina's en als metatag in de paginabron. Beschrijf in één alinea wat Mastodon is en wat deze server speciaal maakt.
title: Korte omschrijving Mastodonserver title: Omschrijving Mastodonserver (website)
site_terms: site_terms:
desc_html: Je kan hier jouw eigen privacybeleid, gebruiksvoorwaarden en ander juridisch jargon kwijt. Je kan HTML gebruiken desc_html: Je kan hier jouw eigen privacybeleid, gebruiksvoorwaarden en ander juridisch jargon kwijt. Je kan HTML gebruiken
title: Aangepaste gebruiksvoorwaarden title: Aangepaste gebruiksvoorwaarden
@ -585,6 +592,9 @@ nl:
content: Het spijt ons, er is aan onze kant iets fout gegaan. content: Het spijt ons, er is aan onze kant iets fout gegaan.
title: Er is iets mis title: Er is iets mis
noscript_html: Schakel JavaScript in om de webapp van Mastodon te kunnen gebruiken. Als alternatief kan je een <a href="%{apps_path}">Mastodon-app</a> zoeken voor jouw platform. noscript_html: Schakel JavaScript in om de webapp van Mastodon te kunnen gebruiken. Als alternatief kan je een <a href="%{apps_path}">Mastodon-app</a> zoeken voor jouw platform.
existing_username_validator:
not_found: Kon geen lokale gebruiker met die gebruikersnaam vinden
not_found_multiple: Kon %{usernames} niet vinden
exports: exports:
archive_takeout: archive_takeout:
date: Datum date: Datum
@ -628,10 +638,13 @@ nl:
all: Alles all: Alles
changes_saved_msg: Wijzigingen succesvol opgeslagen! changes_saved_msg: Wijzigingen succesvol opgeslagen!
copy: Kopiëren copy: Kopiëren
order_by: Sorteer op
save_changes: Wijzigingen opslaan save_changes: Wijzigingen opslaan
validation_errors: validation_errors:
one: Er is iets niet helemaal goed! Bekijk onderstaande fout one: Er is iets niet helemaal goed! Bekijk onderstaande fout
other: Er is iets niet helemaal goed! Bekijk onderstaande %{count} fouten other: Er is iets niet helemaal goed! Bekijk onderstaande %{count} fouten
html_validator:
invalid_markup: 'bevat ongeldige HTML-opmaak: %{error}'
identity_proofs: identity_proofs:
active: Actief active: Actief
authorize: Ja, autoriseren authorize: Ja, autoriseren
@ -646,6 +659,8 @@ nl:
i_am_html: Ik ben %{username} op %{service}. i_am_html: Ik ben %{username} op %{service}.
identity: Identiteit identity: Identiteit
inactive: Inactief inactive: Inactief
publicize_checkbox: 'En toot dit:'
publicize_toot: 'Het is bewezen! Ik ben %{username} op %{service}: %{url}'
status: Verificatiestatus status: Verificatiestatus
view_proof: Bekijk bewijs view_proof: Bekijk bewijs
imports: imports:
@ -768,6 +783,8 @@ nl:
relationships: relationships:
activity: Accountactiviteit activity: Accountactiviteit
dormant: Sluimerend dormant: Sluimerend
last_active: Laatst actief
most_recent: Recentelijk gevolgd
moved: Verhuisd moved: Verhuisd
mutual: Wederzijds mutual: Wederzijds
primary: Primair primary: Primair
@ -843,6 +860,9 @@ nl:
revoke_success: Sessie succesvol ingetrokken revoke_success: Sessie succesvol ingetrokken
title: Sessies title: Sessies
settings: settings:
account: Account
account_settings: Accountinstellingen
appearance: Uiterlijk
authorized_apps: Geautoriseerde apps authorized_apps: Geautoriseerde apps
back: Terug naar Mastodon back: Terug naar Mastodon
delete: Account verwijderen delete: Account verwijderen
@ -852,9 +872,11 @@ nl:
featured_tags: Uitgelichte hashtags featured_tags: Uitgelichte hashtags
identity_proofs: Identiteitsbewijzen identity_proofs: Identiteitsbewijzen
import: Importeren import: Importeren
import_and_export: Importeren en exporteren
migrate: Accountmigratie migrate: Accountmigratie
notifications: Meldingen notifications: Meldingen
preferences: Voorkeuren preferences: Voorkeuren
profile: Profiel
relationships: Volgers en gevolgden relationships: Volgers en gevolgden
two_factor_authentication: Tweestapsverificatie two_factor_authentication: Tweestapsverificatie
statuses: statuses:

View File

@ -68,6 +68,7 @@ pt-BR:
admin: Administrador admin: Administrador
bot: Robô bot: Robô
moderator: Moderador moderator: Moderador
unavailable: Perfil indisponível
unfollow: Deixar de seguir unfollow: Deixar de seguir
admin: admin:
account_actions: account_actions:
@ -80,6 +81,7 @@ pt-BR:
destroyed_msg: Nota de moderação excluída com sucesso! destroyed_msg: Nota de moderação excluída com sucesso!
accounts: accounts:
approve: Aprovar approve: Aprovar
approve_all: Aprovar tudo
are_you_sure: Você tem certeza? are_you_sure: Você tem certeza?
avatar: Avatar avatar: Avatar
by_domain: Domínio by_domain: Domínio
@ -132,6 +134,7 @@ pt-BR:
moderation_notes: Notas de moderação moderation_notes: Notas de moderação
most_recent_activity: Atividade mais recente most_recent_activity: Atividade mais recente
most_recent_ip: IP mais recente most_recent_ip: IP mais recente
no_account_selected: Nenhuma conta foi modificada, pois nenhuma conta foi selecionada
no_limits_imposed: Nenhum limite imposto no_limits_imposed: Nenhum limite imposto
not_subscribed: Não está inscrito not_subscribed: Não está inscrito
outbox_url: URL da caixa de saída outbox_url: URL da caixa de saída
@ -144,6 +147,7 @@ pt-BR:
push_subscription_expires: Inscrição PuSH expira push_subscription_expires: Inscrição PuSH expira
redownload: Atualizar perfil redownload: Atualizar perfil
reject: Rejeitar reject: Rejeitar
reject_all: Rejeitar tudo
remove_avatar: Remover avatar remove_avatar: Remover avatar
remove_header: Remover cabeçalho remove_header: Remover cabeçalho
resend_confirmation: resend_confirmation:
@ -330,6 +334,8 @@ pt-BR:
expired: Expirados expired: Expirados
title: Filtro title: Filtro
title: Convites title: Convites
pending_accounts:
title: Contas pendentes (%{count})
relays: relays:
add_new: Adicionar novo repetidor add_new: Adicionar novo repetidor
delete: Excluir delete: Excluir
@ -854,6 +860,9 @@ pt-BR:
revoke_success: Sessão revogada com sucesso revoke_success: Sessão revogada com sucesso
title: Sessões title: Sessões
settings: settings:
account: Conta
account_settings: Configurações da conta
appearance: Aparência
authorized_apps: Apps autorizados authorized_apps: Apps autorizados
back: Voltar para o Mastodon back: Voltar para o Mastodon
delete: Exclusão de conta delete: Exclusão de conta
@ -863,9 +872,11 @@ pt-BR:
featured_tags: Hashtags em destaque featured_tags: Hashtags em destaque
identity_proofs: Provas de identidade identity_proofs: Provas de identidade
import: Importar import: Importar
import_and_export: Importar e exportar
migrate: Migração de conta migrate: Migração de conta
notifications: Notificações notifications: Notificações
preferences: Preferências preferences: Preferências
profile: Perfil
relationships: Seguindo e seguidores relationships: Seguindo e seguidores
two_factor_authentication: Autenticação em dois passos two_factor_authentication: Autenticação em dois passos
statuses: statuses:

View File

@ -41,6 +41,8 @@ nl:
name: 'Je wilt misschien een van deze gebruiken:' name: 'Je wilt misschien een van deze gebruiken:'
imports: imports:
data: CSV-bestand dat op een andere Mastodonserver werd geëxporteerd data: CSV-bestand dat op een andere Mastodonserver werd geëxporteerd
invite_request:
text: Dit helpt ons om jouw aanvraag te beoordelen
sessions: sessions:
otp: 'Voer de tweestaps-aanmeldcode vanaf jouw mobiele telefoon in of gebruik een van jouw herstelcodes:' otp: 'Voer de tweestaps-aanmeldcode vanaf jouw mobiele telefoon in of gebruik een van jouw herstelcodes:'
user: user:
@ -118,12 +120,15 @@ nl:
must_be_follower: Meldingen van mensen die jou niet volgen blokkeren must_be_follower: Meldingen van mensen die jou niet volgen blokkeren
must_be_following: Meldingen van mensen die jij niet volgt blokkeren must_be_following: Meldingen van mensen die jij niet volgt blokkeren
must_be_following_dm: Directe berichten van mensen die jij niet volgt blokkeren must_be_following_dm: Directe berichten van mensen die jij niet volgt blokkeren
invite_request:
text: Waarom wil jij je aanmelden?
notification_emails: notification_emails:
digest: Periodiek e-mails met een samenvatting versturen digest: Periodiek e-mails met een samenvatting versturen
favourite: Een e-mail versturen wanneer iemand jouw toot aan hun favorieten heeft toegevoegd favourite: Een e-mail versturen wanneer iemand jouw toot aan hun favorieten heeft toegevoegd
follow: Een e-mail versturen wanneer iemand jou volgt follow: Een e-mail versturen wanneer iemand jou volgt
follow_request: Een e-mail versturen wanneer iemand jou wil volgen follow_request: Een e-mail versturen wanneer iemand jou wil volgen
mention: Een e-mail versturen wanneer iemand jou vermeld mention: Een e-mail versturen wanneer iemand jou vermeld
pending_account: Een e-mail verzenden wanneer een nieuw account moet worden beoordeeld
reblog: Een e-mail versturen wanneer iemand jouw toot heeft geboost reblog: Een e-mail versturen wanneer iemand jouw toot heeft geboost
report: Verstuur een e-mail wanneer een nieuw rapportage is ingediend report: Verstuur een e-mail wanneer een nieuw rapportage is ingediend
'no': Nee 'no': Nee

View File

@ -29,8 +29,8 @@ pl:
setting_aggregate_reblogs: Nie pokazuj nowych podbić dla wpisów, które zostały niedawno podbite (dotyczy tylko nowo otrzymanych podbić) setting_aggregate_reblogs: Nie pokazuj nowych podbić dla wpisów, które zostały niedawno podbite (dotyczy tylko nowo otrzymanych podbić)
setting_default_language: Język Twoich wpisów może być wykrywany automatycznie, ale nie zawsze jest to dokładne setting_default_language: Język Twoich wpisów może być wykrywany automatycznie, ale nie zawsze jest to dokładne
setting_display_media_default: Ukrywaj zawartość oznaczoną jako wrażliwa setting_display_media_default: Ukrywaj zawartość oznaczoną jako wrażliwa
setting_display_media_hide_all: Zawsze ukrywaj zawartość multimedialną setting_display_media_hide_all: Zawsze oznaczaj zawartość multimedialną jako wrażliwą
setting_display_media_show_all: Zawsze pokazuj zawartość multimedialną jako wrażliwą setting_display_media_show_all: Nie ukrywaj zawartości multimedialnej oznaczonej jako wrażliwa
setting_hide_network: Informacje o tym, kto Cię śledzi i kogo śledzisz nie będą widoczne setting_hide_network: Informacje o tym, kto Cię śledzi i kogo śledzisz nie będą widoczne
setting_noindex: Wpływa na widoczność strony profilu i Twoich wpisów setting_noindex: Wpływa na widoczność strony profilu i Twoich wpisów
setting_show_application: W informacjach o wpisie będzie widoczna informacja o aplikacji, z której został wysłany setting_show_application: W informacjach o wpisie będzie widoczna informacja o aplikacji, z której został wysłany

View File

@ -41,6 +41,8 @@ pt-BR:
name: 'Você pode querer usar um destes:' name: 'Você pode querer usar um destes:'
imports: imports:
data: Arquivo CSV exportado de outra instância do Mastodon data: Arquivo CSV exportado de outra instância do Mastodon
invite_request:
text: Isso vai nos ajudar a revisar sua aplicação
sessions: sessions:
otp: 'Insira o código de autenticação gerado pelo app no seu celular ou use um dos códigos de recuperação:' otp: 'Insira o código de autenticação gerado pelo app no seu celular ou use um dos códigos de recuperação:'
user: user:
@ -119,12 +121,15 @@ pt-BR:
must_be_follower: Bloquear notificações de não-seguidores must_be_follower: Bloquear notificações de não-seguidores
must_be_following: Bloquear notificações de pessoas que você não segue must_be_following: Bloquear notificações de pessoas que você não segue
must_be_following_dm: Bloquear mensagens diretas de pessoas que você não segue must_be_following_dm: Bloquear mensagens diretas de pessoas que você não segue
invite_request:
text: Por que você quer se cadastrar?
notification_emails: notification_emails:
digest: Mandar e-mails com relatórios digest: Mandar e-mails com relatórios
favourite: Mandar um e-mail quando alguém favoritar suas postagens favourite: Mandar um e-mail quando alguém favoritar suas postagens
follow: Mandar um e-mail quando alguém te seguir follow: Mandar um e-mail quando alguém te seguir
follow_request: Mandar um e-maill quando alguém solicitar ser seu seguidor follow_request: Mandar um e-maill quando alguém solicitar ser seu seguidor
mention: Mandar um e-mail quando alguém te mencionar mention: Mandar um e-mail quando alguém te mencionar
pending_account: Mandar um -mail quando uma nova conta precisar ser revisada
reblog: Mandar um e-mail quando alguém compartilhar suas postagens reblog: Mandar um e-mail quando alguém compartilhar suas postagens
report: Mandar um e-mail quando uma nova denúncia é submetida report: Mandar um e-mail quando uma nova denúncia é submetida
'no': Não 'no': Não

View File

@ -10,7 +10,7 @@ sk:
api: API api: API
apps: Aplikácie apps: Aplikácie
apps_platforms: Uživaj Mastodon z iOSu, Androidu a iných platforiem apps_platforms: Uživaj Mastodon z iOSu, Androidu a iných platforiem
browse_directory: Prehľadávaj databázu profilov a filtruj ju podľa záujmov browse_directory: Prehľadávaj databázu profilov, filtruj podľa záujmov
browse_public_posts: Prebádaj naživo prúd verejných príspevkov na Mastodone browse_public_posts: Prebádaj naživo prúd verejných príspevkov na Mastodone
contact: Kontakt contact: Kontakt
contact_missing: Nezadaný contact_missing: Nezadaný
@ -20,9 +20,9 @@ sk:
extended_description_html: | extended_description_html: |
<h3>Pravidlá</h3> <h3>Pravidlá</h3>
<p>Žiadne zatiaľ uvedené nie sú</p> <p>Žiadne zatiaľ uvedené nie sú</p>
federation_hint_html: S účtom na %{instance} budeš môcť následovať ľúdí na hociakom inom Mastodon serveri, ale aj inde. federation_hint_html: S účtom na %{instance} budeš môcť následovať ľúdí na hociakom Mastodon serveri, ale aj inde.
generic_description: "%{domain} je jeden server v sieti" generic_description: "%{domain} je jeden server v sieti"
get_apps: Vyskúšaj mobilnú aplikáciu get_apps: Vyskúšaj aplikácie
hosted_on: Mastodon hostovaný na %{domain} hosted_on: Mastodon hostovaný na %{domain}
learn_more: Zisti viac learn_more: Zisti viac
privacy_policy: Ustanovenia o súkromí privacy_policy: Ustanovenia o súkromí
@ -32,22 +32,22 @@ sk:
status_count_after: status_count_after:
few: príspevkov few: príspevkov
one: príspevok one: príspevok
other: príspevkov other: príspevky
status_count_before: Ktorí napísali status_count_before: Ktorí napísali
tagline: Následuj kamarátov, a objavuj nových tagline: Následuj kamarátov, a objavuj nových
terms: Podmienky užívania terms: Podmienky užívania
user_count_after: user_count_after:
few: užívatelia few: užívateľov
one: užívateľ one: užívateľ
other: užívateľov other: užívatelia
user_count_before: Domov pre user_count_before: Domov pre
what_is_mastodon: Čo je Mastodon? what_is_mastodon: Čo je Mastodon?
accounts: accounts:
choices_html: "%{name}vé voľby:" choices_html: "%{name}vé voľby:"
follow: Sleduj follow: Následuj
followers: followers:
few: Sledovatelia few: Sledovateľov
one: Sledujúci one: Sledovateľ
other: Sledovatelia other: Sledovatelia
following: Sledovaní following: Sledovaní
joined: Pridal/a sa v %{date} joined: Pridal/a sa v %{date}
@ -70,8 +70,9 @@ sk:
reserved_username: Prihlasovacie meno je rezervované reserved_username: Prihlasovacie meno je rezervované
roles: roles:
admin: Administrátor admin: Administrátor
bot: Automat bot: Bot
moderator: Moderátor moderator: Moderátor
unavailable: Profil nieje dostupný
unfollow: Prestaň sledovať unfollow: Prestaň sledovať
admin: admin:
account_actions: account_actions:
@ -84,12 +85,13 @@ sk:
destroyed_msg: Moderátorska poznámka bola úspešne zmazaná! destroyed_msg: Moderátorska poznámka bola úspešne zmazaná!
accounts: accounts:
approve: Schváľ approve: Schváľ
approve_all: Schváľ všetky
are_you_sure: Si si istý/á? are_you_sure: Si si istý/á?
avatar: Maskot avatar: Maskot
by_domain: Doména by_domain: Doména
change_email: change_email:
changed_msg: Email k tomuto účtu bol úspešne zmenený! changed_msg: Email pre tento účet bol úspešne zmenený!
current_email: Súčastný email current_email: Súčasný email
label: Zmeň email label: Zmeň email
new_email: Nový email new_email: Nový email
submit: Zmeň email submit: Zmeň email
@ -97,37 +99,37 @@ sk:
confirm: Potvrď confirm: Potvrď
confirmed: Potvrdený confirmed: Potvrdený
confirming: Potvrdzujúci confirming: Potvrdzujúci
deleted: Zmazané deleted: Vymazané
demote: Degradovať demote: Degraduj
disable: Zablokuj disable: Zablokuj
disable_two_factor_authentication: Zakáž 2FA disable_two_factor_authentication: Zakáž 2FA
disabled: Blokovaný disabled: Blokovaný
display_name: Zobraziť meno display_name: Ukáž meno
domain: Doména domain: Doména
edit: Uprav edit: Uprav
email: Email email: Email
email_status: Stav emailu email_status: Stav emailu
enable: Povoliť enable: Povoľ
enabled: Povolený enabled: Povolený
feed_url: URL časovej osi feed_url: URL adresa časovej osi
followers: Sledujúci followers: Sledujúci
followers_url: URL sledujúcich followers_url: URL adresa sledujúcich
follows: Sledovania follows: Sledovania
header: Hlavička header: Hlavička
inbox_url: URL prijatých správ inbox_url: URL adresa prijatých správ
invited_by: Pozvaný/á užívateľom invited_by: Pozvaný/á užívateľom
ip: IP ip: IP adresa
joined: Pridal/a sa joined: Pridal/a sa
location: location:
all: Všetko all: Všetko
local: Miestne local: Miestne
remote: Federované remote: Federované
title: Lokácia title: Umiestnenie
login_status: Stav prihlásenia login_status: Stav prihlásenia
media_attachments: Prílohy media_attachments: Prílohy
memorialize: Zmeniť na "Navždy budeme spomínať" memorialize: Zmeň na "Navždy budeme spomínať"
moderation: moderation:
active: Aktívny active: Aktívny/a
all: Všetko all: Všetko
pending: Čakajúci pending: Čakajúci
silenced: Umlčané silenced: Umlčané
@ -135,21 +137,22 @@ sk:
title: Moderácia title: Moderácia
moderation_notes: Moderátorské poznámky moderation_notes: Moderátorské poznámky
most_recent_activity: Posledná aktivita most_recent_activity: Posledná aktivita
most_recent_ip: Posledná IP most_recent_ip: Posledná IP adresa
no_limits_imposed: Nie sú stanovené žiadné obmedzenia no_limits_imposed: Nie sú stanovené žiadné obmedzenia
not_subscribed: Neodoberá not_subscribed: Neodoberá
outbox_url: URL poslaných outbox_url: URL poslaných
pending: Vyžaduje posúdenie pending: Vyžaduje posúdenie
perform_full_suspension: Vylúč perform_full_suspension: Vylúč
profile_url: URL profilu profile_url: URL adresa profilu
promote: Povýš promote: Vyzdvihni
protocol: Protokol protocol: Protokol
public: Verejná os public: Verejná časová os
push_subscription_expires: PuSH odoberanie expiruje push_subscription_expires: PuSH odoberanie expiruje
redownload: Obnov profil redownload: Obnov profil
reject: Odmietni reject: Zamietni
remove_avatar: Odstrániť avatár reject_all: Zamietni všetky
remove_header: Odstráň hlavičku remove_avatar: Vymaž avatar
remove_header: Vymaž hlavičku
resend_confirmation: resend_confirmation:
already_confirmed: Tento užívateľ je už potvrdený already_confirmed: Tento užívateľ je už potvrdený
send: Odošli potvrdzovací email znovu send: Odošli potvrdzovací email znovu
@ -164,7 +167,7 @@ sk:
staff: Člen staff: Člen
user: Užívateľ user: Užívateľ
salmon_url: Salmon adresa salmon_url: Salmon adresa
search: Hľadať search: Hľadaj
shared_inbox_url: URL zdieľanej schránky shared_inbox_url: URL zdieľanej schránky
show: show:
created_reports: Vytvorené hlásenia created_reports: Vytvorené hlásenia
@ -172,15 +175,15 @@ sk:
silence: Stíš silence: Stíš
silenced: Utíšený/é silenced: Utíšený/é
statuses: Príspevky statuses: Príspevky
subscribe: Odoberať subscribe: Odoberaj
suspended: Zablokovaní suspended: Zablokovaní
title: Účty title: Účty
unconfirmed_email: Nepotvrdený email unconfirmed_email: Nepotvrdený email
undo_silenced: Zruš stíšenie undo_silenced: Zruš stíšenie
undo_suspension: Zruš blokovanie undo_suspension: Zruš blokovanie
unsubscribe: Prestaň odoberať unsubscribe: Prestaň odoberať
username: Prezývka username: Prezývka
warn: Varovať warn: Varuj
web: Web web: Web
action_logs: action_logs:
actions: actions:
@ -227,7 +230,7 @@ sk:
disable: Zakázať disable: Zakázať
disabled_msg: Emoji bolo úspešne zakázané disabled_msg: Emoji bolo úspešne zakázané
emoji: Emotikony emoji: Emotikony
enable: Povoliť enable: Povoľ
enabled_msg: Emoji bolo úspešne povolené enabled_msg: Emoji bolo úspešne povolené
image_hint: PNG do 50KB image_hint: PNG do 50KB
listed: V zozname listed: V zozname
@ -240,7 +243,7 @@ sk:
unlisted: Nie je na zozname unlisted: Nie je na zozname
update_failed_msg: Nebolo možné aktualizovať toto emoji update_failed_msg: Nebolo možné aktualizovať toto emoji
updated_msg: Emoji bolo úspešne aktualizované! updated_msg: Emoji bolo úspešne aktualizované!
upload: Nahrať upload: Nahraj
dashboard: dashboard:
backlog: odložené aktivity backlog: odložené aktivity
config: Nastavenia config: Nastavenia
@ -249,10 +252,11 @@ sk:
feature_profile_directory: Katalóg profilov feature_profile_directory: Katalóg profilov
feature_registrations: Registrácie feature_registrations: Registrácie
feature_relay: Federovací mostík feature_relay: Federovací mostík
feature_timeline_preview: Náhľad časovej osi
features: Vymoženosti features: Vymoženosti
hidden_service: Federácia so skrytými službami hidden_service: Federácia so skrytými službami
open_reports: otvorené hlásenia open_reports: otvorené hlásenia
recent_users: Nedávny užívatelia recent_users: Nedávni užívatelia
search: Celofrázové vyhľadávanie search: Celofrázové vyhľadávanie
single_user_mode: Jednouživateľské rozhranie single_user_mode: Jednouživateľské rozhranie
software: Softvér software: Softvér
@ -264,10 +268,11 @@ sk:
week_users_active: aktívni tento týždeň week_users_active: aktívni tento týždeň
week_users_new: užívateľov počas tohto týždňa week_users_new: užívateľov počas tohto týždňa
domain_blocks: domain_blocks:
add_new: Pridaj nové doménové blokovanie add_new: Blokuj novú doménu
created_msg: Doména je v procese blokovania created_msg: Doména je v štádiu blokovania
destroyed_msg: Blokovanie domény bolo zrušené destroyed_msg: Blokovanie domény bolo zrušené
domain: Doména domain: Doména
existing_domain_block_html: Pre účet %{name} si už nahodil/a přísnejšie obmedzenie, najskôr ho teda musíš <a href="%{unblock_url}">odblokovať</a>.
new: new:
create: Vytvor blokovanie domény create: Vytvor blokovanie domény
hint: Blokovanie domény stále dovolí vytvárať nové účty v databázi, ale tieto budú spätne automaticky moderované. hint: Blokovanie domény stále dovolí vytvárať nové účty v databázi, ale tieto budú spätne automaticky moderované.
@ -284,7 +289,7 @@ sk:
rejecting_media: odmietanie médiálnych súborov rejecting_media: odmietanie médiálnych súborov
rejecting_reports: odmietané hlásenia rejecting_reports: odmietané hlásenia
severity: severity:
silence: utíšené silence: stíšený
suspend: vylúčený suspend: vylúčený
show: show:
affected_accounts: affected_accounts:
@ -295,12 +300,12 @@ sk:
silence: Zruš stíšenie všetkých existujúcich účtov z tejto domény silence: Zruš stíšenie všetkých existujúcich účtov z tejto domény
suspend: Zruš suspendáciu všetkých existujúcich účtov z tejto domény suspend: Zruš suspendáciu všetkých existujúcich účtov z tejto domény
title: Zruš blokovanie domény %{domain} title: Zruš blokovanie domény %{domain}
undo: Vrátiť späť undo: Vráť späť
undo: Odvolaj blokovanie domény undo: Odvolaj blokovanie domény
email_domain_blocks: email_domain_blocks:
add_new: Pridaj nový add_new: Pridaj nový
created_msg: Emailová doména bola úspešne pridaná do zoznamu zakázaných created_msg: Emailová doména bola úspešne pridaná do zoznamu zakázaných
delete: Zmazať delete: Vymaž
destroyed_msg: Emailová doména bola úspešne vymazaná zo zoznamu zakázaných destroyed_msg: Emailová doména bola úspešne vymazaná zo zoznamu zakázaných
domain: Doména domain: Doména
new: new:
@ -328,15 +333,15 @@ sk:
total_reported: Nahlásenia o nich total_reported: Nahlásenia o nich
total_storage: Mediálne prílohy total_storage: Mediálne prílohy
invites: invites:
deactivate_all: Pozastav všetky deactivate_all: Pozastav všetky
filter: filter:
all: Všetky all: Všetky
available: Dostupné available: Dostupné
expired: Vypršalo expired: Vypršalo
title: Filtrovať title: Filtruj
title: Pozvánky title: Pozvánky
relays: relays:
add_new: Pridaj novú priechodnú oporu add_new: Pridaj nový federovací mostík
delete: Vymaž delete: Vymaž
description_html: "<strong>Federovací mostík</strong> je prechodný server ktorý obmieňa veľké množstvá verejných príspevkov medzi tými servermi ktoré na od neho odoberajú, aj doňho prispievajú. <strong>Môže to pomôcť malým a stredným instanciám objavovať federovaný obsah</strong>, čo inak vyžaduje aby miestni užívatelia ručne následovali iných ľudí zo vzdialených instancií." description_html: "<strong>Federovací mostík</strong> je prechodný server ktorý obmieňa veľké množstvá verejných príspevkov medzi tými servermi ktoré na od neho odoberajú, aj doňho prispievajú. <strong>Môže to pomôcť malým a stredným instanciám objavovať federovaný obsah</strong>, čo inak vyžaduje aby miestni užívatelia ručne následovali iných ľudí zo vzdialených instancií."
disable: Pozastav disable: Pozastav
@ -344,7 +349,7 @@ sk:
enable: Povoľ enable: Povoľ
enable_hint: Ak povolíš, tvoj server bude odoberať všetky verejné príspevky z tohto mostu, a začne posielať verejné príspevky tvojho servera na tento most. enable_hint: Ak povolíš, tvoj server bude odoberať všetky verejné príspevky z tohto mostu, a začne posielať verejné príspevky tvojho servera na tento most.
enabled: Povolené enabled: Povolené
inbox_url: URL mostu inbox_url: URL adresa mostu
pending: Čakám na povolenie od prechodného mostu pending: Čakám na povolenie od prechodného mostu
save_and_enable: Uložiť a povoliť save_and_enable: Uložiť a povoliť
setup: Nastav prepojenie s mostom setup: Nastav prepojenie s mostom
@ -359,7 +364,7 @@ sk:
report: nahlás report: nahlás
action_taken_by: Zákrok vykonal/a action_taken_by: Zákrok vykonal/a
are_you_sure: Si si istý/á? are_you_sure: Si si istý/á?
assign_to_self: Priraď k sebe assign_to_self: Priraď sebe
assigned: Priradený moderátor assigned: Priradený moderátor
comment: comment:
none: Žiadne none: Žiadne
@ -379,7 +384,7 @@ sk:
resolved: Vyriešené resolved: Vyriešené
resolved_msg: Hlásenie úspešne vyriešené! resolved_msg: Hlásenie úspešne vyriešené!
status: Stav status: Stav
title: Reporty title: Hlásenia
unassign: Odobrať unassign: Odobrať
unresolved: Nevyriešené unresolved: Nevyriešené
updated_at: Aktualizované updated_at: Aktualizované
@ -391,7 +396,7 @@ sk:
desc_html: Ak je prezývok viacero, každú oddeľte čiarkou. Možno zadať iba miestne, odomknuté účty. Pokiaľ necháte prázdne, je to pre všetkých miestnych administrátorov. desc_html: Ak je prezývok viacero, každú oddeľte čiarkou. Možno zadať iba miestne, odomknuté účty. Pokiaľ necháte prázdne, je to pre všetkých miestnych administrátorov.
title: Štandardní následovníci nových užívateľov title: Štandardní následovníci nových užívateľov
contact_information: contact_information:
email: Pracovný e-mail email: Pracovný email
username: Kontaktné užívateľské meno username: Kontaktné užívateľské meno
custom_css: custom_css:
desc_html: Uprav vzhľad pomocou CSS, ktoré je načítané na každej stránke desc_html: Uprav vzhľad pomocou CSS, ktoré je načítané na každej stránke
@ -508,11 +513,13 @@ sk:
invalid_url: Zadaná URL adresa je nesprávna invalid_url: Zadaná URL adresa je nesprávna
regenerate_token: Znovu vygenerovať prístupový token regenerate_token: Znovu vygenerovať prístupový token
token_regenerated: Prístupový token bol úspešne vygenerovaný znova token_regenerated: Prístupový token bol úspešne vygenerovaný znova
warning: Na tieto údaje dávajte ohromný pozor. Nikdy ich s nikým nezďieľajte! warning: Na tieto údaje dávaj ohromný pozor. Nikdy ich s nikým nezďieľaj!
your_token: Váš prístupový token your_token: Tvoj prístupový token
auth: auth:
apply_for_account: Vyžiadaj si pozvánku
change_password: Heslo change_password: Heslo
confirm_email: Potvrdiť email checkbox_agreement_html: Súhlasím s <a href="%{rules_path}" target="_blank">pravidlami servera</a>, aj s <a href="%{terms_path}" target="_blank">prevoznými podmienkami</a>
confirm_email: Potvrď email
delete_account: Vymaž účet delete_account: Vymaž účet
delete_account_html: Pokiaľ chceš svoj účet odtiaľto vymazať, môžeš tak <a href="%{path}">urobiť tu</a>. Budeš požiadaný/á o potvrdenie tohto kroku. delete_account_html: Pokiaľ chceš svoj účet odtiaľto vymazať, môžeš tak <a href="%{path}">urobiť tu</a>. Budeš požiadaný/á o potvrdenie tohto kroku.
didnt_get_confirmation: Neobdržal/a si kroky na potvrdenie? didnt_get_confirmation: Neobdržal/a si kroky na potvrdenie?
@ -521,16 +528,17 @@ sk:
login: Prihlás sa login: Prihlás sa
logout: Odhlás sa logout: Odhlás sa
migrate_account: Presúvam sa na iný účet migrate_account: Presúvam sa na iný účet
migrate_account_html: Pokiaľ si želáš presmerovať tento účet na nejaký iný, môžeš si to <a href="%{path}">nastaviť tu</a>. migrate_account_html: Ak si želáš presmerovať tento účet na nejaký iný, môžeš si to <a href="%{path}">nastaviť tu</a>.
or_log_in_with: Alebo prihlásiť z or_log_in_with: Alebo prihlás s
providers: providers:
cas: CAS cas: CAS
saml: SAML saml: SAML
register: Zaregistruj sa register: Zaregistruj sa
resend_confirmation: Poslať potvrdzujúce pokyny znovu resend_confirmation: Zašli potvrdzujúce pokyny znovu
reset_password: Resetovať heslo reset_password: Obnov heslo
security: Zabezpečenie security: Zabezpečenie
set_new_password: Nastaviť nové heslo set_new_password: Nastav nové heslo
trouble_logging_in: Problém s prihlásením?
authorize_follow: authorize_follow:
already_following: Tento účet už následuješ already_following: Tento účet už následuješ
error: Naneštastie nastala chyba pri hľadaní vzdialeného účtu error: Naneštastie nastala chyba pri hľadaní vzdialeného účtu
@ -636,10 +644,10 @@ sk:
other: Niečo ešte stále nieje v poriadku! Prosím skontroluj všetky %{count} nižšie uvedené pochybenia other: Niečo ešte stále nieje v poriadku! Prosím skontroluj všetky %{count} nižšie uvedené pochybenia
imports: imports:
modes: modes:
merge: Spoj dohromady merge: Spoj dohromady
merge_long: Ponechaj existujúce záznamy a pridaj k nim nové merge_long: Ponechaj existujúce záznamy a pridaj k nim nové
overwrite: Prepíš overwrite: Prepíš
overwrite_long: Nahraď súčasné záznamy s novými overwrite_long: Nahraď súčasné záznamy novými
preface: Môžeš nahrať dáta ktoré si exportoval/a z iného Mastodon serveru, ako sú napríklad zoznamy ľudí ktorých sleduješ, alebo blokuješ. preface: Môžeš nahrať dáta ktoré si exportoval/a z iného Mastodon serveru, ako sú napríklad zoznamy ľudí ktorých sleduješ, alebo blokuješ.
success: Tvoje dáta boli nahraté úspešne, a teraz budú spracované v danom čase success: Tvoje dáta boli nahraté úspešne, a teraz budú spracované v danom čase
types: types:
@ -647,10 +655,10 @@ sk:
domain_blocking: Zoznam blokovaných domén domain_blocking: Zoznam blokovaných domén
following: Zoznam sledovaných following: Zoznam sledovaných
muting: Zoznam ignorovaných muting: Zoznam ignorovaných
upload: Nahrať upload: Nahraj
in_memoriam_html: V pamäti. in_memoriam_html: V pamäti.
invites: invites:
delete: Deaktivovať delete: Deaktivuj
expired: Neplatné expired: Neplatné
expires_in: expires_in:
'1800': 30 minút '1800': 30 minút
@ -661,13 +669,13 @@ sk:
'86400': 1 deň '86400': 1 deň
expires_in_prompt: Nikdy expires_in_prompt: Nikdy
generate: Vygeneruj generate: Vygeneruj
invited_by: 'Bol/a si pozvan/á užívateľom:' invited_by: 'Bol/a si pozvaný/á užívateľom:'
max_uses: max_uses:
few: "%{count} použitia" few: "%{count} použitia"
one: jedno použitie one: jedno použitie
other: "%{count} použití" other: "%{count} použití"
max_uses_prompt: Bez limitov max_uses_prompt: Bez obmedzení
prompt: Vygeneruj a zdieľaj linky s ostatnými aby mali umožnený prístup k tomuto serveru prompt: Vygeneruj a zdieľaj linky s ostatnými, aby mali umožnený prístup k tomuto serveru
table: table:
expires_at: Vyprší expires_at: Vyprší
uses: Používa uses: Používa
@ -692,16 +700,16 @@ sk:
body: Tu nájdete krátky súhrn správ ktoré ste zmeškali od svojej poslednj návštevi od %{since} body: Tu nájdete krátky súhrn správ ktoré ste zmeškali od svojej poslednj návštevi od %{since}
mention: "%{name} ťa spomenul/a v:" mention: "%{name} ťa spomenul/a v:"
new_followers_summary: new_followers_summary:
few: Taktiež, získal/a si %{count} nových následovníkov za tú dobu čo si bol/a preč. Yay! few: Tiež si získal/a %{count} nových následovateľov za tú dobu čo si bol/a preč. Yay!
one: Taktiež, získal/a si jedného nového následovníka zatiaľ čo si bol/a preč. Yay! one: Tiež si získal/a jedného nového následovateľa zatiaľ čo si bol/a preč. Yay!
other: Taktiež, získal/a si %{count} nových následovníkov za tú dobu čo si bol/a preč. Yay! other: Tiež si získal/a %{count} nových následovateľov za tú dobu čo si bol/a preč. Yay!
subject: subject:
few: "%{count} nové notifikácie od tvojej poslednej návštevy \U0001F418" few: "%{count} nové notifikácie od tvojej poslednej návštevy \U0001F418"
one: "1 nová notifikácia od tvojej poslednej návštevy \U0001F418" one: "1 nové oboznámenie od tvojej poslednej návštevy \U0001F418"
other: "%{count} nových notifikácií od tvojej poslednej návštevy \U0001F418" other: "%{count} nových oboznámení od tvojej poslednej návštevy \U0001F418"
title: Zatiaľ čo si bol/a preč… title: Zatiaľ čo si bol/a preč…
favourite: favourite:
body: 'Tvoj príspevok bol uložený medi obľúbené užívateľa %{name}:' body: 'Tvoj príspevok bol uložený medzi obľúbené užívateľa %{name}:'
subject: "%{name} si obľúbil/a tvoj príspevok" subject: "%{name} si obľúbil/a tvoj príspevok"
title: Nové obľúbené title: Nové obľúbené
follow: follow:
@ -712,16 +720,16 @@ sk:
action: Spravuj žiadosti o sledovanie action: Spravuj žiadosti o sledovanie
body: "%{name} žiada povolenie ťa následovať" body: "%{name} žiada povolenie ťa následovať"
subject: "%{name} ťa žiadá o možnosť sledovania" subject: "%{name} ťa žiadá o možnosť sledovania"
title: Nová žiadosť o sledovanie title: Nová žiadosť o sledovanie
mention: mention:
action: Odpovedať action: Odpovedať
body: "%{name} ťa spomenul/a v:" body: "%{name} ťa spomenul/a v:"
subject: Bol/a si spomenutý/á užívateľom %{name} subject: Bol/a si spomenutý/á užívateľom %{name}
title: Novo spomenutý/á title: Novo spomenutý/á
reblog: reblog:
body: 'Tvoj príspevok bol pozdvihnutý užívateľom %{name}:' body: 'Tvoj príspevok bol vyzdvihnutý užívateľom %{name}:'
subject: "%{name} pozdvihli tvoj príspevok" subject: "%{name} vyzdvihli tvoj príspevok"
title: Novo pozdvyhnuté title: Novo vyzdvyhnuté
number: number:
human: human:
decimal_units: decimal_units:
@ -820,17 +828,23 @@ sk:
revoke_success: Sezóna úspešne zamietnutá revoke_success: Sezóna úspešne zamietnutá
title: Sezóny title: Sezóny
settings: settings:
account: Účet
account_settings: Nastavenia účtu
appearance: Vzhľad
authorized_apps: Povolené aplikácie authorized_apps: Povolené aplikácie
back: Späť na Mastodon back: Späť na Mastodon
delete: Vymazanie účtu delete: Vymazanie účtu
development: Vývoj development: Vývoj
edit_profile: Uprav profil edit_profile: Uprav profil
export: Exportovať dáta export: Exportuj dáta
featured_tags: Popredne zvýraznené haštagy featured_tags: Zvýraznené haštagy
import: Importovať import: Importuj
migrate: Presunutie účtu import_and_export: Import a export
notifications: Oznámenia migrate: Presuň účet
notifications: Oboznámenia
preferences: Voľby preferences: Voľby
profile: Profil
relationships: Následovaní a následovatelia
two_factor_authentication: Dvoj-faktorové overenie two_factor_authentication: Dvoj-faktorové overenie
statuses: statuses:
attached: attached:
@ -846,9 +860,9 @@ sk:
boosted_from_html: Povýšené od %{acct_link} boosted_from_html: Povýšené od %{acct_link}
content_warning: 'Varovanie o obsahu: %{warning}' content_warning: 'Varovanie o obsahu: %{warning}'
disallowed_hashtags: disallowed_hashtags:
few: 'obsahoval nepovolené hashtagy: %{tags}' few: 'obsahoval nepovolené haštagy: %{tags}'
one: 'obsahoval nepovolený hashtag: %{tags}' one: 'obsahoval nepovolený haštag: %{tags}'
other: 'obsahoval nepovolené hashtagy: %{tags}' other: 'obsahoval nepovolené haštagy: %{tags}'
language_detection: Zisti automaticky language_detection: Zisti automaticky
open_in_web: Otvor v okne na webe open_in_web: Otvor v okne na webe
over_character_limit: limit %{max} znakov bol presiahnutý over_character_limit: limit %{max} znakov bol presiahnutý
@ -856,7 +870,7 @@ sk:
limit: Už si si pripol ten najvyšší možný počet hlášok limit: Už si si pripol ten najvyšší možný počet hlášok
ownership: Nieje možné pripnúť hlášku od niekoho iného ownership: Nieje možné pripnúť hlášku od niekoho iného
private: Neverejné príspevky nemôžu byť pripnuté private: Neverejné príspevky nemôžu byť pripnuté
reblog: Pozdvihnutie sa nedá pripnúť reblog: Vyzdvihnutie sa nedá pripnúť
poll: poll:
total_votes: total_votes:
few: "%{count} hlas(y)ov" few: "%{count} hlas(y)ov"
@ -874,14 +888,14 @@ sk:
unlisted: Nezaradené unlisted: Nezaradené
unlisted_long: Všetci môžu vidieť, ale nieje zaradené do verejnej osi unlisted_long: Všetci môžu vidieť, ale nieje zaradené do verejnej osi
stream_entries: stream_entries:
pinned: Pripnutý toot pinned: Pripnutý príspevok
reblogged: vyzdvihnutý reblogged: vyzdvihnutý
sensitive_content: Senzitívny obsah sensitive_content: Senzitívny obsah
terms: terms:
body_html: | body_html: |
<h2>Podmienky súkromia</h2> <h2>Podmienky súkromia</h2>
<h3 id="collect">Aké informácie zbierame?</h3> <h3 id="collect">Aké informácie sú zbierané?</h3>
<ul> <ul>
<li><em>Základné informácie o účte</em>: Ak sa na tomto serveri zaregistruješ, budeš môcť byť požiadaný/á zadať prezývku, emailovú adresu a heslo. Budeš tiež môcť zadať aj ďalšie profilové údaje, ako napríklad meno a životopis, a nahrať profilovú fotku aj obrázok v záhlaví. Tvoja prezývka, meno, životopis, profilová fotka a obrázok v záhlaví sú vždy zobrazené verejne.</li><li><em>Príspevky, sledovania a iné verejné informácie</em>: <li><em>Základné informácie o účte</em>: Ak sa na tomto serveri zaregistruješ, budeš môcť byť požiadaný/á zadať prezývku, emailovú adresu a heslo. Budeš tiež môcť zadať aj ďalšie profilové údaje, ako napríklad meno a životopis, a nahrať profilovú fotku aj obrázok v záhlaví. Tvoja prezývka, meno, životopis, profilová fotka a obrázok v záhlaví sú vždy zobrazené verejne.</li><li><em>Príspevky, sledovania a iné verejné informácie</em>:
@ -973,7 +987,7 @@ sk:
invalid_otp_token: Neplatný kód pre dvojfaktorovú autentikáciu invalid_otp_token: Neplatný kód pre dvojfaktorovú autentikáciu
otp_lost_help_html: Pokiaľ si stratil/a prístup k obom, môžeš dať vedieť %{email} otp_lost_help_html: Pokiaľ si stratil/a prístup k obom, môžeš dať vedieť %{email}
seamless_external_login: Si prihlásená/ý cez externú službu, takže nastavenia hesla a emailu ti niesú prístupné. seamless_external_login: Si prihlásená/ý cez externú službu, takže nastavenia hesla a emailu ti niesú prístupné.
signed_in_as: 'Prihlásený ako:' signed_in_as: 'Prihlásená/ý ako:'
verification: verification:
explanation_html: 'Môžeš sa <strong>overiť ako majiteľ odkazov v metadátach tvojho profilu</strong>. Na to musí ale odkazovaná stránka obsahovať odkaz späť na tvoj Mastodon profil. Tento spätný odkaz <strong>musí</strong> mať prívlastok <code>rel="me"</code>. Na texte odkazu nezáleží. Tu je príklad:' explanation_html: 'Môžeš sa <strong>overiť ako majiteľ odkazov v metadátach tvojho profilu</strong>. Na to musí ale odkazovaná stránka obsahovať odkaz späť na tvoj Mastodon profil. Tento spätný odkaz <strong>musí</strong> mať prívlastok <code>rel="me"</code>. Na texte odkazu nezáleží. Tu je príklad:'
verification: Overenie verification: Overenie

View File

@ -0,0 +1,5 @@
class AddBlurhashToMediaAttachments < ActiveRecord::Migration[5.2]
def change
add_column :media_attachments, :blurhash, :string
end
end

View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_04_09_054914) do ActiveRecord::Schema.define(version: 2019_04_20_025523) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -362,6 +362,7 @@ ActiveRecord::Schema.define(version: 2019_04_09_054914) do
t.bigint "account_id" t.bigint "account_id"
t.text "description" t.text "description"
t.bigint "scheduled_status_id" t.bigint "scheduled_status_id"
t.string "blurhash"
t.index ["account_id"], name: "index_media_attachments_on_account_id" t.index ["account_id"], name: "index_media_attachments_on_account_id"
t.index ["scheduled_status_id"], name: "index_media_attachments_on_scheduled_status_id" t.index ["scheduled_status_id"], name: "index_media_attachments_on_scheduled_status_id"
t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true

View File

@ -43,7 +43,7 @@ services:
- external_network - external_network
- internal_network - internal_network
healthcheck: healthcheck:
test: ["CMD-SHELL", "wget -q --spider --header 'x-forwarded-proto: https' --proxy off localhost:3000/api/v1/instance || exit 1"] test: ["CMD-SHELL", "wget -q --spider --header 'x-forwarded-proto: https' --proxy=off localhost:3000/api/v1/instance || exit 1"]
ports: ports:
- "127.0.0.1:3000:3000" - "127.0.0.1:3000:3000"
depends_on: depends_on:
@ -63,7 +63,7 @@ services:
- external_network - external_network
- internal_network - internal_network
healthcheck: healthcheck:
test: ["CMD-SHELL", "wget -q --spider --header 'x-forwarded-proto: https' --proxy off localhost:4000/api/v1/streaming/health || exit 1"] test: ["CMD-SHELL", "wget -q --spider --header 'x-forwarded-proto: https' --proxy=off localhost:4000/api/v1/streaming/health || exit 1"]
ports: ports:
- "127.0.0.1:4000:4000" - "127.0.0.1:4000:4000"
depends_on: depends_on:

View File

@ -9,6 +9,7 @@ require_relative 'mastodon/search_cli'
require_relative 'mastodon/settings_cli' require_relative 'mastodon/settings_cli'
require_relative 'mastodon/statuses_cli' require_relative 'mastodon/statuses_cli'
require_relative 'mastodon/domains_cli' require_relative 'mastodon/domains_cli'
require_relative 'mastodon/cache_cli'
require_relative 'mastodon/version' require_relative 'mastodon/version'
module Mastodon module Mastodon
@ -41,6 +42,9 @@ module Mastodon
desc 'domains SUBCOMMAND ...ARGS', 'Manage account domains' desc 'domains SUBCOMMAND ...ARGS', 'Manage account domains'
subcommand 'domains', Mastodon::DomainsCLI subcommand 'domains', Mastodon::DomainsCLI
desc 'cache SUBCOMMAND ...ARGS', 'Manage cache'
subcommand 'cache', Mastodon::CacheCLI
option :dry_run, type: :boolean option :dry_run, type: :boolean
desc 'self-destruct', 'Erase the server from the federation' desc 'self-destruct', 'Erase the server from the federation'
long_desc <<~LONG_DESC long_desc <<~LONG_DESC

View File

@ -73,7 +73,7 @@ module Mastodon
def create(username) def create(username)
account = Account.new(username: username) account = Account.new(username: username)
password = SecureRandom.hex password = SecureRandom.hex
user = User.new(email: options[:email], password: password, agreement: true, admin: options[:role] == 'admin', moderator: options[:role] == 'moderator', confirmed_at: options[:confirmed] ? Time.now.utc : nil) user = User.new(email: options[:email], password: password, agreement: true, approved: true, admin: options[:role] == 'admin', moderator: options[:role] == 'moderator', confirmed_at: options[:confirmed] ? Time.now.utc : nil)
if options[:reattach] if options[:reattach]
account = Account.find_local(username) || Account.new(username: username) account = Account.find_local(username) || Account.new(username: username)
@ -115,6 +115,7 @@ module Mastodon
option :enable, type: :boolean option :enable, type: :boolean
option :disable, type: :boolean option :disable, type: :boolean
option :disable_2fa, type: :boolean option :disable_2fa, type: :boolean
option :approve, type: :boolean
desc 'modify USERNAME', 'Modify a user' desc 'modify USERNAME', 'Modify a user'
long_desc <<-LONG_DESC long_desc <<-LONG_DESC
Modify a user account. Modify a user account.
@ -128,6 +129,9 @@ module Mastodon
With the --disable option, lock the user out of their account. The With the --disable option, lock the user out of their account. The
--enable option is the opposite. --enable option is the opposite.
With the --approve option, the account will be approved, if it was
previously not due to not having open registrations.
With the --disable-2fa option, the two-factor authentication With the --disable-2fa option, the two-factor authentication
requirement for the user can be removed. requirement for the user can be removed.
LONG_DESC LONG_DESC
@ -147,6 +151,7 @@ module Mastodon
user.email = options[:email] if options[:email] user.email = options[:email] if options[:email]
user.disabled = false if options[:enable] user.disabled = false if options[:enable]
user.disabled = true if options[:disable] user.disabled = true if options[:disable]
user.approved = true if options[:approve]
user.otp_required_for_login = false if options[:disable_2fa] user.otp_required_for_login = false if options[:disable_2fa]
user.confirm if options[:confirm] user.confirm if options[:confirm]

19
lib/mastodon/cache_cli.rb Normal file
View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
require_relative '../../config/boot'
require_relative '../../config/environment'
require_relative 'cli_helper'
module Mastodon
class CacheCLI < Thor
def self.exit_on_failure?
true
end
desc 'clear', 'Clear out the cache storage'
def clear
Rails.cache.clear
say('OK', :green)
end
end
end

View File

@ -13,7 +13,7 @@ module Mastodon
end end
def patch def patch
0 2
end end
def pre def pre
@ -33,16 +33,16 @@ module Mastodon
end end
def repository def repository
'tootsuite/mastodon' ENV.fetch('GITHUB_REPOSITORY') { 'tootsuite/mastodon' }
end end
def source_base_url def source_base_url
"https://github.com/#{repository}" ENV.fetch('SOURCE_BASE_URL') { "https://github.com/#{repository}" }
end end
# specify git tag or commit hash here # specify git tag or commit hash here
def source_tag def source_tag
nil ENV.fetch('SOURCE_TAG') { nil }
end end
def source_url def source_url

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module Paperclip
class BlurhashTranscoder < Paperclip::Processor
def make
return @file unless options[:style] == :small
pixels = convert(':source RGB:-', source: File.expand_path(@file.path)).unpack('C*')
geometry = options.fetch(:file_geometry_parser).from_file(@file)
attachment.instance.blurhash = Blurhash.encode(geometry.width, geometry.height, pixels, options[:blurhash] || {})
@file
end
end
end

View File

@ -78,6 +78,7 @@
"babel-plugin-react-intl": "^3.0.1", "babel-plugin-react-intl": "^3.0.1",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24", "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"blurhash": "^1.0.0",
"classnames": "^2.2.5", "classnames": "^2.2.5",
"compression-webpack-plugin": "^2.0.0", "compression-webpack-plugin": "^2.0.0",
"cross-env": "^5.1.4", "cross-env": "^5.1.4",

View File

@ -2,3 +2,4 @@
User-agent: * User-agent: *
Disallow: /media_proxy/ Disallow: /media_proxy/
Disallow: /interact/

View File

@ -37,7 +37,7 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do
end end
it 'renders new when failed to save' do it 'renders new when failed to save' do
Fabricate(:domain_block, domain: 'example.com') Fabricate(:domain_block, domain: 'example.com', severity: 'suspend')
allow(DomainBlockWorker).to receive(:perform_async).and_return(true) allow(DomainBlockWorker).to receive(:perform_async).and_return(true)
post :create, params: { domain_block: { domain: 'example.com', severity: 'silence' } } post :create, params: { domain_block: { domain: 'example.com', severity: 'silence' } }
@ -45,6 +45,17 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do
expect(DomainBlockWorker).not_to have_received(:perform_async) expect(DomainBlockWorker).not_to have_received(:perform_async)
expect(response).to render_template :new expect(response).to render_template :new
end end
it 'allows upgrading a block' do
Fabricate(:domain_block, domain: 'example.com', severity: 'silence')
allow(DomainBlockWorker).to receive(:perform_async).and_return(true)
post :create, params: { domain_block: { domain: 'example.com', severity: 'silence', reject_media: true, reject_reports: true } }
expect(DomainBlockWorker).to have_received(:perform_async)
expect(flash[:notice]).to eq I18n.t('admin.domain_blocks.created_msg')
expect(response).to redirect_to(admin_instances_path(limited: '1'))
end
end end
describe 'DELETE #destroy' do describe 'DELETE #destroy' do

View File

@ -107,6 +107,89 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
end end
end end
context 'approval-based registrations without invite' do
around do |example|
registrations_mode = Setting.registrations_mode
example.run
Setting.registrations_mode = registrations_mode
end
subject do
Setting.registrations_mode = 'approved'
request.headers["Accept-Language"] = accept_language
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678' } }
end
it 'redirects to login page' do
subject
expect(response).to redirect_to new_user_session_path
end
it 'creates user' do
subject
user = User.find_by(email: 'test@example.com')
expect(user).to_not be_nil
expect(user.locale).to eq(accept_language)
expect(user.approved).to eq(false)
end
end
context 'approval-based registrations with expired invite' do
around do |example|
registrations_mode = Setting.registrations_mode
example.run
Setting.registrations_mode = registrations_mode
end
subject do
Setting.registrations_mode = 'approved'
request.headers["Accept-Language"] = accept_language
invite = Fabricate(:invite, max_uses: nil, expires_at: 1.hour.ago)
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', 'invite_code': invite.code } }
end
it 'redirects to login page' do
subject
expect(response).to redirect_to new_user_session_path
end
it 'creates user' do
subject
user = User.find_by(email: 'test@example.com')
expect(user).to_not be_nil
expect(user.locale).to eq(accept_language)
expect(user.approved).to eq(false)
end
end
context 'approval-based registrations with valid invite' do
around do |example|
registrations_mode = Setting.registrations_mode
example.run
Setting.registrations_mode = registrations_mode
end
subject do
Setting.registrations_mode = 'approved'
request.headers["Accept-Language"] = accept_language
invite = Fabricate(:invite, max_uses: nil, expires_at: 1.hour.from_now)
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', 'invite_code': invite.code } }
end
it 'redirects to login page' do
subject
expect(response).to redirect_to new_user_session_path
end
it 'creates user' do
subject
user = User.find_by(email: 'test@example.com')
expect(user).to_not be_nil
expect(user.locale).to eq(accept_language)
expect(user.approved).to eq(true)
end
end
it 'does nothing if user already exists' do it 'does nothing if user already exists' do
Fabricate(:user, account: Fabricate(:account, username: 'test')) Fabricate(:user, account: Fabricate(:account, username: 'test'))
subject subject

View File

@ -36,4 +36,35 @@ RSpec.describe DomainBlock, type: :model do
expect(DomainBlock.blocked?('domain')).to eq false expect(DomainBlock.blocked?('domain')).to eq false
end end
end end
describe 'stricter_than?' do
it 'returns true if the new block has suspend severity while the old has lower severity' do
suspend = DomainBlock.new(domain: 'domain', severity: :suspend)
silence = DomainBlock.new(domain: 'domain', severity: :silence)
noop = DomainBlock.new(domain: 'domain', severity: :noop)
expect(suspend.stricter_than?(silence)).to be true
expect(suspend.stricter_than?(noop)).to be true
end
it 'returns false if the new block has lower severity than the old one' do
suspend = DomainBlock.new(domain: 'domain', severity: :suspend)
silence = DomainBlock.new(domain: 'domain', severity: :silence)
noop = DomainBlock.new(domain: 'domain', severity: :noop)
expect(silence.stricter_than?(suspend)).to be false
expect(noop.stricter_than?(suspend)).to be false
expect(noop.stricter_than?(silence)).to be false
end
it 'returns false if the new block does is less strict regarding reports' do
older = DomainBlock.new(domain: 'domain', severity: :silence, reject_reports: true)
newer = DomainBlock.new(domain: 'domain', severity: :silence, reject_reports: false)
expect(newer.stricter_than?(older)).to be false
end
it 'returns false if the new block does is less strict regarding media' do
older = DomainBlock.new(domain: 'domain', severity: :silence, reject_media: true)
newer = DomainBlock.new(domain: 'domain', severity: :silence, reject_media: false)
expect(newer.stricter_than?(older)).to be false
end
end
end end

View File

@ -8,6 +8,7 @@ RSpec.describe BlacklistedEmailValidator, type: :validator do
let(:errors) { double(add: nil) } let(:errors) { double(add: nil) }
before do before do
allow(user).to receive(:invited?) { false }
allow_any_instance_of(described_class).to receive(:blocked_email?) { blocked_email } allow_any_instance_of(described_class).to receive(:blocked_email?) { blocked_email }
described_class.new.validate(user) described_class.new.validate(user)
end end

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