Merge branch 'master' into glitch-soc/merge-upstream

Conflicts:
	app/lib/user_settings_decorator.rb
	app/models/user.rb
	app/serializers/initial_state_serializer.rb
	app/views/stream_entries/_simple_status.html.haml
	config/locales/simple_form.en.yml
	config/locales/simple_form.ja.yml
	config/locales/simple_form.pl.yml
	config/routes.rb
master
Thibaut Girka 5 years ago
commit c91d9b7389
  1. 20
      Gemfile
  2. 99
      Gemfile.lock
  3. 4
      app/controllers/api/base_controller.rb
  4. 7
      app/controllers/api/v1/accounts/statuses_controller.rb
  5. 7
      app/controllers/api/v1/favourites_controller.rb
  6. 4
      app/controllers/api/v1/instances_controller.rb
  7. 7
      app/controllers/api/v1/notifications_controller.rb
  8. 5
      app/controllers/api/v1/reports_controller.rb
  9. 5
      app/controllers/api/v1/timelines/home_controller.rb
  10. 5
      app/controllers/api/v1/timelines/list_controller.rb
  11. 7
      app/controllers/api/v1/timelines/public_controller.rb
  12. 7
      app/controllers/api/v1/timelines/tag_controller.rb
  13. 3
      app/controllers/settings/preferences_controller.rb
  14. 4
      app/helpers/application_helper.rb
  15. 3
      app/javascript/mastodon/actions/importer/normalizer.js
  16. 27
      app/javascript/mastodon/components/__tests__/__snapshots__/autosuggest_emoji-test.js.snap
  17. 29
      app/javascript/mastodon/components/__tests__/autosuggest_emoji-test.js
  18. 4
      app/javascript/mastodon/components/media_gallery.js
  19. 2
      app/javascript/mastodon/components/status.js
  20. 40
      app/javascript/mastodon/components/status_content.js
  21. 4
      app/javascript/mastodon/features/account_gallery/components/media_item.js
  22. 9
      app/javascript/mastodon/features/notifications/components/column_settings.js
  23. 5
      app/javascript/mastodon/features/notifications/components/notification.js
  24. 4
      app/javascript/mastodon/features/notifications/components/setting_toggle.js
  25. 8
      app/javascript/mastodon/features/ui/index.js
  26. 15
      app/javascript/mastodon/features/video/index.js
  27. 3
      app/javascript/mastodon/initial_state.js
  28. 1
      app/javascript/mastodon/locales/ar.json
  29. 1
      app/javascript/mastodon/locales/ast.json
  30. 1
      app/javascript/mastodon/locales/bg.json
  31. 1
      app/javascript/mastodon/locales/ca.json
  32. 1
      app/javascript/mastodon/locales/co.json
  33. 1
      app/javascript/mastodon/locales/cs.json
  34. 668
      app/javascript/mastodon/locales/cy.json
  35. 1
      app/javascript/mastodon/locales/da.json
  36. 1
      app/javascript/mastodon/locales/de.json
  37. 6
      app/javascript/mastodon/locales/defaultMessages.json
  38. 1
      app/javascript/mastodon/locales/el.json
  39. 1
      app/javascript/mastodon/locales/en.json
  40. 1
      app/javascript/mastodon/locales/eo.json
  41. 1
      app/javascript/mastodon/locales/es.json
  42. 1
      app/javascript/mastodon/locales/eu.json
  43. 1
      app/javascript/mastodon/locales/fa.json
  44. 1
      app/javascript/mastodon/locales/fi.json
  45. 1
      app/javascript/mastodon/locales/fr.json
  46. 1
      app/javascript/mastodon/locales/gl.json
  47. 1
      app/javascript/mastodon/locales/he.json
  48. 1
      app/javascript/mastodon/locales/hr.json
  49. 1
      app/javascript/mastodon/locales/hu.json
  50. 1
      app/javascript/mastodon/locales/hy.json
  51. 1
      app/javascript/mastodon/locales/id.json
  52. 1
      app/javascript/mastodon/locales/io.json
  53. 23
      app/javascript/mastodon/locales/it.json
  54. 1
      app/javascript/mastodon/locales/ja.json
  55. 1
      app/javascript/mastodon/locales/ka.json
  56. 1
      app/javascript/mastodon/locales/ko.json
  57. 1
      app/javascript/mastodon/locales/nl.json
  58. 1
      app/javascript/mastodon/locales/no.json
  59. 1
      app/javascript/mastodon/locales/oc.json
  60. 1
      app/javascript/mastodon/locales/pl.json
  61. 1
      app/javascript/mastodon/locales/pt-BR.json
  62. 1
      app/javascript/mastodon/locales/pt.json
  63. 1
      app/javascript/mastodon/locales/ro.json
  64. 1
      app/javascript/mastodon/locales/ru.json
  65. 1
      app/javascript/mastodon/locales/sk.json
  66. 1
      app/javascript/mastodon/locales/sl.json
  67. 1
      app/javascript/mastodon/locales/sr-Latn.json
  68. 1
      app/javascript/mastodon/locales/sr.json
  69. 1
      app/javascript/mastodon/locales/sv.json
  70. 1
      app/javascript/mastodon/locales/ta.json
  71. 1
      app/javascript/mastodon/locales/te.json
  72. 1
      app/javascript/mastodon/locales/th.json
  73. 1
      app/javascript/mastodon/locales/tr.json
  74. 1
      app/javascript/mastodon/locales/uk.json
  75. 1
      app/javascript/mastodon/locales/zh-CN.json
  76. 1
      app/javascript/mastodon/locales/zh-HK.json
  77. 1
      app/javascript/mastodon/locales/zh-TW.json
  78. 2
      app/javascript/mastodon/service_worker/web_push_notifications.js
  79. 2
      app/javascript/styles/mailer.scss
  80. 46
      app/javascript/styles/mastodon/about.scss
  81. 2
      app/javascript/styles/mastodon/admin.scss
  82. 6
      app/javascript/styles/mastodon/basics.scss
  83. 33
      app/javascript/styles/mastodon/components.scss
  84. 4
      app/javascript/styles/mastodon/containers.scss
  85. 2
      app/javascript/styles/mastodon/dashboard.scss
  86. 8
      app/javascript/styles/mastodon/forms.scss
  87. 30
      app/javascript/styles/mastodon/rtl.scss
  88. 2
      app/javascript/styles/mastodon/tables.scss
  89. 4
      app/javascript/styles/mastodon/variables.scss
  90. 2
      app/lib/activitypub/activity/create.rb
  91. 1
      app/lib/feed_manager.rb
  92. 45
      app/lib/user_settings_decorator.rb
  93. 8
      app/models/concerns/paginable.rb
  94. 16
      app/models/feed.rb
  95. 8
      app/models/home_feed.rb
  96. 12
      app/models/user.rb
  97. 19
      app/serializers/initial_state_serializer.rb
  98. 2
      app/views/accounts/_header.html.haml
  99. 2
      app/views/accounts/_moved.html.haml
  100. 4
      app/views/admin/reports/_status.html.haml
  101. Some files were not shown because too many files have changed in this diff Show More

@ -12,7 +12,7 @@ gem 'thor', '~> 0.20'
gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.0'
gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.1'
gem 'pghero', '~> 2.2'
gem 'dotenv-rails', '~> 2.2', '< 2.3'
gem 'aws-sdk-s3', '~> 1.9', require: false
@ -24,13 +24,13 @@ gem 'streamio-ffmpeg', '~> 3.0'
gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.5'
gem 'bootsnap', '~> 1.3'
gem 'bootsnap', '~> 1.3', require: false
gem 'browser'
gem 'charlock_holmes', '~> 0.7.6'
gem 'iso-639'
gem 'chewy', '~> 5.0'
gem 'cld3', '~> 3.2.0'
gem 'devise', '~> 4.4'
gem 'devise', '~> 4.5'
gem 'devise-two-factor', '~> 3.0'
group :pam_authentication, optional: true do
@ -57,10 +57,10 @@ gem 'httplog', '~> 1.0'
gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.1'
gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.1', require: 'mime/types/columnar'
gem 'mime-types', '~> 3.2', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.8'
gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.5'
gem 'oj', '~> 3.6'
gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.9'
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
@ -75,8 +75,8 @@ gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 0.10'
gem 'ruby-progressbar', '~> 1.4'
gem 'sanitize', '~> 4.6'
gem 'sidekiq', '~> 5.1'
gem 'sidekiq-scheduler', '~> 2.2'
gem 'sidekiq', '~> 5.2'
gem 'sidekiq-scheduler', '~> 3.0'
gem 'sidekiq-unique-jobs', '~> 5.0'
gem 'sidekiq-bulk', '~>0.1.1'
gem 'simple-navigation', '~> 4.0'
@ -85,7 +85,7 @@ gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.1.3'
gem 'strong_migrations', '~> 0.2'
gem 'tty-command', '~> 0.8', require: false
gem 'tty-prompt', '~> 0.16', require: false
gem 'tty-prompt', '~> 0.17', require: false
gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2018'
gem 'webpacker', '~> 3.5'
@ -104,7 +104,7 @@ group :development, :test do
end
group :production, :test do
gem 'private_address_check', '~> 0.4.1'
gem 'private_address_check', '~> 0.5'
end
group :test do
@ -133,7 +133,7 @@ group :development do
gem 'bundler-audit', '~> 0.6', require: false
gem 'scss_lint', '~> 0.57', require: false
gem 'capistrano', '~> 3.10'
gem 'capistrano', '~> 3.11'
gem 'capistrano-rails', '~> 1.3'
gem 'capistrano-rbenv', '~> 2.1'
gem 'capistrano-yarn', '~> 2.0'

@ -96,7 +96,7 @@ GEM
rack (>= 0.9.0)
binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1)
bootsnap (1.3.0)
bootsnap (1.3.2)
msgpack (~> 1.0)
brakeman (4.2.1)
browser (2.5.3)
@ -108,7 +108,7 @@ GEM
bundler (~> 1.2)
thor (~> 0.18)
byebug (10.0.2)
capistrano (3.10.2)
capistrano (3.11.0)
airbrussh (>= 1.0.0)
i18n
rake (>= 10.0.0)
@ -147,7 +147,7 @@ GEM
coderay (1.1.2)
colorize (0.8.1)
concurrent-ruby (1.0.5)
connection_pool (2.2.1)
connection_pool (2.2.2)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.4)
@ -162,7 +162,7 @@ GEM
rack (>= 1)
rake (> 10, < 13)
thor (~> 0.19)
devise (4.4.3)
devise (4.5.0)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0, < 6.0)
@ -202,7 +202,7 @@ GEM
encryptor (3.0.0)
equatable (0.5.0)
erubi (1.7.1)
et-orbi (1.1.0)
et-orbi (1.1.6)
tzinfo
excon (0.62.0)
fabrication (2.20.1)
@ -212,7 +212,7 @@ GEM
multipart-post (>= 1.2, < 3)
fast_blank (1.0.0)
fastimage (2.1.1)
ffi (1.9.23)
ffi (1.9.25)
fog-core (1.45.0)
builder
excon (~> 0.58)
@ -225,6 +225,9 @@ GEM
fog-json (>= 1.0)
ipaddress (>= 0.8)
formatador (0.2.5)
fugit (1.1.6)
et-orbi (~> 1.1, >= 1.1.6)
raabro (~> 1.1)
fuubar (2.3.1)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
@ -252,7 +255,7 @@ GEM
heapy (0.1.3)
highline (1.7.10)
hiredis (0.6.1)
hitimes (1.2.6)
hitimes (1.3.0)
hkdf (0.3.0)
html2text (0.2.1)
nokogiri (~> 1.6)
@ -328,14 +331,14 @@ GEM
mimemagic (~> 0.3.2)
mario-redis-lock (1.2.1)
redis (>= 3.0.5)
memory_profiler (0.9.10)
memory_profiler (0.9.11)
method_source (0.9.0)
microformats (4.0.7)
json
nokogiri
mime-types (3.1)
mime-types (3.2.2)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mime-types-data (3.2018.0812)
mimemagic (0.3.2)
mini_mime (1.0.0)
mini_portile2 (2.3.0)
@ -347,7 +350,7 @@ GEM
net-ldap (0.16.1)
net-scp (1.2.1)
net-ssh (>= 2.6.5)
net-ssh (4.2.0)
net-ssh (5.0.2)
nio4r (2.3.1)
nokogiri (1.8.4)
mini_portile2 (~> 2.3.0)
@ -358,7 +361,7 @@ GEM
concurrent-ruby (~> 1.0.0)
sidekiq (>= 3.5.0)
statsd-ruby (~> 1.2.0)
oj (3.5.1)
oj (3.6.11)
omniauth (1.8.1)
hashie (>= 3.4.6, < 3.6.0)
rack (>= 1.6.2, < 3)
@ -393,9 +396,9 @@ GEM
equatable (~> 0.5.0)
tty-color (~> 0.4.0)
pg (1.0.0)
pghero (2.1.0)
pghero (2.2.0)
activerecord
pkg-config (1.3.0)
pkg-config (1.3.1)
powerpack (0.1.1)
premailer (1.11.1)
addressable
@ -404,7 +407,7 @@ GEM
premailer-rails (1.10.2)
actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9)
private_address_check (0.4.1)
private_address_check (0.5.0)
pry (0.11.3)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
@ -417,11 +420,12 @@ GEM
puma (3.11.4)
pundit (1.1.0)
activesupport (>= 3.0.0)
raabro (1.1.6)
rack (2.0.5)
rack-attack (5.2.0)
rack
rack-cors (1.0.2)
rack-protection (2.0.1)
rack-protection (2.0.4)
rack
rack-proxy (0.6.4)
rack
@ -528,10 +532,10 @@ GEM
ruby-progressbar (1.9.0)
ruby-saml (1.7.2)
nokogiri (>= 1.5.10)
rufus-scheduler (3.4.2)
et-orbi (~> 1.0)
rufus-scheduler (3.5.2)
fugit (~> 1.1, >= 1.1.5)
safe_yaml (1.0.4)
sanitize (4.6.4)
sanitize (4.6.6)
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4)
@ -543,15 +547,14 @@ GEM
scss_lint (0.57.0)
rake (>= 0.9, < 13)
sass (~> 3.5.5)
sidekiq (5.1.3)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
sidekiq (5.2.2)
connection_pool (~> 2.2, >= 2.2.2)
rack-protection (>= 1.5.0)
redis (>= 3.3.5, < 5)
sidekiq-bulk (0.1.1)
activesupport
sidekiq
sidekiq-scheduler (2.2.1)
sidekiq-scheduler (3.0.0)
redis (>= 3, < 5)
rufus-scheduler (~> 3.2)
sidekiq (>= 3)
@ -561,9 +564,9 @@ GEM
thor (~> 0)
simple-navigation (4.0.5)
activesupport (>= 2.3.2)
simple_form (4.0.0)
actionpack (> 4)
activemodel (> 4)
simple_form (4.0.1)
actionpack (>= 5.0)
activemodel (>= 5.0)
simplecov (0.16.1)
docile (~> 1.1)
json (>= 1.8, < 3)
@ -576,15 +579,15 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sshkit (1.16.0)
sshkit (1.17.0)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
stackprof (0.2.11)
stackprof (0.2.12)
statsd-ruby (1.2.1)
stoplight (2.1.3)
streamio-ffmpeg (3.0.2)
multi_json (~> 1.8)
strong_migrations (0.2.2)
strong_migrations (0.2.3)
activerecord (>= 3.2.0)
temple (0.8.0)
terminal-table (1.8.0)
@ -597,26 +600,26 @@ GEM
tilt (2.0.8)
timers (4.1.2)
hitimes
tty-color (0.4.2)
tty-command (0.8.0)
tty-color (0.4.3)
tty-command (0.8.2)
pastel (~> 0.7.0)
tty-cursor (0.5.0)
tty-prompt (0.16.0)
tty-cursor (0.6.0)
tty-prompt (0.17.0)
necromancer (~> 0.4.0)
pastel (~> 0.7.0)
timers (~> 4.0)
tty-cursor (~> 0.5.0)
tty-reader (~> 0.2.0)
tty-reader (0.2.0)
tty-cursor (~> 0.5.0)
tty-cursor (~> 0.6.0)
tty-reader (~> 0.4.0)
tty-reader (0.4.0)
tty-cursor (~> 0.6.0)
tty-screen (~> 0.6.4)
wisper (~> 2.0.0)
tty-screen (0.6.4)
tty-screen (0.6.5)
twitter-text (1.14.7)
unf (~> 0.1.0)
tzinfo (1.2.5)
thread_safe (~> 0.1)
tzinfo-data (1.2018.4)
tzinfo-data (1.2018.5)
tzinfo (>= 1.0.0)
unf (0.1.4)
unf_ext
@ -659,7 +662,7 @@ DEPENDENCIES
browser
bullet (~> 5.7)
bundler-audit (~> 0.6)
capistrano (~> 3.10)
capistrano (~> 3.11)
capistrano-rails (~> 1.3)
capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0)
@ -669,7 +672,7 @@ DEPENDENCIES
cld3 (~> 3.2.0)
climate_control (~> 0.2)
derailed_benchmarks
devise (~> 4.4)
devise (~> 4.5)
devise-two-factor (~> 3.0)
devise_pam_authenticatable2 (~> 9.2)
doorkeeper (~> 5.0)
@ -703,11 +706,11 @@ DEPENDENCIES
mario-redis-lock (~> 1.2)
memory_profiler
microformats (~> 4.0)
mime-types (~> 3.1)
mime-types (~> 3.2)
net-ldap (~> 0.10)
nokogiri (~> 1.8)
nsa (~> 0.2)
oj (~> 3.5)
oj (~> 3.6)
omniauth (~> 1.2)
omniauth-cas (~> 1.1)
omniauth-saml (~> 1.10)
@ -717,11 +720,11 @@ DEPENDENCIES
paperclip-av-transcoder (~> 0.6)
parallel_tests (~> 2.21)
pg (~> 1.0)
pghero (~> 2.1)
pghero (~> 2.2)
pkg-config (~> 1.3)
posix-spawn!
premailer-rails
private_address_check (~> 0.4.1)
private_address_check (~> 0.5)
pry-byebug (~> 3.6)
pry-rails (~> 0.3)
puma (~> 3.11)
@ -743,9 +746,9 @@ DEPENDENCIES
ruby-progressbar (~> 1.4)
sanitize (~> 4.6)
scss_lint (~> 0.57)
sidekiq (~> 5.1)
sidekiq (~> 5.2)
sidekiq-bulk (~> 0.1.1)
sidekiq-scheduler (~> 2.2)
sidekiq-scheduler (~> 3.0)
sidekiq-unique-jobs (~> 5.0)
simple-navigation (~> 4.0)
simple_form (~> 4.0)
@ -757,7 +760,7 @@ DEPENDENCIES
strong_migrations (~> 0.2)
thor (~> 0.20)
tty-command (~> 0.8)
tty-prompt (~> 0.16)
tty-prompt (~> 0.17)
twitter-text (~> 1.14)
tzinfo-data (~> 1.2018)
webmock (~> 3.3)

@ -53,6 +53,10 @@ class Api::BaseController < ApplicationController
[params[:limit].to_i.abs, default_limit * 2].min
end
def params_slice(*keys)
params.slice(*keys).permit(*keys)
end
def current_resource_owner
@current_user ||= User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
end

@ -28,10 +28,9 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
def account_statuses
statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
statuses = statuses.paginate_by_max_id(
statuses = statuses.paginate_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
params_slice(:max_id, :since_id, :min_id)
)
statuses.merge!(only_media_scope) if truthy_param?(:only_media)
@ -82,7 +81,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
def prev_path
unless @statuses.empty?
api_v1_account_statuses_url pagination_params(since_id: pagination_since_id)
api_v1_account_statuses_url pagination_params(min_id: pagination_since_id)
end
end

@ -26,10 +26,9 @@ class Api::V1::FavouritesController < Api::BaseController
end
def results
@_results ||= account_favourites.paginate_by_max_id(
@_results ||= account_favourites.paginate_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
params_slice(:max_id, :since_id, :min_id)
)
end
@ -49,7 +48,7 @@ class Api::V1::FavouritesController < Api::BaseController
def prev_path
unless results.empty?
api_v1_favourites_url pagination_params(since_id: pagination_since_id)
api_v1_favourites_url pagination_params(min_id: pagination_since_id)
end
end

@ -4,6 +4,8 @@ class Api::V1::InstancesController < Api::BaseController
respond_to :json
def show
render json: {}, serializer: REST::InstanceSerializer
render_cached_json('api:v1:instances', expires_in: 5.minutes) do
ActiveModelSerializers::SerializableResource.new({}, serializer: REST::InstanceSerializer)
end
end
end

@ -46,10 +46,9 @@ class Api::V1::NotificationsController < Api::BaseController
end
def paginated_notifications
browserable_account_notifications.paginate_by_max_id(
browserable_account_notifications.paginate_by_id(
limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
params[:max_id],
params[:since_id]
params_slice(:max_id, :since_id, :min_id)
)
end
@ -73,7 +72,7 @@ class Api::V1::NotificationsController < Api::BaseController
def prev_path
unless @notifications.empty?
api_v1_notifications_url pagination_params(since_id: pagination_since_id)
api_v1_notifications_url pagination_params(min_id: pagination_since_id)
end
end

@ -7,11 +7,6 @@ class Api::V1::ReportsController < Api::BaseController
respond_to :json
def index
@reports = current_account.reports
render json: @reports, each_serializer: REST::ReportSerializer
end
def create
@report = ReportService.new.call(
current_account,

@ -30,7 +30,8 @@ class Api::V1::Timelines::HomeController < Api::BaseController
account_home_feed.get(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
params[:since_id],
params[:min_id]
)
end
@ -51,7 +52,7 @@ class Api::V1::Timelines::HomeController < Api::BaseController
end
def prev_path
api_v1_timelines_home_url pagination_params(since_id: pagination_since_id)
api_v1_timelines_home_url pagination_params(min_id: pagination_since_id)
end
def pagination_max_id

@ -32,7 +32,8 @@ class Api::V1::Timelines::ListController < Api::BaseController
list_feed.get(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
params[:since_id],
params[:min_id]
)
end
@ -53,7 +54,7 @@ class Api::V1::Timelines::ListController < Api::BaseController
end
def prev_path
api_v1_timelines_list_url params[:id], pagination_params(since_id: pagination_since_id)
api_v1_timelines_list_url params[:id], pagination_params(min_id: pagination_since_id)
end
def pagination_max_id

@ -21,10 +21,9 @@ class Api::V1::Timelines::PublicController < Api::BaseController
end
def public_statuses
statuses = public_timeline_statuses.paginate_by_max_id(
statuses = public_timeline_statuses.paginate_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
params_slice(:max_id, :since_id, :min_id)
)
if truthy_param?(:only_media)
@ -53,7 +52,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
end
def prev_path
api_v1_timelines_public_url pagination_params(since_id: pagination_since_id)
api_v1_timelines_public_url pagination_params(min_id: pagination_since_id)
end
def pagination_max_id

@ -29,10 +29,9 @@ class Api::V1::Timelines::TagController < Api::BaseController
if @tag.nil?
[]
else
statuses = tag_timeline_statuses.paginate_by_max_id(
statuses = tag_timeline_statuses.paginate_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
params_slice(:max_id, :since_id, :min_id)
)
if truthy_param?(:only_media)
@ -62,7 +61,7 @@ class Api::V1::Timelines::TagController < Api::BaseController
end
def prev_path
api_v1_timelines_tag_url params[:id], pagination_params(since_id: pagination_since_id)
api_v1_timelines_tag_url params[:id], pagination_params(min_id: pagination_since_id)
end
def pagination_max_id

@ -37,7 +37,8 @@ class Settings::PreferencesController < Settings::BaseController
:setting_favourite_modal,
:setting_delete_modal,
:setting_auto_play_gif,
:setting_display_sensitive_media,
:setting_display_media,
:setting_expand_spoilers,
:setting_reduce_motion,
:setting_system_font_ui,
:setting_noindex,

@ -7,8 +7,8 @@ module ApplicationHelper
follow
).freeze
def active_nav_class(path)
current_page?(path) ? 'active' : ''
def active_nav_class(*paths)
paths.any? { |path| current_page?(path) } ? 'active' : ''
end
def active_link_to(label, path, **options)

@ -1,6 +1,7 @@
import escapeTextContentForBrowser from 'escape-html';
import emojify from '../../features/emoji/emoji';
import { unescapeHTML } from '../../utils/html';
import { expandSpoilers } from '../../initial_state';
const domParser = new DOMParser();
@ -57,7 +58,7 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = spoilerText.length > 0 || normalStatus.sensitive;
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
}
return normalStatus;

@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AutosuggestEmoji /> renders emoji with custom url 1`] = `
<div
className="autosuggest-emoji"
>
<img
alt="foobar"
className="emojione"
src="http://example.com/emoji.png"
/>
:foobar:
</div>
`;
exports[`<AutosuggestEmoji /> renders native emoji 1`] = `
<div
className="autosuggest-emoji"
>
<img
alt="💙"
className="emojione"
src="/emoji/1f499.svg"
/>
:foobar:
</div>
`;

@ -0,0 +1,29 @@
import React from 'react';
import renderer from 'react-test-renderer';
import AutosuggestEmoji from '../autosuggest_emoji';
describe('<AutosuggestEmoji />', () => {
it('renders native emoji', () => {
const emoji = {
native: '💙',
colons: ':foobar:',
};
const component = renderer.create(<AutosuggestEmoji emoji={emoji} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders emoji with custom url', () => {
const emoji = {
custom: true,
imageUrl: 'http://example.com/emoji.png',
native: 'foobar',
colons: ':foobar:',
};
const component = renderer.create(<AutosuggestEmoji emoji={emoji} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});

@ -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, displaySensitiveMedia } from '../initial_state';
import { autoPlayGif, displayMedia } from '../initial_state';
const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
@ -197,7 +197,7 @@ class MediaGallery extends React.PureComponent {
};
state = {
visible: !this.props.sensitive || displaySensitiveMedia,
visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all',
};
componentWillReceiveProps (nextProps) {

@ -285,7 +285,7 @@ class Status extends ImmutablePureComponent {
</a>
</div>
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} collapsable />
{media}

@ -6,6 +6,8 @@ import { FormattedMessage } from 'react-intl';
import Permalink from './permalink';
import classnames from 'classnames';
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
export default class StatusContent extends React.PureComponent {
static contextTypes = {
@ -17,10 +19,12 @@ export default class StatusContent extends React.PureComponent {
expanded: PropTypes.bool,
onExpandedToggle: PropTypes.func,
onClick: PropTypes.func,
collapsable: PropTypes.bool,
};
state = {
hidden: true,
collapsed: null, // `collapsed: null` indicates that an element doesn't need collapsing, while `true` or `false` indicates that it does (and is/isn't).
};
_updateStatusLinks () {
@ -53,6 +57,16 @@ export default class StatusContent extends React.PureComponent {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener');
}
if (
this.props.collapsable
&& this.props.onClick
&& this.state.collapsed === null
&& node.clientHeight > MAX_HEIGHT
&& this.props.status.get('spoiler_text').length === 0
) {
this.setState({ collapsed: true });
}
}
componentDidMount () {
@ -113,6 +127,11 @@ export default class StatusContent extends React.PureComponent {
}
}
handleCollapsedClick = (e) => {
e.preventDefault();
this.setState({ collapsed: !this.state.collapsed });
}
setRef = (c) => {
this.node = c;
}
@ -132,12 +151,19 @@ export default class StatusContent extends React.PureComponent {
const classNames = classnames('status__content', {
'status__content--with-action': this.props.onClick && this.context.router,
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
'status__content--collapsed': this.state.collapsed === true,
});
if (isRtl(status.get('search_index'))) {
directionStyle.direction = 'rtl';
}
const readMoreButton = (
<button className='status__content__read-more-button' onClick={this.props.onClick}>
<FormattedMessage id='status.read_more' defaultMessage='Read more' /><i className='fa fa-fw fa-angle-right' />
</button>
);
if (status.get('spoiler_text').length > 0) {
let mentionsPlaceholder = '';
@ -167,17 +193,23 @@ export default class StatusContent extends React.PureComponent {
</div>
);
} else if (this.props.onClick) {
return (
const output = [
<div
ref={this.setRef}
tabIndex='0'
className={classNames}
style={directionStyle}
dangerouslySetInnerHTML={content}
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
dangerouslySetInnerHTML={content}
/>
);
/>,
];
if (this.state.collapsed) {
output.push(readMoreButton);
}
return output;
} else {
return (
<div

@ -2,7 +2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Permalink from '../../../components/permalink';
import { displaySensitiveMedia } from '../../../initial_state';
import { displayMedia } from '../../../initial_state';
export default class MediaItem extends ImmutablePureComponent {
@ -11,7 +11,7 @@ export default class MediaItem extends ImmutablePureComponent {
};
state = {
visible: !this.props.media.getIn(['status', 'sensitive']) || displaySensitiveMedia,
visible: displayMedia !== 'hide_all' && !this.props.media.getIn(['status', 'sensitive']) || displayMedia === 'show_all',
};
handleClick = () => {

@ -27,7 +27,6 @@ export default class ColumnSettings extends React.PureComponent {
const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
const pushMeta = showPushSettings && <FormattedMessage id='notifications.column_settings.push_meta' defaultMessage='This device' />;
return (
<div>
@ -40,7 +39,7 @@ export default class ColumnSettings extends React.PureComponent {
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} />
</div>
@ -51,7 +50,7 @@ export default class ColumnSettings extends React.PureComponent {
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'favourite']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'favourite']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'favourite']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
</div>
@ -62,7 +61,7 @@ export default class ColumnSettings extends React.PureComponent {
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'mention']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'mention']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'mention']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'mention']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'mention']} onChange={onChange} label={soundStr} />
</div>
@ -73,7 +72,7 @@ export default class ColumnSettings extends React.PureComponent {
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'reblog']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'reblog']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
</div>

@ -85,8 +85,9 @@ class Notification extends ImmutablePureComponent {
<div className='notification__favourite-icon-wrapper'>
<i className='fa fa-fw fa-user-plus' />
</div>
<FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} />
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} />
</span>
</div>
<AccountContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} />

@ -10,7 +10,6 @@ export default class SettingToggle extends React.PureComponent {
settings: ImmutablePropTypes.map.isRequired,
settingPath: PropTypes.array.isRequired,
label: PropTypes.node.isRequired,
meta: PropTypes.node,
onChange: PropTypes.func.isRequired,
}
@ -19,14 +18,13 @@ export default class SettingToggle extends React.PureComponent {
}
render () {
const { prefix, settings, settingPath, label, meta } = this.props;
const { prefix, settings, settingPath, label } = this.props;
const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-');
return (
<div className='setting-toggle'>
<Toggle id={id} checked={settings.getIn(settingPath)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
<label htmlFor={id} className='setting-toggle__label'>{label}</label>
{meta && <span className='setting-meta__label'>{meta}</span>}
</div>
);
}

@ -59,7 +59,8 @@ const messages = defineMessages({
const mapStateToProps = state => ({
isComposing: state.getIn(['compose', 'is_composing']),
hasComposingText: state.getIn(['compose', 'text']) !== '',
hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0,
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
});
@ -201,6 +202,7 @@ class UI extends React.PureComponent {
children: PropTypes.node,
isComposing: PropTypes.bool,
hasComposingText: PropTypes.bool,
hasMediaAttachments: PropTypes.bool,
location: PropTypes.object,
intl: PropTypes.object.isRequired,
dropdownMenuIsOpen: PropTypes.bool,
@ -211,9 +213,9 @@ class UI extends React.PureComponent {
};
handleBeforeUnload = (e) => {
const { intl, isComposing, hasComposingText } = this.props;
const { intl, isComposing, hasComposingText, hasMediaAttachments } = this.props;
if (isComposing && hasComposingText) {
if (isComposing && (hasComposingText || hasMediaAttachments)) {
// Setting returnValue to any string causes confirmation dialog.
// Many browsers no longer display this text to users,
// but we set user-friendly message for other browsers, e.g. Edge.

@ -5,7 +5,7 @@ import { fromJS } from 'immutable';
import { throttle } from 'lodash';
import classNames from 'classnames';
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
import { displaySensitiveMedia } from '../../initial_state';
import { displayMedia } from '../../initial_state';
const messages = defineMessages({
play: { id: 'video.play', defaultMessage: 'Play' },
@ -111,7 +111,7 @@ class Video extends React.PureComponent {
fullscreen: false,
hovered: false,
muted: false,
revealed: !this.props.sensitive || displaySensitiveMedia,
revealed: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all',
};
setPlayerRef = c => {
@ -272,7 +272,7 @@ class Video extends React.PureComponent {
}
render () {
const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed } = this.props;
const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive } = this.props;
const { containerWidth, currentTime, duration, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
const progress = (currentTime / duration) * 100;
const playerStyle = {};
@ -296,6 +296,13 @@ class Video extends React.PureComponent {
preload = 'none';
}
let warning;
if (sensitive) {
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
} else {
warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
}
return (
<div
role='menuitem'
@ -328,7 +335,7 @@ class Video extends React.PureComponent {
/>
<button type='button' className={classNames('video-player__spoiler', { active: !revealed })} onClick={this.toggleReveal}>
<span className='video-player__spoiler__title'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
<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>

@ -5,7 +5,8 @@ const getMeta = (prop) => initialState && initialState.meta && initialState.meta
export const reduceMotion = getMeta('reduce_motion');
export const autoPlayGif = getMeta('auto_play_gif');
export const displaySensitiveMedia = getMeta('display_sensitive_media');
export const displayMedia = getMeta('display_media');
export const expandSpoilers = getMeta('expand_spoilers');
export const unfollowModal = getMeta('unfollow_modal');
export const boostModal = getMeta('boost_modal');
export const deleteModal = getMeta('delete_modal');

@ -216,7 +216,6 @@
"notifications.column_settings.follow": "متابعُون جُدُد :",
"notifications.column_settings.mention": "الإشارات :",
"notifications.column_settings.push": "الإخطارات المدفوعة",
"notifications.column_settings.push_meta": "هذا الجهاز",
"notifications.column_settings.reblog": "الترقيّات:",
"notifications.column_settings.show": "إعرِضها في عمود",
"notifications.column_settings.sound": "أصدر صوتا",

@ -216,7 +216,6 @@
"notifications.column_settings.follow": "Siguidores nuevos:",
"notifications.column_settings.mention": "Menciones:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "Esti preséu",
"notifications.column_settings.reblog": "Boosts:",
"notifications.column_settings.show": "Amosar en columna",
"notifications.column_settings.sound": "Reproducir soníu",

@ -216,7 +216,6 @@
"notifications.column_settings.follow": "Нови последователи:",
"notifications.column_settings.mention": "Споменавания:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Споделяния:",
"notifications.column_settings.show": "Покажи в колона",
"notifications.column_settings.sound": "Play sound",

@ -216,7 +216,6 @@
"notifications.column_settings.follow": "Nous seguidors:",
"notifications.column_settings.mention": "Mencions:",
"notifications.column_settings.push": "Push notificacions",
"notifications.column_settings.push_meta": "Aquest dispositiu",
"notifications.column_settings.reblog": "Impulsos:",
"notifications.column_settings.show": "Mostrar en la columna",
"notifications.column_settings.sound": "Reproduïr so",

@ -216,7 +216,6 @@
"notifications.column_settings.follow": "Abbunati novi:",
"notifications.column_settings.mention": "Minzione:",
"notifications.column_settings.push": "Nutificazione Push",
"notifications.column_settings.push_meta": "Quess'apparechju",
"notifications.column_settings.reblog": "Spartere:",
"notifications.column_settings.show": "Mustrà indè a colonna",
"notifications.column_settings.sound": "Sunà",

@ -216,7 +216,6 @@
"notifications.column_settings.follow": "Noví sledovatelé:",
"notifications.column_settings.mention": "Zmínky:",
"notifications.column_settings.push": "Push oznámení",
"notifications.column_settings.push_meta": "Toto zařízení",
"notifications.column_settings.reblog": "Boosty:",
"notifications.column_settings.show": "Zobrazit ve sloupci",
"notifications.column_settings.sound": "Přehrát zvuk",

@ -1,337 +1,335 @@
{
"account.badges.bot": "Bot",
"account.block": "Blociwch @{name}",
"account.block_domain": "Cuddiwch bopeth rhag {domain}",
"account.blocked": "Blociwyd",
"account.direct": "Neges breifat @{name}",
"account.disclaimer_full": "Gall y wybodaeth isod adlewyrchu darlun anghyflawn o broffil defnyddiwr.",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Golygu proffil",
"account.endorse": "Feature on profile",
"account.follow": "Dilyn",
"account.followers": "Dilynwyr",
"account.followers.empty": "Nid oes neb yn dilyn y defnyddiwr hwn eto.",
"account.follows": "Yn dilyn",
"account.follows.empty": "Nid yw'r defnyddiwr hwn yn dilyn unrhyw un eto.",
"account.follows_you": "Yn eich dilyn chi",
"account.hide_reblogs": "Hide boosts from @{name}",
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.media": "Cyfryngau",
"account.mention": "Crybwyll @{name}",
"account.moved_to": "Mae @{name} wedi symud i:",
"account.mute": "Mute @{name}",
"account.mute_notifications": "Mute notifications from @{name}",
"account.muted": "Distewyd",
"account.posts": "Tŵtiau",
"account.posts_with_replies": "Toots and replies",
"account.report": "Adroddwch @{name}",
"account.requested": "Awaiting approval. Click to cancel follow request",</