Browse Source

Merge branch 'master' of https://github.com/tootsuite/mastodon

Signed-off-by: Baptiste Lemoine <contact@cipherbliss.com>

# Conflicts:
#	app/javascript/mastodon/components/status_action_bar.js
#	app/javascript/mastodon/features/status/components/action_bar.js
feature/markdown-support
Baptiste Lemoine 3 years ago
parent
commit
b872c1c7c0
  1. 21
      CHANGELOG.md
  2. 12
      Gemfile
  3. 38
      Gemfile.lock
  4. 6
      app/controllers/api/v1/announcements_controller.rb
  5. 16
      app/controllers/api/web/embeds_controller.rb
  6. 2
      app/controllers/concerns/signature_verification.rb
  7. 10
      app/controllers/tags_controller.rb
  8. 2
      app/helpers/accounts_helper.rb
  9. 42
      app/javascript/mastodon/actions/announcements.js
  10. 87
      app/javascript/mastodon/features/getting_started/components/announcements.js
  11. 7
      app/javascript/mastodon/features/getting_started/containers/announcements_container.js
  12. 30
      app/javascript/mastodon/features/home_timeline/index.js
  13. 2
      app/javascript/mastodon/features/status/components/action_bar.js
  14. 174
      app/javascript/mastodon/features/video/index.js
  15. 11
      app/javascript/mastodon/locales/ar.json
  16. 153
      app/javascript/mastodon/locales/ast.json
  17. 3
      app/javascript/mastodon/locales/bg.json
  18. 3
      app/javascript/mastodon/locales/bn.json
  19. 3
      app/javascript/mastodon/locales/br.json
  20. 7
      app/javascript/mastodon/locales/ca.json
  21. 3
      app/javascript/mastodon/locales/co.json
  22. 5
      app/javascript/mastodon/locales/cs.json
  23. 5
      app/javascript/mastodon/locales/cy.json
  24. 3
      app/javascript/mastodon/locales/da.json
  25. 5
      app/javascript/mastodon/locales/de.json
  26. 3
      app/javascript/mastodon/locales/el.json
  27. 7
      app/javascript/mastodon/locales/eo.json
  28. 3
      app/javascript/mastodon/locales/es-AR.json
  29. 3
      app/javascript/mastodon/locales/es.json
  30. 5
      app/javascript/mastodon/locales/et.json
  31. 3
      app/javascript/mastodon/locales/eu.json
  32. 101
      app/javascript/mastodon/locales/fa.json
  33. 9
      app/javascript/mastodon/locales/fi.json
  34. 3
      app/javascript/mastodon/locales/fr.json
  35. 3
      app/javascript/mastodon/locales/ga.json
  36. 27
      app/javascript/mastodon/locales/gl.json
  37. 3
      app/javascript/mastodon/locales/he.json
  38. 3
      app/javascript/mastodon/locales/hi.json
  39. 3
      app/javascript/mastodon/locales/hr.json
  40. 3
      app/javascript/mastodon/locales/hu.json
  41. 3
      app/javascript/mastodon/locales/hy.json
  42. 5
      app/javascript/mastodon/locales/id.json
  43. 3
      app/javascript/mastodon/locales/io.json
  44. 5
      app/javascript/mastodon/locales/is.json
  45. 5
      app/javascript/mastodon/locales/it.json
  46. 3
      app/javascript/mastodon/locales/ja.json
  47. 3
      app/javascript/mastodon/locales/ka.json
  48. 101
      app/javascript/mastodon/locales/kab.json
  49. 3
      app/javascript/mastodon/locales/kk.json
  50. 3
      app/javascript/mastodon/locales/kn.json
  51. 5
      app/javascript/mastodon/locales/ko.json
  52. 3
      app/javascript/mastodon/locales/lt.json
  53. 3
      app/javascript/mastodon/locales/lv.json
  54. 3
      app/javascript/mastodon/locales/mk.json
  55. 3
      app/javascript/mastodon/locales/ml.json
  56. 3
      app/javascript/mastodon/locales/mr.json
  57. 3
      app/javascript/mastodon/locales/ms.json
  58. 15
      app/javascript/mastodon/locales/nl.json
  59. 3
      app/javascript/mastodon/locales/nn.json
  60. 3
      app/javascript/mastodon/locales/no.json
  61. 19
      app/javascript/mastodon/locales/oc.json
  62. 15
      app/javascript/mastodon/locales/pl.json
  63. 5
      app/javascript/mastodon/locales/pt-BR.json
  64. 5
      app/javascript/mastodon/locales/pt-PT.json
  65. 3
      app/javascript/mastodon/locales/ro.json
  66. 7
      app/javascript/mastodon/locales/ru.json
  67. 3
      app/javascript/mastodon/locales/sk.json
  68. 3
      app/javascript/mastodon/locales/sl.json
  69. 3
      app/javascript/mastodon/locales/sq.json
  70. 3
      app/javascript/mastodon/locales/sr-Latn.json
  71. 3
      app/javascript/mastodon/locales/sr.json
  72. 3
      app/javascript/mastodon/locales/sv.json
  73. 123
      app/javascript/mastodon/locales/ta.json
  74. 3
      app/javascript/mastodon/locales/te.json
  75. 3
      app/javascript/mastodon/locales/th.json
  76. 7
      app/javascript/mastodon/locales/tr.json
  77. 3
      app/javascript/mastodon/locales/uk.json
  78. 3
      app/javascript/mastodon/locales/ur.json
  79. 3
      app/javascript/mastodon/locales/vi.json
  80. 15
      app/javascript/mastodon/locales/zh-CN.json
  81. 3
      app/javascript/mastodon/locales/zh-HK.json
  82. 3
      app/javascript/mastodon/locales/zh-TW.json
  83. 38
      app/javascript/mastodon/reducers/announcements.js
  84. 4
      app/javascript/packs/public.js
  85. 61
      app/javascript/styles/bliss.scss
  86. 45
      app/javascript/styles/bliss/variables.scss
  87. 2
      app/javascript/styles/mastodon-light.scss
  88. 65
      app/javascript/styles/mastodon-light/diff.scss
  89. 11
      app/javascript/styles/mastodon/components.scss
  90. 2
      app/lib/formatter.rb
  91. 45
      app/lib/sanitize_config.rb
  92. 1
      app/mailers/user_mailer.rb
  93. 3
      app/models/account.rb
  94. 8
      app/models/concerns/account_finder_concern.rb
  95. 2
      app/models/concerns/remotable.rb
  96. 14
      app/serializers/rest/announcement_serializer.rb
  97. 2
      app/serializers/rest/status_serializer.rb
  98. 3
      app/validators/unique_username_validator.rb
  99. 2
      app/views/accounts/_og.html.haml
  100. 2
      app/views/accounts/show.html.haml
  101. Some files were not shown because too many files have changed in this diff Show More

21
CHANGELOG.md

@ -3,7 +3,12 @@ Changelog
All notable changes to this project will be documented in this file.
## Unreleased
## [3.1.1] - 2020-02-10
### Fixed
- Fix yanked dependency preventing installation ([mayaeh](https://github.com/tootsuite/mastodon/pull/13059))
## [3.1.0] - 2020-02-09
### Added
- Add bookmarks ([ThibG](https://github.com/tootsuite/mastodon/pull/7107), [Gargron](https://github.com/tootsuite/mastodon/pull/12494), [Gomasy](https://github.com/tootsuite/mastodon/pull/12381))
@ -38,8 +43,9 @@ All notable changes to this project will be documented in this file.
- Add support for KaiOS arrow navigation to public pages ([nolanlawson](https://github.com/tootsuite/mastodon/pull/12251))
- Add `discoverable` to accounts in REST API ([trwnh](https://github.com/tootsuite/mastodon/pull/12508))
- Add admin setting to disable default follows ([ArisuOngaku](https://github.com/tootsuite/mastodon/pull/12566))
- Add support for LDAP and PAM in the OAuth password grant strategy ([ntl-purism](https://github.com/tootsuite/mastodon/pull/12390))
- Add support for LDAP and PAM in the OAuth password grant strategy ([ntl-purism](https://github.com/tootsuite/mastodon/pull/12390), [Gargron](https://github.com/tootsuite/mastodon/pull/12743))
- Allow support for `Accept`/`Reject` activities with a non-embedded object ([puckipedia](https://github.com/tootsuite/mastodon/pull/12199))
- Add "Show thread" button to public profiles ([Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/13000))
### Changed
@ -65,6 +71,7 @@ All notable changes to this project will be documented in this file.
- Change to fallback to to `Create` audience when `object` has no defined audience ([ThibG](https://github.com/tootsuite/mastodon/pull/12249))
- Change Twemoji library to 12.1.3 in web UI ([koyuawsmbrtn](https://github.com/tootsuite/mastodon/pull/12342))
- Change blocked users to be hidden from following/followers lists ([ThibG](https://github.com/tootsuite/mastodon/pull/12733))
- Change signature verification to ignore signatures with invalid host ([Gargron](https://github.com/tootsuite/mastodon/pull/13033))
### Removed
@ -92,14 +99,13 @@ All notable changes to this project will be documented in this file.
- Fix old migrations failing because of strong migrations update ([ThibG](https://github.com/tootsuite/mastodon/pull/12787), [ThibG](https://github.com/tootsuite/mastodon/pull/12692))
- Fix reuse of detailed status components in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12792))
- Fix base64-encoded file uploads not being possible in REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/12748), [Gargron](https://github.com/tootsuite/mastodon/pull/12857))
- Fix resource_owner_from_credentials in Doorkeeper initializer ([Gargron](https://github.com/tootsuite/mastodon/pull/12743))
- Fix error due to missing authentication call in filters controller ([Gargron](https://github.com/tootsuite/mastodon/pull/12746))
- Fix uncaught unknown format error in host meta controller ([Gargron](https://github.com/tootsuite/mastodon/pull/12747))
- Fix URL search not returning private toots user has access to ([ThibG](https://github.com/tootsuite/mastodon/pull/12742), [ThibG](https://github.com/tootsuite/mastodon/pull/12336))
- Fix cache digesting log noise on status embeds ([Gargron](https://github.com/tootsuite/mastodon/pull/12750))
- Fix slowness due to layout thrashing when reloading a large set of statuses in web UI ([panarom](https://github.com/tootsuite/mastodon/pull/12661), [panarom](https://github.com/tootsuite/mastodon/pull/12744), [Gargron](https://github.com/tootsuite/mastodon/pull/12712))
- Fix error when fetching followers/following from REST API when user has network hidden ([Gargron](https://github.com/tootsuite/mastodon/pull/12716))
- Fix IDN mentions not being processed, IDN domains not being rendered ([Gargron](https://github.com/tootsuite/mastodon/pull/12715))
- Fix IDN mentions not being processed, IDN domains not being rendered ([Gargron](https://github.com/tootsuite/mastodon/pull/12715), [Gargron](https://github.com/tootsuite/mastodon/pull/13035), [Gargron](https://github.com/tootsuite/mastodon/pull/13030))
- Fix error when searching for empty phrase ([Gargron](https://github.com/tootsuite/mastodon/pull/12711))
- Fix backups stopping due to read timeouts ([chr-1x](https://github.com/tootsuite/mastodon/pull/12281))
- Fix batch actions on non-pending tags in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/12537))
@ -152,6 +158,13 @@ All notable changes to this project will be documented in this file.
- Fix voting issue with remote polls that contain trailing spaces ([ThibG](https://github.com/tootsuite/mastodon/pull/12515))
- Fix dynamic elements not working in pgHero due to CSP rules ([ykzts](https://github.com/tootsuite/mastodon/pull/12489))
- Fix overly verbose backtraces when delivering ActivityPub payloads ([zunda](https://github.com/tootsuite/mastodon/pull/12798))
- Fix rendering `<a>` without `href` when scheme unsupported ([Gargron](https://github.com/tootsuite/mastodon/pull/13040))
- Fix unfiltered params error when generating ActivityPub tag pagination ([Gargron](https://github.com/tootsuite/mastodon/pull/13049))
- Fix malformed HTML causing uncaught error ([Gargron](https://github.com/tootsuite/mastodon/pull/13042))
- Fix native share button not being displayed for unlisted toots ([ThibG](https://github.com/tootsuite/mastodon/pull/13045))
- Fix remote convertible media attachments (e.g. GIFs) not being saved ([Gargron](https://github.com/tootsuite/mastodon/pull/13032))
- Fix account query not using faster index ([abcang](https://github.com/tootsuite/mastodon/pull/13016))
- Fix error when sending moderation notification ([renatolond](https://github.com/tootsuite/mastodon/pull/13014))
### Security

12
Gemfile

@ -60,7 +60,7 @@ gem 'htmlentities', '~> 4.3'
gem 'http', '~> 4.3'
gem 'http_accept_language', '~> 2.1'
gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2', submodules: true
gem 'httplog', '~> 1.4'
gem 'httplog', '~> 1.4.2'
gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.1'
gem 'link_header', '~> 0.0'
@ -101,14 +101,14 @@ gem 'webpacker', '~> 4.2'
gem 'webpush'
gem 'json-ld'
gem 'json-ld-preloaded', '~> 3.0'
gem 'json-ld-preloaded', '~> 3.1'
gem 'rdf-normalize', '~> 0.4'
group :development, :test do
gem 'fabrication', '~> 2.21'
gem 'fuubar', '~> 2.5'
gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-byebug', '~> 3.7'
gem 'pry-byebug', '~> 3.8'
gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 3.9'
end
@ -118,13 +118,13 @@ group :production, :test do
end
group :test do
gem 'capybara', '~> 3.30'
gem 'capybara', '~> 3.31'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.10'
gem 'microformats', '~> 4.2'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.17', require: false
gem 'simplecov', '~> 0.18', require: false
gem 'webmock', '~> 3.8'
gem 'parallel_tests', '~> 2.30'
end
@ -136,7 +136,7 @@ group :development do
gem 'binding_of_caller', '~> 0.7'
gem 'bullet', '~> 6.1'
gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.3'
gem 'letter_opener_web', '~> 1.4'
gem 'memory_profiler'
gem 'rubocop', '~> 0.79', require: false
gem 'rubocop-rails', '~> 2.4', require: false

38
Gemfile.lock

@ -127,7 +127,7 @@ GEM
bundler-audit (0.6.1)
bundler (>= 1.2.0, < 3)
thor (~> 0.18)
byebug (11.0.0)
byebug (11.1.1)
capistrano (3.11.2)
airbrussh (>= 1.0.0)
i18n
@ -144,7 +144,7 @@ GEM
sshkit (~> 1.3)
capistrano-yarn (2.0.2)
capistrano (~> 3.0)
capybara (3.30.0)
capybara (3.31.0)
addressable
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
@ -282,7 +282,7 @@ GEM
http-parser (1.2.1)
ffi-compiler (>= 1.0, < 2.0)
http_accept_language (2.1.1)
httplog (1.4.0)
httplog (1.4.2)
rack (>= 1.0)
rainbow (>= 2.0.0)
i18n (1.8.2)
@ -311,10 +311,9 @@ GEM
multi_json (~> 1.14)
rack (~> 2.0)
rdf (~> 3.1)
json-ld-preloaded (3.0.6)
json-ld (~> 3.0)
multi_json (~> 1.12)
rdf (~> 3.0)
json-ld-preloaded (3.1.0)
json-ld (~> 3.1)
rdf (~> 3.1)
jsonapi-renderer (0.2.2)
jwt (2.1.0)
kaminari (1.1.1)
@ -333,7 +332,7 @@ GEM
addressable (~> 2.3)
letter_opener (1.7.0)
launchy (~> 2.2)
letter_opener_web (1.3.4)
letter_opener_web (1.4.0)
actionmailer (>= 3.2)
letter_opener (~> 1.0)
railties (>= 3.2)
@ -430,7 +429,7 @@ GEM
pry (0.12.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
pry-byebug (3.7.0)
pry-byebug (3.8.0)
byebug (~> 11.0)
pry (~> 0.10)
pry-rails (0.3.9)
@ -550,7 +549,7 @@ GEM
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7)
rubocop-rails (2.4.1)
rubocop-rails (2.4.2)
rack (>= 1.1)
rubocop (>= 0.72.0)
ruby-progressbar (1.10.1)
@ -584,11 +583,10 @@ GEM
simple_form (5.0.1)
actionpack (>= 5.0)
activemodel (>= 5.0)
simplecov (0.17.1)
simplecov (0.18.1)
docile (~> 1.1)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
simplecov-html (~> 0.11.0)
simplecov-html (0.11.0)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
@ -680,7 +678,7 @@ DEPENDENCIES
capistrano-rails (~> 1.4)
capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0)
capybara (~> 3.30)
capybara (~> 3.31)
charlock_holmes (~> 0.7.7)
chewy (~> 5.1)
cld3 (~> 3.2.6)
@ -709,15 +707,15 @@ DEPENDENCIES
http (~> 4.3)
http_accept_language (~> 2.1)
http_parser.rb (~> 0.6)!
httplog (~> 1.4)
httplog (~> 1.4.2)
i18n-tasks (~> 0.9)
idn-ruby
iso-639
json-ld
json-ld-preloaded (~> 3.0)
json-ld-preloaded (~> 3.1)
kaminari (~> 1.1)
letter_opener (~> 1.7)
letter_opener_web (~> 1.3)
letter_opener_web (~> 1.4)
link_header (~> 0.0)
lograge (~> 0.11)
makara (~> 0.4)
@ -745,7 +743,7 @@ DEPENDENCIES
posix-spawn!
premailer-rails
private_address_check (~> 0.5)
pry-byebug (~> 3.7)
pry-byebug (~> 3.8)
pry-rails (~> 0.3)
puma (~> 4.3)
pundit (~> 2.1)
@ -773,7 +771,7 @@ DEPENDENCIES
sidekiq-unique-jobs (~> 6.0)
simple-navigation (~> 4.1)
simple_form (~> 5.0)
simplecov (~> 0.17)
simplecov (~> 0.18)
sprockets (~> 3.7.2)
sprockets-rails (~> 3.2)
stackprof

6
app/controllers/api/v1/announcements_controller.rb

@ -19,11 +19,7 @@ class Api::V1::AnnouncementsController < Api::BaseController
def set_announcements
@announcements = begin
scope = Announcement.published
scope.merge!(Announcement.without_muted(current_account)) unless truthy_param?(:with_dismissed)
scope.chronological
Announcement.published.chronological
end
end

16
app/controllers/api/web/embeds_controller.rb

@ -7,15 +7,21 @@ class Api::Web::EmbedsController < Api::Web::BaseController
def create
status = StatusFinder.new(params[:url]).status
return not_found if status.hidden?
render json: status, serializer: OEmbedSerializer, width: 400
rescue ActiveRecord::RecordNotFound
oembed = FetchOEmbedService.new.call(params[:url])
oembed[:html] = Formatter.instance.sanitize(oembed[:html], Sanitize::Config::MASTODON_OEMBED) if oembed[:html].present?
if oembed
render json: oembed
else
render json: {}, status: :not_found
return not_found if oembed.nil?
begin
oembed[:html] = Formatter.instance.sanitize(oembed[:html], Sanitize::Config::MASTODON_OEMBED)
rescue ArgumentError
return not_found
end
render json: oembed
end
end

2
app/controllers/concerns/signature_verification.rb

@ -160,6 +160,8 @@ module SignatureVerification
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) }
account
end
rescue Mastodon::HostValidationError
nil
end
def stoplight_wrap_request(&block)

10
app/controllers/tags_controller.rb

@ -24,7 +24,7 @@ class TagsController < ApplicationController
format.rss do
expires_in 0, public: true
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none)).limit(PAGE_SIZE)
@statuses = HashtagQueryService.new.call(@tag, filter_params).limit(PAGE_SIZE)
@statuses = cache_collection(@statuses, Status)
render xml: RSS::TagSerializer.render(@tag, @statuses)
@ -33,7 +33,7 @@ class TagsController < ApplicationController
format.json do
expires_in 3.minutes, public: public_fetch_mode?
@statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id])
@statuses = HashtagQueryService.new.call(@tag, filter_params, current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id])
@statuses = cache_collection(@statuses, Status)
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
@ -57,10 +57,14 @@ class TagsController < ApplicationController
def collection_presenter
ActivityPub::CollectionPresenter.new(
id: tag_url(@tag, params.slice(:any, :all, :none)),
id: tag_url(@tag, filter_params),
type: :ordered,
size: @tag.statuses.count,
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
)
end
def filter_params
params.slice(:any, :all, :none).permit(:any, :all, :none)
end
end

2
app/helpers/accounts_helper.rb

@ -11,7 +11,7 @@ module AccountsHelper
def acct(account)
if account.local?
"@#{account.acct}@#{Rails.configuration.x.local_domain}"
"@#{account.acct}@#{site_hostname}"
else
"@#{account.pretty_acct}"
end

42
app/javascript/mastodon/actions/announcements.js

@ -3,17 +3,21 @@ import { normalizeAnnouncement } from './importer/normalizer';
export const ANNOUNCEMENTS_FETCH_REQUEST = 'ANNOUNCEMENTS_FETCH_REQUEST';
export const ANNOUNCEMENTS_FETCH_SUCCESS = 'ANNOUNCEMENTS_FETCH_SUCCESS';
export const ANNOUNCEMENTS_FETCH_FAIL = 'ANNOUNCEMENTS_FETCH_FAIL';
export const ANNOUNCEMENTS_UPDATE = 'ANNOUNCEMENTS_UPDATE';
export const ANNOUNCEMENTS_DELETE = 'ANNOUNCEMENTS_DELETE';
export const ANNOUNCEMENTS_FETCH_FAIL = 'ANNOUNCEMENTS_FETCH_FAIL';
export const ANNOUNCEMENTS_UPDATE = 'ANNOUNCEMENTS_UPDATE';
export const ANNOUNCEMENTS_DELETE = 'ANNOUNCEMENTS_DELETE';
export const ANNOUNCEMENTS_DISMISS_REQUEST = 'ANNOUNCEMENTS_DISMISS_REQUEST';
export const ANNOUNCEMENTS_DISMISS_SUCCESS = 'ANNOUNCEMENTS_DISMISS_SUCCESS';
export const ANNOUNCEMENTS_DISMISS_FAIL = 'ANNOUNCEMENTS_DISMISS_FAIL';
export const ANNOUNCEMENTS_REACTION_ADD_REQUEST = 'ANNOUNCEMENTS_REACTION_ADD_REQUEST';
export const ANNOUNCEMENTS_REACTION_ADD_SUCCESS = 'ANNOUNCEMENTS_REACTION_ADD_SUCCESS';
export const ANNOUNCEMENTS_REACTION_ADD_FAIL = 'ANNOUNCEMENTS_REACTION_ADD_FAIL';
export const ANNOUNCEMENTS_REACTION_ADD_FAIL = 'ANNOUNCEMENTS_REACTION_ADD_FAIL';
export const ANNOUNCEMENTS_REACTION_REMOVE_REQUEST = 'ANNOUNCEMENTS_REACTION_REMOVE_REQUEST';
export const ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS = 'ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS';
export const ANNOUNCEMENTS_REACTION_REMOVE_FAIL = 'ANNOUNCEMENTS_REACTION_REMOVE_FAIL';
export const ANNOUNCEMENTS_REACTION_REMOVE_FAIL = 'ANNOUNCEMENTS_REACTION_REMOVE_FAIL';
export const ANNOUNCEMENTS_REACTION_UPDATE = 'ANNOUNCEMENTS_REACTION_UPDATE';
@ -52,10 +56,36 @@ export const fetchAnnouncementsFail= error => ({
});
export const updateAnnouncements = announcement => ({
type: ANNOUNCEMENTS_UPDATE,
type : ANNOUNCEMENTS_UPDATE,
announcement: normalizeAnnouncement(announcement),
});
export const dismissAnnouncement = announcementId => (dispatch, getState) => {
dispatch(dismissAnnouncementRequest(announcementId));
api(getState).post(`/api/v1/announcements/${announcementId}/dismiss`).then(() => {
dispatch(dismissAnnouncementSuccess(announcementId));
}).catch(error => {
dispatch(dismissAnnouncementFail(announcementId, error));
});
};
export const dismissAnnouncementRequest = announcementId => ({
type: ANNOUNCEMENTS_DISMISS_REQUEST,
id : announcementId,
});
export const dismissAnnouncementSuccess = announcementId => ({
type: ANNOUNCEMENTS_DISMISS_SUCCESS,
id : announcementId,
});
export const dismissAnnouncementFail = (announcementId, error) => ({
type: ANNOUNCEMENTS_DISMISS_FAIL,
id : announcementId,
error,
});
export const addReaction = (announcementId, name) => (dispatch, getState) => {
const announcement = getState().getIn(['announcements', 'items']).find(x => x.get('id') === announcementId);

87
app/javascript/mastodon/features/getting_started/components/announcements.js

@ -5,10 +5,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import IconButton from 'mastodon/components/icon_button';
import Icon from 'mastodon/components/icon';
import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl';
import { autoPlayGif, reduceMotion } from 'mastodon/initial_state';
import { defineMessages, FormattedDate, FormattedMessage, injectIntl } from 'react-intl';
import { autoPlayGif, mascot, reduceMotion } from 'mastodon/initial_state';
import elephantUIPlane from 'mastodon/../images/elephant_ui_plane.svg';
import { mascot } from 'mastodon/initial_state';
import unicodeMapping from 'mastodon/features/emoji/emoji_unicode_mapping_light';
import classNames from 'classnames';
import EmojiPickerDropdown from 'mastodon/features/compose/containers/emoji_picker_dropdown_container';
@ -17,7 +16,7 @@ import TransitionMotion from 'react-motion/lib/TransitionMotion';
import spring from 'react-motion/lib/spring';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
close : { id: 'lightbox.close', defaultMessage: 'Close' },
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
next: { id: 'lightbox.next', defaultMessage: 'Next' },
});
@ -34,7 +33,7 @@ class Content extends ImmutablePureComponent {
setRef = c => {
this.node = c;
}
};
componentDidMount () {
this._updateLinks();
@ -109,7 +108,7 @@ class Content extends ImmutablePureComponent {
e.preventDefault();
this.context.router.history.push(`/accounts/${mention.get('id')}`);
}
}
};
onHashtagClick = (hashtag, e) => {
hashtag = hashtag.replace(/^#/, '');
@ -118,15 +117,15 @@ class Content extends ImmutablePureComponent {
e.preventDefault();
this.context.router.history.push(`/timelines/tag/${hashtag}`);
}
}
};
handleEmojiMouseEnter = ({ target }) => {
target.src = target.getAttribute('data-original');
}
};
handleEmojiMouseLeave = ({ target }) => {
target.src = target.getAttribute('data-static');
}
};
render () {
const { announcement } = this.props;
@ -211,11 +210,11 @@ class Reaction extends ImmutablePureComponent {
} else {
addReaction(announcementId, reaction.get('name'));
}
}
};
handleMouseEnter = () => this.setState({ hovered: true })
handleMouseEnter = () => this.setState({ hovered: true });
handleMouseLeave = () => this.setState({ hovered: false })
handleMouseLeave = () => this.setState({ hovered: false });
render () {
const { reaction } = this.props;
@ -249,7 +248,7 @@ class ReactionsBar extends ImmutablePureComponent {
handleEmojiPick = data => {
const { addReaction, announcementId } = this.props;
addReaction(announcementId, data.native.replace(/:/g, ''));
}
};
willEnter () {
return { scale: reduceMotion ? 1 : 0 };
@ -297,15 +296,28 @@ class ReactionsBar extends ImmutablePureComponent {
class Announcement extends ImmutablePureComponent {
static propTypes = {
announcement: ImmutablePropTypes.map.isRequired,
emojiMap: ImmutablePropTypes.map.isRequired,
addReaction: PropTypes.func.isRequired,
announcement : ImmutablePropTypes.map.isRequired,
emojiMap : ImmutablePropTypes.map.isRequired,
addReaction : PropTypes.func.isRequired,
removeReaction: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
intl : PropTypes.object.isRequired,
selected : PropTypes.bool,
};
render () {
state = {
unread: !this.props.announcement.get('read'),
};
componentDidUpdate() {
const { selected, announcement } = this.props;
if (!selected && this.state.unread !== !announcement.get('read')) {
this.setState({ unread: !announcement.get('read') });
}
}
render() {
const { announcement } = this.props;
const { unread } = this.state;
const startsAt = announcement.get('starts_at') && new Date(announcement.get('starts_at'));
const endsAt = announcement.get('ends_at') && new Date(announcement.get('ends_at'));
const now = new Date();
@ -330,7 +342,9 @@ class Announcement extends ImmutablePureComponent {
removeReaction={this.props.removeReaction}
emojiMap={this.props.emojiMap}
/>
</div>
{unread && <span className='announcements__item__unread' />}
</div >
);
}
@ -340,28 +354,44 @@ export default @injectIntl
class Announcements extends ImmutablePureComponent {
static propTypes = {
announcements: ImmutablePropTypes.list,
emojiMap: ImmutablePropTypes.map.isRequired,
addReaction: PropTypes.func.isRequired,
removeReaction: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
announcements : ImmutablePropTypes.list,
emojiMap : ImmutablePropTypes.map.isRequired,
dismissAnnouncement: PropTypes.func.isRequired,
addReaction : PropTypes.func.isRequired,
removeReaction : PropTypes.func.isRequired,
intl : PropTypes.object.isRequired,
};
state = {
index: 0,
};
componentDidMount() {
this._markAnnouncementAsRead();
}
componentDidUpdate() {
this._markAnnouncementAsRead();
}
_markAnnouncementAsRead() {
const { dismissAnnouncement, announcements } = this.props;
const { index } = this.state;
const announcement = announcements.get(index);
if (!announcement.get('read')) dismissAnnouncement(announcement.get('id'));
}
handleChangeIndex = index => {
this.setState({ index: index % this.props.announcements.size });
}
};
handleNextClick = () => {
this.setState({ index: (this.state.index + 1) % this.props.announcements.size });
}
};
handlePrevClick = () => {
this.setState({ index: (this.props.announcements.size + this.state.index - 1) % this.props.announcements.size });
}
};
render () {
const { announcements, intl } = this.props;
@ -377,7 +407,7 @@ class Announcements extends ImmutablePureComponent {
<div className='announcements__container'>
<ReactSwipeableViews animateHeight={!reduceMotion} adjustHeight={reduceMotion} index={index} onChangeIndex={this.handleChangeIndex}>
{announcements.map(announcement => (
{announcements.map((announcement, idx) => (
<Announcement
key={announcement.get('id')}
announcement={announcement}
@ -385,6 +415,7 @@ class Announcements extends ImmutablePureComponent {
addReaction={this.props.addReaction}
removeReaction={this.props.removeReaction}
intl={intl}
selected={index === idx}
/>
))}
</ReactSwipeableViews>

7
app/javascript/mastodon/features/getting_started/containers/announcements_container.js

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { addReaction, removeReaction } from 'mastodon/actions/announcements';
import { addReaction, dismissAnnouncement, removeReaction } from 'mastodon/actions/announcements';
import Announcements from '../components/announcements';
import { createSelector } from 'reselect';
import { Map as ImmutableMap } from 'immutable';
@ -12,8 +12,9 @@ const mapStateToProps = state => ({
});
const mapDispatchToProps = dispatch => ({
addReaction: (id, name) => dispatch(addReaction(id, name)),
removeReaction: (id, name) => dispatch(removeReaction(id, name)),
dismissAnnouncement: id => dispatch(dismissAnnouncement(id)),
addReaction : (id, name) => dispatch(addReaction(id, name)),
removeReaction : (id, name) => dispatch(removeReaction(id, name)),
});
export default connect(mapStateToProps, mapDispatchToProps)(Announcements);

30
app/javascript/mastodon/features/home_timeline/index.js

@ -5,8 +5,8 @@ import PropTypes from 'prop-types';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { addColumn, moveColumn, removeColumn } from '../../actions/columns';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container';
import { Link } from 'react-router-dom';
import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/announcements';
@ -15,17 +15,17 @@ import classNames from 'classnames';
import IconWithBadge from 'mastodon/components/icon_with_badge';
const messages = defineMessages({
title: { id: 'column.home', defaultMessage: 'Home' },
title : { id: 'column.home', defaultMessage: 'Home' },
show_announcements: { id: 'home.show_announcements', defaultMessage: 'Show announcements' },
hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide announcements' },
});
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
isPartial: state.getIn(['timelines', 'home', 'isPartial']),
hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(),
unreadAnnouncements: state.getIn(['announcements', 'unread']).size,
showAnnouncements: state.getIn(['announcements', 'show']),
hasUnread : state.getIn(['timelines', 'home', 'unread']) > 0,
isPartial : state.getIn(['timelines', 'home', 'isPartial']),
hasAnnouncements : !state.getIn(['announcements', 'items']).isEmpty(),
unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')),
showAnnouncements : state.getIn(['announcements', 'show']),
});
export default @connect(mapStateToProps)
@ -53,24 +53,24 @@ class HomeTimeline extends React.PureComponent {
} else {
dispatch(addColumn('HOME', {}));
}
}
};
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
};
handleHeaderClick = () => {
this.column.scrollTop();
}
};
setRef = c => {
this.column = c;
}
};
handleLoadMore = maxId => {
this.props.dispatch(expandHomeTimeline({ maxId }));
}
};
componentDidMount () {
this.props.dispatch(fetchAnnouncements());
@ -89,7 +89,7 @@ class HomeTimeline extends React.PureComponent {
const { dispatch } = this.props;
if (wasPartial === isPartial) {
return;
} else if (!wasPartial && isPartial) {
this.polling = setInterval(() => {
dispatch(expandHomeTimeline());
@ -109,7 +109,7 @@ class HomeTimeline extends React.PureComponent {
handleToggleAnnouncementsClick = (e) => {
e.stopPropagation();
this.props.dispatch(toggleShowAnnouncements());
}
};
render () {
const { intl, shouldUpdateScroll, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;

2
app/javascript/mastodon/features/status/components/action_bar.js

@ -304,8 +304,6 @@ class ActionBar extends React.PureComponent {
if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
else if (status.get('visibility') === 'private') reblogIcon = 'lock';
let reblog_disabled = (status.get('visibility') === 'direct' || status.get('visibility') === 'private');
return (
<div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton

174
app/javascript/mastodon/features/video/index.js

@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { fromJS, is } from 'immutable';
import { throttle } from 'lodash';
import classNames from 'classnames';
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
import { exitFullscreen, isFullscreen, requestFullscreen } from '../ui/util/fullscreen';
import { displayMedia, useBlurhash } from '../../initial_state';
import Icon from 'mastodon/components/icon';
import { decode } from 'blurhash';
@ -112,28 +112,41 @@ class Video extends React.PureComponent {
};
state = {
currentTime: 0,
duration: 0,
volume: 0.5,
paused: true,
dragging: false,
currentTime : 0,
duration : 0,
volume : 0.5,
paused : true,
dragging : false,
containerWidth: this.props.width,
fullscreen: false,
hovered: false,
muted: false,
revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
fullscreen : false,
hovered : false,
muted : false,
revealed : this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
};
// Hard-coded in components.scss
// Any way to get ::before values programatically?
volWidth = 50;
volWidth = 50;
volOffset = 70;
handleScroll = throttle(() => {
if (!this.video) {
return;
}
const { top, height } = this.video.getBoundingClientRect();
const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
if (!this.state.paused && !inView) {
this.setState({ paused: true }, () => this.video.pause());
}
}, 150, { trailing: true });
volHandleOffset = v => {
const offset = v * this.volWidth + this.volOffset;
return (offset > 110) ? 110 : offset;
}
};
setPlayerRef = c => {
this.player = c;
@ -145,7 +158,7 @@ class Video extends React.PureComponent {
containerWidth: c.offsetWidth,
});
}
}
};
setVideoRef = c => {
this.video = c;
@ -153,36 +166,36 @@ class Video extends React.PureComponent {
if (this.video) {
this.setState({ volume: this.video.volume, muted: this.video.muted });
}
}
};
setSeekRef = c => {
this.seek = c;
}
};
setVolumeRef = c => {
this.volume = c;
}
};
handleClickRoot = e => e.stopPropagation();
setCanvasRef = c => {
this.canvas = c;
}
handleClickRoot = e => e.stopPropagation();
};
handlePlay = () => {
this.setState({ paused: false });
}
};
handlePause = () => {
this.setState({ paused: true });
}
};
handleTimeUpdate = () => {
this.setState({
currentTime: Math.floor(this.video.currentTime),
duration: Math.floor(this.video.duration),
duration : Math.floor(this.video.duration),
});
}
};
handleVolumeMouseDown = e => {
document.addEventListener('mousemove', this.handleMouseVolSlide, true);
@ -194,14 +207,7 @@ class Video extends React.PureComponent {
e.preventDefault();
e.stopPropagation();
}
handleVolumeMouseUp = () => {
document.removeEventListener('mousemove', this.handleMouseVolSlide, true);
document.removeEventListener('mouseup', this.handleVolumeMouseUp, true);
document.removeEventListener('touchmove', this.handleMouseVolSlide, true);
document.removeEventListener('touchend', this.handleVolumeMouseUp, true);
}
};
handleMouseVolSlide = throttle(e => {
const rect = this.volume.getBoundingClientRect();
@ -212,7 +218,7 @@ class Video extends React.PureComponent {
if(x > 1) {
slideamt = 1;
} else if(x < 0) {
} else if (x < 0) {
slideamt = 0;
}
@ -221,6 +227,13 @@ class Video extends React.PureComponent {
}
}, 60);
handleVolumeMouseUp = () => {
document.removeEventListener('mousemove', this.handleMouseVolSlide, true);
document.removeEventListener('mouseup', this.handleVolumeMouseUp, true);
document.removeEventListener('touchmove', this.handleMouseVolSlide, true);
document.removeEventListener('touchend', this.handleVolumeMouseUp, true);
};
handleMouseDown = e => {
document.addEventListener('mousemove', this.handleMouseMove, true);
document.addEventListener('mouseup', this.handleMouseUp, true);
@ -233,17 +246,7 @@ class Video extends React.PureComponent {
e.preventDefault();
e.stopPropagation();
}
handleMouseUp = () => {
document.removeEventListener('mousemove', this.handleMouseMove, true);
document.removeEventListener('mouseup', this.handleMouseUp, true);
document.removeEventListener('touchmove', this.handleMouseMove, true);
document.removeEventListener('touchend', this.handleMouseUp, true);
this.setState({ dragging: false });
this.video.play();
}
};
handleMouseMove = throttle(e => {
const { x } = getPointerPosition(this.seek, e);
@ -255,23 +258,25 @@ class Video extends React.PureComponent {
}
}, 60);
handleMouseUp = () => {
document.removeEventListener('mousemove', this.handleMouseMove, true);
document.removeEventListener('mouseup', this.handleMouseUp, true);
document.removeEventListener('touchmove', this.handleMouseMove, true);
document.removeEventListener('touchend', this.handleMouseUp, true);
this.setState({ dragging: false });
this.video.play();
};
togglePlay = () => {
if (this.state.paused) {
this.setState({ paused: false }, () => this.video.play());
} else {
this.setState({ paused: true }, () => this.video.pause());
}
}
toggleFullscreen = () => {
if (isFullscreen()) {
exitFullscreen();
} else {
requestFullscreen(this.player);
}
}
};
componentDidMount () {
componentDidMount() {
document.addEventListener('fullscreenchange', this.handleFullscreenChange, true);
document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
@ -316,37 +321,32 @@ class Video extends React.PureComponent {
const pixels = decode(hash, 32, 32);
if (pixels) {
const ctx = this.canvas.getContext('2d');
const ctx = this.canvas.getContext('2d');
const imageData = new ImageData(pixels, 32, 32);
ctx.putImageData(imageData, 0, 0);
}
}
handleScroll = throttle(() => {
if (!this.video) {
return;
}
const { top, height } = this.video.getBoundingClientRect();
const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
if (!this.state.paused && !inView) {
this.setState({ paused: true }, () => this.video.pause());
toggleFullscreen = () => {
if (isFullscreen()) {
exitFullscreen();
} else {
requestFullscreen(this.player);
}
}, 150, { trailing: true })
};
handleFullscreenChange = () => {
this.setState({ fullscreen: isFullscreen() });
}
};
handleMouseEnter = () => {
this.setState({ hovered: true });
}
};
handleMouseLeave = () => {
this.setState({ hovered: false });
}
};
toggleMute = () => {
const muted = !this.video.muted;
@ -354,7 +354,7 @@ class Video extends React.PureComponent {
this.setState({ muted }, () => {
this.video.muted = muted;
});
}
};
toggleReveal = () => {
if (this.props.onToggleVisibility) {
@ -362,31 +362,31 @@ class Video extends React.PureComponent {
} else {
this.setState({ revealed: !this.state.revealed });
}
}
};
handleLoadedData = () => {
if (this.props.startTime) {
this.video.currentTime = this.props.startTime;
this.video.play();
}
}
};
handleProgress = () => {
if (this.video.buffered.length > 0) {
this.setState({ buffer: this.video.buffered.end(0) / this.video.duration * 100 });
}
}
};
handleVolumeChange = () => {
this.setState({ volume: this.video.volume, muted: this.video.muted });
}
};
handleOpenVideo = () => {
const { src, preview, width, height, alt } = this.props;
const media = fromJS({
type: 'video',
url: src,
type : 'video',
url : src,
preview_url: preview,
description: alt,
width,
@ -395,12 +395,12 @@ class Video extends React.PureComponent {
this.video.pause();
this.props.onOpenVideo(media, this.video.currentTime);
}
};
handleCloseVideo = () => {
this.video.pause();
this.props.onCloseVideo();
}
};
render () {
const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, editable } = this.props;
@ -517,8 +517,22 @@ class Video extends React.PureComponent {
</div>
<div className='video-player__buttons right'>
{(!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 && !editable && !fullscreen) && <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>

11
app/javascript/mastodon/locales/ar.json

@ -1,9 +1,9 @@
{
"account.add_or_remove_from_list": "أضفه أو أزله من القائمة",
"account.badges.bot": "روبوت",
"account.badges.group": "Group",
"account.badges.group": "فريق",
"account.block": "حظر @{name}",
"account.block_domain": "إخفاء كل شيئ قادم من اسم النطاق {domain}",
"account.block_domain": "إخفاء كل شيء قادم من اسم النطاق {domain}",
"account.blocked": "محظور",
"account.cancel_follow_request": "إلغاء طلب المتابَعة",
"account.direct": "رسالة خاصة إلى @{name}",
@ -43,7 +43,7 @@
"alert.rate_limited.title": "المعدل محدود",
"alert.unexpected.message": "لقد طرأ هناك خطأ غير متوقّع.",
"alert.unexpected.title": "المعذرة!",
"announcement.announcement": "Announcement",
"announcement.announcement": "إعلان",
"autosuggest_hashtag.per_week": "{count} في الأسبوع",
"boost_modal.combo": "يمكنك/ي ضغط {combo} لتخطّي هذه في المرّة القادمة",
"bundle_column_error.body": "لقد وقع هناك خطأ أثناء عملية تحميل هذا العنصر.",
@ -184,6 +184,8 @@
"home.column_settings.basic": "الأساسية",
"home.column_settings.show_reblogs": "اعرض الترقيات",
"home.column_settings.show_replies": "اعرض الردود",
"home.hide_announcements": "إخفاء الإعلانات",
"home.show_announcements": "إظهار الإعلانات",
"intervals.full.days": "{number, plural, one {# يوم} other {# أيام}}",
"intervals.full.hours": "{number, plural, one {# ساعة} other {# ساعات}}",
"intervals.full.minutes": "{number, plural, one {# دقيقة} other {# دقائق}}",
@ -333,6 +335,7 @@
"relative_time.just_now": "الآن",
"relative_time.minutes": "{number}د",
"relative_time.seconds": "{number}ثا",
"relative_time.today": "اليوم",
"reply_indicator.cancel": "إلغاء",
"report.forward": "التحويل إلى {target}",
"report.forward_hint": "هذا الحساب ينتمي إلى خادوم آخَر. هل تودّ إرسال نسخة مجهولة مِن التقرير إلى هنالك أيضًا؟",
@ -420,7 +423,7 @@
"upload_form.video_description": "وصف للمعاقين بصريا أو لِذي قِصر السمع",
"upload_modal.analyzing_picture": "جارٍ فحص الصورة…",
"upload_modal.apply": "طبّق",
"upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
"upload_modal.description_placeholder": "نصٌّ حكيمٌ لهُ سِرٌّ قاطِعٌ وَذُو شَأنٍ عَظيمٍ مكتوبٌ على ثوبٍ أخضرَ ومُغلفٌ بجلدٍ أزرق",
"upload_modal.detect_text": "اكتشف النص مِن الصورة",
"upload_modal.edit_media": "تعديل الوسائط",
"upload_modal.hint": "اضغط أو اسحب الدائرة على خانة المعاينة لاختيار نقطة التركيز التي ستُعرَض دائمًا على كل المصغرات.",

153
app/javascript/mastodon/locales/ast.json

@ -1,22 +1,22 @@
{
"account.add_or_remove_from_list": "Add or Remove from lists",
"account.badges.bot": "Robó",
"account.badges.group": "Group",
"account.badges.group": "Grupu",
"account.block": "Bloquiar a @{name}",
"account.block_domain": "Anubrir tolo de {domain}",
"account.blocked": "Blocked",
"account.cancel_follow_request": "Cancel follow request",
"account.cancel_follow_request": "Encaboxar la solicitú de siguimientu",
"account.direct": "Unviar un mensaxe direutu a @{name}",
"account.domain_blocked": "Dominiu anubríu",
"account.edit_profile": "Editar el perfil",
"account.endorse": "Destacar nel perfil",
"account.follow": "Follow",
"account.follow": "Siguir",
"account.followers": "Siguidores",
"account.followers.empty": "Naide sigue a esti usuariu entá.",
"account.follows": "Sigue a",