Merge tag 'v2.8.2' into instance_only_statuses
This commit is contained in:
commit
84c8b1e200
51
CHANGELOG.md
51
CHANGELOG.md
@ -3,6 +3,57 @@ Changelog
|
||||
|
||||
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
|
||||
### Added
|
||||
|
||||
|
17
Gemfile
17
Gemfile
@ -21,6 +21,7 @@ gem 'fog-openstack', '~> 0.3', require: false
|
||||
gem 'paperclip', '~> 6.0'
|
||||
gem 'paperclip-av-transcoder', '~> 0.6'
|
||||
gem 'streamio-ffmpeg', '~> 3.0'
|
||||
gem 'blurhash', '~> 0.1'
|
||||
|
||||
gem 'active_model_serializers', '~> 0.10'
|
||||
gem 'addressable', '~> 2.6'
|
||||
@ -29,7 +30,7 @@ gem 'browser'
|
||||
gem 'charlock_holmes', '~> 0.7.6'
|
||||
gem 'iso-639'
|
||||
gem 'chewy', '~> 5.0'
|
||||
gem 'cld3', '~> 3.2.3'
|
||||
gem 'cld3', '~> 3.2.4'
|
||||
gem 'devise', '~> 4.6'
|
||||
gem 'devise-two-factor', '~> 3.0'
|
||||
|
||||
@ -42,7 +43,7 @@ gem 'omniauth-cas', '~> 1.1'
|
||||
gem 'omniauth-saml', '~> 1.10'
|
||||
gem 'omniauth', '~> 1.9'
|
||||
|
||||
gem 'doorkeeper', '~> 5.0'
|
||||
gem 'doorkeeper', '~> 5.1'
|
||||
gem 'fast_blank', '~> 1.0'
|
||||
gem 'fastimage'
|
||||
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 'pundit', '~> 2.0'
|
||||
gem 'premailer-rails'
|
||||
gem 'rack-attack', '~> 5.4'
|
||||
gem 'rack-attack', '~> 6.0'
|
||||
gem 'rack-cors', '~> 1.0', require: 'rack/cors'
|
||||
gem 'rails-i18n', '~> 5.1'
|
||||
gem 'rails-settings-cached', '~> 0.6'
|
||||
@ -107,7 +108,7 @@ group :production, :test do
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'capybara', '~> 3.16'
|
||||
gem 'capybara', '~> 3.18'
|
||||
gem 'climate_control', '~> 0.2'
|
||||
gem 'faker', '~> 1.9'
|
||||
gem 'microformats', '~> 4.1'
|
||||
@ -123,14 +124,14 @@ group :development do
|
||||
gem 'annotate', '~> 2.7'
|
||||
gem 'better_errors', '~> 2.5'
|
||||
gem 'binding_of_caller', '~> 0.7'
|
||||
gem 'bullet', '~> 5.9'
|
||||
gem 'bullet', '~> 6.0'
|
||||
gem 'letter_opener', '~> 1.7'
|
||||
gem 'letter_opener_web', '~> 1.3'
|
||||
gem 'memory_profiler'
|
||||
gem 'rubocop', '~> 0.67', require: false
|
||||
gem 'rubocop', '~> 0.68', require: false
|
||||
gem 'brakeman', '~> 4.5', 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-rails', '~> 1.4'
|
||||
@ -142,7 +143,7 @@ group :development do
|
||||
end
|
||||
|
||||
group :production do
|
||||
gem 'lograge', '~> 0.10'
|
||||
gem 'lograge', '~> 0.11'
|
||||
gem 'redis-rails', '~> 5.0'
|
||||
end
|
||||
|
||||
|
81
Gemfile.lock
81
Gemfile.lock
@ -66,8 +66,8 @@ GEM
|
||||
public_suffix (>= 2.0.2, < 4.0)
|
||||
airbrussh (1.3.0)
|
||||
sshkit (>= 1.6.1, != 1.7.0)
|
||||
annotate (2.7.4)
|
||||
activerecord (>= 3.2, < 6.0)
|
||||
annotate (2.7.5)
|
||||
activerecord (>= 3.2, < 7.0)
|
||||
rake (>= 10.4, < 13.0)
|
||||
arel (9.0.0)
|
||||
ast (2.4.0)
|
||||
@ -76,16 +76,16 @@ GEM
|
||||
av (0.9.0)
|
||||
cocaine (~> 0.5.3)
|
||||
aws-eventstream (1.0.2)
|
||||
aws-partitions (1.147.0)
|
||||
aws-sdk-core (3.48.3)
|
||||
aws-partitions (1.151.0)
|
||||
aws-sdk-core (3.48.4)
|
||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||
aws-partitions (~> 1.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.16.0)
|
||||
aws-sdk-kms (1.17.0)
|
||||
aws-sdk-core (~> 3, >= 3.48.2)
|
||||
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-kms (~> 1)
|
||||
aws-sigv4 (~> 1.0)
|
||||
@ -99,12 +99,14 @@ GEM
|
||||
rack (>= 0.9.0)
|
||||
binding_of_caller (0.8.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
bootsnap (1.4.3)
|
||||
blurhash (0.1.3)
|
||||
ffi (~> 1.10.0)
|
||||
bootsnap (1.4.4)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (4.5.0)
|
||||
browser (2.5.3)
|
||||
builder (3.2.3)
|
||||
bullet (5.9.0)
|
||||
bullet (6.0.0)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
bundler-audit (0.6.1)
|
||||
@ -127,7 +129,7 @@ GEM
|
||||
sshkit (~> 1.3)
|
||||
capistrano-yarn (2.0.2)
|
||||
capistrano (~> 3.0)
|
||||
capybara (3.16.1)
|
||||
capybara (3.18.0)
|
||||
addressable
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (~> 1.8)
|
||||
@ -143,8 +145,8 @@ GEM
|
||||
elasticsearch (>= 2.0.0)
|
||||
elasticsearch-dsl
|
||||
chunky_png (1.3.10)
|
||||
cld3 (3.2.3)
|
||||
ffi (>= 1.1.0, < 1.10.0)
|
||||
cld3 (3.2.4)
|
||||
ffi (>= 1.1.0, < 1.11.0)
|
||||
climate_control (0.2.0)
|
||||
cocaine (0.5.8)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
@ -184,8 +186,8 @@ GEM
|
||||
docile (1.3.0)
|
||||
domain_name (0.5.20180417)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (5.0.2)
|
||||
railties (>= 4.2)
|
||||
doorkeeper (5.1.0)
|
||||
railties (>= 5)
|
||||
dotenv (2.7.2)
|
||||
dotenv-rails (2.7.2)
|
||||
dotenv (= 2.7.2)
|
||||
@ -205,14 +207,14 @@ GEM
|
||||
et-orbi (1.1.6)
|
||||
tzinfo
|
||||
excon (0.62.0)
|
||||
fabrication (2.20.1)
|
||||
fabrication (2.20.2)
|
||||
faker (1.9.3)
|
||||
i18n (>= 0.7)
|
||||
faraday (0.15.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
fast_blank (1.0.0)
|
||||
fastimage (2.1.5)
|
||||
ffi (1.9.25)
|
||||
ffi (1.10.0)
|
||||
fog-core (2.1.0)
|
||||
builder
|
||||
excon (~> 0.58)
|
||||
@ -318,7 +320,7 @@ GEM
|
||||
letter_opener (~> 1.0)
|
||||
railties (>= 3.2)
|
||||
link_header (0.0.8)
|
||||
lograge (0.10.0)
|
||||
lograge (0.11.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
@ -346,7 +348,7 @@ GEM
|
||||
mini_mime (1.0.1)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.11.3)
|
||||
msgpack (1.2.9)
|
||||
msgpack (1.2.10)
|
||||
multi_json (1.13.1)
|
||||
multipart-post (2.0.0)
|
||||
necromancer (0.4.0)
|
||||
@ -355,7 +357,7 @@ GEM
|
||||
net-ssh (>= 2.6.5)
|
||||
net-ssh (5.0.2)
|
||||
nio4r (2.3.1)
|
||||
nokogiri (1.10.2)
|
||||
nokogiri (1.10.3)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
nokogumbo (2.0.0)
|
||||
nokogiri (~> 1.8, >= 1.8.4)
|
||||
@ -364,7 +366,7 @@ GEM
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sidekiq (>= 3.5)
|
||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||
oj (3.7.11)
|
||||
oj (3.7.12)
|
||||
omniauth (1.9.0)
|
||||
hashie (>= 3.4.6, < 3.7.0)
|
||||
rack (>= 1.6.2, < 3)
|
||||
@ -393,7 +395,7 @@ GEM
|
||||
parallel (1.17.0)
|
||||
parallel_tests (2.28.0)
|
||||
parallel
|
||||
parser (2.6.2.0)
|
||||
parser (2.6.3.0)
|
||||
ast (~> 2.4.0)
|
||||
pastel (0.7.2)
|
||||
equatable (~> 0.5.0)
|
||||
@ -418,14 +420,13 @@ GEM
|
||||
pry (~> 0.10)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
psych (3.1.0)
|
||||
public_suffix (3.0.3)
|
||||
puma (3.12.1)
|
||||
pundit (2.0.1)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.1.6)
|
||||
rack (2.0.7)
|
||||
rack-attack (5.4.2)
|
||||
rack-attack (6.0.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-cors (1.0.3)
|
||||
rack-protection (2.0.5)
|
||||
@ -470,8 +471,8 @@ GEM
|
||||
rainbow (3.0.0)
|
||||
rake (12.3.2)
|
||||
rb-fsevent (0.10.3)
|
||||
rb-inotify (0.9.10)
|
||||
ffi (>= 0.5.0, < 2)
|
||||
rb-inotify (0.10.0)
|
||||
ffi (~> 1.0)
|
||||
rdf (3.0.9)
|
||||
hamster (~> 3.0)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
@ -496,7 +497,7 @@ GEM
|
||||
redis-store (>= 1.2, < 2)
|
||||
redis-store (1.5.0)
|
||||
redis (>= 2.2, < 5)
|
||||
regexp_parser (1.3.0)
|
||||
regexp_parser (1.4.0)
|
||||
request_store (1.4.1)
|
||||
rack (>= 1.4)
|
||||
responders (2.4.1)
|
||||
@ -526,11 +527,10 @@ GEM
|
||||
rspec-core (~> 3.0, >= 3.0.0)
|
||||
sidekiq (>= 2.4.0)
|
||||
rspec-support (3.8.0)
|
||||
rubocop (0.67.1)
|
||||
rubocop (0.68.1)
|
||||
jaro_winkler (~> 1.5.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.5, != 2.5.1.1)
|
||||
psych (>= 3.1.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 1.6)
|
||||
@ -544,15 +544,15 @@ GEM
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.8.0)
|
||||
nokogumbo (~> 2.0)
|
||||
sass (3.6.0)
|
||||
sass (3.7.4)
|
||||
sass-listen (~> 4.0.0)
|
||||
sass-listen (4.0.0)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
scss_lint (0.57.1)
|
||||
scss_lint (0.58.0)
|
||||
rake (>= 0.9, < 13)
|
||||
sass (~> 3.5, >= 3.5.5)
|
||||
sidekiq (5.2.5)
|
||||
sidekiq (5.2.7)
|
||||
connection_pool (~> 2.2, >= 2.2.2)
|
||||
rack (>= 1.5.0)
|
||||
rack-protection (>= 1.5.0)
|
||||
@ -564,7 +564,7 @@ GEM
|
||||
rufus-scheduler (~> 3.2)
|
||||
sidekiq (>= 3)
|
||||
tilt (>= 1.4.0)
|
||||
sidekiq-unique-jobs (6.0.12)
|
||||
sidekiq-unique-jobs (6.0.13)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||
sidekiq (>= 4.0, < 7.0)
|
||||
thor (~> 0)
|
||||
@ -640,7 +640,7 @@ GEM
|
||||
activesupport (>= 4.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 4.2)
|
||||
webpush (0.3.7)
|
||||
webpush (0.3.8)
|
||||
hkdf (~> 0.2)
|
||||
jwt (~> 2.0)
|
||||
websocket-driver (0.7.0)
|
||||
@ -661,26 +661,27 @@ DEPENDENCIES
|
||||
aws-sdk-s3 (~> 1.36)
|
||||
better_errors (~> 2.5)
|
||||
binding_of_caller (~> 0.7)
|
||||
blurhash (~> 0.1)
|
||||
bootsnap (~> 1.4)
|
||||
brakeman (~> 4.5)
|
||||
browser
|
||||
bullet (~> 5.9)
|
||||
bullet (~> 6.0)
|
||||
bundler-audit (~> 0.6)
|
||||
capistrano (~> 3.11)
|
||||
capistrano-rails (~> 1.4)
|
||||
capistrano-rbenv (~> 2.1)
|
||||
capistrano-yarn (~> 2.0)
|
||||
capybara (~> 3.16)
|
||||
capybara (~> 3.18)
|
||||
charlock_holmes (~> 0.7.6)
|
||||
chewy (~> 5.0)
|
||||
cld3 (~> 3.2.3)
|
||||
cld3 (~> 3.2.4)
|
||||
climate_control (~> 0.2)
|
||||
concurrent-ruby
|
||||
derailed_benchmarks
|
||||
devise (~> 4.6)
|
||||
devise-two-factor (~> 3.0)
|
||||
devise_pam_authenticatable2 (~> 9.2)
|
||||
doorkeeper (~> 5.0)
|
||||
doorkeeper (~> 5.1)
|
||||
dotenv-rails (~> 2.7)
|
||||
fabrication (~> 2.20)
|
||||
faker (~> 1.9)
|
||||
@ -706,7 +707,7 @@ DEPENDENCIES
|
||||
letter_opener (~> 1.7)
|
||||
letter_opener_web (~> 1.3)
|
||||
link_header (~> 0.0)
|
||||
lograge (~> 0.10)
|
||||
lograge (~> 0.11)
|
||||
makara (~> 0.4)
|
||||
mario-redis-lock (~> 1.2)
|
||||
memory_profiler
|
||||
@ -734,7 +735,7 @@ DEPENDENCIES
|
||||
pry-rails (~> 0.3)
|
||||
puma (~> 3.12)
|
||||
pundit (~> 2.0)
|
||||
rack-attack (~> 5.4)
|
||||
rack-attack (~> 6.0)
|
||||
rack-cors (~> 1.0)
|
||||
rails (~> 5.2.3)
|
||||
rails-controller-testing (~> 1.0)
|
||||
@ -747,9 +748,9 @@ DEPENDENCIES
|
||||
rqrcode (~> 0.10)
|
||||
rspec-rails (~> 3.8)
|
||||
rspec-sidekiq (~> 3.0)
|
||||
rubocop (~> 0.67)
|
||||
rubocop (~> 0.68)
|
||||
sanitize (~> 5.0)
|
||||
scss_lint (~> 0.57)
|
||||
scss_lint (~> 0.58)
|
||||
sidekiq (~> 5.2)
|
||||
sidekiq-bulk (~> 0.2.0)
|
||||
sidekiq-scheduler (~> 3.0)
|
||||
|
@ -13,13 +13,25 @@ module Admin
|
||||
authorize :domain_block, :create?
|
||||
|
||||
@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
|
||||
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
|
||||
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
|
||||
@domain_block.save
|
||||
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
|
||||
@domain_block.errors[:domain].clear
|
||||
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
|
||||
|
||||
|
@ -9,6 +9,8 @@ class Api::BaseController < ApplicationController
|
||||
skip_before_action :store_current_location
|
||||
skip_before_action :check_user_permissions
|
||||
|
||||
before_action :set_cache_headers
|
||||
|
||||
protect_from_forgery with: :null_session
|
||||
|
||||
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
|
||||
@ -88,4 +90,8 @@ class Api::BaseController < ApplicationController
|
||||
def authorize_if_got_token!(*scopes)
|
||||
doorkeeper_authorize!(*scopes) if doorkeeper_token
|
||||
end
|
||||
|
||||
def set_cache_headers
|
||||
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
|
||||
end
|
||||
end
|
||||
|
@ -3,6 +3,8 @@
|
||||
class Api::V1::CustomEmojisController < Api::BaseController
|
||||
respond_to :json
|
||||
|
||||
skip_before_action :set_cache_headers
|
||||
|
||||
def index
|
||||
render_cached_json('api:v1:custom_emojis', expires_in: 1.minute) do
|
||||
ActiveModelSerializers::SerializableResource.new(CustomEmoji.local.where(disabled: false), each_serializer: REST::CustomEmojiSerializer)
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
class Api::V1::Instances::ActivityController < Api::BaseController
|
||||
before_action :require_enabled_api!
|
||||
skip_before_action :set_cache_headers
|
||||
|
||||
respond_to :json
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
class Api::V1::Instances::PeersController < Api::BaseController
|
||||
before_action :require_enabled_api!
|
||||
skip_before_action :set_cache_headers
|
||||
|
||||
respond_to :json
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
class Api::V1::InstancesController < Api::BaseController
|
||||
respond_to :json
|
||||
skip_before_action :set_cache_headers
|
||||
|
||||
def show
|
||||
render_cached_json('api:v1:instances', expires_in: 5.minutes) do
|
||||
|
@ -91,7 +91,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def determine_layout
|
||||
|
@ -25,7 +25,7 @@ class Settings::NotificationsController < Settings::BaseController
|
||||
|
||||
def user_settings_params
|
||||
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)
|
||||
)
|
||||
end
|
||||
|
@ -205,8 +205,8 @@ export function uploadCompose(files) {
|
||||
return function (dispatch, getState) {
|
||||
const uploadLimit = 4;
|
||||
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);
|
||||
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
|
||||
|
||||
if (files.length + media.size > uploadLimit) {
|
||||
dispatch(showAlert(undefined, messages.uploadErrorLimit));
|
||||
@ -226,6 +226,8 @@ export function uploadCompose(files) {
|
||||
resizeImage(f).then(file => {
|
||||
const data = new FormData();
|
||||
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, {
|
||||
onUploadProgress: function({ loaded }){
|
||||
|
@ -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 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 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 expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => {
|
||||
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||
|
@ -7,6 +7,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { isIOS } from '../is_mobile';
|
||||
import classNames from 'classnames';
|
||||
import { autoPlayGif, displayMedia } from '../initial_state';
|
||||
import { decode } from 'blurhash';
|
||||
|
||||
const messages = defineMessages({
|
||||
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
|
||||
@ -21,6 +22,7 @@ class Item extends React.PureComponent {
|
||||
size: PropTypes.number.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
displayWidth: PropTypes.number,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -29,6 +31,10 @@ class Item extends React.PureComponent {
|
||||
size: 1,
|
||||
};
|
||||
|
||||
state = {
|
||||
loaded: false,
|
||||
};
|
||||
|
||||
handleMouseEnter = (e) => {
|
||||
if (this.hoverToPlay()) {
|
||||
e.target.play();
|
||||
@ -62,8 +68,40 @@ class Item extends React.PureComponent {
|
||||
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 () {
|
||||
const { attachment, index, size, standalone, displayWidth } = this.props;
|
||||
const { attachment, index, size, standalone, displayWidth, visible } = this.props;
|
||||
|
||||
let width = 50;
|
||||
let height = 100;
|
||||
@ -116,12 +154,20 @@ class Item extends React.PureComponent {
|
||||
|
||||
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 previewWidth = attachment.getIn(['meta', 'small', 'width']);
|
||||
|
||||
const originalUrl = attachment.get('url');
|
||||
const originalWidth = attachment.getIn(['meta', 'original', 'width']);
|
||||
const originalUrl = attachment.get('url');
|
||||
const originalWidth = attachment.getIn(['meta', 'original', 'width']);
|
||||
|
||||
const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
|
||||
|
||||
@ -147,6 +193,7 @@ class Item extends React.PureComponent {
|
||||
alt={attachment.get('description')}
|
||||
title={attachment.get('description')}
|
||||
style={{ objectPosition: `${x}% ${y}%` }}
|
||||
onLoad={this.handleImageLoad}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
@ -176,7 +223,8 @@ class Item extends React.PureComponent {
|
||||
|
||||
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}%` }}>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
@ -225,6 +273,7 @@ class MediaGallery extends React.PureComponent {
|
||||
if (node /*&& this.isStandaloneEligible()*/) {
|
||||
// offsetWidth triggers a layout, so only calculate when we need to
|
||||
if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth);
|
||||
|
||||
this.setState({
|
||||
width: node.offsetWidth,
|
||||
});
|
||||
@ -242,7 +291,7 @@ class MediaGallery extends React.PureComponent {
|
||||
|
||||
const width = this.state.width || defaultWidth;
|
||||
|
||||
let children;
|
||||
let children, spoilerButton;
|
||||
|
||||
const style = {};
|
||||
|
||||
@ -256,35 +305,28 @@ class MediaGallery extends React.PureComponent {
|
||||
style.height = height;
|
||||
}
|
||||
|
||||
if (!visible) {
|
||||
let warning;
|
||||
const size = media.take(4).size;
|
||||
|
||||
if (sensitive) {
|
||||
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
|
||||
} else {
|
||||
warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
|
||||
}
|
||||
if (this.isStandaloneEligible()) {
|
||||
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />;
|
||||
} else {
|
||||
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} visible={visible} />);
|
||||
}
|
||||
|
||||
children = (
|
||||
<button type='button' className='media-spoiler' onClick={this.handleOpen} style={style} ref={this.handleRef}>
|
||||
<span className='media-spoiler__warning'>{warning}</span>
|
||||
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
if (visible) {
|
||||
spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon='eye-slash' overlay onClick={this.handleOpen} />;
|
||||
} else {
|
||||
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>
|
||||
);
|
||||
} 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 (
|
||||
<div className='media-gallery' style={style} ref={this.handleRef}>
|
||||
<div className={classNames('spoiler-button', { 'spoiler-button--visible': visible })}>
|
||||
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />
|
||||
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible })}>
|
||||
{spoilerButton}
|
||||
</div>
|
||||
|
||||
{children}
|
||||
|
@ -274,7 +274,7 @@ class Status extends ImmutablePureComponent {
|
||||
if (status.get('poll')) {
|
||||
media = <PollContainer pollId={status.get('poll')} />;
|
||||
} 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 = (
|
||||
<AttachmentList
|
||||
compact
|
||||
@ -289,6 +289,7 @@ class Status extends ImmutablePureComponent {
|
||||
{Component => (
|
||||
<Component
|
||||
preview={video.get('preview_url')}
|
||||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
width={this.props.cachedMediaWidth}
|
||||
|
@ -46,22 +46,28 @@ export default class StatusList extends ImmutablePureComponent {
|
||||
|
||||
handleMoveUp = (id, featured) => {
|
||||
const elementIndex = this.getCurrentStatusIndex(id, featured) - 1;
|
||||
this._selectChild(elementIndex);
|
||||
this._selectChild(elementIndex, true);
|
||||
}
|
||||
|
||||
handleMoveDown = (id, featured) => {
|
||||
const elementIndex = this.getCurrentStatusIndex(id, featured) + 1;
|
||||
this._selectChild(elementIndex);
|
||||
this._selectChild(elementIndex, false);
|
||||
}
|
||||
|
||||
handleLoadOlder = debounce(() => {
|
||||
this.props.onLoadMore(this.props.statusIds.size > 0 ? this.props.statusIds.last() : undefined);
|
||||
}, 300, { leading: true })
|
||||
|
||||
_selectChild (index) {
|
||||
const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
_selectChild (index, align_top) {
|
||||
const container = this.node.node;
|
||||
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -1,62 +1,142 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Permalink from '../../../components/permalink';
|
||||
import { displayMedia } from '../../../initial_state';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import { autoPlayGif, displayMedia } from 'mastodon/initial_state';
|
||||
import classNames from 'classnames';
|
||||
import { decode } from 'blurhash';
|
||||
import { isIOS } from 'mastodon/is_mobile';
|
||||
|
||||
export default class MediaItem extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
attachment: ImmutablePropTypes.map.isRequired,
|
||||
displayWidth: PropTypes.number.isRequired,
|
||||
onOpenMedia: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
if (!this.state.visible) {
|
||||
this.setState({ visible: true });
|
||||
return true;
|
||||
componentDidMount () {
|
||||
if (this.props.attachment.get('blurhash')) {
|
||||
this._decode();
|
||||
}
|
||||
}
|
||||
|
||||
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 () {
|
||||
const { media } = this.props;
|
||||
const { visible } = 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 = {};
|
||||
const { attachment, displayWidth } = this.props;
|
||||
const { visible, loaded } = this.state;
|
||||
|
||||
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') {
|
||||
label = <span className='media-gallery__gifv__label'>GIF</span>;
|
||||
}
|
||||
let thumbnail = '';
|
||||
|
||||
if (visible) {
|
||||
style.backgroundImage = `url(${media.get('preview_url')})`;
|
||||
style.backgroundPosition = `${x}% ${y}%`;
|
||||
} else {
|
||||
icon = (
|
||||
<span className='account-gallery__item__icons'>
|
||||
<Icon id='eye-slash' />
|
||||
</span>
|
||||
if (attachment.get('type') === 'unknown') {
|
||||
// Skip
|
||||
} else if (attachment.get('type') === 'image') {
|
||||
const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
|
||||
const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
|
||||
const x = ((focusX / 2) + .5) * 100;
|
||||
const y = ((focusY / -2) + .5) * 100;
|
||||
|
||||
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 (
|
||||
<div className='account-gallery__item'>
|
||||
<Permalink to={`/statuses/${status.get('id')}`} href={status.get('url')} style={style} onInterceptClick={this.handleClick}>
|
||||
{icon}
|
||||
{label}
|
||||
</Permalink>
|
||||
<div className='account-gallery__item' style={{ width, height }}>
|
||||
<a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick}>
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} />
|
||||
{visible && thumbnail}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -2,24 +2,25 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { fetchAccount } from '../../actions/accounts';
|
||||
import { fetchAccount } from 'mastodon/actions/accounts';
|
||||
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 ColumnBackButton from '../../components/column_back_button';
|
||||
import ColumnBackButton from 'mastodon/components/column_back_button';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { getAccountGallery } from '../../selectors';
|
||||
import { getAccountGallery } from 'mastodon/selectors';
|
||||
import MediaItem from './components/media_item';
|
||||
import HeaderContainer from '../account_timeline/containers/header_container';
|
||||
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 { openModal } from 'mastodon/actions/modal';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
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']),
|
||||
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
|
||||
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
|
||||
});
|
||||
|
||||
class LoadMoreMedia extends ImmutablePureComponent {
|
||||
@ -51,12 +52,16 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
medias: ImmutablePropTypes.list.isRequired,
|
||||
attachments: ImmutablePropTypes.list.isRequired,
|
||||
isLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
width: 323,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
this.props.dispatch(fetchAccount(this.props.params.accountId));
|
||||
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId));
|
||||
@ -71,11 +76,11 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
|
||||
handleScrollToBottom = () => {
|
||||
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 offset = scrollHeight - scrollTop - clientHeight;
|
||||
|
||||
@ -88,13 +93,31 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId, { maxId }));
|
||||
};
|
||||
|
||||
handleLoadOlder = (e) => {
|
||||
handleLoadOlder = e => {
|
||||
e.preventDefault();
|
||||
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 () {
|
||||
const { medias, shouldUpdateScroll, isLoading, hasMore, isAccount } = this.props;
|
||||
const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount } = this.props;
|
||||
const { width } = this.state;
|
||||
|
||||
if (!isAccount) {
|
||||
return (
|
||||
@ -104,9 +127,7 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
let loadOlder = null;
|
||||
|
||||
if (!medias && isLoading) {
|
||||
if (!attachments && isLoading) {
|
||||
return (
|
||||
<Column>
|
||||
<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} />;
|
||||
}
|
||||
|
||||
@ -126,23 +149,17 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
|
||||
<HeaderContainer accountId={this.props.params.accountId} />
|
||||
|
||||
<div role='feed' className='account-gallery__container'>
|
||||
{medias.map((media, index) => media === null ? (
|
||||
<LoadMoreMedia
|
||||
key={'more:' + medias.getIn(index + 1, 'id')}
|
||||
maxId={index > 0 ? medias.getIn(index - 1, 'id') : null}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
/>
|
||||
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
||||
{attachments.map((attachment, index) => attachment === null ? (
|
||||
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
|
||||
) : (
|
||||
<MediaItem
|
||||
key={media.get('id')}
|
||||
media={media}
|
||||
/>
|
||||
<MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
|
||||
))}
|
||||
|
||||
{loadOlder}
|
||||
</div>
|
||||
|
||||
{isLoading && medias.size === 0 && (
|
||||
{isLoading && attachments.size === 0 && (
|
||||
<div className='scrollable__append'>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
|
@ -11,7 +11,6 @@ import { defineMessages, injectIntl } from 'react-intl';
|
||||
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
||||
import PrivacyDropdownContainer from '../containers/privacy_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 PollFormContainer from '../containers/poll_form_container';
|
||||
import UploadFormContainer from '../containers/upload_form_container';
|
||||
@ -41,18 +40,17 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
suggestion_token: PropTypes.string,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
spoiler: PropTypes.bool,
|
||||
privacy: PropTypes.string,
|
||||
federation: PropTypes.bool,
|
||||
spoiler_text: PropTypes.string,
|
||||
spoilerText: PropTypes.string,
|
||||
focusDate: PropTypes.instanceOf(Date),
|
||||
caretPosition: PropTypes.number,
|
||||
preselectDate: PropTypes.instanceOf(Date),
|
||||
is_submitting: PropTypes.bool,
|
||||
is_changing_upload: PropTypes.bool,
|
||||
is_uploading: PropTypes.bool,
|
||||
isSubmitting: PropTypes.bool,
|
||||
isChangingUpload: PropTypes.bool,
|
||||
isUploading: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
@ -87,10 +85,10 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
// Submit disabled:
|
||||
const { is_submitting, is_changing_upload, is_uploading, anyMedia } = this.props;
|
||||
const fulltext = [this.props.spoiler_text, countableText(this.props.text)].join('');
|
||||
const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -135,7 +133,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
|
||||
this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
|
||||
this.autosuggestTextarea.textarea.focus();
|
||||
} else if(prevProps.is_submitting && !this.props.is_submitting) {
|
||||
} else if(prevProps.isSubmitting && !this.props.isSubmitting) {
|
||||
this.autosuggestTextarea.textarea.focus();
|
||||
} else if (this.props.spoiler !== prevProps.spoiler) {
|
||||
if (this.props.spoiler) {
|
||||
@ -164,9 +162,9 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
|
||||
render () {
|
||||
const { intl, onPaste, showSearch, anyMedia } = this.props;
|
||||
const disabled = this.props.is_submitting;
|
||||
const text = [this.props.spoiler_text, 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 disabled = this.props.isSubmitting;
|
||||
const text = [this.props.spoilerText, countableText(this.props.text)].join('');
|
||||
const disabledButton = disabled || this.props.isUploading || this.props.isChangingUpload || length(text) > 500 || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
|
||||
let publishText = '';
|
||||
|
||||
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' : ''}`}>
|
||||
<label>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@ -217,7 +215,6 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
<UploadButtonContainer />
|
||||
<PollButtonContainer />
|
||||
<PrivacyDropdownContainer />
|
||||
<SensitiveButtonContainer />
|
||||
<SpoilerButtonContainer />
|
||||
<FederationDropdownContainer />
|
||||
</div>
|
||||
|
@ -26,6 +26,7 @@ class Option extends React.PureComponent {
|
||||
isPollMultiple: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onToggleMultiple: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
@ -37,13 +38,24 @@ class Option extends React.PureComponent {
|
||||
this.props.onRemove(this.props.index);
|
||||
};
|
||||
|
||||
handleToggleMultiple = e => {
|
||||
this.props.onToggleMultiple();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { isPollMultiple, title, index, intl } = this.props;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<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
|
||||
type='text'
|
||||
@ -86,6 +98,10 @@ class PollForm extends ImmutablePureComponent {
|
||||
this.props.onChangeSettings(e.target.value, this.props.isMultiple);
|
||||
};
|
||||
|
||||
handleToggleMultiple = () => {
|
||||
this.props.onChangeSettings(this.props.expiresIn, !this.props.isMultiple);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl } = this.props;
|
||||
|
||||
@ -96,7 +112,7 @@ class PollForm extends ImmutablePureComponent {
|
||||
return (
|
||||
<div className='compose-form__poll-wrapper'>
|
||||
<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>
|
||||
|
||||
<div className='poll__footer'>
|
||||
|
@ -73,7 +73,7 @@ class Search extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
handleKeyUp = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
@ -82,10 +82,6 @@ class Search extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
noop () {
|
||||
|
||||
}
|
||||
|
||||
handleFocus = () => {
|
||||
this.setState({ expanded: true });
|
||||
this.props.onShow();
|
||||
@ -110,7 +106,7 @@ class Search extends React.PureComponent {
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={this.handleKeyDown}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
/>
|
||||
|
@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import UploadProgressContainer from '../containers/upload_progress_container';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import UploadContainer from '../containers/upload_container';
|
||||
import SensitiveButtonContainer from '../containers/sensitive_button_container';
|
||||
|
||||
export default class UploadForm extends ImmutablePureComponent {
|
||||
|
||||
@ -22,6 +23,8 @@ export default class UploadForm extends ImmutablePureComponent {
|
||||
<UploadContainer id={id} key={id} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!mediaIds.isEmpty() && <SensitiveButtonContainer />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ComposeForm from '../components/compose_form';
|
||||
import { uploadCompose } from '../../../actions/compose';
|
||||
import {
|
||||
changeCompose,
|
||||
submitCompose,
|
||||
@ -9,22 +8,22 @@ import {
|
||||
selectComposeSuggestion,
|
||||
changeComposeSpoilerText,
|
||||
insertEmojiCompose,
|
||||
uploadCompose,
|
||||
} from '../../../actions/compose';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
text: state.getIn(['compose', 'text']),
|
||||
suggestion_token: state.getIn(['compose', 'suggestion_token']),
|
||||
suggestions: state.getIn(['compose', 'suggestions']),
|
||||
spoiler: state.getIn(['compose', 'spoiler']),
|
||||
spoiler_text: state.getIn(['compose', 'spoiler_text']),
|
||||
spoilerText: state.getIn(['compose', 'spoiler_text']),
|
||||
privacy: state.getIn(['compose', 'privacy']),
|
||||
federation: state.getIn(['compose', 'federation']),
|
||||
focusDate: state.getIn(['compose', 'focusDate']),
|
||||
caretPosition: state.getIn(['compose', 'caretPosition']),
|
||||
preselectDate: state.getIn(['compose', 'preselectDate']),
|
||||
is_submitting: state.getIn(['compose', 'is_submitting']),
|
||||
is_changing_upload: state.getIn(['compose', 'is_changing_upload']),
|
||||
is_uploading: state.getIn(['compose', 'is_uploading']),
|
||||
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
|
||||
isUploading: state.getIn(['compose', 'is_uploading']),
|
||||
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
||||
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
});
|
||||
@ -47,8 +46,8 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
dispatch(fetchComposeSuggestions(token));
|
||||
},
|
||||
|
||||
onSuggestionSelected (position, token, accountId) {
|
||||
dispatch(selectComposeSuggestion(position, token, accountId));
|
||||
onSuggestionSelected (position, token, suggestion) {
|
||||
dispatch(selectComposeSuggestion(position, token, suggestion));
|
||||
},
|
||||
|
||||
onChangeSpoilerText (checked) {
|
||||
|
@ -2,11 +2,9 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import { changeComposeSensitivity } from '../../../actions/compose';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import { changeComposeSensitivity } from 'mastodon/actions/compose';
|
||||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' },
|
||||
@ -14,7 +12,6 @@ const messages = defineMessages({
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
visible: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||
active: state.getIn(['compose', 'sensitive']),
|
||||
disabled: state.getIn(['compose', 'spoiler']),
|
||||
});
|
||||
@ -30,7 +27,6 @@ const mapDispatchToProps = dispatch => ({
|
||||
class SensitiveButton extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
visible: PropTypes.bool,
|
||||
active: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
@ -38,32 +34,14 @@ class SensitiveButton extends React.PureComponent {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { visible, active, disabled, onClick, intl } = this.props;
|
||||
const { active, disabled, onClick, intl } = this.props;
|
||||
|
||||
return (
|
||||
<Motion defaultStyle={{ scale: 0.87 }} style={{ scale: spring(visible ? 1 : 0.87, { stiffness: 200, damping: 3 }) }}>
|
||||
{({ scale }) => {
|
||||
const icon = active ? 'eye-slash' : 'eye';
|
||||
const className = classNames('compose-form__sensitive-button', {
|
||||
'compose-form__sensitive-button--visible': visible,
|
||||
});
|
||||
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>
|
||||
<div className='compose-form__sensitive-button'>
|
||||
<button className={classNames('icon-button', { active })} onClick={onClick} disabled={disabled} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}>
|
||||
<Icon id='eye-slash' /> <FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -20,18 +20,24 @@ export default class ConversationsList extends ImmutablePureComponent {
|
||||
|
||||
handleMoveUp = id => {
|
||||
const elementIndex = this.getCurrentIndex(id) - 1;
|
||||
this._selectChild(elementIndex);
|
||||
this._selectChild(elementIndex, true);
|
||||
}
|
||||
|
||||
handleMoveDown = id => {
|
||||
const elementIndex = this.getCurrentIndex(id) + 1;
|
||||
this._selectChild(elementIndex);
|
||||
this._selectChild(elementIndex, false);
|
||||
}
|
||||
|
||||
_selectChild (index) {
|
||||
const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
_selectChild (index, align_top) {
|
||||
const container = this.node.node;
|
||||
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { me, 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 { List as ImmutableList } from 'immutable';
|
||||
import { Link } from 'react-router-dom';
|
||||
@ -172,7 +172,7 @@ class GettingStarted extends ImmutablePureComponent {
|
||||
<FormattedMessage
|
||||
id='getting_started.open_source_notice'
|
||||
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>
|
||||
</div>
|
||||
|
@ -113,18 +113,24 @@ class Notifications extends React.PureComponent {
|
||||
|
||||
handleMoveUp = id => {
|
||||
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
|
||||
this._selectChild(elementIndex);
|
||||
this._selectChild(elementIndex, true);
|
||||
}
|
||||
|
||||
handleMoveDown = id => {
|
||||
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
|
||||
this._selectChild(elementIndex);
|
||||
this._selectChild(elementIndex, false);
|
||||
}
|
||||
|
||||
_selectChild (index) {
|
||||
const element = this.column.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
_selectChild (index, align_top) {
|
||||
const container = this.column.node;
|
||||
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ export default class StatusCheckBox extends React.PureComponent {
|
||||
{Component => (
|
||||
<Component
|
||||
preview={video.get('preview_url')}
|
||||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
width={239}
|
||||
|
@ -5,7 +5,6 @@ import Avatar from '../../../components/avatar';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
import StatusContent from '../../../components/status_content';
|
||||
import MediaGallery from '../../../components/media_gallery';
|
||||
import AttachmentList from '../../../components/attachment_list';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { defineMessages, injectIntl, FormattedDate, FormattedNumber } from 'react-intl';
|
||||
import Card from './card';
|
||||
@ -116,14 +115,13 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||
if (status.get('poll')) {
|
||||
media = <PollContainer pollId={status.get('poll')} />;
|
||||
} else if (status.get('media_attachments').size > 0) {
|
||||
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
|
||||
media = <AttachmentList media={status.get('media_attachments')} />;
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
const video = status.getIn(['media_attachments', 0]);
|
||||
|
||||
media = (
|
||||
<Video
|
||||
preview={video.get('preview_url')}
|
||||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
width={300}
|
||||
|
@ -316,15 +316,15 @@ class Status extends ImmutablePureComponent {
|
||||
const { status, ancestorsIds, descendantsIds } = this.props;
|
||||
|
||||
if (id === status.get('id')) {
|
||||
this._selectChild(ancestorsIds.size - 1);
|
||||
this._selectChild(ancestorsIds.size - 1, true);
|
||||
} else {
|
||||
let index = ancestorsIds.indexOf(id);
|
||||
|
||||
if (index === -1) {
|
||||
index = descendantsIds.indexOf(id);
|
||||
this._selectChild(ancestorsIds.size + index);
|
||||
this._selectChild(ancestorsIds.size + index, true);
|
||||
} else {
|
||||
this._selectChild(index - 1);
|
||||
this._selectChild(index - 1, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -333,23 +333,29 @@ class Status extends ImmutablePureComponent {
|
||||
const { status, ancestorsIds, descendantsIds } = this.props;
|
||||
|
||||
if (id === status.get('id')) {
|
||||
this._selectChild(ancestorsIds.size + 1);
|
||||
this._selectChild(ancestorsIds.size + 1, false);
|
||||
} else {
|
||||
let index = ancestorsIds.indexOf(id);
|
||||
|
||||
if (index === -1) {
|
||||
index = descendantsIds.indexOf(id);
|
||||
this._selectChild(ancestorsIds.size + index + 2);
|
||||
this._selectChild(ancestorsIds.size + index + 2, false);
|
||||
} else {
|
||||
this._selectChild(index + 1);
|
||||
this._selectChild(index + 1, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_selectChild (index) {
|
||||
const element = this.node.querySelectorAll('.focusable')[index];
|
||||
_selectChild (index, align_top) {
|
||||
const container = this.node;
|
||||
const element = container.querySelectorAll('.focusable')[index];
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ export default class ActionsModal extends ImmutablePureComponent {
|
||||
<div className='modal-root__modal actions-modal'>
|
||||
{status}
|
||||
|
||||
<ul>
|
||||
<ul className={classNames({ 'with-status': !!status })}>
|
||||
{this.props.actions.map(this.renderAction)}
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -2,11 +2,11 @@ import React from 'react';
|
||||
import ReactSwipeableViews from 'react-swipeable-views';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import Video from '../../video';
|
||||
import ExtendedVideoPlayer from '../../../components/extended_video_player';
|
||||
import Video from 'mastodon/features/video';
|
||||
import ExtendedVideoPlayer from 'mastodon/components/extended_video_player';
|
||||
import classNames from 'classnames';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import IconButton from 'mastodon/components/icon_button';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImageLoader from './image_loader';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
@ -24,6 +24,7 @@ class MediaModal extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
media: ImmutablePropTypes.list.isRequired,
|
||||
status: ImmutablePropTypes.map,
|
||||
index: PropTypes.number.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
@ -72,9 +73,12 @@ class MediaModal extends ImmutablePureComponent {
|
||||
|
||||
componentDidMount () {
|
||||
window.addEventListener('keydown', this.handleKeyDown, false);
|
||||
|
||||
if (this.context.router) {
|
||||
const history = this.context.router.history;
|
||||
|
||||
history.push(history.location.pathname, previewState);
|
||||
|
||||
this.unlistenHistory = history.listen(() => {
|
||||
this.props.onClose();
|
||||
});
|
||||
@ -83,6 +87,7 @@ class MediaModal extends ImmutablePureComponent {
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
|
||||
if (this.context.router) {
|
||||
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 () {
|
||||
const { media, intl, onClose } = this.props;
|
||||
const { media, status, intl, onClose } = this.props;
|
||||
const { navigationHidden } = this.state;
|
||||
|
||||
const index = this.getIndex();
|
||||
@ -144,6 +156,7 @@ class MediaModal extends ImmutablePureComponent {
|
||||
return (
|
||||
<Video
|
||||
preview={image.get('preview_url')}
|
||||
blurhash={image.get('blurhash')}
|
||||
src={image.get('url')}
|
||||
width={image.get('width')}
|
||||
height={image.get('height')}
|
||||
@ -206,10 +219,19 @@ class MediaModal extends ImmutablePureComponent {
|
||||
{content}
|
||||
</ReactSwipeableViews>
|
||||
</div>
|
||||
|
||||
<div className={navigationClassName}>
|
||||
<IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={40} />
|
||||
|
||||
{leftNav}
|
||||
{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'>
|
||||
{pagination}
|
||||
</ul>
|
||||
|
@ -1,28 +1,69 @@
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import Video from '../../video';
|
||||
import Video from 'mastodon/features/video';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export const previewState = 'previewVideoModal';
|
||||
|
||||
export default class VideoModal extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
status: ImmutablePropTypes.map,
|
||||
time: PropTypes.number,
|
||||
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 () {
|
||||
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 (
|
||||
<div className='modal-root__modal video-modal'>
|
||||
<div>
|
||||
<Video
|
||||
preview={media.get('preview_url')}
|
||||
blurhash={media.get('blurhash')}
|
||||
src={media.get('url')}
|
||||
startTime={time}
|
||||
onCloseVideo={onClose}
|
||||
link={link}
|
||||
detailed
|
||||
alt={media.get('description')}
|
||||
/>
|
||||
|
@ -47,7 +47,8 @@ import {
|
||||
Lists,
|
||||
} from './util/async-components';
|
||||
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.
|
||||
// Without this it ends up in ~8 very commonly used bundles.
|
||||
@ -121,7 +122,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
||||
}
|
||||
|
||||
shouldUpdateScroll (_, { location }) {
|
||||
return location.state !== previewState;
|
||||
return location.state !== previewMediaState && location.state !== previewVideoState;
|
||||
}
|
||||
|
||||
handleResize = debounce(() => {
|
||||
@ -367,11 +368,16 @@ class UI extends React.PureComponent {
|
||||
handleHotkeyFocusColumn = e => {
|
||||
const index = (e.key * 1) + 1; // First child is drawer, skip that
|
||||
const column = this.node.querySelector(`.column:nth-child(${index})`);
|
||||
if (!column) return;
|
||||
const container = column.querySelector('.scrollable');
|
||||
|
||||
if (column) {
|
||||
const status = column.querySelector('.focusable');
|
||||
if (container) {
|
||||
const status = container.querySelector('.focusable');
|
||||
|
||||
if (status) {
|
||||
if (container.scrollTop > status.offsetTop) {
|
||||
status.scrollIntoView(true);
|
||||
}
|
||||
status.focus();
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import classNames from 'classnames';
|
||||
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
|
||||
import { displayMedia } from '../../initial_state';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import { decode } from 'blurhash';
|
||||
|
||||
const messages = defineMessages({
|
||||
play: { id: 'video.play', defaultMessage: 'Play' },
|
||||
@ -102,6 +103,8 @@ class Video extends React.PureComponent {
|
||||
inline: PropTypes.bool,
|
||||
cacheWidth: PropTypes.func,
|
||||
intl: PropTypes.object.isRequired,
|
||||
blurhash: PropTypes.string,
|
||||
link: PropTypes.node,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -139,6 +142,7 @@ class Video extends React.PureComponent {
|
||||
|
||||
setVideoRef = c => {
|
||||
this.video = c;
|
||||
|
||||
if (this.video) {
|
||||
this.setState({ volume: this.video.volume, muted: this.video.muted });
|
||||
}
|
||||
@ -152,6 +156,10 @@ class Video extends React.PureComponent {
|
||||
this.volume = c;
|
||||
}
|
||||
|
||||
setCanvasRef = c => {
|
||||
this.canvas = c;
|
||||
}
|
||||
|
||||
handleClickRoot = e => e.stopPropagation();
|
||||
|
||||
handlePlay = () => {
|
||||
@ -170,7 +178,6 @@ class Video extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleVolumeMouseDown = e => {
|
||||
|
||||
document.addEventListener('mousemove', this.handleMouseVolSlide, true);
|
||||
document.addEventListener('mouseup', this.handleVolumeMouseUp, true);
|
||||
document.addEventListener('touchmove', this.handleMouseVolSlide, true);
|
||||
@ -190,7 +197,6 @@ class Video extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleMouseVolSlide = throttle(e => {
|
||||
|
||||
const rect = this.volume.getBoundingClientRect();
|
||||
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('mozfullscreenchange', this.handleFullscreenChange, true);
|
||||
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
|
||||
|
||||
if (this.props.blurhash) {
|
||||
this._decode();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
@ -270,6 +280,24 @@ class Video extends React.PureComponent {
|
||||
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 = () => {
|
||||
this.setState({ fullscreen: isFullscreen() });
|
||||
}
|
||||
@ -314,6 +342,7 @@ class Video extends React.PureComponent {
|
||||
|
||||
handleOpenVideo = () => {
|
||||
const { src, preview, width, height, alt } = this.props;
|
||||
|
||||
const media = fromJS({
|
||||
type: 'video',
|
||||
url: src,
|
||||
@ -333,7 +362,7 @@ class Video extends React.PureComponent {
|
||||
}
|
||||
|
||||
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 progress = (currentTime / duration) * 100;
|
||||
|
||||
@ -351,6 +380,7 @@ class Video extends React.PureComponent {
|
||||
}
|
||||
|
||||
let preload;
|
||||
|
||||
if (startTime || fullscreen || dragging) {
|
||||
preload = 'auto';
|
||||
} else if (detailed) {
|
||||
@ -360,6 +390,7 @@ class Video extends React.PureComponent {
|
||||
}
|
||||
|
||||
let warning;
|
||||
|
||||
if (sensitive) {
|
||||
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
|
||||
} else {
|
||||
@ -377,7 +408,9 @@ class Video extends React.PureComponent {
|
||||
onClick={this.handleClickRoot}
|
||||
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}
|
||||
src={src}
|
||||
poster={preview}
|
||||
@ -397,12 +430,13 @@ class Video extends React.PureComponent {
|
||||
onLoadedData={this.handleLoadedData}
|
||||
onProgress={this.handleProgress}
|
||||
onVolumeChange={this.handleVolumeChange}
|
||||
/>
|
||||
/>}
|
||||
|
||||
<button type='button' className={classNames('video-player__spoiler', { active: !revealed })} onClick={this.toggleReveal}>
|
||||
<span className='video-player__spoiler__title'>{warning}</span>
|
||||
<span className='video-player__spoiler__subtitle'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
</button>
|
||||
<div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed })}>
|
||||
<button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}>
|
||||
<span className='spoiler-button__overlay__label'>{warning}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={classNames('video-player__controls', { active: paused || hovered })}>
|
||||
<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'>
|
||||
<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>
|
||||
|
||||
<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
|
||||
<div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
|
||||
<span
|
||||
@ -429,17 +464,19 @@ class Video extends React.PureComponent {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(detailed || fullscreen) &&
|
||||
{(detailed || fullscreen) && (
|
||||
<span>
|
||||
<span className='video-player__time-current'>{formatTime(currentTime)}</span>
|
||||
<span className='video-player__time-sep'>/</span>
|
||||
<span className='video-player__time-total'>{formatTime(duration)}</span>
|
||||
</span>
|
||||
}
|
||||
)}
|
||||
|
||||
{link && <span className='video-player__link'>{link}</span>}
|
||||
</div>
|
||||
|
||||
<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>}
|
||||
{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>
|
||||
|
@ -13,6 +13,8 @@ export const deleteModal = getMeta('delete_modal');
|
||||
export const me = getMeta('me');
|
||||
export const searchEnabled = getMeta('search_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 mascot = getMeta('mascot');
|
||||
export const profile_directory = getMeta('profile_directory');
|
||||
|
@ -77,6 +77,7 @@
|
||||
"compose_form.poll.remove_option": "Odstranit tuto volbu",
|
||||
"compose_form.publish": "Tootnout",
|
||||
"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.unmarked": "Média nejsou označena jako citlivá",
|
||||
"compose_form.spoiler.marked": "Text je skrytý za varováním",
|
||||
@ -214,6 +215,7 @@
|
||||
"lightbox.close": "Zavřít",
|
||||
"lightbox.next": "Další",
|
||||
"lightbox.previous": "Předchozí",
|
||||
"lightbox.view_context": "Zobrazit kontext",
|
||||
"lists.account.add": "Přidat do seznamu",
|
||||
"lists.account.remove": "Odebrat ze seznamu",
|
||||
"lists.delete": "Smazat seznam",
|
||||
|
384
app/javascript/mastodon/locales/hi.json
Normal file
384
app/javascript/mastodon/locales/hi.json
Normal 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"
|
||||
}
|
@ -244,11 +244,11 @@
|
||||
"navigation_bar.lists": "Ցանկեր",
|
||||
"navigation_bar.logout": "Դուրս գալ",
|
||||
"navigation_bar.mutes": "Լռեցրած օգտատերեր",
|
||||
"navigation_bar.personal": "Personal",
|
||||
"navigation_bar.personal": "Անձնական",
|
||||
"navigation_bar.pins": "Ամրացված թթեր",
|
||||
"navigation_bar.preferences": "Նախապատվություններ",
|
||||
"navigation_bar.public_timeline": "Դաշնային հոսք",
|
||||
"navigation_bar.security": "Security",
|
||||
"navigation_bar.security": "Անվտանգություն",
|
||||
"notification.favourite": "{name} հավանեց թութդ",
|
||||
"notification.follow": "{name} սկսեց հետեւել քեզ",
|
||||
"notification.mention": "{name} նշեց քեզ",
|
||||
@ -314,7 +314,7 @@
|
||||
"search_results.accounts": "People",
|
||||
"search_results.hashtags": "Hashtags",
|
||||
"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_status": "Open this status in the moderation interface",
|
||||
"status.block": "Արգելափակել @{name}֊ին",
|
||||
|
@ -11,7 +11,7 @@
|
||||
"account.follow": "Volgen",
|
||||
"account.followers": "Volgers",
|
||||
"account.followers.empty": "Niemand volgt nog deze gebruiker.",
|
||||
"account.follows": "Volgt",
|
||||
"account.follows": "Volgend",
|
||||
"account.follows.empty": "Deze gebruiker volgt nog niemand.",
|
||||
"account.follows_you": "Volgt jou",
|
||||
"account.hide_reblogs": "Verberg boosts van @{name}",
|
||||
|
@ -117,7 +117,7 @@
|
||||
"emoji_button.symbols": "Símbolos",
|
||||
"emoji_button.travel": "Viagens & Lugares",
|
||||
"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.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.",
|
||||
|
@ -118,7 +118,6 @@
|
||||
"emoji_button.travel": "Путешествия",
|
||||
"empty_column.account_timeline": "Статусов нет!",
|
||||
"empty_column.account_unavailable": "Профиль недоступен",
|
||||
"empty_column.account_timeline_blocked": "Вы заблокированы",
|
||||
"empty_column.blocks": "Вы ещё никого не заблокировали.",
|
||||
"empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!",
|
||||
"empty_column.direct": "У Вас пока нет личных сообщений. Когда Вы начнёте их отправлять или получать, они появятся здесь.",
|
||||
|
2
app/javascript/mastodon/locales/whitelist_hi.json
Normal file
2
app/javascript/mastodon/locales/whitelist_hi.json
Normal file
@ -0,0 +1,2 @@
|
||||
[
|
||||
]
|
@ -173,6 +173,21 @@ function main() {
|
||||
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 }) => {
|
||||
const header = document.querySelector('.card .card__img img');
|
||||
const [file] = target.files || [];
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'mastodon-font-display';
|
||||
src: local('Montserrat'),
|
||||
src: local('Montserrat Medium'),
|
||||
url('../fonts/montserrat/Montserrat-Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
|
@ -1,6 +1,6 @@
|
||||
@font-face {
|
||||
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.woff') format('woff'),
|
||||
url('../fonts/roboto/roboto-italic-webfont.ttf') format('truetype'),
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
@font-face {
|
||||
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.woff') format('woff'),
|
||||
url('../fonts/roboto/roboto-bold-webfont.ttf') format('truetype'),
|
||||
@ -22,7 +22,7 @@
|
||||
|
||||
@font-face {
|
||||
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.woff') format('woff'),
|
||||
url('../fonts/roboto/roboto-medium-webfont.ttf') format('truetype'),
|
||||
|
@ -50,6 +50,7 @@ $content-width: 840px;
|
||||
color: $darker-text-color;
|
||||
text-decoration: none;
|
||||
transition: all 200ms linear;
|
||||
transition-property: color, background-color;
|
||||
border-radius: 4px 0 0 4px;
|
||||
|
||||
i.fa {
|
||||
@ -60,6 +61,7 @@ $content-width: 840px;
|
||||
color: $primary-text-color;
|
||||
background-color: darken($ui-base-color, 5%);
|
||||
transition: all 100ms linear;
|
||||
transition-property: color, background-color;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
|
@ -264,6 +264,16 @@
|
||||
.compose-form {
|
||||
padding: 10px;
|
||||
|
||||
&__sensitive-button {
|
||||
padding: 10px;
|
||||
padding-top: 0;
|
||||
|
||||
.icon-button {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.compose-form__warning {
|
||||
color: $inverted-text-color;
|
||||
margin-bottom: 10px;
|
||||
@ -1962,6 +1972,7 @@ a.account__display-name {
|
||||
font-weight: 500;
|
||||
border-bottom: 2px solid lighten($ui-base-color, 8%);
|
||||
transition: all 50ms linear;
|
||||
transition-property: border-bottom, background, color;
|
||||
|
||||
.fa {
|
||||
font-weight: 400;
|
||||
@ -2127,7 +2138,7 @@ a.account__display-name {
|
||||
padding: 0;
|
||||
border-radius: 30px;
|
||||
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 {
|
||||
@ -2180,7 +2191,6 @@ a.account__display-name {
|
||||
}
|
||||
|
||||
.react-toggle-thumb {
|
||||
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
@ -2191,6 +2201,7 @@ a.account__display-name {
|
||||
background-color: darken($simple-background-color, 2%);
|
||||
box-sizing: border-box;
|
||||
transition: all 0.25s ease;
|
||||
transition-property: border-color, left;
|
||||
}
|
||||
|
||||
.react-toggle--checked .react-toggle-thumb {
|
||||
@ -2412,7 +2423,7 @@ a.account__display-name {
|
||||
|
||||
& > div {
|
||||
background: rgba($base-shadow-color, 0.6);
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
padding: 12px 9px;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
@ -2423,19 +2434,18 @@ a.account__display-name {
|
||||
button,
|
||||
a {
|
||||
display: inline;
|
||||
color: $primary-text-color;
|
||||
color: $secondary-text-color;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0 5px;
|
||||
padding: 0 8px;
|
||||
text-decoration: none;
|
||||
opacity: 0.6;
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
opacity: 1;
|
||||
color: $primary-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2932,15 +2942,49 @@ a.status-card.compact:hover {
|
||||
}
|
||||
|
||||
.spoiler-button {
|
||||
display: none;
|
||||
left: 4px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color;
|
||||
top: 4px;
|
||||
z-index: 100;
|
||||
|
||||
&.spoiler-button--visible {
|
||||
&--minified {
|
||||
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;
|
||||
opacity: 0;
|
||||
transition: all 100ms linear;
|
||||
transition-property: transform, opacity;
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
@ -3728,6 +3773,31 @@ a.status-card.compact:hover {
|
||||
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 {
|
||||
display: inline-block;
|
||||
}
|
||||
@ -3961,14 +4031,6 @@ a.status-card.compact:hover {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.confirmation-modal {
|
||||
max-width: 85vw;
|
||||
|
||||
@media screen and (min-width: 480px) {
|
||||
max-width: 380px;
|
||||
}
|
||||
}
|
||||
|
||||
.mute-modal {
|
||||
line-height: 24px;
|
||||
}
|
||||
@ -4093,6 +4155,11 @@ a.status-card.compact:hover {
|
||||
ul {
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
max-height: 80vh;
|
||||
|
||||
&.with-status {
|
||||
max-height: calc(80vh - 75px);
|
||||
}
|
||||
|
||||
li:empty {
|
||||
margin: 0;
|
||||
@ -4147,6 +4214,10 @@ a.status-card.compact:hover {
|
||||
color: darken($lighter-text-color, 4%);
|
||||
}
|
||||
}
|
||||
|
||||
.confirmation-modal__secondary-button {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.confirmation-modal__container,
|
||||
@ -4199,6 +4270,7 @@ a.status-card.compact:hover {
|
||||
pointer-events: none;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.1s ease;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.media-gallery__gifv {
|
||||
@ -4312,6 +4384,8 @@ a.status-card.compact:hover {
|
||||
text-decoration: none;
|
||||
color: $secondary-text-color;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&,
|
||||
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 {
|
||||
height: 100%;
|
||||
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 {
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
@ -4711,62 +4817,18 @@ a.status-card.compact:hover {
|
||||
|
||||
.account-gallery__container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 2px;
|
||||
padding: 4px 2px;
|
||||
}
|
||||
|
||||
.account-gallery__item {
|
||||
flex-grow: 1;
|
||||
width: 50%;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
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;
|
||||
}
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.notification__filter-bar,
|
||||
|
@ -533,6 +533,17 @@ code {
|
||||
color: $error-value-color;
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
color: $darker-text-color;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: $primary-text-color;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
|
||||
&__img {
|
||||
width: 100%;
|
||||
height: 167px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 4px 4px 0 0;
|
||||
|
@ -194,7 +194,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
next if attachment['url'].blank?
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
def supported_blurhash?(blurhash)
|
||||
components = blurhash.blank? ? nil : Blurhash.components(blurhash)
|
||||
components.present? && components.none? { |comp| comp > 5 }
|
||||
end
|
||||
|
||||
def skip_download?
|
||||
return @skip_download if defined?(@skip_download)
|
||||
@skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
|
||||
|
@ -19,6 +19,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
|
||||
conversation: { 'ostatus' => 'http://ostatus.org#', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'conversation' => 'ostatus:conversation' },
|
||||
focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } },
|
||||
identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' },
|
||||
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
|
||||
}.freeze
|
||||
|
||||
def self.default_key_transform
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
class ProofProvider::Keybase
|
||||
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
|
||||
|
||||
|
@ -6,6 +6,7 @@ module LdapAuthenticable
|
||||
def ldap_setup(_attributes)
|
||||
self.confirmed_at = Time.now.utc
|
||||
self.admin = false
|
||||
self.external = true
|
||||
|
||||
save!
|
||||
end
|
||||
|
@ -66,6 +66,7 @@ module Omniauthable
|
||||
email: email || "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com",
|
||||
password: Devise.friendly_token[0, 20],
|
||||
agreement: true,
|
||||
external: true,
|
||||
account_attributes: {
|
||||
username: ensure_unique_username(auth.uid),
|
||||
display_name: display_name,
|
||||
|
@ -34,6 +34,7 @@ module PamAuthenticable
|
||||
self.confirmed_at = Time.now.utc
|
||||
self.admin = false
|
||||
self.account = account
|
||||
self.external = true
|
||||
|
||||
account.destroy! unless save
|
||||
end
|
||||
|
@ -29,4 +29,11 @@ class DomainBlock < ApplicationRecord
|
||||
def self.blocked?(domain)
|
||||
where(domain: domain, severity: :suspend).exists?
|
||||
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
|
||||
|
@ -18,6 +18,7 @@
|
||||
# account_id :bigint(8)
|
||||
# description :text
|
||||
# scheduled_status_id :bigint(8)
|
||||
# blurhash :string
|
||||
#
|
||||
|
||||
class MediaAttachment < ApplicationRecord
|
||||
@ -32,6 +33,11 @@ class MediaAttachment < ApplicationRecord
|
||||
VIDEO_MIME_TYPES = ['video/webm', 'video/mp4', 'video/quicktime'].freeze
|
||||
VIDEO_CONVERTIBLE_MIME_TYPES = ['video/webm', 'video/quicktime'].freeze
|
||||
|
||||
BLURHASH_OPTIONS = {
|
||||
x_comp: 4,
|
||||
y_comp: 4,
|
||||
}.freeze
|
||||
|
||||
IMAGE_STYLES = {
|
||||
original: {
|
||||
pixels: 1_638_400, # 1280x1280px
|
||||
@ -41,6 +47,7 @@ class MediaAttachment < ApplicationRecord
|
||||
small: {
|
||||
pixels: 160_000, # 400x400px
|
||||
file_geometry_parser: FastGeometryParser,
|
||||
blurhash: BLURHASH_OPTIONS,
|
||||
},
|
||||
}.freeze
|
||||
|
||||
@ -53,6 +60,8 @@ class MediaAttachment < ApplicationRecord
|
||||
},
|
||||
format: 'png',
|
||||
time: 0,
|
||||
file_geometry_parser: FastGeometryParser,
|
||||
blurhash: BLURHASH_OPTIONS,
|
||||
},
|
||||
}.freeze
|
||||
|
||||
@ -166,11 +175,11 @@ class MediaAttachment < ApplicationRecord
|
||||
|
||||
def file_processors(f)
|
||||
if f.file_content_type == 'image/gif'
|
||||
[:gif_transcoder]
|
||||
[:gif_transcoder, :blurhash_transcoder]
|
||||
elsif VIDEO_MIME_TYPES.include? f.file_content_type
|
||||
[:video_transcoder]
|
||||
[:video_transcoder, :blurhash_transcoder]
|
||||
else
|
||||
[:lazy_thumbnail]
|
||||
[:lazy_thumbnail, :blurhash_transcoder]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -78,7 +78,7 @@ class User < ApplicationRecord
|
||||
accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? }
|
||||
|
||||
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 :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
|
||||
|
||||
attr_reader :invite_code
|
||||
attr_writer :external
|
||||
|
||||
def confirmed?
|
||||
confirmed_at.present?
|
||||
end
|
||||
|
||||
def invited?
|
||||
invite_id.present?
|
||||
invite_id.present? && invite.valid_for_use?
|
||||
end
|
||||
|
||||
def disable!
|
||||
@ -273,13 +274,17 @@ class User < ApplicationRecord
|
||||
private
|
||||
|
||||
def set_approved
|
||||
self.approved = open_registrations? || invited?
|
||||
self.approved = open_registrations? || invited? || external?
|
||||
end
|
||||
|
||||
def open_registrations?
|
||||
Setting.registrations_mode == 'open'
|
||||
end
|
||||
|
||||
def external?
|
||||
!!@external
|
||||
end
|
||||
|
||||
def sanitize_languages
|
||||
return if chosen_languages.nil?
|
||||
chosen_languages.reject!(&:blank?)
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||
context_extensions :atom_uri, :conversation, :sensitive,
|
||||
:hashtag, :emoji, :focal_point
|
||||
:hashtag, :emoji, :focal_point, :blurhash
|
||||
|
||||
attributes :id, :type, :summary,
|
||||
:in_reply_to, :published, :url,
|
||||
@ -153,7 +153,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||
class MediaAttachmentSerializer < ActivityPub::Serializer
|
||||
include RoutingHelper
|
||||
|
||||
attributes :type, :media_type, :url, :name
|
||||
attributes :type, :media_type, :url, :name, :blurhash
|
||||
attribute :focal_point, if: :focal_point?
|
||||
|
||||
def type
|
||||
|
@ -14,6 +14,8 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||
domain: Rails.configuration.x.local_domain,
|
||||
admin: object.admin&.id&.to_s,
|
||||
search_enabled: Chewy.enabled?,
|
||||
repository: Mastodon::Version.repository,
|
||||
source_url: Mastodon::Version.source_url,
|
||||
version: Mastodon::Version.to_s,
|
||||
invites_enabled: Setting.min_invite_role == 'user',
|
||||
mascot: instance_presenter.mascot&.file&.url,
|
||||
|
@ -5,7 +5,7 @@ class REST::MediaAttachmentSerializer < ActiveModel::Serializer
|
||||
|
||||
attributes :id, :type, :url, :preview_url,
|
||||
:remote_url, :text_url, :meta,
|
||||
:description
|
||||
:description, :blurhash
|
||||
|
||||
def id
|
||||
object.id.to_s
|
||||
|
@ -6,6 +6,7 @@ class BlockService < BaseService
|
||||
|
||||
UnfollowService.new.call(account, target_account) if account.following?(target_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)
|
||||
|
||||
|
@ -165,7 +165,7 @@ class FetchLinkCardService < BaseService
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def lock_options
|
||||
|
@ -2,7 +2,10 @@
|
||||
|
||||
class BlacklistedEmailValidator < ActiveModel::Validator
|
||||
def validate(user)
|
||||
return if user.invited?
|
||||
|
||||
@email = user.email
|
||||
|
||||
user.errors.add(:email, I18n.t('users.invalid_email')) if blocked_email?
|
||||
end
|
||||
|
||||
@ -13,7 +16,7 @@ class BlacklistedEmailValidator < ActiveModel::Validator
|
||||
end
|
||||
|
||||
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?
|
||||
|
||||
domains = Rails.configuration.x.email_domains_blacklist.gsub('.', '\.')
|
||||
|
@ -3,7 +3,7 @@
|
||||
= image_tag (current_account&.user&.setting_auto_play_gif ? account.header_original_url : account.header_static_url), class: 'parallax'
|
||||
.public-account-header__bar
|
||||
= 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__name
|
||||
%h1
|
||||
|
@ -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)
|
||||
|
||||
.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'
|
||||
|
@ -28,7 +28,7 @@
|
||||
- elsif !status.media_attachments.empty?
|
||||
- if status.media_attachments.first.video?
|
||||
- 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 }
|
||||
- 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
|
||||
|
@ -32,7 +32,7 @@
|
||||
- elsif !status.media_attachments.empty?
|
||||
- if status.media_attachments.first.video?
|
||||
- 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 }
|
||||
- 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
|
||||
|
@ -7,5 +7,7 @@ class ActivityPub::ProcessingWorker
|
||||
|
||||
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)
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Rails.logger.debug "Error processing incoming ActivityPub object: #{e}"
|
||||
end
|
||||
end
|
||||
|
@ -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']
|
||||
Rails.logger.info("Rate limit hit (#{req.env['rack.attack.match_type']}): #{req.ip} #{req.request_method} #{req.fullpath}")
|
||||
end
|
||||
|
@ -1,3 +1,4 @@
|
||||
require 'stoplight'
|
||||
|
||||
Stoplight::Light.default_data_store = Stoplight::DataStore::Redis.new(Redis.current)
|
||||
Stoplight::Light.default_notifiers = [Stoplight::Notifier::Logger.new(Rails.logger)]
|
||||
|
@ -2,8 +2,9 @@
|
||||
pt-BR:
|
||||
activerecord:
|
||||
attributes:
|
||||
status:
|
||||
owned_poll: enquete
|
||||
poll:
|
||||
expires_at: Expira em
|
||||
options: Escolhas
|
||||
errors:
|
||||
models:
|
||||
account:
|
||||
|
@ -3,7 +3,7 @@ sk:
|
||||
activerecord:
|
||||
attributes:
|
||||
poll:
|
||||
expires_at: Uzávierka
|
||||
expires_at: Trvá do
|
||||
options: Voľby
|
||||
status:
|
||||
owned_poll: Anketa
|
||||
|
@ -68,6 +68,7 @@ ca:
|
||||
admin: Administrador
|
||||
bot: Bot
|
||||
moderator: Moderador
|
||||
unavailable: Perfil inaccessible
|
||||
unfollow: Deixa de seguir
|
||||
admin:
|
||||
account_actions:
|
||||
@ -80,6 +81,7 @@ ca:
|
||||
destroyed_msg: Nota de moderació destruïda amb èxit!
|
||||
accounts:
|
||||
approve: Aprova
|
||||
approve_all: Aprova'ls tots
|
||||
are_you_sure: N'estàs segur?
|
||||
avatar: Avatar
|
||||
by_domain: Domini
|
||||
@ -132,6 +134,7 @@ ca:
|
||||
moderation_notes: Notes de moderació
|
||||
most_recent_activity: Activitat 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
|
||||
not_subscribed: No subscrit
|
||||
outbox_url: URL de la bústia de sortida
|
||||
@ -144,6 +147,7 @@ ca:
|
||||
push_subscription_expires: La subscripció PuSH expira
|
||||
redownload: Actualitza el perfil
|
||||
reject: Rebutja
|
||||
reject_all: Rebutja'ls tots
|
||||
remove_avatar: Eliminar avatar
|
||||
remove_header: Treu la capçalera
|
||||
resend_confirmation:
|
||||
@ -330,6 +334,8 @@ ca:
|
||||
expired: Caducat
|
||||
title: Filtre
|
||||
title: Convida
|
||||
pending_accounts:
|
||||
title: Comptes pendents (%{count})
|
||||
relays:
|
||||
add_new: Afegiu un nou relay
|
||||
delete: Esborra
|
||||
@ -854,18 +860,23 @@ ca:
|
||||
revoke_success: S'ha revocat la sessió amb èxit
|
||||
title: Sessions
|
||||
settings:
|
||||
account: Compte
|
||||
account_settings: Ajustos del compte
|
||||
appearance: Aparènça
|
||||
authorized_apps: Aplicacions autoritzades
|
||||
back: Torna a l'inici
|
||||
back: Torna a Mastodon
|
||||
delete: Eliminació del compte
|
||||
development: Desenvolupament
|
||||
edit_profile: Editar perfil
|
||||
export: Exportar informació
|
||||
export: Exportar dades
|
||||
featured_tags: Etiquetes destacades
|
||||
identity_proofs: Proves d'identitat
|
||||
import: Importar
|
||||
import_and_export: Importar i exportar
|
||||
migrate: Migració del compte
|
||||
notifications: Notificacions
|
||||
preferences: Preferències
|
||||
profile: Perfil
|
||||
relationships: Seguits i seguidors
|
||||
two_factor_authentication: Autenticació de dos factors
|
||||
statuses:
|
||||
@ -1040,7 +1051,7 @@ ca:
|
||||
welcome:
|
||||
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.
|
||||
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_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
|
||||
|
@ -269,6 +269,7 @@ co:
|
||||
created_msg: U blucchime di u duminiu hè attivu
|
||||
destroyed_msg: U blucchime di u duminiu ùn hè più attivu
|
||||
domain: Duminiu
|
||||
existing_domain_block_html: Avete digià impostu limite più strette nant'à %{name}, duvete <a href="%{unblock_url}">sbluccallu</a> primu.
|
||||
new:
|
||||
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.
|
||||
@ -862,6 +863,7 @@ co:
|
||||
settings:
|
||||
account: Contu
|
||||
account_settings: Parametri di u contu
|
||||
appearance: Apparenza
|
||||
authorized_apps: Applicazione auturizate
|
||||
back: Ritornu nant’à Mastodon
|
||||
delete: Suppressione di u contu
|
||||
|
@ -273,6 +273,7 @@ cs:
|
||||
created_msg: Blokace domény se právě vyřizuje
|
||||
destroyed_msg: Blokace domény byla zrušena
|
||||
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:
|
||||
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í.
|
||||
|
@ -3,7 +3,7 @@ sk:
|
||||
devise:
|
||||
confirmations:
|
||||
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.
|
||||
failure:
|
||||
already_authenticated: Už si prihlásený/á.
|
||||
@ -11,27 +11,27 @@ sk:
|
||||
invalid: Nesprávny %{authentication_keys}, alebo heslo.
|
||||
last_attempt: Máš posledný pokus pred zamknutím tvojho účtu.
|
||||
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ý.
|
||||
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ť.
|
||||
unconfirmed: Pred pokračovaním musíš potvrdiť svoj email.
|
||||
mailer:
|
||||
confirmation_instructions:
|
||||
action: Potvŕď emailovú adresu
|
||||
action: Potvrď emailovú adresu
|
||||
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.
|
||||
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}'
|
||||
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 pokyny pre %{instance}'
|
||||
title: Potvrď emailovú adresu
|
||||
email_changed:
|
||||
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á'
|
||||
title: Nová emailová adresa
|
||||
password_change:
|
||||
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é'
|
||||
title: Heslo bolo zmenené
|
||||
reconfirmation_instructions:
|
||||
@ -42,17 +42,17 @@ sk:
|
||||
reset_password_instructions:
|
||||
action: Zmeň svoje heslo
|
||||
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é.
|
||||
subject: 'Mastodon: Inštrukcie pre obnovu hesla'
|
||||
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: Pokyny pre obnovu hesla'
|
||||
title: Nastav nové heslo
|
||||
unlock_instructions:
|
||||
subject: 'Mastodon: Inštrukcie pre odomknutie účtu'
|
||||
subject: 'Mastodon: Pokyny na odomknutie účtu'
|
||||
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}.
|
||||
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.
|
||||
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.
|
||||
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: 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íš.
|
||||
updated: Tvoje heslo bolo úspešne zmenené. Teraz si prihlásený/á.
|
||||
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.
|
||||
updated: Tvoj účet bol úspešne aktualizovaný.
|
||||
sessions:
|
||||
already_signed_out: Odhlásil/a si sa úspešné.
|
||||
signed_in: Prihlásil/a si sa úspešné.
|
||||
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šne.
|
||||
signed_out: Odhlásil/a si sa úspešne.
|
||||
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_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ý
|
||||
expired: vypŕšal, prosím, vyžiadaj si nový
|
||||
not_found: nenájdený
|
||||
not_locked: nebol uzamknutý
|
||||
not_locked: nebol zamknutý
|
||||
not_saved:
|
||||
few: "%{resource} nebol uložený kôli %{count} chybám:"
|
||||
one: "%{resource} nebol uložený kôli chybe:"
|
||||
other: "%{resource} nebol uložený kôli %{count} chybám:"
|
||||
few: "%{resource} nebol uložený kvôli %{count} chybám:"
|
||||
one: "%{resource} nebol uložený kvôli chybe:"
|
||||
other: "%{resource} nebol uložený kvôli %{count} chybám:"
|
||||
|
@ -269,6 +269,7 @@ en:
|
||||
created_msg: Domain block is now being processed
|
||||
destroyed_msg: Domain block has been undone
|
||||
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:
|
||||
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.
|
||||
|
@ -260,10 +260,10 @@ fr:
|
||||
title: Nouveau blocage de domaine
|
||||
reject_media: Fichiers média rejetés
|
||||
reject_media_hint: Supprime localement les fichiers média stockés et refuse d’en télécharger ultérieurement. Ne concerne pas les suspensions
|
||||
reject_reports: Rapports de rejet
|
||||
reject_reports_hint: Ignorez tous les rapports provenant de ce domaine. Sans objet pour les suspensions
|
||||
reject_reports: Rejeter les signalements
|
||||
reject_reports_hint: Ignorez tous les signalements provenant de ce domaine. Ne concerne pas les suspensions
|
||||
rejecting_media: rejet des fichiers multimédia
|
||||
rejecting_reports: rejet de rapports
|
||||
rejecting_reports: rejet des signalements
|
||||
severity:
|
||||
silence: silencié
|
||||
suspend: suspendu
|
||||
|
@ -68,6 +68,7 @@ nl:
|
||||
admin: Beheerder
|
||||
bot: Bot
|
||||
moderator: Moderator
|
||||
unavailable: Profiel niet beschikbaar
|
||||
unfollow: Ontvolgen
|
||||
admin:
|
||||
account_actions:
|
||||
@ -80,6 +81,7 @@ nl:
|
||||
destroyed_msg: Verwijderen van opmerking voor moderatoren geslaagd!
|
||||
accounts:
|
||||
approve: Goedkeuren
|
||||
approve_all: Alles goedkeuren
|
||||
are_you_sure: Weet je het zeker?
|
||||
avatar: Avatar
|
||||
by_domain: Domein
|
||||
@ -132,6 +134,7 @@ nl:
|
||||
moderation_notes: Opmerkingen voor moderatoren
|
||||
most_recent_activity: Laatst actief
|
||||
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
|
||||
not_subscribed: Niet geabonneerd
|
||||
outbox_url: Outbox-URL
|
||||
@ -144,6 +147,7 @@ nl:
|
||||
push_subscription_expires: PuSH-abonnement verloopt op
|
||||
redownload: Profiel vernieuwen
|
||||
reject: Afkeuren
|
||||
reject_all: Alles afkeuren
|
||||
remove_avatar: Avatar verwijderen
|
||||
remove_header: Omslagfoto verwijderen
|
||||
resend_confirmation:
|
||||
@ -245,6 +249,7 @@ nl:
|
||||
feature_profile_directory: Gebruikersgids
|
||||
feature_registrations: Registraties
|
||||
feature_relay: Federatierelay
|
||||
feature_timeline_preview: Voorvertoning van tijdlijn
|
||||
features: Functies
|
||||
hidden_service: Federatie met verborgen diensten
|
||||
open_reports: onopgeloste rapportages
|
||||
@ -329,6 +334,8 @@ nl:
|
||||
expired: Verlopen
|
||||
title: Filter
|
||||
title: Uitnodigingen
|
||||
pending_accounts:
|
||||
title: Accounts in afwachting (%{count})
|
||||
relays:
|
||||
add_new: Nieuwe relayserver toevoegen
|
||||
delete: Verwijderen
|
||||
@ -428,14 +435,14 @@ nl:
|
||||
desc_html: Medewerkersbadge op profielpagina tonen
|
||||
title: Medewerkersbadge tonen
|
||||
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><a></code> en <code><em></code>.
|
||||
title: Omschrijving Mastodonserver
|
||||
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><a></code> en <code><em></code>.
|
||||
title: Omschrijving Mastodonserver (API)
|
||||
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
|
||||
title: Uitgebreide omschrijving Mastodonserver
|
||||
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.
|
||||
title: Korte omschrijving Mastodonserver
|
||||
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: Omschrijving Mastodonserver (website)
|
||||
site_terms:
|
||||
desc_html: Je kan hier jouw eigen privacybeleid, gebruiksvoorwaarden en ander juridisch jargon kwijt. Je kan HTML gebruiken
|
||||
title: Aangepaste gebruiksvoorwaarden
|
||||
@ -585,6 +592,9 @@ nl:
|
||||
content: Het spijt ons, er is aan onze kant iets fout gegaan.
|
||||
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.
|
||||
existing_username_validator:
|
||||
not_found: Kon geen lokale gebruiker met die gebruikersnaam vinden
|
||||
not_found_multiple: Kon %{usernames} niet vinden
|
||||
exports:
|
||||
archive_takeout:
|
||||
date: Datum
|
||||
@ -628,10 +638,13 @@ nl:
|
||||
all: Alles
|
||||
changes_saved_msg: Wijzigingen succesvol opgeslagen!
|
||||
copy: Kopiëren
|
||||
order_by: Sorteer op
|
||||
save_changes: Wijzigingen opslaan
|
||||
validation_errors:
|
||||
one: Er is iets niet helemaal goed! Bekijk onderstaande fout
|
||||
other: Er is iets niet helemaal goed! Bekijk onderstaande %{count} fouten
|
||||
html_validator:
|
||||
invalid_markup: 'bevat ongeldige HTML-opmaak: %{error}'
|
||||
identity_proofs:
|
||||
active: Actief
|
||||
authorize: Ja, autoriseren
|
||||
@ -646,6 +659,8 @@ nl:
|
||||
i_am_html: Ik ben %{username} op %{service}.
|
||||
identity: Identiteit
|
||||
inactive: Inactief
|
||||
publicize_checkbox: 'En toot dit:'
|
||||
publicize_toot: 'Het is bewezen! Ik ben %{username} op %{service}: %{url}'
|
||||
status: Verificatiestatus
|
||||
view_proof: Bekijk bewijs
|
||||
imports:
|
||||
@ -768,6 +783,8 @@ nl:
|
||||
relationships:
|
||||
activity: Accountactiviteit
|
||||
dormant: Sluimerend
|
||||
last_active: Laatst actief
|
||||
most_recent: Recentelijk gevolgd
|
||||
moved: Verhuisd
|
||||
mutual: Wederzijds
|
||||
primary: Primair
|
||||
@ -843,6 +860,9 @@ nl:
|
||||
revoke_success: Sessie succesvol ingetrokken
|
||||
title: Sessies
|
||||
settings:
|
||||
account: Account
|
||||
account_settings: Accountinstellingen
|
||||
appearance: Uiterlijk
|
||||
authorized_apps: Geautoriseerde apps
|
||||
back: Terug naar Mastodon
|
||||
delete: Account verwijderen
|
||||
@ -852,9 +872,11 @@ nl:
|
||||
featured_tags: Uitgelichte hashtags
|
||||
identity_proofs: Identiteitsbewijzen
|
||||
import: Importeren
|
||||
import_and_export: Importeren en exporteren
|
||||
migrate: Accountmigratie
|
||||
notifications: Meldingen
|
||||
preferences: Voorkeuren
|
||||
profile: Profiel
|
||||
relationships: Volgers en gevolgden
|
||||
two_factor_authentication: Tweestapsverificatie
|
||||
statuses:
|
||||
|
@ -68,6 +68,7 @@ pt-BR:
|
||||
admin: Administrador
|
||||
bot: Robô
|
||||
moderator: Moderador
|
||||
unavailable: Perfil indisponível
|
||||
unfollow: Deixar de seguir
|
||||
admin:
|
||||
account_actions:
|
||||
@ -80,6 +81,7 @@ pt-BR:
|
||||
destroyed_msg: Nota de moderação excluída com sucesso!
|
||||
accounts:
|
||||
approve: Aprovar
|
||||
approve_all: Aprovar tudo
|
||||
are_you_sure: Você tem certeza?
|
||||
avatar: Avatar
|
||||
by_domain: Domínio
|
||||
@ -132,6 +134,7 @@ pt-BR:
|
||||
moderation_notes: Notas de moderação
|
||||
most_recent_activity: Atividade 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
|
||||
not_subscribed: Não está inscrito
|
||||
outbox_url: URL da caixa de saída
|
||||
@ -144,6 +147,7 @@ pt-BR:
|
||||
push_subscription_expires: Inscrição PuSH expira
|
||||
redownload: Atualizar perfil
|
||||
reject: Rejeitar
|
||||
reject_all: Rejeitar tudo
|
||||
remove_avatar: Remover avatar
|
||||
remove_header: Remover cabeçalho
|
||||
resend_confirmation:
|
||||
@ -330,6 +334,8 @@ pt-BR:
|
||||
expired: Expirados
|
||||
title: Filtro
|
||||
title: Convites
|
||||
pending_accounts:
|
||||
title: Contas pendentes (%{count})
|
||||
relays:
|
||||
add_new: Adicionar novo repetidor
|
||||
delete: Excluir
|
||||
@ -854,6 +860,9 @@ pt-BR:
|
||||
revoke_success: Sessão revogada com sucesso
|
||||
title: Sessões
|
||||
settings:
|
||||
account: Conta
|
||||
account_settings: Configurações da conta
|
||||
appearance: Aparência
|
||||
authorized_apps: Apps autorizados
|
||||
back: Voltar para o Mastodon
|
||||
delete: Exclusão de conta
|
||||
@ -863,9 +872,11 @@ pt-BR:
|
||||
featured_tags: Hashtags em destaque
|
||||
identity_proofs: Provas de identidade
|
||||
import: Importar
|
||||
import_and_export: Importar e exportar
|
||||
migrate: Migração de conta
|
||||
notifications: Notificações
|
||||
preferences: Preferências
|
||||
profile: Perfil
|
||||
relationships: Seguindo e seguidores
|
||||
two_factor_authentication: Autenticação em dois passos
|
||||
statuses:
|
||||
|
@ -41,6 +41,8 @@ nl:
|
||||
name: 'Je wilt misschien een van deze gebruiken:'
|
||||
imports:
|
||||
data: CSV-bestand dat op een andere Mastodonserver werd geëxporteerd
|
||||
invite_request:
|
||||
text: Dit helpt ons om jouw aanvraag te beoordelen
|
||||
sessions:
|
||||
otp: 'Voer de tweestaps-aanmeldcode vanaf jouw mobiele telefoon in of gebruik een van jouw herstelcodes:'
|
||||
user:
|
||||
@ -118,12 +120,15 @@ nl:
|
||||
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_dm: Directe berichten van mensen die jij niet volgt blokkeren
|
||||
invite_request:
|
||||
text: Waarom wil jij je aanmelden?
|
||||
notification_emails:
|
||||
digest: Periodiek e-mails met een samenvatting versturen
|
||||
favourite: Een e-mail versturen wanneer iemand jouw toot aan hun favorieten heeft toegevoegd
|
||||
follow: Een e-mail versturen wanneer iemand jou volgt
|
||||
follow_request: Een e-mail versturen wanneer iemand jou wil volgen
|
||||
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
|
||||
report: Verstuur een e-mail wanneer een nieuw rapportage is ingediend
|
||||
'no': Nee
|
||||
|
@ -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_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_hide_all: Zawsze ukrywaj zawartość multimedialną
|
||||
setting_display_media_show_all: Zawsze pokazuj zawartość multimedialną jako wrażliwą
|
||||
setting_display_media_hide_all: Zawsze oznaczaj 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_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
|
||||
|
@ -41,6 +41,8 @@ pt-BR:
|
||||
name: 'Você pode querer usar um destes:'
|
||||
imports:
|
||||
data: Arquivo CSV exportado de outra instância do Mastodon
|
||||
invite_request:
|
||||
text: Isso vai nos ajudar a revisar sua aplicação
|
||||
sessions:
|
||||
otp: 'Insira o código de autenticação gerado pelo app no seu celular ou use um dos códigos de recuperação:'
|
||||
user:
|
||||
@ -119,12 +121,15 @@ pt-BR:
|
||||
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_dm: Bloquear mensagens diretas de pessoas que você não segue
|
||||
invite_request:
|
||||
text: Por que você quer se cadastrar?
|
||||
notification_emails:
|
||||
digest: Mandar e-mails com relatórios
|
||||
favourite: Mandar um e-mail quando alguém favoritar suas postagens
|
||||
follow: Mandar um e-mail quando alguém te seguir
|
||||
follow_request: Mandar um e-maill quando alguém solicitar ser seu seguidor
|
||||
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
|
||||
report: Mandar um e-mail quando uma nova denúncia é submetida
|
||||
'no': Não
|
||||
|
@ -10,7 +10,7 @@ sk:
|
||||
api: API
|
||||
apps: Aplikácie
|
||||
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
|
||||
contact: Kontakt
|
||||
contact_missing: Nezadaný
|
||||
@ -20,9 +20,9 @@ sk:
|
||||
extended_description_html: |
|
||||
<h3>Pravidlá</h3>
|
||||
<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"
|
||||
get_apps: Vyskúšaj mobilnú aplikáciu
|
||||
get_apps: Vyskúšaj aplikácie
|
||||
hosted_on: Mastodon hostovaný na %{domain}
|
||||
learn_more: Zisti viac
|
||||
privacy_policy: Ustanovenia o súkromí
|
||||
@ -32,22 +32,22 @@ sk:
|
||||
status_count_after:
|
||||
few: príspevkov
|
||||
one: príspevok
|
||||
other: príspevkov
|
||||
other: príspevky
|
||||
status_count_before: Ktorí napísali
|
||||
tagline: Následuj kamarátov, a objavuj nových
|
||||
terms: Podmienky užívania
|
||||
user_count_after:
|
||||
few: užívatelia
|
||||
few: užívateľov
|
||||
one: užívateľ
|
||||
other: užívateľov
|
||||
other: užívatelia
|
||||
user_count_before: Domov pre
|
||||
what_is_mastodon: Čo je Mastodon?
|
||||
accounts:
|
||||
choices_html: "%{name}vé voľby:"
|
||||
follow: Sleduj
|
||||
follow: Následuj
|
||||
followers:
|
||||
few: Sledovatelia
|
||||
one: Sledujúci
|
||||
few: Sledovateľov
|
||||
one: Sledovateľ
|
||||
other: Sledovatelia
|
||||
following: Sledovaní
|
||||
joined: Pridal/a sa v %{date}
|
||||
@ -70,8 +70,9 @@ sk:
|
||||
reserved_username: Prihlasovacie meno je rezervované
|
||||
roles:
|
||||
admin: Administrátor
|
||||
bot: Automat
|
||||
bot: Bot
|
||||
moderator: Moderátor
|
||||
unavailable: Profil nieje dostupný
|
||||
unfollow: Prestaň sledovať
|
||||
admin:
|
||||
account_actions:
|
||||
@ -84,12 +85,13 @@ sk:
|
||||
destroyed_msg: Moderátorska poznámka bola úspešne zmazaná!
|
||||
accounts:
|
||||
approve: Schváľ
|
||||
approve_all: Schváľ všetky
|
||||
are_you_sure: Si si istý/á?
|
||||
avatar: Maskot
|
||||
by_domain: Doména
|
||||
change_email:
|
||||
changed_msg: Email k tomuto účtu bol úspešne zmenený!
|
||||
current_email: Súčastný email
|
||||
changed_msg: Email pre tento účet bol úspešne zmenený!
|
||||
current_email: Súčasný email
|
||||
label: Zmeň email
|
||||
new_email: Nový email
|
||||
submit: Zmeň email
|
||||
@ -97,37 +99,37 @@ sk:
|
||||
confirm: Potvrď
|
||||
confirmed: Potvrdený
|
||||
confirming: Potvrdzujúci
|
||||
deleted: Zmazané
|
||||
demote: Degradovať
|
||||
deleted: Vymazané
|
||||
demote: Degraduj
|
||||
disable: Zablokuj
|
||||
disable_two_factor_authentication: Zakáž 2FA
|
||||
disabled: Blokovaný
|
||||
display_name: Zobraziť meno
|
||||
display_name: Ukáž meno
|
||||
domain: Doména
|
||||
edit: Uprav
|
||||
email: Email
|
||||
email_status: Stav emailu
|
||||
enable: Povoliť
|
||||
enable: Povoľ
|
||||
enabled: Povolený
|
||||
feed_url: URL časovej osi
|
||||
feed_url: URL adresa časovej osi
|
||||
followers: Sledujúci
|
||||
followers_url: URL sledujúcich
|
||||
followers_url: URL adresa sledujúcich
|
||||
follows: Sledovania
|
||||
header: Hlavička
|
||||
inbox_url: URL prijatých správ
|
||||
inbox_url: URL adresa prijatých správ
|
||||
invited_by: Pozvaný/á užívateľom
|
||||
ip: IP
|
||||
ip: IP adresa
|
||||
joined: Pridal/a sa
|
||||
location:
|
||||
all: Všetko
|
||||
local: Miestne
|
||||
remote: Federované
|
||||
title: Lokácia
|
||||
title: Umiestnenie
|
||||
login_status: Stav prihlásenia
|
||||
media_attachments: Prílohy
|
||||
memorialize: Zmeniť na "Navždy budeme spomínať"
|
||||
memorialize: Zmeň na "Navždy budeme spomínať"
|
||||
moderation:
|
||||
active: Aktívny
|
||||
active: Aktívny/a
|
||||
all: Všetko
|
||||
pending: Čakajúci
|
||||
silenced: Umlčané
|
||||
@ -135,21 +137,22 @@ sk:
|
||||
title: Moderácia
|
||||
moderation_notes: Moderátorské poznámky
|
||||
most_recent_activity: Posledná aktivita
|
||||
most_recent_ip: Posledná IP
|
||||
most_recent_ip: Posledná IP adresa
|
||||
no_limits_imposed: Nie sú stanovené žiadné obmedzenia
|
||||
not_subscribed: Neodoberá
|
||||
outbox_url: URL poslaných
|
||||
pending: Vyžaduje posúdenie
|
||||
perform_full_suspension: Vylúč
|
||||
profile_url: URL profilu
|
||||
promote: Povýš
|
||||
profile_url: URL adresa profilu
|
||||
promote: Vyzdvihni
|
||||
protocol: Protokol
|
||||
public: Verejná os
|
||||
public: Verejná časová os
|
||||
push_subscription_expires: PuSH odoberanie expiruje
|
||||
redownload: Obnov profil
|
||||
reject: Odmietni
|
||||
remove_avatar: Odstrániť avatár
|
||||
remove_header: Odstráň hlavičku
|
||||
reject: Zamietni
|
||||
reject_all: Zamietni všetky
|
||||
remove_avatar: Vymaž avatar
|
||||
remove_header: Vymaž hlavičku
|
||||
resend_confirmation:
|
||||
already_confirmed: Tento užívateľ je už potvrdený
|
||||
send: Odošli potvrdzovací email znovu
|
||||
@ -164,7 +167,7 @@ sk:
|
||||
staff: Člen
|
||||
user: Užívateľ
|
||||
salmon_url: Salmon adresa
|
||||
search: Hľadať
|
||||
search: Hľadaj
|
||||
shared_inbox_url: URL zdieľanej schránky
|
||||
show:
|
||||
created_reports: Vytvorené hlásenia
|
||||
@ -172,15 +175,15 @@ sk:
|
||||
silence: Stíš
|
||||
silenced: Utíšený/é
|
||||
statuses: Príspevky
|
||||
subscribe: Odoberať
|
||||
subscribe: Odoberaj
|
||||
suspended: Zablokovaní
|
||||
title: Účty
|
||||
unconfirmed_email: Nepotvrdený email
|
||||
undo_silenced: Zrušiť stíšenie
|
||||
undo_silenced: Zruš stíšenie
|
||||
undo_suspension: Zruš blokovanie
|
||||
unsubscribe: Prestaň odoberať
|
||||
username: Prezývka
|
||||
warn: Varovať
|
||||
warn: Varuj
|
||||
web: Web
|
||||
action_logs:
|
||||
actions:
|
||||
@ -227,7 +230,7 @@ sk:
|
||||
disable: Zakázať
|
||||
disabled_msg: Emoji bolo úspešne zakázané
|
||||
emoji: Emotikony
|
||||
enable: Povoliť
|
||||
enable: Povoľ
|
||||
enabled_msg: Emoji bolo úspešne povolené
|
||||
image_hint: PNG do 50KB
|
||||
listed: V zozname
|
||||
@ -240,7 +243,7 @@ sk:
|
||||
unlisted: Nie je na zozname
|
||||
update_failed_msg: Nebolo možné aktualizovať toto emoji
|
||||
updated_msg: Emoji bolo úspešne aktualizované!
|
||||
upload: Nahrať
|
||||
upload: Nahraj
|
||||
dashboard:
|
||||
backlog: odložené aktivity
|
||||
config: Nastavenia
|
||||
@ -249,10 +252,11 @@ sk:
|
||||
feature_profile_directory: Katalóg profilov
|
||||
feature_registrations: Registrácie
|
||||
feature_relay: Federovací mostík
|
||||
feature_timeline_preview: Náhľad časovej osi
|
||||
features: Vymoženosti
|
||||
hidden_service: Federácia so skrytými službami
|
||||
open_reports: otvorené hlásenia
|
||||
recent_users: Nedávny užívatelia
|
||||
recent_users: Nedávni užívatelia
|
||||
search: Celofrázové vyhľadávanie
|
||||
single_user_mode: Jednouživateľské rozhranie
|
||||
software: Softvér
|
||||
@ -264,10 +268,11 @@ sk:
|
||||
week_users_active: aktívni tento týždeň
|
||||
week_users_new: užívateľov počas tohto týždňa
|
||||
domain_blocks:
|
||||
add_new: Pridaj nové doménové blokovanie
|
||||
created_msg: Doména je v procese blokovania
|
||||
add_new: Blokuj novú doménu
|
||||
created_msg: Doména je v štádiu blokovania
|
||||
destroyed_msg: Blokovanie domény bolo zrušené
|
||||
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:
|
||||
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é.
|
||||
@ -284,7 +289,7 @@ sk:
|
||||
rejecting_media: odmietanie médiálnych súborov
|
||||
rejecting_reports: odmietané hlásenia
|
||||
severity:
|
||||
silence: utíšené
|
||||
silence: stíšený
|
||||
suspend: vylúčený
|
||||
show:
|
||||
affected_accounts:
|
||||
@ -295,12 +300,12 @@ sk:
|
||||
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
|
||||
title: Zruš blokovanie domény %{domain}
|
||||
undo: Vrátiť späť
|
||||
undo: Vráť späť
|
||||
undo: Odvolaj blokovanie domény
|
||||
email_domain_blocks:
|
||||
add_new: Pridaj nový
|
||||
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
|
||||
domain: Doména
|
||||
new:
|
||||
@ -328,15 +333,15 @@ sk:
|
||||
total_reported: Nahlásenia o nich
|
||||
total_storage: Mediálne prílohy
|
||||
invites:
|
||||
deactivate_all: Pozastaviť všetky
|
||||
deactivate_all: Pozastav všetky
|
||||
filter:
|
||||
all: Všetky
|
||||
available: Dostupné
|
||||
expired: Vypršalo
|
||||
title: Filtrovať
|
||||
title: Filtruj
|
||||
title: Pozvánky
|
||||
relays:
|
||||
add_new: Pridaj novú priechodnú oporu
|
||||
add_new: Pridaj nový federovací mostík
|
||||
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í."
|
||||
disable: Pozastav
|
||||
@ -344,7 +349,7 @@ sk:
|
||||
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.
|
||||
enabled: Povolené
|
||||
inbox_url: URL mostu
|
||||
inbox_url: URL adresa mostu
|
||||
pending: Čakám na povolenie od prechodného mostu
|
||||
save_and_enable: Uložiť a povoliť
|
||||
setup: Nastav prepojenie s mostom
|
||||
@ -359,7 +364,7 @@ sk:
|
||||
report: nahlás
|
||||
action_taken_by: Zákrok vykonal/a
|
||||
are_you_sure: Si si istý/á?
|
||||
assign_to_self: Priraď k sebe
|
||||
assign_to_self: Priraď sebe
|
||||
assigned: Priradený moderátor
|
||||
comment:
|
||||
none: Žiadne
|
||||
@ -379,7 +384,7 @@ sk:
|
||||
resolved: Vyriešené
|
||||
resolved_msg: Hlásenie úspešne vyriešené!
|
||||
status: Stav
|
||||
title: Reporty
|
||||
title: Hlásenia
|
||||
unassign: Odobrať
|
||||
unresolved: Nevyriešené
|
||||
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.
|
||||
title: Štandardní následovníci nových užívateľov
|
||||
contact_information:
|
||||
email: Pracovný e-mail
|
||||
email: Pracovný email
|
||||
username: Kontaktné užívateľské meno
|
||||
custom_css:
|
||||
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
|
||||
regenerate_token: Znovu vygenerovať prístupový token
|
||||
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!
|
||||
your_token: Váš prístupový token
|
||||
warning: Na tieto údaje dávaj ohromný pozor. Nikdy ich s nikým nezďieľaj!
|
||||
your_token: Tvoj prístupový token
|
||||
auth:
|
||||
apply_for_account: Vyžiadaj si pozvánku
|
||||
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_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?
|
||||
@ -521,16 +528,17 @@ sk:
|
||||
login: Prihlás sa
|
||||
logout: Odhlás sa
|
||||
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>.
|
||||
or_log_in_with: Alebo prihlásiť z
|
||||
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ás s
|
||||
providers:
|
||||
cas: CAS
|
||||
saml: SAML
|
||||
register: Zaregistruj sa
|
||||
resend_confirmation: Poslať potvrdzujúce pokyny znovu
|
||||
reset_password: Resetovať heslo
|
||||
resend_confirmation: Zašli potvrdzujúce pokyny znovu
|
||||
reset_password: Obnov heslo
|
||||
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:
|
||||
already_following: Tento účet už následuješ
|
||||
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
|
||||
imports:
|
||||
modes:
|
||||
merge: Spojiť dohromady
|
||||
merge: Spoj dohromady
|
||||
merge_long: Ponechaj existujúce záznamy a pridaj k nim nové
|
||||
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š.
|
||||
success: Tvoje dáta boli nahraté úspešne, a teraz budú spracované v danom čase
|
||||
types:
|
||||
@ -647,10 +655,10 @@ sk:
|
||||
domain_blocking: Zoznam blokovaných domén
|
||||
following: Zoznam sledovaných
|
||||
muting: Zoznam ignorovaných
|
||||
upload: Nahrať
|
||||
upload: Nahraj
|
||||
in_memoriam_html: V pamäti.
|
||||
invites:
|
||||
delete: Deaktivovať
|
||||
delete: Deaktivuj
|
||||
expired: Neplatné
|
||||
expires_in:
|
||||
'1800': 30 minút
|
||||
@ -661,13 +669,13 @@ sk:
|
||||
'86400': 1 deň
|
||||
expires_in_prompt: Nikdy
|
||||
generate: Vygeneruj
|
||||
invited_by: 'Bol/a si pozvan/á užívateľom:'
|
||||
invited_by: 'Bol/a si pozvaný/á užívateľom:'
|
||||
max_uses:
|
||||
few: "%{count} použitia"
|
||||
one: jedno použitie
|
||||
other: "%{count} použití"
|
||||
max_uses_prompt: Bez limitov
|
||||
prompt: Vygeneruj a zdieľaj linky s ostatnými aby mali umožnený prístup k tomuto serveru
|
||||
max_uses_prompt: Bez obmedzení
|
||||
prompt: Vygeneruj a zdieľaj linky s ostatnými, aby mali umožnený prístup k tomuto serveru
|
||||
table:
|
||||
expires_at: Vyprší
|
||||
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}
|
||||
mention: "%{name} ťa spomenul/a v:"
|
||||
new_followers_summary:
|
||||
few: Taktiež, získal/a si %{count} nových následovníkov 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!
|
||||
other: 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: Tiež si získal/a jedného nového následovateľa zatiaľ č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:
|
||||
few: "%{count} nové notifikácie od tvojej poslednej návštevy \U0001F418"
|
||||
one: "1 nová notifikácia od tvojej poslednej návštevy \U0001F418"
|
||||
other: "%{count} nových notifikácií od tvojej poslednej návštevy \U0001F418"
|
||||
one: "1 nové oboznámenie 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č…
|
||||
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"
|
||||
title: Nové obľúbené
|
||||
follow:
|
||||
@ -712,16 +720,16 @@ sk:
|
||||
action: Spravuj žiadosti o sledovanie
|
||||
body: "%{name} žiada povolenie ťa následovať"
|
||||
subject: "%{name} ťa žiadá o možnosť sledovania"
|
||||
title: Nová žiadosť o sledovanie
|
||||
title: Nová žiadosť o následovanie
|
||||
mention:
|
||||
action: Odpovedať
|
||||
body: "%{name} ťa spomenul/a v:"
|
||||
subject: Bol/a si spomenutý/á užívateľom %{name}
|
||||
title: Novo spomenutý/á
|
||||
reblog:
|
||||
body: 'Tvoj príspevok bol pozdvihnutý užívateľom %{name}:'
|
||||
subject: "%{name} pozdvihli tvoj príspevok"
|
||||
title: Novo pozdvyhnuté
|
||||
body: 'Tvoj príspevok bol vyzdvihnutý užívateľom %{name}:'
|
||||
subject: "%{name} vyzdvihli tvoj príspevok"
|
||||
title: Novo vyzdvyhnuté
|
||||
number:
|
||||
human:
|
||||
decimal_units:
|
||||
@ -820,17 +828,23 @@ sk:
|
||||
revoke_success: Sezóna úspešne zamietnutá
|
||||
title: Sezóny
|
||||
settings:
|
||||
account: Účet
|
||||
account_settings: Nastavenia účtu
|
||||
appearance: Vzhľad
|
||||
authorized_apps: Povolené aplikácie
|
||||
back: Späť na Mastodon
|
||||
delete: Vymazanie účtu
|
||||
development: Vývoj
|
||||
edit_profile: Uprav profil
|
||||
export: Exportovať dáta
|
||||
featured_tags: Popredne zvýraznené haštagy
|
||||
import: Importovať
|
||||
migrate: Presunutie účtu
|
||||
notifications: Oznámenia
|
||||
export: Exportuj dáta
|
||||
featured_tags: Zvýraznené haštagy
|
||||
import: Importuj
|
||||
import_and_export: Import a export
|
||||
migrate: Presuň účet
|
||||
notifications: Oboznámenia
|
||||
preferences: Voľby
|
||||
profile: Profil
|
||||
relationships: Následovaní a následovatelia
|
||||
two_factor_authentication: Dvoj-faktorové overenie
|
||||
statuses:
|
||||
attached:
|
||||
@ -846,9 +860,9 @@ sk:
|
||||
boosted_from_html: Povýšené od %{acct_link}
|
||||
content_warning: 'Varovanie o obsahu: %{warning}'
|
||||
disallowed_hashtags:
|
||||
few: 'obsahoval nepovolené hashtagy: %{tags}'
|
||||
one: 'obsahoval nepovolený hashtag: %{tags}'
|
||||
other: 'obsahoval nepovolené hashtagy: %{tags}'
|
||||
few: 'obsahoval nepovolené haštagy: %{tags}'
|
||||
one: 'obsahoval nepovolený haštag: %{tags}'
|
||||
other: 'obsahoval nepovolené haštagy: %{tags}'
|
||||
language_detection: Zisti automaticky
|
||||
open_in_web: Otvor v okne na webe
|
||||
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
|
||||
ownership: Nieje možné pripnúť hlášku od niekoho iného
|
||||
private: Neverejné príspevky nemôžu byť pripnuté
|
||||
reblog: Pozdvihnutie sa nedá pripnúť
|
||||
reblog: Vyzdvihnutie sa nedá pripnúť
|
||||
poll:
|
||||
total_votes:
|
||||
few: "%{count} hlas(y)ov"
|
||||
@ -874,14 +888,14 @@ sk:
|
||||
unlisted: Nezaradené
|
||||
unlisted_long: Všetci môžu vidieť, ale nieje zaradené do verejnej osi
|
||||
stream_entries:
|
||||
pinned: Pripnutý toot
|
||||
pinned: Pripnutý príspevok
|
||||
reblogged: vyzdvihnutý
|
||||
sensitive_content: Senzitívny obsah
|
||||
terms:
|
||||
body_html: |
|
||||
<h2>Podmienky súkromia</h2>
|
||||
|
||||
<h3 id="collect">Aké informácie zbierame?</h3>
|
||||
<h3 id="collect">Aké informácie sú zbierané?</h3>
|
||||
|
||||
<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>:
|
||||
@ -973,7 +987,7 @@ sk:
|
||||
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}
|
||||
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:
|
||||
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
|
||||
|
@ -0,0 +1,5 @@
|
||||
class AddBlurhashToMediaAttachments < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :media_attachments, :blurhash, :string
|
||||
end
|
||||
end
|
@ -10,7 +10,7 @@
|
||||
#
|
||||
# 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
|
||||
enable_extension "plpgsql"
|
||||
@ -362,6 +362,7 @@ ActiveRecord::Schema.define(version: 2019_04_09_054914) do
|
||||
t.bigint "account_id"
|
||||
t.text "description"
|
||||
t.bigint "scheduled_status_id"
|
||||
t.string "blurhash"
|
||||
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 ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true
|
||||
|
@ -43,7 +43,7 @@ services:
|
||||
- external_network
|
||||
- internal_network
|
||||
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:
|
||||
- "127.0.0.1:3000:3000"
|
||||
depends_on:
|
||||
@ -63,7 +63,7 @@ services:
|
||||
- external_network
|
||||
- internal_network
|
||||
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:
|
||||
- "127.0.0.1:4000:4000"
|
||||
depends_on:
|
||||
|
@ -9,6 +9,7 @@ require_relative 'mastodon/search_cli'
|
||||
require_relative 'mastodon/settings_cli'
|
||||
require_relative 'mastodon/statuses_cli'
|
||||
require_relative 'mastodon/domains_cli'
|
||||
require_relative 'mastodon/cache_cli'
|
||||
require_relative 'mastodon/version'
|
||||
|
||||
module Mastodon
|
||||
@ -41,6 +42,9 @@ module Mastodon
|
||||
desc 'domains SUBCOMMAND ...ARGS', 'Manage account domains'
|
||||
subcommand 'domains', Mastodon::DomainsCLI
|
||||
|
||||
desc 'cache SUBCOMMAND ...ARGS', 'Manage cache'
|
||||
subcommand 'cache', Mastodon::CacheCLI
|
||||
|
||||
option :dry_run, type: :boolean
|
||||
desc 'self-destruct', 'Erase the server from the federation'
|
||||
long_desc <<~LONG_DESC
|
||||
|
@ -73,7 +73,7 @@ module Mastodon
|
||||
def create(username)
|
||||
account = Account.new(username: username)
|
||||
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]
|
||||
account = Account.find_local(username) || Account.new(username: username)
|
||||
@ -115,6 +115,7 @@ module Mastodon
|
||||
option :enable, type: :boolean
|
||||
option :disable, type: :boolean
|
||||
option :disable_2fa, type: :boolean
|
||||
option :approve, type: :boolean
|
||||
desc 'modify USERNAME', 'Modify a user'
|
||||
long_desc <<-LONG_DESC
|
||||
Modify a user account.
|
||||
@ -128,6 +129,9 @@ module Mastodon
|
||||
With the --disable option, lock the user out of their account. The
|
||||
--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
|
||||
requirement for the user can be removed.
|
||||
LONG_DESC
|
||||
@ -147,6 +151,7 @@ module Mastodon
|
||||
user.email = options[:email] if options[:email]
|
||||
user.disabled = false if options[:enable]
|
||||
user.disabled = true if options[:disable]
|
||||
user.approved = true if options[:approve]
|
||||
user.otp_required_for_login = false if options[:disable_2fa]
|
||||
user.confirm if options[:confirm]
|
||||
|
||||
|
19
lib/mastodon/cache_cli.rb
Normal file
19
lib/mastodon/cache_cli.rb
Normal 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
|
@ -13,7 +13,7 @@ module Mastodon
|
||||
end
|
||||
|
||||
def patch
|
||||
0
|
||||
2
|
||||
end
|
||||
|
||||
def pre
|
||||
@ -33,16 +33,16 @@ module Mastodon
|
||||
end
|
||||
|
||||
def repository
|
||||
'tootsuite/mastodon'
|
||||
ENV.fetch('GITHUB_REPOSITORY') { 'tootsuite/mastodon' }
|
||||
end
|
||||
|
||||
def source_base_url
|
||||
"https://github.com/#{repository}"
|
||||
ENV.fetch('SOURCE_BASE_URL') { "https://github.com/#{repository}" }
|
||||
end
|
||||
|
||||
# specify git tag or commit hash here
|
||||
def source_tag
|
||||
nil
|
||||
ENV.fetch('SOURCE_TAG') { nil }
|
||||
end
|
||||
|
||||
def source_url
|
||||
|
16
lib/paperclip/blurhash_transcoder.rb
Normal file
16
lib/paperclip/blurhash_transcoder.rb
Normal 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
|
@ -78,6 +78,7 @@
|
||||
"babel-plugin-react-intl": "^3.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"blurhash": "^1.0.0",
|
||||
"classnames": "^2.2.5",
|
||||
"compression-webpack-plugin": "^2.0.0",
|
||||
"cross-env": "^5.1.4",
|
||||
|
@ -2,3 +2,4 @@
|
||||
|
||||
User-agent: *
|
||||
Disallow: /media_proxy/
|
||||
Disallow: /interact/
|
||||
|
@ -37,7 +37,7 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
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(response).to render_template :new
|
||||
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
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
|
@ -107,6 +107,89 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
|
||||
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
|
||||
Fabricate(:user, account: Fabricate(:account, username: 'test'))
|
||||
subject
|
||||
|
@ -36,4 +36,35 @@ RSpec.describe DomainBlock, type: :model do
|
||||
expect(DomainBlock.blocked?('domain')).to eq false
|
||||
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
|
||||
|
@ -8,6 +8,7 @@ RSpec.describe BlacklistedEmailValidator, type: :validator do
|
||||
let(:errors) { double(add: nil) }
|
||||
|
||||
before do
|
||||
allow(user).to receive(:invited?) { false }
|
||||
allow_any_instance_of(described_class).to receive(:blocked_email?) { blocked_email }
|
||||
described_class.new.validate(user)
|
||||
end
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user