Browse Source

Merge branch 'master' into live

master
Zac 7 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 @@
# frozen_string_literal: true

module Admin
class AccountActionsController < BaseController
before_action :set_account

def new
@account_action = Admin::AccountAction.new(type: params[:type], report_id: params[:report_id], send_email_notification: true)
@warning_presets = AccountWarningPreset.all
end

def create
account_action = Admin::AccountAction.new(resource_params)
account_action.target_account = @account
account_action.current_account = current_account

account_action.save!

if account_action.with_report?
redirect_to admin_report_path(account_action.report)
else
redirect_to admin_account_path(@account.id)
end
end

private

def set_account
@account = Account.find(params[:account_id])
end

def resource_params
params.require(:admin_account_action).permit(:type, :report_id, :warning_preset_id, :text, :send_email_notification)
end
end
end

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

@@ -14,6 +14,7 @@ module Admin
else
@account = @account_moderation_note.target_account
@moderation_notes = @account.targeted_moderation_notes.latest
@warnings = @account.targeted_account_warnings.latest.custom

render template: 'admin/accounts/show'
end

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

@@ -2,9 +2,9 @@

module Admin
class AccountsController < BaseController
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :disable, :memorialize]
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize]
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
before_action :require_local_account!, only: [:enable, :disable, :memorialize]
before_action :require_local_account!, only: [:enable, :memorialize]

def index
authorize :account, :index?
@@ -13,8 +13,10 @@ module Admin

def show
authorize @account, :show?

@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
@moderation_notes = @account.targeted_moderation_notes.latest
@moderation_notes = @account.targeted_moderation_notes.latest
@warnings = @account.targeted_account_warnings.latest.custom
end

def subscribe
@@ -43,10 +45,17 @@ module Admin
redirect_to admin_account_path(@account.id)
end

def disable
authorize @account.user, :disable?
@account.user.disable!
log_action :disable, @account.user
def unsilence
authorize @account, :unsilence?
@account.unsilence!
log_action :unsilence, @account
redirect_to admin_account_path(@account.id)
end

def unsuspend
authorize @account, :unsuspend?
@account.unsuspend!
log_action :unsuspend, @account
redirect_to admin_account_path(@account.id)
end


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

@@ -13,75 +13,42 @@ module Admin
authorize @report, :show?

@report_note = @report.notes.new
@report_notes = (@report.notes.latest + @report.history).sort_by(&:created_at)
@report_notes = (@report.notes.latest + @report.history + @report.target_account.targeted_account_warnings.latest.custom).sort_by(&:created_at)
@form = Form::StatusBatch.new
end

def update
def assign_to_self
authorize @report, :update?
process_report

if @report.action_taken?
redirect_to admin_reports_path, notice: I18n.t('admin.reports.resolved_msg')
else
redirect_to admin_report_path(@report)
end
@report.update!(assigned_account_id: current_account.id)
log_action :assigned_to_self, @report
redirect_to admin_report_path(@report)
end

private

def process_report
case params[:outcome].to_s
when 'assign_to_self'
@report.update!(assigned_account_id: current_account.id)
log_action :assigned_to_self, @report
when 'unassign'
@report.update!(assigned_account_id: nil)
log_action :unassigned, @report
when 'reopen'
@report.unresolve!
log_action :reopen, @report
when 'resolve'
@report.resolve!(current_account)
log_action :resolve, @report
when 'disable'
@report.resolve!(current_account)
@report.target_account.user.disable!

log_action :resolve, @report
log_action :disable, @report.target_account.user

resolve_all_target_account_reports
when 'silence'
@report.resolve!(current_account)
@report.target_account.update!(silenced: true)

log_action :resolve, @report
log_action :silence, @report.target_account

resolve_all_target_account_reports
else
raise ActiveRecord::RecordNotFound
end

@report.reload
def unassign
authorize @report, :update?
@report.update!(assigned_account_id: nil)
log_action :unassigned, @report
redirect_to admin_report_path(@report)
end

def resolve_all_target_account_reports
unresolved_reports_for_target_account.update_all(action_taken: true, action_taken_by_account_id: current_account.id)
def reopen
authorize @report, :update?
@report.unresolve!
log_action :reopen, @report
redirect_to admin_report_path(@report)
end

def unresolved_reports_for_target_account
Report.where(
target_account: @report.target_account
).unresolved
def resolve
authorize @report, :update?
@report.resolve!(current_account)
log_action :resolve, @report
redirect_to admin_reports_path, notice: I18n.t('admin.reports.resolved_msg')
end

private

def filtered_reports
ReportFilter.new(filter_params).results.order(id: :desc).includes(
:account,
:target_account
)
ReportFilter.new(filter_params).results.order(id: :desc).includes(:account, :target_account)
end

def filter_params

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

@@ -29,6 +29,7 @@ module Admin
preview_sensitive_media
custom_css
profile_directory
hide_followers_count
).freeze

BOOLEAN_SETTINGS = %w(
@@ -41,6 +42,7 @@ module Admin
show_known_fediverse_at_about_page
preview_sensitive_media
profile_directory
hide_followers_count
).freeze

UPLOAD_SETTINGS = %w(

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

@@ -1,27 +0,0 @@
# frozen_string_literal: true

module Admin
class SilencesController < BaseController
before_action :set_account

def create
authorize @account, :silence?
@account.update!(silenced: true)
log_action :silence, @account
redirect_to admin_accounts_path
end

def destroy
authorize @account, :unsilence?
@account.update!(silenced: false)
log_action :unsilence, @account
redirect_to admin_accounts_path
end

private

def set_account
@account = Account.find(params[:account_id])
end
end
end

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

@@ -1,60 +0,0 @@
# frozen_string_literal: true

module Admin
class SuspensionsController < BaseController
before_action :set_account

def new
@suspension = Form::AdminSuspensionConfirmation.new(report_id: params[:report_id])
end

def create
authorize @account, :suspend?

@suspension = Form::AdminSuspensionConfirmation.new(suspension_params)

if suspension_params[:acct] == @account.acct
resolve_report! if suspension_params[:report_id].present?
perform_suspend!
mark_reports_resolved!
redirect_to admin_accounts_path
else
flash.now[:alert] = I18n.t('admin.suspensions.bad_acct_msg')
render :new
end
end

def destroy
authorize @account, :unsuspend?
@account.unsuspend!
log_action :unsuspend, @account
redirect_to admin_accounts_path
end

private

def set_account
@account = Account.find(params[:account_id])
end

def suspension_params
params.require(:form_admin_suspension_confirmation).permit(:acct, :report_id)
end

def resolve_report!
report = Report.find(suspension_params[:report_id])
report.resolve!(current_account)
log_action :resolve, report
end

def perform_suspend!
@account.suspend!
Admin::SuspensionWorker.perform_async(@account.id)
log_action :suspend, @account
end

def mark_reports_resolved!
Report.where(target_account: @account).unresolved.update_all(action_taken: true, action_taken_by_account_id: current_account.id)
end
end
end

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

@@ -0,0 +1,58 @@
# frozen_string_literal: true

module Admin
class WarningPresetsController < BaseController
before_action :set_warning_preset, except: [:index, :create]

def index
authorize :account_warning_preset, :index?

@warning_presets = AccountWarningPreset.all
@warning_preset = AccountWarningPreset.new
end

def create
authorize :account_warning_preset, :create?

@warning_preset = AccountWarningPreset.new(warning_preset_params)

if @warning_preset.save
redirect_to admin_warning_presets_path
else
@warning_presets = AccountWarningPreset.all
render :index
end
end

def edit
authorize @warning_preset, :update?
end

def update
authorize @warning_preset, :update?

if @warning_preset.update(warning_preset_params)
redirect_to admin_warning_presets_path
else
render :edit
end
end

def destroy
authorize @warning_preset, :destroy?

@warning_preset.destroy!
redirect_to admin_warning_presets_path
end

private

def set_warning_preset
@warning_preset = AccountWarningPreset.find(params[:id])
end

def warning_preset_params
params.require(:account_warning_preset).permit(:text)
end
end
end

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

@@ -1,7 +1,7 @@
# frozen_string_literal: true

class Api::V1::Accounts::StatusesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
before_action :set_account
after_action :insert_pagination_headers


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

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

if oembed
render json: oembed

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

@@ -37,7 +37,7 @@ class DirectoriesController < ApplicationController
end

def set_accounts
@accounts = Account.discoverable.page(params[:page]).per(30).tap do |query|
@accounts = Account.discoverable.page(params[:page]).per(40).tap do |query|
query.merge!(Account.tagged_with(@tag.id)) if @tag
end
end

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

@@ -36,22 +36,22 @@ class FollowerAccountsController < ApplicationController
end

def collection_presenter
options = { type: :ordered }
options[:size] = @account.followers_count unless Setting.hide_followers_count || @account.user&.setting_hide_followers_count
if params[:page].present?
ActivityPub::CollectionPresenter.new(
id: account_followers_url(@account, page: params.fetch(:page, 1)),
type: :ordered,
size: @account.followers_count,
items: follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) },
part_of: account_followers_url(@account),
next: page_url(follows.next_page),
prev: page_url(follows.prev_page)
prev: page_url(follows.prev_page),
**options
)
else
ActivityPub::CollectionPresenter.new(
id: account_followers_url(@account),
type: :ordered,
size: @account.followers_count,
first: page_url(1)
first: page_url(1),
**options
)
end
end

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

@@ -43,6 +43,7 @@ class Settings::PreferencesController < Settings::BaseController
:setting_system_font_ui,
:setting_noindex,
:setting_hide_network,
:setting_hide_followers_count,
:setting_aggregate_reblogs,
notification_emails: %i(follow follow_request reblog favourite mention digest report),
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
link_to record.domain, "https://#{record.domain}"
when 'Status'
link_to record.account.acct, TagManager.instance.url_for(record)
when 'AccountWarning'
link_to record.target_account.acct, admin_account_path(record.target_account_id)
end
end

@@ -34,6 +36,7 @@ module Admin::ActionLogsHelper
link_to attributes['domain'], "https://#{attributes['domain']}"
when 'Status'
tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count'))

if tmp_status.account
link_to tmp_status.account&.acct || "##{tmp_status.account_id}", admin_account_path(tmp_status.account_id)
else
@@ -81,6 +84,8 @@ module Admin::ActionLogsHelper
'envelope'
when 'Status'
'pencil'
when 'AccountWarning'
'warning'
end
end

@@ -104,6 +109,6 @@ module Admin::ActionLogsHelper
private

def opposite_verbs?(log)
%w(DomainBlock EmailDomainBlock).include?(log.target_type)
%w(DomainBlock EmailDomainBlock AccountWarning).include?(log.target_type)
end
end

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

@@ -1,4 +0,0 @@
# frozen_string_literal: true

module MailerHelper
end

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

@@ -60,8 +60,12 @@ module StreamEntriesHelper
end
end

def hide_followers_count?(account)
Setting.hide_followers_count || account.user&.setting_hide_followers_count
end

def account_description(account)
prepend_str = [
prepend_stats = [
[
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
I18n.t('accounts.posts', count: account.statuses_count),
@@ -71,14 +75,16 @@ module StreamEntriesHelper
number_to_human(account.following_count, strip_insignificant_zeros: true),
I18n.t('accounts.following', count: account.following_count),
].join(' '),
]

[
unless hide_followers_count?(account)
prepend_stats << [
number_to_human(account.followers_count, strip_insignificant_zeros: true),
I18n.t('accounts.followers', count: account.followers_count),
].join(' '),
].join(', ')
].join(' ')
end

[prepend_str, account.note].join(' · ')
[prepend_stats.join(', '), account.note].join(' · ')
end

def media_summary(status)

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

@@ -11,7 +11,7 @@ import { getLocale } from 'mastodon/locales';

const { messages } = getLocale();

export function connectTimelineStream (timelineId, path, pollingRefresh = null) {
export function connectTimelineStream (timelineId, path, pollingRefresh = null, accept = null) {

return connectStream (path, pollingRefresh, (dispatch, getState) => {
const locale = getState().getIn(['meta', 'locale']);
@@ -23,7 +23,7 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null)
onReceive (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline(timelineId, JSON.parse(data.payload)));
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), accept));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
@@ -47,6 +47,6 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
export const connectPublicStream = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`);
export const connectHashtagStream = tag => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
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';

export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
export const TIMELINE_CLEAR = 'TIMELINE_CLEAR';

export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
@@ -12,8 +13,12 @@ export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';

export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';

export function updateTimeline(timeline, status) {
export function updateTimeline(timeline, status, accept) {
return (dispatch, getState) => {
if (typeof accept === 'function' && !accept(status)) {
return;
}

dispatch({
type: TIMELINE_UPDATE,
timeline,
@@ -38,8 +43,20 @@ export function deleteFromTimelines(id) {
};
};

export function clearTimeline(timeline) {
return (dispatch) => {
dispatch({ type: TIMELINE_CLEAR, timeline });
};
};

const noOp = () => {};

const parseTags = (tags = {}, mode) => {
return (tags[mode] || []).map((tag) => {
return tag.value;
});
};

export function expandTimeline(timelineId, path, params = {}, done = noOp) {
return (dispatch, getState) => {
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
@@ -76,9 +93,17 @@ export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => ex
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true });
export const expandHashtagTimeline = (hashtag, { maxId } = {}, done = noOp) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId }, done);
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);

export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => {
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
max_id: maxId,
any: parseTags(tags, 'any'),
all: parseTags(tags, 'all'),
none: parseTags(tags, 'none'),
}, done);
};

export function expandTimelineRequest(timeline, isLoadingMore) {
return {
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 {
animatingNCD: false,
};

historyBack = () => {
// if history is exhausted, or we would leave mastodon, just go to root.
if (window.history.state) {
this.context.router.history.goBack();
} else {
this.context.router.history.push('/');
}
}

handleToggleClick = (e) => {
e.stopPropagation();
this.setState({ collapsed: !this.state.collapsed, animating: true });
@@ -65,12 +74,7 @@ export default class ColumnHeader extends React.PureComponent {
}

handleBackClick = () => {
// if history is exhausted, or we would leave mastodon, just go to root.
if (window.history.state) {
this.context.router.history.goBack();
} else {
this.context.router.history.push('/');
}
this.historyBack();
}

handleTransitionEnd = () => {
@@ -81,13 +85,20 @@ export default class ColumnHeader extends React.PureComponent {
this.setState({ animatingNCD: false });
}

handlePin = () => {
if (!this.props.pinned) {
this.historyBack();
}
this.props.onPin();
}

onEnterCleaningMode = () => {
this.setState({ animatingNCD: true });
this.props.onEnterCleaningMode(!this.props.notifCleaningActive);
}

render () {
const { intl, icon, active, children, pinned, onPin, multiColumn, extraButton, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive } = this.props;
const { intl, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive } = this.props;
const { collapsed, animating, animatingNCD } = this.state;

let title = this.props.title;
@@ -132,7 +143,7 @@ export default class ColumnHeader extends React.PureComponent {
}

if (multiColumn && pinned) {
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>;
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>;

moveButtons = (
<div key='move-buttons' className='column-header__setting-arrows'>
@@ -141,7 +152,7 @@ export default class ColumnHeader extends React.PureComponent {
</div>
);
} else if (multiColumn) {
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>;
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>;
}

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 {
} else if (!nextProps.children) {
this.setState({ revealed: false });
}
if (!nextProps.children && !!this.props.children) {
this.activeElement.focus();
this.activeElement = null;
}
}

componentDidUpdate (prevProps) {
if (!this.props.children && !!prevProps.children) {
this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
this.activeElement.focus();
this.activeElement = null;
this.handleModalClose();
}
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 {
const { scrollTop, scrollHeight, clientHeight } = this.node;
const offset = scrollHeight - scrollTop - clientHeight;

if (400 > offset && this.props.onLoadMore && !this.props.isLoading) {
if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
this.props.onLoadMore();
}


+ 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 {

<NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}>
<FormattedMessage id='account.followers' defaultMessage='Followers' />
<strong><FormattedNumber value={account.get('followers_count')} /></strong>
<strong>{ account.get('followers_count') < 0 ? '-' : <FormattedNumber value={account.get('followers_count')} /> }</strong>
</NavLink>
</div>
</div>

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

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

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

@@ -23,7 +23,7 @@ import DrawerResults from './results';
import DrawerSearch from './search';

// Utils.
import { me } from 'flavours/glitch/util/initial_state';
import { me, mascot } from 'flavours/glitch/util/initial_state';
import { wrap } from 'flavours/glitch/util/redux_helpers';

// Messages.
@@ -121,10 +121,17 @@ class Drawer extends React.Component {
submitted={submitted}
value={searchValue}
/> }
<div className='contents'>
{!isSearchPage && <DrawerAccount account={account} />}
{!isSearchPage && <Composer />}
{multiColumn && <button className='mastodon' onClick={onClickElefriend} />}
<div className='drawer__pager'>
{!isSearchPage && <div className='drawer__inner'>
<DrawerAccount account={account} />
<Composer />
{multiColumn && (
<div className='drawer__inner__mastodon'>
{mascot ? <img alt='' draggable='false' src={mascot} /> : <button className='mastodon' onClick={onClickElefriend} />}
</div>
)}
</div>}

{(multiColumn || isSearchPage) &&
<DrawerResults
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 {

<div className='getting-started__footer'>
<ul>
<li><a href='https://bridge.joinmastodon.org/' target='_blank'><FormattedMessage id='getting_started.find_friends' defaultMessage='Find friends from Twitter' /></a> · </li>
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
<li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this instance' /></a> · </li>
<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 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, FormattedMessage } from 'react-intl';
import Toggle from 'react-toggle';
import AsyncSelect from 'react-select/lib/Async';

@injectIntl
export default class ColumnSettings extends React.PureComponent {

static propTypes = {
settings: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
onLoad: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};

state = {
open: this.hasTags(),
};

hasTags () {
return ['all', 'any', 'none'].map(mode => this.tags(mode).length > 0).includes(true);
}

tags (mode) {
let tags = this.props.settings.getIn(['tags', mode]) || [];
if (tags.toJSON) {
return tags.toJSON();
} else {
return tags;
}
};

onSelect = (mode) => {
return (value) => {
this.props.onChange(['tags', mode], value);
};
};

onToggle = () => {
if (this.state.open && this.hasTags()) {
this.props.onChange('tags', {});
}
this.setState({ open: !this.state.open });
};

modeSelect (mode) {
return (
<div className='column-settings__section'>
{this.modeLabel(mode)}
<AsyncSelect
isMulti
autoFocus
value={this.tags(mode)}
settings={this.props.settings}
settingPath={['tags', mode]}
onChange={this.onSelect(mode)}
loadOptions={this.props.onLoad}
classNamePrefix='column-settings__hashtag-select'
name='tags'
/>
</div>
);
}

modeLabel (mode) {
switch(mode) {
case 'any': return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />;
case 'all': return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />;
case 'none': return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />;
}
return '';
};

render () {
return (
<div>
<div className='column-settings__row'>
<div className='setting-toggle'>
<Toggle
id='hashtag.column_settings.tag_toggle'
onChange={this.onToggle}
checked={this.state.open}
/>
<span className='setting-toggle__label'>
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
</span>
</div>
</div>
{this.state.open &&
<div className='column-settings__hashtags'>
{this.modeSelect('any')}
{this.modeSelect('all')}
{this.modeSelect('none')}
</div>
}
</div>
);
}

}

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

@@ -0,0 +1,31 @@
import { connect } from 'react-redux';
import ColumnSettings from '../components/column_settings';
import { changeColumnParams } from 'flavours/glitch/actions/columns';
import api from 'flavours/glitch/util/api';

const mapStateToProps = (state, { columnId }) => {
const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === columnId);

if (!(columnId && index >= 0)) {
return {};
}

return { settings: columns.get(index).get('params') };
};

const mapDispatchToProps = (dispatch, { columnId }) => ({
onChange (key, value) {
dispatch(changeColumnParams(columnId, key, value));
},

onLoad (value) {
return api().get('/api/v2/search', { params: { q: value } }).then(response => {
return (response.data.hashtags || []).map((tag) => {
return { value: tag.name, label: `#${tag.name}` };
});
});
},
});

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';
import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
import Column from 'flavours/glitch/components/column';
import ColumnHeader from 'flavours/glitch/components/column_header';
import { expandHashtagTimeline } from 'flavours/glitch/actions/timelines';
import ColumnSettingsContainer from './containers/column_settings_container';
import { expandHashtagTimeline, clearTimeline } from 'flavours/glitch/actions/timelines';
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
import { FormattedMessage } from 'react-intl';
import { connectHashtagStream } from 'flavours/glitch/actions/streaming';
import { isEqual } from 'lodash';

const mapStateToProps = (state, props) => ({
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0,
@@ -16,6 +18,8 @@ const mapStateToProps = (state, props) => ({
@connect(mapStateToProps)
export default class HashtagTimeline extends React.PureComponent {

disconnects = [];

static propTypes = {
params: PropTypes.object.isRequired,
columnId: PropTypes.string,
@@ -34,6 +38,30 @@ export default class HashtagTimeline extends React.PureComponent {
}
}

title = () => {
let title = [this.props.params.id];
if (this.additionalFor('any')) {
title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.any' values={{ additional: this.additionalFor('any') }} defaultMessage='or {additional}' />);
}
if (this.additionalFor('all')) {
title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.all' values={{ additional: this.additionalFor('all') }} defaultMessage='and {additional}' />);
}
if (this.additionalFor('none')) {
title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.none' values={{ additional: this.additionalFor('none') }} defaultMessage='without {additional}' />);
}
return title;
}

additionalFor = (mode) => {
const { tags } = this.props.params;

if (tags && (tags[mode] || []).length > 0) {
return tags[mode].map(tag => tag.value).join('/');
} else {
return '';
}
}

handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
@@ -43,30 +71,40 @@ export default class HashtagTimeline extends React.PureComponent {
this.column.scrollTop();
}

_subscribe (dispatch, id) {
this.disconnect = dispatch(connectHashtagStream(id));
_subscribe (dispatch, id, tags = {}) {
let any = (tags.any || []).map(tag => tag.value);
let all = (tags.all || []).map(tag => tag.value);
let none = (tags.none || []).map(tag => tag.value);

[id, ...any].map((tag) => {
this.disconnects.push(dispatch(connectHashtagStream(id, tag, (status) => {
let tags = status.tags.map(tag => tag.name);
return all.filter(tag => tags.includes(tag)).length === all.length &&
none.filter(tag => tags.includes(tag)).length === 0;
})));
});
}

_unsubscribe () {
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
this.disconnects.map(disconnect => disconnect());
this.disconnects = [];
}

componentDidMount () {
const { dispatch } = this.props;
const { id } = this.props.params;
const { id, tags } = this.props.params;

dispatch(expandHashtagTimeline(id));
this._subscribe(dispatch, id);
dispatch(expandHashtagTimeline(id, { tags }));
}

componentWillReceiveProps (nextProps) {
if (nextProps.params.id !== this.props.params.id) {
this.props.dispatch(expandHashtagTimeline(nextProps.params.id));
const { dispatch, params } = this.props;
const { id, tags } = nextProps.params;
if (id !== params.id || !isEqual(tags, params.tags)) {
this._unsubscribe();
this._subscribe(this.props.dispatch, nextProps.params.id);
this._subscribe(dispatch, id, tags);
this.props.dispatch(clearTimeline(`hashtag:${id}`));
this.props.dispatch(expandHashtagTimeline(id, { tags }));
}
}

@@ -79,7 +117,8 @@ export default class HashtagTimeline extends React.PureComponent {
}

handleLoadMore = maxId => {
this.props.dispatch(expandHashtagTimeline(this.props.params.id, { maxId }));
const { id, tags } = this.props.params;
this.props.dispatch(expandHashtagTimeline(id, { maxId, tags }));
}

render () {
@@ -92,14 +131,16 @@ export default class HashtagTimeline extends React.PureComponent {
<ColumnHeader
icon='hashtag'
active={hasUnread}
title={id}
title={this.title()}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
showBackButton
/>
>
{columnId && <ColumnSettingsContainer columnId={columnId} />}
</ColumnHeader>

<StatusListContainer
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 {
const { dispatch, hashtag } = this.props;

dispatch(expandHashtagTimeline(hashtag));
this.disconnect = dispatch(connectHashtagStream(hashtag));
this.disconnect = dispatch(connectHashtagStream(hashtag, hashtag));
}

componentWillUnmount () {

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

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

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

@@ -1,245 +0,0 @@
import {
NOTIFICATIONS_MOUNT,
NOTIFICATIONS_UNMOUNT,
NOTIFICATIONS_SET_VISIBILITY,
NOTIFICATIONS_UPDATE,
NOTIFICATIONS_EXPAND_SUCCESS,
NOTIFICATIONS_EXPAND_REQUEST,
NOTIFICATIONS_EXPAND_FAIL,
NOTIFICATIONS_CLEAR,
NOTIFICATIONS_SCROLL_TOP,
NOTIFICATIONS_DELETE_MARKED_REQUEST,
NOTIFICATIONS_DELETE_MARKED_SUCCESS,
NOTIFICATION_MARK_FOR_DELETE,
NOTIFICATIONS_DELETE_MARKED_FAIL,
NOTIFICATIONS_ENTER_CLEARING_MODE,
NOTIFICATIONS_MARK_ALL_FOR_DELETE,
} from 'flavours/glitch/actions/notifications';
import {
ACCOUNT_BLOCK_SUCCESS,
ACCOUNT_MUTE_SUCCESS,
} from 'flavours/glitch/actions/accounts';
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from 'flavours/glitch/actions/timelines';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from 'flavours/glitch/util/compare_id';

const initialState = ImmutableMap({
items: ImmutableList(),
hasMore: true,
top: true,
mounted: 0,
unread: 0,
lastReadId: '0',
isLoading: false,
cleaningMode: false,
isTabVisible: true,
// notification removal mark of new notifs loaded whilst cleaningMode is true.
markNewForDelete: false,
});

const notificationToMap = (state, notification) => ImmutableMap({
id: notification.id,
type: notification.type,
account: notification.account.id,
markedForDelete: state.get('markNewForDelete'),
status: notification.status ? notification.status.id : null,
});

const normalizeNotification = (state, notification) => {
const top = !shouldCountUnreadNotifications(state);

if (top) {
state = state.set('lastReadId', notification.id);
} else {
state = state.update('unread', unread => unread + 1);
}

return state.update('items', list => {
if (top && list.size > 40) {
list = list.take(20);
}

return list.unshift(notificationToMap(state, notification));
});
};

const expandNormalizedNotifications = (state, notifications, next) => {
const top = !(shouldCountUnreadNotifications(state));
const lastReadId = state.get('lastReadId');
let items = ImmutableList();

notifications.forEach((n, i) => {
items = items.set(i, notificationToMap(state, n));
});

return state.withMutations(mutable => {
if (!items.isEmpty()) {
mutable.update('items', list => {
const lastIndex = 1 + list.findLastIndex(
item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id'))
);

const firstIndex = 1 + list.take(lastIndex).findLastIndex(
item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0
);

return list.take(firstIndex).concat(items, list.skip(lastIndex));
});
}

if (top) {
if (!items.isEmpty()) {
mutable.update('lastReadId', id => compareId(id, items.first().get('id')) > 0 ? id : items.first().get('id'));
}
} else {
mutable.update('unread', unread => unread + items.filter(item => compareId(item.get('id'), lastReadId) > 0).size);
}

if (!next) {
mutable.set('hasMore', false);
}

mutable.set('isLoading', false);
});
};

const filterNotifications = (state, relationship) => {
return state.update('items', list => list.filterNot(item => item !== null && item.get('account') === relationship.id));
};

const clearUnread = (state) => {
state = state.set('unread', 0);
const lastNotification = state.get('items').find(item => item !== null);
return state.set('lastReadId', lastNotification ? lastNotification.get('id') : '0');
}

const updateTop = (state, top) => {
state = state.set('top', top);

if (!shouldCountUnreadNotifications(state)) {
state = clearUnread(state);
}

return state.set('top', top);
};

const deleteByStatus = (state, statusId) => {
const top = !(shouldCountUnreadNotifications(state));
if (!top) {
const lastReadId = state.get('lastReadId');
const deletedUnread = state.get('items').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0);
state = state.update('unread', unread => unread - deletedUnread.size);
}
return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId));
};

const markForDelete = (state, notificationId, yes) => {
return state.update('items', list => list.map(item => {
if(item.get('id') === notificationId) {
return item.set('markedForDelete', yes);
} else {
return item;
}
}));
};

const markAllForDelete = (state, yes) => {
return state.update('items', list => list.map(item => {
if(yes !== null) {
return item.set('markedForDelete', yes);
} else {
return item.set('markedForDelete', !item.get('markedForDelete'));
}
}));
};

const unmarkAllForDelete = (state) => {
return state.update('items', list => list.map(item => item.set('markedForDelete', false)));
};

const deleteMarkedNotifs = (state) => {
return state.update('items', list => list.filterNot(item => item.get('markedForDelete')));
};

const updateMounted = (state) => {
state = state.update('mounted', count => count + 1);
if (!shouldCountUnreadNotifications(state)) {
state = clearUnread(state);
}
return state;
};

const updateVisibility = (state, visibility) => {
state = state.set('isTabVisible', visibility);
if (!shouldCountUnreadNotifications(state)) {
state = clearUnread(state);
}
return state;
};

const shouldCountUnreadNotifications = (state) => {
return !(state.get('isTabVisible') && state.get('top') && state.get('mounted') > 0);
};

export default function notifications(state = initialState, action) {
let st;

switch(action.type) {
case NOTIFICATIONS_MOUNT:
return updateMounted(state);
case NOTIFICATIONS_UNMOUNT:
return state.update('mounted', count => count - 1);
case NOTIFICATIONS_SET_VISIBILITY:
return updateVisibility(state, action.visibility);
case NOTIFICATIONS_EXPAND_REQUEST:
case NOTIFICATIONS_DELETE_MARKED_REQUEST:
return state.set('isLoading', true);
case NOTIFICATIONS_DELETE_MARKED_FAIL:
case NOTIFICATIONS_EXPAND_FAIL:
return state.set('isLoading', false);
case NOTIFICATIONS_SCROLL_TOP:
return updateTop(state, action.top);
case NOTIFICATIONS_UPDATE:
return normalizeNotification(state, action.notification);
case NOTIFICATIONS_EXPAND_SUCCESS:
return expandNormalizedNotifications(state, action.notifications, action.next);
case ACCOUNT_BLOCK_SUCCESS:
case ACCOUNT_MUTE_SUCCESS:
return filterNotifications(state, action.relationship);
case NOTIFICATIONS_CLEAR:
return state.set('items', ImmutableList()).set('hasMore', false);
case TIMELINE_DELETE:
return deleteByStatus(state, action.id);
case TIMELINE_DISCONNECT:
return action.timeline === 'home' ?
state.update('items', items => items.first() ? items.unshift(null) : items) :
state;

case NOTIFICATION_MARK_FOR_DELETE:
return markForDelete(state, action.id, action.yes);

case NOTIFICATIONS_DELETE_MARKED_SUCCESS:
return deleteMarkedNotifs(state).set('isLoading', false);

case NOTIFICATIONS_ENTER_CLEARING_MODE:
st = state.set('cleaningMode', action.yes);
if (!action.yes) {
return unmarkAllForDelete(st).set('markNewForDelete', false);
} else {
return st;
}

case NOTIFICATIONS_MARK_ALL_FOR_DELETE:
st = state;
if (action.yes === null) {
// Toggle - this is a bit confusing, as it toggles the all-none mode
//st = st.set('markNewForDelete', !st.get('markNewForDelete'));
} else {
st = st.set('markNewForDelete', action.yes);
}
return markAllForDelete(st, action.yes);

default:
return state;
}
};

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

@@ -1,6 +1,7 @@
import {
TIMELINE_UPDATE,
TIMELINE_DELETE,
TIMELINE_CLEAR,
TIMELINE_EXPAND_SUCCESS,
TIMELINE_EXPAND_REQUEST,
TIMELINE_EXPAND_FAIL,
@@ -81,6 +82,10 @@ const deleteStatus = (state, id, accountId, references) => {
return state;
};

const clearTimeline = (state, timeline) => {
return state.updateIn([timeline, 'items'], list => list.clear());
};

const filterTimelines = (state, relationship, statuses) => {
let references;

@@ -121,6 +126,8 @@ export default function timelines(state = initialState, action) {
return updateTimeline(state, action.timeline, fromJS(action.status));
case TIMELINE_DELETE:
return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
case TIMELINE_CLEAR:
return clearTimeline(state, action.timeline);
case ACCOUNT_BLOCK_SUCCESS:
case ACCOUNT_MUTE_SUCCESS:
return filterTimelines(state, action.relationship, action.statuses);

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

@@ -51,3 +51,34 @@
border-radius: 0px;
}
}

@mixin search-input() {
outline: 0;
box-sizing: border-box;
width: 100%;
border: none;
box-shadow: none;
font-family: inherit;
background: $ui-base-color;
color: $darker-text-color;
font-size: 14px;
margin: 0;

&::-moz-focus-inner {
border: 0;
}

&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}

&:focus {
background: lighten($ui-base-color, 4%);
}

@media screen and (max-width: 600px) {
font-size: 16px;
}
}

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

@@ -553,6 +553,10 @@ a.name-tag,
border-left-color: lighten($error-red, 12%);
}

&.warning {
border-left-color: $gold-star;
}

&__bubble {
padding: 16px;
padding-left: 14px;

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

@@ -339,6 +339,26 @@
display: block;
font-weight: 500;
margin-bottom: 10px;

.column-settings__hashtag-select {
&__control {
@include search-input();
}

&__multi-value {
background: lighten($ui-base-color, 8%);
}

&__multi-value__label,
&__input {
color: $darker-text-color;
}

&__indicator-separator,
&__dropdown-indicator {
display: none;
}
}
}

.column-settings__row {
@@ -451,10 +471,12 @@
border-bottom: 1px solid lighten($ui-base-color, 8%);
cursor: default;
display: flex;
flex-shrink: 0;

button {
background: darken($ui-base-color, 4%);
border: 0;
margin: 0;
}

button,

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

@@ -1,9 +1,10 @@
.drawer {
width: 300px;
box-sizing: border-box;
display: flex;
flex-direction: column;
box-sizing: border-box;
overflow-y: hidden;
padding: 10px 5px;
width: 300px;
flex: none;

&:first-child {
@@ -38,41 +39,6 @@
}

.react-swipeable-view-container & { height: 100% }

& > .contents {
display: flex;
position: relative;
flex-direction: column;
padding: 0;
flex-grow: 1;
background: lighten($ui-base-color, 13%);
overflow-x: hidden;
overflow-y: auto;

& > .mastodon {
flex: 1;
border: none;
cursor: inherit;
}
}

@for $i from 0 through 3 {
&.mbstobon-#{$i} > .contents {
@if $i == 3 {
background: url('~flavours/glitch/images/wave-drawer.png') no-repeat bottom / 100% auto, lighten($ui-base-color, 13%);
} @else {
background: url('~flavours/glitch/images/wave-drawer-glitched.png') no-repeat bottom / 100% auto, lighten($ui-base-color, 13%);
}

& > .mastodon {
background: url("~flavours/glitch/images/mbstobon-ui-#{$i}.png") no-repeat left bottom / contain;

@if $i != 3 {
filter: contrast(50%) brightness(50%);
}
}
}
}
}

.drawer--header {
@@ -342,6 +308,31 @@
}
}

.drawer__inner__mastodon {
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;
flex: 1;
min-height: 47px;

> img {
display: block;
object-fit: contain;
object-position: bottom left;
width: 100%;
height: 100%;
pointer-events: none;
user-drag: none;
user-select: none;
}

> .mastodon {
display: block;
width: 100%;
height: 100%;
border: none;
cursor: inherit;
}
}

.pseudo-drawer {
background: lighten($ui-base-color, 13%);
font-size: 13px;
@@ -357,3 +348,21 @@
height: 100%;
background: rgba($base-overlay-background, 0.5);
}

@for $i from 0 through 3 {
.mbstobon-#{$i} .drawer__inner__mastodon {
@if $i == 3 {
background: url('~flavours/glitch/images/wave-drawer.png') no-repeat bottom / 100% auto, lighten($ui-base-color, 13%);
} @else {
background: url('~flavours/glitch/images/wave-drawer-glitched.png') no-repeat bottom / 100% auto, lighten($ui-base-color, 13%);
}

& > .mastodon {
background: url("~flavours/glitch/images/mbstobon-ui-#{$i}.png") no-repeat left bottom / contain;

@if $i != 3 {
filter: contrast(50%) brightness(50%);
}
}
}
}

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

@@ -3,36 +3,10 @@
}

.search__input {
outline: 0;
box-sizing: border-box;
display: block;
width: 100%;
border: none;
padding: 10px;
padding-right: 30px;
font-family: inherit;
background: $ui-base-color;
color: $darker-text-color;
font-size: 14px;
margin: 0;

&::-moz-focus-inner {
border: 0;
}

&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}

&:focus {
background: lighten($ui-base-color, 4%);
}

@media screen and (max-width: 600px) {
font-size: 16px;
}
@include search-input();
}

.search__icon {

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

@@ -58,7 +58,7 @@
background: $ui-base-color;
}

.drawer > .contents {
.drawer__inner__mastodon {
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;

.mastodon {

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

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

export default initialState;

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

@@ -0,0 +1,4 @@
<svg fill="#FFFFFF" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M14.4 6L14 4H5v17h2v-7h5.6l.4 2h7V6z"/>
</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 {
} else if (!nextProps.children) {
this.setState({ revealed: false });
}
if (!nextProps.children && !!this.props.children) {
this.activeElement.focus();
this.activeElement = null;
}
}

componentDidUpdate (prevProps) {
if (!this.props.children && !!prevProps.children) {
this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
this.activeElement.focus();
this.activeElement = null;
}
if (this.props.children) {
requestAnimationFrame(() => {

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

@@ -49,7 +49,7 @@ export default class ScrollableList extends PureComponent {
const { scrollTop, scrollHeight, clientHeight } = this.node;
const offset = scrollHeight - scrollTop - clientHeight;

if (400 > offset && this.props.onLoadMore && !this.props.isLoading) {
if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
this.props.onLoadMore();
}


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

@@ -136,7 +136,7 @@ class GettingStarted extends ImmutablePureComponent {

<div className='getting-started__footer'>
<ul>
<li><a href='https://bridge.joinmastodon.org/' target='_blank'><FormattedMessage id='getting_started.find_friends' defaultMessage='Find friends from Twitter' /></a> · </li>
<li><a href='/explore' target='_blank'><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></a> · </li>
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
{multiColumn && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
<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 {
dispatch(expandPublicTimeline({ maxId, onlyMedia }));
}

handleSettingChanged = (key, checked) => {
const { columnId } = this.props;
if (!columnId && key[0] === 'other' && key[1] === 'onlyMedia') {
this.context.router.history.replace(`/timelines/public${checked ? '/media' : ''}`);
}
}

render () {
const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia } = this.props;
const pinned = !!columnId;
@@ -123,7 +116,7 @@ class PublicTimeline extends React.PureComponent {
pinned={pinned}
multiColumn={multiColumn}
>
<ColumnSettingsContainer onChange={this.handleSettingChanged} columnId={columnId} />
<ColumnSettingsContainer columnId={columnId} />
</ColumnHeader>

<StatusListContainer

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

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

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

@@ -150,9 +150,7 @@ class SwitchingColumnsArea extends React.PureComponent {
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
<WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/public/media' component={PublicTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll, onlyMedia: true }} />
<WrappedRoute path='/timelines/public/local' exact component={CommunityTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/public/local/media' component={CommunityTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll, onlyMedia: true }} />
<WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
<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 @@
"follow_request.authorize": "ترخيص",
"follow_request.reject": "رفض",
"getting_started.developers": "المُطوِّرون",
"getting_started.directory": "Profile directory",
"getting_started.documentation": "Documentation",
"getting_started.find_friends": "البحث عن أصدقاء على تويتر",
"getting_started.heading": "إستعدّ للبدء",
"getting_started.invite": "دعوة أشخاص",
"getting_started.open_source_notice": "ماستدون برنامج مفتوح المصدر. يمكنك المساهمة، أو الإبلاغ عن تقارير الأخطاء، على جيت هب {github}.",
@@ -149,6 +149,23 @@
"home.column_settings.basic": "أساسية",
"home.column_settings.show_reblogs": "عرض الترقيات",
"home.column_settings.show_replies": "عرض الردود",
"introduction.federation.action": "Next",
"introduction.federation.federated.headline": "Federated",
"introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
"introduction.federation.home.headline": "Home",
"introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
"introduction.federation.local.headline": "Local",
"introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
"introduction.interactions.action": "Finish tutorial!",
"introduction.interactions.favourite.headline": "Favourite",
"introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
"introduction.interactions.reblog.headline": "Boost",
"introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
"introduction.interactions.reply.headline": "Reply",
"introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
"introduction.welcome.action": "Let's go!",
"introduction.welcome.headline": "First steps",
"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.",
"keyboard_shortcuts.back": "للعودة",
"keyboard_shortcuts.blocked": "لفتح قائمة المستخدمين المحظورين",
"keyboard_shortcuts.boost": "للترقية",
@@ -225,34 +242,21 @@
"notifications.clear_confirmation": "أمتأكد من أنك تود مسح جل الإخطارات الخاصة بك و المتلقاة إلى حد الآن ؟",
"notifications.column_settings.alert": "إشعارات سطح المكتب",
"notifications.column_settings.favourite": "المُفَضَّلة :",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show",
"notifications.column_settings.follow": "متابعُون جُدُد :",
"notifications.column_settings.mention": "الإشارات :",
"notifications.column_settings.push": "الإخطارات المدفوعة",
"notifications.column_settings.reblog": "الترقيّات:",
"notifications.column_settings.show": "إعرِضها في عمود",
"notifications.column_settings.sound": "أصدر صوتا",
"notifications.filter.all": "All",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
"notifications.filter.follows": "Follows",
"notifications.filter.mentions": "Mentions",
"notifications.group": "{count} إشعارات",
"onboarding.done": "تم",
"onboarding.next": "التالي",
"onboarding.page_five.public_timelines": "تُعرَض في الخيط الزمني المحلي المشاركات العامة المحررة من طرف جميع المسجلين في {domain}. أما في الخيط الزمني الموحد ، فإنه يتم عرض جميع المشاركات العامة المنشورة من طرف جميع الأشخاص المتابَعين من طرف أعضاء {domain}. هذه هي الخيوط الزمنية العامة، وهي طريقة رائعة للتعرف أشخاص جدد.",
"onboarding.page_four.home": "تعرض الصفحة الرئيسية منشورات جميع الأشخاص الذين تتابعهم.",
"onboarding.page_four.notifications": "فعندما يتفاعل شخص ما معك، عمود الإخطارات يخبرك.",
"onboarding.page_one.federation": "ماستدون شبكة من خوادم مستقلة متلاحمة تهدف إلى إنشاء أكبر شبكة اجتماعية موحدة. تسمى هذه السرفيرات بمثيلات خوادم.",
"onboarding.page_one.full_handle": "عنوانك الكامل",
"onboarding.page_one.handle_hint": "هذا هو ما يجب عليك توصيله لأصدقائك للبحث عنه.",
"onboarding.page_one.welcome": "مرحبا بك في ماستدون !",
"onboarding.page_six.admin": "مدير(ة) مثيل الخادم هذا {admin}.",
"onboarding.page_six.almost_done": "أنهيت تقريبا ...",
"onboarding.page_six.appetoot": "تمتع بالتبويق !",
"onboarding.page_six.apps_available": "هناك {apps} متوفرة لأنظمة آي أو إس و أندرويد و غيرها من المنصات و الأنظمة.",
"onboarding.page_six.github": "ماستدون برنامج مفتوح المصدر. يمكنك المساهمة، أو الإبلاغ عن تقارير الأخطاء، على GitHub {github}.",
"onboarding.page_six.guidelines": "المبادئ التوجيهية للمجتمع",
"onboarding.page_six.read_guidelines": "رجاءا، قم بالإطلاع على {guidelines} لـ {domain} !",
"onboarding.page_six.various_app": "تطبيقات الجوال",
"onboarding.page_three.profile": "يمكنك إدخال تعديلات على ملفك الشخصي عن طريق تغيير الصورة الرمزية و السيرة و إسمك المستعار. هناك، سوف تجد أيضا تفضيلات أخرى متاحة.",
"onboarding.page_three.search": "باستخدام شريط البحث يمكنك العثور على أشخاص و أصدقاء أو الإطلاع على أوسمة، كـ {illustration} و {introductions}. للبحث عن شخص غير مسجل في مثيل الخادم هذا، استخدم مُعرّفه الكامل.",
"onboarding.page_two.compose": "حرر مشاركاتك عبر عمود التحرير. يمكنك من خلاله تحميل الصور وتغيير إعدادات الخصوصية وإضافة تحذيرات عن المحتوى باستخدام الرموز أدناه.",
"onboarding.skip": "تخطي",
"privacy.change": "إضبط خصوصية المنشور",
"privacy.direct.long": "أنشر إلى المستخدمين المشار إليهم فقط",
"privacy.direct.short": "مباشر",

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

@@ -132,8 +132,8 @@
"follow_request.authorize": "Autorizar",
"follow_request.reject": "Refugar",
"getting_started.developers": "Desendolcadores",
"getting_started.directory": "Profile directory",
"getting_started.documentation": "Documentación",
"getting_started.find_friends": "Alcontrar collacios de Twitter",
"getting_started.heading": "Entamu",
"getting_started.invite": "Convidar xente",
"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 @@
"home.column_settings.basic": "Basic",
"home.column_settings.show_reblogs": "Amosar toots compartíos",
"home.column_settings.show_replies": "Amosar rempuestes",
"introduction.federation.action": "Next",
"introduction.federation.federated.headline": "Federated",
"introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
"introduction.federation.home.headline": "Home",
"introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
"introduction.federation.local.headline": "Local",
"introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
"introduction.interactions.action": "Finish tutorial!",
"introduction.interactions.favourite.headline": "Favourite",
"introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
"introduction.interactions.reblog.headline": "Boost",
"introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
"introduction.interactions.reply.headline": "Reply",
"introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
"introduction.welcome.action": "Let's go!",
"introduction.welcome.headline": "First steps",
"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.",
"keyboard_shortcuts.back": "pa dir p'atrás",
"keyboard_shortcuts.blocked": "p'abrir la llista d'usuarios bloquiaos",
"keyboard_shortcuts.boost": "pa compartir un toot",
@@ -225,34 +242,21 @@
"notifications.clear_confirmation": "¿De xuru que quies llimpiar dafechu tolos avisos?",
"notifications.column_settings.alert": "Avisos d'escritoriu",
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show",
"notifications.column_settings.follow": "Siguidores nuevos:",
"notifications.column_settings.mention": "Menciones:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.reblog": "Toots compartíos:",
"notifications.column_settings.show": "Amosar en columna",
"notifications.column_settings.sound": "Reproducir soníu",
"notifications.filter.all": "All",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
"notifications.filter.follows": "Follows",
"notifications.filter.mentions": "Mentions",
"notifications.group": "{count} avisos",
"onboarding.done": "Fecho",
"onboarding.next": "Siguiente",
"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.",
"onboarding.page_four.home": "La llinia temporal d'aniciu amuesa artículos de xente a la que sigues.",
"onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
"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.",
"onboarding.page_one.full_handle": "Your full handle",
"onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.",
"onboarding.page_one.welcome": "¡Afáyate en Mastodon!",
"onboarding.page_six.admin": "Your instance's admin is {admin}.",
"onboarding.page_six.almost_done": "Almost done...",
"onboarding.page_six.appetoot": "Bon Appetoot!",
"onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
"onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"onboarding.page_six.guidelines": "community guidelines",
"onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
"onboarding.page_six.various_app": "aplicaciones pa móviles",
"onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
"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.",
"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.",
"onboarding.skip": "Skip",
"privacy.change": "Adjust status privacy",
"privacy.direct.long": "Post to mentioned users only",
"privacy.direct.short": "Direct",

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

@@ -132,8 +132,8 @@
"follow_request.authorize": "Authorize",
"follow_request.reject": "Reject",
"getting_started.developers": "Developers",
"getting_started.directory": "Profile directory",
"getting_started.documentation": "Documentation",
"getting_started.find_friends": "Find friends from Twitter",
"getting_started.heading": "Първи стъпки",
"getting_started.invite": "Invite people",
"getting_started.open_source_notice": "Mastodon е софтуер с отворен код. Можеш да помогнеш или да докладваш за проблеми в Github: {github}.",
@@ -149,6 +149,23 @@
"home.column_settings.basic": "Basic",
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
"introduction.federation.action": "Next",
"introduction.federation.federated.headline": "Federated",
"introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
"introduction.federation.home.headline": "Home",
"introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
"introduction.federation.local.headline": "Local",
"introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",
"introduction.interactions.action": "Finish tutorial!",
"introduction.interactions.favourite.headline": "Favourite",
"introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.",
"introduction.interactions.reblog.headline": "Boost",
"introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.",
"introduction.interactions.reply.headline": "Reply",
"introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.",
"introduction.welcome.action": "Let's go!",
"introduction.welcome.headline": "First steps",
"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.",
"keyboard_shortcuts.back": "to navigate back",
"keyboard_shortcuts.blocked": "to open blocked users list",
"keyboard_shortcuts.boost": "to boost",
@@ -225,34 +242,21 @@
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
"notifications.column_settings.alert": "Десктоп известия",
"notifications.column_settings.favourite": "Предпочитани:",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show",
"notifications.column_settings.follow": "Нови последователи:",
"notifications.column_settings.mention": "Споменавания:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.reblog": "Споделяния:",
"notifications.column_settings.show": "Покажи в колона",
"notifications.column_settings.sound": "Play sound",
"notifications.filter.all": "All",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
"notifications.filter.follows": "Follows",
"notifications.filter.mentions": "Mentions",
"notifications.group": "{count} notifications",
"onboarding.done": "Done",
"onboarding.next": "Next",
"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.",
"onboarding.page_four.home": "The home timeline shows posts from people you follow.",
"onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
"onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_one.full_handle": "Your full handle",
"onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.",
"onboarding.page_one.welcome": "Welcome to Mastodon!",
"onboarding.page_six.admin": "Your instance's admin is {admin}.",
"onboarding.page_six.almost_done": "Almost done...",
"onboarding.page_six.appetoot": "Bon Appetoot!",
"onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
"onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"onboarding.page_six.guidelines": "community guidelines",
"onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
"onboarding.page_six.various_app": "mobile apps",
"onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
"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.",
"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.",
"onboarding.skip": "Skip",
"privacy.change": "Adjust status privacy",
"privacy.direct.long": "Post to mentioned users only",
"privacy.direct.short": "Direct",

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

@@ -132,8 +132,8 @@
"follow_request.authorize": "Autoritzar",
"follow_request.reject": "Rebutjar",
"getting_started.developers": "Desenvolupadors",
"getting_started.directory": "Profile directory",
"getting_started.documentation": "Documentació",
"getting_started.find_friends": "Troba amics de Twitter",
"getting_started.heading": "Començant",
"getting_started.invite": "Convida gent",
"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 @@
"home.column_settings.basic": "Bàsic",
"home.column_settings.show_reblogs": "Mostrar impulsos",
"home.column_settings.show_replies": "Mostrar respostes",
"introduction.federation.action": "Next",
"introduction.federation.federated.headline": "Federated",
"introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.",
"introduction.federation.home.headline": "Home",
"introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!",
"introduction.federation.local.headline": "Local",
"introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.",