Browse Source

Merge branch 'master' into live

Zac 3 months ago
parent
commit
57ca099277
100 changed files with 2010 additions and 1806 deletions
  1. 36
    0
      app/controllers/admin/account_actions_controller.rb
  2. 1
    0
      app/controllers/admin/account_moderation_notes_controller.rb
  3. 16
    7
      app/controllers/admin/accounts_controller.rb
  4. 23
    56
      app/controllers/admin/reports_controller.rb
  5. 2
    0
      app/controllers/admin/settings_controller.rb
  6. 0
    27
      app/controllers/admin/silences_controller.rb
  7. 0
    60
      app/controllers/admin/suspensions_controller.rb
  8. 58
    0
      app/controllers/admin/warning_presets_controller.rb
  9. 1
    1
      app/controllers/api/v1/accounts/statuses_controller.rb
  10. 1
    0
      app/controllers/api/web/embeds_controller.rb
  11. 1
    1
      app/controllers/directories_controller.rb
  12. 6
    6
      app/controllers/follower_accounts_controller.rb
  13. 1
    0
      app/controllers/settings/preferences_controller.rb
  14. 6
    1
      app/helpers/admin/action_logs_helper.rb
  15. 0
    4
      app/helpers/mailer_helper.rb
  16. 11
    5
      app/helpers/stream_entries_helper.rb
  17. 3
    3
      app/javascript/flavours/glitch/actions/streaming.js
  18. 27
    2
      app/javascript/flavours/glitch/actions/timelines.js
  19. 20
    9
      app/javascript/flavours/glitch/components/column_header.js
  20. 4
    2
      app/javascript/flavours/glitch/components/modal_root.js
  21. 1
    1
      app/javascript/flavours/glitch/components/scrollable_list.js
  22. 1
    1
      app/javascript/flavours/glitch/features/account/components/action_bar.js
  23. 0
    1
      app/javascript/flavours/glitch/features/composer/index.js
  24. 12
    5
      app/javascript/flavours/glitch/features/drawer/index.js
  25. 0
    1
      app/javascript/flavours/glitch/features/getting_started/index.js
  26. 102
    0
      app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js
  27. 31
    0
      app/javascript/flavours/glitch/features/hashtag_timeline/containers/column_settings_container.js
  28. 57
    16
      app/javascript/flavours/glitch/features/hashtag_timeline/index.js
  29. 1
    1
      app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js
  30. 2
    2
      app/javascript/flavours/glitch/reducers/accounts_counters.js
  31. 0
    245
      app/javascript/flavours/glitch/reducers/notifications.js.orig
  32. 7
    0
      app/javascript/flavours/glitch/reducers/timelines.js
  33. 31
    0
      app/javascript/flavours/glitch/styles/_mixins.scss
  34. 4
    0
      app/javascript/flavours/glitch/styles/admin.scss
  35. 22
    0
      app/javascript/flavours/glitch/styles/components/accounts.scss
  36. 46
    37
      app/javascript/flavours/glitch/styles/components/drawer.scss
  37. 1
    27
      app/javascript/flavours/glitch/styles/components/search.scss
  38. 1
    1
      app/javascript/flavours/glitch/styles/mastodon-light/diff.scss
  39. 1
    0
      app/javascript/flavours/glitch/util/initial_state.js
  40. 4
    0
      app/javascript/images/icon_flag.svg
  41. BIN
      app/javascript/images/mailer/icon_warning.png
  42. 4
    2
      app/javascript/mastodon/components/modal_root.js
  43. 1
    1
      app/javascript/mastodon/components/scrollable_list.js
  44. 1
    1
      app/javascript/mastodon/features/getting_started/index.js
  45. 1
    8
      app/javascript/mastodon/features/public_timeline/index.js
  46. 1
    0
      app/javascript/mastodon/features/ui/components/embed_modal.js
  47. 0
    2
      app/javascript/mastodon/features/ui/index.js
  48. 26
    22
      app/javascript/mastodon/locales/ar.json
  49. 26
    22
      app/javascript/mastodon/locales/ast.json
  50. 26
    22
      app/javascript/mastodon/locales/bg.json
  51. 26
    22
      app/javascript/mastodon/locales/ca.json
  52. 26
    22
      app/javascript/mastodon/locales/co.json
  53. 31
    27
      app/javascript/mastodon/locales/cs.json
  54. 26
    22
      app/javascript/mastodon/locales/cy.json
  55. 26
    22
      app/javascript/mastodon/locales/da.json
  56. 26
    22
      app/javascript/mastodon/locales/de.json
  57. 112
    107
      app/javascript/mastodon/locales/defaultMessages.json
  58. 55
    51
      app/javascript/mastodon/locales/el.json
  59. 26
    30
      app/javascript/mastodon/locales/en.json
  60. 26
    22
      app/javascript/mastodon/locales/eo.json
  61. 26
    22
      app/javascript/mastodon/locales/es.json
  62. 26
    22
      app/javascript/mastodon/locales/eu.json
  63. 26
    22
      app/javascript/mastodon/locales/fa.json
  64. 26
    22
      app/javascript/mastodon/locales/fi.json
  65. 26
    22
      app/javascript/mastodon/locales/fr.json
  66. 26
    22
      app/javascript/mastodon/locales/gl.json
  67. 26
    22
      app/javascript/mastodon/locales/he.json
  68. 26
    22
      app/javascript/mastodon/locales/hr.json
  69. 26
    22
      app/javascript/mastodon/locales/hu.json
  70. 26
    22
      app/javascript/mastodon/locales/hy.json
  71. 26
    22
      app/javascript/mastodon/locales/id.json
  72. 26
    22
      app/javascript/mastodon/locales/io.json
  73. 26
    22
      app/javascript/mastodon/locales/it.json
  74. 27
    23
      app/javascript/mastodon/locales/ja.json
  75. 26
    22
      app/javascript/mastodon/locales/ka.json
  76. 36
    32
      app/javascript/mastodon/locales/ko.json
  77. 26
    22
      app/javascript/mastodon/locales/ms.json
  78. 26
    22
      app/javascript/mastodon/locales/nl.json
  79. 26
    22
      app/javascript/mastodon/locales/no.json
  80. 28
    24
      app/javascript/mastodon/locales/oc.json
  81. 26
    30
      app/javascript/mastodon/locales/pl.json
  82. 26
    22
      app/javascript/mastodon/locales/pt-BR.json
  83. 26
    22
      app/javascript/mastodon/locales/pt.json
  84. 26
    22
      app/javascript/mastodon/locales/ro.json
  85. 26
    22
      app/javascript/mastodon/locales/ru.json
  86. 26
    22
      app/javascript/mastodon/locales/sk.json
  87. 26
    22
      app/javascript/mastodon/locales/sl.json
  88. 26
    22
      app/javascript/mastodon/locales/sr-Latn.json
  89. 26
    22
      app/javascript/mastodon/locales/sr.json
  90. 26
    22
      app/javascript/mastodon/locales/sv.json
  91. 26
    22
      app/javascript/mastodon/locales/ta.json
  92. 26
    22
      app/javascript/mastodon/locales/te.json
  93. 26
    22
      app/javascript/mastodon/locales/th.json
  94. 26
    22
      app/javascript/mastodon/locales/tr.json
  95. 26
    22
      app/javascript/mastodon/locales/uk.json
  96. 2
    0
      app/javascript/mastodon/locales/whitelist_ms.json
  97. 26
    22
      app/javascript/mastodon/locales/zh-CN.json
  98. 26
    22
      app/javascript/mastodon/locales/zh-HK.json
  99. 26
    22
      app/javascript/mastodon/locales/zh-TW.json
  100. 0
    0
      app/javascript/styles/mailer.scss

+ 36
- 0
app/controllers/admin/account_actions_controller.rb View File

@@ -0,0 +1,36 @@
1
+# frozen_string_literal: true
2
+
3
+module Admin
4
+  class AccountActionsController < BaseController
5
+    before_action :set_account
6
+
7
+    def new
8
+      @account_action  = Admin::AccountAction.new(type: params[:type], report_id: params[:report_id], send_email_notification: true)
9
+      @warning_presets = AccountWarningPreset.all
10
+    end
11
+
12
+    def create
13
+      account_action                 = Admin::AccountAction.new(resource_params)
14
+      account_action.target_account  = @account
15
+      account_action.current_account = current_account
16
+
17
+      account_action.save!
18
+
19
+      if account_action.with_report?
20
+        redirect_to admin_report_path(account_action.report)
21
+      else
22
+        redirect_to admin_account_path(@account.id)
23
+      end
24
+    end
25
+
26
+    private
27
+
28
+    def set_account
29
+      @account = Account.find(params[:account_id])
30
+    end
31
+
32
+    def resource_params
33
+      params.require(:admin_account_action).permit(:type, :report_id, :warning_preset_id, :text, :send_email_notification)
34
+    end
35
+  end
36
+end

+ 1
- 0
app/controllers/admin/account_moderation_notes_controller.rb View File

@@ -14,6 +14,7 @@ module Admin
14 14
       else
15 15
         @account          = @account_moderation_note.target_account
16 16
         @moderation_notes = @account.targeted_moderation_notes.latest
17
+        @warnings         = @account.targeted_account_warnings.latest.custom
17 18
 
18 19
         render template: 'admin/accounts/show'
19 20
       end

+ 16
- 7
app/controllers/admin/accounts_controller.rb View File

@@ -2,9 +2,9 @@
2 2
 
3 3
 module Admin
4 4
   class AccountsController < BaseController
5
-    before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :disable, :memorialize]
5
+    before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize]
6 6
     before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
7
-    before_action :require_local_account!, only: [:enable, :disable, :memorialize]
7
+    before_action :require_local_account!, only: [:enable, :memorialize]
8 8
 
9 9
     def index
10 10
       authorize :account, :index?
@@ -13,8 +13,10 @@ module Admin
13 13
 
14 14
     def show
15 15
       authorize @account, :show?
16
+
16 17
       @account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
17
-      @moderation_notes = @account.targeted_moderation_notes.latest
18
+      @moderation_notes        = @account.targeted_moderation_notes.latest
19
+      @warnings                = @account.targeted_account_warnings.latest.custom
18 20
     end
19 21
 
20 22
     def subscribe
@@ -43,10 +45,17 @@ module Admin
43 45
       redirect_to admin_account_path(@account.id)
44 46
     end
45 47
 
46
-    def disable
47
-      authorize @account.user, :disable?
48
-      @account.user.disable!
49
-      log_action :disable, @account.user
48
+    def unsilence
49
+      authorize @account, :unsilence?
50
+      @account.unsilence!
51
+      log_action :unsilence, @account
52
+      redirect_to admin_account_path(@account.id)
53
+    end
54
+
55
+    def unsuspend
56
+      authorize @account, :unsuspend?
57
+      @account.unsuspend!
58
+      log_action :unsuspend, @account
50 59
       redirect_to admin_account_path(@account.id)
51 60
     end
52 61
 

+ 23
- 56
app/controllers/admin/reports_controller.rb View File

@@ -13,75 +13,42 @@ module Admin
13 13
       authorize @report, :show?
14 14
 
15 15
       @report_note  = @report.notes.new
16
-      @report_notes = (@report.notes.latest + @report.history).sort_by(&:created_at)
16
+      @report_notes = (@report.notes.latest + @report.history + @report.target_account.targeted_account_warnings.latest.custom).sort_by(&:created_at)
17 17
       @form         = Form::StatusBatch.new
18 18
     end
19 19
 
20
-    def update
20
+    def assign_to_self
21 21
       authorize @report, :update?
22
-      process_report
23
-
24
-      if @report.action_taken?
25
-        redirect_to admin_reports_path, notice: I18n.t('admin.reports.resolved_msg')
26
-      else
27
-        redirect_to admin_report_path(@report)
28
-      end
22
+      @report.update!(assigned_account_id: current_account.id)
23
+      log_action :assigned_to_self, @report
24
+      redirect_to admin_report_path(@report)
29 25
     end
30 26
 
31
-    private
32
-
33
-    def process_report
34
-      case params[:outcome].to_s
35
-      when 'assign_to_self'
36
-        @report.update!(assigned_account_id: current_account.id)
37
-        log_action :assigned_to_self, @report
38
-      when 'unassign'
39
-        @report.update!(assigned_account_id: nil)
40
-        log_action :unassigned, @report
41
-      when 'reopen'
42
-        @report.unresolve!
43
-        log_action :reopen, @report
44
-      when 'resolve'
45
-        @report.resolve!(current_account)
46
-        log_action :resolve, @report
47
-      when 'disable'
48
-        @report.resolve!(current_account)
49
-        @report.target_account.user.disable!
50
-
51
-        log_action :resolve, @report
52
-        log_action :disable, @report.target_account.user
53
-
54
-        resolve_all_target_account_reports
55
-      when 'silence'
56
-        @report.resolve!(current_account)
57
-        @report.target_account.update!(silenced: true)
58
-
59
-        log_action :resolve, @report
60
-        log_action :silence, @report.target_account
61
-
62
-        resolve_all_target_account_reports
63
-      else
64
-        raise ActiveRecord::RecordNotFound
65
-      end
66
-
67
-      @report.reload
27
+    def unassign
28
+      authorize @report, :update?
29
+      @report.update!(assigned_account_id: nil)
30
+      log_action :unassigned, @report
31
+      redirect_to admin_report_path(@report)
68 32
     end
69 33
 
70
-    def resolve_all_target_account_reports
71
-      unresolved_reports_for_target_account.update_all(action_taken: true, action_taken_by_account_id: current_account.id)
34
+    def reopen
35
+      authorize @report, :update?
36
+      @report.unresolve!
37
+      log_action :reopen, @report
38
+      redirect_to admin_report_path(@report)
72 39
     end
73 40
 
74
-    def unresolved_reports_for_target_account
75
-      Report.where(
76
-        target_account: @report.target_account
77
-      ).unresolved
41
+    def resolve
42
+      authorize @report, :update?
43
+      @report.resolve!(current_account)
44
+      log_action :resolve, @report
45
+      redirect_to admin_reports_path, notice: I18n.t('admin.reports.resolved_msg')
78 46
     end
79 47
 
48
+    private
49
+
80 50
     def filtered_reports
81
-      ReportFilter.new(filter_params).results.order(id: :desc).includes(
82
-        :account,
83
-        :target_account
84
-      )
51
+      ReportFilter.new(filter_params).results.order(id: :desc).includes(:account, :target_account)
85 52
     end
86 53
 
87 54
     def filter_params

+ 2
- 0
app/controllers/admin/settings_controller.rb View File

@@ -29,6 +29,7 @@ module Admin
29 29
       preview_sensitive_media
30 30
       custom_css
31 31
       profile_directory
32
+      hide_followers_count
32 33
     ).freeze
33 34
 
34 35
     BOOLEAN_SETTINGS = %w(
@@ -41,6 +42,7 @@ module Admin
41 42
       show_known_fediverse_at_about_page
42 43
       preview_sensitive_media
43 44
       profile_directory
45
+      hide_followers_count
44 46
     ).freeze
45 47
 
46 48
     UPLOAD_SETTINGS = %w(

+ 0
- 27
app/controllers/admin/silences_controller.rb View File

@@ -1,27 +0,0 @@
1
-# frozen_string_literal: true
2
-
3
-module Admin
4
-  class SilencesController < BaseController
5
-    before_action :set_account
6
-
7
-    def create
8
-      authorize @account, :silence?
9
-      @account.update!(silenced: true)
10
-      log_action :silence, @account
11
-      redirect_to admin_accounts_path
12
-    end
13
-
14
-    def destroy
15
-      authorize @account, :unsilence?
16
-      @account.update!(silenced: false)
17
-      log_action :unsilence, @account
18
-      redirect_to admin_accounts_path
19
-    end
20
-
21
-    private
22
-
23
-    def set_account
24
-      @account = Account.find(params[:account_id])
25
-    end
26
-  end
27
-end

+ 0
- 60
app/controllers/admin/suspensions_controller.rb View File

@@ -1,60 +0,0 @@
1
-# frozen_string_literal: true
2
-
3
-module Admin
4
-  class SuspensionsController < BaseController
5
-    before_action :set_account
6
-
7
-    def new
8
-      @suspension = Form::AdminSuspensionConfirmation.new(report_id: params[:report_id])
9
-    end
10
-
11
-    def create
12
-      authorize @account, :suspend?
13
-
14
-      @suspension = Form::AdminSuspensionConfirmation.new(suspension_params)
15
-
16
-      if suspension_params[:acct] == @account.acct
17
-        resolve_report! if suspension_params[:report_id].present?
18
-        perform_suspend!
19
-        mark_reports_resolved!
20
-        redirect_to admin_accounts_path
21
-      else
22
-        flash.now[:alert] = I18n.t('admin.suspensions.bad_acct_msg')
23
-        render :new
24
-      end
25
-    end
26
-
27
-    def destroy
28
-      authorize @account, :unsuspend?
29
-      @account.unsuspend!
30
-      log_action :unsuspend, @account
31
-      redirect_to admin_accounts_path
32
-    end
33
-
34
-    private
35
-
36
-    def set_account
37
-      @account = Account.find(params[:account_id])
38
-    end
39
-
40
-    def suspension_params
41
-      params.require(:form_admin_suspension_confirmation).permit(:acct, :report_id)
42
-    end
43
-
44
-    def resolve_report!
45
-      report = Report.find(suspension_params[:report_id])
46
-      report.resolve!(current_account)
47
-      log_action :resolve, report
48
-    end
49
-
50
-    def perform_suspend!
51
-      @account.suspend!
52
-      Admin::SuspensionWorker.perform_async(@account.id)
53
-      log_action :suspend, @account
54
-    end
55
-
56
-    def mark_reports_resolved!
57
-      Report.where(target_account: @account).unresolved.update_all(action_taken: true, action_taken_by_account_id: current_account.id)
58
-    end
59
-  end
60
-end

+ 58
- 0
app/controllers/admin/warning_presets_controller.rb View File

@@ -0,0 +1,58 @@
1
+# frozen_string_literal: true
2
+
3
+module Admin
4
+  class WarningPresetsController < BaseController
5
+    before_action :set_warning_preset, except: [:index, :create]
6
+
7
+    def index
8
+      authorize :account_warning_preset, :index?
9
+
10
+      @warning_presets = AccountWarningPreset.all
11
+      @warning_preset  = AccountWarningPreset.new
12
+    end
13
+
14
+    def create
15
+      authorize :account_warning_preset, :create?
16
+
17
+      @warning_preset = AccountWarningPreset.new(warning_preset_params)
18
+
19
+      if @warning_preset.save
20
+        redirect_to admin_warning_presets_path
21
+      else
22
+        @warning_presets = AccountWarningPreset.all
23
+        render :index
24
+      end
25
+    end
26
+
27
+    def edit
28
+      authorize @warning_preset, :update?
29
+    end
30
+
31
+    def update
32
+      authorize @warning_preset, :update?
33
+
34
+      if @warning_preset.update(warning_preset_params)
35
+        redirect_to admin_warning_presets_path
36
+      else
37
+        render :edit
38
+      end
39
+    end
40
+
41
+    def destroy
42
+      authorize @warning_preset, :destroy?
43
+
44
+      @warning_preset.destroy!
45
+      redirect_to admin_warning_presets_path
46
+    end
47
+
48
+    private
49
+
50
+    def set_warning_preset
51
+      @warning_preset = AccountWarningPreset.find(params[:id])
52
+    end
53
+
54
+    def warning_preset_params
55
+      params.require(:account_warning_preset).permit(:text)
56
+    end
57
+  end
58
+end

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

@@ -1,7 +1,7 @@
1 1
 # frozen_string_literal: true
2 2
 
3 3
 class Api::V1::Accounts::StatusesController < Api::BaseController
4
-  before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
4
+  before_action -> { authorize_if_got_token! :read, :'read:statuses' }
5 5
   before_action :set_account
6 6
   after_action :insert_pagination_headers
7 7
 

+ 1
- 0
app/controllers/api/web/embeds_controller.rb View File

@@ -10,6 +10,7 @@ class Api::Web::EmbedsController < Api::Web::BaseController
10 10
     render json: status, serializer: OEmbedSerializer, width: 400
11 11
   rescue ActiveRecord::RecordNotFound
12 12
     oembed = FetchOEmbedService.new.call(params[:url])
13
+    oembed[:html] = Formatter.instance.sanitize(oembed[:html], Sanitize::Config::MASTODON_OEMBED) if oembed[:html].present?
13 14
 
14 15
     if oembed
15 16
       render json: oembed

+ 1
- 1
app/controllers/directories_controller.rb View File

@@ -37,7 +37,7 @@ class DirectoriesController < ApplicationController
37 37
   end
38 38
 
39 39
   def set_accounts
40
-    @accounts = Account.discoverable.page(params[:page]).per(30).tap do |query|
40
+    @accounts = Account.discoverable.page(params[:page]).per(40).tap do |query|
41 41
       query.merge!(Account.tagged_with(@tag.id)) if @tag
42 42
     end
43 43
   end

+ 6
- 6
app/controllers/follower_accounts_controller.rb View File

@@ -36,22 +36,22 @@ class FollowerAccountsController < ApplicationController
36 36
   end
37 37
 
38 38
   def collection_presenter
39
+    options = { type: :ordered }
40
+    options[:size] = @account.followers_count unless Setting.hide_followers_count || @account.user&.setting_hide_followers_count
39 41
     if params[:page].present?
40 42
       ActivityPub::CollectionPresenter.new(
41 43
         id: account_followers_url(@account, page: params.fetch(:page, 1)),
42
-        type: :ordered,
43
-        size: @account.followers_count,
44 44
         items: follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) },
45 45
         part_of: account_followers_url(@account),
46 46
         next: page_url(follows.next_page),
47
-        prev: page_url(follows.prev_page)
47
+        prev: page_url(follows.prev_page),
48
+        **options
48 49
       )
49 50
     else
50 51
       ActivityPub::CollectionPresenter.new(
51 52
         id: account_followers_url(@account),
52
-        type: :ordered,
53
-        size: @account.followers_count,
54
-        first: page_url(1)
53
+        first: page_url(1),
54
+        **options
55 55
       )
56 56
     end
57 57
   end

+ 1
- 0
app/controllers/settings/preferences_controller.rb View File

@@ -43,6 +43,7 @@ class Settings::PreferencesController < Settings::BaseController
43 43
       :setting_system_font_ui,
44 44
       :setting_noindex,
45 45
       :setting_hide_network,
46
+      :setting_hide_followers_count,
46 47
       :setting_aggregate_reblogs,
47 48
       notification_emails: %i(follow follow_request reblog favourite mention digest report),
48 49
       interactions: %i(must_be_follower must_be_following)

+ 6
- 1
app/helpers/admin/action_logs_helper.rb View File

@@ -23,6 +23,8 @@ module Admin::ActionLogsHelper
23 23
       link_to record.domain, "https://#{record.domain}"
24 24
     when 'Status'
25 25
       link_to record.account.acct, TagManager.instance.url_for(record)
26
+    when 'AccountWarning'
27
+      link_to record.target_account.acct, admin_account_path(record.target_account_id)
26 28
     end
27 29
   end
28 30
 
@@ -34,6 +36,7 @@ module Admin::ActionLogsHelper
34 36
       link_to attributes['domain'], "https://#{attributes['domain']}"
35 37
     when 'Status'
36 38
       tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count'))
39
+
37 40
       if tmp_status.account
38 41
         link_to tmp_status.account&.acct || "##{tmp_status.account_id}", admin_account_path(tmp_status.account_id)
39 42
       else
@@ -81,6 +84,8 @@ module Admin::ActionLogsHelper
81 84
       'envelope'
82 85
     when 'Status'
83 86
       'pencil'
87
+    when 'AccountWarning'
88
+      'warning'
84 89
     end
85 90
   end
86 91
 
@@ -104,6 +109,6 @@ module Admin::ActionLogsHelper
104 109
   private
105 110
 
106 111
   def opposite_verbs?(log)
107
-    %w(DomainBlock EmailDomainBlock).include?(log.target_type)
112
+    %w(DomainBlock EmailDomainBlock AccountWarning).include?(log.target_type)
108 113
   end
109 114
 end

+ 0
- 4
app/helpers/mailer_helper.rb View File

@@ -1,4 +0,0 @@
1
-# frozen_string_literal: true
2
-
3
-module MailerHelper
4
-end

+ 11
- 5
app/helpers/stream_entries_helper.rb View File

@@ -60,8 +60,12 @@ module StreamEntriesHelper
60 60
     end
61 61
   end
62 62
 
63
+  def hide_followers_count?(account)
64
+    Setting.hide_followers_count || account.user&.setting_hide_followers_count
65
+  end
66
+
63 67
   def account_description(account)
64
-    prepend_str = [
68
+    prepend_stats = [
65 69
       [
66 70
         number_to_human(account.statuses_count, strip_insignificant_zeros: true),
67 71
         I18n.t('accounts.posts', count: account.statuses_count),
@@ -71,14 +75,16 @@ module StreamEntriesHelper
71 75
         number_to_human(account.following_count, strip_insignificant_zeros: true),
72 76
         I18n.t('accounts.following', count: account.following_count),
73 77
       ].join(' '),
78
+    ]
74 79
 
75
-      [
80
+    unless hide_followers_count?(account)
81
+      prepend_stats << [
76 82
         number_to_human(account.followers_count, strip_insignificant_zeros: true),
77 83
         I18n.t('accounts.followers', count: account.followers_count),
78
-      ].join(' '),
79
-    ].join(', ')
84
+      ].join(' ')
85
+    end
80 86
 
81
-    [prepend_str, account.note].join(' · ')
87
+    [prepend_stats.join(', '), account.note].join(' · ')
82 88
   end
83 89
 
84 90
   def media_summary(status)

+ 3
- 3
app/javascript/flavours/glitch/actions/streaming.js View File

@@ -11,7 +11,7 @@ import { getLocale } from 'mastodon/locales';
11 11
 
12 12
 const { messages } = getLocale();
13 13
 
14
-export function connectTimelineStream (timelineId, path, pollingRefresh = null) {
14
+export function connectTimelineStream (timelineId, path, pollingRefresh = null, accept = null) {
15 15
 
16 16
   return connectStream (path, pollingRefresh, (dispatch, getState) => {
17 17
     const locale = getState().getIn(['meta', 'locale']);
@@ -23,7 +23,7 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null)
23 23
       onReceive (data) {
24 24
         switch(data.event) {
25 25
         case 'update':
26
-          dispatch(updateTimeline(timelineId, JSON.parse(data.payload)));
26
+          dispatch(updateTimeline(timelineId, JSON.parse(data.payload), accept));
27 27
           break;
28 28
         case 'delete':
29 29
           dispatch(deleteFromTimelines(data.payload));
@@ -47,6 +47,6 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
47 47
 export const connectUserStream      = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
48 48
 export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
49 49
 export const connectPublicStream    = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`);
50
-export const connectHashtagStream   = tag => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
50
+export const connectHashtagStream   = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
51 51
 export const connectDirectStream    = () => connectTimelineStream('direct', 'direct');
52 52
 export const connectListStream      = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);

+ 27
- 2
app/javascript/flavours/glitch/actions/timelines.js View File

@@ -3,6 +3,7 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
3 3
 
4 4
 export const TIMELINE_UPDATE  = 'TIMELINE_UPDATE';
5 5
 export const TIMELINE_DELETE  = 'TIMELINE_DELETE';
6
+export const TIMELINE_CLEAR   = 'TIMELINE_CLEAR';
6 7
 
7 8
 export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
8 9
 export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
@@ -12,8 +13,12 @@ export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
12 13
 
13 14
 export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
14 15
 
15
-export function updateTimeline(timeline, status) {
16
+export function updateTimeline(timeline, status, accept) {
16 17
   return (dispatch, getState) => {
18
+    if (typeof accept === 'function' && !accept(status)) {
19
+      return;
20
+    }
21
+
17 22
     dispatch({
18 23
       type: TIMELINE_UPDATE,
19 24
       timeline,
@@ -38,8 +43,20 @@ export function deleteFromTimelines(id) {
38 43
   };
39 44
 };
40 45
 
46
+export function clearTimeline(timeline) {
47
+  return (dispatch) => {
48
+    dispatch({ type: TIMELINE_CLEAR, timeline });
49
+  };
50
+};
51
+
41 52
 const noOp = () => {};
42 53
 
54
+const parseTags = (tags = {}, mode) => {
55
+  return (tags[mode] || []).map((tag) => {
56
+    return tag.value;
57
+  });
58
+};
59
+
43 60
 export function expandTimeline(timelineId, path, params = {}, done = noOp) {
44 61
   return (dispatch, getState) => {
45 62
     const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
@@ -76,9 +93,17 @@ export const expandDirectTimeline          = ({ maxId } = {}, done = noOp) => ex
76 93
 export const expandAccountTimeline         = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
77 94
 export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
78 95
 export const expandAccountMediaTimeline    = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true });
79
-export const expandHashtagTimeline         = (hashtag, { maxId } = {}, done = noOp) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId }, done);
80 96
 export const expandListTimeline            = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
81 97
 
98
+export const expandHashtagTimeline       = (hashtag, { maxId, tags } = {}, done = noOp) => {
99
+  return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
100
+    max_id: maxId,
101
+    any: parseTags(tags, 'any'),
102
+    all: parseTags(tags, 'all'),
103
+    none: parseTags(tags, 'none'),
104
+  }, done);
105
+};
106
+
82 107
 export function expandTimelineRequest(timeline, isLoadingMore) {
83 108
   return {
84 109
     type: TIMELINE_EXPAND_REQUEST,

+ 20
- 9
app/javascript/flavours/glitch/components/column_header.js View File

@@ -47,6 +47,15 @@ export default class ColumnHeader extends React.PureComponent {
47 47
     animatingNCD: false,
48 48
   };
49 49
 
50
+  historyBack = () => {
51
+    // if history is exhausted, or we would leave mastodon, just go to root.
52
+    if (window.history.state) {
53
+      this.context.router.history.goBack();
54
+    } else {
55
+      this.context.router.history.push('/');
56
+    }
57
+  }
58
+
50 59
   handleToggleClick = (e) => {
51 60
     e.stopPropagation();
52 61
     this.setState({ collapsed: !this.state.collapsed, animating: true });
@@ -65,12 +74,7 @@ export default class ColumnHeader extends React.PureComponent {
65 74
   }
66 75
 
67 76
   handleBackClick = () => {
68
-    // if history is exhausted, or we would leave mastodon, just go to root.
69
-    if (window.history.state) {
70
-      this.context.router.history.goBack();
71
-    } else {
72
-      this.context.router.history.push('/');
73
-    }
77
+    this.historyBack();
74 78
   }
75 79
 
76 80
   handleTransitionEnd = () => {
@@ -81,13 +85,20 @@ export default class ColumnHeader extends React.PureComponent {
81 85
     this.setState({ animatingNCD: false });
82 86
   }
83 87
 
88
+  handlePin = () => {
89
+    if (!this.props.pinned) {
90
+      this.historyBack();
91
+    }
92
+    this.props.onPin();
93
+  }
94
+
84 95
   onEnterCleaningMode = () => {
85 96
     this.setState({ animatingNCD: true });
86 97
     this.props.onEnterCleaningMode(!this.props.notifCleaningActive);
87 98
   }
88 99
 
89 100
   render () {
90
-    const { intl, icon, active, children, pinned, onPin, multiColumn, extraButton, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive } = this.props;
101
+    const { intl, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive } = this.props;
91 102
     const { collapsed, animating, animatingNCD } = this.state;
92 103
 
93 104
     let title = this.props.title;
@@ -132,7 +143,7 @@ export default class ColumnHeader extends React.PureComponent {
132 143
     }
133 144
 
134 145
     if (multiColumn && pinned) {
135
-      pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={onPin}><i className='fa fa fa-times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
146
+      pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
136 147
 
137 148
       moveButtons = (
138 149
         <div key='move-buttons' className='column-header__setting-arrows'>
@@ -141,7 +152,7 @@ export default class ColumnHeader extends React.PureComponent {
141 152
         </div>
142 153
       );
143 154
     } else if (multiColumn) {
144
-      pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={onPin}><i className='fa fa fa-plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
155
+      pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
145 156
     }
146 157
 
147 158
     if (!pinned && (multiColumn || showBackButton)) {

+ 4
- 2
app/javascript/flavours/glitch/components/modal_root.js View File

@@ -39,13 +39,15 @@ export default class ModalRoot extends React.PureComponent {
39 39
     } else if (!nextProps.children) {
40 40
       this.setState({ revealed: false });
41 41
     }
42
+    if (!nextProps.children && !!this.props.children) {
43
+      this.activeElement.focus();
44
+      this.activeElement = null;
45
+    }
42 46
   }
43 47
 
44 48
   componentDidUpdate (prevProps) {
45 49
     if (!this.props.children && !!prevProps.children) {
46 50
       this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
47
-      this.activeElement.focus();
48
-      this.activeElement = null;
49 51
       this.handleModalClose();
50 52
     }
51 53
     if (this.props.children) {

+ 1
- 1
app/javascript/flavours/glitch/components/scrollable_list.js View File

@@ -47,7 +47,7 @@ export default class ScrollableList extends PureComponent {
47 47
       const { scrollTop, scrollHeight, clientHeight } = this.node;
48 48
       const offset = scrollHeight - scrollTop - clientHeight;
49 49
 
50
-      if (400 > offset && this.props.onLoadMore && !this.props.isLoading) {
50
+      if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
51 51
         this.props.onLoadMore();
52 52
       }
53 53
 

+ 1
- 1
app/javascript/flavours/glitch/features/account/components/action_bar.js View File

@@ -164,7 +164,7 @@ export default class ActionBar extends React.PureComponent {
164 164
 
165 165
             <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}>
166 166
               <FormattedMessage id='account.followers' defaultMessage='Followers' />
167
-              <strong><FormattedNumber value={account.get('followers_count')} /></strong>
167
+              <strong>{ account.get('followers_count') < 0 ? '-' : <FormattedNumber value={account.get('followers_count')} /> }</strong>
168 168
             </NavLink>
169 169
           </div>
170 170
         </div>

+ 0
- 1
app/javascript/flavours/glitch/features/composer/index.js View File

@@ -167,7 +167,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
167 167
       confirm: intl.formatMessage(messages.missingDescriptionConfirm),
168 168
       onConfirm: () => dispatch(submitCompose(routerHistory)),
169 169
       onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_missing_media_description'], false)),
170
-      onConfirm: () => dispatch(submitCompose()),
171 170
     }));
172 171
   },
173 172
   onSubmit(routerHistory) {

+ 12
- 5
app/javascript/flavours/glitch/features/drawer/index.js View File

@@ -23,7 +23,7 @@ import DrawerResults from './results';
23 23
 import DrawerSearch from './search';
24 24
 
25 25
 //  Utils.
26
-import { me } from 'flavours/glitch/util/initial_state';
26
+import { me, mascot } from 'flavours/glitch/util/initial_state';
27 27
 import { wrap } from 'flavours/glitch/util/redux_helpers';
28 28
 
29 29
 //  Messages.
@@ -121,10 +121,17 @@ class Drawer extends React.Component {
121 121
             submitted={submitted}
122 122
             value={searchValue}
123 123
           /> }
124
-        <div className='contents'>
125
-          {!isSearchPage && <DrawerAccount account={account} />}
126
-          {!isSearchPage && <Composer />}
127
-          {multiColumn && <button className='mastodon' onClick={onClickElefriend} />}
124
+        <div className='drawer__pager'>
125
+          {!isSearchPage && <div className='drawer__inner'>
126
+            <DrawerAccount account={account} />
127
+            <Composer />
128
+            {multiColumn && (
129
+              <div className='drawer__inner__mastodon'>
130
+                {mascot ? <img alt='' draggable='false' src={mascot} /> : <button className='mastodon' onClick={onClickElefriend} />}
131
+              </div>
132
+            )}
133
+          </div>}
134
+
128 135
           {(multiColumn || isSearchPage) &&
129 136
             <DrawerResults
130 137
               results={results}

+ 0
- 1
app/javascript/flavours/glitch/features/getting_started/index.js View File

@@ -165,7 +165,6 @@ export default class GettingStarted extends ImmutablePureComponent {
165 165
 
166 166
           <div className='getting-started__footer'>
167 167
             <ul>
168
-              <li><a href='https://bridge.joinmastodon.org/' target='_blank'><FormattedMessage id='getting_started.find_friends' defaultMessage='Find friends from Twitter' /></a> · </li>
169 168
               {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
170 169
               <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this instance' /></a> · </li>
171 170
               <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>

+ 102
- 0
app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js View File

@@ -0,0 +1,102 @@
1
+import React from 'react';
2
+import PropTypes from 'prop-types';
3
+import ImmutablePropTypes from 'react-immutable-proptypes';
4
+import { injectIntl, FormattedMessage } from 'react-intl';
5
+import Toggle from 'react-toggle';
6
+import AsyncSelect from 'react-select/lib/Async';
7
+
8
+@injectIntl
9
+export default class ColumnSettings extends React.PureComponent {
10
+
11
+  static propTypes = {
12
+    settings: ImmutablePropTypes.map.isRequired,
13
+    onChange: PropTypes.func.isRequired,
14
+    onLoad: PropTypes.func.isRequired,
15
+    intl: PropTypes.object.isRequired,
16
+  };
17
+
18
+  state = {
19
+    open: this.hasTags(),
20
+  };
21
+
22
+  hasTags () {
23
+    return ['all', 'any', 'none'].map(mode => this.tags(mode).length > 0).includes(true);
24
+  }
25
+
26
+  tags (mode) {
27
+    let tags = this.props.settings.getIn(['tags', mode]) || [];
28
+    if (tags.toJSON) {
29
+      return tags.toJSON();
30
+    } else {
31
+      return tags;
32
+    }
33
+  };
34
+
35
+  onSelect = (mode) => {
36
+    return (value) => {
37
+      this.props.onChange(['tags', mode], value);
38
+    };
39
+  };
40
+
41
+  onToggle = () => {
42
+    if (this.state.open && this.hasTags()) {
43
+      this.props.onChange('tags', {});
44
+    }
45
+    this.setState({ open: !this.state.open });
46
+  };
47
+
48
+  modeSelect (mode) {
49
+    return (
50
+      <div className='column-settings__section'>
51
+        {this.modeLabel(mode)}
52
+        <AsyncSelect
53
+          isMulti
54
+          autoFocus
55
+          value={this.tags(mode)}
56
+          settings={this.props.settings}
57
+          settingPath={['tags', mode]}
58
+          onChange={this.onSelect(mode)}
59
+          loadOptions={this.props.onLoad}
60
+          classNamePrefix='column-settings__hashtag-select'
61
+          name='tags'
62
+        />
63
+      </div>
64
+    );
65
+  }
66
+
67
+  modeLabel (mode) {
68
+    switch(mode) {
69
+    case 'any':  return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />;
70
+    case 'all':  return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />;
71
+    case 'none': return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />;
72
+    }
73
+    return '';
74
+  };
75
+
76
+  render () {
77
+    return (
78
+      <div>
79
+        <div className='column-settings__row'>
80
+          <div className='setting-toggle'>
81
+            <Toggle
82
+              id='hashtag.column_settings.tag_toggle'
83
+              onChange={this.onToggle}
84
+              checked={this.state.open}
85
+            />
86
+            <span className='setting-toggle__label'>
87
+              <FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
88
+            </span>
89
+          </div>
90
+        </div>
91
+        {this.state.open &&
92
+          <div className='column-settings__hashtags'>
93
+            {this.modeSelect('any')}
94
+            {this.modeSelect('all')}
95
+            {this.modeSelect('none')}
96
+          </div>
97
+        }
98
+      </div>
99
+    );
100
+  }
101
+
102
+}

+ 31
- 0
app/javascript/flavours/glitch/features/hashtag_timeline/containers/column_settings_container.js View File

@@ -0,0 +1,31 @@
1
+import { connect } from 'react-redux';
2
+import ColumnSettings from '../components/column_settings';
3
+import { changeColumnParams } from 'flavours/glitch/actions/columns';
4
+import api from 'flavours/glitch/util/api';
5
+
6
+const mapStateToProps = (state, { columnId }) => {
7
+  const columns = state.getIn(['settings', 'columns']);
8
+  const index   = columns.findIndex(c => c.get('uuid') === columnId);
9
+
10
+  if (!(columnId && index >= 0)) {
11
+    return {};
12
+  }
13
+
14
+  return { settings: columns.get(index).get('params') };
15
+};
16
+
17
+const mapDispatchToProps = (dispatch, { columnId }) => ({
18
+  onChange (key, value) {
19
+    dispatch(changeColumnParams(columnId, key, value));
20
+  },
21
+
22
+  onLoad (value) {
23
+    return api().get('/api/v2/search', { params: { q: value } }).then(response => {
24
+      return (response.data.hashtags || []).map((tag) => {
25
+        return { value: tag.name, label: `#${tag.name}` };
26
+      });
27
+    });
28
+  },
29
+});
30
+
31
+export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);

+ 57
- 16
app/javascript/flavours/glitch/features/hashtag_timeline/index.js View File

@@ -4,10 +4,12 @@ import PropTypes from 'prop-types';
4 4
 import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
5 5
 import Column from 'flavours/glitch/components/column';
6 6
 import ColumnHeader from 'flavours/glitch/components/column_header';
7
-import { expandHashtagTimeline } from 'flavours/glitch/actions/timelines';
7
+import ColumnSettingsContainer from './containers/column_settings_container';
8
+import { expandHashtagTimeline, clearTimeline } from 'flavours/glitch/actions/timelines';
8 9
 import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
9 10
 import { FormattedMessage } from 'react-intl';
10 11
 import { connectHashtagStream } from 'flavours/glitch/actions/streaming';
12
+import { isEqual } from 'lodash';
11 13
 
12 14
 const mapStateToProps = (state, props) => ({
13 15
   hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0,
@@ -16,6 +18,8 @@ const mapStateToProps = (state, props) => ({
16 18
 @connect(mapStateToProps)
17 19
 export default class HashtagTimeline extends React.PureComponent {
18 20
 
21
+  disconnects = [];
22
+
19 23
   static propTypes = {
20 24
     params: PropTypes.object.isRequired,
21 25
     columnId: PropTypes.string,
@@ -34,6 +38,30 @@ export default class HashtagTimeline extends React.PureComponent {
34 38
     }
35 39
   }
36 40
 
41
+  title = () => {
42
+    let title = [this.props.params.id];
43
+    if (this.additionalFor('any')) {
44
+      title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.any'  values={{ additional: this.additionalFor('any') }} defaultMessage='or {additional}' />);
45
+    }
46
+    if (this.additionalFor('all')) {
47
+      title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.all'  values={{ additional: this.additionalFor('all') }} defaultMessage='and {additional}' />);
48
+    }
49
+    if (this.additionalFor('none')) {
50
+      title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.none' values={{ additional: this.additionalFor('none') }} defaultMessage='without {additional}' />);
51
+    }
52
+    return title;
53
+  }
54
+
55
+  additionalFor = (mode) => {
56
+    const { tags } = this.props.params;
57
+
58
+    if (tags && (tags[mode] || []).length > 0) {
59
+      return tags[mode].map(tag => tag.value).join('/');
60
+    } else {
61
+      return '';
62
+    }
63
+  }
64
+
37 65
   handleMove = (dir) => {
38 66
     const { columnId, dispatch } = this.props;
39 67
     dispatch(moveColumn(columnId, dir));
@@ -43,30 +71,40 @@ export default class HashtagTimeline extends React.PureComponent {
43 71
     this.column.scrollTop();
44 72
   }
45 73
 
46
-  _subscribe (dispatch, id) {
47
-    this.disconnect = dispatch(connectHashtagStream(id));
74
+  _subscribe (dispatch, id, tags = {}) {
75
+    let any  = (tags.any || []).map(tag => tag.value);
76
+    let all  = (tags.all || []).map(tag => tag.value);
77
+    let none = (tags.none || []).map(tag => tag.value);
78
+
79
+    [id, ...any].map((tag) => {
80
+      this.disconnects.push(dispatch(connectHashtagStream(id, tag, (status) => {
81
+        let tags = status.tags.map(tag => tag.name);
82
+        return all.filter(tag => tags.includes(tag)).length === all.length &&
83
+               none.filter(tag => tags.includes(tag)).length === 0;
84
+      })));
85
+    });
48 86
   }
49 87
 
50 88
   _unsubscribe () {
51
-    if (this.disconnect) {
52
-      this.disconnect();
53
-      this.disconnect = null;
54
-    }
89
+    this.disconnects.map(disconnect => disconnect());
90
+    this.disconnects = [];
55 91
   }
56 92
 
57 93
   componentDidMount () {
58 94
     const { dispatch } = this.props;
59
-    const { id } = this.props.params;
95
+    const { id, tags } = this.props.params;
60 96
 
61
-    dispatch(expandHashtagTimeline(id));
62
-    this._subscribe(dispatch, id);
97
+    dispatch(expandHashtagTimeline(id, { tags }));
63 98
   }
64 99
 
65 100
   componentWillReceiveProps (nextProps) {
66
-    if (nextProps.params.id !== this.props.params.id) {
67
-      this.props.dispatch(expandHashtagTimeline(nextProps.params.id));
101
+    const { dispatch, params } = this.props;
102
+    const { id, tags } = nextProps.params;
103
+    if (id !== params.id || !isEqual(tags, params.tags)) {
68 104
       this._unsubscribe();
69
-      this._subscribe(this.props.dispatch, nextProps.params.id);
105
+      this._subscribe(dispatch, id, tags);
106
+      this.props.dispatch(clearTimeline(`hashtag:${id}`));
107
+      this.props.dispatch(expandHashtagTimeline(id, { tags }));
70 108
     }
71 109
   }
72 110
 
@@ -79,7 +117,8 @@ export default class HashtagTimeline extends React.PureComponent {
79 117
   }
80 118
 
81 119
   handleLoadMore = maxId => {
82
-    this.props.dispatch(expandHashtagTimeline(this.props.params.id, { maxId }));
120
+    const { id, tags } = this.props.params;
121
+    this.props.dispatch(expandHashtagTimeline(id, { maxId, tags }));
83 122
   }
84 123
 
85 124
   render () {
@@ -92,14 +131,16 @@ export default class HashtagTimeline extends React.PureComponent {
92 131
         <ColumnHeader
93 132
           icon='hashtag'
94 133
           active={hasUnread}
95
-          title={id}
134
+          title={this.title()}
96 135
           onPin={this.handlePin}
97 136
           onMove={this.handleMove}
98 137
           onClick={this.handleHeaderClick}
99 138
           pinned={pinned}
100 139
           multiColumn={multiColumn}
101 140
           showBackButton
102
-        />
141
+        >
142
+          {columnId && <ColumnSettingsContainer columnId={columnId} />}
143
+        </ColumnHeader>
103 144
 
104 145
         <StatusListContainer
105 146
           trackScroll={!pinned}

+ 1
- 1
app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js View File

@@ -27,7 +27,7 @@ export default class HashtagTimeline extends React.PureComponent {
27 27
     const { dispatch, hashtag } = this.props;
28 28
 
29 29
     dispatch(expandHashtagTimeline(hashtag));
30
-    this.disconnect = dispatch(connectHashtagStream(hashtag));
30
+    this.disconnect = dispatch(connectHashtagStream(hashtag, hashtag));
31 31
   }
32 32
 
33 33
   componentWillUnmount () {

+ 2
- 2
app/javascript/flavours/glitch/reducers/accounts_counters.js View File

@@ -141,9 +141,9 @@ export default function accountsCounters(state = initialState, action) {
141 141
     if (action.alreadyFollowing) {
142 142
       return state;
143 143
     }
144
-    return state.updateIn([action.relationship.id, 'followers_count'], num => num + 1);
144
+    return state.updateIn([action.relationship.id, 'followers_count'], num => num < 0 ? num : num + 1);
145 145
   case ACCOUNT_UNFOLLOW_SUCCESS:
146
-    return state.updateIn([action.relationship.id, 'followers_count'], num => Math.max(0, num - 1));
146
+    return state.updateIn([action.relationship.id, 'followers_count'], num => num < 0 ? num : Math.max(0, num - 1));
147 147
   default:
148 148
     return state;
149 149
   }

+ 0
- 245
app/javascript/flavours/glitch/reducers/notifications.js.orig View File

@@ -1,245 +0,0 @@
1
-import {
2
-  NOTIFICATIONS_MOUNT,
3
-  NOTIFICATIONS_UNMOUNT,
4
-  NOTIFICATIONS_SET_VISIBILITY,
5
-  NOTIFICATIONS_UPDATE,
6
-  NOTIFICATIONS_EXPAND_SUCCESS,
7
-  NOTIFICATIONS_EXPAND_REQUEST,
8
-  NOTIFICATIONS_EXPAND_FAIL,
9
-  NOTIFICATIONS_CLEAR,
10
-  NOTIFICATIONS_SCROLL_TOP,
11
-  NOTIFICATIONS_DELETE_MARKED_REQUEST,
12
-  NOTIFICATIONS_DELETE_MARKED_SUCCESS,
13
-  NOTIFICATION_MARK_FOR_DELETE,
14
-  NOTIFICATIONS_DELETE_MARKED_FAIL,
15
-  NOTIFICATIONS_ENTER_CLEARING_MODE,
16
-  NOTIFICATIONS_MARK_ALL_FOR_DELETE,
17
-} from 'flavours/glitch/actions/notifications';
18
-import {
19
-  ACCOUNT_BLOCK_SUCCESS,
20
-  ACCOUNT_MUTE_SUCCESS,
21
-} from 'flavours/glitch/actions/accounts';
22
-import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from 'flavours/glitch/actions/timelines';
23
-import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
24
-import compareId from 'flavours/glitch/util/compare_id';
25
-
26
-const initialState = ImmutableMap({
27
-  items: ImmutableList(),
28
-  hasMore: true,
29
-  top: true,
30
-  mounted: 0,
31
-  unread: 0,
32
-  lastReadId: '0',
33
-  isLoading: false,
34
-  cleaningMode: false,
35
-  isTabVisible: true,
36
-  // notification removal mark of new notifs loaded whilst cleaningMode is true.
37
-  markNewForDelete: false,
38
-});
39
-
40
-const notificationToMap = (state, notification) => ImmutableMap({
41
-  id: notification.id,
42
-  type: notification.type,
43
-  account: notification.account.id,
44
-  markedForDelete: state.get('markNewForDelete'),
45
-  status: notification.status ? notification.status.id : null,
46
-});
47
-
48
-const normalizeNotification = (state, notification) => {
49
-  const top = !shouldCountUnreadNotifications(state);
50
-
51
-  if (top) {
52
-    state = state.set('lastReadId', notification.id);
53
-  } else {
54
-    state = state.update('unread', unread => unread + 1);
55
-  }
56
-
57
-  return state.update('items', list => {
58
-    if (top && list.size > 40) {
59
-      list = list.take(20);
60
-    }
61
-
62
-    return list.unshift(notificationToMap(state, notification));
63
-  });
64
-};
65
-
66
-const expandNormalizedNotifications = (state, notifications, next) => {
67
-  const top = !(shouldCountUnreadNotifications(state));
68
-  const lastReadId = state.get('lastReadId');
69
-  let items = ImmutableList();
70
-
71
-  notifications.forEach((n, i) => {
72
-    items = items.set(i, notificationToMap(state, n));
73
-  });
74
-
75
-  return state.withMutations(mutable => {
76
-    if (!items.isEmpty()) {
77
-      mutable.update('items', list => {
78
-        const lastIndex = 1 + list.findLastIndex(
79
-          item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id'))
80
-        );
81
-
82
-        const firstIndex = 1 + list.take(lastIndex).findLastIndex(
83
-          item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0
84
-        );
85
-
86
-        return list.take(firstIndex).concat(items, list.skip(lastIndex));
87
-      });
88
-    }
89
-
90
-    if (top) {
91
-      if (!items.isEmpty()) {
92
-        mutable.update('lastReadId', id => compareId(id, items.first().get('id')) > 0 ? id : items.first().get('id'));
93
-      }
94
-    } else {
95
-      mutable.update('unread', unread => unread + items.filter(item => compareId(item.get('id'), lastReadId) > 0).size);
96
-    }
97
-
98
-    if (!next) {
99
-      mutable.set('hasMore', false);
100
-    }
101
-
102
-    mutable.set('isLoading', false);
103
-  });
104
-};
105
-
106
-const filterNotifications = (state, relationship) => {
107
-  return state.update('items', list => list.filterNot(item => item !== null && item.get('account') === relationship.id));
108
-};
109
-
110
-const clearUnread = (state) => {
111
-  state = state.set('unread', 0);
112
-  const lastNotification = state.get('items').find(item => item !== null);
113
-  return state.set('lastReadId', lastNotification ? lastNotification.get('id') : '0');
114
-}
115
-
116
-const updateTop = (state, top) => {
117
-  state = state.set('top', top);
118
-
119
-  if (!shouldCountUnreadNotifications(state)) {
120
-    state = clearUnread(state);
121
-  }
122
-
123
-  return state.set('top', top);
124
-};
125
-
126
-const deleteByStatus = (state, statusId) => {
127
-  const top = !(shouldCountUnreadNotifications(state));
128
-  if (!top) {
129
-    const lastReadId = state.get('lastReadId');
130
-    const deletedUnread = state.get('items').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0);
131
-    state = state.update('unread', unread => unread - deletedUnread.size);
132
-  }
133
-  return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId));
134
-};
135
-
136
-const markForDelete = (state, notificationId, yes) => {
137
-  return state.update('items', list => list.map(item => {
138
-    if(item.get('id') === notificationId) {
139
-      return item.set('markedForDelete', yes);
140
-    } else {
141
-      return item;
142
-    }
143
-  }));
144
-};
145
-
146
-const markAllForDelete = (state, yes) => {
147
-  return state.update('items', list => list.map(item => {
148
-    if(yes !== null) {
149
-      return item.set('markedForDelete', yes);
150
-    } else {
151
-      return item.set('markedForDelete', !item.get('markedForDelete'));
152
-    }
153
-  }));
154
-};
155
-
156
-const unmarkAllForDelete = (state) => {
157
-  return state.update('items', list => list.map(item => item.set('markedForDelete', false)));
158
-};
159
-
160
-const deleteMarkedNotifs = (state) => {
161
-  return state.update('items', list => list.filterNot(item => item.get('markedForDelete')));
162
-};
163
-
164
-const updateMounted = (state) => {
165
-  state = state.update('mounted', count => count + 1);
166
-  if (!shouldCountUnreadNotifications(state)) {
167
-    state = clearUnread(state);
168
-  }
169
-  return state;
170
-};
171
-
172
-const updateVisibility = (state, visibility) => {
173
-  state = state.set('isTabVisible', visibility);
174
-  if (!shouldCountUnreadNotifications(state)) {
175
-    state = clearUnread(state);
176
-  }
177
-  return state;
178
-};
179
-
180
-const shouldCountUnreadNotifications = (state) => {
181
-  return !(state.get('isTabVisible') && state.get('top') && state.get('mounted') > 0);
182
-};
183
-
184
-export default function notifications(state = initialState, action) {
185
-  let st;
186
-
187
-  switch(action.type) {
188
-  case NOTIFICATIONS_MOUNT:
189
-    return updateMounted(state);
190
-  case NOTIFICATIONS_UNMOUNT:
191
-    return state.update('mounted', count => count - 1);
192
-  case NOTIFICATIONS_SET_VISIBILITY:
193
-    return updateVisibility(state, action.visibility);
194
-  case NOTIFICATIONS_EXPAND_REQUEST:
195
-  case NOTIFICATIONS_DELETE_MARKED_REQUEST:
196
-    return state.set('isLoading', true);
197
-  case NOTIFICATIONS_DELETE_MARKED_FAIL:
198
-  case NOTIFICATIONS_EXPAND_FAIL:
199
-    return state.set('isLoading', false);
200
-  case NOTIFICATIONS_SCROLL_TOP:
201
-    return updateTop(state, action.top);
202
-  case NOTIFICATIONS_UPDATE:
203
-    return normalizeNotification(state, action.notification);
204
-  case NOTIFICATIONS_EXPAND_SUCCESS:
205
-    return expandNormalizedNotifications(state, action.notifications, action.next);
206
-  case ACCOUNT_BLOCK_SUCCESS:
207
-  case ACCOUNT_MUTE_SUCCESS:
208
-    return filterNotifications(state, action.relationship);
209
-  case NOTIFICATIONS_CLEAR:
210
-    return state.set('items', ImmutableList()).set('hasMore', false);
211
-  case TIMELINE_DELETE:
212
-    return deleteByStatus(state, action.id);
213
-  case TIMELINE_DISCONNECT:
214
-    return action.timeline === 'home' ?
215
-      state.update('items', items => items.first() ? items.unshift(null) : items) :
216
-      state;
217
-
218
-  case NOTIFICATION_MARK_FOR_DELETE:
219
-    return markForDelete(state, action.id, action.yes);
220
-
221
-  case NOTIFICATIONS_DELETE_MARKED_SUCCESS:
222
-    return deleteMarkedNotifs(state).set('isLoading', false);
223
-
224
-  case NOTIFICATIONS_ENTER_CLEARING_MODE:
225
-    st = state.set('cleaningMode', action.yes);
226
-    if (!action.yes) {
227
-      return unmarkAllForDelete(st).set('markNewForDelete', false);
228
-    } else {
229
-      return st;
230
-    }
231
-
232
-  case NOTIFICATIONS_MARK_ALL_FOR_DELETE:
233
-    st = state;
234
-    if (action.yes === null) {
235
-      // Toggle - this is a bit confusing, as it toggles the all-none mode
236
-      //st = st.set('markNewForDelete', !st.get('markNewForDelete'));
237
-    } else {
238
-      st = st.set('markNewForDelete', action.yes);
239
-    }
240
-    return markAllForDelete(st, action.yes);
241
-
242
-  default:
243
-    return state;
244
-  }
245
-};

+ 7
- 0
app/javascript/flavours/glitch/reducers/timelines.js View File

@@ -1,6 +1,7 @@
1 1
 import {
2 2
   TIMELINE_UPDATE,
3 3
   TIMELINE_DELETE,
4
+  TIMELINE_CLEAR,
4 5
   TIMELINE_EXPAND_SUCCESS,
5 6
   TIMELINE_EXPAND_REQUEST,
6 7
   TIMELINE_EXPAND_FAIL,
@@ -81,6 +82,10 @@ const deleteStatus = (state, id, accountId, references) => {
81 82
   return state;
82 83
 };
83 84
 
85
+const clearTimeline = (state, timeline) => {
86
+  return state.updateIn([timeline, 'items'], list => list.clear());
87
+};
88
+
84 89
 const filterTimelines = (state, relationship, statuses) => {
85 90
   let references;
86 91
 
@@ -121,6 +126,8 @@ export default function timelines(state = initialState, action) {
121 126
     return updateTimeline(state, action.timeline, fromJS(action.status));
122 127
   case TIMELINE_DELETE:
123 128
     return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
129
+  case TIMELINE_CLEAR:
130
+    return clearTimeline(state, action.timeline);
124 131
   case ACCOUNT_BLOCK_SUCCESS:
125 132
   case ACCOUNT_MUTE_SUCCESS:
126 133
     return filterTimelines(state, action.relationship, action.statuses);

+ 31
- 0
app/javascript/flavours/glitch/styles/_mixins.scss View File

@@ -51,3 +51,34 @@
51 51
     border-radius: 0px;
52 52
   }
53 53
 }
54
+
55
+@mixin search-input() {
56
+  outline: 0;
57
+  box-sizing: border-box;
58
+  width: 100%;
59
+  border: none;
60
+  box-shadow: none;
61
+  font-family: inherit;
62
+  background: $ui-base-color;
63
+  color: $darker-text-color;
64
+  font-size: 14px;
65
+  margin: 0;
66
+
67
+  &::-moz-focus-inner {
68
+    border: 0;
69
+  }
70
+
71
+  &::-moz-focus-inner,
72
+  &:focus,
73
+  &:active {
74
+    outline: 0 !important;
75
+  }
76
+
77
+  &:focus {
78
+    background: lighten($ui-base-color, 4%);
79
+  }
80
+
81
+  @media screen and (max-width: 600px) {
82
+    font-size: 16px;
83
+  }
84
+}

+ 4
- 0
app/javascript/flavours/glitch/styles/admin.scss View File

@@ -553,6 +553,10 @@ a.name-tag,
553 553
     border-left-color: lighten($error-red, 12%);
554 554
   }
555 555
 
556
+  &.warning {
557
+    border-left-color: $gold-star;
558
+  }
559
+
556 560
   &__bubble {
557 561
     padding: 16px;
558 562
     padding-left: 14px;

+ 22
- 0
app/javascript/flavours/glitch/styles/components/accounts.scss View File

@@ -339,6 +339,26 @@
339 339
   display: block;
340 340
   font-weight: 500;
341 341
   margin-bottom: 10px;
342
+
343
+  .column-settings__hashtag-select {
344
+    &__control {
345
+      @include search-input();
346
+    }
347
+
348
+    &__multi-value {
349
+      background: lighten($ui-base-color, 8%);
350
+    }
351
+
352
+    &__multi-value__label,
353
+    &__input {
354
+      color: $darker-text-color;
355
+    }
356
+
357
+    &__indicator-separator,
358
+    &__dropdown-indicator {
359
+      display: none;
360
+    }
361
+  }
342 362
 }
343 363
 
344 364
 .column-settings__row {
@@ -451,10 +471,12 @@
451 471
   border-bottom: 1px solid lighten($ui-base-color, 8%);
452 472
   cursor: default;
453 473
   display: flex;
474
+  flex-shrink: 0;
454 475
 
455 476
   button {
456 477
     background: darken($ui-base-color, 4%);
457 478
     border: 0;
479
+    margin: 0;
458 480
   }
459 481
 
460 482
   button,

+ 46
- 37
app/javascript/flavours/glitch/styles/components/drawer.scss View File

@@ -1,9 +1,10 @@
1 1
 .drawer {
2
+  width: 300px;
3
+  box-sizing: border-box;
2 4
   display: flex;
3 5
   flex-direction: column;
4
-  box-sizing: border-box;
6
+  overflow-y: hidden;
5 7
   padding: 10px 5px;
6
-  width: 300px;
7 8
   flex: none;
8 9
 
9 10
   &:first-child {
@@ -38,41 +39,6 @@
38 39
   }
39 40
 
40 41
   .react-swipeable-view-container & { height: 100% }
41
-
42
-  & > .contents {
43
-    display: flex;
44
-    position: relative;
45
-    flex-direction: column;
46
-    padding: 0;
47
-    flex-grow: 1;
48
-    background: lighten($ui-base-color, 13%);
49
-    overflow-x: hidden;
50
-    overflow-y: auto;
51
-
52
-    & > .mastodon {
53
-      flex: 1;
54
-      border: none;
55
-      cursor: inherit;
56
-    }
57
-  }
58
-
59
-  @for $i from 0 through 3 {
60
-    &.mbstobon-#{$i} > .contents {
61
-      @if $i == 3 {
62
-        background: url('~flavours/glitch/images/wave-drawer.png') no-repeat bottom / 100% auto, lighten($ui-base-color, 13%);
63
-      } @else {
64
-        background: url('~flavours/glitch/images/wave-drawer-glitched.png') no-repeat bottom / 100% auto, lighten($ui-base-color, 13%);
65
-      }
66
-
67
-      & > .mastodon {
68
-        background: url("~flavours/glitch/images/mbstobon-ui-#{$i}.png") no-repeat left bottom / contain;
69
-
70
-        @if $i != 3 {
71
-          filter: contrast(50%) brightness(50%);
72
-        }
73
-      }
74
-    }
75
-  }
76 42
 }
77 43
 
78 44
 .drawer--header {
@@ -342,6 +308,31 @@
342 308
   }
343 309
 }
344 310
 
311
+.drawer__inner__mastodon {
312
+  background: lighten($ui-base-color, 13%) url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>') no-repeat bottom / 100% auto;
313
+  flex: 1;
314
+  min-height: 47px;
315
+
316
+  > img {
317
+    display: block;
318
+    object-fit: contain;
319
+    object-position: bottom left;
320
+    width: 100%;
321
+    height: 100%;
322
+    pointer-events: none;
323
+    user-drag: none;
324
+    user-select: none;
325
+  }
326
+
327
+  > .mastodon {
328
+    display: block;
329
+    width: 100%;
330
+    height: 100%;
331
+    border: none;
332
+    cursor: inherit;
333
+  }
334
+}
335
+
345 336
 .pseudo-drawer {
346 337
   background: lighten($ui-base-color, 13%);
347 338
   font-size: 13px;
@@ -357,3 +348,21 @@
357 348
   height: 100%;
358 349
   background: rgba($base-overlay-background, 0.5);
359 350
 }
351
+
352
+@for $i from 0 through 3 {
353
+  .mbstobon-#{$i} .drawer__inner__mastodon {
354
+    @if $i == 3 {
355
+      background: url('~flavours/glitch/images/wave-drawer.png') no-repeat bottom / 100% auto, lighten($ui-base-color, 13%);
356
+    } @else {
357
+      background: url('~flavours/glitch/images/wave-drawer-glitched.png') no-repeat bottom / 100% auto, lighten($ui-base-color, 13%);
358
+    }
359
+
360
+    & > .mastodon {
361
+      background: url("~flavours/glitch/images/mbstobon-ui-#{$i}.png") no-repeat left bottom / contain;
362
+
363
+      @if $i != 3 {
364
+        filter: contrast(50%) brightness(50%);
365
+      }
366
+    }
367
+  }
368
+}

+ 1
- 27
app/javascript/flavours/glitch/styles/components/search.scss View File

@@ -3,36 +3,10 @@
3 3
 }
4 4
 
5 5
 .search__input {
6
-  outline: 0;
7
-  box-sizing: border-box;
8 6
   display: block;
9
-  width: 100%;
10
-  border: none;
11 7
   padding: 10px;
12 8
   padding-right: 30px;
13
-  font-family: inherit;
14
-  background: $ui-base-color;
15
-  color: $darker-text-color;
16
-  font-size: 14px;
17
-  margin: 0;
18
-
19
-  &::-moz-focus-inner {
20
-    border: 0;
21
-  }
22
-
23
-  &::-moz-focus-inner,
24
-  &:focus,
25
-  &:active {
26
-    outline: 0 !important;
27
-  }
28
-
29
-  &:focus {
30
-    background: lighten($ui-base-color, 4%);
31
-  }
32
-
33
-  @media screen and (max-width: 600px) {
34
-    font-size: 16px;
35
-  }
9
+  @include search-input();
36 10
 }
37 11
 
38 12
 .search__icon {

+ 1
- 1
app/javascript/flavours/glitch/styles/mastodon-light/diff.scss View File

@@ -58,7 +58,7 @@
58 58
   background: $ui-base-color;
59 59
 }
60 60
 
61
-.drawer > .contents {
61
+.drawer__inner__mastodon {
62 62
   background: $ui-base-color url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color(darken($ui-base-color, 13%))}"/></svg>') no-repeat bottom / 100% auto !important;
63 63
 
64 64
   .mastodon {

+ 1
- 0
app/javascript/flavours/glitch/util/initial_state.js View File

@@ -24,6 +24,7 @@ export const searchEnabled = getMeta('search_enabled');
24 24
 export const maxChars = (initialState && initialState.max_toot_chars) || 500;
25 25
 export const invitesEnabled = getMeta('invites_enabled');
26 26
 export const version = getMeta('version');
27
+export const mascot = getMeta('mascot');
27 28
 export const isStaff = getMeta('is_staff');
28 29
 
29 30
 export default initialState;

+ 4
- 0
app/javascript/images/icon_flag.svg View File

@@ -0,0 +1,4 @@
1
+<svg fill="#FFFFFF" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
2
+  <path d="M0 0h24v24H0z" fill="none"/>
3
+  <path d="M14.4 6L14 4H5v17h2v-7h5.6l.4 2h7V6z"/>
4
+</svg>

BIN
app/javascript/images/mailer/icon_warning.png View File


+ 4
- 2
app/javascript/mastodon/components/modal_root.js View File

@@ -33,13 +33,15 @@ export default class ModalRoot extends React.PureComponent {
33 33
     } else if (!nextProps.children) {
34 34
       this.setState({ revealed: false });
35 35
     }
36
+    if (!nextProps.children && !!this.props.children) {
37
+      this.activeElement.focus();
38
+      this.activeElement = null;
39
+    }
36 40
   }
37 41
 
38 42
   componentDidUpdate (prevProps) {
39 43
     if (!this.props.children && !!prevProps.children) {
40 44
       this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
41
-      this.activeElement.focus();
42
-      this.activeElement = null;
43 45
     }
44 46
     if (this.props.children) {
45 47
       requestAnimationFrame(() => {

+ 1
- 1
app/javascript/mastodon/components/scrollable_list.js View File

@@ -49,7 +49,7 @@ export default class ScrollableList extends PureComponent {
49 49
       const { scrollTop, scrollHeight, clientHeight } = this.node;
50 50
       const offset = scrollHeight - scrollTop - clientHeight;
51 51
 
52
-      if (400 > offset && this.props.onLoadMore && !this.props.isLoading) {
52
+      if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
53 53
         this.props.onLoadMore();
54 54
       }
55 55
 

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

@@ -136,7 +136,7 @@ class GettingStarted extends ImmutablePureComponent {
136 136
 
137 137
           <div className='getting-started__footer'>
138 138
             <ul>
139
-              <li><a href='https://bridge.joinmastodon.org/' target='_blank'><FormattedMessage id='getting_started.find_friends' defaultMessage='Find friends from Twitter' /></a> · </li>
139
+              <li><a href='/explore' target='_blank'><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></a> · </li>
140 140
               {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
141 141
               {multiColumn && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
142 142
               <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>

+ 1
- 8
app/javascript/mastodon/features/public_timeline/index.js View File

@@ -100,13 +100,6 @@ class PublicTimeline extends React.PureComponent {
100 100
     dispatch(expandPublicTimeline({ maxId, onlyMedia }));
101 101
   }
102 102
 
103
-  handleSettingChanged = (key, checked) => {
104
-    const { columnId } = this.props;
105
-    if (!columnId && key[0] === 'other' && key[1] === 'onlyMedia') {
106
-      this.context.router.history.replace(`/timelines/public${checked ? '/media' : ''}`);
107
-    }
108
-  }
109
-
110 103
   render () {
111 104
     const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia } = this.props;
112 105
     const pinned = !!columnId;
@@ -123,7 +116,7 @@ class PublicTimeline extends React.PureComponent {
123 116
           pinned={pinned}
124 117
           multiColumn={multiColumn}
125 118
         >
126
-          <ColumnSettingsContainer onChange={this.handleSettingChanged} columnId={columnId} />
119
+          <ColumnSettingsContainer columnId={columnId} />
127 120
         </ColumnHeader>
128 121
 
129 122
         <StatusListContainer

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

@@ -77,6 +77,7 @@ class EmbedModal extends ImmutablePureComponent {
77 77
             className='embed-modal__iframe'
78 78
             frameBorder='0'
79 79
             ref={this.setIframeRef}
80
+            sandbox='allow-same-origin'
80 81
             title='preview'
81 82
           />
82 83
         </div>

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

@@ -150,9 +150,7 @@ class SwitchingColumnsArea extends React.PureComponent {
150 150
           <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
151 151
           <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
152 152
           <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
153
-          <WrappedRoute path='/timelines/public/media' component={PublicTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll, onlyMedia: true }} />
154 153
           <WrappedRoute path='/timelines/public/local' exact component={CommunityTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
155
-          <WrappedRoute path='/timelines/public/local/media' component={CommunityTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll, onlyMedia: true }} />
156 154
           <WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
157 155
           <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
158 156
           <WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />

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

@@ -132,8 +132,8 @@
132 132
   "follow_request.authorize": "ترخيص",
133 133
   "follow_request.reject": "رفض",
134 134
   "getting_started.developers": "المُطوِّرون",
135
+  "getting_started.directory": "Profile directory",
135 136
   "getting_started.documentation": "Documentation",
136
-  "getting_started.find_friends": "البحث عن أصدقاء على تويتر",
137 137
   "getting_started.heading": "إستعدّ للبدء",
138 138
   "getting_started.invite": "دعوة أشخاص",
139 139
   "getting_started.open_source_notice": "ماستدون برنامج مفتوح المصدر. يمكنك المساهمة، أو الإبلاغ عن تقارير الأخطاء، على جيت هب {github}.",
@@ -149,6 +149,23 @@
149 149
   "home.column_settings.basic": "أساسية",
150 150
   "home.column_settings.show_reblogs": "عرض الترقيات",
151 151
   "home.column_settings.show_replies": "عرض الردود",
152
+  "introduction.federation.action": "Next",
153
+  "introduction.federation.federated.headline": "Federated",
154
+  "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
155
+  "introduction.federation.home.headline": "Home",
156
+  "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
157
+  "introduction.federation.local.headline": "Local",
158
+  "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
159
+  "introduction.interactions.action": "Finish tutorial!",
160
+  "introduction.interactions.favourite.headline": "Favourite",
161
+  "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
162
+  "introduction.interactions.reblog.headline": "Boost",
163
+  "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
164
+  "introduction.interactions.reply.headline": "Reply",
165
+  "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
166
+  "introduction.welcome.action": "Let's go!",
167
+  "introduction.welcome.headline": "First steps",
168
+  "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
152 169
   "keyboard_shortcuts.back": "للعودة",
153 170
   "keyboard_shortcuts.blocked": "لفتح قائمة المستخدمين المحظورين",
154 171
   "keyboard_shortcuts.boost": "للترقية",
@@ -225,34 +242,21 @@
225 242
   "notifications.clear_confirmation": "أمتأكد من أنك تود مسح جل الإخطارات الخاصة بك و المتلقاة إلى حد الآن ؟",
226 243
   "notifications.column_settings.alert": "إشعارات سطح المكتب",
227 244
   "notifications.column_settings.favourite": "المُفَضَّلة :",
245
+  "notifications.column_settings.filter_bar.advanced": "Display all categories",
246
+  "notifications.column_settings.filter_bar.category": "Quick filter bar",
247
+  "notifications.column_settings.filter_bar.show": "Show",
228 248
   "notifications.column_settings.follow": "متابعُون جُدُد :",
229 249
   "notifications.column_settings.mention": "الإشارات :",
230 250
   "notifications.column_settings.push": "الإخطارات المدفوعة",
231 251
   "notifications.column_settings.reblog": "الترقيّات:",
232 252
   "notifications.column_settings.show": "إعرِضها في عمود",
233 253
   "notifications.column_settings.sound": "أصدر صوتا",
254
+  "notifications.filter.all": "All",
255
+  "notifications.filter.boosts": "Boosts",
256
+  "notifications.filter.favourites": "Favourites",
257
+  "notifications.filter.follows": "Follows",
258
+  "notifications.filter.mentions": "Mentions",
234 259
   "notifications.group": "{count} إشعارات",
235
-  "onboarding.done": "تم",
236
-  "onboarding.next": "التالي",
237
-  "onboarding.page_five.public_timelines": "تُعرَض في الخيط الزمني المحلي المشاركات العامة المحررة من طرف جميع المسجلين في {domain}. أما في الخيط الزمني الموحد ، فإنه يتم عرض جميع المشاركات العامة المنشورة من طرف جميع الأشخاص المتابَعين من طرف أعضاء {domain}. هذه هي الخيوط الزمنية العامة، وهي طريقة رائعة للتعرف أشخاص جدد.",
238
-  "onboarding.page_four.home": "تعرض الصفحة الرئيسية منشورات جميع الأشخاص الذين تتابعهم.",
239
-  "onboarding.page_four.notifications": "فعندما يتفاعل شخص ما معك، عمود الإخطارات يخبرك.",
240
-  "onboarding.page_one.federation": "ماستدون شبكة من خوادم مستقلة متلاحمة تهدف إلى إنشاء أكبر شبكة اجتماعية موحدة. تسمى هذه السرفيرات بمثيلات خوادم.",
241
-  "onboarding.page_one.full_handle": "عنوانك الكامل",
242
-  "onboarding.page_one.handle_hint": "هذا هو ما يجب عليك توصيله لأصدقائك للبحث عنه.",
243
-  "onboarding.page_one.welcome": "مرحبا بك في ماستدون !",
244
-  "onboarding.page_six.admin": "مدير(ة) مثيل الخادم هذا {admin}.",
245
-  "onboarding.page_six.almost_done": "أنهيت تقريبا ...",
246
-  "onboarding.page_six.appetoot": "تمتع بالتبويق !",
247
-  "onboarding.page_six.apps_available": "هناك {apps} متوفرة لأنظمة آي أو إس و أندرويد و غيرها من المنصات و الأنظمة.",
248
-  "onboarding.page_six.github": "ماستدون برنامج مفتوح المصدر. يمكنك المساهمة، أو الإبلاغ عن تقارير الأخطاء، على GitHub {github}.",
249
-  "onboarding.page_six.guidelines": "المبادئ التوجيهية للمجتمع",
250
-  "onboarding.page_six.read_guidelines": "رجاءا، قم بالإطلاع على {guidelines} لـ {domain} !",
251
-  "onboarding.page_six.various_app": "تطبيقات الجوال",
252
-  "onboarding.page_three.profile": "يمكنك إدخال تعديلات على ملفك الشخصي عن طريق تغيير الصورة الرمزية و السيرة و إسمك المستعار. هناك، سوف تجد أيضا تفضيلات أخرى متاحة.",
253
-  "onboarding.page_three.search": "باستخدام شريط البحث يمكنك العثور على أشخاص و أصدقاء أو الإطلاع على أوسمة، كـ {illustration} و {introductions}. للبحث عن شخص غير مسجل في مثيل الخادم هذا، استخدم مُعرّفه الكامل.",
254
-  "onboarding.page_two.compose": "حرر مشاركاتك عبر عمود التحرير. يمكنك من خلاله تحميل الصور وتغيير إعدادات الخصوصية وإضافة تحذيرات عن المحتوى باستخدام الرموز أدناه.",
255
-  "onboarding.skip": "تخطي",
256 260
   "privacy.change": "إضبط خصوصية المنشور",
257 261
   "privacy.direct.long": "أنشر إلى المستخدمين المشار إليهم فقط",
258 262
   "privacy.direct.short": "مباشر",

+ 26
- 22
app/javascript/mastodon/locales/ast.json View File

@@ -132,8 +132,8 @@
132 132
   "follow_request.authorize": "Autorizar",
133 133
   "follow_request.reject": "Refugar",
134 134
   "getting_started.developers": "Desendolcadores",
135
+  "getting_started.directory": "Profile directory",
135 136
   "getting_started.documentation": "Documentación",
136
-  "getting_started.find_friends": "Alcontrar collacios de Twitter",
137 137
   "getting_started.heading": "Entamu",
138 138
   "getting_started.invite": "Convidar xente",
139 139
   "getting_started.open_source_notice": "Mastodon ye software de códigu abiertu. Pues collaborar o informar de fallos en {github} (GitHub).",
@@ -149,6 +149,23 @@
149 149
   "home.column_settings.basic": "Basic",
150 150
   "home.column_settings.show_reblogs": "Amosar toots compartíos",
151 151
   "home.column_settings.show_replies": "Amosar rempuestes",
152
+  "introduction.federation.action": "Next",
153
+  "introduction.federation.federated.headline": "Federated",
154
+  "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
155
+  "introduction.federation.home.headline": "Home",
156
+  "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
157
+  "introduction.federation.local.headline": "Local",
158
+  "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
159
+  "introduction.interactions.action": "Finish tutorial!",
160
+  "introduction.interactions.favourite.headline": "Favourite",
161
+  "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
162
+  "introduction.interactions.reblog.headline": "Boost",
163
+  "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
164
+  "introduction.interactions.reply.headline": "Reply",
165
+  "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
166
+  "introduction.welcome.action": "Let's go!",
167
+  "introduction.welcome.headline": "First steps",
168
+  "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
152 169
   "keyboard_shortcuts.back": "pa dir p'atrás",
153 170
   "keyboard_shortcuts.blocked": "p'abrir la llista d'usuarios bloquiaos",
154 171
   "keyboard_shortcuts.boost": "pa compartir un toot",
@@ -225,34 +242,21 @@
225 242
   "notifications.clear_confirmation": "¿De xuru que quies llimpiar dafechu tolos avisos?",
226 243
   "notifications.column_settings.alert": "Avisos d'escritoriu",
227 244
   "notifications.column_settings.favourite": "Favoritos:",
245
+  "notifications.column_settings.filter_bar.advanced": "Display all categories",
246
+  "notifications.column_settings.filter_bar.category": "Quick filter bar",
247
+  "notifications.column_settings.filter_bar.show": "Show",
228 248
   "notifications.column_settings.follow": "Siguidores nuevos:",
229 249
   "notifications.column_settings.mention": "Menciones:",
230 250
   "notifications.column_settings.push": "Push notifications",
231 251
   "notifications.column_settings.reblog": "Toots compartíos:",
232 252
   "notifications.column_settings.show": "Amosar en columna",
233 253
   "notifications.column_settings.sound": "Reproducir soníu",
254
+  "notifications.filter.all": "All",
255
+  "notifications.filter.boosts": "Boosts",
256
+  "notifications.filter.favourites": "Favourites",
257
+  "notifications.filter.follows": "Follows",
258
+  "notifications.filter.mentions": "Mentions",
234 259
   "notifications.group": "{count} avisos",
235
-  "onboarding.done": "Fecho",
236
-  "onboarding.next": "Siguiente",
237
-  "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
238
-  "onboarding.page_four.home": "La llinia temporal d'aniciu amuesa artículos de xente a la que sigues.",
239
-  "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
240
-  "onboarding.page_one.federation": "Mastodon ye una rede de sividores independientes xuníos pa facer una rede social grande. Nós llamamos instancies a esos sirvidores.",
241
-  "onboarding.page_one.full_handle": "Your full handle",
242
-  "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.",
243
-  "onboarding.page_one.welcome": "¡Afáyate en Mastodon!",
244
-  "onboarding.page_six.admin": "Your instance's admin is {admin}.",
245
-  "onboarding.page_six.almost_done": "Almost done...",
246
-  "onboarding.page_six.appetoot": "Bon Appetoot!",
247
-  "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
248
-  "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
249
-  "onboarding.page_six.guidelines": "community guidelines",
250
-  "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
251
-  "onboarding.page_six.various_app": "aplicaciones pa móviles",
252
-  "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
253
-  "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
254
-  "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
255
-  "onboarding.skip": "Skip",
256 260
   "privacy.change": "Adjust status privacy",
257 261
   "privacy.direct.long": "Post to mentioned users only",
258 262
   "privacy.direct.short": "Direct",

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

@@ -132,8 +132,8 @@
132 132
   "follow_request.authorize": "Authorize",
133 133
   "follow_request.reject": "Reject",
134 134
   "getting_started.developers": "Developers",
135
+  "getting_started.directory": "Profile directory",
135 136
   "getting_started.documentation": "Documentation",
136
-  "getting_started.find_friends": "Find friends from Twitter",
137 137
   "getting_started.heading": "Първи стъпки",
138 138
   "getting_started.invite": "Invite people",
139 139
   "getting_started.open_source_notice": "Mastodon е софтуер с отворен код. Можеш да помогнеш или да докладваш за проблеми в Github: {github}.",
@@ -149,6 +149,23 @@
149 149
   "home.column_settings.basic": "Basic",
150 150
   "home.column_settings.show_reblogs": "Show boosts",
151 151
   "home.column_settings.show_replies": "Show replies",
152
+  "introduction.federation.action": "Next",
153
+  "introduction.federation.federated.headline": "Federated",
154
+  "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
155
+  "introduction.federation.home.headline": "Home",
156
+  "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
157
+  "introduction.federation.local.headline": "Local",
158
+  "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
159
+  "introduction.interactions.action": "Finish tutorial!",
160
+  "introduction.interactions.favourite.headline": "Favourite",
161
+  "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
162
+  "introduction.interactions.reblog.headline": "Boost",
163
+  "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
164
+  "introduction.interactions.reply.headline": "Reply",
165
+  "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
166
+  "introduction.welcome.action": "Let's go!",
167
+  "introduction.welcome.headline": "First steps",
168
+  "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
152 169
   "keyboard_shortcuts.back": "to navigate back",
153 170
   "keyboard_shortcuts.blocked": "to open blocked users list",
154 171
   "keyboard_shortcuts.boost": "to boost",
@@ -225,34 +242,21 @@
225 242
   "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
226 243
   "notifications.column_settings.alert": "Десктоп известия",
227 244
   "notifications.column_settings.favourite": "Предпочитани:",
245
+  "notifications.column_settings.filter_bar.advanced": "Display all categories",
246
+  "notifications.column_settings.filter_bar.category": "Quick filter bar",
247
+  "notifications.column_settings.filter_bar.show": "Show",
228 248
   "notifications.column_settings.follow": "Нови последователи:",
229 249
   "notifications.column_settings.mention": "Споменавания:",
230 250
   "notifications.column_settings.push": "Push notifications",
231 251
   "notifications.column_settings.reblog": "Споделяния:",
232 252
   "notifications.column_settings.show": "Покажи в колона",
233 253
   "notifications.column_settings.sound": "Play sound",
254
+  "notifications.filter.all": "All",
255
+  "notifications.filter.boosts": "Boosts",
256
+  "notifications.filter.favourites": "Favourites",
257
+  "notifications.filter.follows": "Follows",
258
+  "notifications.filter.mentions": "Mentions",
234 259
   "notifications.group": "{count} notifications",
235
-  "onboarding.done": "Done",
236
-  "onboarding.next": "Next",
237
-  "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
238
-  "onboarding.page_four.home": "The home timeline shows posts from people you follow.",
239
-  "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
240
-  "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
241
-  "onboarding.page_one.full_handle": "Your full handle",
242
-  "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.",
243
-  "onboarding.page_one.welcome": "Welcome to Mastodon!",
244
-  "onboarding.page_six.admin": "Your instance's admin is {admin}.",
245
-  "onboarding.page_six.almost_done": "Almost done...",
246
-  "onboarding.page_six.appetoot": "Bon Appetoot!",
247
-  "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
248
-  "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
249
-  "onboarding.page_six.guidelines": "community guidelines",
250
-  "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
251
-  "onboarding.page_six.various_app": "mobile apps",
252
-  "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
253
-  "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
254
-  "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
255
-  "onboarding.skip": "Skip",
256 260
   "privacy.change": "Adjust status privacy",
257 261
   "privacy.direct.long": "Post to mentioned users only",
258 262
   "privacy.direct.short": "Direct",

+ 26
- 22
app/javascript/mastodon/locales/ca.json View File

@@ -132,8 +132,8 @@
132 132
   "follow_request.authorize": "Autoritzar",
133 133
   "follow_request.reject": "Rebutjar",
134 134
   "getting_started.developers": "Desenvolupadors",
135
+  "getting_started.directory": "Profile directory",
135 136
   "getting_started.documentation": "Documentació",
136
-  "getting_started.find_friends": "Troba amics de Twitter",
137 137
   "getting_started.heading": "Començant",
138 138
   "getting_started.invite": "Convida gent",
139 139
   "getting_started.open_source_notice": "Mastodon és un programari de codi obert. Pots contribuir o informar de problemes a GitHub a {github}.",
@@ -149,6 +149,23 @@
149 149
   "home.column_settings.basic": "Bàsic",
150 150
   "home.column_settings.show_reblogs": "Mostrar impulsos",
151 151
   "home.column_settings.show_replies": "Mostrar respostes",
152
+  "introduction.federation.action": "Next",
153
+  "introduction.federation.federated.headline": "Federated",
154
+  "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
155
+  "introduction.federation.home.headline": "Home",
156
+  "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
157
+  "introduction.federation.local.headline": "Local",
158
+  "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
159
+  "introduction.interactions.action": "Finish tutorial!",
160
+  "introduction.interactions.favourite.headline": "Favourite",
161
+  "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
162
+  "introduction.interactions.reblog.headline": "Boost",
163
+  "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
164
+  "introduction.interactions.reply.headline": "Reply",
165
+  "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
166
+  "introduction.welcome.action": "Let's go!",
167
+  "introduction.welcome.headline": "First steps",
168
+  "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
152 169
   "keyboard_shortcuts.back": "navegar enrera",
153 170
   "keyboard_shortcuts.blocked": "per obrir la llista d'usuaris bloquejats",
154 171
   "keyboard_shortcuts.boost": "impulsar",
@@ -225,34 +242,21 @@
225 242
   "notifications.clear_confirmation": "Estàs segur que vols esborrar permanenment totes les teves notificacions?",
226 243
   "notifications.column_settings.alert": "Notificacions d'escriptori",
227 244
   "notifications.column_settings.favourite": "Favorits:",
245
+  "notifications.column_settings.filter_bar.advanced": "Display all categories",
246
+  "notifications.column_settings.filter_bar.category": "Quick filter bar",
247
+  "notifications.column_settings.filter_bar.show": "Show",
228 248
   "notifications.column_settings.follow": "Nous seguidors:",
229 249
   "notifications.column_settings.mention": "Mencions:",
230 250
   "notifications.column_settings.push": "Push notificacions",
231 251
   "notifications.column_settings.reblog": "Impulsos:",
232 252
   "notifications.column_settings.show": "Mostrar en la columna",
233 253
   "notifications.column_settings.sound": "Reproduïr so",
254
+  "notifications.filter.all": "All",
255
+  "notifications.filter.boosts": "Boosts",
256
+  "notifications.filter.favourites": "Favourites",
257
+  "notifications.filter.follows": "Follows",
258
+  "notifications.filter.mentions": "Mentions",
234 259
   "notifications.group": "{count} notificacions",
235
-  "onboarding.done": "Fet",
236
-  "onboarding.next": "Següent",
237
-  "onboarding.page_five.public_timelines": "La línia de temps local mostra missatges públics de tothom de {domain}. La línia de temps federada mostra els missatges públics de tothom que la gent de {domain} segueix. Aquests són les línies de temps Públiques, una bona manera de descobrir noves persones.",
238
-  "onboarding.page_four.home": "La línia de temps d'Inici mostra missatges de les persones que segueixes.",
239
-  "onboarding.page_four.notifications": "La columna Notificacions mostra quan algú interactua amb tu.",
240
-  "onboarding.page_one.federation": "Mastodon és una xarxa de servidors independents que s'uneixen per fer una xarxa social encara més gran. A aquests servidors els hi diem instàncies.",
241
-  "onboarding.page_one.full_handle": "El teu usuari complet",
242
-  "onboarding.page_one.handle_hint": "Això és el que els hi diries als teus amics que cerquin.",
243
-  "onboarding.page_one.welcome": "Benvingut a Mastodon!",
244
-  "onboarding.page_six.admin": "L'administrador de la teva instància és {admin}.",
245
-  "onboarding.page_six.almost_done": "Quasi fet...",
246
-  "onboarding.page_six.appetoot": "Bon Appetoot!",
247
-  "onboarding.page_six.apps_available": "Hi ha {apps} disponibles per iOS, Android i altres plataformes.",
248
-  "onboarding.page_six.github": "Mastodon és un programari de codi obert. Pots informar d'errors, sol·licitar característiques o contribuir en el codi a {github}.",
249
-  "onboarding.page_six.guidelines": "Normes de la comunitat",
250
-  "onboarding.page_six.read_guidelines": "Si us plau llegeix les {guidelines} de {domain}!",
251
-  "onboarding.page_six.various_app": "aplicacions per mòbils",
252
-  "onboarding.page_three.profile": "Edita el teu perfil per canviar el teu avatar, bio o el nom de visualització. També hi trobaràs altres preferències.",
253
-  "onboarding.page_three.search": "Utilitza la barra de cerca per trobar gent i mirar etiquetes, com a {illustration} i {introductions}. Per buscar una persona que no està en aquesta instància, utilitza tot el seu nom d'usuari complert.",
254
-  "onboarding.page_two.compose": "Escriu missatges en la columna de redacció. Pots pujar imatges, canviar la configuració de privacitat i afegir les advertències de contingut amb les icones de sota.",
255
-  "onboarding.skip": "Omet",
256 260
   "privacy.change": "Ajusta l'estat de privacitat",
257 261
   "privacy.direct.long": "Publicar només per als usuaris esmentats",
258 262
   "privacy.direct.short": "Directe",

+ 26
- 22
app/javascript/mastodon/locales/co.json View File

@@ -132,8 +132,8 @@
132 132
   "follow_request.authorize": "Auturizà",
133 133
   "follow_request.reject": "Righjittà",
134 134
   "getting_started.developers": "Sviluppatori",
135
+  "getting_started.directory": "Profile directory",
135 136
   "getting_started.documentation": "Documentation",
136
-  "getting_started.find_friends": "Truvà amichi da Twitter",
137 137
   "getting_started.heading": "Per principià",
138 138
   "getting_started.invite": "Invità ghjente",
139 139
   "getting_started.open_source_notice": "Mastodon ghjè un lugiziale liberu. Pudete cuntribuisce à u codice o a traduzione, o palisà un bug, nant'à GitHub: {github}.",
@@ -149,6 +149,23 @@
149 149
   "home.column_settings.basic": "Bàsichi",
150 150
   "home.column_settings.show_reblogs": "Vede e spartere",
151 151
   "home.column_settings.show_replies": "Vede e risposte",
152
+  "introduction.federation.action": "Next",
153
+  "introduction.federation.federated.headline": "Federated",
154
+  "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
155
+  "introduction.federation.home.headline": "Home",
156
+  "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
157
+  "introduction.federation.local.headline": "Local",
158
+  "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
159
+  "introduction.interactions.action": "Finish tutorial!",
160
+  "introduction.interactions.favourite.headline": "Favourite",
161
+  "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
162
+  "introduction.interactions.reblog.headline": "Boost",
163
+  "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
164
+  "introduction.interactions.reply.headline": "Reply",
165
+  "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
166
+  "introduction.welcome.action": "Let's go!",
167
+  "introduction.welcome.headline": "First steps",
168
+  "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
152 169
   "keyboard_shortcuts.back": "rivultà",
153 170
   "keyboard_shortcuts.blocked": "per apre una lista d'utilizatori bluccati",
154 171
   "keyboard_shortcuts.boost": "sparte",
@@ -225,34 +242,21 @@
225 242
   "notifications.clear_confirmation": "Site sicuru·a che vulete toglie tutte ste nutificazione?",
226 243
   "notifications.column_settings.alert": "Nutificazione nant'à l'urdinatore",
227 244
   "notifications.column_settings.favourite": "Favuriti:",
245
+  "notifications.column_settings.filter_bar.advanced": "Display all categories",
246
+  "notifications.column_settings.filter_bar.category": "Quick filter bar",
247
+  "notifications.column_settings.filter_bar.show": "Show",
228 248
   "notifications.column_settings.follow": "Abbunati novi:",
229 249
   "notifications.column_settings.mention": "Minzione:",
230 250
   "notifications.column_settings.push": "Nutificazione Push",
231 251
   "notifications.column_settings.reblog": "Spartere:",
232 252
   "notifications.column_settings.show": "Mustrà indè a colonna",
233 253
   "notifications.column_settings.sound": "Sunà",
254
+  "notifications.filter.all": "All",
255
+  "notifications.filter.boosts": "Boosts",
256
+  "notifications.filter.favourites": "Favourites",
257
+  "notifications.filter.follows": "Follows",
258
+  "notifications.filter.mentions": "Mentions",
234 259
   "notifications.group": "{count} nutificazione",
235
-  "onboarding.done": "Fatta",
236
-  "onboarding.next": "Siguente",
237
-  "onboarding.page_five.public_timelines": "A linea pubblica lucale mostra statuti pubblichi da tuttu u mondu nant'à {domain}. A linea pubblica glubale mostra ancu quelli di a ghjente seguitata da l'utilizatori di {domain}. Quesse sò una bona manera d'incuntrà nove parsone.",
238
-  "onboarding.page_four.home": "A linea d'accolta mostra i statuti di i vostr'abbunamenti.",
239
-  "onboarding.page_four.notifications": "A colonna di nutificazione mostra l'interazzione ch'altre parsone anu cù u vostru contu.",
240
-  "onboarding.page_one.federation": "Mastodon ghjè una rete di servori independenti, chjamati istanze, uniti indè una sola rete suciale.",
241
-  "onboarding.page_one.full_handle": "U vostru identificatore cumplettu",
242
-  "onboarding.page_one.handle_hint": "Quessu ghjè cio chì direte à i vostri amichi per circavi.",
243
-  "onboarding.page_one.welcome": "Benvenuti/a nant'à Mastodon!",
244
-  "onboarding.page_six.admin": "L'amministratore di a vostr'istanza hè {admin}.",
245
-  "onboarding.page_six.almost_done": "Quasgi finitu...",
246
-  "onboarding.page_six.appetoot": "Bon Appitootu!",
247
-  "onboarding.page_six.apps_available": "Ci sò {apps} dispunibule per iOS, Android è altre piattaforme.",
248
-  "onboarding.page_six.github": "Mastodon ghjè un lugiziale liberu. Pudete cuntribuisce à u codice o a traduzione, o palisà un prublemu, nant'à {github}.",
249
-  "onboarding.page_six.guidelines": "regule di a cumunità",
250
-  "onboarding.page_six.read_guidelines": "Ùn vi scurdate di leghje e {guidelines} di {domain}!",
251
-  "onboarding.page_six.various_app": "applicazione pè u telefuninu",
252
-  "onboarding.page_three.profile": "Pudete mudificà u prufile per cambia u ritrattu, a descrizzione è u nome affissatu. Ci sò ancu alcun'altre preferenze.",
253
-  "onboarding.page_three.search": "Fate usu di l'area di ricerca per truvà altre persone è vede hashtag cum'è {illustration} o {introductions}. Per vede qualcunu ch'ùn hè micca nant'à st'istanza, cercate u so identificatore complettu (pare un'email).",
254
-  "onboarding.page_two.compose": "I statuti è missaghji si scrivenu indè l'area di ridazzione. Pudete caricà imagine, cambià i parametri di pubblicazione, è mette avertimenti di cuntenuti cù i buttoni quì sottu.",
255
-  "onboarding.skip": "Passà",
256 260
   "privacy.change": "Mudificà a cunfidenzialità di u statutu",
257 261
   "privacy.direct.long": "Mandà solu à quelli chì so mintuvati",
258 262
   "privacy.direct.short": "Direttu",

+ 31
- 27
app/javascript/mastodon/locales/cs.json View File

@@ -132,8 +132,8 @@
132 132
   "follow_request.authorize": "Autorizovat",
133 133
   "follow_request.reject": "Odmítnout",
134 134
   "getting_started.developers": "Vývojáři",
135
+  "getting_started.directory": "Profile directory",
135 136
   "getting_started.documentation": "Dokumentace",
136
-  "getting_started.find_friends": "Najděte si přátele z Twitteru",
137 137
   "getting_started.heading": "Začínáme",
138 138
   "getting_started.invite": "Pozvat lidi",
139 139
   "getting_started.open_source_notice": "Mastodon je otevřený software. Na GitHubu k němu můžete přispět nebo nahlásit chyby: {github}.",
@@ -149,6 +149,23 @@
149 149
   "home.column_settings.basic": "Základní",
150 150
   "home.column_settings.show_reblogs": "Zobrazit boosty",
151 151
   "home.column_settings.show_replies": "Zobrazit odpovědi",
152
+  "introduction.federation.action": "Next",
153
+  "introduction.federation.federated.headline": "Federated",
154
+  "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
155
+  "introduction.federation.home.headline": "Home",
156
+  "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
157
+  "introduction.federation.local.headline": "Local",
158
+  "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
159
+  "introduction.interactions.action": "Finish tutorial!",
160
+  "introduction.interactions.favourite.headline": "Favourite",
161
+  "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
162
+  "introduction.interactions.reblog.headline": "Boost",
163
+  "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
164
+  "introduction.interactions.reply.headline": "Reply",
165
+  "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
166
+  "introduction.welcome.action": "Let's go!",
167
+  "introduction.welcome.headline": "First steps",
168
+  "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
152 169
   "keyboard_shortcuts.back": "k návratu zpět",
153 170
   "keyboard_shortcuts.blocked": "k otevření seznamu blokovaných uživatelů",
154 171
   "keyboard_shortcuts.boost": "k boostnutí",
@@ -195,7 +212,7 @@
195 212
   "media_gallery.toggle_visible": "Přepínat viditelnost",
196 213
   "missing_indicator.label": "Nenalezeno",
197 214
   "missing_indicator.sublabel": "Tento zdroj se nepodařilo najít",
198
-  "mute_modal.hide_notifications": "Skrýt oznámení před tímto uživatelem?",
215
+  "mute_modal.hide_notifications": "Skrýt oznámení od tohoto uživatele?",
199 216
   "navigation_bar.apps": "Mobilní aplikace",
200 217
   "navigation_bar.blocks": "Blokovaní uživatelé",
201 218
   "navigation_bar.community_timeline": "Místní časová osa",
@@ -225,34 +242,21 @@
225 242
   "notifications.clear_confirmation": "Jste si jistý/á, že chcete trvale vymazat všechna vaše oznámení?",
226 243
   "notifications.column_settings.alert": "Desktopová oznámení",
227 244
   "notifications.column_settings.favourite": "Oblíbené:",
245
+  "notifications.column_settings.filter_bar.advanced": "Display all categories",
246
+  "notifications.column_settings.filter_bar.category": "Quick filter bar",
247
+  "notifications.column_settings.filter_bar.show": "Show",
228 248
   "notifications.column_settings.follow": "Noví sledovatelé:",
229 249
   "notifications.column_settings.mention": "Zmínky:",
230 250
   "notifications.column_settings.push": "Push oznámení",
231 251
   "notifications.column_settings.reblog": "Boosty:",
232 252
   "notifications.column_settings.show": "Zobrazit ve sloupci",
233 253
   "notifications.column_settings.sound": "Přehrát zvuk",
254
+  "notifications.filter.all": "All",
255
+  "notifications.filter.boosts": "Boosts",
256
+  "notifications.filter.favourites": "Favourites",
257
+  "notifications.filter.follows": "Follows",
258
+  "notifications.filter.mentions": "Mentions",
234 259
   "notifications.group": "{count} oznámení",
235
-  "onboarding.done": "Hotovo",
236
-  "onboarding.next": "Další",
237
-  "onboarding.page_five.public_timelines": "Místní časová osa zobrazuje veřejné příspěvky od všech lidí na {domain}. Federovaná časová osa zobrazuje veřejné příspěvky ode všech, které lidé na {domain} sledují. Toto jsou veřejné časové osy, výborný způsob, jak objevovat nové lidi.",
238
-  "onboarding.page_four.home": "Domovská časová osa zobrazuje příspěvky od lidí, které sledujete.",
239
-  "onboarding.page_four.notifications": "Sloupec oznámení se zobrazí, když s vámi někdo bude komunikovat.",
240
-  "onboarding.page_one.federation": "Mastodon je síť nezávislých serverů, jejichž propojením vzniká jedna velká sociální síť. Těmto serverům říkáme instance.",
241
-  "onboarding.page_one.full_handle": "Vaše celá adresa profilu",
242
-  "onboarding.page_one.handle_hint": "Tohle je, co byste řekl/a svým přátelům, aby hledali.",
243
-  "onboarding.page_one.welcome": "Vítejte na Mastodonu!",
244
-  "onboarding.page_six.admin": "Administrátorem vaší instance je {admin}.",
245
-  "onboarding.page_six.almost_done": "Skoro hotovo...",
246
-  "onboarding.page_six.appetoot": "Bon appetoot!",
247
-  "onboarding.page_six.apps_available": "Jsou dostupné {apps} pro iOS, Android a jiné platformy.",
248
-  "onboarding.page_six.github": "Mastodon je svobodný a otevřený software. Na {github} můžete nahlásit chyby, požádat o nové funkce, nebo přispívat ke kódu.",
249
-  "onboarding.page_six.guidelines": "komunitní pravidla",
250
-  "onboarding.page_six.read_guidelines": "Prosím přečtěte si {guidelines} {domain}!",
251
-  "onboarding.page_six.various_app": "mobilní aplikace",
252
-  "onboarding.page_three.profile": "Upravte si svůj profil a změňte si svůj avatar, popis profilu a zobrazované jméno. V nastaveních najdete i další možnosti.",
253
-  "onboarding.page_three.search": "Pomocí vyhledávacího řádku najděte lidi a podívejte se na hashtagy jako {illustration} a {introductions}. Chcete-li najít někoho, kdo není na této instanci, použijte jeho celou adresu profilu.",
254
-  "onboarding.page_two.compose": "Příspěvky pište z pole na komponování. Ikonami níže můžete nahrávat obrázky, změnit nastavení soukromí a přidat varování o obsahu.",
255
-  "onboarding.skip": "Přeskočit",
256 260
   "privacy.change": "Změnit soukromí příspěvku",
257 261
   "privacy.direct.long": "Odeslat pouze zmíněným uživatelům",
258 262
   "privacy.direct.short": "Přímý",
@@ -270,12 +274,12 @@
270 274
   "relative_time.minutes": "{number} m",
271 275
   "relative_time.seconds": "{number} s",
272 276
   "reply_indicator.cancel": "Zrušit",
273
-  "report.forward": "Přeposlat k {target}",
277
+  "report.forward": "Přeposlat na {target}",
274 278
   "report.forward_hint": "Tento účet je z jiného serveru. Chcete na něj také poslat anonymizovanou kopii?",
275 279
   "report.hint": "Toto nahlášení bude zasláno moderátorům vaší instance. Níže můžete uvést, proč tento účet nahlašujete:",
276
-  "report.placeholder": "Další komentáře",
280
+  "report.placeholder": "Dodatečné komentáře",
277 281
   "report.submit": "Odeslat",
278
-  "report.target": "Nahlásit {target}",
282
+  "report.target": "Nahlášení uživatele {target}",
279 283
   "search.placeholder": "Hledat",
280 284
   "search_popout.search_format": "Pokročilé hledání",
281 285
   "search_popout.tips.full_text": "Jednoduchý textový výpis příspěvků, které jste napsal/a, oblíbil/a si, boostnul/a, nebo v nich byl/a zmíněn/a, včetně odpovídajících přezdívek, zobrazovaných jmen a hashtagů.",
@@ -291,7 +295,7 @@
291 295
   "status.block": "Zablokovat uživatele @{name}",
292 296
   "status.cancel_reblog_private": "Zrušit boost",
293 297
   "status.cannot_reblog": "Tento příspěvek nemůže být boostnutý",
294
-  "status.delete": "Delete",
298
+  "status.delete": "Smazat",
295 299
   "status.detailed_status": "Detailní zobrazení konverzace",
296 300
   "status.direct": "Poslat přímou zprávu uživateli @{name}",
297 301
   "status.embed": "Vložit",

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

@@ -132,8 +132,8 @@
132 132
   "follow_request.authorize": "Caniatau",
133 133
   "follow_request.reject": "Gwrthod",
134 134
   "getting_started.developers": "Datblygwyr",
135
+  "getting_started.directory": "Profile directory",
135 136
   "getting_started.documentation": "Dogfennaeth",
136
-  "getting_started.find_friends": "Canfod ffrindiau o Twitter",
137 137
   "getting_started.heading": "Dechrau",
138 138
   "getting_started.invite": "Gwahodd pobl",
139 139
   "getting_started.open_source_notice": "Mae Mastodon yn feddalwedd côd agored. Mae modd cyfrannu neu adrodd materion ar GitHUb ar {github}.",
@@ -149,6 +149,23 @@
149 149
   "home.column_settings.basic": "Syml",
150 150
   "home.column_settings.show_reblogs": "Dangos bŵstiau",
151 151
   "home.column_settings.show_replies": "Dangos ymatebion",
152
+  "introduction.federation.action": "Next",
153
+  "introduction.federation.federated.headline": "Federated",
154
+  "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
155
+  "introduction.federation.home.headline": "Home",
156
+  "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
157
+  "introduction.federation.local.headline": "Local",
158
+  "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
159
+  "introduction.interactions.action": "Finish tutorial!",
160
+  "introduction.interactions.favourite.headline": "Favourite",
161
+  "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
162
+  "introduction.interactions.reblog.headline": "Boost",
163
+  "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
164
+  "introduction.interactions.reply.headline": "Reply",
165
+  "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
166
+  "introduction.welcome.action": "Let's go!",
167
+  "introduction.welcome.headline": "First steps",
168
+  "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.",
152 169
   "keyboard_shortcuts.back": "i lywio nôl",
153 170
   "keyboard_shortcuts.blocked": "i agor rhestr defnyddwyr a flociwyd",
154 171
   "keyboard_shortcuts.boost": "i fŵstio",
@@ -225,34 +242,21 @@
225 242
   "notifications.clear_confirmation": "Ydych chi'n sicr eich bod am glirio'ch holl hysbysiadau am byth?",
226 243
   "notifications.column_settings.alert": "Hysbysiadau bwrdd gwaith",
227 244
   "notifications.column_settings.favourite": "Ffefrynnau:",
245
+  "notifications.column_settings.filter_bar.advanced": "Display all categories",
246
+  "notifications.column_settings.filter_bar.category": "Quick filter bar",
247
+  "notifications.column_settings.filter_bar.show": "Show",
228 248
   "notifications.column_settings.follow": "Dilynwyr newydd:",
229 249
   "notifications.column_settings.mention": "Crybwylliadau:",
230 250
   "notifications.column_settings.push": "Hysbysiadau push",
231 251
   "notifications.column_settings.reblog": "Boosts:",
232 252
   "notifications.column_settings.show": "Dangos yn y golofn",
233 253
   "notifications.column_settings.sound": "Chwarae sain",
254
+  "notifications.filter.all": "All",
255
+  "notifications.filter.boosts": "Boosts",
256
+  "notifications.filter.favourites": "Favourites",
257
+  "notifications.filter.follows": "Follows",
258
+  "notifications.filter.mentions": "Mentions",
234 259
   "notifications.group": "{count} o hysbysiadau",
235
-  "onboarding.done": "Wedi'i wneud",
236
-  "onboarding.next": "Nesaf",
237
-  "onboarding.page_five.public_timelines": "Mae'r ffrwd leol yn dangos tŵtiau cyhoeddus o bawb ar y {domain}. Mae ffrwd y ffederasiwn yn dangos tŵtiau cyhoeddus o bawb y mae pobl ar y {domain} yn dilyn. Mae rhain yn Ffrydiau Cyhoeddus, ffordd wych o ddarganfod pobl newydd.",
238
-  "onboarding.page_four.home": "Mae'r ffrwd gartref yn dangos twtiau o bobl yr ydych yn dilyn.",
239
-  "onboarding.page_four.notifications": "Mae'r golofn hysbysiadau yn dangos pan mae rhywun yn ymwneud a chi.",
240
-  "onboarding.page_one.federation": "Mae mastodon yn rwydwaith o weinyddwyr anibynnol sy'n uno i greu un rhwydwaith gymdeithasol mwy. Yr ydym yn galw'r gweinyddwyr yma yn achosion.",
241
-  "onboarding.page_one.full_handle": "Eich enw Mastodon llawn",
242
-  "onboarding.page_one.handle_hint": "Dyma beth y bysech chi'n dweud wrth eich ffrindiau i chwilota amdano.",
243
-  "onboarding.page_one.welcome": "Croeso i Mastodon!",
244
-  "onboarding.page_six.admin": "Gweinyddwr eich achos yw {admin}.",
245
-  "onboarding.page_six.almost_done": "Bron a gorffen...",
246
-  "onboarding.page_six.appetoot": "Bon Apetŵt!",
247
-  "onboarding.page_six.apps_available": "Mae yna {apps} ar gael i iOS, Android a platfformau eraill.",
248
-  "onboarding.page_six.github": "Mae Mastodon yn feddalwedd côd agored rhad ac am ddim. Mae modd adrodd bygiau, gwneud ceisiadau am nodweddion penodol, neu gyfrannu i'r côd ar {github}.",
249
-  "onboarding.page_six.guidelines": "canllawiau cymunedol",
250
-  "onboarding.page_six.read_guidelines": "Darllenwch {guidelines} y {domain} os gwelwch yn dda!",
251
-  "onboarding.page_six.various_app": "apiau symudol",
252
-  "onboarding.page_three.profile": "Golygwch eich proffil i newid eich afatar, bywgraffiad, ac enw arddangos. Yno fe fyddwch hefyd yn canfod gosodiadau eraill.",
253
-  "onboarding.page_three.search": "Defnyddiwch y bar chwilio i ganfod pobl ac i edrych ar eu hashnodau, megis {illustration} ac {introductions}. I chwilio am rhywun nad ydynt ar yr achos hwn, defnyddiwch eu enw Mastodon llawn.",
254
-  "onboarding.page_two.compose": "Ysrgifenwch dŵtiau o'r golofn cyfansoddi. Mae modd uwchlwytho lluniau, newid gosodiadau preifatrwydd, ac ychwanegu rhybudd cynnwys gyda'r eiconau isod.",
255
-  "onboarding.skip": "Sgipio",
256 260
   "privacy.change": "Addasu preifatrwdd y statws",
257 261
   "privacy.direct.long": "Cyhoeddi i'r defnyddwyr sy'n cael eu crybwyll yn unig",
258 262
   "privacy.direct.short": "Uniongyrchol",

+ 26
- 22
app/javascript/mastodon/locales/da.json