Browse Source

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

Conflicts:
- README.md
  discarded upstream changes
- app/controllers/api/v1/bookmarks_controller.rb
  finally merged upstream, some code style fixes
  and slightly changed pagination code
- app/controllers/application_controller.rb
  changed upstream to always return HTML error pages
  slight conflict caused by theming code
- app/models/bookmark.rb
  finally merged upstream, no real conflict
- spec/controllers/api/v1/bookmarks_controller_spec.rb
  finally merged upstream, slightly changed pagination code
master
Thibaut Girka 3 weeks ago
parent
commit
ff67385cfb
100 changed files with 4171 additions and 1509 deletions
  1. 0
    0
      .cache/.gitkeep
  2. 5
    0
      .github/ISSUE_TEMPLATE/config.yml
  3. 1
    1
      CONTRIBUTING.md
  4. 8
    8
      Gemfile
  5. 32
    31
      Gemfile.lock
  6. 1
    1
      app/controllers/api/v1/accounts/credentials_controller.rb
  7. 4
    9
      app/controllers/api/v1/bookmarks_controller.rb
  8. 2
    8
      app/controllers/application_controller.rb
  9. 4
    2
      app/helpers/admin/action_logs_helper.rb
  10. 3
    0
      app/helpers/settings_helper.rb
  11. 90
    0
      app/javascript/mastodon/actions/bookmarks.js
  12. 80
    0
      app/javascript/mastodon/actions/interactions.js
  13. 86
    10
      app/javascript/mastodon/components/status_action_bar.js
  14. 6
    0
      app/javascript/mastodon/containers/dropdown_menu_container.js
  15. 41
    1
      app/javascript/mastodon/containers/status_container.js
  16. 104
    0
      app/javascript/mastodon/features/bookmarked_statuses/index.js
  17. 4
    3
      app/javascript/mastodon/features/community_timeline/index.js
  18. 3
    1
      app/javascript/mastodon/features/getting_started/index.js
  19. 5
    3
      app/javascript/mastodon/features/public_timeline/index.js
  20. 86
    11
      app/javascript/mastodon/features/status/components/action_bar.js
  21. 46
    1
      app/javascript/mastodon/features/status/index.js
  22. 2
    0
      app/javascript/mastodon/features/ui/components/columns_area.js
  23. 1
    0
      app/javascript/mastodon/features/ui/components/navigation_panel.js
  24. 2
    0
      app/javascript/mastodon/features/ui/index.js
  25. 4
    0
      app/javascript/mastodon/features/ui/util/async-components.js
  26. 9
    9
      app/javascript/mastodon/locales/ar.json
  27. 5
    5
      app/javascript/mastodon/locales/bg.json
  28. 74
    74
      app/javascript/mastodon/locales/bn.json
  29. 8
    8
      app/javascript/mastodon/locales/cy.json
  30. 9
    9
      app/javascript/mastodon/locales/da.json
  31. 1
    1
      app/javascript/mastodon/locales/de.json
  32. 3
    3
      app/javascript/mastodon/locales/es-AR.json
  33. 36
    36
      app/javascript/mastodon/locales/et.json
  34. 7
    7
      app/javascript/mastodon/locales/eu.json
  35. 11
    11
      app/javascript/mastodon/locales/fi.json
  36. 37
    37
      app/javascript/mastodon/locales/fr.json
  37. 254
    254
      app/javascript/mastodon/locales/hi.json
  38. 49
    49
      app/javascript/mastodon/locales/hu.json
  39. 2
    2
      app/javascript/mastodon/locales/hy.json
  40. 78
    78
      app/javascript/mastodon/locales/id.json
  41. 10
    10
      app/javascript/mastodon/locales/it.json
  42. 46
    46
      app/javascript/mastodon/locales/kk.json
  43. 423
    0
      app/javascript/mastodon/locales/kn.json
  44. 4
    4
      app/javascript/mastodon/locales/ko.json
  45. 175
    175
      app/javascript/mastodon/locales/mk.json
  46. 423
    0
      app/javascript/mastodon/locales/ml.json
  47. 423
    0
      app/javascript/mastodon/locales/mr.json
  48. 6
    6
      app/javascript/mastodon/locales/nl.json
  49. 177
    177
      app/javascript/mastodon/locales/nn.json
  50. 37
    37
      app/javascript/mastodon/locales/no.json
  51. 5
    5
      app/javascript/mastodon/locales/oc.json
  52. 2
    2
      app/javascript/mastodon/locales/pl.json
  53. 36
    36
      app/javascript/mastodon/locales/pt-PT.json
  54. 26
    26
      app/javascript/mastodon/locales/ru.json
  55. 13
    13
      app/javascript/mastodon/locales/sk.json
  56. 1
    1
      app/javascript/mastodon/locales/sl.json
  57. 62
    62
      app/javascript/mastodon/locales/sv.json
  58. 102
    102
      app/javascript/mastodon/locales/ta.json
  59. 21
    21
      app/javascript/mastodon/locales/th.json
  60. 21
    21
      app/javascript/mastodon/locales/tr.json
  61. 13
    13
      app/javascript/mastodon/locales/uk.json
  62. 423
    0
      app/javascript/mastodon/locales/ur.json
  63. 2
    0
      app/javascript/mastodon/locales/whitelist_kn.json
  64. 2
    0
      app/javascript/mastodon/locales/whitelist_ml.json
  65. 2
    0
      app/javascript/mastodon/locales/whitelist_mr.json
  66. 2
    0
      app/javascript/mastodon/locales/whitelist_ur.json
  67. 38
    38
      app/javascript/mastodon/locales/zh-CN.json
  68. 29
    0
      app/javascript/mastodon/reducers/status_lists.js
  69. 6
    0
      app/javascript/mastodon/reducers/statuses.js
  70. 4
    0
      app/javascript/styles/mastodon/components.scss
  71. 2
    0
      app/javascript/styles/mastodon/variables.scss
  72. 1
    1
      app/lib/feed_manager.rb
  73. 3
    3
      app/models/bookmark.rb
  74. 16
    7
      app/services/fetch_link_card_service.rb
  75. 30
    1
      app/services/fetch_oembed_service.rb
  76. 6
    0
      app/services/resolve_url_service.rb
  77. 4
    0
      config/application.rb
  78. 3
    0
      config/i18n-tasks.yml
  79. 1
    1
      config/locales/activerecord.fi.yml
  80. 1
    0
      config/locales/activerecord.hi.yml
  81. 1
    0
      config/locales/activerecord.kn.yml
  82. 1
    0
      config/locales/activerecord.ml.yml
  83. 13
    0
      config/locales/activerecord.mr.yml
  84. 16
    0
      config/locales/activerecord.ta.yml
  85. 1
    0
      config/locales/activerecord.ur.yml
  86. 15
    4
      config/locales/ar.yml
  87. 64
    0
      config/locales/bn.yml
  88. 7
    0
      config/locales/ca.yml
  89. 9
    2
      config/locales/co.yml
  90. 6
    1
      config/locales/cs.yml
  91. 96
    3
      config/locales/cy.yml
  92. 20
    0
      config/locales/da.yml
  93. 3
    0
      config/locales/de.yml
  94. 58
    0
      config/locales/devise.bn.yml
  95. 4
    0
      config/locales/devise.da.yml
  96. 12
    0
      config/locales/devise.et.yml
  97. 8
    0
      config/locales/devise.eu.yml
  98. 27
    12
      config/locales/devise.fi.yml
  99. 6
    6
      config/locales/devise.fr.yml
  100. 0
    0
      config/locales/devise.hi.yml

+ 0
- 0
.cache/.gitkeep View File


+ 5
- 0
.github/ISSUE_TEMPLATE/config.yml View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Mastodon Meta Discussion Board
url: https://discourse.joinmastodon.org/
about: Please ask and answer questions here.

+ 1
- 1
CONTRIBUTING.md View File

@@ -48,7 +48,7 @@ If your contributions are accepted into Mastodon, you can request to be paid thr

## Bug reports

Bug reports and feature suggestions can be submitted to [GitHub Issues](https://github.com/tootsuite/mastodon/issues). Please make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected in the past using the search function. Please also use descriptive, concise titles.
Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/tootsuite/mastodon/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected.

## Translations


+ 8
- 8
Gemfile View File

@@ -15,7 +15,7 @@ gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.3'
gem 'dotenv-rails', '~> 2.7'

gem 'aws-sdk-s3', '~> 1.52', require: false
gem 'aws-sdk-s3', '~> 1.55', require: false
gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0'
@@ -68,11 +68,11 @@ gem 'oj', '~> 3.9'
gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.11'
gem 'parslet'
gem 'parallel', '~> 1.17'
gem 'parallel', '~> 1.18'
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
gem 'pundit', '~> 2.1'
gem 'premailer-rails'
gem 'rack-attack', '~> 6.1'
gem 'rack-attack', '~> 6.2'
gem 'rack-cors', '~> 1.0', require: 'rack/cors'
gem 'rails-i18n', '~> 5.1'
gem 'rails-settings-cached', '~> 0.6'
@@ -88,7 +88,7 @@ gem 'sidekiq-bulk', '~>0.2.0'
gem 'simple-navigation', '~> 4.1'
gem 'simple_form', '~> 5.0'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.1.3'
gem 'stoplight', '~> 2.2.0'
gem 'strong_migrations', '~> 0.4'
gem 'tty-command', '~> 0.9', require: false
gem 'tty-prompt', '~> 0.19', require: false
@@ -105,7 +105,7 @@ gem 'redcarpet', '~> 3.4'

group :development, :test do
gem 'fabrication', '~> 2.20'
gem 'fuubar', '~> 2.4'
gem 'fuubar', '~> 2.5'
gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-byebug', '~> 3.7'
gem 'pry-rails', '~> 0.3'
@@ -119,7 +119,7 @@ end
group :test do
gem 'capybara', '~> 3.29'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.6'
gem 'faker', '~> 2.7'
gem 'microformats', '~> 4.1'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0'
@@ -137,9 +137,9 @@ group :development do
gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.3'
gem 'memory_profiler'
gem 'rubocop', '~> 0.75', require: false
gem 'rubocop', '~> 0.76', require: false
gem 'rubocop-rails', '~> 2.3', require: false
gem 'brakeman', '~> 4.6', require: false
gem 'brakeman', '~> 4.7', require: false
gem 'bundler-audit', '~> 0.6', require: false

gem 'capistrano', '~> 3.11'

+ 32
- 31
Gemfile.lock View File

@@ -95,9 +95,9 @@ GEM
public_suffix (>= 2.0.2, < 5.0)
airbrussh (1.3.4)
sshkit (>= 1.6.1, != 1.7.0)
annotate (3.0.2)
annotate (3.0.3)
activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 13.0)
rake (>= 10.4, < 14.0)
arel (9.0.0)
ast (2.4.0)
attr_encrypted (3.1.0)
@@ -105,17 +105,17 @@ GEM
av (0.9.0)
cocaine (~> 0.5.3)
aws-eventstream (1.0.3)
aws-partitions (1.230.0)
aws-sdk-core (3.72.0)
aws-partitions (1.240.0)
aws-sdk-core (3.78.0)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-partitions (~> 1, >= 1.228.0)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.25.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.52.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sdk-s3 (1.55.0)
aws-sdk-core (~> 3, >= 3.77.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.0)
@@ -132,7 +132,7 @@ GEM
ffi (~> 1.10.0)
bootsnap (1.4.5)
msgpack (~> 1.0)
brakeman (4.6.1)
brakeman (4.7.1)
browser (2.6.1)
builder (3.2.3)
bullet (6.0.2)
@@ -188,7 +188,7 @@ GEM
css_parser (1.7.0)
addressable
debug_inspector (0.0.3)
derailed_benchmarks (1.4.1)
derailed_benchmarks (1.4.2)
benchmark-ips (~> 2)
get_process_mem (~> 0)
heapy (~> 0)
@@ -218,7 +218,7 @@ GEM
docile (1.3.2)
domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.2.1)
doorkeeper (5.2.2)
railties (>= 5)
dotenv (2.7.5)
dotenv-rails (2.7.5)
@@ -240,7 +240,7 @@ GEM
tzinfo
excon (0.62.0)
fabrication (2.20.2)
faker (2.6.0)
faker (2.7.0)
i18n (>= 1.6, < 1.8)
faraday (0.15.4)
multipart-post (>= 1.2, < 3)
@@ -263,10 +263,10 @@ GEM
fugit (1.1.6)
et-orbi (~> 1.1, >= 1.1.6)
raabro (~> 1.1)
fuubar (2.4.1)
fuubar (2.5.0)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
get_process_mem (0.2.4)
get_process_mem (0.2.5)
ffi (~> 1.0)
globalid (0.4.2)
activesupport (>= 4.2.0)
@@ -304,7 +304,7 @@ GEM
domain_name (~> 0.5)
http-form_data (2.1.1)
http_accept_language (2.1.1)
httplog (1.3.2)
httplog (1.3.3)
rack (>= 1.0)
rainbow (>= 2.0.0)
i18n (1.7.0)
@@ -322,7 +322,7 @@ GEM
idn-ruby (0.1.0)
ipaddress (0.8.3)
iso-639 (0.2.8)
jaro_winkler (1.5.3)
jaro_winkler (1.5.4)
jmespath (1.4.0)
json (2.2.0)
json-canonicalization (0.1.0)
@@ -380,7 +380,7 @@ GEM
mimemagic (0.3.3)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.12.2)
minitest (5.13.0)
msgpack (1.3.1)
multi_json (1.13.1)
multipart-post (2.1.1)
@@ -390,7 +390,7 @@ GEM
net-ssh (>= 2.6.5, < 6.0.0)
net-ssh (5.2.0)
nio4r (2.5.1)
nokogiri (1.10.4)
nokogiri (1.10.5)
mini_portile2 (~> 2.4.0)
nokogumbo (2.0.1)
nokogiri (~> 1.8, >= 1.8.4)
@@ -425,7 +425,7 @@ GEM
paperclip-av-transcoder (0.6.4)
av (~> 0.9.0)
paperclip (>= 2.5.2)
parallel (1.17.0)
parallel (1.18.0)
parallel_tests (2.29.2)
parallel
parser (2.6.5.0)
@@ -461,9 +461,10 @@ GEM
activesupport (>= 3.0.0)
raabro (1.1.6)
rack (2.0.7)
rack-attack (6.1.0)
rack-attack (6.2.1)
rack (>= 1.0, < 3)
rack-cors (1.0.3)
rack-cors (1.0.6)
rack (>= 1.6.0)
rack-protection (2.0.7)
rack
rack-proxy (0.6.5)
@@ -504,7 +505,7 @@ GEM
rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0)
rainbow (3.0.0)
rake (12.3.3)
rake (13.0.1)
rdf (3.0.12)
hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8)
@@ -560,7 +561,7 @@ GEM
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.9.0)
rubocop (0.75.1)
rubocop (0.76.0)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.6)
@@ -619,7 +620,7 @@ GEM
net-ssh (>= 2.8.0)
stackprof (0.2.13)
statsd-ruby (1.4.0)
stoplight (2.1.3)
stoplight (2.2.0)
streamio-ffmpeg (3.0.2)
multi_json (~> 1.8)
strong_migrations (0.4.2)
@@ -684,12 +685,12 @@ DEPENDENCIES
active_record_query_trace (~> 1.7)
addressable (~> 2.7)
annotate (~> 3.0)
aws-sdk-s3 (~> 1.52)
aws-sdk-s3 (~> 1.55)
better_errors (~> 2.5)
binding_of_caller (~> 0.7)
blurhash (~> 0.1)
bootsnap (~> 1.4)
brakeman (~> 4.6)
brakeman (~> 4.7)
browser
bullet (~> 6.0)
bundler-audit (~> 0.6)
@@ -712,12 +713,12 @@ DEPENDENCIES
doorkeeper (~> 5.2)
dotenv-rails (~> 2.7)
fabrication (~> 2.20)
faker (~> 2.6)
faker (~> 2.7)
fast_blank (~> 1.0)
fastimage
fog-core (<= 2.1.0)
fog-openstack (~> 0.3)
fuubar (~> 2.4)
fuubar (~> 2.5)
goldfinger (~> 2.1)
hamlit-rails (~> 0.2)
health_check!
@@ -755,7 +756,7 @@ DEPENDENCIES
ox (~> 2.11)
paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6)
parallel (~> 1.17)
parallel (~> 1.18)
parallel_tests (~> 2.29)
parslet
pg (~> 1.1)
@@ -768,7 +769,7 @@ DEPENDENCIES
pry-rails (~> 0.3)
puma (~> 4.2)
pundit (~> 2.1)
rack-attack (~> 6.1)
rack-attack (~> 6.2)
rack-cors (~> 1.0)
rails (~> 5.2.3)
rails-controller-testing (~> 1.0)
@@ -782,7 +783,7 @@ DEPENDENCIES
rqrcode (~> 0.10)
rspec-rails (~> 3.9)
rspec-sidekiq (~> 3.0)
rubocop (~> 0.75)
rubocop (~> 0.76)
rubocop-rails (~> 2.3)
ruby-progressbar (~> 1.10)
sanitize (~> 5.1)
@@ -795,7 +796,7 @@ DEPENDENCIES
simplecov (~> 0.17)
sprockets-rails (~> 3.2)
stackprof
stoplight (~> 2.1.3)
stoplight (~> 2.2.0)
streamio-ffmpeg (~> 3.0)
strong_migrations (~> 0.4)
thor (~> 0.20)

+ 1
- 1
app/controllers/api/v1/accounts/credentials_controller.rb View File

@@ -25,7 +25,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
end

def user_settings_params
return nil unless params.key?(:source)
return nil if params[:source].blank?

source_params = params.require(:source)


+ 4
- 9
app/controllers/api/v1/bookmarks_controller.rb View File

@@ -26,10 +26,9 @@ class Api::V1::BookmarksController < Api::BaseController
end

def results
@_results ||= account_bookmarks.paginate_by_max_id(
@_results ||= account_bookmarks.paginate_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
params_slice(:max_id, :since_id, :min_id)
)
end

@@ -42,15 +41,11 @@ class Api::V1::BookmarksController < Api::BaseController
end

def next_path
if records_continue?
api_v1_bookmarks_url pagination_params(max_id: pagination_max_id)
end
api_v1_bookmarks_url pagination_params(max_id: pagination_max_id) if records_continue?
end

def prev_path
unless results.empty?
api_v1_bookmarks_url pagination_params(since_id: pagination_since_id)
end
api_v1_bookmarks_url pagination_params(min_id: pagination_since_id) unless results.empty?
end

def pagination_max_id

+ 2
- 8
app/controllers/application_controller.rb View File

@@ -211,13 +211,7 @@ class ApplicationController < ActionController::Base
end

def respond_with_error(code)
respond_to do |format|
format.any { head code }

format.html do
use_pack 'error'
render "errors/#{code}", layout: 'error', status: code
end
end
use_pack 'error'
render "errors/#{code}", layout: 'error', status: code
end
end

+ 4
- 2
app/helpers/admin/action_logs_helper.rb View File

@@ -44,6 +44,8 @@ module Admin::ActionLogsHelper
'flag'
when 'DomainBlock'
'lock'
when 'DomainAllow'
'plus-circle'
when 'EmailDomainBlock'
'envelope'
when 'Status'
@@ -86,7 +88,7 @@ module Admin::ActionLogsHelper
record.shortcode
when 'Report'
link_to "##{record.id}", admin_report_path(record)
when 'DomainBlock', 'EmailDomainBlock'
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock'
link_to record.domain, "https://#{record.domain}"
when 'Status'
link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record)
@@ -99,7 +101,7 @@ module Admin::ActionLogsHelper
case type
when 'CustomEmoji'
attributes['shortcode']
when 'DomainBlock', 'EmailDomainBlock'
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock'
link_to attributes['domain'], "https://#{attributes['domain']}"
when 'Status'
tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count'))

+ 3
- 0
app/helpers/settings_helper.rb View File

@@ -36,11 +36,13 @@ module SettingsHelper
ja: '日本語',
ka: 'ქართული',
kk: 'Қазақша',
kn: 'ಕನ್ನಡ',
ko: '한국어',
lt: 'Lietuvių',
lv: 'Latviešu',
mk: 'Македонски',
ml: 'മലയാളം',
mr: 'मराठी',
ms: 'Bahasa Melayu',
nl: 'Nederlands',
nn: 'Nynorsk',
@@ -63,6 +65,7 @@ module SettingsHelper
th: 'ไทย',
tr: 'Türkçe',
uk: 'Українська',
ur: 'اُردُو',
'zh-CN': '简体中文',
'zh-HK': '繁體中文(香港)',
'zh-TW': '繁體中文(臺灣)',

+ 90
- 0
app/javascript/mastodon/actions/bookmarks.js View File

@@ -0,0 +1,90 @@
import api, { getLinks } from '../api';
import { importFetchedStatuses } from './importer';

export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST';
export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS';
export const BOOKMARKED_STATUSES_FETCH_FAIL = 'BOOKMARKED_STATUSES_FETCH_FAIL';

export const BOOKMARKED_STATUSES_EXPAND_REQUEST = 'BOOKMARKED_STATUSES_EXPAND_REQUEST';
export const BOOKMARKED_STATUSES_EXPAND_SUCCESS = 'BOOKMARKED_STATUSES_EXPAND_SUCCESS';
export const BOOKMARKED_STATUSES_EXPAND_FAIL = 'BOOKMARKED_STATUSES_EXPAND_FAIL';

export function fetchBookmarkedStatuses() {
return (dispatch, getState) => {
if (getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) {
return;
}

dispatch(fetchBookmarkedStatusesRequest());

api(getState).get('/api/v1/bookmarks').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchBookmarkedStatusesFail(error));
});
};
};

export function fetchBookmarkedStatusesRequest() {
return {
type: BOOKMARKED_STATUSES_FETCH_REQUEST,
};
};

export function fetchBookmarkedStatusesSuccess(statuses, next) {
return {
type: BOOKMARKED_STATUSES_FETCH_SUCCESS,
statuses,
next,
};
};

export function fetchBookmarkedStatusesFail(error) {
return {
type: BOOKMARKED_STATUSES_FETCH_FAIL,
error,
};
};

export function expandBookmarkedStatuses() {
return (dispatch, getState) => {
const url = getState().getIn(['status_lists', 'bookmarks', 'next'], null);

if (url === null || getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) {
return;
}

dispatch(expandBookmarkedStatusesRequest());

api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandBookmarkedStatusesFail(error));
});
};
};

export function expandBookmarkedStatusesRequest() {
return {
type: BOOKMARKED_STATUSES_EXPAND_REQUEST,
};
};

export function expandBookmarkedStatusesSuccess(statuses, next) {
return {
type: BOOKMARKED_STATUSES_EXPAND_SUCCESS,
statuses,
next,
};
};

export function expandBookmarkedStatusesFail(error) {
return {
type: BOOKMARKED_STATUSES_EXPAND_FAIL,
error,
};
};

+ 80
- 0
app/javascript/mastodon/actions/interactions.js View File

@@ -33,6 +33,14 @@ export const UNPIN_REQUEST = 'UNPIN_REQUEST';
export const UNPIN_SUCCESS = 'UNPIN_SUCCESS';
export const UNPIN_FAIL = 'UNPIN_FAIL';

export const BOOKMARK_REQUEST = 'BOOKMARK_REQUEST';
export const BOOKMARK_SUCCESS = 'BOOKMARKED_SUCCESS';
export const BOOKMARK_FAIL = 'BOOKMARKED_FAIL';

export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';

export function reblog(status) {
return function (dispatch, getState) {
dispatch(reblogRequest(status));
@@ -187,6 +195,78 @@ export function unfavouriteFail(status, error) {
};
};

export function bookmark(status) {
return function (dispatch, getState) {
dispatch(bookmarkRequest(status));

api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function (response) {
dispatch(importFetchedStatus(response.data));
dispatch(bookmarkSuccess(status, response.data));
}).catch(function (error) {
dispatch(bookmarkFail(status, error));
});
};
};

export function unbookmark(status) {
return (dispatch, getState) => {
dispatch(unbookmarkRequest(status));

api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(unbookmarkSuccess(status, response.data));
}).catch(error => {
dispatch(unbookmarkFail(status, error));
});
};
};

export function bookmarkRequest(status) {
return {
type: BOOKMARK_REQUEST,
status: status,
};
};

export function bookmarkSuccess(status, response) {
return {
type: BOOKMARK_SUCCESS,
status: status,
response: response,
};
};

export function bookmarkFail(status, error) {
return {
type: BOOKMARK_FAIL,
status: status,
error: error,
};
};

export function unbookmarkRequest(status) {
return {
type: UNBOOKMARK_REQUEST,
status: status,
};
};

export function unbookmarkSuccess(status, response) {
return {
type: UNBOOKMARK_SUCCESS,
status: status,
response: response,
};
};

export function unbookmarkFail(status, error) {
return {
type: UNBOOKMARK_FAIL,
status: status,
error: error,
};
};

export function fetchReblogs(id) {
return (dispatch, getState) => {
dispatch(fetchReblogsRequest(id));

+ 86
- 10
app/javascript/mastodon/components/status_action_bar.js View File

@@ -1,5 +1,6 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import IconButton from './icon_button';
import DropdownMenuContainer from '../containers/dropdown_menu_container';
@@ -23,6 +24,8 @@ const messages = defineMessages({
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' },
open: { id: 'status.open', defaultMessage: 'Expand this status' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
@@ -33,6 +36,10 @@ const messages = defineMessages({
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
});

const obfuscatedCount = count => {
@@ -45,7 +52,12 @@ const obfuscatedCount = count => {
}
};

export default @injectIntl
const mapStateToProps = (state, { status }) => ({
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
});

export default @connect(mapStateToProps)
@injectIntl
class StatusActionBar extends ImmutablePureComponent {

static contextTypes = {
@@ -54,6 +66,7 @@ class StatusActionBar extends ImmutablePureComponent {

static propTypes = {
status: ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.map,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
@@ -61,11 +74,16 @@ class StatusActionBar extends ImmutablePureComponent {
onDirect: PropTypes.func,
onMention: PropTypes.func,
onMute: PropTypes.func,
onUnmute: PropTypes.func,
onBlock: PropTypes.func,
onUnblock: PropTypes.func,
onBlockDomain: PropTypes.func,
onUnblockDomain: PropTypes.func,
onReport: PropTypes.func,
onEmbed: PropTypes.func,
onMuteConversation: PropTypes.func,
onPin: PropTypes.func,
onBookmark: PropTypes.func,
withDismiss: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
@@ -74,6 +92,7 @@ class StatusActionBar extends ImmutablePureComponent {
// evaluate to false. See react-immutable-pure-component for usage.
updateOnProps = [
'status',
'relationship',
'withDismiss',
]

@@ -114,6 +133,10 @@ class StatusActionBar extends ImmutablePureComponent {
window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
}

handleBookmarkClick = () => {
this.props.onBookmark(this.props.status);
}

handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.context.router.history);
}
@@ -135,11 +158,39 @@ class StatusActionBar extends ImmutablePureComponent {
}

handleMuteClick = () => {
this.props.onMute(this.props.status.get('account'));
const { status, relationship, onMute, onUnmute } = this.props;
const account = status.get('account');

if (relationship && relationship.get('muting')) {
onUnmute(account);
} else {
onMute(account);
}
}

handleBlockClick = () => {
this.props.onBlock(this.props.status);
const { status, relationship, onBlock, onUnblock } = this.props;
const account = status.get('account');

if (relationship && relationship.get('blocking')) {
onBlock(status);
} else {
onUnblock(account);
}
}

handleBlockDomain = () => {
const { status, onBlockDomain } = this.props;
const account = status.get('account');

onBlockDomain(account.get('acct').split('@')[1]);
}

handleUnblockDomain = () => {
const { status, onUnblockDomain } = this.props;
const account = status.get('account');

onUnblockDomain(account.get('acct').split('@')[1]);
}

handleOpen = () => {
@@ -178,11 +229,12 @@ class StatusActionBar extends ImmutablePureComponent {
}

render () {
const { status, intl, withDismiss } = this.props;
const { status, relationship, intl, withDismiss } = this.props;

const mutingConversation = status.get('muted');
const anonymousAccess = !me;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const account = status.get('account');

let menu = [];
let reblogIcon = 'retweet';
@@ -196,6 +248,7 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
}

menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
menu.push(null);

if (status.getIn(['account', 'id']) === me || withDismiss) {
@@ -215,16 +268,39 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
} else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });

if (relationship && relationship.get('muting')) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
} else {
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick });
}

if (relationship && relationship.get('blocking')) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
} else {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick });
}

menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport });

if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1];

menu.push(null);

if (relationship && relationship.get('domain_blocking')) {
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
} else {
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain });
}
}

if (isStaff) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
}
}

+ 6
- 0
app/javascript/mastodon/containers/dropdown_menu_container.js View File

@@ -1,4 +1,5 @@
import { openDropdownMenu, closeDropdownMenu } from '../actions/dropdown_menu';
import { fetchRelationships } from 'mastodon/actions/accounts';
import { openModal, closeModal } from '../actions/modal';
import { connect } from 'react-redux';
import DropdownMenu from '../components/dropdown_menu';
@@ -13,12 +14,17 @@ const mapStateToProps = state => ({

const mapDispatchToProps = (dispatch, { status, items }) => ({
onOpen(id, onItemClick, dropdownPlacement, keyboard) {
if (status) {
dispatch(fetchRelationships([status.getIn(['account', 'id'])]));
}

dispatch(isUserTouching() ? openModal('ACTIONS', {
status,
actions: items,
onClick: onItemClick,
}) : openDropdownMenu(id, dropdownPlacement, keyboard));
},

onClose(id) {
dispatch(closeModal('ACTIONS'));
dispatch(closeDropdownMenu(id));

+ 41
- 1
app/javascript/mastodon/containers/status_container.js View File

@@ -1,3 +1,4 @@
import React from 'react';
import { connect } from 'react-redux';
import Status from '../components/status';
import { makeGetStatus } from '../selectors';
@@ -9,8 +10,10 @@ import {
import {
reblog,
favourite,
bookmark,
unreblog,
unfavourite,
unbookmark,
pin,
unpin,
} from '../actions/interactions';
@@ -21,11 +24,19 @@ import {
hideStatus,
revealStatus,
} from '../actions/statuses';
import {
unmuteAccount,
unblockAccount,
} from '../actions/accounts';
import {
blockDomain,
unblockDomain,
} from '../actions/domain_blocks';
import { initMuteModal } from '../actions/mutes';
import { initBlockModal } from '../actions/blocks';
import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal';
import { defineMessages, injectIntl } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { boostModal, deleteModal } from '../initial_state';
import { showAlertForError } from '../actions/alerts';

@@ -36,6 +47,7 @@ const messages = defineMessages({
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
});

const makeMapStateToProps = () => {
@@ -90,6 +102,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},

onBookmark (status) {
if (status.get('bookmarked')) {
dispatch(unbookmark(status));
} else {
dispatch(bookmark(status));
}
},

onPin (status) {
if (status.get('pinned')) {
dispatch(unpin(status));
@@ -138,6 +158,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(initBlockModal(account));
},

onUnblock (account) {
dispatch(unblockAccount(account.get('id')));
},

onReport (status) {
dispatch(initReport(status.get('account'), status));
},
@@ -146,6 +170,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(initMuteModal(account));
},

onUnmute (account) {
dispatch(unmuteAccount(account.get('id')));
},

onMuteConversation (status) {
if (status.get('muted')) {
dispatch(unmuteStatus(status.get('id')));
@@ -162,6 +190,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},

onBlockDomain (domain) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => dispatch(blockDomain(domain)),
}));
},

onUnblockDomain (domain) {
dispatch(unblockDomain(domain));
},

});

export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));

+ 104
- 0
app/javascript/mastodon/features/bookmarked_statuses/index.js View File

@@ -0,0 +1,104 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from '../../actions/bookmarks';
import Column from '../ui/components/column';
import ColumnHeader from '../../components/column_header';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import StatusList from '../../components/status_list';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { debounce } from 'lodash';

const messages = defineMessages({
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
});

const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'bookmarks', 'items']),
isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
});

export default @connect(mapStateToProps)
@injectIntl
class Bookmarks extends ImmutablePureComponent {

static propTypes = {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
statusIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
};

componentWillMount () {
this.props.dispatch(fetchBookmarkedStatuses());
}

handlePin = () => {
const { columnId, dispatch } = this.props;

if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('BOOKMARKS', {}));
}
}

handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}

handleHeaderClick = () => {
this.column.scrollTop();
}

setRef = c => {
this.column = c;
}

handleLoadMore = debounce(() => {
this.props.dispatch(expandBookmarkedStatuses());
}, 300, { leading: true })

render () {
const { intl, shouldUpdateScroll, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
const pinned = !!columnId;

const emptyMessage = <FormattedMessage id='empty_column.bookmarked_statuses' defaultMessage="You don't have any bookmarked toots yet. When you bookmark one, it will show up here." />;

return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
<ColumnHeader
icon='bookmark'
title={intl.formatMessage(messages.heading)}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
showBackButton
/>

<StatusList
trackScroll={!pinned}
statusIds={statusIds}
scrollKey={`bookmarked_statuses-${columnId}`}
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
/>
</Column>
);
}

}

+ 4
- 3
app/javascript/mastodon/features/community_timeline/index.js View File

@@ -14,15 +14,16 @@ const messages = defineMessages({
title: { id: 'column.community', defaultMessage: 'Local timeline' },
});

const mapStateToProps = (state, { onlyMedia, columnId }) => {
const mapStateToProps = (state, { columnId }) => {
const uuid = columnId;
const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid);
const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'community', 'other', 'onlyMedia']);
const timelineState = state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`]);

return {
hasUnread: !!timelineState && (timelineState.get('unread') > 0 || timelineState.get('pendingItems').size > 0),
onlyMedia: (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'community', 'other', 'onlyMedia']),
hasUnread: !!timelineState && timelineState.get('unread') > 0,
onlyMedia,
};
};


+ 3
- 1
app/javascript/mastodon/features/getting_started/index.js View File

@@ -22,6 +22,7 @@ const messages = defineMessages({
settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' },
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
@@ -126,11 +127,12 @@ class GettingStarted extends ImmutablePureComponent {

navItems.push(
<ColumnLink key={i++} icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />,
<ColumnLink key={i++} icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />,
<ColumnLink key={i++} icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key={i++} icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />
);

height += 48*3;
height += 48*4;

if (myAccount.get('locked') || unreadFollowRequests > 0) {
navItems.push(<ColumnLink key={i++} icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);

+ 5
- 3
app/javascript/mastodon/features/public_timeline/index.js View File

@@ -14,14 +14,16 @@ const messages = defineMessages({
title: { id: 'column.public', defaultMessage: 'Federated timeline' },
});

const mapStateToProps = (state, { onlyMedia, columnId }) => {
const mapStateToProps = (state, { columnId }) => {
const uuid = columnId;
const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid);
const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']);
const timelineState = state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`]);

return {
hasUnread: state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`, 'unread']) > 0,
onlyMedia: (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']),
hasUnread: !!timelineState && timelineState.get('unread') > 0,
onlyMedia,
};
};


+ 86
- 11
app/javascript/mastodon/features/status/components/action_bar.js View File

@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import IconButton from '../../../components/icon_button';
import ImmutablePropTypes from 'react-immutable-proptypes';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
@@ -17,6 +18,7 @@ const messages = defineMessages({
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
@@ -29,9 +31,18 @@ const messages = defineMessages({
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
});

export default @injectIntl
const mapStateToProps = (state, { status }) => ({
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
});

export default @connect(mapStateToProps)
@injectIntl
class ActionBar extends React.PureComponent {

static contextTypes = {
@@ -40,15 +51,21 @@ class ActionBar extends React.PureComponent {

static propTypes = {
status: ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.map,
onReply: PropTypes.func.isRequired,
onReblog: PropTypes.func.isRequired,
onFavourite: PropTypes.func.isRequired,
onBookmark: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onMute: PropTypes.func,
onMuteConversation: PropTypes.func,
onUnmute: PropTypes.func,
onBlock: PropTypes.func,
onUnblock: PropTypes.func,
onBlockDomain: PropTypes.func,
onUnblockDomain: PropTypes.func,
onMuteConversation: PropTypes.func,
onReport: PropTypes.func,
onPin: PropTypes.func,
onEmbed: PropTypes.func,
@@ -67,6 +84,10 @@ class ActionBar extends React.PureComponent {
this.props.onFavourite(this.props.status);
}

handleBookmarkClick = (e) => {
this.props.onBookmark(this.props.status, e);
}

handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.context.router.history);
}
@@ -84,15 +105,43 @@ class ActionBar extends React.PureComponent {
}

handleMuteClick = () => {
this.props.onMute(this.props.status.get('account'));
}
const { status, relationship, onMute, onUnmute } = this.props;
const account = status.get('account');

handleConversationMuteClick = () => {
this.props.onMuteConversation(this.props.status);
if (relationship && relationship.get('muting')) {
onUnmute(account);
} else {
onMute(account);
}
}

handleBlockClick = () => {
this.props.onBlock(this.props.status);
const { status, relationship, onBlock, onUnblock } = this.props;
const account = status.get('account');

if (relationship && relationship.get('blocking')) {
onBlock(status);
} else {
onUnblock(account);
}
}

handleBlockDomain = () => {
const { status, onBlockDomain } = this.props;
const account = status.get('account');

onBlockDomain(account.get('acct').split('@')[1]);
}

handleUnblockDomain = () => {
const { status, onUnblockDomain } = this.props;
const account = status.get('account');

onUnblockDomain(account.get('acct').split('@')[1]);
}

handleConversationMuteClick = () => {
this.props.onMuteConversation(this.props.status);
}

handleReport = () => {
@@ -134,10 +183,11 @@ class ActionBar extends React.PureComponent {
}

render () {
const { status, intl } = this.props;
const { status, relationship, intl } = this.props;

const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const mutingConversation = status.get('muted');
const account = status.get('account');

let menu = [];

@@ -165,9 +215,33 @@ class ActionBar extends React.PureComponent {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });

if (relationship && relationship.get('muting')) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
} else {
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick });
}

if (relationship && relationship.get('blocking')) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
} else {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick });
}

menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });

if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1];

menu.push(null);

if (relationship && relationship.get('domain_blocking')) {
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
} else {
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain });
}
}

if (isStaff) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
@@ -198,9 +272,10 @@ class ActionBar extends React.PureComponent {
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
{shareButton}
<div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>

<div className='detailed-status__action-bar-dropdown'>
<DropdownMenuContainer size={18} icon='ellipsis-h' items={menu} direction='left' title='More' />
<DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title='More' />
</div>
</div>
);

+ 46
- 1
app/javascript/mastodon/features/status/index.js View File

@@ -13,6 +13,8 @@ import Column from '../ui/components/column';
import {
favourite,
unfavourite,
bookmark,
unbookmark,
reblog,
unreblog,
pin,
@@ -30,6 +32,14 @@ import {
hideStatus,
revealStatus,
} from '../../actions/statuses';
import {
unblockAccount,
unmuteAccount,
} from '../../actions/accounts';
import {
blockDomain,
unblockDomain,
} from '../../actions/domain_blocks';
import { initMuteModal } from '../../actions/mutes';
import { initBlockModal } from '../../actions/blocks';
import { initReport } from '../../actions/reports';
@@ -39,7 +49,7 @@ import ColumnBackButton from '../../components/column_back_button';
import ColumnHeader from '../../components/column_header';
import StatusContainer from '../../containers/status_container';
import { openModal } from '../../actions/modal';
import { defineMessages, injectIntl } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys';
import { boostModal, deleteModal } from '../../initial_state';
@@ -57,6 +67,7 @@ const messages = defineMessages({
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
});

const makeMapStateToProps = () => {
@@ -232,6 +243,14 @@ class Status extends ImmutablePureComponent {
}
}

handleBookmarkClick = (status) => {
if (status.get('bookmarked')) {
this.props.dispatch(unbookmark(status));
} else {
this.props.dispatch(bookmark(status));
}
}

handleDeleteClick = (status, history, withRedraft = false) => {
const { dispatch, intl } = this.props;

@@ -307,6 +326,27 @@ class Status extends ImmutablePureComponent {
this.props.dispatch(openModal('EMBED', { url: status.get('url') }));
}

handleUnmuteClick = account => {
this.props.dispatch(unmuteAccount(account.get('id')));
}

handleUnblockClick = account => {
this.props.dispatch(unblockAccount(account.get('id')));
}

handleBlockDomainClick = domain => {
this.props.dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: this.props.intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => this.props.dispatch(blockDomain(domain)),
}));
}

handleUnblockDomainClick = domain => {
this.props.dispatch(unblockDomain(domain));
}


handleHotkeyMoveUp = () => {
this.handleMoveUp(this.props.status.get('id'));
}
@@ -499,12 +539,17 @@ class Status extends ImmutablePureComponent {
onReply={this.handleReplyClick}
onFavourite={this.handleFavouriteClick}
onReblog={this.handleReblogClick}
onBookmark={this.handleBookmarkClick}
onDelete={this.handleDeleteClick}
onDirect={this.handleDirectClick}
onMention={this.handleMentionClick}
onMute={this.handleMuteClick}
onUnmute={this.handleUnmuteClick}
onMuteConversation={this.handleConversationMuteClick}
onBlock={this.handleBlockClick}
onUnblock={this.handleUnblockClick}
onBlockDomain={this.handleBlockDomainClick}
onUnblockDomain={this.handleUnblockDomainClick}
onReport={this.handleReport}
onPin={this.handlePin}
onEmbed={this.handleEmbed}

+ 2
- 0
app/javascript/mastodon/features/ui/components/columns_area.js View File

@@ -21,6 +21,7 @@ import {
HashtagTimeline,
DirectTimeline,
FavouritedStatuses,
BookmarkedStatuses,
ListTimeline,
Directory,
} from '../../ui/util/async-components';
@@ -40,6 +41,7 @@ const componentMap = {
'HASHTAG': HashtagTimeline,
'DIRECT': DirectTimeline,
'FAVOURITES': FavouritedStatuses,
'BOOKMARKS': BookmarkedStatuses,
'LIST': ListTimeline,
'DIRECTORY': Directory,
};

+ 1
- 0
app/javascript/mastodon/features/ui/components/navigation_panel.js View File

@@ -17,6 +17,7 @@ const NavigationPanel = () => (
<NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
{profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>}


+ 2
- 0
app/javascript/mastodon/features/ui/index.js View File

@@ -41,6 +41,7 @@ import {
FollowRequests,
GenericNotFound,
FavouritedStatuses,
BookmarkedStatuses,
ListTimeline,
Blocks,
DomainBlocks,
@@ -190,6 +191,7 @@ class SwitchingColumnsArea extends React.PureComponent {

<WrappedRoute path='/notifications' component={Notifications} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />

<WrappedRoute path='/search' component={Search} content={children} />

+ 4
- 0
app/javascript/mastodon/features/ui/util/async-components.js View File

@@ -90,6 +90,10 @@ export function FavouritedStatuses () {
return import(/* webpackChunkName: "features/favourited_statuses" */'../../favourited_statuses');
}

export function BookmarkedStatuses () {
return import(/* webpackChunkName: "features/bookmarked_statuses" */'../../bookmarked_statuses');
}

export function Blocks () {
return import(/* webpackChunkName: "features/blocks" */'../../blocks');
}

+ 9
- 9
app/javascript/mastodon/locales/ar.json View File

@@ -10,7 +10,7 @@
"account.edit_profile": "تعديل الملف التعريفي",
"account.endorse": "أوصِ به على صفحتك",
"account.follow": "تابِع",
"account.followers": "متابعون",
"account.followers": "مُتابِعون",
"account.followers.empty": "لا أحد يتبع هذا الحساب بعد.",
"account.follows": "يتبع",
"account.follows.empty": "هذا الحساب لا يتبع أحدًا بعد.",
@@ -27,7 +27,7 @@
"account.muted": "مكتوم",
"account.never_active": "أبدا",
"account.posts": "تبويقات",
"account.posts_with_replies": "التبويقات و الردود",
"account.posts_with_replies": "التبويقات والردود",
"account.report": "ابلِغ عن @{name}",
"account.requested": "في انتظار الموافقة. اضْغَطْ/ي لإلغاء طلب المتابعة",
"account.share": "شارك ملف تعريف @{name}",
@@ -53,7 +53,7 @@
"column.blocks": "الحسابات المحجوبة",
"column.community": "الخيط العام المحلي",
"column.direct": "الرسائل المباشرة",
"column.directory": "استعرض الملفات التعريفية",
"column.directory": "استعراض الملفات التعريفية",
"column.domain_blocks": "النطاقات المخفية",
"column.favourites": "المفضلة",
"column.follow_requests": "طلبات المتابعة",
@@ -106,7 +106,7 @@
"confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.",
"confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟",
"confirmations.redraft.confirm": "إزالة و إعادة الصياغة",
"confirmations.redraft.message": "هل أنت متأكد من أنك تريد حذف هذا المنشور و إعادة صياغته ؟ سوف تفقد جميع الإعجابات و الترقيات أما الردود المتصلة به فستُصبِح يتيمة.",
"confirmations.redraft.message": "هل أنت متأكد من أنك تريد حذف هذا المنشور و إعادة صياغته؟ سوف تفقد جميع الإعجابات و الترقيات أما الردود المتصلة به فستُصبِح يتيمة.",
"confirmations.reply.confirm": "رد",
"confirmations.reply.message": "الرد في الحين سوف يُعيد كتابة الرسالة التي أنت بصدد كتابتها. متأكد من أنك تريد المواصلة؟",
"confirmations.unfollow.confirm": "إلغاء المتابعة",
@@ -176,7 +176,7 @@
"hashtag.column_settings.tag_mode.none": "لا شيء مِن هذه",
"hashtag.column_settings.tag_toggle": "إدراج الوسوم الإضافية لهذا العمود",
"home.column_settings.basic": "الأساسية",
"home.column_settings.show_reblogs": "عرض الترقيات",
"home.column_settings.show_reblogs": "اعرض الترقيات",
"home.column_settings.show_replies": "اعرض الردود",
"intervals.full.days": "{number, plural, one {# يوم} other {# أيام}}",
"intervals.full.hours": "{number, plural, one {# ساعة} other {# ساعات}}",
@@ -282,7 +282,7 @@
"notifications.column_settings.favourite": "المُفَضَّلة:",
"notifications.column_settings.filter_bar.advanced": "اعرض كافة الفئات",
"notifications.column_settings.filter_bar.category": "شريط الفلترة السريعة",
"notifications.column_settings.filter_bar.show": "اعرض",
"notifications.column_settings.filter_bar.show": "اظهِره",
"notifications.column_settings.follow": "متابعُون جُدُد:",
"notifications.column_settings.mention": "الإشارات:",
"notifications.column_settings.poll": "نتائج استطلاع الرأي:",
@@ -299,7 +299,7 @@
"notifications.group": "{count} إشعارات",
"poll.closed": "انتهى",
"poll.refresh": "تحديث",
"poll.total_people": "{count, plural, one {# شخص} other {# أشخاص}}",
"poll.total_people": "{count, plural, one {# شخص} two {# شخصين} few {# أشخاص} many {# أشخاص} other {# أشخاص}}",
"poll.total_votes": "{count, plural, one {# صوت} other {# أصوات}}",
"poll.vote": "صَوّت",
"poll.voted": "لقد صوّتت على هذه الإجابة",
@@ -340,7 +340,7 @@
"search_results.hashtags": "الوُسوم",
"search_results.statuses": "التبويقات",
"search_results.statuses_fts_disabled": "البحث في التبويقات عن طريق المحتوى ليس مفعل في خادم ماستدون هذا.",
"search_results.total": "{count, number} {count, plural, one {result} و {results}}",
"search_results.total": "{count, number} {count, plural, zero {} one {نتيجة} two {نتيجتين} few {نتائج} many {نتائج} other {نتائج}}",
"status.admin_account": "افتح الواجهة الإدارية لـ @{name}",
"status.admin_status": "افتح هذا المنشور على واجهة الإشراف",
"status.block": "احجب @{name}",
@@ -393,7 +393,7 @@
"time_remaining.minutes": "{number, plural, one {# دقيقة} other {# دقائق}} متبقية",
"time_remaining.moments": "لحظات متبقية",
"time_remaining.seconds": "{number, plural, one {# ثانية} other {# ثوانٍ}} متبقية",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} آخرون {people}} يتحدثون",
"trends.count_by_accounts": "{count} {rawCount, plural, zero {} one {شخص واحد} two {شخصين} few {أشخاص} many {أشخاص} other {أشخاص}} تتحدّث",
"trends.trending_now": "المتداولة الآن",
"ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.",
"upload_area.title": "اسحب ثم أفلت للرفع",

+ 5
- 5
app/javascript/mastodon/locales/bg.json View File

@@ -4,19 +4,19 @@
"account.block": "Блокирай",
"account.block_domain": "скрий всичко от (домейн)",
"account.blocked": "Блокирани",
"account.cancel_follow_request": "Cancel follow request",
"account.cancel_follow_request": "Откажи искането за следване",
"account.direct": "Direct Message @{name}",
"account.domain_blocked": "Скрит домейн",
"account.edit_profile": "Редактирай профила си",
"account.endorse": "Feature on profile",
"account.endorse": "Характеристика на профила",
"account.follow": "Последвай",
"account.followers": "Последователи",
"account.followers.empty": "No one follows this user yet.",
"account.followers.empty": "Все още никой не следва този потребител.",
"account.follows": "Следвам",
"account.follows.empty": "This user doesn't follow anyone yet.",
"account.follows.empty": "Този потребител все още не следва никого.",
"account.follows_you": "Твой последовател",
"account.hide_reblogs": "Hide boosts from @{name}",
"account.last_status": "Last active",
"account.last_status": "Последно активен/а",
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Media",

+ 74
- 74
app/javascript/mastodon/locales/bn.json View File

@@ -1,31 +1,31 @@
{
"account.add_or_remove_from_list": "তালিকাতে আরো যুক্ত বা মুছে ফেলুন",
"account.badges.bot": "রোবট",
"account.block": "@{name} কে বন্ধ করুন",
"account.block_domain": "{domain} থেকে সব সরিয়ে ফেলুন",
"account.blocked": "বন্ধ করা হয়েছে",
"account.cancel_follow_request": "Cancel follow request",
"account.direct": "@{name} এর কাছে সরকারি লেখা পাঠাতে",
"account.domain_blocked": "ওয়েবসাইট সরিয়ে ফেলা হয়েছে",
"account.edit_profile": "নিজের পাতা সম্পাদনা করতে",
"account.endorse": "আপনার নিজের পাতায় দেখাতে",
"account.add_or_remove_from_list": "তালিকা হতে মুছুন অথবা যুক্ত করুন",
"account.badges.bot": "বট",
"account.block": "@{name} কে ব্লক করুন",
"account.block_domain": "{domain} থেকে সব লুকান",
"account.blocked": "ব্লককৃত",
"account.cancel_follow_request": "অসুসারণ অনুরোধ বাতিল করুন",
"account.direct": "@{name} কে সরাসরি বার্তা",
"account.domain_blocked": "ডোমেন লুকানো আছে",
"account.edit_profile": "প্রোফাইল সম্পাদন করুন",
"account.endorse": "নিজের পাতায় দেখান",
"account.follow": "অনুসরণ করুন",
"account.followers": "অনুসরণকারক",
"account.followers.empty": "এই ব্যবহারকারীকে কেও এখনো অনুসরণ করে না।",
"account.follows": "যাদেরকে অনুসরণ করেন",
"account.follows.empty": "এই ব্যবহারকারী কাওকে এখনো অনুসরণ করেন না।",
"account.follows_you": "আপনাকে অনুসরণ করে",
"account.hide_reblogs": "@{name}র সমর্থনগুলি সরিয়ে ফেলুন",
"account.last_status": "Last active",
"account.hide_reblogs": "@{name}'র সমর্থনগুলি লুকিয়ে ফেলুন",
"account.last_status": "শেষ সক্রিয় ছিল",
"account.link_verified_on": "এই লিংকের মালিকানা চেক করা হয়েছে {date} তারিকে",
"account.locked_info": "এই নিবন্ধনের গোপনীয়তার ক্ষেত্র তালা দেওয়া আছে। নিবন্ধনকারী অনুসরণ করার অনুমতি যাদেরকে দেবেন, শুধু তারাই অনুসরণ করতে পারবেন।",
"account.media": "ছবি বা ভিডিও",
"account.mention": "@{name} কে উল্লেখ করতে",
"account.moved_to": "{name} চলে গেছে এখানে:",
"account.mute": "@{name} সব কার্যক্রম আপনার সময়রেখা থেকে সরিয়ে ফেলতে",
"account.media": "মিডিয়া",
"account.mention": "@{name} কে উল্লেখ করুন",
"account.moved_to": "{name} কে এখানে সরানো হয়েছে:",
"account.mute": "@{name} কে নিঃশব্দ করুন",
"account.mute_notifications": "@{name}র প্রজ্ঞাপন আপনার কাছ থেকে সরিয়ে ফেলুন",
"account.muted": "সরানো আছে",
"account.never_active": "Never",
"account.never_active": "কখনও নয়",
"account.posts": "টুট",
"account.posts_with_replies": "টুট এবং মতামত",
"account.report": "@{name} কে রিপোর্ট করতে",
@@ -33,16 +33,16 @@
"account.share": "@{name}র পাতা অন্যদের দেখান",
"account.show_reblogs": "@{name}র সমর্থনগুলো দেখুন",
"account.unblock": "@{name}র কার্যকলাপ আবার দেখুন",
"account.unblock_domain": "{domain}থেকে আবার দেখুন",
"account.unblock_domain": "{domain} থেকে আবার দেখুন",
"account.unendorse": "আপনার নিজের পাতায় এটা না দেখাতে",
"account.unfollow": "অনুসরণ না করতে",
"account.unmute": "@{name}র কার্যকলাপ আবার দেখুন",
"account.unmute_notifications": "@{name}র প্রজ্ঞাপন দেওয়ার অনুমতি দিন",
"alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
"alert.rate_limited.title": "Rate limited",
"alert.rate_limited.message": "{retry_time, time, medium} -এর পরে আবার প্রচেষ্টা করুন।",
"alert.rate_limited.title": "হার সীমিত",
"alert.unexpected.message": "অপ্রত্যাশিত একটি সমস্যা হয়েছে।",
"alert.unexpected.title": "ওহো!",
"autosuggest_hashtag.per_week": "{count} per week",
"autosuggest_hashtag.per_week": "প্রতি সপ্তাহে {count}",
"boost_modal.combo": "পরেরবার আপনি {combo} চাপ দিলে এটার শেষে চলে যেতে পারবেন",
"bundle_column_error.body": "এই অংশটি দেখতে যেয়ে কোনো সমস্যা হয়েছে।",
"bundle_column_error.retry": "আবার চেষ্টা করুন",
@@ -50,11 +50,11 @@
"bundle_modal_error.close": "বন্ধ করুন",
"bundle_modal_error.message": "এই অংশটি দেখাতে যেয়ে কোনো সমস্যা হয়েছে।",
"bundle_modal_error.retry": "আবার চেষ্টা করুন",
"column.blocks": "যাদের বন্ধ করে রাখা হয়েছে",
"column.blocks": "যাদের ব্লক করে রাখা হয়েছে",
"column.community": "স্থানীয় সময়সারি",
"column.direct": "সরাসরি লেখা",
"column.directory": "Browse profiles",
"column.domain_blocks": "সরিয়ে ফেলা ওয়েবসাইট",
"column.directory": "প্রোফাইল ব্রাউজ করুন",
"column.domain_blocks": "লুকোনো ডোমেনগুলি",
"column.favourites": "পছন্দের গুলো",
"column.follow_requests": "অনুসরণের অনুমতি চেয়েছে যারা",
"column.home": "বাড়ি",
@@ -87,23 +87,23 @@
"compose_form.sensitive.hide": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করতে",
"compose_form.sensitive.marked": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করা হয়েছে",
"compose_form.sensitive.unmarked": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করা হয়নি",
"compose_form.spoiler.marked": "লেখাটি সাবধানতার পেছনে লুকানো আছে",
"compose_form.spoiler.marked": "সতর্কতার পিছনে লেখানটি লুকানো আছে",
"compose_form.spoiler.unmarked": "লেখাটি লুকানো নেই",
"compose_form.spoiler_placeholder": "আপনার লেখা দেখার সাবধানবাণী লিখুন",
"confirmation_modal.cancel": "বাতিল করুন",
"confirmations.block.block_and_report": "বন্ধ করুন এবং রিপোর্ট করুন",
"confirmations.block.confirm": "বন্ধ করুন",
"confirmations.block.message": "আপনি কি নিশ্চিত {name} কে বন্ধ করতে চান ?",
"confirmations.block.block_and_report": "ব্লক করুন এবং রিপোর্ট করুন",
"confirmations.block.confirm": "ব্লক করুন",
"confirmations.block.message": "আপনি কি নিশ্চিত {name} কে ব্লক করতে চান?",
"confirmations.delete.confirm": "মুছে ফেলুন",
"confirmations.delete.message": "আপনি কি নিশ্চিত যে এই লেখাটি মুছে ফেলতে চান ?",
"confirmations.delete_list.confirm": "মুছে ফেলুন",
"confirmations.delete_list.message": "আপনি কি নিশ্চিত যে আপনি এই তালিকাটি স্থায়িভাবে মুছে ফেলতে চান ?",
"confirmations.domain_block.confirm": "এই ওয়েবসাইট থেকে সব সরান",
"confirmations.domain_block.message": "আপনি কি সত্যি সত্যি নিশ্চিত যে {domain} ওয়েবসাইট থেকে সব সরাতে চান ? সাধারণত কিছু লক্ষ্যবস্তু বন্ধ এবং সরানোযা যথেষ্ট। নিশ্চিত করলে ওই ওয়েবসাইট থেকে কোনোকিছু কোনখানে দেখবেন না। যারা আপনাকে অনুসরণ করে ওই ওয়েবসাইট থেকে তাদেরকেও মুছে ফেলা হবে।",
"confirmations.logout.confirm": "Log out",
"confirmations.logout.message": "Are you sure you want to log out?",
"confirmations.domain_block.confirm": "এই ডোমেন থেকে সব লুকান",
"confirmations.domain_block.message": "আপনি কি সত্যিই সত্যই নিশ্চিত যে আপনি পুরো {domain}'টি ব্লক করতে চান? বেশিরভাগ ক্ষেত্রে কয়েকটি লক্ষ্যযুক্ত ব্লক বা নীরবতা যথেষ্ট এবং পছন্দসই। আপনি কোনও পাবলিক টাইমলাইন বা আপনার বিজ্ঞপ্তিগুলিতে সেই ডোমেন থেকে সামগ্রী দেখতে পাবেন না। সেই ডোমেন থেকে আপনার অনুসরণকারীদের সরানো হবে।",
"confirmations.logout.confirm": "প্রস্থান",
"confirmations.logout.message": "আপনি লগ আউট করতে চান?",
"confirmations.mute.confirm": "সরিয়ে ফেলুন",
"confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.",
"confirmations.mute.explanation": "এটি তাদের কাছ থেকে পোস্ট এবং তাদেরকে মেনশন করা পোস্টগুলি হাইড করবে, তবুও তাদেরকে এটি আপনার পোস্ট গুলো দেখতে দিবে ও তারা আপনাকে অনুসরন করতে পারবে।.",
"confirmations.mute.message": "আপনি কি নিশ্চিত {name} সরিয়ে ফেলতে চান ?",
"confirmations.redraft.confirm": "মুছে ফেলুন এবং আবার সম্পাদন করুন",
"confirmations.redraft.message": "আপনি কি নিশ্চিত এটি মুছে ফেলে এবং আবার সম্পাদন করতে চান ? এটাতে যা পছন্দিত, সমর্থন বা মতামত আছে সেগুলো নতুন লেখার সাথে যুক্ত থাকবে না।",
@@ -111,14 +111,14 @@
"confirmations.reply.message": "এখন মতামত লিখতে গেলে আপনার এখন যেটা লিখছেন সেটা মুছে যাবে। আপনি নি নিশ্চিত এটা করতে চান ?",
"confirmations.unfollow.confirm": "অনুসরণ করা বাতিল করতে",
"confirmations.unfollow.message": "আপনি কি নিশ্চিত {name} কে আর অনুসরণ করতে চান না ?",
"conversation.delete": "Delete conversation",
"conversation.mark_as_read": "Mark as read",
"conversation.open": "View conversation",
"conversation.with": "With {names}",
"directory.federated": "From known fediverse",
"directory.local": "From {domain} only",
"directory.new_arrivals": "New arrivals",
"directory.recently_active": "Recently active",
"conversation.delete": "কথোপকথন মুছে ফেলুন",
"conversation.mark_as_read": "পঠিত হিসেবে চিহ্নিত করুন",
"conversation.open": "কথপোকথন দেখান",
"conversation.with": "{names} এর সঙ্গে",
"directory.federated": "পরিচিত ফেডিভারসের থেকে",
"directory.local": "শুধু {domain} থেকে",
"directory.new_arrivals": "নতুন আগত",
"directory.recently_active": "সম্প্রতি সক্রিয়",
"embed.instructions": "এই লেখাটি আপনার ওয়েবসাইটে যুক্ত করতে নিচের কোডটি বেবহার করুন।",
"embed.preview": "সেটা দেখতে এরকম হবে:",
"emoji_button.activity": "কার্যকলাপ",
@@ -137,25 +137,25 @@
"emoji_button.travel": "ভ্রমণ এবং স্থান",
"empty_column.account_timeline": "এখানে কোনো টুট নেই!",
"empty_column.account_unavailable": "নিজস্ব পাতা নেই",
"empty_column.blocks": "আপনি কোনো ব্যবহারকারীদের বন্ধ করেন নি।",
"empty_column.blocks": "আপনি কোনো ব্যবহারকারীদের ব্লক করেন নি।",
"empty_column.community": "স্থানীয় সময়রেখাতে কিছু নেই। প্রকাশ্যভাবে কিছু লিখে লেখালেখির উদ্বোধন করে ফেলুন!",
"empty_column.direct": "আপনার কাছে সরাসরি পাঠানো কোনো লেখা নেই। যদি কেও পাঠায়, সেটা এখানে দেখা যাবে।",
"empty_column.domain_blocks": "এখনো কোনো সরানো ওয়েবসাইট নেই।",
"empty_column.domain_blocks": "এখনও কোনও লুকানো ডোমেন নেই।",
"empty_column.favourited_statuses": "আপনার পছন্দের কোনো টুট এখনো নেই। আপনি কোনো লেখা পছন্দের হিসেবে চিহ্নিত করলে এখানে পাওয়া যাবে।",
"empty_column.favourites": "কেও এখনো এটাকে পছন্দের টুট হিসেবে চিহ্নিত করেনি। যদি করে, তখন তাদের এখানে পাওয়া যাবে।",
"empty_column.follow_requests": "আপনার এখনো কোনো অনুসরণের আবেদন পাঠানো নেই। যদি পাঠায়, এখানে পাওয়া যাবে।",
"empty_column.hashtag": "এই হেসটাগে এখনো কিছু নেই।",
"empty_column.home": "আপনার বাড়ির সময়রেখা এখনো খালি! {public}এ ঘুরে আসুন অথবা অনুসন্ধান বেবহার করে শুরু করতে পারেন এবং অন্য ব্যবহারকারীদের সাথে সাক্ষাৎ করতে পারেন।",
"empty_column.home": "আপনার বাড়ির সময়রেখা এখনো খালি! {public} এ ঘুরে আসুন অথবা অনুসন্ধান বেবহার করে শুরু করতে পারেন এবং অন্য ব্যবহারকারীদের সাথে সাক্ষাৎ করতে পারেন।",
"empty_column.home.public_timeline": "প্রকাশ্য সময়রেখা",
"empty_column.list": "এই তালিকাতে এখনো কিছু নেই. যখন এই তালিকায় থাকা ব্যবহারকারী নতুন কিছু লিখবে, সেগুলো এখানে পাওয়া যাবে।",
"empty_column.lists": "আপনার এখনো কোনো তালিকা তৈরী নেই। যদি বা যখন তৈরী করেন, সেগুলো এখানে পাওয়া যাবে।",
"empty_column.mutes": "আপনি এখনো কোনো ব্যবহারকারীকে সরাননি।",
"empty_column.mutes": "আপনি এখনো কোনো ব্যবহারকারীকে নিঃশব্দ করেননি।",
"empty_column.notifications": "আপনার এখনো কোনো প্রজ্ঞাপন নেই। কথোপকথন শুরু করতে, অন্যদের সাথে মেলামেশা করতে পারেন।",
"empty_column.public": "এখানে এখনো কিছু নেই! প্রকাশ্য ভাবে কিছু লিখুন বা অন্য সার্ভার থেকে কাওকে অনুসরণ করে এই জায়গা ভরে ফেলুন",
"error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.",
"error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard",
"errors.unexpected_crash.report_issue": "Report issue",
"error.unexpected_crash.explanation": "আমাদের কোড বা ব্রাউজারের সামঞ্জস্য ইস্যুতে একটি বাগের কারণে এই পৃষ্ঠাটি সঠিকভাবে প্রদর্শিত করা যায় নি।",
"error.unexpected_crash.next_steps": "পাতাটি রিফ্রেশ করে চেষ্টা করুন। তবুও যদি না হয়, তবে আপনি অন্য একটি ব্রাউজার অথবা আপনার ডিভাইসের জন্যে এপের মাধ্যমে মাস্টডন ব্যাবহার করতে পারবেন।.",
"errors.unexpected_crash.copy_stacktrace": "স্টেকট্রেস ক্লিপবোর্ডে কপি করুন",
"errors.unexpected_crash.report_issue": "সমস্যার প্রতিবেদন করুন",
"follow_request.authorize": "অনুমতি দিন",
"follow_request.reject": "প্রত্যাখ্যান করুন",
"getting_started.developers": "তৈরিকারকদের জন্য",
@@ -180,7 +180,7 @@
"home.column_settings.show_replies": "মতামত দেখান",
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# ঘটা} other {# ঘটা}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
"intervals.full.minutes": "{number, plural, one {# মিনিট} other {# মিনিট}}",
"introduction.federation.action": "পরবর্তী",
"introduction.federation.federated.headline": "যুক্তবিশ্ব",
"introduction.federation.federated.text": "অন্যান্য যুক্তবিশ্বের সার্ভারের লেখাগুলি যুক্তবিশ্বের সময়রেখাতে আসবে ।",
@@ -199,7 +199,7 @@
"introduction.welcome.headline": "প্রথম ধাপ",
"introduction.welcome.text": "যুক্তবিশ্বে স্বাগতম! কিছুক্ষনের মধ্যেই আপনি আপনার লেখা বিভিন্ন সার্ভারে সম্প্রচার করতে পারবেন। কিন্তু মনে রাখবে যে এটা একটা বিশেষ সার্ভার, {domain} কারণ এখানে আপনার নিজেস্ব পাতা রাখা হচ্ছে।",
"keyboard_shortcuts.back": "পেছনে যেতে",
"keyboard_shortcuts.blocked": "বন্ধ করা ব্যবহারকারীদের তালিকা দেখতে",
"keyboard_shortcuts.blocked": "ব্লক করা ব্যবহারকারীদের তালিকা খুলতে",
"keyboard_shortcuts.boost": "সমর্থন করতে",
"keyboard_shortcuts.column": "কোনো কলামএ কোনো লেখা ফোকাস করতে",
"keyboard_shortcuts.compose": "লেখা সম্পদনার জায়গায় ফোকাস করতে",
@@ -243,7 +243,7 @@
"lists.new.title_placeholder": "তালিকার নতুন শিরোনাম দিতে",
"lists.search": "যাদের অনুসরণ করেন তাদের ভেতরে খুঁজুন",
"lists.subheading": "আপনার তালিকা",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"load_pending": "{count, plural, one {# নতুন জিনিস} other {# নতুন জিনিস}}",
"loading_indicator.label": "আসছে...",
"media_gallery.toggle_visible": "দৃশ্যতার অবস্থা বদলান",
"missing_indicator.label": "খুঁজে পাওয়া যায়নি",
@@ -255,17 +255,17 @@
"navigation_bar.compose": "নতুন টুট লিখুন",
"navigation_bar.direct": "সরাসরি লেখাগুলি",
"navigation_bar.discover": "ঘুরে দেখুন",
"navigation_bar.domain_blocks": "বন্ধ করা ওয়েবসাইট",
"navigation_bar.domain_blocks": "লুকানো ডোমেনগুলি",
"navigation_bar.edit_profile": "নিজের পাতা সম্পাদনা করতে",
"navigation_bar.favourites": "পছন্দের",
"navigation_bar.filters": "বন্ধ করা শব্দ",
"navigation_bar.follow_requests": "অনুসরণের অনুরোধগুলি",
"navigation_bar.follows_and_followers": "যাদেরকে অনুসরণ করেন এবং যারা তাকে অনুসরণ করে",
"navigation_bar.follows_and_followers": "অনুসরণ এবং অনুসরণকারী",
"navigation_bar.info": "এই সার্ভার সম্পর্কে",
"navigation_bar.keyboard_shortcuts": "হটকীগুলি",
"navigation_bar.lists": "তালিকাগুলো",
"navigation_bar.logout": "বাইরে যান",
"navigation_bar.mutes": "যেসব বেভহারকরীদের কার্যক্রম বন্ধ করা আছে",
"navigation_bar.mutes": "যাদের কার্যক্রম দেখা বন্ধ আছে",
"navigation_bar.personal": "নিজস্ব",
"navigation_bar.pins": "পিন দেওয়া টুট",
"navigation_bar.preferences": "পছন্দসমূহ",
@@ -299,10 +299,10 @@
"notifications.group": "{count} প্রজ্ঞাপন",
"poll.closed": "বন্ধ",
"poll.refresh": "বদলেছে কিনা দেখতে",
"poll.total_people": "{count, plural, one {# person} other {# people}}",
"poll.total_people": "{count, plural, one {# ব্যক্তি} other {# ব্যক্তি}}",
"poll.total_votes": "{count, plural, one {# ভোট} other {# ভোট}}",
"poll.vote": "ভোট",
"poll.voted": "You voted for this answer",
"poll.voted": "আপনি এই উত্তরের পক্ষে ভোট দিয়েছেন",
"poll_button.add_poll": "একটা নির্বাচন যোগ করতে",
"poll_button.remove_poll": "নির্বাচন বাদ দিতে",
"privacy.change": "লেখার গোপনীয়তা অবস্থা ঠিক করতে",
@@ -314,13 +314,13 @@
"privacy.public.short": "সর্বজনীন প্রকাশ্য",
"privacy.unlisted.long": "সর্বজনীন প্রকাশ্য সময়রেখাতে না দেখাতে",
"privacy.unlisted.short": "প্রকাশ্য নয়",
"refresh": "Refresh",
"refresh": "সতেজ করা",
"regeneration_indicator.label": "আসছে…",
"regeneration_indicator.sublabel": "আপনার বাড়ির-সময়রেখা প্রস্তূত করা হচ্ছে!",
"relative_time.days": "{number} দিন",
"relative_time.hours": "{number} ঘন্টা",
"relative_time.just_now": "এখন",
"relative_time.minutes": "{number}ম",
"relative_time.minutes": "{number}মিঃ",
"relative_time.seconds": "{number} সেকেন্ড",
"reply_indicator.cancel": "বাতিল করতে",
"report.forward": "এটা আরো পাঠান {target} তে",
@@ -329,7 +329,7 @@
"report.placeholder": "অন্য কোনো মন্তব্য",
"report.submit": "জমা দিন",
"report.target": "{target} রিপোর্ট করুন",
"search.placeholder": "খুঁজতে",
"search.placeholder": "অনুসন্ধান",
"search_popout.search_format": "বিস্তারিতভাবে খোঁজার পদ্ধতি",
"search_popout.tips.full_text": "সাধারণ লেখা দিয়ে খুঁজলে বের হবে সেরকম আপনার লেখা, পছন্দের লেখা, সমর্থন করা লেখা, আপনাকে উল্লেখকরা কোনো লেখা, যা খুঁজছেন সেরকম কোনো ব্যবহারকারীর নাম বা কোনো হ্যাশট্যাগগুলো।",
"search_popout.tips.hashtag": "হ্যাশট্যাগ",
@@ -339,11 +339,11 @@
"search_results.accounts": "মানুষ",
"search_results.hashtags": "হ্যাশট্যাগগুলি",
"search_results.statuses": "টুট",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.statuses_fts_disabled": "তাদের সামগ্রী দ্বারা টুটগুলি অনুসন্ধান এই মস্তোডন সার্ভারে সক্ষম নয়।",
"search_results.total": "{count, number} {count, plural, one {ফলাফল} other {ফলাফল}}",
"status.admin_account": "@{name} র জন্য পরিচালনার ইন্টারফেসে ঢুকুন",
"status.admin_status": "যায় লেখাটি পরিচালনার ইন্টারফেসে খুলুন",
"status.block": "@{name}কে বন্ধ করুন",
"status.block": "@{name} কে ব্লক করুন",
"status.cancel_reblog_private": "সমর্থন বাতিল করতে",
"status.cannot_reblog": "এটিতে সমর্থন দেওয়া যাবেনা",
"status.copy": "লেখাটির লিংক কপি করতে",
@@ -354,7 +354,7 @@
"status.favourite": "পছন্দের করতে",
"status.filtered": "ছাঁকনিদিত",
"status.load_more": "আরো দেখুন",
"status.media_hidden": "ছবি বা ভিডিও পেছনে",
"status.media_hidden": "মিডিয়া লুকানো আছে",
"status.mention": "@{name}কে উল্লেখ করতে",
"status.more": "আরো",
"status.mute": "@{name}র কার্যক্রম সরিয়ে ফেলতে",
@@ -378,7 +378,7 @@
"status.show_more": "আরো দেখাতে",
"status.show_more_all": "সবগুলোতে আরো দেখতে",
"status.show_thread": "আলোচনা দেখতে",
"status.uncached_media_warning": "Not available",
"status.uncached_media_warning": "পাওয়া যাচ্ছে না",
"status.unmute_conversation": "আলোচনার প্রজ্ঞাপন চালু করতে",
"status.unpin": "নিজের পাতা থেকে পিন করে রাখাটির পিন খুলতে",
"suggestions.dismiss": "সাহায্যের পরামর্শগুলো সরাতে",
@@ -387,29 +387,29 @@
"tabs_bar.home": "বাড়ি",
"tabs_bar.local_timeline": "স্থানীয়",
"tabs_bar.notifications": "প্রজ্ঞাপনগুলো",
"tabs_bar.search": "খুঁজতে",
"tabs_bar.search": "অনুসন্ধান",
"time_remaining.days": "{number, plural, one {# day} other {# days}} বাকি আছে",
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} বাকি আছে",
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} বাকি আছে",
"time_remaining.minutes": "{number, plural, one {# মিনিট} other {# মিনিট}} বাকি আছে",
"time_remaining.moments": "সময় বাকি আছে",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} বাকি আছে",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} কথা বলছে",
"trends.trending_now": "Trending now",
"trends.trending_now": "বর্তমানে জনপ্রিয়",
"ui.beforeunload": "যে পর্যন্ত এটা লেখা হয়েছে, মাস্টাডন থেকে চলে গেলে এটা মুছে যাবে।",
"upload_area.title": "টেনে এখানে ছেড়ে দিলে এখানে যুক্ত করা যাবে",
"upload_button.label": "ছবি বা ভিডিও যুক্ত করতে (এসব ধরণের: JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_error.limit": "যা যুক্ত করতে চাচ্ছেন সেটি বেশি বড়, এখানকার সর্বাধিকের মেমোরির উপরে চলে গেছে।",
"upload_error.poll": "নির্বাচনক্ষেত্রে কোনো ফাইল যুক্ত করা যাবেনা।",
"upload_form.description": "যারা দেখতে পায়না তাদের জন্য এটা বর্ণনা করতে",
"upload_form.edit": "Edit",
"upload_form.edit": "সম্পাদন",
"upload_form.undo": "মুছে ফেলতে",
"upload_modal.analyzing_picture": "Analyzing picture…",
"upload_modal.apply": "Apply",
"upload_modal.analyzing_picture": "চিত্র বিশ্লেষণ করা হচ্ছে…",
"upload_modal.apply": "প্রয়োগ করুন",
"upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
"upload_modal.detect_text": "Detect text from picture",
"upload_modal.edit_media": "Edit media",
"upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
"upload_modal.preview_label": "Preview ({ratio})",
"upload_modal.detect_text": "ছবি থেকে পাঠ্য সনাক্ত করুন",
"upload_modal.edit_media": "মিডিয়া সম্পাদনা করুন",
"upload_modal.hint": "একটি দৃশ্যমান পয়েন্ট নির্বাচন করুন ক্লিক অথবা টানার মাধ্যমে যেটি সবময় সব থাম্বনেলে দেখা যাবে।",
"upload_modal.preview_label": "পূর্বরূপ({ratio})",
"upload_progress.label": "যুক্ত করতে পাঠানো হচ্ছে...",
"video.close": "ভিডিওটি বন্ধ করতে",
"video.exit_fullscreen": "পূর্ণ পর্দা থেকে বাইরে বের হতে",

+ 8
- 8
app/javascript/mastodon/locales/cy.json View File

@@ -103,7 +103,7 @@