forked from cybrespace/mastodon
Compare commits
50 Commits
cybrespace
...
fix-backup
Author | SHA1 | Date |
---|---|---|
khr | 3821241c46 | |
dependabot-preview[bot] | 685b0db882 | |
dependabot-preview[bot] | 741a85c064 | |
dependabot-preview[bot] | 0745fa8449 | |
dependabot-preview[bot] | 291106e11c | |
dependabot-preview[bot] | 52c9044ea9 | |
dependabot-preview[bot] | 008d15d2cd | |
dependabot-preview[bot] | e551274897 | |
dependabot-preview[bot] | 0caa707726 | |
dependabot-preview[bot] | 254ddfc08a | |
dependabot-preview[bot] | 4ecfd4308d | |
dependabot-preview[bot] | da67b1fa37 | |
dependabot-preview[bot] | 3565fc1a00 | |
dependabot-preview[bot] | 91b02afe4a | |
dependabot-preview[bot] | 7be994e0a9 | |
Yamagishi Kazutoshi | a4301b5202 | |
Takeshi Umeda | 5b46467474 | |
Yamagishi Kazutoshi | 7512f3a3e0 | |
Eugen Rochko | 4988ebba4e | |
Nima Boscarino | 9b36f62df6 | |
Eugen Rochko | 91945aa78a | |
Faye Duxovni | 48f75b86ae | |
Takeshi Umeda | a6269b2f83 | |
Nima Boscarino | a9530e29a2 | |
ThibG | 15c192ce40 | |
ThibG | bcf694dce7 | |
BenisonSebastian | d3145ced1f | |
umonaca | f4be89e24d | |
Hugo Gameiro | 488dd0ff7a | |
ThibG | 3a929dbedd | |
Hinaloe | 547a5bac9d | |
kodai | 5966d1b5a3 | |
ThibG | aa884e0484 | |
nightpool | 9762fe382c | |
Soft. Dev | 3ebd903535 | |
puckipedia | d2919f7e94 | |
ThibG | bd684e25d9 | |
BSKY | fccf83e1f2 | |
dependabot-preview[bot] | 237293fd8c | |
dependabot-preview[bot] | 6bee7b820d | |
dependabot-preview[bot] | e9e6ca9041 | |
dependabot-preview[bot] | f7f6f0726c | |
dependabot-preview[bot] | 1a7cf80b7f | |
dependabot-preview[bot] | 183fc9d3cb | |
dependabot-preview[bot] | bc9c116e60 | |
dependabot-preview[bot] | 912496de07 | |
dependabot-preview[bot] | 7840844919 | |
dependabot-preview[bot] | 13b6d5d01b | |
dependabot-preview[bot] | 1ecd71c53e | |
dependabot-preview[bot] | ae945cb1f4 |
|
@ -1,2 +1,3 @@
|
|||
VAGRANT=true
|
||||
LOCAL_DOMAIN=mastodon.local
|
||||
BIND=0.0.0.0
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
daysUntilStale: 120
|
||||
daysUntilClose: 7
|
||||
exemptLabels:
|
||||
- security
|
||||
staleLabel: wontfix
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
only: pulls
|
16
Gemfile
16
Gemfile
|
@ -3,7 +3,7 @@
|
|||
source 'https://rubygems.org'
|
||||
ruby '>= 2.4.0', '< 2.7.0'
|
||||
|
||||
gem 'pkg-config', '~> 1.3'
|
||||
gem 'pkg-config', '~> 1.4'
|
||||
|
||||
gem 'puma', '~> 4.2'
|
||||
gem 'rails', '~> 5.2.3'
|
||||
|
@ -15,7 +15,7 @@ gem 'makara', '~> 0.4'
|
|||
gem 'pghero', '~> 2.3'
|
||||
gem 'dotenv-rails', '~> 2.7'
|
||||
|
||||
gem 'aws-sdk-s3', '~> 1.48', require: false
|
||||
gem 'aws-sdk-s3', '~> 1.52', require: false
|
||||
gem 'fog-core', '<= 2.1.0'
|
||||
gem 'fog-openstack', '~> 0.3', require: false
|
||||
gem 'paperclip', '~> 6.0'
|
||||
|
@ -85,7 +85,7 @@ gem 'sidekiq-scheduler', '~> 3.0'
|
|||
gem 'sidekiq-unique-jobs', '~> 6.0'
|
||||
gem 'sidekiq-bulk', '~>0.2.0'
|
||||
gem 'simple-navigation', '~> 4.1'
|
||||
gem 'simple_form', '~> 4.1'
|
||||
gem 'simple_form', '~> 5.0'
|
||||
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
|
||||
gem 'stoplight', '~> 2.1.3'
|
||||
gem 'strong_migrations', '~> 0.4'
|
||||
|
@ -106,7 +106,7 @@ group :development, :test do
|
|||
gem 'i18n-tasks', '~> 0.9', require: false
|
||||
gem 'pry-byebug', '~> 3.7'
|
||||
gem 'pry-rails', '~> 0.3'
|
||||
gem 'rspec-rails', '~> 3.8'
|
||||
gem 'rspec-rails', '~> 3.9'
|
||||
end
|
||||
|
||||
group :production, :test do
|
||||
|
@ -116,7 +116,7 @@ end
|
|||
group :test do
|
||||
gem 'capybara', '~> 3.29'
|
||||
gem 'climate_control', '~> 0.2'
|
||||
gem 'faker', '~> 2.5'
|
||||
gem 'faker', '~> 2.6'
|
||||
gem 'microformats', '~> 4.1'
|
||||
gem 'rails-controller-testing', '~> 1.0'
|
||||
gem 'rspec-sidekiq', '~> 3.0'
|
||||
|
@ -126,15 +126,15 @@ group :test do
|
|||
end
|
||||
|
||||
group :development do
|
||||
gem 'active_record_query_trace', '~> 1.6'
|
||||
gem 'annotate', '~> 2.7'
|
||||
gem 'active_record_query_trace', '~> 1.7'
|
||||
gem 'annotate', '~> 3.0'
|
||||
gem 'better_errors', '~> 2.5'
|
||||
gem 'binding_of_caller', '~> 0.7'
|
||||
gem 'bullet', '~> 6.0'
|
||||
gem 'letter_opener', '~> 1.7'
|
||||
gem 'letter_opener_web', '~> 1.3'
|
||||
gem 'memory_profiler'
|
||||
gem 'rubocop', '~> 0.74', require: false
|
||||
gem 'rubocop', '~> 0.75', require: false
|
||||
gem 'rubocop-rails', '~> 2.3', require: false
|
||||
gem 'brakeman', '~> 4.6', require: false
|
||||
gem 'bundler-audit', '~> 0.6', require: false
|
||||
|
|
98
Gemfile.lock
98
Gemfile.lock
|
@ -72,7 +72,7 @@ GEM
|
|||
activemodel (>= 4.1, < 6.1)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
active_record_query_trace (1.6.2)
|
||||
active_record_query_trace (1.7)
|
||||
activejob (5.2.3)
|
||||
activesupport (= 5.2.3)
|
||||
globalid (>= 0.3.6)
|
||||
|
@ -95,7 +95,7 @@ GEM
|
|||
public_suffix (>= 2.0.2, < 5.0)
|
||||
airbrussh (1.3.4)
|
||||
sshkit (>= 1.6.1, != 1.7.0)
|
||||
annotate (2.7.5)
|
||||
annotate (3.0.2)
|
||||
activerecord (>= 3.2, < 7.0)
|
||||
rake (>= 10.4, < 13.0)
|
||||
arel (9.0.0)
|
||||
|
@ -105,17 +105,17 @@ GEM
|
|||
av (0.9.0)
|
||||
cocaine (~> 0.5.3)
|
||||
aws-eventstream (1.0.3)
|
||||
aws-partitions (1.207.0)
|
||||
aws-sdk-core (3.65.1)
|
||||
aws-partitions (1.230.0)
|
||||
aws-sdk-core (3.72.0)
|
||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||
aws-partitions (~> 1.0)
|
||||
aws-partitions (~> 1, >= 1.228.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.24.0)
|
||||
aws-sdk-core (~> 3, >= 3.61.1)
|
||||
aws-sdk-kms (1.25.0)
|
||||
aws-sdk-core (~> 3, >= 3.71.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.48.0)
|
||||
aws-sdk-core (~> 3, >= 3.61.1)
|
||||
aws-sdk-s3 (1.52.0)
|
||||
aws-sdk-core (~> 3, >= 3.71.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.1.0)
|
||||
|
@ -184,17 +184,17 @@ GEM
|
|||
connection_pool (2.2.2)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.4)
|
||||
crass (1.0.5)
|
||||
css_parser (1.7.0)
|
||||
addressable
|
||||
debug_inspector (0.0.3)
|
||||
derailed_benchmarks (1.4.0)
|
||||
derailed_benchmarks (1.4.1)
|
||||
benchmark-ips (~> 2)
|
||||
get_process_mem (~> 0)
|
||||
heapy (~> 0)
|
||||
memory_profiler (~> 0)
|
||||
rack (>= 1)
|
||||
rake (> 10, < 13)
|
||||
rake (> 10, < 14)
|
||||
ruby-statistics (>= 2.1)
|
||||
thor (~> 0.19)
|
||||
devise (4.7.1)
|
||||
|
@ -235,13 +235,13 @@ GEM
|
|||
multi_json
|
||||
encryptor (3.0.0)
|
||||
equatable (0.6.1)
|
||||
erubi (1.8.0)
|
||||
erubi (1.9.0)
|
||||
et-orbi (1.1.6)
|
||||
tzinfo
|
||||
excon (0.62.0)
|
||||
fabrication (2.20.2)
|
||||
faker (2.5.0)
|
||||
i18n (~> 1.6.0)
|
||||
faker (2.6.0)
|
||||
i18n (>= 1.6, < 1.8)
|
||||
faraday (0.15.4)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
fast_blank (1.0.0)
|
||||
|
@ -305,7 +305,7 @@ GEM
|
|||
httplog (1.3.2)
|
||||
rack (>= 1.0)
|
||||
rainbow (>= 2.0.0)
|
||||
i18n (1.6.0)
|
||||
i18n (1.7.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-tasks (0.9.29)
|
||||
activesupport (>= 4.0.2)
|
||||
|
@ -356,7 +356,7 @@ GEM
|
|||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.2.3)
|
||||
loofah (2.3.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
|
@ -378,7 +378,7 @@ GEM
|
|||
mimemagic (0.3.3)
|
||||
mini_mime (1.0.2)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.12.0)
|
||||
minitest (5.12.2)
|
||||
msgpack (1.3.1)
|
||||
multi_json (1.13.1)
|
||||
multipart-post (2.1.1)
|
||||
|
@ -397,7 +397,7 @@ GEM
|
|||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sidekiq (>= 3.5)
|
||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||
oj (3.9.1)
|
||||
oj (3.9.2)
|
||||
omniauth (1.9.0)
|
||||
hashie (>= 3.4.6, < 3.7.0)
|
||||
rack (>= 1.6.2, < 3)
|
||||
|
@ -426,7 +426,7 @@ GEM
|
|||
parallel (1.17.0)
|
||||
parallel_tests (2.29.2)
|
||||
parallel
|
||||
parser (2.6.4.0)
|
||||
parser (2.6.5.0)
|
||||
ast (~> 2.4.0)
|
||||
parslet (1.8.2)
|
||||
pastel (0.7.3)
|
||||
|
@ -435,7 +435,7 @@ GEM
|
|||
pg (1.1.4)
|
||||
pghero (2.3.0)
|
||||
activerecord (>= 5)
|
||||
pkg-config (1.3.9)
|
||||
pkg-config (1.4.0)
|
||||
premailer (1.11.1)
|
||||
addressable
|
||||
css_parser (>= 1.6.0)
|
||||
|
@ -462,7 +462,7 @@ GEM
|
|||
rack-attack (6.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-cors (1.0.3)
|
||||
rack-protection (2.0.5)
|
||||
rack-protection (2.0.7)
|
||||
rack
|
||||
rack-proxy (0.6.5)
|
||||
rack
|
||||
|
@ -488,8 +488,8 @@ GEM
|
|||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.2.0)
|
||||
loofah (~> 2.2, >= 2.2.2)
|
||||
rails-html-sanitizer (1.3.0)
|
||||
loofah (~> 2.3)
|
||||
rails-i18n (5.1.3)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 5.0, < 6)
|
||||
|
@ -537,27 +537,27 @@ GEM
|
|||
rpam2 (4.0.2)
|
||||
rqrcode (0.10.1)
|
||||
chunky_png (~> 1.0)
|
||||
rspec-core (3.8.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-expectations (3.8.2)
|
||||
rspec-core (3.9.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-expectations (3.9.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-mocks (3.8.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-mocks (3.9.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-rails (3.8.2)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-rails (3.9.0)
|
||||
actionpack (>= 3.0)
|
||||
activesupport (>= 3.0)
|
||||
railties (>= 3.0)
|
||||
rspec-core (~> 3.8.0)
|
||||
rspec-expectations (~> 3.8.0)
|
||||
rspec-mocks (~> 3.8.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-core (~> 3.9.0)
|
||||
rspec-expectations (~> 3.9.0)
|
||||
rspec-mocks (~> 3.9.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-sidekiq (3.0.3)
|
||||
rspec-core (~> 3.0, >= 3.0.0)
|
||||
sidekiq (>= 2.4.0)
|
||||
rspec-support (3.8.0)
|
||||
rubocop (0.74.0)
|
||||
rspec-support (3.9.0)
|
||||
rubocop (0.75.1)
|
||||
jaro_winkler (~> 1.5.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.6)
|
||||
|
@ -590,13 +590,13 @@ GEM
|
|||
rufus-scheduler (~> 3.2)
|
||||
sidekiq (>= 3)
|
||||
tilt (>= 1.4.0)
|
||||
sidekiq-unique-jobs (6.0.13)
|
||||
sidekiq-unique-jobs (6.0.15)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||
sidekiq (>= 4.0, < 7.0)
|
||||
thor (~> 0)
|
||||
simple-navigation (4.1.0)
|
||||
activesupport (>= 2.3.2)
|
||||
simple_form (4.1.0)
|
||||
simple_form (5.0.1)
|
||||
actionpack (>= 5.0)
|
||||
activemodel (>= 5.0)
|
||||
simplecov (0.17.1)
|
||||
|
@ -614,12 +614,12 @@ GEM
|
|||
sshkit (1.20.0)
|
||||
net-scp (>= 1.1.2)
|
||||
net-ssh (>= 2.8.0)
|
||||
stackprof (0.2.12)
|
||||
stackprof (0.2.13)
|
||||
statsd-ruby (1.4.0)
|
||||
stoplight (2.1.3)
|
||||
streamio-ffmpeg (3.0.2)
|
||||
multi_json (~> 1.8)
|
||||
strong_migrations (0.4.1)
|
||||
strong_migrations (0.4.2)
|
||||
activerecord (>= 5)
|
||||
temple (0.8.1)
|
||||
terminal-table (1.8.0)
|
||||
|
@ -678,10 +678,10 @@ PLATFORMS
|
|||
|
||||
DEPENDENCIES
|
||||
active_model_serializers (~> 0.10)
|
||||
active_record_query_trace (~> 1.6)
|
||||
active_record_query_trace (~> 1.7)
|
||||
addressable (~> 2.7)
|
||||
annotate (~> 2.7)
|
||||
aws-sdk-s3 (~> 1.48)
|
||||
annotate (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.52)
|
||||
better_errors (~> 2.5)
|
||||
binding_of_caller (~> 0.7)
|
||||
blurhash (~> 0.1)
|
||||
|
@ -709,7 +709,7 @@ DEPENDENCIES
|
|||
doorkeeper (~> 5.2)
|
||||
dotenv-rails (~> 2.7)
|
||||
fabrication (~> 2.20)
|
||||
faker (~> 2.5)
|
||||
faker (~> 2.6)
|
||||
fast_blank (~> 1.0)
|
||||
fastimage
|
||||
fog-core (<= 2.1.0)
|
||||
|
@ -756,7 +756,7 @@ DEPENDENCIES
|
|||
parslet
|
||||
pg (~> 1.1)
|
||||
pghero (~> 2.3)
|
||||
pkg-config (~> 1.3)
|
||||
pkg-config (~> 1.4)
|
||||
posix-spawn!
|
||||
premailer-rails
|
||||
private_address_check (~> 0.5)
|
||||
|
@ -775,9 +775,9 @@ DEPENDENCIES
|
|||
redis-namespace (~> 1.5)
|
||||
redis-rails (~> 5.0)
|
||||
rqrcode (~> 0.10)
|
||||
rspec-rails (~> 3.8)
|
||||
rspec-rails (~> 3.9)
|
||||
rspec-sidekiq (~> 3.0)
|
||||
rubocop (~> 0.74)
|
||||
rubocop (~> 0.75)
|
||||
rubocop-rails (~> 2.3)
|
||||
ruby-progressbar (~> 1.10)
|
||||
sanitize (~> 5.1)
|
||||
|
@ -786,7 +786,7 @@ DEPENDENCIES
|
|||
sidekiq-scheduler (~> 3.0)
|
||||
sidekiq-unique-jobs (~> 6.0)
|
||||
simple-navigation (~> 4.1)
|
||||
simple_form (~> 4.1)
|
||||
simple_form (~> 5.0)
|
||||
simplecov (~> 0.17)
|
||||
sprockets-rails (~> 3.2)
|
||||
stackprof
|
||||
|
|
|
@ -78,7 +78,7 @@ A **Vagrant** configuration is included for development purposes.
|
|||
|
||||
## Contributing
|
||||
|
||||
Mastodon is **free, open source software** licensed under **AGPLv3**.
|
||||
Mastodon is **free, open-source software** licensed under **AGPLv3**.
|
||||
|
||||
You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository, or submit translations using Weblate. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md). If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon).
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
|||
|
||||
before_action :skip_unknown_actor_delete
|
||||
before_action :require_signature!
|
||||
skip_before_action :authenticate_user!
|
||||
|
||||
def create
|
||||
upgrade_account
|
||||
|
|
|
@ -57,6 +57,7 @@ class Settings::PreferencesController < Settings::BaseController
|
|||
:setting_use_blurhash,
|
||||
:setting_use_pending_items,
|
||||
:setting_trends,
|
||||
:setting_crop_images,
|
||||
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag),
|
||||
interactions: %i(must_be_follower must_be_following must_be_following_dm)
|
||||
)
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AccountsHelper
|
||||
def display_name(account, **options)
|
||||
if options[:custom_emojify]
|
||||
Formatter.instance.format_display_name(account, options)
|
||||
else
|
||||
account.display_name.presence || account.username
|
||||
end
|
||||
end
|
||||
|
||||
def acct(account)
|
||||
if account.local?
|
||||
"@#{account.acct}@#{Rails.configuration.x.local_domain}"
|
||||
else
|
||||
"@#{account.acct}"
|
||||
end
|
||||
end
|
||||
|
||||
def account_action_button(account)
|
||||
if user_signed_in?
|
||||
if account.id == current_user.account_id
|
||||
link_to settings_profile_url, class: 'button logo-button' do
|
||||
safe_join([svg_logo, t('settings.edit_profile')])
|
||||
end
|
||||
elsif current_account.following?(account) || current_account.requested?(account)
|
||||
link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do
|
||||
safe_join([svg_logo, t('accounts.unfollow')])
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do
|
||||
safe_join([svg_logo, t('accounts.follow')])
|
||||
end
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do
|
||||
safe_join([svg_logo, t('accounts.follow')])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def minimal_account_action_button(account)
|
||||
if user_signed_in?
|
||||
return if account.id == current_user.account_id
|
||||
|
||||
if current_account.following?(account) || current_account.requested?(account)
|
||||
link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do
|
||||
fa_icon('user-times fw')
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do
|
||||
fa_icon('user-plus fw')
|
||||
end
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do
|
||||
fa_icon('user-plus fw')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def account_badge(account, all: false)
|
||||
if account.bot?
|
||||
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
|
||||
elsif (Setting.show_staff_badge && account.user_staff?) || all
|
||||
content_tag(:div, class: 'roles') do
|
||||
if all && !account.user_staff?
|
||||
content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role')
|
||||
elsif account.user_admin?
|
||||
content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin')
|
||||
elsif account.user_moderator?
|
||||
content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def account_description(account)
|
||||
prepend_str = [
|
||||
[
|
||||
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.posts', count: account.statuses_count),
|
||||
].join(' '),
|
||||
|
||||
[
|
||||
number_to_human(account.following_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.following', count: account.following_count),
|
||||
].join(' '),
|
||||
|
||||
[
|
||||
number_to_human(account.followers_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.followers', count: account.followers_count),
|
||||
].join(' '),
|
||||
].join(', ')
|
||||
|
||||
[prepend_str, account.note].join(' · ')
|
||||
end
|
||||
|
||||
def svg_logo
|
||||
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
|
||||
end
|
||||
|
||||
def svg_logo_full
|
||||
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678')
|
||||
end
|
||||
end
|
|
@ -4,80 +4,6 @@ module StatusesHelper
|
|||
EMBEDDED_CONTROLLER = 'statuses'
|
||||
EMBEDDED_ACTION = 'embed'
|
||||
|
||||
def display_name(account, **options)
|
||||
if options[:custom_emojify]
|
||||
Formatter.instance.format_display_name(account, options)
|
||||
else
|
||||
account.display_name.presence || account.username
|
||||
end
|
||||
end
|
||||
|
||||
def account_action_button(account)
|
||||
if user_signed_in?
|
||||
if account.id == current_user.account_id
|
||||
link_to settings_profile_url, class: 'button logo-button' do
|
||||
safe_join([svg_logo, t('settings.edit_profile')])
|
||||
end
|
||||
elsif current_account.following?(account) || current_account.requested?(account)
|
||||
link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do
|
||||
safe_join([svg_logo, t('accounts.unfollow')])
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do
|
||||
safe_join([svg_logo, t('accounts.follow')])
|
||||
end
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do
|
||||
safe_join([svg_logo, t('accounts.follow')])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def minimal_account_action_button(account)
|
||||
if user_signed_in?
|
||||
return if account.id == current_user.account_id
|
||||
|
||||
if current_account.following?(account) || current_account.requested?(account)
|
||||
link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do
|
||||
fa_icon('user-times fw')
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do
|
||||
fa_icon('user-plus fw')
|
||||
end
|
||||
end
|
||||
elsif !(account.memorial? || account.moved?)
|
||||
link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do
|
||||
fa_icon('user-plus fw')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def svg_logo
|
||||
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
|
||||
end
|
||||
|
||||
def svg_logo_full
|
||||
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678')
|
||||
end
|
||||
|
||||
def account_badge(account, all: false)
|
||||
if account.bot?
|
||||
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
|
||||
elsif (Setting.show_staff_badge && account.user_staff?) || all
|
||||
content_tag(:div, class: 'roles') do
|
||||
if all && !account.user_staff?
|
||||
content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role')
|
||||
elsif account.user_admin?
|
||||
content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin')
|
||||
elsif account.user_moderator?
|
||||
content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def link_to_more(url)
|
||||
link_to t('statuses.show_more'), url, class: 'load-more load-gap'
|
||||
end
|
||||
|
@ -88,27 +14,6 @@ module StatusesHelper
|
|||
end
|
||||
end
|
||||
|
||||
def account_description(account)
|
||||
prepend_str = [
|
||||
[
|
||||
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.posts', count: account.statuses_count),
|
||||
].join(' '),
|
||||
|
||||
[
|
||||
number_to_human(account.following_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.following', count: account.following_count),
|
||||
].join(' '),
|
||||
|
||||
[
|
||||
number_to_human(account.followers_count, strip_insignificant_zeros: true),
|
||||
I18n.t('accounts.followers', count: account.followers_count),
|
||||
].join(' '),
|
||||
].join(', ')
|
||||
|
||||
[prepend_str, account.note].join(' · ')
|
||||
end
|
||||
|
||||
def media_summary(status)
|
||||
attachments = { image: 0, video: 0 }
|
||||
|
||||
|
@ -154,14 +59,6 @@ module StatusesHelper
|
|||
embedded_view? ? '_blank' : nil
|
||||
end
|
||||
|
||||
def acct(account)
|
||||
if account.local?
|
||||
"@#{account.acct}@#{Rails.configuration.x.local_domain}"
|
||||
else
|
||||
"@#{account.acct}"
|
||||
end
|
||||
end
|
||||
|
||||
def style_classes(status, is_predecessor, is_successor, include_threads)
|
||||
classes = ['entry']
|
||||
classes << 'entry-predecessor' if is_predecessor
|
||||
|
|
|
@ -25,7 +25,7 @@ export default class AttachmentList extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<li key={attachment.get('id')}>
|
||||
<a href={displayUrl} target='_blank' rel='noopener'><Icon id='link' /> {filename(displayUrl)}</a>
|
||||
<a href={displayUrl} target='_blank' rel='noopener noreferrer'><Icon id='link' /> {filename(displayUrl)}</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
@ -46,7 +46,7 @@ export default class AttachmentList extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<li key={attachment.get('id')}>
|
||||
<a href={displayUrl} target='_blank' rel='noopener'>{filename(displayUrl)}</a>
|
||||
<a href={displayUrl} target='_blank' rel='noopener noreferrer'>{filename(displayUrl)}</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -143,7 +143,7 @@ class DropdownMenu extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<li className='dropdown-menu__item' key={`${text}-${i}`}>
|
||||
<a href={href} target={target} data-method={method} rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
|
||||
<a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
|
||||
{text}
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -58,7 +58,7 @@ export default class ErrorBoundary extends React.PureComponent {
|
|||
<div>
|
||||
<p className='error-boundary__error'><FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' /></p>
|
||||
<p><FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' /></p>
|
||||
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied && 'copied'}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
|
||||
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied && 'copied'}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import React from 'react';
|
||||
import Motion from '../features/ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
@ -37,6 +35,21 @@ export default class IconButton extends React.PureComponent {
|
|||
tabIndex: '0',
|
||||
};
|
||||
|
||||
state = {
|
||||
activate: false,
|
||||
deactivate: false,
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (!nextProps.animate) return;
|
||||
|
||||
if (this.props.active && !nextProps.active) {
|
||||
this.setState({ activate: false, deactivate: true });
|
||||
} else if (!this.props.active && nextProps.active) {
|
||||
this.setState({ activate: true, deactivate: false });
|
||||
}
|
||||
}
|
||||
|
||||
handleClick = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -75,7 +88,6 @@ export default class IconButton extends React.PureComponent {
|
|||
|
||||
const {
|
||||
active,
|
||||
animate,
|
||||
className,
|
||||
disabled,
|
||||
expanded,
|
||||
|
@ -87,57 +99,37 @@ export default class IconButton extends React.PureComponent {
|
|||
title,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
activate,
|
||||
deactivate,
|
||||
} = this.state;
|
||||
|
||||
const classes = classNames(className, 'icon-button', {
|
||||
active,
|
||||
disabled,
|
||||
inverted,
|
||||
activate,
|
||||
deactivate,
|
||||
overlayed: overlay,
|
||||
});
|
||||
|
||||
if (!animate) {
|
||||
// Perf optimization: avoid unnecessary <Motion> components unless
|
||||
// we actually need to animate.
|
||||
return (
|
||||
<button
|
||||
aria-label={title}
|
||||
aria-pressed={pressed}
|
||||
aria-expanded={expanded}
|
||||
title={title}
|
||||
className={classes}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
style={style}
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon id={icon} fixedWidth aria-hidden='true' />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Motion defaultStyle={{ rotate: active ? -360 : 0 }} style={{ rotate: animate ? spring(active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
|
||||
{({ rotate }) => (
|
||||
<button
|
||||
aria-label={title}
|
||||
aria-pressed={pressed}
|
||||
aria-expanded={expanded}
|
||||
title={title}
|
||||
className={classes}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
style={style}
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon id={icon} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />
|
||||
</button>
|
||||
)}
|
||||
</Motion>
|
||||
<button
|
||||
aria-label={title}
|
||||
aria-pressed={pressed}
|
||||
aria-expanded={expanded}
|
||||
title={title}
|
||||
className={classes}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
style={style}
|
||||
tabIndex={tabIndex}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon id={icon} fixedWidth aria-hidden='true' />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import IconButton from './icon_button';
|
|||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { isIOS } from '../is_mobile';
|
||||
import classNames from 'classnames';
|
||||
import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
|
||||
import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_state';
|
||||
import { decode } from 'blurhash';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -159,7 +159,7 @@ class Item extends React.PureComponent {
|
|||
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') || attachment.get('url')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}>
|
||||
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} target='_blank' rel='noopener noreferrer'>
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
|
||||
</a>
|
||||
</div>
|
||||
|
@ -187,6 +187,7 @@ class Item extends React.PureComponent {
|
|||
href={attachment.get('remote_url') || originalUrl}
|
||||
onClick={this.handleClick}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<img
|
||||
src={previewUrl}
|
||||
|
@ -280,7 +281,7 @@ class MediaGallery extends React.PureComponent {
|
|||
}
|
||||
|
||||
handleRef = (node) => {
|
||||
if (node /*&& this.isStandaloneEligible()*/) {
|
||||
if (node) {
|
||||
// offsetWidth triggers a layout, so only calculate when we need to
|
||||
if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth);
|
||||
|
||||
|
@ -290,13 +291,13 @@ class MediaGallery extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
isStandaloneEligible() {
|
||||
const { media, standalone } = this.props;
|
||||
return standalone && media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
|
||||
isFullSizeEligible() {
|
||||
const { media } = this.props;
|
||||
return media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { media, intl, sensitive, height, defaultWidth } = this.props;
|
||||
const { media, intl, sensitive, height, defaultWidth, standalone } = this.props;
|
||||
const { visible } = this.state;
|
||||
|
||||
const width = this.state.width || defaultWidth;
|
||||
|
@ -305,7 +306,7 @@ class MediaGallery extends React.PureComponent {
|
|||
|
||||
const style = {};
|
||||
|
||||
if (this.isStandaloneEligible()) {
|
||||
if (this.isFullSizeEligible() && (standalone || !cropImages)) {
|
||||
if (width) {
|
||||
style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']);
|
||||
}
|
||||
|
@ -318,7 +319,7 @@ class MediaGallery extends React.PureComponent {
|
|||
const size = media.take(4).size;
|
||||
const uncached = media.every(attachment => attachment.get('type') === 'unknown');
|
||||
|
||||
if (this.isStandaloneEligible()) {
|
||||
if (standalone && this.isFullSizeEligible()) {
|
||||
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />;
|
||||
} else {
|
||||
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} visible={visible || uncached} />);
|
||||
|
|
|
@ -39,7 +39,8 @@ class Poll extends ImmutablePureComponent {
|
|||
|
||||
static getDerivedStateFromProps (props, state) {
|
||||
const { poll, intl } = props;
|
||||
const expired = poll.get('expired') || (new Date(poll.get('expires_at'))).getTime() < intl.now();
|
||||
const expires_at = poll.get('expires_at');
|
||||
const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < intl.now();
|
||||
return (expired === state.expired) ? null : { expired };
|
||||
}
|
||||
|
||||
|
|
|
@ -437,9 +437,9 @@ class Status extends ImmutablePureComponent {
|
|||
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
|
||||
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
|
||||
<div className='status__info'>
|
||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||
|
||||
<a onClick={this.handleAccountClick} target='_blank' data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name'>
|
||||
<a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
|
||||
<div className='status__avatar'>
|
||||
{statusAvatar}
|
||||
</div>
|
||||
|
|
|
@ -59,7 +59,7 @@ export default class StatusContent extends React.PureComponent {
|
|||
}
|
||||
|
||||
link.setAttribute('target', '_blank');
|
||||
link.setAttribute('rel', 'noopener');
|
||||
link.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
|
||||
if (
|
||||
|
|
|
@ -253,7 +253,7 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
<div className='account__header__bar'>
|
||||
<div className='account__header__tabs'>
|
||||
<a className='avatar' href={account.get('url')} rel='noopener' target='_blank'>
|
||||
<a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'>
|
||||
<Avatar account={account} size={90} />
|
||||
</a>
|
||||
|
||||
|
@ -282,10 +282,10 @@ class Header extends ImmutablePureComponent {
|
|||
<dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} />
|
||||
|
||||
<dd className='verified'>
|
||||
<a href={proof.get('proof_url')} target='_blank' rel='noopener'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
|
||||
<a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
|
||||
<Icon id='check' className='verified__mark' />
|
||||
</span></a>
|
||||
<a href={proof.get('profile_url')} target='_blank' rel='noopener'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a>
|
||||
<a href={proof.get('profile_url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a>
|
||||
</dd>
|
||||
</dl>
|
||||
))}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { decode } from 'blurhash';
|
||||
import classNames from 'classnames';
|
||||
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';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
export default class MediaItem extends ImmutablePureComponent {
|
||||
|
||||
|
@ -151,7 +151,7 @@ export default class MediaItem extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<div className='account-gallery__item' style={{ width, height }}>
|
||||
<a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick} title={title}>
|
||||
<a className='media-gallery__item-thumbnail' href={status.get('url')} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'>
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} />
|
||||
{visible && thumbnail}
|
||||
{!visible && icon}
|
||||
|
|
|
@ -12,6 +12,7 @@ const messages = defineMessages({
|
|||
pause: { id: 'video.pause', defaultMessage: 'Pause' },
|
||||
mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
|
||||
unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
|
||||
download: { id: 'video.download', defaultMessage: 'Download file' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
|
@ -202,6 +203,7 @@ class Audio extends React.PureComponent {
|
|||
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
|
||||
|
||||
<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
|
||||
|
||||
<div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
|
||||
|
||||
<span
|
||||
|
@ -217,6 +219,14 @@ class Audio extends React.PureComponent {
|
|||
<span className='video-player__time-total'>{formatTime(this.state.duration || Math.floor(this.props.duration))}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='video-player__buttons right'>
|
||||
<button type='button' aria-label={intl.formatMessage(messages.download)}>
|
||||
<a className='video-player__download__icon' href={this.props.src} download>
|
||||
<Icon id={'download'} fixedWidth />
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import Toggle from 'react-toggle';
|
||||
import AsyncSelect from 'react-select/lib/Async';
|
||||
import AsyncSelect from 'react-select/async';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' },
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import StatusContainer from '../../../containers/status_container';
|
||||
import AccountContainer from '../../../containers/account_container';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import Permalink from '../../../components/permalink';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import StatusContainer from 'mastodon/containers/status_container';
|
||||
import AccountContainer from 'mastodon/containers/account_container';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import Permalink from 'mastodon/components/permalink';
|
||||
|
||||
const messages = defineMessages({
|
||||
favourite: { id: 'notification.favourite', defaultMessage: '{name} favourited your status' },
|
||||
follow: { id: 'notification.follow', defaultMessage: '{name} followed you' },
|
||||
ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
|
||||
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
|
||||
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
|
||||
});
|
||||
|
||||
const notificationForScreenReader = (intl, message, timestamp) => {
|
||||
const output = [message];
|
||||
|
@ -107,7 +116,7 @@ class Notification extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<div className='notification notification-follow focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow', defaultMessage: '{name} followed you' }, { name: account.get('acct') }), notification.get('created_at'))}>
|
||||
<div className='notification notification-follow focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.follow, { name: account.get('acct') }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<div className='notification__favourite-icon-wrapper'>
|
||||
<Icon id='user-plus' fixedWidth />
|
||||
|
@ -146,7 +155,7 @@ class Notification extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<div className='notification notification-favourite focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.favourite', defaultMessage: '{name} favourited your status' }, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||
<div className='notification notification-favourite focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.favourite, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<div className='notification__favourite-icon-wrapper'>
|
||||
<Icon id='star' className='star-icon' fixedWidth />
|
||||
|
@ -178,7 +187,7 @@ class Notification extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<div className='notification notification-reblog focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.reblog', defaultMessage: '{name} boosted your status' }, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||
<div className='notification notification-reblog focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.reblog, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<div className='notification__favourite-icon-wrapper'>
|
||||
<Icon id='retweet' fixedWidth />
|
||||
|
@ -205,25 +214,31 @@ class Notification extends ImmutablePureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
renderPoll (notification) {
|
||||
renderPoll (notification, account) {
|
||||
const { intl } = this.props;
|
||||
const ownPoll = me === account.get('id');
|
||||
const message = ownPoll ? intl.formatMessage(messages.ownPoll) : intl.formatMessage(messages.poll);
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<div className='notification notification-poll focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' }), notification.get('created_at'))}>
|
||||
<div className='notification notification-poll focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, message, notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<div className='notification__favourite-icon-wrapper'>
|
||||
<Icon id='tasks' fixedWidth />
|
||||
</div>
|
||||
|
||||
<span title={notification.get('created_at')}>
|
||||
<FormattedMessage id='notification.poll' defaultMessage='A poll you have voted in has ended' />
|
||||
{ownPoll ? (
|
||||
<FormattedMessage id='notification.ownPoll' defaultMessage='Your poll has ended' />
|
||||
) : (
|
||||
<FormattedMessage id='notification.poll' defaultMessage='A poll you have voted in has ended' />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<StatusContainer
|
||||
id={notification.get('status')}
|
||||
account={notification.get('account')}
|
||||
account={account}
|
||||
muted
|
||||
withDismiss
|
||||
hidden={this.props.hidden}
|
||||
|
@ -253,7 +268,7 @@ class Notification extends ImmutablePureComponent {
|
|||
case 'reblog':
|
||||
return this.renderReblog(notification, link);
|
||||
case 'poll':
|
||||
return this.renderPoll(notification);
|
||||
return this.renderPoll(notification, account);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -148,7 +148,7 @@ export default class Card extends React.PureComponent {
|
|||
const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded;
|
||||
const interactive = card.get('type') !== 'link';
|
||||
const className = classnames('status-card', { horizontal, compact, interactive });
|
||||
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
|
||||
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
|
||||
const ratio = card.get('width') / card.get('height');
|
||||
const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
|
||||
|
||||
|
@ -180,7 +180,7 @@ export default class Card extends React.PureComponent {
|
|||
<div className='status-card__actions'>
|
||||
<div>
|
||||
<button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
|
||||
{horizontal && <a href={card.get('url')} target='_blank' rel='noopener'><Icon id='external-link' /></a>}
|
||||
{horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -208,7 +208,7 @@ export default class Card extends React.PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<a href={card.get('url')} className={className} target='_blank' rel='noopener' ref={this.setRef}>
|
||||
<a href={card.get('url')} className={className} target='_blank' rel='noopener noreferrer' ref={this.setRef}>
|
||||
{embed}
|
||||
{description}
|
||||
</a>
|
||||
|
|
|
@ -156,7 +156,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
if (status.get('application')) {
|
||||
applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>;
|
||||
applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></span>;
|
||||
}
|
||||
|
||||
if (status.get('visibility') === 'direct') {
|
||||
|
@ -220,7 +220,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
{media}
|
||||
|
||||
<div className='detailed-status__meta'>
|
||||
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
|
||||
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
|
||||
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
||||
</a>{applicationLink} · {reblogLink} · {favouriteLink}
|
||||
</div>
|
||||
|
|
|
@ -26,7 +26,7 @@ export default class ActionsModal extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<li key={`${text}-${i}`}>
|
||||
<a href={href} target='_blank' rel='noopener' onClick={this.props.onClick} data-index={i} className={classNames({ active })}>
|
||||
<a href={href} target='_blank' rel='noopener noreferrer' onClick={this.props.onClick} data-index={i} className={classNames({ active })}>
|
||||
{icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' inverted />}
|
||||
<div>
|
||||
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
|
||||
|
@ -42,7 +42,7 @@ export default class ActionsModal extends ImmutablePureComponent {
|
|||
<div className='status light'>
|
||||
<div className='boost-modal__status-header'>
|
||||
<div className='boost-modal__status-time'>
|
||||
<a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener'>
|
||||
<a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
||||
<RelativeTimestamp timestamp={this.props.status.get('created_at')} />
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -61,7 +61,7 @@ class BoostModal extends ImmutablePureComponent {
|
|||
<div className='status light'>
|
||||
<div className='boost-modal__status-header'>
|
||||
<div className='boost-modal__status-time'>
|
||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||
</div>
|
||||
|
||||
<a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'>
|
||||
|
|
|
@ -182,7 +182,7 @@ class ColumnsArea extends ImmutablePureComponent {
|
|||
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
|
||||
|
||||
const content = columnIndex !== -1 ? (
|
||||
<ReactSwipeableViews key='content' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
|
||||
<ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
|
||||
{links.map(this.renderView)}
|
||||
</ReactSwipeableViews>
|
||||
) : (
|
||||
|
|
|
@ -62,7 +62,7 @@ class LinkFooter extends React.PureComponent {
|
|||
<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={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span> }}
|
||||
values={{ github: <span><a href={source_url} rel='noopener noreferrer' target='_blank'>{repository}</a> (v{version})</span> }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -19,6 +19,7 @@ const messages = defineMessages({
|
|||
close: { id: 'video.close', defaultMessage: 'Close video' },
|
||||
fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
|
||||
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
|
||||
download: { id: 'video.download', defaultMessage: 'Download file' },
|
||||
});
|
||||
|
||||
export const formatTime = secondsNum => {
|
||||
|
@ -470,6 +471,7 @@ class Video extends React.PureComponent {
|
|||
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
|
||||
|
||||
<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
|
||||
|
||||
<div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
|
||||
<span
|
||||
className={classNames('video-player__volume__handle')}
|
||||
|
@ -493,7 +495,13 @@ class Video extends React.PureComponent {
|
|||
{(!onCloseVideo && !editable) && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
|
||||
{(!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(messages.download)}>
|
||||
<a className='video-player__download__icon' href={this.props.src} download>
|
||||
<Icon id={'download'} fixedWidth />
|
||||
</a>
|
||||
</button>
|
||||
<button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -24,5 +24,6 @@ export const useBlurhash = getMeta('use_blurhash');
|
|||
export const usePendingItems = getMeta('use_pending_items');
|
||||
export const showTrends = getMeta('trends');
|
||||
export const title = getMeta('title');
|
||||
export const cropImages = getMeta('crop_images');
|
||||
|
||||
export default initialState;
|
||||
|
|
|
@ -776,6 +776,10 @@
|
|||
{
|
||||
"defaultMessage": "Unmute sound",
|
||||
"id": "video.unmute"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Download file",
|
||||
"id": "video.download"
|
||||
}
|
||||
],
|
||||
"path": "app/javascript/mastodon/features/audio/index.json"
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
REBLOG_FAIL,
|
||||
FAVOURITE_REQUEST,
|
||||
FAVOURITE_FAIL,
|
||||
UNFAVOURITE_SUCCESS,
|
||||
} from '../actions/interactions';
|
||||
import {
|
||||
STATUS_MUTE_SUCCESS,
|
||||
|
@ -37,6 +38,9 @@ export default function statuses(state = initialState, action) {
|
|||
return importStatuses(state, action.statuses);
|
||||
case FAVOURITE_REQUEST:
|
||||
return state.setIn([action.status.get('id'), 'favourited'], true);
|
||||
case UNFAVOURITE_SUCCESS:
|
||||
const favouritesCount = action.status.get('favourites_count');
|
||||
return state.setIn([action.status.get('id'), 'favourites_count'], favouritesCount - 1);
|
||||
case FAVOURITE_FAIL:
|
||||
return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], false);
|
||||
case REBLOG_REQUEST:
|
||||
|
|
|
@ -1591,6 +1591,20 @@ a.account__display-name {
|
|||
color: $gold-star;
|
||||
}
|
||||
|
||||
.no-reduce-motion .icon-button.star-icon {
|
||||
&.activate {
|
||||
& > .fa-star {
|
||||
animation: spring-rotate-in 1s linear;
|
||||
}
|
||||
}
|
||||
|
||||
&.deactivate {
|
||||
& > .fa-star {
|
||||
animation: spring-rotate-out 1s linear;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notification__display-name {
|
||||
color: inherit;
|
||||
font-weight: 500;
|
||||
|
@ -3373,6 +3387,50 @@ a.status-card.compact:hover {
|
|||
animation: loader-figure 1.15s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
}
|
||||
|
||||
@keyframes spring-rotate-in {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: rotate(-484.8deg);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: rotate(-316.7deg);
|
||||
}
|
||||
|
||||
90% {
|
||||
transform: rotate(-375deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spring-rotate-out {
|
||||
0% {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: rotate(124.8deg);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: rotate(-43.27deg);
|
||||
}
|
||||
|
||||
90% {
|
||||
transform: rotate(15deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loader-figure {
|
||||
0% {
|
||||
width: 0;
|
||||
|
@ -5194,6 +5252,7 @@ a.status-card.compact:hover {
|
|||
max-height: 100% !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5271,6 +5330,10 @@ a.status-card.compact:hover {
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 10px;
|
||||
|
||||
.video-player__download__icon {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
|
|
|
@ -149,10 +149,6 @@ a.table-action-link {
|
|||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions,
|
||||
|
@ -174,10 +170,6 @@ a.table-action-link {
|
|||
text-align: right;
|
||||
padding-right: 16px - 5px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__form {
|
||||
|
@ -198,7 +190,7 @@ a.table-action-link {
|
|||
background: darken($ui-base-color, 4%);
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
&:first-child {
|
||||
.optional &:first-child {
|
||||
border-top: 1px solid darken($ui-base-color, 8%);
|
||||
}
|
||||
}
|
||||
|
@ -264,6 +256,13 @@ a.table-action-link {
|
|||
}
|
||||
}
|
||||
|
||||
&.optional .batch-table__toolbar,
|
||||
&.optional .batch-table__row__select {
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.status__content {
|
||||
padding-top: 0;
|
||||
|
||||
|
|
|
@ -153,6 +153,14 @@ class ActivityPub::Activity
|
|||
fetch_remote_original_status
|
||||
end
|
||||
|
||||
def follow_request_from_object
|
||||
@follow_request ||= FollowRequest.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
|
||||
end
|
||||
|
||||
def follow_from_object
|
||||
@follow ||= Follow.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
|
||||
end
|
||||
|
||||
def fetch_remote_original_status
|
||||
if object_uri.start_with?('http')
|
||||
return if ActivityPub::TagManager.instance.local_uri?(object_uri)
|
||||
|
|
|
@ -2,17 +2,18 @@
|
|||
|
||||
class ActivityPub::Activity::Accept < ActivityPub::Activity
|
||||
def perform
|
||||
return accept_follow_for_relay if relay_follow?
|
||||
return follow_request_from_object.authorize! unless follow_request_from_object.nil?
|
||||
|
||||
case @object['type']
|
||||
when 'Follow'
|
||||
accept_follow
|
||||
accept_embedded_follow
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def accept_follow
|
||||
return accept_follow_for_relay if relay_follow?
|
||||
|
||||
def accept_embedded_follow
|
||||
target_account = account_from_uri(target_uri)
|
||||
|
||||
return if target_account.nil? || !target_account.local?
|
||||
|
|
|
@ -2,17 +2,19 @@
|
|||
|
||||
class ActivityPub::Activity::Reject < ActivityPub::Activity
|
||||
def perform
|
||||
return reject_follow_for_relay if relay_follow?
|
||||
return follow_request_from_object.reject! unless follow_request_from_object.nil?
|
||||
return UnfollowService.new.call(follow_from_object.target_account, @account) unless follow_from_object.nil?
|
||||
|
||||
case @object['type']
|
||||
when 'Follow'
|
||||
reject_follow
|
||||
reject_embedded_follow
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reject_follow
|
||||
return reject_follow_for_relay if relay_follow?
|
||||
|
||||
def reject_embedded_follow
|
||||
target_account = account_from_uri(target_uri)
|
||||
|
||||
return if target_account.nil? || !target_account.local?
|
||||
|
|
|
@ -251,7 +251,7 @@ class Formatter
|
|||
|
||||
def link_to_url(entity, options = {})
|
||||
url = Addressable::URI.parse(entity[:url])
|
||||
html_attrs = { target: '_blank', rel: 'nofollow noopener' }
|
||||
html_attrs = { target: '_blank', rel: 'nofollow noopener noreferrer' }
|
||||
|
||||
html_attrs[:rel] = "me #{html_attrs[:rel]}" if options[:me]
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ class Sanitize
|
|||
|
||||
add_attributes: {
|
||||
'a' => {
|
||||
'rel' => 'nofollow noopener',
|
||||
'rel' => 'nofollow noopener noreferrer',
|
||||
'target' => '_blank',
|
||||
},
|
||||
},
|
||||
|
|
|
@ -37,6 +37,7 @@ class UserSettingsDecorator
|
|||
user.settings['use_blurhash'] = use_blurhash_preference if change?('setting_use_blurhash')
|
||||
user.settings['use_pending_items'] = use_pending_items_preference if change?('setting_use_pending_items')
|
||||
user.settings['trends'] = trends_preference if change?('setting_trends')
|
||||
user.settings['crop_images'] = crop_images_preference if change?('setting_crop_images')
|
||||
end
|
||||
|
||||
def merged_notification_emails
|
||||
|
@ -127,6 +128,10 @@ class UserSettingsDecorator
|
|||
boolean_cast_setting 'setting_trends'
|
||||
end
|
||||
|
||||
def crop_images_preference
|
||||
boolean_cast_setting 'setting_crop_images'
|
||||
end
|
||||
|
||||
def boolean_cast_setting(key)
|
||||
ActiveModel::Type::Boolean.new.cast(settings[key])
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class AdminMailer < ApplicationMailer
|
||||
layout 'plain_mailer'
|
||||
|
||||
helper :statuses
|
||||
helper :accounts
|
||||
|
||||
def new_report(recipient, report)
|
||||
@report = report
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class NotificationMailer < ApplicationMailer
|
||||
helper :accounts
|
||||
helper :statuses
|
||||
|
||||
add_template_helper RoutingHelper
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
class UserMailer < Devise::Mailer
|
||||
layout 'mailer'
|
||||
|
||||
helper :accounts
|
||||
helper :application
|
||||
helper :instance
|
||||
helper :statuses
|
||||
|
||||
add_template_helper RoutingHelper
|
||||
|
||||
|
|
|
@ -87,6 +87,7 @@ class MediaAttachment < ApplicationRecord
|
|||
convert_options: {
|
||||
output: {
|
||||
'loglevel' => 'fatal',
|
||||
'map_metadata' => '-1',
|
||||
'q:a' => 2,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -108,7 +108,7 @@ class User < ApplicationRecord
|
|||
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal,
|
||||
:reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network,
|
||||
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
|
||||
:advanced_layout, :use_blurhash, :use_pending_items, :trends,
|
||||
:advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images,
|
||||
to: :settings, prefix: :setting, allow_nil: false
|
||||
|
||||
attr_reader :invite_code
|
||||
|
|
|
@ -38,11 +38,13 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||
store[:use_pending_items] = object.current_account.user.setting_use_pending_items
|
||||
store[:is_staff] = object.current_account.user.staff?
|
||||
store[:trends] = Setting.trends && object.current_account.user.setting_trends
|
||||
store[:crop_images] = object.current_account.user.setting_crop_images
|
||||
else
|
||||
store[:auto_play_gif] = Setting.auto_play_gif
|
||||
store[:display_media] = Setting.display_media
|
||||
store[:reduce_motion] = Setting.reduce_motion
|
||||
store[:use_blurhash] = Setting.use_blurhash
|
||||
store[:crop_images] = Setting.crop_images
|
||||
end
|
||||
|
||||
store
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
class RSS::AccountSerializer
|
||||
include ActionView::Helpers::NumberHelper
|
||||
include StatusesHelper
|
||||
include AccountsHelper
|
||||
include RoutingHelper
|
||||
|
||||
def render(account, statuses, tag)
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
class RSS::TagSerializer
|
||||
include ActionView::Helpers::NumberHelper
|
||||
include ActionView::Helpers::SanitizeHelper
|
||||
include StatusesHelper
|
||||
include RoutingHelper
|
||||
|
||||
def render(tag, statuses)
|
||||
|
|
|
@ -143,6 +143,7 @@ class BackupService < BaseService
|
|||
end
|
||||
end
|
||||
rescue Errno::ENOENT
|
||||
rescue Seahorse::Client::NetworkingError
|
||||
Rails.logger.warn "Could not backup file #{filename}: file not found"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -84,7 +84,7 @@ class FetchLinkCardService < BaseService
|
|||
|
||||
def skip_link?(a)
|
||||
# Avoid links for hashtags and mentions (microformats)
|
||||
a['rel']&.include?('tag') || a['class']&.include?('u-url') || mention_link?(a)
|
||||
a['rel']&.include?('tag') || a['class']&.match?(/u-url|h-card/) || mention_link?(a)
|
||||
end
|
||||
|
||||
def attempt_oembed
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
%small= t('about.browse_public_posts')
|
||||
|
||||
.directory__tag
|
||||
= link_to 'https://joinmastodon.org/apps', target: '_blank', rel: 'noopener' do
|
||||
= link_to 'https://joinmastodon.org/apps', target: '_blank', rel: 'noopener noreferrer' do
|
||||
%h4
|
||||
= fa_icon 'tablet fw'
|
||||
= t('about.get_apps')
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
= t('accounts.moved_html', name: content_tag(:bdi, content_tag(:strong, display_name(account, custom_emojify: true), class: :emojify)), new_profile_link: link_to(content_tag(:strong, safe_join(['@', content_tag(:span, moved_to_account.acct)])), ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'mention'))
|
||||
|
||||
.moved-account-widget__card
|
||||
= link_to ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'me noopener' do
|
||||
= link_to ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'me noopener noreferrer' do
|
||||
.detailed-status__display-avatar
|
||||
.account__avatar-overlay
|
||||
.account__avatar-overlay-base{ style: "background-image: url('#{moved_to_account.avatar.url(:original)}')" }
|
||||
|
|
|
@ -143,12 +143,15 @@
|
|||
%th= t('admin.accounts.most_recent_ip')
|
||||
%td= @account.user_current_sign_in_ip
|
||||
%td
|
||||
- if @account.user_current_sign_in_ip
|
||||
= table_link_to 'search', t('admin.accounts.search_same_ip'), admin_accounts_path(ip: @account.user_current_sign_in_ip)
|
||||
|
||||
%tr
|
||||
%th= t('admin.accounts.most_recent_activity')
|
||||
%td
|
||||
- if @account.user_current_sign_in_at
|
||||
%time.formatted{ datetime: @account.user_current_sign_in_at.iso8601, title: l(@account.user_current_sign_in_at) }= l @account.user_current_sign_in_at
|
||||
%td
|
||||
|
||||
- if @account.user&.invited?
|
||||
%tr
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
= react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.proper.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }
|
||||
|
||||
.detailed-status__meta
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener' do
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener noreferrer' do
|
||||
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
||||
- if status.discarded?
|
||||
·
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
- Admin::FilterHelper::TAGS_FILTERS.each do |key|
|
||||
= hidden_field_tag key, params[key] if params[key].present?
|
||||
|
||||
.batch-table
|
||||
.batch-table.optional
|
||||
.batch-table__toolbar
|
||||
%label.batch-table__toolbar__select.batch-checkbox-all
|
||||
= check_box_tag :batch_checkbox_all, nil, false
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
.dashboard__counters
|
||||
%div
|
||||
= link_to tag_url(@tag), target: '_blank', rel: 'noopener' do
|
||||
= link_to tag_url(@tag), target: '_blank', rel: 'noopener noreferrer' do
|
||||
.dashboard__counters__num= number_with_delimiter @accounts_today
|
||||
.dashboard__counters__label= t 'admin.tags.accounts_today'
|
||||
%div
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
- account_url = local_assigns[:admin] ? admin_account_path(account.id) : ActivityPub::TagManager.instance.url_for(account)
|
||||
|
||||
.card.h-card
|
||||
= link_to account_url, target: '_blank', rel: 'noopener' do
|
||||
= link_to account_url, target: '_blank', rel: 'noopener noreferrer' do
|
||||
.card__img
|
||||
= image_tag account.header.url, alt: ''
|
||||
.card__bar
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
- if application.website.blank?
|
||||
= application.name
|
||||
- else
|
||||
= link_to application.name, application.website, target: '_blank', rel: 'noopener'
|
||||
= link_to application.name, application.website, target: '_blank', rel: 'noopener noreferrer'
|
||||
%th!= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join(', ')
|
||||
%td= l application.created_at
|
||||
%td
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
|
||||
.page-header
|
||||
%h1= t('about.see_whats_happening')
|
||||
%p= t('about.browse_public_posts')
|
||||
|
||||
- if Setting.show_known_fediverse_at_about_page
|
||||
%p= t('about.browse_public_posts')
|
||||
- else
|
||||
%p= t('about.browse_local_posts')
|
||||
|
||||
#mastodon-timeline{ data: { props: Oj.dump(default_props) }}
|
||||
#modal-container
|
||||
|
|
|
@ -25,6 +25,11 @@
|
|||
= f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label
|
||||
= f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label
|
||||
|
||||
%h4= t 'appearance.toot_layout'
|
||||
|
||||
.fields-group
|
||||
= f.input :setting_crop_images, as: :boolean, wrapper: :with_label
|
||||
|
||||
%h4= t 'appearance.discovery'
|
||||
|
||||
.fields-group
|
||||
|
|
|
@ -44,14 +44,14 @@
|
|||
.detailed-status__meta
|
||||
%data.dt-published{ value: status.created_at.to_time.iso8601 }
|
||||
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener' do
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener noreferrer' do
|
||||
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
||||
·
|
||||
- if status.application && @account.user&.setting_show_application
|
||||
- if status.application.website.blank?
|
||||
%strong.detailed-status__application= status.application.name
|
||||
- else
|
||||
= link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener'
|
||||
= link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener noreferrer'
|
||||
·
|
||||
= link_to remote_interaction_path(status, type: :reply), class: 'modal-button detailed-status__link' do
|
||||
- if status.in_reply_to_id.nil?
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
.status
|
||||
.status__info
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__relative-time u-url u-uid', target: stream_link_target, rel: 'noopener' do
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__relative-time u-url u-uid', target: stream_link_target, rel: 'noopener noreferrer' do
|
||||
%time.time-ago{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
||||
%data.dt-published{ value: status.created_at.to_time.iso8601 }
|
||||
|
||||
.p-author.h-card
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'status__display-name u-url', target: stream_link_target, rel: 'noopener' do
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'status__display-name u-url', target: stream_link_target, rel: 'noopener noreferrer' do
|
||||
.status__avatar
|
||||
%div
|
||||
- if current_account&.user&.setting_auto_play_gif || autoplay
|
||||
|
|
|
@ -11,6 +11,7 @@ en:
|
|||
apps: Mobile apps
|
||||
apps_platforms: Use Mastodon from iOS, Android and other platforms
|
||||
browse_directory: Browse a profile directory and filter by interests
|
||||
browse_local_posts: Browse a live stream of public posts from this server
|
||||
browse_public_posts: Browse a live stream of public posts on Mastodon
|
||||
contact: Contact
|
||||
contact_missing: Not set
|
||||
|
@ -175,6 +176,7 @@ en:
|
|||
user: User
|
||||
salmon_url: Salmon URL
|
||||
search: Search
|
||||
search_same_ip: Other users with the same IP
|
||||
shared_inbox_url: Shared inbox URL
|
||||
show:
|
||||
created_reports: Made reports
|
||||
|
@ -575,6 +577,7 @@ en:
|
|||
confirmation_dialogs: Confirmation dialogs
|
||||
discovery: Discovery
|
||||
sensitive_content: Sensitive content
|
||||
toot_layout: Toot layout
|
||||
application_mailer:
|
||||
notification_preferences: Change e-mail preferences
|
||||
salutation: "%{name},"
|
||||
|
|
|
@ -78,7 +78,7 @@ en:
|
|||
text: Custom warning
|
||||
type: Action
|
||||
types:
|
||||
disable: Disable
|
||||
disable: Disable login
|
||||
none: Do nothing
|
||||
silence: Silence
|
||||
suspend: Suspend and irreversibly delete account data
|
||||
|
@ -113,6 +113,7 @@ en:
|
|||
setting_aggregate_reblogs: Group boosts in timelines
|
||||
setting_auto_play_gif: Auto-play animated GIFs
|
||||
setting_boost_modal: Show confirmation dialog before boosting
|
||||
setting_crop_images: Crop images in non-expanded toots to 16x9
|
||||
setting_default_language: Posting language
|
||||
setting_default_privacy: Posting privacy
|
||||
setting_default_sensitive: Always mark media as sensitive
|
||||
|
|
|
@ -36,6 +36,7 @@ defaults: &defaults
|
|||
use_pending_items: false
|
||||
trends: true
|
||||
trendable_by_default: false
|
||||
crop_images: true
|
||||
notification_emails:
|
||||
follow: false
|
||||
reblog: false
|
||||
|
|
26
package.json
26
package.json
|
@ -61,7 +61,7 @@
|
|||
"dependencies": {
|
||||
"@babel/core": "^7.4.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
||||
"@babel/plugin-proposal-decorators": "^7.4.4",
|
||||
"@babel/plugin-proposal-decorators": "^7.6.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.4.4",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/plugin-transform-react-inline-elements": "^7.2.0",
|
||||
|
@ -71,7 +71,7 @@
|
|||
"@babel/preset-env": "^7.6.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/runtime": "^7.5.4",
|
||||
"@clusterws/cws": "^0.15.0",
|
||||
"@clusterws/cws": "^0.15.2",
|
||||
"array-includes": "^3.0.3",
|
||||
"autoprefixer": "^9.6.1",
|
||||
"axios": "^0.19.0",
|
||||
|
@ -85,11 +85,11 @@
|
|||
"classnames": "^2.2.5",
|
||||
"compression-webpack-plugin": "^3.0.0",
|
||||
"copy-webpack-plugin": "^5.0.4",
|
||||
"cross-env": "^5.1.4",
|
||||
"cross-env": "^6.0.3",
|
||||
"css-loader": "^3.2.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"detect-passive-events": "^1.0.2",
|
||||
"dotenv": "^8.0.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"emoji-mart": "Gargron/emoji-mart#build",
|
||||
"es6-symbol": "^3.1.2",
|
||||
"escape-html": "^1.0.3",
|
||||
|
@ -125,8 +125,8 @@
|
|||
"prop-types": "^15.5.10",
|
||||
"punycode": "^2.1.0",
|
||||
"rails-ujs": "^5.2.3",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"react": "^16.10.2",
|
||||
"react-dom": "^16.10.2",
|
||||
"react-hotkeys": "^1.1.4",
|
||||
"react-immutable-proptypes": "^2.1.0",
|
||||
"react-immutable-pure-component": "^1.1.1",
|
||||
|
@ -139,7 +139,7 @@
|
|||
"react-redux-loading-bar": "^4.0.8",
|
||||
"react-router-dom": "^4.1.1",
|
||||
"react-router-scroll-4": "^1.0.0-beta.1",
|
||||
"react-select": "^2.4.4",
|
||||
"react-select": "^3.0.5",
|
||||
"react-sparklines": "^1.7.0",
|
||||
"react-swipeable-views": "^0.13.3",
|
||||
"react-textarea-autosize": "^7.1.0",
|
||||
|
@ -152,11 +152,11 @@
|
|||
"requestidlecallback": "^0.3.0",
|
||||
"reselect": "^4.0.0",
|
||||
"rimraf": "^3.0.0",
|
||||
"sass": "^1.22.12",
|
||||
"sass": "^1.23.1",
|
||||
"sass-loader": "^7.0.3",
|
||||
"stringz": "^2.0.0",
|
||||
"substring-trie": "^1.0.2",
|
||||
"terser-webpack-plugin": "^1.4.1",
|
||||
"terser-webpack-plugin": "^2.2.1",
|
||||
"tesseract.js": "^2.0.0-alpha.16",
|
||||
"throng": "^4.0.0",
|
||||
"tiny-queue": "^0.2.1",
|
||||
|
@ -164,7 +164,7 @@
|
|||
"wavesurfer.js": "^3.0.0",
|
||||
"webpack": "^4.35.3",
|
||||
"webpack-assets-manifest": "^3.1.1",
|
||||
"webpack-bundle-analyzer": "^3.3.2",
|
||||
"webpack-bundle-analyzer": "^3.6.0",
|
||||
"webpack-cli": "^3.3.7",
|
||||
"webpack-merge": "^4.2.1",
|
||||
"websocket.js": "^0.1.12"
|
||||
|
@ -174,15 +174,15 @@
|
|||
"babel-jest": "^24.9.0",
|
||||
"enzyme": "^3.10.0",
|
||||
"enzyme-adapter-react-16": "^1.14.0",
|
||||
"eslint": "^6.5.0",
|
||||
"eslint": "^6.5.1",
|
||||
"eslint-plugin-import": "~2.18.2",
|
||||
"eslint-plugin-jsx-a11y": "~6.2.3",
|
||||
"eslint-plugin-promise": "~4.2.1",
|
||||
"eslint-plugin-react": "~7.14.3",
|
||||
"eslint-plugin-react": "~7.16.0",
|
||||
"jest": "^24.9.0",
|
||||
"raf": "^3.4.1",
|
||||
"react-intl-translations-manager": "^5.0.3",
|
||||
"react-test-renderer": "^16.8.6",
|
||||
"react-test-renderer": "^16.10.2",
|
||||
"sass-lint": "^1.13.1",
|
||||
"webpack-dev-server": "^3.8.0",
|
||||
"yargs": "^13.3.0"
|
||||
|
|
|
@ -123,7 +123,7 @@
|
|||
<published>2016-10-10T00:41:31Z</published>
|
||||
<updated>2016-10-10T00:41:31Z</updated>
|
||||
<title>Social media needs MOAR cats! http://kickass.zone/media/3</title>
|
||||
<content type="html"><p>Social media needs MOAR cats! <a rel="nofollow noopener" href="http://kickass.zone/media/3">http://kickass.zone/media/3</a></p></content>
|
||||
<content type="html"><p>Social media needs MOAR cats! <a rel="nofollow noopener noreferrer" href="http://kickass.zone/media/3">http://kickass.zone/media/3</a></p></content>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
|
||||
<link rel="self" type="application/atom+xml" href="http://kickass.zone/users/localhost/updates/9.atom"/>
|
||||
<link rel="alternate" type="text/html" href="http://kickass.zone/users/localhost/updates/9"/>
|
||||
|
@ -135,7 +135,7 @@
|
|||
<published>2016-10-10T00:38:39Z</published>
|
||||
<updated>2016-10-10T00:38:39Z</updated>
|
||||
<title>http://kickass.zone/media/2</title>
|
||||
<content type="html"><p><a rel="nofollow noopener" href="http://kickass.zone/media/2">http://kickass.zone/media/2</a></p></content>
|
||||
<content type="html"><p><a rel="nofollow noopener noreferrer" href="http://kickass.zone/media/2">http://kickass.zone/media/2</a></p></content>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
|
||||
<link rel="self" type="application/atom+xml" href="http://kickass.zone/users/localhost/updates/8.atom"/>
|
||||
<link rel="alternate" type="text/html" href="http://kickass.zone/users/localhost/updates/8"/>
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AccountsHelper, type: :helper do
|
||||
def set_not_embedded_view
|
||||
params[:controller] = "not_#{StatusesHelper::EMBEDDED_CONTROLLER}"
|
||||
params[:action] = "not_#{StatusesHelper::EMBEDDED_ACTION}"
|
||||
end
|
||||
|
||||
def set_embedded_view
|
||||
params[:controller] = StatusesHelper::EMBEDDED_CONTROLLER
|
||||
params[:action] = StatusesHelper::EMBEDDED_ACTION
|
||||
end
|
||||
|
||||
describe '#display_name' do
|
||||
it 'uses the display name when it exists' do
|
||||
account = Account.new(display_name: "Display", username: "Username")
|
||||
|
||||
expect(helper.display_name(account)).to eq "Display"
|
||||
end
|
||||
|
||||
it 'uses the username when display name is nil' do
|
||||
account = Account.new(display_name: nil, username: "Username")
|
||||
|
||||
expect(helper.display_name(account)).to eq "Username"
|
||||
end
|
||||
end
|
||||
|
||||
describe '#acct' do
|
||||
it 'is fully qualified for embedded local accounts' do
|
||||
allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain')
|
||||
set_embedded_view
|
||||
account = Account.new(domain: nil, username: 'user')
|
||||
|
||||
acct = helper.acct(account)
|
||||
|
||||
expect(acct).to eq '@user@local_domain'
|
||||
end
|
||||
|
||||
it 'is fully qualified for embedded foreign accounts' do
|
||||
set_embedded_view
|
||||
account = Account.new(domain: 'foreign_server.com', username: 'user')
|
||||
|
||||
acct = helper.acct(account)
|
||||
|
||||
expect(acct).to eq '@user@foreign_server.com'
|
||||
end
|
||||
|
||||
it 'is fully qualified for non embedded foreign accounts' do
|
||||
set_not_embedded_view
|
||||
account = Account.new(domain: 'foreign_server.com', username: 'user')
|
||||
|
||||
acct = helper.acct(account)
|
||||
|
||||
expect(acct).to eq '@user@foreign_server.com'
|
||||
end
|
||||
|
||||
it 'is fully qualified for non embedded local accounts' do
|
||||
allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain')
|
||||
set_not_embedded_view
|
||||
account = Account.new(domain: nil, username: 'user')
|
||||
|
||||
acct = helper.acct(account)
|
||||
|
||||
expect(acct).to eq '@user@local_domain'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,7 +3,7 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Admin::AccountModerationNotesHelper, type: :helper do
|
||||
include StatusesHelper
|
||||
include AccountsHelper
|
||||
|
||||
describe '#admin_account_link_to' do
|
||||
context 'account is nil' do
|
||||
|
|
|
@ -1,20 +1,6 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe StatusesHelper, type: :helper do
|
||||
describe '#display_name' do
|
||||
it 'uses the display name when it exists' do
|
||||
account = Account.new(display_name: "Display", username: "Username")
|
||||
|
||||
expect(helper.display_name(account)).to eq "Display"
|
||||
end
|
||||
|
||||
it 'uses the username when display name is nil' do
|
||||
account = Account.new(display_name: nil, username: "Username")
|
||||
|
||||
expect(helper.display_name(account)).to eq "Username"
|
||||
end
|
||||
end
|
||||
|
||||
describe '#stream_link_target' do
|
||||
it 'returns nil if it is not an embedded view' do
|
||||
set_not_embedded_view
|
||||
|
@ -29,46 +15,6 @@ RSpec.describe StatusesHelper, type: :helper do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#acct' do
|
||||
it 'is fully qualified for embedded local accounts' do
|
||||
allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain')
|
||||
set_embedded_view
|
||||
account = Account.new(domain: nil, username: 'user')
|
||||
|
||||
acct = helper.acct(account)
|
||||
|
||||
expect(acct).to eq '@user@local_domain'
|
||||
end
|
||||
|
||||
it 'is fully qualified for embedded foreign accounts' do
|
||||
set_embedded_view
|
||||
account = Account.new(domain: 'foreign_server.com', username: 'user')
|
||||
|
||||
acct = helper.acct(account)
|
||||
|
||||
expect(acct).to eq '@user@foreign_server.com'
|
||||
end
|
||||
|
||||
it 'is fully qualified for non embedded foreign accounts' do
|
||||
set_not_embedded_view
|
||||
account = Account.new(domain: 'foreign_server.com', username: 'user')
|
||||
|
||||
acct = helper.acct(account)
|
||||
|
||||
expect(acct).to eq '@user@foreign_server.com'
|
||||
end
|
||||
|
||||
it 'is fully qualified for non embedded local accounts' do
|
||||
allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain')
|
||||
set_not_embedded_view
|
||||
account = Account.new(domain: nil, username: 'user')
|
||||
|
||||
acct = helper.acct(account)
|
||||
|
||||
expect(acct).to eq '@user@local_domain'
|
||||
end
|
||||
end
|
||||
|
||||
def set_not_embedded_view
|
||||
params[:controller] = "not_#{StatusesHelper::EMBEDDED_CONTROLLER}"
|
||||
params[:action] = "not_#{StatusesHelper::EMBEDDED_ACTION}"
|
||||
|
|
|
@ -24,7 +24,7 @@ describe Sanitize::Config do
|
|||
end
|
||||
|
||||
it 'keep links in lists' do
|
||||
expect(Sanitize.fragment('<p>Check out:</p><ul><li><a href="https://joinmastodon.org" rel="nofollow noopener" target="_blank">joinmastodon.org</a></li><li>Bar</li></ul>', subject)).to eq '<p>Check out:</p><p><a href="https://joinmastodon.org" rel="nofollow noopener" target="_blank">joinmastodon.org</a><br>Bar</p>'
|
||||
expect(Sanitize.fragment('<p>Check out:</p><ul><li><a href="https://joinmastodon.org" rel="nofollow noopener noreferrer" target="_blank">joinmastodon.org</a></li><li>Bar</li></ul>', subject)).to eq '<p>Check out:</p><p><a href="https://joinmastodon.org" rel="nofollow noopener noreferrer" target="_blank">joinmastodon.org</a><br>Bar</p>'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -80,7 +80,7 @@ RSpec.describe FetchLinkCardService, type: :service do
|
|||
end
|
||||
|
||||
context 'in a remote status' do
|
||||
let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: 'Habt ihr ein paar gute Links zu #<span class="tag"><a href="https://quitter.se/tag/wannacry" target="_blank" rel="tag noopener" title="https://quitter.se/tag/wannacry">Wannacry</a></span> herumfliegen? Ich will mal unter <br> <a href="https://github.com/qbi/WannaCry" target="_blank" rel="noopener" title="https://github.com/qbi/WannaCry">https://github.com/qbi/WannaCry</a> was sammeln. !<a href="http://sn.jonkman.ca/group/416/id" target="_blank" rel="noopener" title="http://sn.jonkman.ca/group/416/id">security</a> ') }
|
||||
let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: 'Habt ihr ein paar gute Links zu #<span class="tag"><a href="https://quitter.se/tag/wannacry" target="_blank" rel="tag noopener noreferrer" title="https://quitter.se/tag/wannacry">Wannacry</a></span> herumfliegen? Ich will mal unter <br> <a href="https://github.com/qbi/WannaCry" target="_blank" rel="noopener noreferrer" title="https://github.com/qbi/WannaCry">https://github.com/qbi/WannaCry</a> was sammeln. !<a href="http://sn.jonkman.ca/group/416/id" target="_blank" rel="noopener noreferrer" title="http://sn.jonkman.ca/group/416/id">security</a> ') }
|
||||
|
||||
it 'parses out URLs' do
|
||||
expect(a_request(:get, 'https://github.com/qbi/WannaCry')).to have_been_made.at_least_once
|
||||
|
|
|
@ -28,12 +28,12 @@ RSpec.describe VerifyLinkService, type: :service do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when a link contains an <a rel="noopener"> back' do
|
||||
context 'when a link contains an <a rel="noopener noreferrer"> back' do
|
||||
let(:html) do
|
||||
<<-HTML
|
||||
<!doctype html>
|
||||
<body>
|
||||
<a href="#{ActivityPub::TagManager.instance.url_for(account)}" rel="noopener me" target="_blank">Follow me on Mastodon</a>
|
||||
<a href="#{ActivityPub::TagManager.instance.url_for(account)}" rel="me noopener noreferrer" target="_blank">Follow me on Mastodon</a>
|
||||
</body>
|
||||
HTML
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue