diff options
Diffstat (limited to 'app')
150 files changed, 2772 insertions, 298 deletions
diff --git a/app/controllers/admin/account_moderation_notes_controller.rb b/app/controllers/admin/account_moderation_notes_controller.rb index 7f69a3363..7d5b9bf52 100644 --- a/app/controllers/admin/account_moderation_notes_controller.rb +++ b/app/controllers/admin/account_moderation_notes_controller.rb @@ -21,7 +21,7 @@ module Admin def destroy authorize @account_moderation_note, :destroy? - @account_moderation_note.destroy + @account_moderation_note.destroy! redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg') end diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 0829bc769..e9a512e70 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -32,18 +32,21 @@ module Admin def memorialize authorize @account, :memorialize? @account.memorialize! + log_action :memorialize, @account redirect_to admin_account_path(@account.id) end def enable authorize @account.user, :enable? @account.user.enable! + log_action :enable, @account.user redirect_to admin_account_path(@account.id) end def disable authorize @account.user, :disable? @account.user.disable! + log_action :disable, @account.user redirect_to admin_account_path(@account.id) end diff --git a/app/controllers/admin/action_logs_controller.rb b/app/controllers/admin/action_logs_controller.rb new file mode 100644 index 000000000..e273dfeae --- /dev/null +++ b/app/controllers/admin/action_logs_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Admin + class ActionLogsController < BaseController + def index + @action_logs = Admin::ActionLog.page(params[:page]) + end + end +end diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index 726134509..fc299f74c 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -3,6 +3,7 @@ module Admin class BaseController < ApplicationController include Authorization + include AccountableConcern layout 'admin' diff --git a/app/controllers/admin/confirmations_controller.rb b/app/controllers/admin/confirmations_controller.rb index c10b0ebee..34dfb458e 100644 --- a/app/controllers/admin/confirmations_controller.rb +++ b/app/controllers/admin/confirmations_controller.rb @@ -7,6 +7,7 @@ module Admin def create authorize @user, :confirm? @user.confirm! + log_action :confirm, @user redirect_to admin_accounts_path end diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index 509f7a48f..3fa2a0b72 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -20,6 +20,7 @@ module Admin @custom_emoji = CustomEmoji.new(resource_params) if @custom_emoji.save + log_action :create, @custom_emoji redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg') else render :new @@ -30,6 +31,7 @@ module Admin authorize @custom_emoji, :update? if @custom_emoji.update(resource_params) + log_action :update, @custom_emoji redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg') else redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg') @@ -38,7 +40,8 @@ module Admin def destroy authorize @custom_emoji, :destroy? - @custom_emoji.destroy + @custom_emoji.destroy! + log_action :destroy, @custom_emoji redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg') end @@ -49,6 +52,7 @@ module Admin emoji.image = @custom_emoji.image if emoji.save + log_action :create, emoji flash[:notice] = I18n.t('admin.custom_emojis.copied_msg') else flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg') @@ -60,12 +64,14 @@ module Admin def enable authorize @custom_emoji, :enable? @custom_emoji.update!(disabled: false) + log_action :enable, @custom_emoji redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg') end def disable authorize @custom_emoji, :disable? @custom_emoji.update!(disabled: true) + log_action :disable, @custom_emoji redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg') end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index e383dc831..64de2cbf0 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -21,6 +21,7 @@ module Admin if @domain_block.save DomainBlockWorker.perform_async(@domain_block.id) + log_action :create, @domain_block redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg') else render :new @@ -34,6 +35,7 @@ module Admin def destroy authorize @domain_block, :destroy? UnblockDomainService.new.call(@domain_block, retroactive_unblock?) + log_action :destroy, @domain_block redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg') end diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb index 01058bf46..9fe85064e 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -20,6 +20,7 @@ module Admin @email_domain_block = EmailDomainBlock.new(resource_params) if @email_domain_block.save + log_action :create, @email_domain_block redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg') else render :new @@ -28,7 +29,8 @@ module Admin def destroy authorize @email_domain_block, :destroy? - @email_domain_block.destroy + @email_domain_block.destroy! + log_action :destroy, @email_domain_block redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg') end diff --git a/app/controllers/admin/invites_controller.rb b/app/controllers/admin/invites_controller.rb new file mode 100644 index 000000000..faccaa7c8 --- /dev/null +++ b/app/controllers/admin/invites_controller.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Admin + class InvitesController < BaseController + def index + authorize :invite, :index? + + @invites = filtered_invites.includes(user: :account).page(params[:page]) + @invite = Invite.new + end + + def create + authorize :invite, :create? + + @invite = Invite.new(resource_params) + @invite.user = current_user + + if @invite.save + redirect_to admin_invites_path + else + @invites = Invite.page(params[:page]) + render :index + end + end + + def destroy + @invite = Invite.find(params[:id]) + authorize @invite, :destroy? + @invite.expire! + redirect_to admin_invites_path + end + + private + + def resource_params + params.require(:invite).permit(:max_uses, :expires_in) + end + + def filtered_invites + InviteFilter.new(filter_params).results + end + + def filter_params + params.permit(:available, :expired) + end + end +end diff --git a/app/controllers/admin/reported_statuses_controller.rb b/app/controllers/admin/reported_statuses_controller.rb index 4f66ce708..535bd11d4 100644 --- a/app/controllers/admin/reported_statuses_controller.rb +++ b/app/controllers/admin/reported_statuses_controller.rb @@ -8,7 +8,7 @@ module Admin def create authorize :status, :update? - @form = Form::StatusBatch.new(form_status_batch_params) + @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account)) flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save redirect_to admin_report_path(@report) @@ -16,13 +16,15 @@ module Admin def update authorize @status, :update? - @status.update(status_params) + @status.update!(status_params) + log_action :update, @status redirect_to admin_report_path(@report) end def destroy authorize @status, :destroy? RemovalWorker.perform_async(@status.id) + log_action :destroy, @status render json: @status end diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 745757ee8..75db6b78a 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -25,12 +25,17 @@ module Admin def process_report case params[:outcome].to_s when 'resolve' - @report.update(action_taken_by_current_attributes) + @report.update!(action_taken_by_current_attributes) + log_action :resolve, @report when 'suspend' Admin::SuspensionWorker.perform_async(@report.target_account.id) + log_action :resolve, @report + log_action :suspend, @report.target_account resolve_all_target_account_reports when 'silence' - @report.target_account.update(silenced: true) + @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 diff --git a/app/controllers/admin/resets_controller.rb b/app/controllers/admin/resets_controller.rb index 00b590bf6..3e27d01ac 100644 --- a/app/controllers/admin/resets_controller.rb +++ b/app/controllers/admin/resets_controller.rb @@ -7,6 +7,7 @@ module Admin def create authorize @user, :reset_password? @user.send_reset_password_instructions + log_action :reset_password, @user redirect_to admin_accounts_path end diff --git a/app/controllers/admin/roles_controller.rb b/app/controllers/admin/roles_controller.rb index 8f8685827..af7ec0740 100644 --- a/app/controllers/admin/roles_controller.rb +++ b/app/controllers/admin/roles_controller.rb @@ -7,12 +7,14 @@ module Admin def promote authorize @user, :promote? @user.promote! + log_action :promote, @user redirect_to admin_account_path(@user.account_id) end def demote authorize @user, :demote? @user.demote! + log_action :demote, @user redirect_to admin_account_path(@user.account_id) end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index e81290228..eed5fb6b5 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -13,14 +13,17 @@ module Admin closed_registrations_message open_deletion timeline_preview + show_staff_badge bootstrap_timeline_accounts thumbnail + min_invite_role ).freeze BOOLEAN_SETTINGS = %w( open_registrations open_deletion timeline_preview + show_staff_badge ).freeze UPLOAD_SETTINGS = %w( diff --git a/app/controllers/admin/silences_controller.rb b/app/controllers/admin/silences_controller.rb index 01fb292de..4c06a9c0c 100644 --- a/app/controllers/admin/silences_controller.rb +++ b/app/controllers/admin/silences_controller.rb @@ -6,13 +6,15 @@ module Admin def create authorize @account, :silence? - @account.update(silenced: true) + @account.update!(silenced: true) + log_action :silence, @account redirect_to admin_accounts_path end def destroy authorize @account, :unsilence? - @account.update(silenced: false) + @account.update!(silenced: false) + log_action :unsilence, @account redirect_to admin_accounts_path end diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb index b54a9b824..5d4325f57 100644 --- a/app/controllers/admin/statuses_controller.rb +++ b/app/controllers/admin/statuses_controller.rb @@ -26,7 +26,7 @@ module Admin def create authorize :status, :update? - @form = Form::StatusBatch.new(form_status_batch_params) + @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account)) flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save redirect_to admin_account_statuses_path(@account.id, current_params) @@ -34,13 +34,15 @@ module Admin def update authorize @status, :update? - @status.update(status_params) + @status.update!(status_params) + log_action :update, @status redirect_to admin_account_statuses_path(@account.id, current_params) end def destroy authorize @status, :destroy? RemovalWorker.perform_async(@status.id) + log_action :destroy, @status render json: @status end diff --git a/app/controllers/admin/suspensions_controller.rb b/app/controllers/admin/suspensions_controller.rb index 778feea5e..5f222e125 100644 --- a/app/controllers/admin/suspensions_controller.rb +++ b/app/controllers/admin/suspensions_controller.rb @@ -7,12 +7,14 @@ module Admin def create authorize @account, :suspend? Admin::SuspensionWorker.perform_async(@account.id) + log_action :suspend, @account redirect_to admin_accounts_path end def destroy authorize @account, :unsuspend? @account.unsuspend! + log_action :unsuspend, @account redirect_to admin_accounts_path end diff --git a/app/controllers/admin/two_factor_authentications_controller.rb b/app/controllers/admin/two_factor_authentications_controller.rb index 5a45d25cd..022107203 100644 --- a/app/controllers/admin/two_factor_authentications_controller.rb +++ b/app/controllers/admin/two_factor_authentications_controller.rb @@ -7,6 +7,7 @@ module Admin def destroy authorize @user, :disable_2fa? @user.disable_two_factor! + log_action :disable_2fa, @user redirect_to admin_accounts_path end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 85eb2d60e..b1a2ed573 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -13,11 +13,9 @@ class Api::V1::AccountsController < Api::BaseController end def follow - reblogs_arg = { reblogs: params[:reblogs] } - - FollowService.new.call(current_user.account, @account.acct, reblogs_arg) + FollowService.new.call(current_user.account, @account.acct, reblogs: params[:reblogs]) - options = @account.locked? ? {} : { following_map: { @account.id => reblogs_arg }, requested_map: { @account.id => false } } + options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: params[:reblogs] } }, requested_map: { @account.id => false } } render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options) end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 42e852c04..f4247fd95 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -17,13 +17,16 @@ class Auth::RegistrationsController < Devise::RegistrationsController def build_resource(hash = nil) super(hash) - resource.locale = I18n.locale + + resource.locale = I18n.locale + resource.invite_code = params[:invite_code] if resource.invite_code.blank? + resource.build_account if resource.account.nil? end def configure_sign_up_params devise_parameter_sanitizer.permit(:sign_up) do |u| - u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation) + u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation, :invite_code) end end @@ -36,7 +39,19 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def check_enabled_registrations - redirect_to root_path if single_user_mode? || !Setting.open_registrations + redirect_to root_path if single_user_mode? || !allowed_registrations? + end + + def allowed_registrations? + Setting.open_registrations || (invite_code.present? && Invite.find_by(code: invite_code)&.valid_for_use?) + end + + def invite_code + if params[:user] + params[:user][:invite_code] + else + params[:invite_code] + end end private diff --git a/app/controllers/concerns/accountable_concern.rb b/app/controllers/concerns/accountable_concern.rb new file mode 100644 index 000000000..3cdcffc51 --- /dev/null +++ b/app/controllers/concerns/accountable_concern.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module AccountableConcern + extend ActiveSupport::Concern + + def log_action(action, target) + Admin::ActionLog.create(account: current_account, action: action, target: target) + end +end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb new file mode 100644 index 000000000..38d6c8d73 --- /dev/null +++ b/app/controllers/invites_controller.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class InvitesController < ApplicationController + include Authorization + + layout 'admin' + + before_action :authenticate_user! + + def index + authorize :invite, :create? + + @invites = Invite.where(user: current_user) + @invite = Invite.new(expires_in: 1.day.to_i) + end + + def create + authorize :invite, :create? + + @invite = Invite.new(resource_params) + @invite.user = current_user + + if @invite.save + redirect_to invites_path + else + @invites = Invite.where(user: current_user) + render :index + end + end + + def destroy + @invite = Invite.where(user: current_user).find(params[:id]) + authorize @invite, :destroy? + @invite.expire! + redirect_to invites_path + end + + private + + def resource_params + params.require(:invite).permit(:max_uses, :expires_in) + end +end diff --git a/app/controllers/settings/migrations_controller.rb b/app/controllers/settings/migrations_controller.rb new file mode 100644 index 000000000..b18403a7f --- /dev/null +++ b/app/controllers/settings/migrations_controller.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class Settings::MigrationsController < ApplicationController + layout 'admin' + + before_action :authenticate_user! + + def show + @migration = Form::Migration.new(account: current_account.moved_to_account) + end + + def update + @migration = Form::Migration.new(resource_params) + + if @migration.valid? && migration_account_changed? + current_account.update!(moved_to_account: @migration.account) + ActivityPub::UpdateDistributionWorker.perform_async(current_account.id) + redirect_to settings_migration_path, notice: I18n.t('migrations.updated_msg') + else + render :show + end + end + + private + + def resource_params + params.require(:migration).permit(:acct) + end + + def migration_account_changed? + current_account.moved_to_account_id != @migration.account&.id + end +end diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb new file mode 100644 index 000000000..e85243e57 --- /dev/null +++ b/app/helpers/admin/action_logs_helper.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +module Admin::ActionLogsHelper + def log_target(log) + if log.target + linkable_log_target(log.target) + else + log_target_from_history(log.target_type, log.recorded_changes) + end + end + + def linkable_log_target(record) + case record.class.name + when 'Account' + link_to record.acct, admin_account_path(record.id) + when 'User' + link_to record.account.acct, admin_account_path(record.account_id) + when 'CustomEmoji' + record.shortcode + when 'Report' + link_to "##{record.id}", admin_report_path(record) + when 'DomainBlock', 'EmailDomainBlock' + link_to record.domain, "https://#{record.domain}" + when 'Status' + link_to record.account.acct, TagManager.instance.url_for(record) + end + end + + def log_target_from_history(type, attributes) + case type + when 'CustomEmoji' + attributes['shortcode'] + when 'DomainBlock', 'EmailDomainBlock' + link_to attributes['domain'], "https://#{attributes['domain']}" + when 'Status' + tmp_status = Status.new(attributes) + link_to tmp_status.account.acct, TagManager.instance.url_for(tmp_status) + end + end + + def relevant_log_changes(log) + if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action) + log.recorded_changes.slice('domain') + elsif log.target_type == 'CustomEmoji' && log.action == :update + log.recorded_changes.slice('domain', 'visible_in_picker') + elsif log.target_type == 'User' && [:promote, :demote].include?(log.action) + log.recorded_changes.slice('moderator', 'admin') + elsif log.target_type == 'DomainBlock' + log.recorded_changes.slice('severity', 'reject_media') + elsif log.target_type == 'Status' && log.action == :update + log.recorded_changes.slice('sensitive') + end + end + + def log_extra_attributes(hash) + safe_join(hash.to_a.map { |key, value| safe_join([content_tag(:span, key, class: 'diff-key'), '=', log_change(value)]) }, ' ') + end + + def log_change(val) + return content_tag(:span, val, class: 'diff-neutral') unless val.is_a?(Array) + safe_join([content_tag(:span, val.first, class: 'diff-old'), content_tag(:span, val.last, class: 'diff-new')], '→') + end + + def icon_for_log(log) + case log.target_type + when 'Account', 'User' + 'user' + when 'CustomEmoji' + 'file' + when 'Report' + 'flag' + when 'DomainBlock' + 'lock' + when 'EmailDomainBlock' + 'envelope' + when 'Status' + 'pencil' + end + end + + def class_for_log_icon(log) + case log.action + when :enable, :unsuspend, :unsilence, :confirm, :promote, :resolve + 'positive' + when :create + opposite_verbs?(log) ? 'negative' : 'positive' + when :update, :reset_password, :disable_2fa, :memorialize + 'neutral' + when :demote, :silence, :disable, :suspend + 'negative' + when :destroy + opposite_verbs?(log) ? 'positive' : 'negative' + else + '' + end + end + + private + + def opposite_verbs?(log) + %w(DomainBlock EmailDomainBlock).include?(log.target_type) + end +end diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index e0fae9d9a..73250cbf5 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -3,8 +3,9 @@ module Admin::FilterHelper ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip).freeze REPORT_FILTERS = %i(resolved account_id target_account_id).freeze + INVITE_FILTER = %i(available expired).freeze - FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER def filter_link_to(text, link_to_params, link_class_params = link_to_params) new_url = filtered_url_for(link_to_params) diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index a3441e6f9..6c7c38070 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -9,6 +9,24 @@ module JsonLdHelper value.is_a?(Array) ? value.first : value end + # The url attribute can be a string, an array of strings, or an array of objects. + # The objects could include a mimeType. Not-included mimeType means it's text/html. + def url_to_href(value, preferred_type = nil) + single_value = if value.is_a?(Array) && !value.first.is_a?(String) + value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) } + elsif value.is_a?(Array) + value.first + else + value + end + + if single_value.nil? || single_value.is_a?(String) + single_value + else + single_value['href'] + end + end + def as_array(value) value.is_a?(Array) ? value : [value] end diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 6662285d0..b0d9e3757 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -9,6 +9,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { MediaGallery, Video } from 'flavours/glitch/util/async-components'; import { HotKeys } from 'react-hotkeys'; import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container'; +import classNames from 'classnames'; // We use the component (and not the container) since we do not want // to use the progress bar to show download progress @@ -21,6 +22,7 @@ export default class Status extends ImmutablePureComponent { }; static propTypes = { + containerId: PropTypes.string, id: PropTypes.string, status: ImmutablePropTypes.map, account: ImmutablePropTypes.map, @@ -59,6 +61,7 @@ export default class Status extends ImmutablePureComponent { 'muted', 'collapse', 'notification', + 'hidden', ] updateOnStates = [ @@ -187,7 +190,9 @@ export default class Status extends ImmutablePureComponent { } handleExpandedToggle = () => { - this.setExpansion(this.state.isExpanded || !this.props.status.get('spoiler') ? null : true); + if (this.props.status.get('spoiler_text')) { + this.setExpansion(this.state.isExpanded ? null : true); + } }; handleOpenVideo = startTime => { @@ -221,11 +226,11 @@ export default class Status extends ImmutablePureComponent { } handleHotkeyMoveUp = () => { - this.props.onMoveUp(this.props.status.get('id')); + this.props.onMoveUp(this.props.containerId || this.props.id); } handleHotkeyMoveDown = () => { - this.props.onMoveDown(this.props.status.get('id')); + this.props.onMoveDown(this.props.containerId || this.props.id); } handleRef = c => { @@ -370,31 +375,24 @@ export default class Status extends ImmutablePureComponent { openProfile: this.handleHotkeyOpenProfile, moveUp: this.handleHotkeyMoveUp, moveDown: this.handleHotkeyMoveDown, + toggleSpoiler: this.handleExpandedToggle, }; + const computedClass = classNames('status', `status-${status.get('visibility')}`, { + collapsed: isExpanded === false, + 'has-background': isExpanded === false && background, + 'marked-for-delete': this.state.markedForDelete, + muted, + }, 'focusable'); + return ( <HotKeys handlers={handlers}> <div - className={ - `status${ - muted ? ' muted' : '' - } status-${status.get('visibility')}${ - isExpanded === false ? ' collapsed' : '' - }${ - isExpanded === false && background ? ' has-background' : '' - }${ - this.state.markedForDelete ? ' marked-for-delete' : '' - }` - } - style={{ - backgroundImage: ( - isExpanded === false && background ? - `url(${background})` : - 'none' - ), - }} + className={computedClass} + style={isExpanded === false && background ? { backgroundImage: `url(${background})` } : null} {...selectorAttribs} ref={handleRef} + tabIndex='0' > {prepend && account ? ( <StatusPrepend diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 1a4f35278..b753de7b3 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -45,6 +45,7 @@ const makeMapStateToProps = () => { } return { + containerId : props.containerId || props.id, // Should match reblogStatus's id for reblogs status : status, account : account || props.account, settings : state.get('local_settings'), diff --git a/app/javascript/flavours/glitch/features/notifications/components/follow.js b/app/javascript/flavours/glitch/features/notifications/components/follow.js index 4b682733e..54506f67c 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/follow.js +++ b/app/javascript/flavours/glitch/features/notifications/components/follow.js @@ -14,6 +14,7 @@ import NotificationOverlayContainer from '../containers/overlay_container'; export default class NotificationFollow extends ImmutablePureComponent { static propTypes = { + hidden: PropTypes.bool, id: PropTypes.string.isRequired, account: ImmutablePropTypes.map.isRequired, notification: ImmutablePropTypes.map.isRequired, @@ -57,7 +58,7 @@ export default class NotificationFollow extends ImmutablePureComponent { } render () { - const { account, notification } = this.props; + const { account, notification, hidden } = this.props; // Links to the display name. const displayName = account.get('display_name_html') || account.get('username'); @@ -87,7 +88,7 @@ export default class NotificationFollow extends ImmutablePureComponent { /> </div> - <AccountContainer id={account.get('id')} withNote={false} /> + <AccountContainer hidden={hidden} id={account.get('id')} withNote={false} /> <NotificationOverlayContainer notification={notification} /> </div> </HotKeys> diff --git a/app/javascript/flavours/glitch/features/notifications/components/notification.js b/app/javascript/flavours/glitch/features/notifications/components/notification.js index 185da8395..cc77426d3 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/notification.js +++ b/app/javascript/flavours/glitch/features/notifications/components/notification.js @@ -16,70 +16,75 @@ export default class Notification extends ImmutablePureComponent { onMoveUp: PropTypes.func.isRequired, onMoveDown: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, - settings: ImmutablePropTypes.map.isRequired, }; - renderFollow () { - const { notification } = this.props; - return ( - <NotificationFollow - id={notification.get('id')} - account={notification.get('account')} - notification={notification} - /> - ); - } - - renderMention () { - const { notification } = this.props; - return ( - <StatusContainer - id={notification.get('status')} - notification={notification} - withDismiss - /> - ); - } - - renderFavourite () { - const { notification } = this.props; - return ( - <StatusContainer - id={notification.get('status')} - account={notification.get('account')} - prepend='favourite' - muted - notification={notification} - withDismiss - /> - ); - } - - renderReblog () { - const { notification } = this.props; - return ( - <StatusContainer - id={notification.get('status')} - account={notification.get('account')} - prepend='reblog' - muted - notification={notification} - withDismiss - /> - ); - } - render () { - const { notification } = this.props; + const { + hidden, + notification, + onMoveDown, + onMoveUp, + onMention, + } = this.props; + switch(notification.get('type')) { case 'follow': - return this.renderFollow(); + return ( + <NotificationFollow + hidden={hidden} + id={notification.get('id')} + account={notification.get('account')} + notification={notification} + onMoveDown={onMoveDown} + onMoveUp={onMoveUp} + onMention={onMention} + /> + ); case 'mention': - return this.renderMention(); + return ( + <StatusContainer + containerId={notification.get('id')} + hidden={hidden} + id={notification.get('status')} + notification={notification} + onMoveDown={onMoveDown} + onMoveUp={onMoveUp} + onMention={onMention} + withDismiss + /> + ); case 'favourite': - return this.renderFavourite(); + return ( + <StatusContainer + containerId={notification.get('id')} + hidden={hidden} + id={notification.get('status')} + account={notification.get('account')} + prepend='favourite' + muted + notification={notification} + onMoveDown={onMoveDown} + onMoveUp={onMoveUp} + onMention={onMention} + withDismiss + /> + ); case 'reblog': - return this.renderReblog(); + return ( + <StatusContainer + containerId={notification.get('id')} + hidden={hidden} + id={notification.get('status')} + account={notification.get('account')} + prepend='reblog' + muted + notification={notification} + onMoveDown={onMoveDown} + onMoveUp={onMoveUp} + onMention={onMention} + withDismiss + /> + ); default: return null; } diff --git a/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js b/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js index c60e72e1c..be007f30b 100644 --- a/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js +++ b/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js @@ -11,7 +11,6 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, props) => ({ notification: getNotification(state, props.notification, props.accountId), - settings: state.get('local_settings'), notifCleaning: state.getIn(['notifications', 'cleaningMode']), }); diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index 7c6f436d6..0cb5238b0 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -41,7 +41,7 @@ export default class DetailedStatus extends ImmutablePureComponent { render () { const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status; - const { settings } = this.props; + const { expanded, setExpansion, settings } = this.props; let media = ''; let mediaIcon = null; @@ -109,6 +109,8 @@ export default class DetailedStatus extends ImmutablePureComponent { status={status} media={media} mediaIcon={mediaIcon} + expanded={expanded} + setExpansion={setExpansion} /> <div className='detailed-status__meta'> diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index 73b212bba..93b0fe9d9 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -71,6 +71,7 @@ export default class Status extends ImmutablePureComponent { state = { fullscreen: false, + isExpanded: null, }; componentWillMount () { @@ -88,6 +89,12 @@ export default class Status extends ImmutablePureComponent { } } + handleExpandedToggle = () => { + if (this.props.status.get('spoiler_text')) { + this.setExpansion(this.state.isExpanded ? null : true); + } + }; + handleFavouriteClick = (status) => { if (status.get('favourited')) { this.props.dispatch(unfavourite(status)); @@ -241,6 +248,10 @@ export default class Status extends ImmutablePureComponent { )); } + setExpansion = value => { + this.setState({ isExpanded: value ? true : null }); + } + setRef = c => { this.node = c; } @@ -272,8 +283,9 @@ export default class Status extends ImmutablePureComponent { render () { let ancestors, descendants; + const { setExpansion } = this; const { status, settings, ancestorsIds, descendantsIds } = this.props; - const { fullscreen } = this.state; + const { fullscreen, isExpanded } = this.state; if (status === null) { return ( @@ -300,6 +312,7 @@ export default class Status extends ImmutablePureComponent { boost: this.handleHotkeyBoost, mention: this.handleHotkeyMention, openProfile: this.handleHotkeyOpenProfile, + toggleSpoiler: this.handleExpandedToggle, }; return ( @@ -317,6 +330,8 @@ export default class Status extends ImmutablePureComponent { settings={settings} onOpenVideo={this.handleOpenVideo} onOpenMedia={this.handleOpenMedia} + expanded={isExpanded} + setExpansion={setExpansion} /> <ActionBar diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index 620630faf..4a1982916 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -84,6 +84,7 @@ const keyMap = { goToProfile: 'g u', goToBlocked: 'g b', goToMuted: 'g m', + toggleSpoiler: 'x', }; @connect(mapStateToProps) diff --git a/app/javascript/flavours/glitch/styles/_mixins.scss b/app/javascript/flavours/glitch/styles/_mixins.scss index 79a8149fd..102723e39 100644 --- a/app/javascript/flavours/glitch/styles/_mixins.scss +++ b/app/javascript/flavours/glitch/styles/_mixins.scss @@ -46,6 +46,7 @@ margin-left: -22px; margin-right: -22px; width: inherit; + max-width: none; height: 250px; } } diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss index 3be8db4b4..ade8df018 100644 --- a/app/javascript/flavours/glitch/styles/components.scss +++ b/app/javascript/flavours/glitch/styles/components.scss @@ -1058,6 +1058,14 @@ color: $ui-secondary-color; } + .account__avatar { + @include avatar-radius(); + @include avatar-size(90px); + display: block; + margin: 0 auto 10px; + overflow: hidden; + } + .account__header__display-name { color: $primary-text-color; display: inline-block; @@ -1248,14 +1256,6 @@ } } -.account__header__avatar { - @include avatar-radius(); - @include avatar-size(90px); - display: block; - margin: 0 auto 10px; - overflow: hidden; -} - .account-authorize { padding: 14px 10px; diff --git a/app/javascript/glitch/locales/pl.json b/app/javascript/glitch/locales/pl.json new file mode 100644 index 000000000..1481b6a2a --- /dev/null +++ b/app/javascript/glitch/locales/pl.json @@ -0,0 +1,44 @@ +{ + "getting_started.open_source_notice": "Glitchsoc jest wolnym i otwartoźródłowym forkiem oprogramowania {Mastodon}. Możesz współtworzyć projekt lub zgłaszać błędy na GitHubie pod adresem {github}.", + "layout.auto": "Automatyczny", + "layout.current_is": "Twój obecny układ to:", + "layout.desktop": "Desktopowy", + "layout.mobile": "Mobilny", + "navigation_bar.app_settings": "Ustawienia aplikacji", + "getting_started.onboarding": "Rozejrzyj się", + "onboarding.page_one.federation": "{domain} jest 'instancją' Mastodona. Mastodon to sieć działających niezależnie serwerów tworzących jedną sieć społecznościową. Te serwery nazywane są instancjami.", + "onboarding.page_one.welcome": "Witamy na {domain}!", + "onboarding.page_six.github": "{domain} jest oparty na Glitchsoc. Glitchsoc jest {forkiem} {Mastodon}a kompatybilnym z każdym klientem i aplikacją Mastodona. Glitchsoc jest całkowicie wolnym i otwartoźródłowym oprogramowaniem. Możesz zgłaszać błędy i sugestie funkcji oraz współtworzyć projekt na {github}.", + "settings.auto_collapse": "Automatyczne zwijanie", + "settings.auto_collapse_all": "Wszystko", + "settings.auto_collapse_lengthy": "Długie wpisy", + "settings.auto_collapse_media": "Wpisy z zawartością multimedialną", + "settings.auto_collapse_notifications": "Powiadomienia", + "settings.auto_collapse_reblogs": "Podbicia", + "settings.auto_collapse_replies": "Odpowiedzi", + "settings.close": "Zamknij", + "settings.collapsed_statuses": "Zwijanie wpisów", + "settings.enable_collapsed": "Włącz zwijanie wpisów", + "settings.general": "Ogólne", + "settings.image_backgrounds": "Obrazy w tle", + "settings.image_backgrounds_media": "Wyświetlaj zawartość multimedialną zwiniętych wpisów", + "settings.image_backgrounds_users": "Nadaj tło zwiniętym wpisom", + "settings.media": "Zawartość multimedialna", + "settings.media_letterbox": "Letterbox media", + "settings.media_fullwidth": "Podgląd zawartości multimedialnej o pełnej szerokości", + "settings.preferences": "Preferencje użyytkownika", + "settings.wide_view": "Szeroki widok (tylko w trybie desktopowym)", + "settings.navbar_under": "Pasek nawigacji na dole (tylko w trybie mobilnym)", + "status.collapse": "Zwiń", + "status.uncollapse": "Rozwiń", + + "notification.markForDeletion": "Oznacz do usunięcia", + "notifications.clear": "Wyczyść wszystkie powiadomienia", + "notifications.marked_clear_confirmation": "Czy na pewno chcesz bezpowrtonie usunąć wszystkie powiadomienia?", + "notifications.marked_clear": "Usuń zaznaczone powiadomienia", + + "notification_purge.btn_all": "Zaznacz\nwszystkie", + "notification_purge.btn_none": "Odznacz\nwszystkie", + "notification_purge.btn_invert": "Odwróć\nzaznaczenie", + "notification_purge.btn_apply": "Usuń\nzaznaczone" +} diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index fbaebf786..f63325658 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -105,12 +105,13 @@ export function fetchAccountFail(id, error) { }; }; -export function followAccount(id) { +export function followAccount(id, reblogs = true) { return (dispatch, getState) => { + const alreadyFollowing = getState().getIn(['relationships', id, 'following']); dispatch(followAccountRequest(id)); - api(getState).post(`/api/v1/accounts/${id}/follow`).then(response => { - dispatch(followAccountSuccess(response.data)); + api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => { + dispatch(followAccountSuccess(response.data, alreadyFollowing)); }).catch(error => { dispatch(followAccountFail(error)); }); @@ -136,10 +137,11 @@ export function followAccountRequest(id) { }; }; -export function followAccountSuccess(relationship) { +export function followAccountSuccess(relationship, alreadyFollowing) { return { type: ACCOUNT_FOLLOW_SUCCESS, relationship, + alreadyFollowing, }; }; diff --git a/app/javascript/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js new file mode 100644 index 000000000..332e42166 --- /dev/null +++ b/app/javascript/mastodon/actions/lists.js @@ -0,0 +1,28 @@ +import api from '../api'; + +export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST'; +export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS'; +export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL'; + +export const fetchList = id => (dispatch, getState) => { + dispatch(fetchListRequest(id)); + + api(getState).get(`/api/v1/lists/${id}`) + .then(({ data }) => dispatch(fetchListSuccess(data))) + .catch(err => dispatch(fetchListFail(err))); +}; + +export const fetchListRequest = id => ({ + type: LIST_FETCH_REQUEST, + id, +}); + +export const fetchListSuccess = list => ({ + type: LIST_FETCH_SUCCESS, + list, +}); + +export const fetchListFail = error => ({ + type: LIST_FETCH_FAIL, + error, +}); diff --git a/app/javascript/mastodon/actions/mutes.js b/app/javascript/mastodon/actions/mutes.js index 3474250fe..daa76a8f7 100644 --- a/app/javascript/mastodon/actions/mutes.js +++ b/app/javascript/mastodon/actions/mutes.js @@ -1,6 +1,6 @@ import api, { getLinks } from '../api'; import { fetchRelationships } from './accounts'; -import { openModal } from '../../mastodon/actions/modal'; +import { openModal } from './modal'; export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST'; export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS'; @@ -100,4 +100,4 @@ export function toggleHideNotifications() { return dispatch => { dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS }); }; -} \ No newline at end of file +} diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index dcce048ca..c22152edd 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -51,3 +51,4 @@ export const connectCommunityStream = () => connectTimelineStream('community', ' export const connectMediaStream = () => connectTimelineStream('community', 'public:local'); export const connectPublicStream = () => connectTimelineStream('public', 'public'); export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`); +export const connectListStream = (id) => connectTimelineStream(`list:${id}`, `list&list=${id}`); diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 09abe2702..f8843d1d9 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -118,6 +118,7 @@ export const refreshCommunityTimeline = () => refreshTimeline('community', '/ export const refreshAccountTimeline = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`); export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true }); export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`); +export const refreshListTimeline = id => refreshTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`); export function refreshTimelineFail(timeline, error, skipLoading) { return { @@ -158,6 +159,7 @@ export const expandCommunityTimeline = () => expandTimeline('community', '/ap export const expandAccountTimeline = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`); export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true }); export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`); +export const expandListTimeline = id => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`); export function expandTimelineRequest(timeline) { return { diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js index 724b10980..1f2d7690f 100644 --- a/app/javascript/mastodon/components/account.js +++ b/app/javascript/mastodon/components/account.js @@ -93,7 +93,7 @@ export default class Account extends ImmutablePureComponent { </div> ); } else { - buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />; + buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following ? true : false} />; } } diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index 3a3ebf487..43dc0d6e3 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -110,7 +110,7 @@ export default class Dropdown extends React.PureComponent { icon: PropTypes.string.isRequired, items: PropTypes.array.isRequired, size: PropTypes.number.isRequired, - ariaLabel: PropTypes.string, + title: PropTypes.string, disabled: PropTypes.bool, status: ImmutablePropTypes.map, isUserTouching: PropTypes.func, @@ -120,7 +120,7 @@ export default class Dropdown extends React.PureComponent { }; static defaultProps = { - ariaLabel: 'Menu', + title: 'Menu', }; state = { @@ -186,14 +186,14 @@ export default class Dropdown extends React.PureComponent { } render () { - const { icon, items, size, ariaLabel, disabled } = this.props; + const { icon, items, size, title, disabled } = this.props; const { expanded } = this.state; return ( <div onKeyDown={this.handleKeyDown}> <IconButton icon={icon} - title={ariaLabel} + title={title} active={expanded} disabled={disabled} size={size} diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 7021c198e..cd59c7845 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -179,7 +179,7 @@ export default class StatusActionBar extends ImmutablePureComponent { {shareButton} <div className='status__action-bar-dropdown'> - <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} /> + <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' title={intl.formatMessage(messages.more)} /> </div> </div> ); diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js index e375131d4..389296c42 100644 --- a/app/javascript/mastodon/features/account/components/action_bar.js +++ b/app/javascript/mastodon/features/account/components/action_bar.js @@ -20,6 +20,8 @@ const messages = defineMessages({ media: { id: 'account.media', defaultMessage: 'Media' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, + hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' }, + showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' }, }); @injectIntl @@ -30,6 +32,7 @@ export default class ActionBar extends React.PureComponent { onFollow: PropTypes.func, onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, + onReblogToggle: PropTypes.func.isRequired, onReport: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired, onBlockDomain: PropTypes.func.isRequired, @@ -60,6 +63,15 @@ export default class ActionBar extends React.PureComponent { if (account.get('id') === me) { menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); } else { + const following = account.getIn(['relationship', 'following']); + if (following) { + if (following.get('reblogs')) { + menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); + } else { + menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); + } + } + if (account.getIn(['relationship', 'muting'])) { menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute }); } else { diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index f0d2d481f..b2399ae9b 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -7,6 +7,7 @@ import Motion from '../../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { autoPlayGif, me } from '../../../initial_state'; +import classNames from 'classnames'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, @@ -102,6 +103,10 @@ export default class Header extends ImmutablePureComponent { } } + if (account.get('moved')) { + actionBtn = ''; + } + if (account.get('locked')) { lockedIcon = <i className='fa fa-lock' />; } @@ -110,7 +115,7 @@ export default class Header extends ImmutablePureComponent { const displayNameHtml = { __html: account.get('display_name_html') }; return ( - <div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}> + <div className={classNames('account__header', { inactive: !!account.get('moved') })} style={{ backgroundImage: `url(${account.get('header')})` }}> <div> <Avatar account={account} /> diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js index 8cf7b92ca..0ddb6b6c1 100644 --- a/app/javascript/mastodon/features/account_timeline/components/header.js +++ b/app/javascript/mastodon/features/account_timeline/components/header.js @@ -5,6 +5,7 @@ import InnerHeader from '../../account/components/header'; import ActionBar from '../../account/components/action_bar'; import MissingIndicator from '../../../components/missing_indicator'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import MovedNote from './moved_note'; export default class Header extends ImmutablePureComponent { @@ -13,6 +14,7 @@ export default class Header extends ImmutablePureComponent { onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, + onReblogToggle: PropTypes.func.isRequired, onReport: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired, onBlockDomain: PropTypes.func.isRequired, @@ -39,6 +41,10 @@ export default class Header extends ImmutablePureComponent { this.props.onReport(this.props.account); } + handleReblogToggle = () => { + this.props.onReblogToggle(this.props.account); + } + handleMute = () => { this.props.onMute(this.props.account); } @@ -68,6 +74,8 @@ export default class Header extends ImmutablePureComponent { return ( <div className='account-timeline__header'> + {account.get('moved') && <MovedNote from={account} to={account.get('moved')} />} + <InnerHeader account={account} onFollow={this.handleFollow} @@ -77,6 +85,7 @@ export default class Header extends ImmutablePureComponent { account={account} onBlock={this.handleBlock} onMention={this.handleMention} + onReblogToggle={this.handleReblogToggle} onReport={this.handleReport} onMute={this.handleMute} onBlockDomain={this.handleBlockDomain} diff --git a/app/javascript/mastodon/features/account_timeline/components/moved_note.js b/app/javascript/mastodon/features/account_timeline/components/moved_note.js new file mode 100644 index 000000000..1c0e081cc --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/moved_note.js @@ -0,0 +1,48 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import AvatarOverlay from '../../../components/avatar_overlay'; +import DisplayName from '../../../components/display_name'; + +export default class MovedNote extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static propTypes = { + from: ImmutablePropTypes.map.isRequired, + to: ImmutablePropTypes.map.isRequired, + }; + + handleAccountClick = e => { + if (e.button === 0) { + e.preventDefault(); + this.context.router.history.push(`/accounts/${this.props.to.get('id')}`); + } + + e.stopPropagation(); + } + + render () { + const { from, to } = this.props; + const displayNameHtml = { __html: from.get('display_name_html') }; + + return ( + <div className='account__moved-note'> + <div className='account__moved-note__message'> + <div className='account__moved-note__icon-wrapper'><i className='fa fa-fw fa-suitcase account__moved-note__icon' /></div> + <FormattedMessage id='account.moved_to' defaultMessage='{name} has moved to:' values={{ name: <strong dangerouslySetInnerHTML={displayNameHtml} /> }} /> + </div> + + <a href={to.get('url')} onClick={this.handleAccountClick} className='detailed-status__display-name'> + <div className='detailed-status__display-avatar'><AvatarOverlay account={to} friend={from} /></div> + <DisplayName account={to} /> + </a> + </div> + ); + } + +} diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js index 8e50ec405..b41eb19d4 100644 --- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js +++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js @@ -67,6 +67,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(mentionCompose(account, router)); }, + onReblogToggle (account) { + if (account.getIn(['relationship', 'following', 'reblogs'])) { + dispatch(followAccount(account.get('id'), false)); + } else { + dispatch(followAccount(account.get('id'), true)); + } + }, + onReport (account) { dispatch(initReport(account)); }, diff --git a/app/javascript/mastodon/features/compose/components/navigation_bar.js b/app/javascript/mastodon/features/compose/components/navigation_bar.js index 7f346854c..3014c4033 100644 --- a/app/javascript/mastodon/features/compose/components/navigation_bar.js +++ b/app/javascript/mastodon/features/compose/components/navigation_bar.js @@ -11,7 +11,7 @@ export default class NavigationBar extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, - onClose: PropTypes.func.isRequired, + onClose: PropTypes.func, }; render () { diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index 4b4ae6947..df4284207 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -25,6 +25,7 @@ const messages = defineMessages({ mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }, pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' }, + keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' }, }); const mapStateToProps = state => ({ @@ -78,6 +79,7 @@ export default class GettingStarted extends ImmutablePureComponent { navItems = navItems.concat([ <ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />, <ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />, + <ColumnLink key='9' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' hideOnMobile />, ]); return ( diff --git a/app/javascript/mastodon/features/keyboard_shortcuts/index.js b/app/javascript/mastodon/features/keyboard_shortcuts/index.js new file mode 100644 index 000000000..22991fcba --- /dev/null +++ b/app/javascript/mastodon/features/keyboard_shortcuts/index.js @@ -0,0 +1,98 @@ +import React from 'react'; +import Column from '../ui/components/column'; +import ColumnBackButtonSlim from '../../components/column_back_button_slim'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import PropTypes from 'prop-types'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const messages = defineMessages({ + heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' }, +}); + +@injectIntl +export default class KeyboardShortcuts extends ImmutablePureComponent { + + static propTypes = { + intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, + }; + + render () { + const { intl } = this.props; + + return ( + <Column icon='question' heading={intl.formatMessage(messages.heading)}> + <ColumnBackButtonSlim /> + <div className='keyboard-shortcuts scrollable optionally-scrollable'> + <table> + <thead> + <tr> + <th><FormattedMessage id='keyboard_shortcuts.hotkey' defaultMessage='Hotkey' /></th> + <th><FormattedMessage id='keyboard_shortcuts.description' defaultMessage='Description' /></th> + </tr> + </thead> + <tbody> + <tr> + <td><code>r</code></td> + <td><FormattedMessage id='keyboard_shortcuts.reply' defaultMessage='to reply' /></td> + </tr> + <tr> + <td><code>m</code></td> + <td><FormattedMessage id='keyboard_shortcuts.mention' defaultMessage='to mention author' /></td> + </tr> + <tr> + <td><code>f</code></td> + <td><FormattedMessage id='keyboard_shortcuts.favourite' defaultMessage='to favourite' /></td> + </tr> + <tr> + <td><code>b</code></td> + <td><FormattedMessage id='keyboard_shortcuts.boost' defaultMessage='to boost' /></td> + </tr> + <tr> + <td><code>enter</code></td> + <td><FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open status' /></td> + </tr> + <tr> + <td><code>up</code></td> + <td><FormattedMessage id='keyboard_shortcuts.up' defaultMessage='to move up in the list' /></td> + </tr> + <tr> + <td><code>down</code></td> + <td><FormattedMessage id='keyboard_shortcuts.down' defaultMessage='to move down in the list' /></td> + </tr> + <tr> + <td><code>1</code>-<code>9</code></td> + <td><FormattedMessage id='keyboard_shortcuts.column' defaultMessage='to focus a status in one of the columns' /></td> + </tr> + <tr> + <td><code>n</code></td> + <td><FormattedMessage id='keyboard_shortcuts.compose' defaultMessage='to focus the compose textarea' /></td> + </tr> + <tr> + <td><code>alt</code>+<code>n</code></td> + <td><FormattedMessage id='keyboard_shortcuts.toot' defaultMessage='to start a brand new toot' /></td> + </tr> + <tr> + <td><code>backspace</code></td> + <td><FormattedMessage id='keyboard_shortcuts.back' defaultMessage='to navigate back' /></td> + </tr> + <tr> + <td><code>s</code></td> + <td><FormattedMessage id='keyboard_shortcuts.search' defaultMessage='to focus search' /></td> + </tr> + <tr> + <td><code>esc</code></td> + <td><FormattedMessage id='keyboard_shortcuts.unfocus' defaultMessage='to un-focus compose textarea/search' /></td> + </tr> + <tr> + <td><code>?</code></td> + <td><FormattedMessage id='keyboard_shortcuts.legend' defaultMessage='to display this legend' /></td> + </tr> + </tbody> + </table> + </div> + </Column> + ); + } + +} diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js new file mode 100644 index 000000000..71f6e36a8 --- /dev/null +++ b/app/javascript/mastodon/features/list_timeline/index.js @@ -0,0 +1,106 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import StatusListContainer from '../ui/containers/status_list_container'; +import Column from '../../components/column'; +import ColumnHeader from '../../components/column_header'; +import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; +import { FormattedMessage } from 'react-intl'; +import { connectListStream } from '../../actions/streaming'; +import { refreshListTimeline, expandListTimeline } from '../../actions/timelines'; +import { fetchList } from '../../actions/lists'; + +const mapStateToProps = (state, props) => ({ + list: state.getIn(['lists', props.params.id]), + hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0, +}); + +@connect(mapStateToProps) +export default class ListTimeline extends React.PureComponent { + + static propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + columnId: PropTypes.string, + hasUnread: PropTypes.bool, + multiColumn: PropTypes.bool, + list: ImmutablePropTypes.map, + }; + + handlePin = () => { + const { columnId, dispatch } = this.props; + + if (columnId) { + dispatch(removeColumn(columnId)); + } else { + dispatch(addColumn('LIST', { id: this.props.params.id })); + } + } + + handleMove = (dir) => { + const { columnId, dispatch } = this.props; + dispatch(moveColumn(columnId, dir)); + } + + handleHeaderClick = () => { + this.column.scrollTop(); + } + + componentDidMount () { + const { dispatch } = this.props; + const { id } = this.props.params; + + dispatch(fetchList(id)); + dispatch(refreshListTimeline(id)); + + this.disconnect = dispatch(connectListStream(id)); + } + + componentWillUnmount () { + if (this.disconnect) { + this.disconnect(); + this.disconnect = null; + } + } + + setRef = c => { + this.column = c; + } + + handleLoadMore = () => { + const { id } = this.props.params; + this.props.dispatch(expandListTimeline(id)); + } + + render () { + const { hasUnread, columnId, multiColumn, list } = this.props; + const { id } = this.props.params; + const pinned = !!columnId; + const title = list ? list.get('title') : id; + + return ( + <Column ref={this.setRef}> + <ColumnHeader + icon='bars' + active={hasUnread} + title={title} + onPin={this.handlePin} + onMove={this.handleMove} + onClick={this.handleHeaderClick} + pinned={pinned} + multiColumn={multiColumn} + /> + + <StatusListContainer + trackScroll={!pinned} + scrollKey={`list_timeline-${columnId}`} + timelineId={`list:${id}`} + loadMore={this.handleLoadMore} + emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet.' />} + /> + </Column> + ); + } + +} diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index 7b65420d0..99834df6c 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -120,7 +120,7 @@ export default class ActionBar extends React.PureComponent { {shareButton} <div className='detailed-status__action-bar-dropdown'> - <DropdownMenuContainer size={18} icon='ellipsis-h' items={menu} direction='left' ariaLabel='More' /> + <DropdownMenuContainer size={18} icon='ellipsis-h' items={menu} direction='left' title='More' /> </div> </div> ); diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js index bb83374b9..680bf63ab 100644 --- a/app/javascript/mastodon/features/status/components/card.js +++ b/app/javascript/mastodon/features/status/components/card.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import Immutable from 'immutable'; import ImmutablePropTypes from 'react-immutable-proptypes'; import punycode from 'punycode'; import classnames from 'classnames'; @@ -24,6 +25,7 @@ export default class Card extends React.PureComponent { static propTypes = { card: ImmutablePropTypes.map, maxDescription: PropTypes.number, + onOpenMedia: PropTypes.func.isRequired, }; static defaultProps = { @@ -34,6 +36,27 @@ export default class Card extends React.PureComponent { width: 0, }; + handlePhotoClick = () => { + const { card, onOpenMedia } = this.props; + + onOpenMedia( + Immutable.fromJS([ + { + type: 'image', + url: card.get('url'), + description: card.get('title'), + meta: { + original: { + width: card.get('width'), + height: card.get('height'), + }, + }, + }, + ]), + 0 + ); + }; + renderLink () { const { card, maxDescription } = this.props; @@ -73,9 +96,16 @@ export default class Card extends React.PureComponent { const { card } = this.props; return ( - <a href={card.get('url')} className='status-card-photo' target='_blank' rel='noopener'> - <img src={card.get('url')} alt={card.get('title')} width={card.get('width')} height={card.get('height')} /> - </a> + <img + className='status-card-photo' + onClick={this.handlePhotoClick} + role='button' + tabIndex='0' + src={card.get('url')} + alt={card.get('title')} + width={card.get('width')} + height={card.get('height')} + /> ); } diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index 81f71749b..abdb9a3f6 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -73,7 +73,7 @@ export default class DetailedStatus extends ImmutablePureComponent { ); } } else if (status.get('spoiler_text').length === 0) { - media = <CardContainer statusId={status.get('id')} />; + media = <CardContainer onOpenMedia={this.props.onOpenMedia} statusId={status.get('id')} />; } if (status.get('application')) { diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 5610095b9..93ed9e605 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -11,7 +11,7 @@ import BundleContainer from '../containers/bundle_container'; import ColumnLoading from './column_loading'; import DrawerLoading from './drawer_loading'; import BundleColumnError from './bundle_column_error'; -import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components'; +import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components'; import detectPassiveEvents from 'detect-passive-events'; import { scrollRight } from '../../../scroll'; @@ -24,6 +24,7 @@ const componentMap = { 'COMMUNITY': CommunityTimeline, 'HASHTAG': HashtagTimeline, 'FAVOURITES': FavouritedStatuses, + 'LIST': ListTimeline, }; @component => injectIntl(component, { withRef: true }) diff --git a/app/javascript/mastodon/features/ui/components/image_loader.js b/app/javascript/mastodon/features/ui/components/image_loader.js index aad594380..e3e7197c5 100644 --- a/app/javascript/mastodon/features/ui/components/image_loader.js +++ b/app/javascript/mastodon/features/ui/components/image_loader.js @@ -7,7 +7,7 @@ export default class ImageLoader extends React.PureComponent { static propTypes = { alt: PropTypes.string, src: PropTypes.string.isRequired, - previewSrc: PropTypes.string.isRequired, + previewSrc: PropTypes.string, width: PropTypes.number, height: PropTypes.number, } @@ -47,7 +47,7 @@ export default class ImageLoader extends React.PureComponent { this.removeEventListeners(); this.setState({ loading: true, error: false }); Promise.all([ - this.loadPreviewCanvas(props), + props.previewSrc && this.loadPreviewCanvas(props), this.hasSize() && this.loadOriginalImage(props), ].filter(Boolean)) .then(() => { diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index f41a83089..02591a51f 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -92,7 +92,7 @@ export default class MediaModal extends ImmutablePureComponent { const height = image.getIn(['meta', 'original', 'height']) || null; if (image.get('type') === 'image') { - return <ImageLoader previewSrc={image.get('preview_url')} src={image.get('url')} width={width} height={height} alt={image.get('description')} key={image.get('preview_url')} />; + return <ImageLoader previewSrc={image.get('preview_url')} src={image.get('url')} width={width} height={height} alt={image.get('description')} key={image.get('url')} />; } else if (image.get('type') === 'gifv') { return <ExtendedVideoPlayer src={image.get('url')} muted controls={false} width={width} height={height} key={image.get('preview_url')} alt={image.get('description')} />; } diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index f28b37099..361326961 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -19,6 +19,7 @@ import { Compose, Status, GettingStarted, + KeyboardShortcuts, PublicTimeline, CommunityTimeline, AccountTimeline, @@ -33,6 +34,7 @@ import { FollowRequests, GenericNotFound, FavouritedStatuses, + ListTimeline, Blocks, Mutes, PinnedStatuses, @@ -55,6 +57,7 @@ const mapStateToProps = state => ({ }); const keyMap = { + help: '?', new: 'n', search: 's', forceNew: 'option+n', @@ -297,6 +300,14 @@ export default class UI extends React.Component { this.hotkeys = c; } + handleHotkeyToggleHelp = () => { + if (this.props.location.pathname === '/keyboard-shortcuts') { + this.context.router.history.goBack(); + } else { + this.context.router.history.push('/keyboard-shortcuts'); + } + } + handleHotkeyGoToHome = () => { this.context.router.history.push('/timelines/home'); } @@ -342,6 +353,7 @@ export default class UI extends React.Component { const { children } = this.props; const handlers = { + help: this.handleHotkeyToggleHelp, new: this.handleHotkeyNew, search: this.handleHotkeySearch, forceNew: this.handleHotkeyForceNew, @@ -368,10 +380,12 @@ export default class UI extends React.Component { <WrappedSwitch> <Redirect from='/' to='/getting-started' exact /> <WrappedRoute path='/getting-started' component={GettingStarted} content={children} /> + <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} /> <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} /> <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} /> <WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} /> <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} /> + <WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} /> <WrappedRoute path='/notifications' component={Notifications} content={children} /> <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} /> diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 39663d5ca..b741f668e 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -26,6 +26,10 @@ export function HashtagTimeline () { return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline'); } +export function ListTimeline () { + return import(/* webpackChunkName: "features/list_timeline" */'../../list_timeline'); +} + export function Status () { return import(/* webpackChunkName: "features/status" */'../../status'); } @@ -34,6 +38,10 @@ export function GettingStarted () { return import(/* webpackChunkName: "features/getting_started" */'../../getting_started'); } +export function KeyboardShortcuts () { + return import(/* webpackChunkName: "features/keyboard_shortcuts" */'../../keyboard_shortcuts'); +} + export function PinnedStatuses () { return import(/* webpackChunkName: "features/pinned_statuses" */'../../pinned_statuses'); } diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 7cc8ea237..596a519bd 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -7,17 +7,22 @@ "account.followers": "المتابعون", "account.follows": "يتبع", "account.follows_you": "يتابعك", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "وسائط", "account.mention": "أُذكُر @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "أكتم @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "المشاركات", "account.report": "أبلغ عن @{name}", "account.requested": "في انتظار الموافقة", "account.share": "مشاركة @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "إلغاء الحظر عن @{name}", "account.unblock_domain": "فك حظر {domain}", "account.unfollow": "إلغاء المتابعة", "account.unmute": "إلغاء الكتم عن @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "عرض الملف الشخصي كاملا", "boost_modal.combo": "يمكنك ضغط {combo} لتخطّي هذه في المرّة القادمة", "bundle_column_error.body": "لقد وقع هناك خطأ أثناء عملية تحميل هذا العنصر.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "ليس هناك بعدُ أي محتوى ذو علاقة بهذا الوسم.", "empty_column.home": "إنك لا تتبع بعد أي شخص إلى حد الآن. زر {public} أو استخدام حقل البحث لكي تبدأ على التعرف على مستخدمين آخرين.", "empty_column.home.public_timeline": "الخيط العام", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.", "empty_column.public": "لا يوجد شيء هنا ! قم بتحرير شيء ما بشكل عام، أو اتبع مستخدمين آخرين في الخوادم المثيلة الأخرى لملء خيط المحادثات العام.", "follow_request.authorize": "ترخيص", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "عرض الترقيات", "home.column_settings.show_replies": "عرض الردود", "home.settings": "إعدادات العمود", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "إغلاق", "lightbox.next": "التالي", "lightbox.previous": "العودة", "loading_indicator.label": "تحميل ...", "media_gallery.toggle_visible": "عرض / إخفاء", "missing_indicator.label": "تعذر العثور عليه", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "الحسابات المحجوبة", "navigation_bar.community_timeline": "الخيط العام المحلي", "navigation_bar.edit_profile": "تعديل الملف الشخصي", "navigation_bar.favourites": "المفضلة", "navigation_bar.follow_requests": "طلبات المتابعة", "navigation_bar.info": "معلومات إضافية", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "خروج", "navigation_bar.mutes": "الحسابات المكتومة", "navigation_bar.pins": "التبويقات المثبتة", @@ -204,6 +229,7 @@ "tabs_bar.home": "الرئيسية", "tabs_bar.local_timeline": "المحلي", "tabs_bar.notifications": "الإخطارات", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "إسحب ثم أفلت للرفع", "upload_button.label": "إضافة وسائط", "upload_form.description": "وصف للمعاقين بصريا", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index da2372cff..82f092648 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -7,17 +7,22 @@ "account.followers": "Последователи", "account.follows": "Следвам", "account.follows_you": "Твой последовател", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Media", "account.mention": "Споменаване", + "account.moved_to": "{name} has moved to:", "account.mute": "Mute @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Публикации", "account.report": "Report @{name}", "account.requested": "В очакване на одобрение", "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Не блокирай", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Не следвай", "account.unmute": "Unmute @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "View full profile", "boost_modal.combo": "You can press {combo} to skip this next time", "bundle_column_error.body": "Something went wrong while loading this component.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "There is nothing in this hashtag yet.", "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", "empty_column.home.public_timeline": "the public timeline", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", "follow_request.authorize": "Authorize", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", "home.settings": "Column settings", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Затвори", "lightbox.next": "Next", "lightbox.previous": "Previous", "loading_indicator.label": "Зареждане...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Blocked users", "navigation_bar.community_timeline": "Local timeline", "navigation_bar.edit_profile": "Редактирай профил", "navigation_bar.favourites": "Favourites", "navigation_bar.follow_requests": "Follow requests", "navigation_bar.info": "Extended information", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Излизане", "navigation_bar.mutes": "Muted users", "navigation_bar.pins": "Pinned toots", @@ -204,6 +229,7 @@ "tabs_bar.home": "Начало", "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Известия", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", "upload_button.label": "Добави медия", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index af732921d..baa9432de 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -7,17 +7,22 @@ "account.followers": "Seguidors", "account.follows": "Seguint", "account.follows_you": "et segueix", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Media", "account.mention": "Esmentar @{name}", + "account.moved_to": "{name} s'ha mogut a:", "account.mute": "Silenciar @{name}", + "account.mute_notifications": "Notificacions desactivades de @{name}", "account.posts": "Publicacions", "account.report": "Informe @{name}", - "account.requested": "Esperant aprovació", + "account.requested": "Esperant aprovació. Clic per a cancel·lar la petició de seguiment", "account.share": "Compartir el perfil de @{name}", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Desbloquejar @{name}", "account.unblock_domain": "Mostra {domain}", "account.unfollow": "Deixar de seguir", "account.unmute": "Treure silenci de @{name}", + "account.unmute_notifications": "Activar notificacions de @{name}", "account.view_full_profile": "Veure el perfil complet", "boost_modal.combo": "Pots premer {combo} per saltar-te això el proper cop", "bundle_column_error.body": "S'ha produït un error en carregar aquest component.", @@ -51,7 +56,7 @@ "compose_form.publish_loud": "{publish}!", "compose_form.sensitive": "Marcar multimèdia com a sensible", "compose_form.spoiler": "Amagar text darrera l'advertència", - "compose_form.spoiler_placeholder": "Advertència de contingut", + "compose_form.spoiler_placeholder": "Escriu l'advertència aquí", "confirmation_modal.cancel": "Cancel·lar", "confirmations.block.confirm": "Bloquejar", "confirmations.block.message": "Estàs segur que vols bloquejar {name}?", @@ -64,7 +69,7 @@ "confirmations.unfollow.confirm": "Deixar de seguir", "confirmations.unfollow.message": "Estàs segur que vols deixar de seguir {name}?", "embed.instructions": "Incrusta aquest estat al lloc web copiant el codi a continuació.", - "embed.preview": "A continuació s'explica com:", + "embed.preview": "Aquí tenim quin aspecte tindrá:", "emoji_button.activity": "Activitat", "emoji_button.custom": "Personalitzat", "emoji_button.flags": "Flags", @@ -83,6 +88,7 @@ "empty_column.hashtag": "Encara no hi ha res amb aquesta etiqueta.", "empty_column.home": "Encara no segueixes ningú. Visita {public} o fes cerca per començar i conèixer altres usuaris.", "empty_column.home.public_timeline": "la línia de temps pública", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "Encara no tens notificacions. Interactua amb altres per iniciar la conversa.", "empty_column.public": "No hi ha res aquí! Escriu alguna cosa públicament o segueix manualment usuaris d'altres instàncies per omplir-ho", "follow_request.authorize": "Autoritzar", @@ -95,21 +101,40 @@ "home.column_settings.advanced": "Avançat", "home.column_settings.basic": "Bàsic", "home.column_settings.filter_regex": "Filtrar per expressió regular", - "home.column_settings.show_reblogs": "Mostrar 'boosts'", + "home.column_settings.show_reblogs": "Mostrar impulsos", "home.column_settings.show_replies": "Mostrar respostes", "home.settings": "Ajustos de columna", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Tancar", "lightbox.next": "Següent", "lightbox.previous": "Anterior", "loading_indicator.label": "Carregant...", "media_gallery.toggle_visible": "Alternar visibilitat", "missing_indicator.label": "No trobat", + "mute_modal.hide_notifications": "Amagar notificacions d'aquest usuari?", "navigation_bar.blocks": "Usuaris bloquejats", "navigation_bar.community_timeline": "Línia de temps Local", "navigation_bar.edit_profile": "Editar perfil", "navigation_bar.favourites": "Favorits", "navigation_bar.follow_requests": "Sol·licituds de seguiment", "navigation_bar.info": "Informació addicional", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Tancar sessió", "navigation_bar.mutes": "Usuaris silenciats", "navigation_bar.pins": "Toots fixats", @@ -127,7 +152,7 @@ "notifications.column_settings.mention": "Mencions:", "notifications.column_settings.push": "Push notificacions", "notifications.column_settings.push_meta": "Aquest dispositiu", - "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.reblog": "Impulsos:", "notifications.column_settings.show": "Mostrar en la columna", "notifications.column_settings.sound": "Reproduïr so", "onboarding.done": "Fet", @@ -159,11 +184,11 @@ "privacy.public.short": "Públic", "privacy.unlisted.long": "No publicar en línies de temps públiques", "privacy.unlisted.short": "No llistat", - "relative_time.days": "fa {number} jorns", + "relative_time.days": "fa {number} dies", "relative_time.hours": "fa {number} hores", "relative_time.just_now": "ara", - "relative_time.minutes": "fa {number} minutes", - "relative_time.seconds": "fa {number} segondes", + "relative_time.minutes": "fa {number} minuts", + "relative_time.seconds": "fa {number} segons", "reply_indicator.cancel": "Cancel·lar", "report.placeholder": "Comentaris addicionals", "report.submit": "Enviar", @@ -187,7 +212,7 @@ "status.mute_conversation": "Silenciar conversació", "status.open": "Ampliar aquest estat", "status.pin": "Fixat en el perfil", - "status.reblog": "Boost", + "status.reblog": "Impuls", "status.reblogged_by": "{name} ha retootejat", "status.reply": "Respondre", "status.replyAll": "Respondre al tema", @@ -204,6 +229,7 @@ "tabs_bar.home": "Inici", "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notificacions", + "ui.beforeunload": "El vostre esborrany es perdrà si sortiu de Mastodon.", "upload_area.title": "Arrossega i deixa anar per carregar", "upload_button.label": "Afegir multimèdia", "upload_form.description": "Descriure els problemes visuals", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 283a2946f..343528899 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -7,17 +7,22 @@ "account.followers": "Folgende", "account.follows": "Folgt", "account.follows_you": "Folgt dir", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Medien", "account.mention": "@{name} erwähnen", + "account.moved_to": "{name} has moved to:", "account.mute": "@{name} stummschalten", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Beiträge", "account.report": "@{name} melden", "account.requested": "Warte auf Erlaubnis. Klicke zum Abbrechen", "account.share": "Profil von @{name} teilen", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "@{name} entblocken", "account.unblock_domain": "{domain} wieder anzeigen", "account.unfollow": "Entfolgen", "account.unmute": "@{name} nicht mehr stummschalten", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "Vollständiges Profil anzeigen", "boost_modal.combo": "Du kannst {combo} drücken, um dies beim nächsten Mal zu überspringen", "bundle_column_error.body": "Etwas ist beim Laden schiefgelaufen.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "Unter diesem Hashtag gibt es noch nichts.", "empty_column.home": "Deine Startseite ist leer! Besuche {public} oder nutze die Suche, um loszulegen und andere Leute zu finden.", "empty_column.home.public_timeline": "die öffentliche Zeitleiste", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um ins Gespräch zu kommen.", "empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Profilen von anderen Instanzen, um die Zeitleiste aufzufüllen", "follow_request.authorize": "Erlauben", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen", "home.column_settings.show_replies": "Antworten anzeigen", "home.settings": "Spalteneinstellungen", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Schließen", "lightbox.next": "Weiter", "lightbox.previous": "Zurück", "loading_indicator.label": "Wird geladen …", "media_gallery.toggle_visible": "Sichtbarkeit umschalten", "missing_indicator.label": "Nicht gefunden", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Blockierte Profile", "navigation_bar.community_timeline": "Lokale Zeitleiste", "navigation_bar.edit_profile": "Profil bearbeiten", "navigation_bar.favourites": "Favoriten", "navigation_bar.follow_requests": "Folgeanfragen", "navigation_bar.info": "Über diese Instanz", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Abmelden", "navigation_bar.mutes": "Stummgeschaltete Profile", "navigation_bar.pins": "Angeheftete Beiträge", @@ -204,6 +229,7 @@ "tabs_bar.home": "Startseite", "tabs_bar.local_timeline": "Lokal", "tabs_bar.notifications": "Mitteilungen", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Zum Hochladen hereinziehen", "upload_button.label": "Mediendatei hinzufügen", "upload_form.description": "Für Menschen mit Sehbehinderung beschreiben", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index f400b283f..be751589e 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -29,6 +29,14 @@ { "defaultMessage": "Unmute @{name}", "id": "account.unmute" + }, + { + "defaultMessage": "Mute notifications from @{name}", + "id": "account.mute_notifications" + }, + { + "defaultMessage": "Unmute notifications from @{name}", + "id": "account.unmute_notifications" } ], "path": "app/javascript/mastodon/components/account.json" @@ -284,16 +292,8 @@ "id": "confirmations.block.confirm" }, { - "defaultMessage": "Mute", - "id": "confirmations.mute.confirm" - }, - { "defaultMessage": "Are you sure you want to block {name}?", "id": "confirmations.block.message" - }, - { - "defaultMessage": "Are you sure you want to mute {name}?", - "id": "confirmations.mute.message" } ], "path": "app/javascript/mastodon/containers/status_container.json" @@ -310,6 +310,15 @@ { "descriptors": [ { + "defaultMessage": "{name} has moved to:", + "id": "account.moved_to" + } + ], + "path": "app/javascript/mastodon/features/account_timeline/components/moved_note.json" + }, + { + "descriptors": [ + { "defaultMessage": "Unfollow", "id": "confirmations.unfollow.confirm" }, @@ -318,10 +327,6 @@ "id": "confirmations.block.confirm" }, { - "defaultMessage": "Mute", - "id": "confirmations.mute.confirm" - }, - { "defaultMessage": "Hide entire domain", "id": "confirmations.domain_block.confirm" }, @@ -334,10 +339,6 @@ "id": "confirmations.block.message" }, { - "defaultMessage": "Are you sure you want to mute {name}?", - "id": "confirmations.mute.message" - }, - { "defaultMessage": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", "id": "confirmations.domain_block.message" } @@ -399,6 +400,14 @@ "id": "account.unblock_domain" }, { + "defaultMessage": "Hide boosts from @{name}", + "id": "account.hide_reblogs" + }, + { + "defaultMessage": "Show boosts from @{name}", + "id": "account.show_reblogs" + }, + { "defaultMessage": "Information below may reflect the user's profile incompletely.", "id": "account.disclaimer_full" }, @@ -849,6 +858,10 @@ "id": "navigation_bar.pins" }, { + "defaultMessage": "Keyboard shortcuts", + "id": "navigation_bar.keyboard_shortcuts" + }, + { "defaultMessage": "FAQ", "id": "getting_started.faq" }, @@ -925,6 +938,88 @@ { "descriptors": [ { + "defaultMessage": "Keyboard Shortcuts", + "id": "keyboard_shortcuts.heading" + }, + { + "defaultMessage": "Hotkey", + "id": "keyboard_shortcuts.hotkey" + }, + { + "defaultMessage": "Description", + "id": "keyboard_shortcuts.description" + }, + { + "defaultMessage": "to reply", + "id": "keyboard_shortcuts.reply" + }, + { + "defaultMessage": "to mention author", + "id": "keyboard_shortcuts.mention" + }, + { + "defaultMessage": "to favourite", + "id": "keyboard_shortcuts.favourite" + }, + { + "defaultMessage": "to boost", + "id": "keyboard_shortcuts.boost" + }, + { + "defaultMessage": "to open status", + "id": "keyboard_shortcuts.enter" + }, + { + "defaultMessage": "to move up in the list", + "id": "keyboard_shortcuts.up" + }, + { + "defaultMessage": "to move down in the list", + "id": "keyboard_shortcuts.down" + }, + { + "defaultMessage": "to focus a status in one of the columns", + "id": "keyboard_shortcuts.column" + }, + { + "defaultMessage": "to focus the compose textarea", + "id": "keyboard_shortcuts.compose" + }, + { + "defaultMessage": "to start a brand new toot", + "id": "keyboard_shortcuts.toot" + }, + { + "defaultMessage": "to navigate back", + "id": "keyboard_shortcuts.back" + }, + { + "defaultMessage": "to focus search", + "id": "keyboard_shortcuts.search" + }, + { + "defaultMessage": "to un-focus compose textarea/search", + "id": "keyboard_shortcuts.unfocus" + }, + { + "defaultMessage": "to display this legend", + "id": "keyboard_shortcuts.legend" + } + ], + "path": "app/javascript/mastodon/features/keyboard_shortcuts/index.json" + }, + { + "descriptors": [ + { + "defaultMessage": "There is nothing in this list yet.", + "id": "empty_column.list" + } + ], + "path": "app/javascript/mastodon/features/list_timeline/index.json" + }, + { + "descriptors": [ + { "defaultMessage": "Muted users", "id": "column.mutes" } @@ -1210,6 +1305,27 @@ { "descriptors": [ { + "defaultMessage": "Are you sure you want to mute {name}?", + "id": "confirmations.mute.message" + }, + { + "defaultMessage": "Hide notifications from this user?", + "id": "mute_modal.hide_notifications" + }, + { + "defaultMessage": "Cancel", + "id": "confirmation_modal.cancel" + }, + { + "defaultMessage": "Mute", + "id": "confirmations.mute.confirm" + } + ], + "path": "app/javascript/mastodon/features/ui/components/mute_modal.json" + }, + { + "descriptors": [ + { "defaultMessage": "Home", "id": "column.home" }, @@ -1362,6 +1478,15 @@ { "descriptors": [ { + "defaultMessage": "Your draft will be lost if you leave Mastodon.", + "id": "ui.beforeunload" + } + ], + "path": "app/javascript/mastodon/features/ui/index.json" + }, + { + "descriptors": [ + { "defaultMessage": "Play", "id": "video.play" }, diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 1d0bbcee5..4e0b838b0 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -7,17 +7,22 @@ "account.followers": "Followers", "account.follows": "Follows", "account.follows_you": "Follows you", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Media", "account.mention": "Mention @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "Mute @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Posts", "account.report": "Report @{name}", "account.requested": "Awaiting approval. Click to cancel follow request", "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Unblock @{name}", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Unfollow", "account.unmute": "Unmute @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "View full profile", "boost_modal.combo": "You can press {combo} to skip this next time", "bundle_column_error.body": "Something went wrong while loading this component.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "There is nothing in this hashtag yet.", "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", "empty_column.home.public_timeline": "the public timeline", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", "follow_request.authorize": "Authorize", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", "home.settings": "Column settings", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Close", "lightbox.next": "Next", "lightbox.previous": "Previous", "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Blocked users", "navigation_bar.community_timeline": "Local timeline", "navigation_bar.edit_profile": "Edit profile", "navigation_bar.favourites": "Favourites", "navigation_bar.follow_requests": "Follow requests", "navigation_bar.info": "About this instance", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Logout", "navigation_bar.mutes": "Muted users", "navigation_bar.pins": "Pinned toots", @@ -204,6 +229,7 @@ "tabs_bar.home": "Home", "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notifications", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", "upload_button.label": "Add media", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 3f67a8fff..f9e32ba5e 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -7,17 +7,22 @@ "account.followers": "Sekvantoj", "account.follows": "Sekvatoj", "account.follows_you": "Sekvas vin", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Sonbildaĵoj", "account.mention": "Mencii @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "Silentigi @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Mesaĝoj", "account.report": "Signali @{name}", "account.requested": "Atendas aprobon", "account.share": "Diskonigi la profilon de @{name}", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Malbloki @{name}", "account.unblock_domain": "Malkaŝi {domain}", "account.unfollow": "Ne plus sekvi", "account.unmute": "Malsilentigi @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "Vidi plenan profilon", "boost_modal.combo": "La proksiman fojon, premu {combo} por pasigi", "bundle_column_error.body": "Io malfunkciis ŝargante tiun ĉi komponanton.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "Ĝise, neniu enhavo estas asociita kun tiu kradvorto.", "empty_column.home": "Via hejma tempolinio estas malplena! Vizitu {public} aŭ uzu la serĉilon por renkonti aliajn uzantojn.", "empty_column.home.public_timeline": "la publika tempolinio", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "Vi dume ne havas sciigojn. Interagi kun aliajn uzantojn por komenci la konversacion.", "empty_column.public": "Estas nenio ĉi tie! Publike skribu ion, aŭ mane sekvu uzantojn de aliaj instancoj por plenigi la publikan tempolinion.", "follow_request.authorize": "Akcepti", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Montri diskonigojn", "home.column_settings.show_replies": "Montri respondojn", "home.settings": "Agordoj de la kolumno", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Fermi", "lightbox.next": "Malantaŭa", "lightbox.previous": "Antaŭa", "loading_indicator.label": "Ŝarganta…", "media_gallery.toggle_visible": "Baskuli videblecon", "missing_indicator.label": "Ne trovita", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Blokitaj uzantoj", "navigation_bar.community_timeline": "Loka tempolinio", "navigation_bar.edit_profile": "Redakti la profilon", "navigation_bar.favourites": "Favoritaj", "navigation_bar.follow_requests": "Abonpetoj", "navigation_bar.info": "Plia informo", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Elsaluti", "navigation_bar.mutes": "Silentigitaj uzantoj", "navigation_bar.pins": "Alpinglitaj pepoj", @@ -204,6 +229,7 @@ "tabs_bar.home": "Hejmo", "tabs_bar.local_timeline": "Loka tempolinio", "tabs_bar.notifications": "Sciigoj", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Algliti por alŝuti", "upload_button.label": "Aldoni sonbildaĵon", "upload_form.description": "Priskribi por la misvidantaj", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 6e8e94700..38ba291e4 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -7,17 +7,22 @@ "account.followers": "Seguidores", "account.follows": "Sigue", "account.follows_you": "Te sigue", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Media", "account.mention": "Mencionar a @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "Silenciar a @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Publicaciones", "account.report": "Reportar a @{name}", "account.requested": "Esperando aprobación", "account.share": "Compartir el perfil de @{name}", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Desbloquear a @{name}", "account.unblock_domain": "Mostrar a {domain}", "account.unfollow": "Dejar de seguir", "account.unmute": "Dejar de silenciar a @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "Ver perfil completo", "boost_modal.combo": "Puedes presionar {combo} para saltear este aviso la próxima vez", "bundle_column_error.body": "Algo salió mal al cargar este componente.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "No hay nada en este hashtag aún.", "empty_column.home": "No estás siguiendo a nadie aún. Visita {public} o haz búsquedas para empezar y conocer gente nueva.", "empty_column.home.public_timeline": "la línea de tiempo pública", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "No tienes ninguna notificación aún. Interactúa con otros para empezar una conversación.", "empty_column.public": "¡No hay nada aquí! Escribe algo públicamente, o sigue usuarios de otras instancias manualmente para llenarlo.", "follow_request.authorize": "Autorizar", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Mostrar retoots", "home.column_settings.show_replies": "Mostrar respuestas", "home.settings": "Ajustes de columna", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Cerrar", "lightbox.next": "Siguiente", "lightbox.previous": "Anterior", "loading_indicator.label": "Cargando…", "media_gallery.toggle_visible": "Cambiar visibilidad", "missing_indicator.label": "No encontrado", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Usuarios bloqueados", "navigation_bar.community_timeline": "Historia local", "navigation_bar.edit_profile": "Editar perfil", "navigation_bar.favourites": "Favoritos", "navigation_bar.follow_requests": "Solicitudes para seguirte", "navigation_bar.info": "Información adicional", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Cerrar sesión", "navigation_bar.mutes": "Usuarios silenciados", "navigation_bar.pins": "Toots fijados", @@ -204,6 +229,7 @@ "tabs_bar.home": "Inicio", "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notificaciones", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Arrastra y suelta para subir", "upload_button.label": "Subir multimedia", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index 995d1b5ae..dafb52bb5 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -7,17 +7,22 @@ "account.followers": "پیگیران", "account.follows": "پی میگیرد", "account.follows_you": "پیگیر شماست", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "رسانه", "account.mention": "نامبردن از @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "بیصدا کردن @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "نوشتهها", "account.report": "گزارش @{name}", "account.requested": "در انتظار پذیرش", "account.share": "همرسانی نمایهٔ @{name}", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "رفع انسداد @{name}", "account.unblock_domain": "رفع پنهانسازی از {domain}", "account.unfollow": "پایان پیگیری", "account.unmute": "باصدا کردن @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "نمایش نمایهٔ کامل", "boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید", "bundle_column_error.body": "هنگام بازکردن این بخش خطایی رخ داد.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "هنوز هیچ چیزی با این هشتگ نیست.", "empty_column.home": "شما هنوز پیگیر کسی نیستید. {public} را ببینید یا چیزی را جستجو کنید تا کاربران دیگر را ببینید.", "empty_column.home.public_timeline": "فهرست نوشتههای همهجا", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "هنوز هیچ اعلانی ندارید. به نوشتههای دیگران واکنش نشان دهید تا گفتگو آغاز شود.", "empty_column.public": "اینجا هنوز چیزی نیست! خودتان چیزی بنویسید یا کاربران دیگر را پی بگیرید تا اینجا پر شود", "follow_request.authorize": "اجازه دهید", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "نمایش بازبوقها", "home.column_settings.show_replies": "نمایش پاسخها", "home.settings": "تنظیمات ستون", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "بستن", "lightbox.next": "بعدی", "lightbox.previous": "قبلی", "loading_indicator.label": "بارگیری...", "media_gallery.toggle_visible": "تغییر پیدایی", "missing_indicator.label": "پیدا نشد", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "کاربران مسدودشده", "navigation_bar.community_timeline": "نوشتههای محلی", "navigation_bar.edit_profile": "ویرایش نمایه", "navigation_bar.favourites": "پسندیدهها", "navigation_bar.follow_requests": "درخواستهای پیگیری", "navigation_bar.info": "اطلاعات تکمیلی", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "خروج", "navigation_bar.mutes": "کاربران بیصداشده", "navigation_bar.pins": "نوشتههای ثابت", @@ -204,6 +229,7 @@ "tabs_bar.home": "خانه", "tabs_bar.local_timeline": "محلی", "tabs_bar.notifications": "اعلانها", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "برای بارگذاری به اینجا بکشید", "upload_button.label": "افزودن تصویر", "upload_form.description": "نوشتهٔ توضیحی برای کمبینایان و نابینایان", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index af08be5d1..e3739eb68 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -7,17 +7,22 @@ "account.followers": "Seuraajia", "account.follows": "Seuraa", "account.follows_you": "Seuraa sinua", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Media", "account.mention": "Mainitse @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "Mute @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Postit", "account.report": "Report @{name}", "account.requested": "Odottaa hyväksyntää", "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Salli @{name}", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Lopeta seuraaminen", "account.unmute": "Unmute @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "View full profile", "boost_modal.combo": "You can press {combo} to skip this next time", "bundle_column_error.body": "Something went wrong while loading this component.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "There is nothing in this hashtag yet.", "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", "empty_column.home.public_timeline": "the public timeline", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", "follow_request.authorize": "Authorize", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", "home.settings": "Column settings", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Sulje", "lightbox.next": "Next", "lightbox.previous": "Previous", "loading_indicator.label": "Ladataan...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Blocked users", "navigation_bar.community_timeline": "Paikallinen aikajana", "navigation_bar.edit_profile": "Muokkaa profiilia", "navigation_bar.favourites": "Favourites", "navigation_bar.follow_requests": "Follow requests", "navigation_bar.info": "Extended information", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Kirjaudu ulos", "navigation_bar.mutes": "Muted users", "navigation_bar.pins": "Pinned toots", @@ -204,6 +229,7 @@ "tabs_bar.home": "Koti", "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Ilmoitukset", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", "upload_button.label": "Lisää mediaa", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 219bf4da1..e4cabf52c 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -7,17 +7,22 @@ "account.followers": "Abonné⋅e⋅s", "account.follows": "Abonnements", "account.follows_you": "Vous suit", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Média", "account.mention": "Mentionner", + "account.moved_to": "{name} has moved to:", "account.mute": "Masquer", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Statuts", "account.report": "Signaler", "account.requested": "Invitation envoyée", "account.share": "Partager le profil de @{name}", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Débloquer", "account.unblock_domain": "Ne plus masquer {domain}", "account.unfollow": "Ne plus suivre", "account.unmute": "Ne plus masquer", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "Afficher le profil complet", "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois", "bundle_column_error.body": "Une erreur s’est produite lors du chargement de ce composant.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "Il n’y a encore aucun contenu associé à ce hashtag", "empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres utilisateur⋅ice⋅s.", "empty_column.home.public_timeline": "le fil public", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateur⋅ice⋅s pour débuter la conversation.", "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateur⋅ice⋅s d’autres instances pour remplir le fil public.", "follow_request.authorize": "Accepter", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Afficher les partages", "home.column_settings.show_replies": "Afficher les réponses", "home.settings": "Paramètres de la colonne", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Fermer", "lightbox.next": "Suivant", "lightbox.previous": "Précédent", "loading_indicator.label": "Chargement…", "media_gallery.toggle_visible": "Modifier la visibilité", "missing_indicator.label": "Non trouvé", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Comptes bloqués", "navigation_bar.community_timeline": "Fil public local", "navigation_bar.edit_profile": "Modifier le profil", "navigation_bar.favourites": "Favoris", "navigation_bar.follow_requests": "Demandes de suivi", "navigation_bar.info": "Plus d’informations", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Déconnexion", "navigation_bar.mutes": "Comptes masqués", "navigation_bar.pins": "Pouets épinglés", @@ -204,6 +229,7 @@ "tabs_bar.home": "Accueil", "tabs_bar.local_timeline": "Fil public local", "tabs_bar.notifications": "Notifications", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Glissez et déposez pour envoyer", "upload_button.label": "Joindre un média", "upload_form.description": "Décrire pour les malvoyants", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index a260f0968..fdebdf92a 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -7,17 +7,22 @@ "account.followers": "עוקבים", "account.follows": "נעקבים", "account.follows_you": "במעקב אחריך", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "מדיה", "account.mention": "אזכור של @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "להשתיק את @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "הודעות", "account.report": "לדווח על @{name}", "account.requested": "בהמתנה לאישור", "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "הסרת חסימה מעל @{name}", "account.unblock_domain": "הסר חסימה מקהילת {domain}", "account.unfollow": "הפסקת מעקב", "account.unmute": "הפסקת השתקת @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "View full profile", "boost_modal.combo": "ניתן להקיש {combo} כדי לדלג בפעם הבאה", "bundle_column_error.body": "Something went wrong while loading this component.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "אין כלום בהאשתג הזה עדיין.", "empty_column.home": "אף אחד לא במעקב עדיין. אפשר לבקר ב{public} או להשתמש בחיפוש כדי להתחיל ולהכיר חצוצרנים אחרים.", "empty_column.home.public_timeline": "ציר זמן בין-קהילתי", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "אין התראות עדיין. יאללה, הגיע הזמן להתחיל להתערבב!", "empty_column.public": "אין פה כלום! כדי למלא את הטור הזה אפשר לכתוב משהו, או להתחיל לעקוב אחרי אנשים מקהילות אחרות.", "follow_request.authorize": "קבלה", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "הצגת הדהודים", "home.column_settings.show_replies": "הצגת תגובות", "home.settings": "הגדרות טור", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "סגירה", "lightbox.next": "Next", "lightbox.previous": "Previous", "loading_indicator.label": "טוען...", "media_gallery.toggle_visible": "נראה\\בלתי נראה", "missing_indicator.label": "לא נמצא", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "חסימות", "navigation_bar.community_timeline": "ציר זמן מקומי", "navigation_bar.edit_profile": "עריכת פרופיל", "navigation_bar.favourites": "חיבובים", "navigation_bar.follow_requests": "בקשות מעקב", "navigation_bar.info": "מידע נוסף", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "יציאה", "navigation_bar.mutes": "השתקות", "navigation_bar.pins": "Pinned toots", @@ -204,6 +229,7 @@ "tabs_bar.home": "בבית", "tabs_bar.local_timeline": "ציר זמן מקומי", "tabs_bar.notifications": "התראות", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "ניתן להעלות על ידי Drag & drop", "upload_button.label": "הוספת מדיה", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index 6ac7fc3b4..497917b9c 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -7,17 +7,22 @@ "account.followers": "Sljedbenici", "account.follows": "Slijedi", "account.follows_you": "te slijedi", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Media", "account.mention": "Spomeni @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "Utišaj @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Postovi", "account.report": "Prijavi @{name}", "account.requested": "Čeka pristanak", "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Deblokiraj @{name}", "account.unblock_domain": "Poništi sakrivanje {domain}", "account.unfollow": "Prestani slijediti", "account.unmute": "Poništi utišavanje @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "View full profile", "boost_modal.combo": "Možeš pritisnuti {combo} kako bi ovo preskočio sljedeći put", "bundle_column_error.body": "Something went wrong while loading this component.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "Još ne postoji ništa s ovim hashtagom.", "empty_column.home": "Još ne slijediš nikoga. Posjeti {public} ili koristi tražilicu kako bi počeo i upoznao druge korisnike.", "empty_column.home.public_timeline": "javni timeline", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "Još nemaš notifikacija. Komuniciraj sa drugima kako bi započeo razgovor.", "empty_column.public": "Ovdje nema ništa! Napiši nešto javno, ili ručno slijedi korisnike sa drugih instanci kako bi popunio", "follow_request.authorize": "Autoriziraj", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Pokaži boostove", "home.column_settings.show_replies": "Pokaži odgovore", "home.settings": "Postavke Stupca", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Zatvori", "lightbox.next": "Next", "lightbox.previous": "Previous", "loading_indicator.label": "Učitavam...", "media_gallery.toggle_visible": "Preklopi vidljivost", "missing_indicator.label": "Nije nađen", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Blokirani korisnici", "navigation_bar.community_timeline": "Lokalni timeline", "navigation_bar.edit_profile": "Uredi profil", "navigation_bar.favourites": "Favoriti", "navigation_bar.follow_requests": "Zahtjevi za slijeđenje", "navigation_bar.info": "Više informacija", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Odjavi se", "navigation_bar.mutes": "Utišani korisnici", "navigation_bar.pins": "Pinned toots", @@ -204,6 +229,7 @@ "tabs_bar.home": "Dom", "tabs_bar.local_timeline": "Lokalno", "tabs_bar.notifications": "Notifikacije", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Povuci i spusti kako bi uploadao", "upload_button.label": "Dodaj media", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 5892e606e..6df09aea9 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -7,17 +7,22 @@ "account.followers": "Követők", "account.follows": "Követve", "account.follows_you": "Követnek téged", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Media", "account.mention": "Említés", + "account.moved_to": "{name} has moved to:", "account.mute": "Mute @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Posts", "account.report": "Report @{name}", "account.requested": "Awaiting approval", "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Blokkolás levétele", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Követés abbahagyása", "account.unmute": "Unmute @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "View full profile", "boost_modal.combo": "You can press {combo} to skip this next time", "bundle_column_error.body": "Something went wrong while loading this component.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "There is nothing in this hashtag yet.", "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", "empty_column.home.public_timeline": "the public timeline", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", "follow_request.authorize": "Authorize", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", "home.settings": "Column settings", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Bezárás", "lightbox.next": "Next", "lightbox.previous": "Previous", "loading_indicator.label": "Betöltés...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Blocked users", "navigation_bar.community_timeline": "Local timeline", "navigation_bar.edit_profile": "Profil szerkesztése", "navigation_bar.favourites": "Favourites", "navigation_bar.follow_requests": "Follow requests", "navigation_bar.info": "Extended information", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Kijelentkezés", "navigation_bar.mutes": "Muted users", "navigation_bar.pins": "Pinned toots", @@ -204,6 +229,7 @@ "tabs_bar.home": "Kezdőlap", "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notifications", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", "upload_button.label": "Média hozzáadása", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index f73ef0e19..66aff71f0 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -7,17 +7,22 @@ "account.followers": "Pengikut", "account.follows": "Mengikuti", "account.follows_you": "Mengikuti anda", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Media", "account.mention": "Balasan @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "Bisukan @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Postingan", "account.report": "Laporkan @{name}", "account.requested": "Menunggu persetujuan", "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Hapus blokir @{name}", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Berhenti mengikuti", "account.unmute": "Berhenti membisukan @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "View full profile", "boost_modal.combo": "Anda dapat menekan {combo} untuk melewati ini", "bundle_column_error.body": "Something went wrong while loading this component.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "Tidak ada apapun dalam hashtag ini.", "empty_column.home": "Anda sedang tidak mengikuti siapapun. Kunjungi {public} atau gunakan pencarian untuk memulai dan bertemu pengguna lain.", "empty_column.home.public_timeline": "linimasa publik", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "Anda tidak memiliki notifikasi apapun. Berinteraksi dengan orang lain untuk memulai percakapan.", "empty_column.public": "Tidak ada apapun disini! Tulis sesuatu, atau ikuti pengguna lain dari server lain untuk mengisinya secara manual", "follow_request.authorize": "Izinkan", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Tampilkan Boost", "home.column_settings.show_replies": "Tampilkan balasan", "home.settings": "Pengaturan kolom", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Tutup", "lightbox.next": "Next", "lightbox.previous": "Previous", "loading_indicator.label": "Tunggu sebentar...", "media_gallery.toggle_visible": "Tampil/Sembunyikan", "missing_indicator.label": "Tidak ditemukan", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Pengguna diblokir", "navigation_bar.community_timeline": "Linimasa lokal", "navigation_bar.edit_profile": "Ubah profil", "navigation_bar.favourites": "Favorit", "navigation_bar.follow_requests": "Permintaan mengikuti", "navigation_bar.info": "Informasi selengkapnya", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Keluar", "navigation_bar.mutes": "Pengguna dibisukan", "navigation_bar.pins": "Pinned toots", @@ -204,6 +229,7 @@ "tabs_bar.home": "Beranda", "tabs_bar.local_timeline": "Lokal", "tabs_bar.notifications": "Notifikasi", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Seret & lepaskan untuk mengunggah", "upload_button.label": "Tambahkan media", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index 53371bece..673f895fe 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -7,17 +7,22 @@ "account.followers": "Sequanti", "account.follows": "Sequas", "account.follows_you": "Sequas tu", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Media", "account.mention": "Mencionar @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "Celar @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Mesaji", "account.report": "Denuncar @{name}", "account.requested": "Vartante aprobo", "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Desblokusar @{name}", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Ne plus sequar", "account.unmute": "Ne plus celar @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "View full profile", "boost_modal.combo": "Tu povas presar sur {combo} por omisar co en la venonta foyo", "bundle_column_error.body": "Something went wrong while loading this component.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "Esas ankore nulo en ta gretovorto.", "empty_column.home": "Tu sequas ankore nulu. Vizitez {public} od uzez la serchilo por komencar e renkontrar altra uzeri.", "empty_column.home.public_timeline": "la publika tempolineo", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "Tu havas ankore nula savigo. Komunikez kun altri por debutar la konverso.", "empty_column.public": "Esas nulo hike! Skribez ulo publike, o manuale sequez uzeri de altra instaluri por plenigar ol.", "follow_request.authorize": "Yurizar", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Montrar repeti", "home.column_settings.show_replies": "Montrar respondi", "home.settings": "Aranji di la kolumno", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Klozar", "lightbox.next": "Next", "lightbox.previous": "Previous", "loading_indicator.label": "Kargante...", "media_gallery.toggle_visible": "Chanjar videbleso", "missing_indicator.label": "Ne trovita", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Blokusita uzeri", "navigation_bar.community_timeline": "Lokala tempolineo", "navigation_bar.edit_profile": "Modifikar profilo", "navigation_bar.favourites": "Favorati", "navigation_bar.follow_requests": "Demandi di sequado", "navigation_bar.info": "Detaloza informi", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Ekirar", "navigation_bar.mutes": "Celita uzeri", "navigation_bar.pins": "Pinned toots", @@ -204,6 +229,7 @@ "tabs_bar.home": "Hemo", "tabs_bar.local_timeline": "Lokala", "tabs_bar.notifications": "Savigi", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Tranar faligar por kargar", "upload_button.label": "Adjuntar kontenajo", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 3873d797e..3d2d47471 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -7,17 +7,22 @@ "account.followers": "Seguaci", "account.follows": "Segue", "account.follows_you": "Ti segue", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Media", "account.mention": "Menziona @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "Silenzia @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Posts", "account.report": "Segnala @{name}", "account.requested": "In attesa di approvazione", "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Sblocca @{name}", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Non seguire", "account.unmute": "Non silenziare @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "View full profile", "boost_modal.combo": "Puoi premere {combo} per saltare questo passaggio la prossima volta", "bundle_column_error.body": "Something went wrong while loading this component.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "Non c'è ancora nessun post con questo hashtag.", "empty_column.home": "Non stai ancora seguendo nessuno. Visita {public} o usa la ricerca per incontrare nuove persone.", "empty_column.home.public_timeline": "la timeline pubblica", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "Non hai ancora nessuna notifica. Interagisci con altri per iniziare conversazioni.", "empty_column.public": "Qui non c'è nulla! Scrivi qualcosa pubblicamente, o aggiungi utenti da altri server per riempire questo spazio.", "follow_request.authorize": "Autorizza", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Mostra post condivisi", "home.column_settings.show_replies": "Mostra risposte", "home.settings": "Impostazioni colonna", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Chiudi", "lightbox.next": "Next", "lightbox.previous": "Previous", "loading_indicator.label": "Carico...", "media_gallery.toggle_visible": "Imposta visibilità", "missing_indicator.label": "Non trovato", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Utenti bloccati", "navigation_bar.community_timeline": "Timeline locale", "navigation_bar.edit_profile": "Modifica profilo", "navigation_bar.favourites": "Apprezzati", "navigation_bar.follow_requests": "Richieste di amicizia", "navigation_bar.info": "Informazioni estese", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Logout", "navigation_bar.mutes": "Utenti silenziati", "navigation_bar.pins": "Pinned toots", @@ -204,6 +229,7 @@ "tabs_bar.home": "Home", "tabs_bar.local_timeline": "Locale", "tabs_bar.notifications": "Notifiche", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Trascina per caricare", "upload_button.label": "Aggiungi file multimediale", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index fb6d11ebe..388dd57e8 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -7,17 +7,22 @@ "account.followers": "フォロワー", "account.follows": "フォロー", "account.follows_you": "フォローされています", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "メディア", "account.mention": "返信", + "account.moved_to": "{name}さんは引っ越しました:", "account.mute": "ミュート", + "account.mute_notifications": "@{name}からの通知を受け取る", "account.posts": "投稿", "account.report": "通報", "account.requested": "承認待ち", "account.share": "@{name} のプロフィールを共有する", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "ブロック解除", "account.unblock_domain": "{domain}を表示", "account.unfollow": "フォロー解除", "account.unmute": "ミュート解除", + "account.unmute_notifications": "@{name}からの通知を受け取らない", "account.view_full_profile": "全ての情報を見る", "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。", "bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。", @@ -83,6 +88,7 @@ "empty_column.hashtag": "このハッシュタグはまだ使われていません。", "empty_column.home": "まだ誰もフォローしていません。{public}を見に行くか、検索を使って他のユーザーを見つけましょう。", "empty_column.home.public_timeline": "連合タイムライン", + "empty_column.list": "このリストにはまだなにもありません。", "empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。", "empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!", "follow_request.authorize": "許可", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "ブースト表示", "home.column_settings.show_replies": "返信表示", "home.settings": "カラム設定", + "keyboard_shortcuts.back": "戻る", + "keyboard_shortcuts.boost": "ブースト", + "keyboard_shortcuts.column": "左からn番目のカラム内最新トゥートに移動", + "keyboard_shortcuts.compose": "トゥート入力欄に移動", + "keyboard_shortcuts.description": "説明", + "keyboard_shortcuts.down": "カラム内一つ下に移動", + "keyboard_shortcuts.enter": "トゥートの詳細を表示", + "keyboard_shortcuts.favourite": "お気に入り", + "keyboard_shortcuts.heading": "キーボードショートカット一覧", + "keyboard_shortcuts.hotkey": "ホットキー", + "keyboard_shortcuts.legend": "この一覧を表示", + "keyboard_shortcuts.mention": "メンション", + "keyboard_shortcuts.reply": "返信", + "keyboard_shortcuts.search": "検索欄に移動", + "keyboard_shortcuts.toot": "新規トゥート", + "keyboard_shortcuts.unfocus": "トゥート入力欄・検索欄から離れる", + "keyboard_shortcuts.up": "カラム内一つ上に移動", "lightbox.close": "閉じる", "lightbox.next": "次", "lightbox.previous": "前", "loading_indicator.label": "読み込み中...", "media_gallery.toggle_visible": "表示切り替え", "missing_indicator.label": "見つかりません", + "mute_modal.hide_notifications": "このユーザーからの通知を隠しますか?", "navigation_bar.blocks": "ブロックしたユーザー", "navigation_bar.community_timeline": "ローカルタイムライン", "navigation_bar.edit_profile": "プロフィールを編集", "navigation_bar.favourites": "お気に入り", "navigation_bar.follow_requests": "フォローリクエスト", "navigation_bar.info": "このインスタンスについて", + "navigation_bar.keyboard_shortcuts": "キーボードショートカット", "navigation_bar.logout": "ログアウト", "navigation_bar.mutes": "ミュートしたユーザー", "navigation_bar.pins": "固定されたトゥート", @@ -204,6 +229,7 @@ "tabs_bar.home": "ホーム", "tabs_bar.local_timeline": "ローカル", "tabs_bar.notifications": "通知", + "ui.beforeunload": "Mastodonから離れるとあなたのドラフトは失われます。", "upload_area.title": "ドラッグ&ドロップでアップロード", "upload_button.label": "メディアを追加", "upload_form.description": "視覚障害者のための説明", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 2919928af..03028fc31 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -7,17 +7,22 @@ "account.followers": "팔로워", "account.follows": "팔로우", "account.follows_you": "날 팔로우합니다", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "미디어", "account.mention": "답장", + "account.moved_to": "{name} has moved to:", "account.mute": "뮤트", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "포스트", "account.report": "신고", "account.requested": "승인 대기 중", "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "차단 해제", "account.unblock_domain": "{domain} 숨김 해제", "account.unfollow": "팔로우 해제", "account.unmute": "뮤트 해제", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "전체 프로필 보기", "boost_modal.combo": "다음부터 {combo}를 누르면 이 과정을 건너뛸 수 있습니다.", "bundle_column_error.body": "Something went wrong while loading this component.", @@ -83,11 +88,12 @@ "empty_column.hashtag": "이 해시태그는 아직 사용되지 않았습니다.", "empty_column.home": "아직 아무도 팔로우 하고 있지 않습니다. {public}를 보러 가거나, 검색하여 다른 사용자를 찾아 보세요.", "empty_column.home.public_timeline": "연합 타임라인", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "아직 알림이 없습니다. 다른 사람과 대화를 시작해 보세요!", "empty_column.public": "여기엔 아직 아무 것도 없습니다! 공개적으로 무언가 포스팅하거나, 다른 인스턴스 유저를 팔로우 해서 가득 채워보세요!", "follow_request.authorize": "허가", "follow_request.reject": "거부", - "getting_started.appsshort": "어플리케이션", + "getting_started.appsshort": "애플리케이션", "getting_started.faq": "자주 있는 질문", "getting_started.heading": "시작", "getting_started.open_source_notice": "Mastodon은 오픈 소스 소프트웨어입니다. 누구나 GitHub({github})에서 개발에 참여하거나, 문제를 보고할 수 있습니다.", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "부스트 표시", "home.column_settings.show_replies": "답글 표시", "home.settings": "컬럼 설정", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "닫기", "lightbox.next": "Next", "lightbox.previous": "Previous", "loading_indicator.label": "불러오는 중...", "media_gallery.toggle_visible": "표시 전환", "missing_indicator.label": "찾을 수 없습니다", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "차단한 사용자", "navigation_bar.community_timeline": "로컬 타임라인", "navigation_bar.edit_profile": "프로필 편집", "navigation_bar.favourites": "즐겨찾기", "navigation_bar.follow_requests": "팔로우 요청", "navigation_bar.info": "이 인스턴스에 대해서", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "로그아웃", "navigation_bar.mutes": "뮤트 중인 사용자", "navigation_bar.pins": "고정된 툿", @@ -145,7 +170,7 @@ "onboarding.page_six.github": "Mastodon는 오픈 소스 소프트웨어입니다. 버그 보고나 기능 추가 요청, 기여는 {github}에서 할 수 있습니다.", "onboarding.page_six.guidelines": "커뮤니티 가이드라인", "onboarding.page_six.read_guidelines": "{guidelines}을 확인하는 것을 잊지 마세요.", - "onboarding.page_six.various_app": "다양한 모바일 어플리케이션", + "onboarding.page_six.various_app": "다양한 모바일 애플리케이션", "onboarding.page_three.profile": "[프로필 편집] 에서 자기 소개나 이름을 변경할 수 있습니다. 또한 다른 설정도 변경할 수 있습니다.", "onboarding.page_three.search": "검색 바에서 {illustration} 나 {introductions} 와 같이 특정 해시태그가 달린 포스트를 보거나, 사용자를 찾을 수 있습니다.", "onboarding.page_two.compose": "이 폼에서 포스팅 할 수 있습니다. 이미지나 공개 범위 설정, 스포일러 경고 설정은 아래 아이콘으로 설정할 수 있습니다.", @@ -204,6 +229,7 @@ "tabs_bar.home": "홈", "tabs_bar.local_timeline": "로컬", "tabs_bar.notifications": "알림", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "드래그 & 드롭으로 업로드", "upload_button.label": "미디어 추가", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index d0727a24d..218709899 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -7,17 +7,22 @@ "account.followers": "Volgers", "account.follows": "Volgt", "account.follows_you": "Volgt jou", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Media", "account.mention": "Vermeld @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "Negeer @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Toots", "account.report": "Rapporteer @{name}", "account.requested": "Wacht op goedkeuring. Klik om volgverzoek te annuleren.", "account.share": "Profiel van @{name} delen", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Deblokkeer @{name}", "account.unblock_domain": "{domain} niet meer negeren", "account.unfollow": "Ontvolgen", "account.unmute": "@{name} niet meer negeren", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "Volledig profiel tonen", "boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan", "bundle_column_error.body": "Tijdens het laden van dit onderdeel is er iets fout gegaan.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "Er is nog niks te vinden onder deze hashtag.", "empty_column.home": "Jij volgt nog niemand. Bezoek {public} of gebruik het zoekvenster om andere mensen te ontmoeten.", "empty_column.home.public_timeline": "de globale tijdlijn", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "Je hebt nog geen meldingen. Heb interactie met andere mensen om het gesprek aan te gaan.", "empty_column.public": "Er is hier helemaal niks! Toot iets in het openbaar of volg mensen van andere Mastodon-servers om het te vullen.", "follow_request.authorize": "Goedkeuren", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Boosts tonen", "home.column_settings.show_replies": "Reacties tonen", "home.settings": "Kolom-instellingen", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Sluiten", "lightbox.next": "Volgende", "lightbox.previous": "Vorige", "loading_indicator.label": "Laden…", "media_gallery.toggle_visible": "Media wel/niet tonen", "missing_indicator.label": "Niet gevonden", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Geblokkeerde gebruikers", "navigation_bar.community_timeline": "Lokale tijdlijn", "navigation_bar.edit_profile": "Profiel bewerken", "navigation_bar.favourites": "Favorieten", "navigation_bar.follow_requests": "Volgverzoeken", "navigation_bar.info": "Uitgebreide informatie", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Afmelden", "navigation_bar.mutes": "Genegeerde gebruikers", "navigation_bar.pins": "Vastgezette toots", @@ -204,6 +229,7 @@ "tabs_bar.home": "Start", "tabs_bar.local_timeline": "Lokaal", "tabs_bar.notifications": "Meldingen", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Hierin slepen om te uploaden", "upload_button.label": "Media toevoegen", "upload_form.description": "Omschrijf dit voor mensen met een visuele beperking", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index d74ae0091..234589d34 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -7,17 +7,22 @@ "account.followers": "Følgere", "account.follows": "Følger", "account.follows_you": "Følger deg", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Media", "account.mention": "Nevn @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "Demp @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Innlegg", "account.report": "Rapportér @{name}", "account.requested": "Venter på godkjennelse", "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Avblokker @{name}", "account.unblock_domain": "Vis {domain}", "account.unfollow": "Avfølg", "account.unmute": "Avdemp @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "View full profile", "boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang", "bundle_column_error.body": "Something went wrong while loading this component.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "Det er ingenting i denne hashtagen ennå.", "empty_column.home": "Du har ikke fulgt noen ennå. Besøk {publlic} eller bruk søk for å komme i gang og møte andre brukere.", "empty_column.home.public_timeline": "en offentlig tidslinje", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "Du har ingen varsler ennå. Kommuniser med andre for å begynne samtalen.", "empty_column.public": "Det er ingenting her! Skriv noe offentlig, eller følg brukere manuelt fra andre instanser for å fylle den opp", "follow_request.authorize": "Autorisér", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Vis fremhevinger", "home.column_settings.show_replies": "Vis svar", "home.settings": "Kolonneinnstillinger", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Lukk", "lightbox.next": "Next", "lightbox.previous": "Previous", "loading_indicator.label": "Laster...", "media_gallery.toggle_visible": "Veksle synlighet", "missing_indicator.label": "Ikke funnet", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Blokkerte brukere", "navigation_bar.community_timeline": "Lokal tidslinje", "navigation_bar.edit_profile": "Rediger profil", "navigation_bar.favourites": "Likt", "navigation_bar.follow_requests": "Følgeforespørsler", "navigation_bar.info": "Utvidet informasjon", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Logg ut", "navigation_bar.mutes": "Dempede brukere", "navigation_bar.pins": "Pinned toots", @@ -204,6 +229,7 @@ "tabs_bar.home": "Hjem", "tabs_bar.local_timeline": "Lokal", "tabs_bar.notifications": "Varslinger", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Dra og slipp for å laste opp", "upload_button.label": "Legg til media", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index d826423b5..6e68d6810 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -7,17 +7,22 @@ "account.followers": "Seguidors", "account.follows": "Abonaments", "account.follows_you": "Vos sèc", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Mèdias", "account.mention": "Mencionar @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "Rescondre @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Estatuts", "account.report": "Senhalar @{name}", "account.requested": "Invitacion mandada. Clicatz per anullar.", "account.share": "Partejar lo perfil a @{name}", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Desblocar @{name}", "account.unblock_domain": "Desblocar {domain}", "account.unfollow": "Quitar de sègre", "account.unmute": "Quitar de rescondre @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "Veire lo perfil complet", "boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven", "bundle_column_error.body": "Quicòm a fach meuca pendent lo cargament d’aqueste compausant.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag", "empty_column.home": "Vòstre flux d’acuèlh es void. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.", "empty_column.home.public_timeline": "lo flux public", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualqu’un per començar una conversacion.", "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public.", "follow_request.authorize": "Autorizar", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Mostrar los partatges", "home.column_settings.show_replies": "Mostrar las responsas", "home.settings": "Paramètres de la colomna", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Tampar", "lightbox.next": "Seguent", "lightbox.previous": "Precedent", "loading_indicator.label": "Cargament…", "media_gallery.toggle_visible": "Modificar la visibilitat", "missing_indicator.label": "Pas trobat", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Personas blocadas", "navigation_bar.community_timeline": "Flux public local", "navigation_bar.edit_profile": "Modificar lo perfil", "navigation_bar.favourites": "Favorits", "navigation_bar.follow_requests": "Demandas d'abonament", "navigation_bar.info": "Mai informacions", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Desconnexion", "navigation_bar.mutes": "Personas rescondudas", "navigation_bar.pins": "Tuts penjats", @@ -204,6 +229,7 @@ "tabs_bar.home": "Acuèlh", "tabs_bar.local_timeline": "Flux public local", "tabs_bar.notifications": "Notificacions", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Lisatz e depausatz per mandar", "upload_button.label": "Ajustar un mèdia", "upload_form.description": "Descripcion pels mal vesents", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index b23a5e69f..907999940 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -7,17 +7,22 @@ "account.followers": "Śledzący", "account.follows": "Śledzeni", "account.follows_you": "Śledzi Cię", + "account.hide_reblogs": "Ukryj podbicia od @{name}", "account.media": "Media", "account.mention": "Wspomnij o @{name}", + "account.moved_to": "{name} przeniósł się do:", "account.mute": "Wycisz @{name}", + "account.mute_notifications": "Wycisz powiadomienia o @{name}", "account.posts": "Wpisy", "account.report": "Zgłoś @{name}", "account.requested": "Oczekująca prośba, kliknij aby anulować", "account.share": "Udostępnij profil @{name}", + "account.show_reblogs": "Pokazuj podbicia od @{name}", "account.unblock": "Odblokuj @{name}", "account.unblock_domain": "Odblokuj domenę {domain}", "account.unfollow": "Przestań śledzić", "account.unmute": "Cofnij wyciszenie @{name}", + "account.unmute_notifications": "Cofnij wyciszenie powiadomień od @{name}", "account.view_full_profile": "Wyświetl pełny profil", "boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem", "bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "Nie ma wpisów oznaczonych tym hashtagiem. Możesz napisać pierwszy!", "empty_column.home": "Nie śledzisz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.", "empty_column.home.public_timeline": "publiczna oś czasu", + "empty_column.list": "Nie ma nic na tej liście.", "empty_column.notifications": "Nie masz żadnych powiadomień. Rozpocznij interakcje z innymi użytkownikami.", "empty_column.public": "Tu nic nie ma! Napisz coś publicznie, lub dodaj ludzi z innych instancji, aby to wyświetlić.", "follow_request.authorize": "Autoryzuj", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Pokazuj podbicia", "home.column_settings.show_replies": "Pokazuj odpowiedzi", "home.settings": "Ustawienia kolumny", + "keyboard_shortcuts.back": "aby cofnąć się", + "keyboard_shortcuts.boost": "aby podbić wpis", + "keyboard_shortcuts.column": "aby przejść do wpisu z jednej z kolumn", + "keyboard_shortcuts.compose": "aby przejść do pola tworzenia wpisu", + "keyboard_shortcuts.description": "Opis", + "keyboard_shortcuts.down": "aby przejść na dół listy", + "keyboard_shortcuts.enter": "aby otworzyć wpis", + "keyboard_shortcuts.favourite": "aby dodać do ulubionych", + "keyboard_shortcuts.heading": "Skróty klawiszowe", + "keyboard_shortcuts.hotkey": "Klawisz", + "keyboard_shortcuts.legend": "aby wyświetlić tą legendę", + "keyboard_shortcuts.mention": "aby wspomnieć o autorze", + "keyboard_shortcuts.reply": "aby odpowiedzieć", + "keyboard_shortcuts.search": "aby przejść do pola wyszukiwania", + "keyboard_shortcuts.toot": "aby utworzyć nowy wpis", + "keyboard_shortcuts.unfocus": "aby opuścić pole wyszukiwania/pisania", + "keyboard_shortcuts.up": "aby przejść na górę listy", "lightbox.close": "Zamknij", "lightbox.next": "Następne", "lightbox.previous": "Poprzednie", "loading_indicator.label": "Ładowanie…", "media_gallery.toggle_visible": "Przełącz widoczność", "missing_indicator.label": "Nie znaleziono", + "mute_modal.hide_notifications": "Chcesz ukryć powiadomienia od tego użytkownika?", "navigation_bar.blocks": "Zablokowani użytkownicy", "navigation_bar.community_timeline": "Lokalna oś czasu", "navigation_bar.edit_profile": "Edytuj profil", "navigation_bar.favourites": "Ulubione", "navigation_bar.follow_requests": "Prośby o śledzenie", "navigation_bar.info": "Szczegółowe informacje", + "navigation_bar.keyboard_shortcuts": "Skróty klawiszowe", "navigation_bar.logout": "Wyloguj", "navigation_bar.mutes": "Wyciszeni użytkownicy", "navigation_bar.pins": "Przypięte wpisy", @@ -204,6 +229,7 @@ "tabs_bar.home": "Strona główna", "tabs_bar.local_timeline": "Lokalne", "tabs_bar.notifications": "Powiadomienia", + "ui.beforeunload": "Utracisz tworzony wpis, jeżeli opuścisz Mastodona.", "upload_area.title": "Przeciągnij i upuść aby wysłać", "upload_button.label": "Dodaj zawartość multimedialną", "upload_form.description": "Wprowadź opis dla niewidomych i niedowidzących", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index a04d1cc31..ba0d2093e 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -7,17 +7,22 @@ "account.followers": "Seguidores", "account.follows": "Segue", "account.follows_you": "Segue você", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Mídia", "account.mention": "Mencionar @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "Silenciar @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Posts", "account.report": "Denunciar @{name}", "account.requested": "Aguardando aprovação. Clique para cancelar a solicitação.", "account.share": "Compartilhar perfil de @{name}", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Desbloquear @{name}", "account.unblock_domain": "Desbloquear {domain}", "account.unfollow": "Deixar de seguir", "account.unmute": "Não silenciar @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "Ver perfil completo", "boost_modal.combo": "Você pode pressionar {combo} para ignorar este diálogo na próxima vez", "bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "Ainda não há qualquer conteúdo com essa hashtag", "empty_column.home": "Você ainda não segue usuário algo. Visite a timeline {public} ou use o buscador para procurar e conhecer outros usuários.", "empty_column.home.public_timeline": "global", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "Você ainda não possui notificações. Interaja com outros usuários para começar a conversar!", "empty_column.public": "Não há nada aqui! Escreva algo publicamente ou siga manualmente usuários de outras instâncias.", "follow_request.authorize": "Autorizar", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Mostrar compartilhamentos", "home.column_settings.show_replies": "Mostrar as respostas", "home.settings": "Configurações de colunas", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Fechar", "lightbox.next": "Próximo", "lightbox.previous": "Anterior", "loading_indicator.label": "Carregando...", "media_gallery.toggle_visible": "Esconder/Mostrar", "missing_indicator.label": "Não encontrado", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Usuários bloqueados", "navigation_bar.community_timeline": "Local", "navigation_bar.edit_profile": "Editar perfil", "navigation_bar.favourites": "Favoritos", "navigation_bar.follow_requests": "Seguidores pendentes", "navigation_bar.info": "Mais informações", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Sair", "navigation_bar.mutes": "Usuários silenciados", "navigation_bar.pins": "Postagens fixadas", @@ -204,6 +229,7 @@ "tabs_bar.home": "Página inicial", "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notificações", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Arraste e solte para enviar", "upload_button.label": "Adicionar mídia", "upload_form.description": "Descreva a imagem para deficientes visuais", diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json index 9ae140df9..f349bd191 100644 --- a/app/javascript/mastodon/locales/pt.json +++ b/app/javascript/mastodon/locales/pt.json @@ -7,17 +7,22 @@ "account.followers": "Seguidores", "account.follows": "Segue", "account.follows_you": "É teu seguidor", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Media", "account.mention": "Mencionar @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "Silenciar @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Posts", "account.report": "Denunciar @{name}", "account.requested": "A aguardar aprovação", "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Não bloquear @{name}", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Deixar de seguir", "account.unmute": "Não silenciar @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "View full profile", "boost_modal.combo": "Pode clicar {combo} para não voltar a ver", "bundle_column_error.body": "Something went wrong while loading this component.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "Ainda não existe qualquer conteúdo com essa hashtag", "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.", "empty_column.home.public_timeline": "global", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.", "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.", "follow_request.authorize": "Autorizar", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Mostrar as partilhas", "home.column_settings.show_replies": "Mostrar as respostas", "home.settings": "Parâmetros da listagem", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Fechar", "lightbox.next": "Next", "lightbox.previous": "Previous", "loading_indicator.label": "Carregando...", "media_gallery.toggle_visible": "Esconder/Mostrar", "missing_indicator.label": "Não encontrado", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Utilizadores bloqueados", "navigation_bar.community_timeline": "Local", "navigation_bar.edit_profile": "Editar perfil", "navigation_bar.favourites": "Favoritos", "navigation_bar.follow_requests": "Seguidores pendentes", "navigation_bar.info": "Mais informações", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Sair", "navigation_bar.mutes": "Utilizadores silenciados", "navigation_bar.pins": "Pinned toots", @@ -204,6 +229,7 @@ "tabs_bar.home": "Home", "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notificações", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Arraste e solte para enviar", "upload_button.label": "Adicionar media", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index 65bc5b374..df52eca14 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -7,17 +7,22 @@ "account.followers": "Подписаны", "account.follows": "Подписки", "account.follows_you": "Подписан(а) на Вас", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Медиаконтент", "account.mention": "Упомянуть", + "account.moved_to": "{name} has moved to:", "account.mute": "Заглушить", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Посты", "account.report": "Пожаловаться", "account.requested": "Ожидает подтверждения", "account.share": "Поделиться профилем @{name}", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Разблокировать", "account.unblock_domain": "Разблокировать {domain}", "account.unfollow": "Отписаться", "account.unmute": "Снять глушение", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "Показать полный профиль", "boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз", "bundle_column_error.body": "Что-то пошло не так при загрузке этого компонента.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "Статусов с таким хэштегом еще не существует.", "empty_column.home": "Пока Вы ни на кого не подписаны. Полистайте {public} или используйте поиск, чтобы освоиться и завести новые знакомства.", "empty_column.home.public_timeline": "публичные ленты", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "У Вас еще нет уведомлений. Заведите знакомство с другими пользователями, чтобы начать разговор.", "empty_column.public": "Здесь ничего нет! Опубликуйте что-нибудь или подпишитесь на пользователей с других узлов, чтобы заполнить ленту.", "follow_request.authorize": "Авторизовать", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Показывать продвижения", "home.column_settings.show_replies": "Показывать ответы", "home.settings": "Настройки колонки", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Закрыть", "lightbox.next": "Далее", "lightbox.previous": "Назад", "loading_indicator.label": "Загрузка...", "media_gallery.toggle_visible": "Показать/скрыть", "missing_indicator.label": "Не найдено", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Список блокировки", "navigation_bar.community_timeline": "Локальная лента", "navigation_bar.edit_profile": "Изменить профиль", "navigation_bar.favourites": "Понравившееся", "navigation_bar.follow_requests": "Запросы на подписку", "navigation_bar.info": "Об узле", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Выйти", "navigation_bar.mutes": "Список глушения", "navigation_bar.pins": "Pinned toots", @@ -204,6 +229,7 @@ "tabs_bar.home": "Главная", "tabs_bar.local_timeline": "Локальная", "tabs_bar.notifications": "Уведомления", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Перетащите сюда, чтобы загрузить", "upload_button.label": "Добавить медиаконтент", "upload_form.description": "Описать для людей с нарушениями зрения", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index 70beb70f7..5f887ef34 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -7,17 +7,22 @@ "account.followers": "Följare", "account.follows": "Följer", "account.follows_you": "Följer dig", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Media", "account.mention": "Nämna @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "Tysta @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Inlägg", "account.report": "Rapportera @{name}", "account.requested": "Inväntar godkännande. Klicka för att avbryta följförfrågan", "account.share": "Dela @{name}'s profil", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Avblockera @{name}", "account.unblock_domain": "Ta fram {domain}", "account.unfollow": "Sluta följa", "account.unmute": "Ta bort tystad @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "Visa hela profilen", "boost_modal.combo": "Du kan trycka {combo} för att slippa denna nästa gång", "bundle_column_error.body": "Något gick fel när du laddade denna komponent.", @@ -82,8 +87,8 @@ "empty_column.community": "Den lokala tidslinjen är tom. Skriv något offentligt för att få bollen att rulla!", "empty_column.hashtag": "Det finns inget i denna hashtag ännu.", "empty_column.home": "Din hemma-tidslinje är tom! Besök {public} eller använd sökning för att komma igång och träffa andra användare.", - "empty_column.home.inactivity": "Ditt hemmafeed är tomt. Om du har varit inaktiv ett tag kommer det att regenereras för dig snart.", "empty_column.home.public_timeline": "den publika tidslinjen", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "Du har inga meddelanden än. Interagera med andra för att starta konversationen.", "empty_column.public": "Det finns inget här! Skriv något offentligt, eller följ manuellt användarna från andra instanser för att fylla på det", "follow_request.authorize": "Godkänn", @@ -99,22 +104,40 @@ "home.column_settings.show_reblogs": "Visa knuffar", "home.column_settings.show_replies": "Visa svar", "home.settings": "Kolumninställningar", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Stäng", "lightbox.next": "Nästa", "lightbox.previous": "Tidigare", "loading_indicator.label": "Laddar...", "media_gallery.toggle_visible": "Växla synlighet", "missing_indicator.label": "Hittades inte", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Blockerade användare", "navigation_bar.community_timeline": "Lokal tidslinje", "navigation_bar.edit_profile": "Redigera profil", "navigation_bar.favourites": "Favoriter", "navigation_bar.follow_requests": "Följförfrågningar", "navigation_bar.info": "Om denna instans", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Logga ut", "navigation_bar.mutes": "Tystade användare", "navigation_bar.pins": "Nålade inlägg (toots)", - "navigation_bar.preferences": "Inställningar", "navigation_bar.public_timeline": "Förenad tidslinje", "notification.favourite": "{name} favoriserade din status", @@ -161,6 +184,11 @@ "privacy.public.short": "Publik", "privacy.unlisted.long": "Skicka inte till publik tidslinje", "privacy.unlisted.short": "Olistad", + "relative_time.days": "{number}d", + "relative_time.hours": "{number}h", + "relative_time.just_now": "now", + "relative_time.minutes": "{number}m", + "relative_time.seconds": "{number}s", "reply_indicator.cancel": "Ångra", "report.placeholder": "Ytterligare kommentarer", "report.submit": "Skicka", @@ -180,6 +208,7 @@ "status.load_more": "Ladda fler", "status.media_hidden": "Media dold", "status.mention": "Omnämn @{name}", + "status.more": "More", "status.mute_conversation": "Tysta konversation", "status.open": "Utvidga denna status", "status.pin": "Fäst i profil", @@ -200,6 +229,7 @@ "tabs_bar.home": "Hem", "tabs_bar.local_timeline": "Lokal", "tabs_bar.notifications": "Meddelanden", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Dra & släpp för att ladda upp", "upload_button.label": "Lägg till media", "upload_form.description": "Beskriv för synskadade", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index db3f3dd0d..7b2b13202 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -7,17 +7,22 @@ "account.followers": "Followers", "account.follows": "Follows", "account.follows_you": "Follows you", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Media", "account.mention": "Mention @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "Mute @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Posts", "account.report": "Report @{name}", "account.requested": "Awaiting approval", "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Unblock @{name}", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Unfollow", "account.unmute": "Unmute @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "View full profile", "boost_modal.combo": "You can press {combo} to skip this next time", "bundle_column_error.body": "Something went wrong while loading this component.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "There is nothing in this hashtag yet.", "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", "empty_column.home.public_timeline": "the public timeline", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", "follow_request.authorize": "Authorize", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", "home.settings": "Column settings", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Close", "lightbox.next": "Next", "lightbox.previous": "Previous", "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Blocked users", "navigation_bar.community_timeline": "Local timeline", "navigation_bar.edit_profile": "Edit profile", "navigation_bar.favourites": "Favourites", "navigation_bar.follow_requests": "Follow requests", "navigation_bar.info": "About this instance", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Logout", "navigation_bar.mutes": "Muted users", "navigation_bar.pins": "Pinned toots", @@ -204,6 +229,7 @@ "tabs_bar.home": "Home", "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notifications", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", "upload_button.label": "Add media", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index cdd3581da..dd5a96bdb 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -7,17 +7,22 @@ "account.followers": "Takipçiler", "account.follows": "Takip ettikleri", "account.follows_you": "Seni takip ediyor", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Media", "account.mention": "Bahset @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "Sustur @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Gönderiler", "account.report": "Rapor et @{name}", "account.requested": "Onay bekleniyor", "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Engeli kaldır @{name}", "account.unblock_domain": "Unhide {domain}", "account.unfollow": "Takipten vazgeç", "account.unmute": "Sesi aç @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "View full profile", "boost_modal.combo": "Bir dahaki sefere {combo} tuşuna basabilirsiniz", "bundle_column_error.body": "Something went wrong while loading this component.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "Henüz bu hashtag’e sahip hiçbir gönderi yok.", "empty_column.home": "Henüz kimseyi takip etmiyorsunuz. {public} ziyaret edebilir veya arama kısmını kullanarak diğer kullanıcılarla iletişime geçebilirsiniz.", "empty_column.home.public_timeline": "herkese açık zaman tüneli", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "Henüz hiçbir bildiriminiz yok. Diğer insanlarla sobhet edebilmek için etkileşime geçebilirsiniz.", "empty_column.public": "Burada hiçbir gönderi yok! Herkese açık bir şeyler yazın, veya diğer sunucudaki insanları takip ederek bu alanın dolmasını sağlayın", "follow_request.authorize": "Yetkilendir", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Boost edilenleri göster", "home.column_settings.show_replies": "Cevapları göster", "home.settings": "Kolon ayarları", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Kapat", "lightbox.next": "Next", "lightbox.previous": "Previous", "loading_indicator.label": "Yükleniyor...", "media_gallery.toggle_visible": "Görünürlüğü değiştir", "missing_indicator.label": "Bulunamadı", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Engellenen kullanıcılar", "navigation_bar.community_timeline": "Yerel zaman tüneli", "navigation_bar.edit_profile": "Profili düzenle", "navigation_bar.favourites": "Favoriler", "navigation_bar.follow_requests": "Takip istekleri", "navigation_bar.info": "Genişletilmiş bilgi", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Çıkış", "navigation_bar.mutes": "Sessize alınmış kullanıcılar", "navigation_bar.pins": "Pinned toots", @@ -204,6 +229,7 @@ "tabs_bar.home": "Ana sayfa", "tabs_bar.local_timeline": "Yerel", "tabs_bar.notifications": "Bildirimler", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Upload için sürükle bırak yapınız", "upload_button.label": "Görsel ekle", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 930529f15..6206cd65b 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -7,17 +7,22 @@ "account.followers": "Підписники", "account.follows": "Підписки", "account.follows_you": "Підписаний(-а) на Вас", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "Медія", "account.mention": "Згадати", + "account.moved_to": "{name} has moved to:", "account.mute": "Заглушити", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "Пости", "account.report": "Поскаржитися", "account.requested": "Очікує підтвердження", "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "Розблокувати", "account.unblock_domain": "Розблокувати {domain}", "account.unfollow": "Відписатися", "account.unmute": "Зняти глушення", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "View full profile", "boost_modal.combo": "Ви можете натиснути {combo}, щоб пропустити це наступного разу", "bundle_column_error.body": "Something went wrong while loading this component.", @@ -83,6 +88,7 @@ "empty_column.hashtag": "Дописів з цим хештегом поки не існує.", "empty_column.home": "Ви поки ні на кого не підписані. Погортайте {public}, або скористуйтесь пошуком, щоб освоїтися та познайомитися з іншими користувачами.", "empty_column.home.public_timeline": "публічні стрічки", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "У вас ще немає сповіщень. Переписуйтесь з іншими користувачами, щоб почати розмову.", "empty_column.public": "Тут поки нічого немає! Опублікуйте щось, або вручну підпишіться на користувачів інших інстанцій, щоб заповнити стрічку.", "follow_request.authorize": "Авторизувати", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "Показувати передмухи", "home.column_settings.show_replies": "Показувати відповіді", "home.settings": "Налаштування колонок", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Закрити", "lightbox.next": "Next", "lightbox.previous": "Previous", "loading_indicator.label": "Завантаження...", "media_gallery.toggle_visible": "Показати/приховати", "missing_indicator.label": "Не знайдено", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Заблоковані користувачі", "navigation_bar.community_timeline": "Локальна стрічка", "navigation_bar.edit_profile": "Редагувати профіль", "navigation_bar.favourites": "Вподобане", "navigation_bar.follow_requests": "Запити на підписку", "navigation_bar.info": "Про інстанцію", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "Вийти", "navigation_bar.mutes": "Заглушені користувачі", "navigation_bar.pins": "Pinned toots", @@ -204,6 +229,7 @@ "tabs_bar.home": "Головна", "tabs_bar.local_timeline": "Локальна", "tabs_bar.notifications": "Сповіщення", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Перетягніть сюди, щоб завантажити", "upload_button.label": "Додати медіаконтент", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/whitelist_sv.json b/app/javascript/mastodon/locales/whitelist_sv.json new file mode 100644 index 000000000..0d4f101c7 --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_sv.json @@ -0,0 +1,2 @@ +[ +] diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 0f4bbb7c1..c23868cda 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -1,23 +1,28 @@ { "account.block": "屏蔽 @{name}", - "account.block_domain": "隐藏一切来自 {domain} 的嘟文", + "account.block_domain": "隐藏来自 {domain} 的内容", "account.disclaimer_full": "此处显示的信息可能不是全部内容。", "account.edit_profile": "修改个人资料", "account.follow": "关注", "account.followers": "关注者", "account.follows": "正在关注", "account.follows_you": "关注了你", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "媒体", "account.mention": "提及 @{name}", - "account.mute": "静音 @{name}", + "account.moved_to": "{name} 已经迁移到:", + "account.mute": "隐藏 @{name}", + "account.mute_notifications": "隐藏来自 @{name} 的通知", "account.posts": "嘟文", "account.report": "举报 @{name}", "account.requested": "正在等待对方同意。点击以取消发送关注请求", "account.share": "分享 @{name} 的个人资料", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "不再屏蔽 @{name}", - "account.unblock_domain": "不再隐藏 {domain}", + "account.unblock_domain": "不再隐藏来自 {domain} 的内容", "account.unfollow": "取消关注", - "account.unmute": "不再静音 @{name}", + "account.unmute": "不再隐藏 @{name}", + "account.unmute_notifications": "不再隐藏来自 @{name} 的通知", "account.view_full_profile": "查看完整资料", "boost_modal.combo": "下次按住 {combo} 即可跳过此提示", "bundle_column_error.body": "载入组件出错。", @@ -31,7 +36,7 @@ "column.favourites": "收藏过的嘟文", "column.follow_requests": "关注请求", "column.home": "主页", - "column.mutes": "被静音的用户", + "column.mutes": "被隐藏的用户", "column.notifications": "通知", "column.pins": "置顶嘟文", "column.public": "跨站公共时间轴", @@ -57,10 +62,10 @@ "confirmations.block.message": "想好了,真的要屏蔽 {name}?", "confirmations.delete.confirm": "删除", "confirmations.delete.message": "想好了,真的要删除这条嘟文?", - "confirmations.domain_block.confirm": "隐藏整个网站", - "confirmations.domain_block.message": "你真的真的确定要隐藏整个 {domain}?多数情况下,屏蔽或静音几个特定的用户就应该能满足你的需要了。", - "confirmations.mute.confirm": "静音", - "confirmations.mute.message": "想好了,真的要静音 {name}?", + "confirmations.domain_block.confirm": "隐藏整个网站的内容", + "confirmations.domain_block.message": "你真的真的确定要隐藏所有来自 {domain} 的内容吗?多数情况下,屏蔽或隐藏几个特定的用户就应该能满足你的需要了。", + "confirmations.mute.confirm": "隐藏", + "confirmations.mute.message": "想好了,真的要隐藏 {name}?", "confirmations.unfollow.confirm": "取消关注", "confirmations.unfollow.message": "确定要取消关注 {name} 吗?", "embed.instructions": "要在你的网站上嵌入这条嘟文,请复制以下代码。", @@ -83,6 +88,7 @@ "empty_column.hashtag": "这个话题标签下暂时没有内容。", "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。", "empty_column.home.public_timeline": "公共时间轴", + "empty_column.list": "这个列表中暂时没有内容。", "empty_column.notifications": "你还没有收到过通知信息,快向其他用户搭讪吧。", "empty_column.public": "这里神马都没有!写一些公开的嘟文,或者关注其他实例的用户,这里就会有嘟文出现了哦!", "follow_request.authorize": "同意", @@ -98,20 +104,39 @@ "home.column_settings.show_reblogs": "显示转嘟", "home.column_settings.show_replies": "显示回复", "home.settings": "栏目设置", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "关闭", "lightbox.next": "下一步", "lightbox.previous": "上一步", "loading_indicator.label": "加载中……", "media_gallery.toggle_visible": "切换显示/隐藏", "missing_indicator.label": "找不到内容", + "mute_modal.hide_notifications": "隐藏来自这个用户的通知", "navigation_bar.blocks": "被屏蔽的用户", "navigation_bar.community_timeline": "本站时间轴", "navigation_bar.edit_profile": "修改个人资料", "navigation_bar.favourites": "收藏的内容", "navigation_bar.follow_requests": "关注请求", "navigation_bar.info": "关于本站", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "注销", - "navigation_bar.mutes": "被静音的用户", + "navigation_bar.mutes": "被隐藏的用户", "navigation_bar.pins": "置顶嘟文", "navigation_bar.preferences": "首选项", "navigation_bar.public_timeline": "跨站公共时间轴", @@ -184,7 +209,7 @@ "status.media_hidden": "隐藏媒体内容", "status.mention": "提及 @{name}", "status.more": "更多", - "status.mute_conversation": "静音此对话", + "status.mute_conversation": "隐藏此对话", "status.open": "展开嘟文", "status.pin": "在个人资料页面置顶", "status.reblog": "转嘟", @@ -197,7 +222,7 @@ "status.share": "分享", "status.show_less": "隐藏内容", "status.show_more": "显示内容", - "status.unmute_conversation": "不再静音此对话", + "status.unmute_conversation": "不再隐藏此对话", "status.unpin": "在个人资料页面取消置顶", "tabs_bar.compose": "撰写", "tabs_bar.federated_timeline": "跨站", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index f6f56fee1..a1c7aa875 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -7,17 +7,22 @@ "account.followers": "關注的人", "account.follows": "正關注", "account.follows_you": "關注你", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "媒體", "account.mention": "提及 @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "將 @{name} 靜音", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "文章", "account.report": "舉報 @{name}", "account.requested": "等候審批", "account.share": "分享 @{name} 的個人資料", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "解除對 @{name} 的封鎖", "account.unblock_domain": "不再隱藏 {domain}", "account.unfollow": "取消關注", "account.unmute": "取消 @{name} 的靜音", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "查看完整資料", "boost_modal.combo": "如你想在下次路過這顯示,請按{combo},", "bundle_column_error.body": "加載本組件出錯。", @@ -83,6 +88,7 @@ "empty_column.hashtag": "這個標籤暫時未有內容。", "empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。", "empty_column.home.public_timeline": "公共時間軸", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "你沒有任何通知紀錄,快向其他用戶搭訕吧。", "empty_column.public": "跨站時間軸暫時沒有內容!快寫一些公共的文章,或者關注另一些服務站的用戶吧!你和本站、友站的交流,將決定這裏出現的內容。", "follow_request.authorize": "批准", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "顯示被轉推的文章", "home.column_settings.show_replies": "顯示回應文章", "home.settings": "欄位設定", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "關閉", "lightbox.next": "繼續", "lightbox.previous": "回退", "loading_indicator.label": "載入中...", "media_gallery.toggle_visible": "打開或關上", "missing_indicator.label": "找不到內容", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "被你封鎖的用戶", "navigation_bar.community_timeline": "本站時間軸", "navigation_bar.edit_profile": "修改個人資料", "navigation_bar.favourites": "最愛的內容", "navigation_bar.follow_requests": "關注請求", "navigation_bar.info": "關於本服務站", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "登出", "navigation_bar.mutes": "被你靜音的用戶", "navigation_bar.pins": "置頂文章", @@ -204,6 +229,7 @@ "tabs_bar.home": "主頁", "tabs_bar.local_timeline": "本站", "tabs_bar.notifications": "通知", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "將檔案拖放至此上載", "upload_button.label": "上載媒體檔案", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 1f43c6a20..b52c5a810 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -7,17 +7,22 @@ "account.followers": "專注者", "account.follows": "正關注", "account.follows_you": "關注你", + "account.hide_reblogs": "Hide boosts from @{name}", "account.media": "媒體", "account.mention": "提到 @{name}", + "account.moved_to": "{name} has moved to:", "account.mute": "消音 @{name}", + "account.mute_notifications": "Mute notifications from @{name}", "account.posts": "貼文", "account.report": "檢舉 @{name}", "account.requested": "正在等待許可", "account.share": "分享 @{name} 的用者資訊", + "account.show_reblogs": "Show boosts from @{name}", "account.unblock": "取消封鎖 @{name}", "account.unblock_domain": "不再隱藏 {domain}", "account.unfollow": "取消關注", "account.unmute": "不再消音 @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "查看完整資訊", "boost_modal.combo": "下次你可以按 {combo} 來跳過", "bundle_column_error.body": "加載本組件出錯。", @@ -83,6 +88,7 @@ "empty_column.hashtag": "這個主題標籤下什麼都沒有。", "empty_column.home": "你還沒關注任何人。造訪{public}或利用搜尋功能找到其他用者。", "empty_column.home.public_timeline": "公開時間軸", + "empty_column.list": "There is nothing in this list yet.", "empty_column.notifications": "還沒有任何通知。和別的使用者互動來開始對話。", "empty_column.public": "這裡什麼都沒有!公開寫些什麼,或是關注其他副本的使用者。", "follow_request.authorize": "授權", @@ -98,18 +104,37 @@ "home.column_settings.show_reblogs": "顯示轉推", "home.column_settings.show_replies": "顯示回應", "home.settings": "欄位設定", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "關閉", "lightbox.next": "繼續", "lightbox.previous": "回退", "loading_indicator.label": "讀取中...", "media_gallery.toggle_visible": "切換可見性", "missing_indicator.label": "找不到", + "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "封鎖的使用者", "navigation_bar.community_timeline": "本地時間軸", "navigation_bar.edit_profile": "編輯用者資訊", "navigation_bar.favourites": "最愛", "navigation_bar.follow_requests": "關注請求", "navigation_bar.info": "關於本站", + "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.logout": "登出", "navigation_bar.mutes": "消音的使用者", "navigation_bar.pins": "置頂貼文", @@ -204,6 +229,7 @@ "tabs_bar.home": "家", "tabs_bar.local_timeline": "本地", "tabs_bar.notifications": "通知", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "拖放來上傳", "upload_button.label": "增加媒體", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/reducers/accounts.js b/app/javascript/mastodon/reducers/accounts.js index 8a4d69f26..5891d0fdd 100644 --- a/app/javascript/mastodon/reducers/accounts.js +++ b/app/javascript/mastodon/reducers/accounts.js @@ -59,6 +59,11 @@ const normalizeAccount = (state, account) => { account.display_name_html = emojify(escapeTextContentForBrowser(displayName)); account.note_emojified = emojify(account.note); + if (account.moved) { + state = normalizeAccount(state, account.moved); + account.moved = account.moved.id; + } + return state.set(account.id, fromJS(account)); }; diff --git a/app/javascript/mastodon/reducers/accounts_counters.js b/app/javascript/mastodon/reducers/accounts_counters.js index 1ed0fe3e3..2a78a9f34 100644 --- a/app/javascript/mastodon/reducers/accounts_counters.js +++ b/app/javascript/mastodon/reducers/accounts_counters.js @@ -126,7 +126,8 @@ export default function accountsCounters(state = initialState, action) { case STATUS_FETCH_SUCCESS: return normalizeAccountFromStatus(state, action.status); case ACCOUNT_FOLLOW_SUCCESS: - return state.updateIn([action.relationship.id, 'followers_count'], num => num + 1); + return action.alreadyFollowing ? state : + state.updateIn([action.relationship.id, 'followers_count'], num => num + 1); case ACCOUNT_UNFOLLOW_SUCCESS: return state.updateIn([action.relationship.id, 'followers_count'], num => Math.max(0, num - 1)); default: diff --git a/app/javascript/mastodon/reducers/contexts.js b/app/javascript/mastodon/reducers/contexts.js index 64d584a01..fe8308d0c 100644 --- a/app/javascript/mastodon/reducers/contexts.js +++ b/app/javascript/mastodon/reducers/contexts.js @@ -1,3 +1,7 @@ +import { + ACCOUNT_BLOCK_SUCCESS, + ACCOUNT_MUTE_SUCCESS, +} from '../actions/accounts'; import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; import { TIMELINE_DELETE, TIMELINE_CONTEXT_UPDATE } from '../actions/timelines'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; @@ -31,6 +35,12 @@ const deleteFromContexts = (state, id) => { return state; }; +const filterContexts = (state, relationship) => { + return state.map( + statuses => statuses.filter( + status => status.get('account') !== relationship.id)); +}; + const updateContext = (state, status, references) => { return state.update('descendants', map => { references.forEach(parentId => { @@ -49,6 +59,9 @@ const updateContext = (state, status, references) => { export default function contexts(state = initialState, action) { switch(action.type) { + case ACCOUNT_BLOCK_SUCCESS: + case ACCOUNT_MUTE_SUCCESS: + return filterContexts(state, action.relationship); case CONTEXT_FETCH_SUCCESS: return normalizeContext(state, action.id, action.ancestors, action.descendants); case TIMELINE_DELETE: diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index 17c870351..425a2acdd 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -22,6 +22,7 @@ import media_attachments from './media_attachments'; import notifications from './notifications'; import height_cache from './height_cache'; import custom_emojis from './custom_emojis'; +import lists from './lists'; const reducers = { timelines, @@ -47,6 +48,7 @@ const reducers = { notifications, height_cache, custom_emojis, + lists, }; export default combineReducers(reducers); diff --git a/app/javascript/mastodon/reducers/lists.js b/app/javascript/mastodon/reducers/lists.js new file mode 100644 index 000000000..3e3908869 --- /dev/null +++ b/app/javascript/mastodon/reducers/lists.js @@ -0,0 +1,15 @@ +import { LIST_FETCH_SUCCESS } from '../actions/lists'; +import { Map as ImmutableMap, fromJS } from 'immutable'; + +const initialState = ImmutableMap(); + +const normalizeList = (state, list) => state.set(list.id, fromJS(list)); + +export default function lists(state = initialState, action) { + switch(action.type) { + case LIST_FETCH_SUCCESS: + return normalizeList(state, action.list); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index b1fb4c5da..5120b2b67 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -23,10 +23,6 @@ import { TIMELINE_EXPAND_SUCCESS, } from '../actions/timelines'; import { - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS, -} from '../actions/accounts'; -import { NOTIFICATIONS_UPDATE, NOTIFICATIONS_REFRESH_SUCCESS, NOTIFICATIONS_EXPAND_SUCCESS, @@ -88,18 +84,6 @@ const deleteStatus = (state, id, references) => { return state.delete(id); }; -const filterStatuses = (state, relationship) => { - state.forEach(status => { - if (status.get('account') !== relationship.id) { - return; - } - - state = deleteStatus(state, status.get('id'), state.filter(item => item.get('reblog') === status.get('id'))); - }); - - return state; -}; - const initialState = ImmutableMap(); export default function statuses(state = initialState, action) { @@ -139,9 +123,6 @@ export default function statuses(state = initialState, action) { return normalizeStatuses(state, action.statuses); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.references); - case ACCOUNT_BLOCK_SUCCESS: - case ACCOUNT_MUTE_SUCCESS: - return filterStatuses(state, action.relationship); default: return state; } diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index bee4c4ef9..9984c3b5d 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -55,7 +55,7 @@ const appendNormalizedTimeline = (state, timeline, statuses, next) => { })); }; -const updateTimeline = (state, timeline, status, references) => { +const updateTimeline = (state, timeline, status) => { const top = state.getIn([timeline, 'top']); const ids = state.getIn([timeline, 'items'], ImmutableList()); const includesId = ids.includes(status.get('id')); @@ -70,7 +70,6 @@ const updateTimeline = (state, timeline, status, references) => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { if (!top) mMap.set('unread', unread + 1); if (top && ids.size > 40) newIds = newIds.take(20); - if (status.getIn(['reblog', 'id'], null) !== null) newIds = newIds.filterNot(item => references.includes(item)); mMap.set('items', newIds.unshift(status.get('id'))); })); }; @@ -129,7 +128,7 @@ export default function timelines(state = initialState, action) { case TIMELINE_EXPAND_SUCCESS: return appendNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next); case TIMELINE_UPDATE: - return updateTimeline(state, action.timeline, fromJS(action.status), action.references); + return updateTimeline(state, action.timeline, fromJS(action.status)); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf); case ACCOUNT_BLOCK_SUCCESS: diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index d26d1b727..e47ec5183 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -4,14 +4,18 @@ import { List as ImmutableList } from 'immutable'; const getAccountBase = (state, id) => state.getIn(['accounts', id], null); const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null); const getAccountRelationship = (state, id) => state.getIn(['relationships', id], null); +const getAccountMoved = (state, id) => state.getIn(['accounts', state.getIn(['accounts', id, 'moved'])]); export const makeGetAccount = () => { - return createSelector([getAccountBase, getAccountCounters, getAccountRelationship], (base, counters, relationship) => { + return createSelector([getAccountBase, getAccountCounters, getAccountRelationship, getAccountMoved], (base, counters, relationship, moved) => { if (base === null) { return null; } - return base.merge(counters).set('relationship', relationship); + return base.merge(counters).withMutations(map => { + map.set('relationship', relationship); + map.set('moved', moved); + }); }); }; diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 87bc710af..7f078470e 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -347,3 +347,120 @@ } } } + +.spacer { + flex: 1 1 auto; +} + +.log-entry { + margin-bottom: 8px; + line-height: 20px; + + &__header { + display: flex; + justify-content: flex-start; + align-items: center; + padding: 10px; + background: $ui-base-color; + color: $ui-primary-color; + border-radius: 4px 4px 0 0; + font-size: 14px; + position: relative; + } + + &__avatar { + margin-right: 10px; + + .avatar { + display: block; + margin: 0; + border-radius: 50%; + width: 40px; + height: 40px; + } + } + + &__title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__timestamp { + color: lighten($ui-base-color, 34%); + } + + &__extras { + background: lighten($ui-base-color, 6%); + border-radius: 0 0 4px 4px; + padding: 10px; + color: $ui-primary-color; + font-family: 'mastodon-font-monospace', monospace; + font-size: 12px; + white-space: nowrap; + min-height: 20px; + } + + &__icon { + font-size: 28px; + margin-right: 10px; + color: lighten($ui-base-color, 34%); + } + + &__icon__overlay { + position: absolute; + top: 10px; + right: 10px; + width: 10px; + height: 10px; + border-radius: 50%; + + &.positive { + background: $success-green; + } + + &.negative { + background: $error-red; + } + + &.neutral { + background: $ui-highlight-color; + } + } + + a, + .username, + .target { + color: $ui-secondary-color; + text-decoration: none; + font-weight: 500; + } + + .diff-old { + color: $error-red; + } + + .diff-neutral { + color: $ui-secondary-color; + } + + .diff-new { + color: $success-green; + } +} + +.name-tag { + display: flex; + align-items: center; + + .avatar { + display: block; + margin: 0; + margin-right: 5px; + border-radius: 50%; + } + + .username { + font-weight: 500; + } +} diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 8566585c5..942a3a3fa 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1,5 +1,3 @@ -@import 'variables'; - .app-body { -webkit-overflow-scrolling: touch; -ms-overflow-style: -ms-autohiding-scrollbar; @@ -917,6 +915,18 @@ background-position: center; position: relative; + &.inactive { + opacity: 0.5; + + .account__header__avatar { + filter: grayscale(100%); + } + + .account__header__username { + color: $ui-primary-color; + } + } + & > div { background: rgba(lighten($ui-base-color, 4%), 0.9); padding: 20px 10px; @@ -2087,6 +2097,27 @@ } } +.keyboard-shortcuts { + padding: 8px 0 0; + overflow: hidden; + + thead { + position: absolute; + left: -9999px; + } + + td { + padding: 0 10px 8px; + } + + code { + display: inline-block; + padding: 3px 5px; + background-color: lighten($ui-base-color, 8%); + border: 1px solid darken($ui-base-color, 4%); + } +} + .setting-text { color: $ui-primary-color; background: transparent; @@ -2170,15 +2201,12 @@ button.icon-button.active i.fa-retweet { } .status-card-photo { + cursor: zoom-in; display: block; text-decoration: none; - - img { - display: block; - width: 100%; - height: auto; - margin: 0; - } + width: 100%; + height: auto; + margin: 0; } .status-card-video { @@ -4375,3 +4403,40 @@ noscript { } } } + +.account__moved-note { + padding: 14px 10px; + padding-bottom: 16px; + background: lighten($ui-base-color, 4%); + border-top: 1px solid lighten($ui-base-color, 8%); + border-bottom: 1px solid lighten($ui-base-color, 8%); + + &__message { + position: relative; + margin-left: 58px; + color: $ui-base-lighter-color; + padding: 8px 0; + padding-top: 0; + padding-bottom: 4px; + font-size: 14px; + + > span { + display: block; + overflow: hidden; + text-overflow: ellipsis; + } + } + + &__icon-wrapper { + left: -26px; + position: absolute; + } + + .detailed-status__display-avatar { + position: relative; + } + + .detailed-status__display-name { + margin-bottom: 0; + } +} diff --git a/app/javascript/styles/mastodon/landing_strip.scss b/app/javascript/styles/mastodon/landing_strip.scss index 0bf9daafd..2f7c98d54 100644 --- a/app/javascript/styles/mastodon/landing_strip.scss +++ b/app/javascript/styles/mastodon/landing_strip.scss @@ -34,3 +34,66 @@ .memoriam-strip { background: rgba($base-shadow-color, 0.7); } + +.moved-strip { + padding: 14px; + border-radius: 4px; + background: rgba(darken($ui-base-color, 7%), 0.8); + color: $ui-secondary-color; + font-weight: 400; + margin-bottom: 20px; + + strong, + a { + font-weight: 500; + } + + a { + color: inherit; + text-decoration: underline; + + &.mention { + text-decoration: none; + + span { + text-decoration: none; + } + + &:focus, + &:hover, + &:active { + text-decoration: none; + + span { + text-decoration: underline; + } + } + } + } + + &__message { + margin-bottom: 15px; + + .fa { + margin-right: 5px; + color: $ui-primary-color; + } + } + + &__card { + .detailed-status__display-avatar { + position: relative; + cursor: pointer; + } + + .detailed-status__display-name { + margin-bottom: 0; + text-decoration: none; + + span { + color: $ui-highlight-color; + font-weight: 400; + } + } + } +} diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 66e4f7c5e..31e0abe39 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true class ActivityPub::Activity::Create < ActivityPub::Activity + SUPPORTED_TYPES = %w(Article Note).freeze + CONVERTED_TYPES = %w(Image Video).freeze + def perform return if delete_arrived_first?(object_uri) || unsupported_object_type? @@ -41,7 +44,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity url: object_url || @object['id'], account: @account, text: text_from_content || '', - language: language_from_content, + language: detected_language, spoiler_text: @object['summary'] || '', created_at: @options[:override_timestamps] ? nil : @object['published'], reply: @object['inReplyTo'].present?, @@ -165,40 +168,62 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def text_from_content + return Formatter.instance.linkify([text_from_name, object_url || @object['id']].join(' ')) if converted_object_type? + if @object['content'].present? @object['content'] - elsif language_map? + elsif content_language_map? @object['contentMap'].values.first end end - def language_from_content - return LanguageDetector.instance.detect(text_from_content, @account) unless language_map? - @object['contentMap'].keys.first + def text_from_name + if @object['name'].present? + @object['name'] + elsif name_language_map? + @object['nameMap'].values.first + end + end + + def detected_language + if content_language_map? + @object['contentMap'].keys.first + elsif name_language_map? + @object['nameMap'].keys.first + elsif supported_object_type? + LanguageDetector.instance.detect(text_from_content, @account) + end end def object_url return if @object['url'].blank? - - value = first_of_value(@object['url']) - - return value if value.is_a?(String) - - value['href'] + url_to_href(@object['url'], 'text/html') end - def language_map? + def content_language_map? @object['contentMap'].is_a?(Hash) && !@object['contentMap'].empty? end + def name_language_map? + @object['nameMap'].is_a?(Hash) && !@object['nameMap'].empty? + end + def unsupported_object_type? - @object.is_a?(String) || !%w(Article Note).include?(@object['type']) + @object.is_a?(String) || !(supported_object_type? || converted_object_type?) end def unsupported_media_type?(mime_type) mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type) end + def supported_object_type? + SUPPORTED_TYPES.include?(@object['type']) + end + + def converted_object_type? + CONVERTED_TYPES.include?(@object['type']) + end + def skip_download? return @skip_download if defined?(@skip_download) @skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media? @@ -210,7 +235,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def forward_for_reply return unless @json['signature'].present? && reply_to_local? - ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id) + ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url]) end def lock_options diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index 4c6afb090..d0fb49342 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -30,8 +30,11 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity def forward_for_reblogs(status) return if @json['signature'].blank? - ActivityPub::RawDistributionWorker.push_bulk(status.reblogs.includes(:account).references(:account).merge(Account.local).pluck(:account_id)) do |account_id| - [payload, account_id] + rebloggers_ids = status.reblogs.includes(:account).references(:account).merge(Account.local).pluck(:account_id) + inboxes = Account.where(id: ::Follow.where(target_account_id: rebloggers_ids).select(:account_id)).inboxes - [@account.preferred_inbox_url] + + ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url| + [payload, rebloggers_ids.first, inbox_url] end end diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index 790d2025c..90d589d90 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -9,6 +9,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base { 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', 'sensitive' => 'as:sensitive', + 'movedTo' => 'as:movedTo', 'Hashtag' => 'as:Hashtag', 'ostatus' => 'http://ostatus.org#', 'atomUri' => 'ostatus:atomUri', diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 76365c7d3..fe5ebfc36 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -149,7 +149,7 @@ class FeedManager return false if receiver_id == status.account_id return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?) - return true if keyword_filter?(status, Glitch::KeywordMute.matcher_for(receiver_id)) + return true if keyword_filter?(status, receiver_id) check_for_mutes = [status.account_id] check_for_mutes.concat(status.mentions.pluck(:account_id)) @@ -162,32 +162,38 @@ class FeedManager return true if Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any? - if status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply - should_filter = !Follow.where(account_id: receiver_id, target_account_id: status.in_reply_to_account_id).exists? # and I'm not following the person it's a reply to - should_filter &&= receiver_id != status.in_reply_to_account_id # and it's not a reply to me - should_filter &&= status.account_id != status.in_reply_to_account_id # and it's not a self-reply + if status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply + should_filter = !Follow.where(account_id: receiver_id, target_account_id: status.in_reply_to_account_id).exists? # and I'm not following the person it's a reply to + should_filter &&= receiver_id != status.in_reply_to_account_id # and it's not a reply to me + should_filter &&= status.account_id != status.in_reply_to_account_id # and it's not a self-reply return should_filter - elsif status.reblog? # Filter out a reblog - src_id = status.account_id - should_filter = Follow.where(account_id: receiver_id, target_account_id: src_id, show_reblogs: false).exists? # if the reblogger's reblogs are suppressed - should_filter ||= Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists? # or if the author of the reblogged status is blocking me - should_filter ||= AccountDomainBlock.where(account_id: receiver_id, domain: status.reblog.account.domain).exists? # or the author's domain is blocked + elsif status.reblog? # Filter out a reblog + should_filter = Follow.where(account_id: receiver_id, target_account_id: status.account_id, show_reblogs: false).exists? # if the reblogger's reblogs are suppressed + should_filter ||= Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists? # or if the author of the reblogged status is blocking me + should_filter ||= AccountDomainBlock.where(account_id: receiver_id, domain: status.reblog.account.domain).exists? # or the author's domain is blocked return should_filter end false end - def keyword_filter?(status, matcher) - should_filter = matcher =~ status.text - should_filter ||= matcher =~ status.spoiler_text + def keyword_filter?(status, receiver_id) + text_matcher = Glitch::KeywordMute.text_matcher_for(receiver_id) + tag_matcher = Glitch::KeywordMute.tag_matcher_for(receiver_id) + + should_filter = text_matcher.matches?(status.text) + should_filter ||= text_matcher.matches?(status.spoiler_text) + should_filter ||= tag_matcher.matches?(status.tags) if status.reblog? - should_filter ||= matcher =~ status.reblog.text - should_filter ||= matcher =~ status.reblog.spoiler_text + reblog = status.reblog + + should_filter ||= text_matcher.matches?(reblog.text) + should_filter ||= text_matcher.matches?(reblog.spoiler_text) + should_filter ||= tag_matcher.matches?(status.tags) end - !!should_filter + should_filter end def filter_from_mentions?(status, receiver_id) @@ -199,7 +205,7 @@ class FeedManager should_filter = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any? # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked should_filter ||= (status.account.silenced? && !Follow.where(account_id: receiver_id, target_account_id: status.account_id).exists?) # of if the account is silenced and I'm not following them - should_filter ||= keyword_filter?(status, Glitch::KeywordMute.matcher_for(receiver_id)) # or if the mention contains a muted keyword + should_filter ||= keyword_filter?(status, receiver_id) # or if the mention contains a muted keyword should_filter end diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 733a1c4b7..9d8bc52db 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -51,12 +51,7 @@ class Formatter def simplified_format(account) return reformat(account.note).html_safe unless account.local? # rubocop:disable Rails/OutputSafety - - html = encode_and_link_urls(account.note) - html = simple_format(html, {}, sanitize: false) - html = html.delete("\n") - - html.html_safe # rubocop:disable Rails/OutputSafety + linkify(account.note) end def sanitize(html, config) @@ -69,6 +64,14 @@ class Formatter html.html_safe # rubocop:disable Rails/OutputSafety end + def linkify(text) + html = encode_and_link_urls(text) + html = simple_format(html, {}, sanitize: false) + html = html.delete("\n") + + html.html_safe # rubocop:disable Rails/OutputSafety + end + private def encode(html) diff --git a/app/models/account.rb b/app/models/account.rb index a4b8e1c0b..ffd19fa52 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -42,6 +42,7 @@ # followers_url :string default(""), not null # protocol :integer default("ostatus"), not null # memorial :boolean default(FALSE), not null +# moved_to_account_id :integer # class Account < ApplicationRecord @@ -102,6 +103,9 @@ class Account < ApplicationRecord has_many :list_accounts, inverse_of: :account, dependent: :destroy has_many :lists, through: :list_accounts + # Account migrations + belongs_to :moved_to_account, class_name: 'Account' + scope :remote, -> { where.not(domain: nil) } scope :local, -> { where(domain: nil) } scope :without_followers, -> { where(followers_count: 0) } @@ -135,6 +139,10 @@ class Account < ApplicationRecord domain.nil? end + def moved? + moved_to_account_id.present? + end + def acct local? ? username : "#{username}@#{domain}" end @@ -208,6 +216,10 @@ class Account < ApplicationRecord Rails.cache.fetch("exclude_domains_for:#{id}") { domain_blocks.pluck(:domain) } end + def preferred_inbox_url + shared_inbox_url.presence || inbox_url + end + class << self def readonly_attributes super - %w(statuses_count following_count followers_count) diff --git a/app/models/admin.rb b/app/models/admin.rb new file mode 100644 index 000000000..d41d18449 --- /dev/null +++ b/app/models/admin.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Admin + def self.table_name_prefix + 'admin_' + end +end diff --git a/app/models/admin/action_log.rb b/app/models/admin/action_log.rb new file mode 100644 index 000000000..4e950fbf7 --- /dev/null +++ b/app/models/admin/action_log.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: admin_action_logs +# +# id :integer not null, primary key +# account_id :integer +# action :string default(""), not null +# target_type :string +# target_id :integer +# recorded_changes :text default(""), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class Admin::ActionLog < ApplicationRecord + serialize :recorded_changes + + belongs_to :account, required: true + belongs_to :target, required: true, polymorphic: true + + default_scope -> { order('id desc') } + + def action + super.to_sym + end + + before_validation :set_changes + + private + + def set_changes + case action + when :destroy, :create + self.recorded_changes = target.attributes + when :update, :promote, :demote + self.recorded_changes = target.previous_changes + end + end +end diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index c41f92581..fdf35a4e3 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -7,7 +7,7 @@ module AccountInteractions def following_map(target_account_ids, account_id) Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping| mapping[follow.target_account_id] = { - reblogs: follow.show_reblogs? + reblogs: follow.show_reblogs?, } end end @@ -31,7 +31,7 @@ module AccountInteractions def requested_map(target_account_ids, account_id) FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping| mapping[follow_request.target_account_id] = { - reblogs: follow_request.show_reblogs? + reblogs: follow_request.show_reblogs?, } end end diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 2b148c82b..c1d2cf420 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -24,8 +24,12 @@ class Form::AdminSettings :open_deletion=, :timeline_preview, :timeline_preview=, + :show_staff_badge, + :show_staff_badge=, :bootstrap_timeline_accounts, :bootstrap_timeline_accounts=, + :min_invite_role, + :min_invite_role=, to: Setting ) end diff --git a/app/models/form/migration.rb b/app/models/form/migration.rb new file mode 100644 index 000000000..b74987337 --- /dev/null +++ b/app/models/form/migration.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Form::Migration + include ActiveModel::Validations + + attr_accessor :acct, :account + + def initialize(attrs = {}) + @account = attrs[:account] + @acct = attrs[:account].acct unless @account.nil? + @acct = attrs[:acct].gsub(/\A@/, '').strip unless attrs[:acct].nil? + end + + def valid? + return false unless super + set_account + errors.empty? + end + + private + + def set_account + self.account = (ResolveRemoteAccountService.new.call(acct) if account.nil? && acct.present?) + end +end diff --git a/app/models/form/status_batch.rb b/app/models/form/status_batch.rb index a97b4aa28..4f08a3049 100644 --- a/app/models/form/status_batch.rb +++ b/app/models/form/status_batch.rb @@ -2,8 +2,9 @@ class Form::StatusBatch include ActiveModel::Model + include AccountableConcern - attr_accessor :status_ids, :action + attr_accessor :status_ids, :action, :current_account ACTION_TYPE = %w(nsfw_on nsfw_off delete).freeze @@ -20,11 +21,14 @@ class Form::StatusBatch def change_sensitive(sensitive) media_attached_status_ids = MediaAttachment.where(status_id: status_ids).pluck(:status_id) + ApplicationRecord.transaction do Status.where(id: media_attached_status_ids).find_each do |status| status.update!(sensitive: sensitive) + log_action :update, status end end + true rescue ActiveRecord::RecordInvalid false @@ -33,7 +37,9 @@ class Form::StatusBatch def delete_statuses Status.where(id: status_ids).find_each do |status| RemovalWorker.perform_async(status.id) + log_action :destroy, status end + true end end diff --git a/app/models/glitch/keyword_mute.rb b/app/models/glitch/keyword_mute.rb index 009de1880..a2481308f 100644 --- a/app/models/glitch/keyword_mute.rb +++ b/app/models/glitch/keyword_mute.rb @@ -16,51 +16,85 @@ class Glitch::KeywordMute < ApplicationRecord validates_presence_of :keyword - after_commit :invalidate_cached_matcher + after_commit :invalidate_cached_matchers - def self.matcher_for(account_id) - Matcher.new(account_id) + def self.text_matcher_for(account_id) + TextMatcher.new(account_id) + end + + def self.tag_matcher_for(account_id) + TagMatcher.new(account_id) end private - def invalidate_cached_matcher - Rails.cache.delete("keyword_mutes:regex:#{account_id}") + def invalidate_cached_matchers + Rails.cache.delete(TextMatcher.cache_key(account_id)) + Rails.cache.delete(TagMatcher.cache_key(account_id)) end - class Matcher + class RegexpMatcher attr_reader :account_id attr_reader :regex def initialize(account_id) @account_id = account_id - regex_text = Rails.cache.fetch("keyword_mutes:regex:#{account_id}") { regex_text_for_account } + regex_text = Rails.cache.fetch(self.class.cache_key(account_id)) { make_regex_text } @regex = /#{regex_text}/ end - def =~(str) - regex =~ str + protected + + def keywords + Glitch::KeywordMute.where(account_id: account_id).pluck(:whole_word, :keyword) end - private + def boundary_regex_for_keyword(keyword) + sb = keyword =~ /\A[[:word:]]/ ? '\b' : '' + eb = keyword =~ /[[:word:]]\Z/ ? '\b' : '' - def keywords - Glitch::KeywordMute.where(account_id: account_id).select(:keyword, :id, :whole_word) + /(?mix:#{sb}#{Regexp.escape(keyword)}#{eb})/ + end + end + + class TextMatcher < RegexpMatcher + def self.cache_key(account_id) + format('keyword_mutes:regex:text:%s', account_id) + end + + def matches?(str) + !!(regex =~ str) end - def regex_text_for_account - kws = keywords.find_each.with_object([]) do |kw, a| - a << (kw.whole_word ? boundary_regex_for_keyword(kw.keyword) : kw.keyword) + private + + def make_regex_text + kws = keywords.map! do |whole_word, keyword| + whole_word ? boundary_regex_for_keyword(keyword) : keyword end Regexp.union(kws).source end + end - def boundary_regex_for_keyword(keyword) - sb = keyword =~ /\A[[:word:]]/ ? '\b' : '' - eb = keyword =~ /[[:word:]]\Z/ ? '\b' : '' + class TagMatcher < RegexpMatcher + def self.cache_key(account_id) + format('keyword_mutes:regex:tag:%s', account_id) + end - /(?mix:#{sb}#{Regexp.escape(keyword)}#{eb})/ + def matches?(tags) + tags.pluck(:name).any? { |n| regex =~ n } + end + + private + + def make_regex_text + kws = keywords.map! do |whole_word, keyword| + term = (Tag::HASHTAG_RE =~ keyword) ? $1 : keyword + whole_word ? boundary_regex_for_keyword(term) : term + end + + Regexp.union(kws).source end end end diff --git a/app/models/invite.rb b/app/models/invite.rb new file mode 100644 index 000000000..6907c1f1d --- /dev/null +++ b/app/models/invite.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: invites +# +# id :integer not null, primary key +# user_id :integer +# code :string default(""), not null +# expires_at :datetime +# max_uses :integer +# uses :integer default(0), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class Invite < ApplicationRecord + belongs_to :user, required: true + has_many :users, inverse_of: :invite + + scope :available, -> { where(expires_at: nil).or(where('expires_at >= ?', Time.now.utc)) } + scope :expired, -> { where.not(expires_at: nil).where('expires_at < ?', Time.now.utc) } + + before_validation :set_code + + attr_reader :expires_in + + def expires_in=(interval) + self.expires_at = interval.to_i.seconds.from_now unless interval.blank? + @expires_in = interval + end + + def valid_for_use? + (max_uses.nil? || uses < max_uses) && !expired? + end + + def expire! + touch(:expires_at) + end + + def expired? + !expires_at.nil? && expires_at < Time.now.utc + end + + private + + def set_code + loop do + self.code = ([*('a'..'z'), *('A'..'Z'), *('0'..'9')] - %w(0 1 I l O)).sample(8).join + break if Invite.find_by(code: code).nil? + end + end +end diff --git a/app/models/invite_filter.rb b/app/models/invite_filter.rb new file mode 100644 index 000000000..7d89bad4a --- /dev/null +++ b/app/models/invite_filter.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class InviteFilter + attr_reader :params + + def initialize(params) + @params = params + end + + def results + scope = Invite.order(created_at: :desc) + + params.each do |key, value| + scope.merge!(scope_for(key, value)) if value.present? + end + + scope + end + + private + + def scope_for(key, _value) + case key.to_s + when 'available' + Invite.available + when 'expired' + Invite.expired + else + raise "Unknown filter: #{key}" + end + end +end diff --git a/app/models/notification.rb b/app/models/notification.rb index a3ffb1f45..976963528 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -24,7 +24,7 @@ class Notification < ApplicationRecord favourite: 'Favourite', }.freeze - STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :media_attachments, :tags, mentions: :account]].freeze + STATUS_INCLUDES = [:account, :application, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :application, :media_attachments, :tags, mentions: :account]].freeze belongs_to :account belongs_to :from_account, class_name: 'Account' @@ -55,9 +55,11 @@ class Notification < ApplicationRecord def target_status case type when :reblog - activity&.reblog - when :favourite, :mention - activity&.status + status&.reblog + when :favourite + favourite&.status + when :mention + mention&.status end end diff --git a/app/models/status.rb b/app/models/status.rb index 172d3a665..70cfdc1c7 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -183,7 +183,7 @@ class Status < ApplicationRecord end def reblogs_map(status_ids, account_id) - select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).map { |s| [s.reblog_of_id, true] }.to_h + select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).reorder(nil).map { |s| [s.reblog_of_id, true] }.to_h end def mutes_map(conversation_ids, account_id) @@ -291,6 +291,7 @@ class Status < ApplicationRecord def set_visibility self.visibility = (account.locked? ? :private : :public) if visibility.nil? + self.visibility = reblog.visibility if reblog? self.sensitive = false if sensitive.nil? end diff --git a/app/models/user.rb b/app/models/user.rb index 2458cf298..29bdcbd67 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -33,6 +33,7 @@ # account_id :integer not null # disabled :boolean default(FALSE), not null # moderator :boolean default(FALSE), not null +# invite_id :integer # class User < ApplicationRecord @@ -47,6 +48,7 @@ class User < ApplicationRecord otp_number_of_backup_codes: 10 belongs_to :account, inverse_of: :user, required: true + belongs_to :invite, counter_cache: :uses accepts_nested_attributes_for :account has_many :applications, class_name: 'Doorkeeper::Application', as: :owner @@ -77,6 +79,8 @@ class User < ApplicationRecord :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, to: :settings, prefix: :setting, allow_nil: false + attr_accessor :invite_code + def confirmed? confirmed_at.present? end @@ -95,6 +99,19 @@ class User < ApplicationRecord end end + def role?(role) + case role + when 'user' + true + when 'moderator' + staff? + when 'admin' + admin? + else + false + end + end + def disable! update!(disabled: true, last_sign_in_at: current_sign_in_at, @@ -169,6 +186,11 @@ class User < ApplicationRecord session.web_push_subscription.nil? ? nil : session.web_push_subscription.as_payload end + def invite_code=(code) + self.invite = Invite.find_by(code: code) unless code.blank? + @invite_code = code + end + protected def send_devise_notification(notification, *args) diff --git a/app/policies/invite_policy.rb b/app/policies/invite_policy.rb new file mode 100644 index 000000000..a2a65f934 --- /dev/null +++ b/app/policies/invite_policy.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class InvitePolicy < ApplicationPolicy + def index? + staff? + end + + def create? + min_required_role? + end + + def destroy? + owner? || (Setting.min_invite_role == 'admin' ? admin? : staff?) + end + + private + + def owner? + record.user_id == current_user&.id + end + + def min_required_role? + current_user&.role?(Setting.min_invite_role) + end +end diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 896d67115..622bdde0c 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -10,6 +10,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer has_one :public_key, serializer: ActivityPub::PublicKeySerializer + attribute :moved_to, if: :moved? + class EndpointsSerializer < ActiveModel::Serializer include RoutingHelper @@ -25,6 +27,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer has_one :icon, serializer: ActivityPub::ImageSerializer, if: :avatar_exists? has_one :image, serializer: ActivityPub::ImageSerializer, if: :header_exists? + delegate :moved?, to: :object + def id account_url(object) end @@ -92,4 +96,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer def manually_approves_followers object.locked end + + def moved_to + ActivityPub::TagManager.instance.uri_for(object.moved_to_account) + end end diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 65fdb0308..bab944c5a 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -7,6 +7,10 @@ class REST::AccountSerializer < ActiveModel::Serializer :note, :url, :avatar, :avatar_static, :header, :header_static, :followers_count, :following_count, :statuses_count + has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved? + + delegate :moved?, to: :object + def id object.id.to_s end diff --git a/app/serializers/rest/list_serializer.rb b/app/serializers/rest/list_serializer.rb index c0150888e..977da7439 100644 --- a/app/serializers/rest/list_serializer.rb +++ b/app/serializers/rest/list_serializer.rb @@ -2,4 +2,8 @@ class REST::ListSerializer < ActiveModel::Serializer attributes :id, :title + + def id + object.id.to_s + end end diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index 8d7b7a17c..7649bceca 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -18,7 +18,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService actor = ActivityPub::TagManager.instance.uri_to_resource(actor_id, Account) actor = ActivityPub::FetchRemoteAccountService.new.call(actor_id, id: true) if actor.nil? || needs_update(actor) - return if actor.suspended? + return if actor.nil? || actor.suspended? ActivityPub::Activity.factory(activity_json, actor).perform end @@ -42,7 +42,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService end def expected_type? - %w(Note Article).include? @json['type'] + (ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES).include? @json['type'] end def needs_update(actor) diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index f93baf4b5..06ca75563 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -74,6 +74,7 @@ class ActivityPub::ProcessAccountService < BaseService @account.statuses_count = outbox_total_items if outbox_total_items.present? @account.following_count = following_total_items if following_total_items.present? @account.followers_count = followers_total_items if followers_total_items.present? + @account.moved_to_account = moved_account if @json['movedTo'].present? end def after_protocol_change! @@ -106,12 +107,7 @@ class ActivityPub::ProcessAccountService < BaseService def url return if @json['url'].blank? - - value = first_of_value(@json['url']) - - return value if value.is_a?(String) - - value['href'] + url_to_href(@json['url'], 'text/html') end def outbox_total_items @@ -137,6 +133,12 @@ class ActivityPub::ProcessAccountService < BaseService @collections[type] = nil end + def moved_account + account = ActivityPub::TagManager.instance.uri_to_resource(@json['movedTo'], Account) + account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], id: true) + account + end + def skip_download? @account.suspended? || domain_block&.reject_media? end diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 14c21b6cc..37cf75379 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -74,6 +74,9 @@ class FetchLinkCardService < BaseService return false unless response.respond_to?(:type) + # The photo will change the URL. So, to avoid duplication of URLs, PreviewCard needs to be checked again. + @card = PreviewCard.find_by(url: response.url) || @card if response.type == 'photo' + @card.type = response.type @card.title = response.respond_to?(:title) ? response.title : '' @card.author_name = response.respond_to?(:author_name) ? response.author_name : '' diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index a229d4ff8..e12721c46 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -18,7 +18,7 @@ class ProcessMentionsService < BaseService end if mentioned_account.nil? - username, domain = match.first.split('@') + username, domain = $1.split('@') mentioned_account = Account.find_remote(username, domain) end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 9617081fd..7789bd441 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -3,7 +3,7 @@ class RemoveStatusService < BaseService include StreamEntryRenderer - def call(status) + def call(status, options = {}) @payload = Oj.dump(event: :delete, payload: status.id.to_s) @status = status @account = status.account @@ -11,6 +11,7 @@ class RemoveStatusService < BaseService @mentions = status.mentions.includes(:account).to_a @reblogs = status.reblogs.to_a @stream_entry = status.stream_entry + @options = options remove_from_self if status.account.local? remove_from_followers @@ -23,7 +24,12 @@ class RemoveStatusService < BaseService @status.destroy! - return unless @account.local? + # There is no reason to send out Undo activities when the + # cause is that the original object has been removed, since + # original object being removed implicitly removes reblogs + # of it. The Delete activity of the original is forwarded + # separately. + return if !@account.local? || @options[:original_removed] remove_from_remote_followers remove_from_remote_affected @@ -105,7 +111,7 @@ class RemoveStatusService < BaseService # without us being able to do all the fancy stuff @reblogs.each do |reblog| - RemoveStatusService.new.call(reblog) + RemoveStatusService.new.call(reblog, original_removed: true) end end diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml index 94ec5ae5b..b0062752c 100644 --- a/app/views/accounts/_header.html.haml +++ b/app/views/accounts/_header.html.haml @@ -1,7 +1,7 @@ - processed_bio = FrontmatterHandler.instance.process_bio Formatter.instance.simplified_format account .card.h-card.p-author{ style: "background-image: url(#{account.header.url(:original)})" } .card__illustration - - unless account.memorial? + - unless account.memorial? || account.moved? - if user_signed_in? && current_account.id != account.id && !current_account.requested?(account) .controls - if current_account.following?(account) @@ -27,15 +27,15 @@ %small %span @#{account.local_username_and_domain} = fa_icon('lock') if account.locked? - - - if account.user_admin? - .roles - .account-role.admin - = t 'accounts.roles.admin' - - elsif account.user_moderator? - .roles - .account-role.moderator - = t 'accounts.roles.moderator' + - if Setting.show_staff_badge + - if account.user_admin? + .roles + .account-role.admin + = t 'accounts.roles.admin' + - elsif account.user_moderator? + .roles + .account-role.moderator + = t 'accounts.roles.moderator' .bio .account__header__content.p-note.emojify!=processed_bio[:text] - if processed_bio[:metadata].length > 0 diff --git a/app/views/accounts/_moved_strip.html.haml b/app/views/accounts/_moved_strip.html.haml new file mode 100644 index 000000000..6a14a5dd3 --- /dev/null +++ b/app/views/accounts/_moved_strip.html.haml @@ -0,0 +1,17 @@ +- moved_to_account = account.moved_to_account + +.moved-strip + .moved-strip__message + = fa_icon 'suitcase' + = t('accounts.moved_html', name: content_tag(:strong, display_name(account), class: :emojify), new_profile_link: link_to(content_tag(:strong, safe_join(['@', content_tag(:span, moved_to_account.acct)])), TagManager.instance.url_for(moved_to_account), class: 'mention')) + + .moved-strip__card + = link_to TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do + .detailed-status__display-avatar + .account__avatar-overlay + .account__avatar-overlay-base{ style: "background-image: url('#{moved_to_account.avatar.url(:original)}')" } + .account__avatar-overlay-overlay{ style: "background-image: url('#{account.avatar.url(:original)}')" } + + %span.display-name + %strong.emojify= display_name(moved_to_account) + %span @#{moved_to_account.acct} diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index fd8ad5530..accad5f78 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -14,6 +14,8 @@ - if @account.memorial? .memoriam-strip= t('in_memoriam_html') +- elsif @account.moved? + = render partial: 'moved_strip', locals: { account: @account } - elsif show_landing_strip? = render partial: 'shared/landing_strip', locals: { account: @account } diff --git a/app/views/admin/action_logs/_action_log.html.haml b/app/views/admin/action_logs/_action_log.html.haml new file mode 100644 index 000000000..ec90961cb --- /dev/null +++ b/app/views/admin/action_logs/_action_log.html.haml @@ -0,0 +1,15 @@ +%li.log-entry + .log-entry__header + .log-entry__avatar + = image_tag action_log.account.avatar.url(:original), alt: '', width: 40, height: 40, class: 'avatar' + .log-entry__content + .log-entry__title + = t("admin.action_logs.actions.#{action_log.action}_#{action_log.target_type.underscore}", name: content_tag(:span, action_log.account.username, class: 'username'), target: content_tag(:span, log_target(action_log), class: 'target')).html_safe + .log-entry__timestamp + %time= l action_log.created_at + .spacer + .log-entry__icon + = fa_icon icon_for_log(action_log) + .log-entry__icon__overlay{ class: class_for_log_icon(action_log) } + .log-entry__extras + = log_extra_attributes relevant_log_changes(action_log) diff --git a/app/views/admin/action_logs/index.html.haml b/app/views/admin/action_logs/index.html.haml new file mode 100644 index 000000000..bb6d7b5d7 --- /dev/null +++ b/app/views/admin/action_logs/index.html.haml @@ -0,0 +1,7 @@ +- content_for :page_title do + = t('admin.action_logs.title') + +%ul + = render @action_logs + += paginate @action_logs diff --git a/app/views/admin/invites/_invite.html.haml b/app/views/admin/invites/_invite.html.haml new file mode 100644 index 000000000..d7b697286 --- /dev/null +++ b/app/views/admin/invites/_invite.html.haml @@ -0,0 +1,21 @@ +%tr + %td + .name-tag + = image_tag invite.user.account.avatar.url(:original), alt: '', width: 16, height: 16, class: 'avatar' + %span.username= invite.user.account.username + %td + = invite.uses + = " / #{invite.max_uses}" unless invite.max_uses.nil? + %td + - if invite.expired? + = t('invites.expired') + - else + - if invite.expires_at.nil? + ∞ + - else + %time.formatted{ datetime: invite.expires_at.iso8601, title: l(invite.expires_at) } + = l invite.expires_at + %td= table_link_to 'link', public_invite_url(invite_code: invite.code), public_invite_url(invite_code: invite.code) + %td + - if !invite.expired? && policy(invite).destroy? + = table_link_to 'times', t('invites.delete'), admin_invite_path(invite), method: :delete diff --git a/app/views/admin/invites/index.html.haml b/app/views/admin/invites/index.html.haml new file mode 100644 index 000000000..944a60471 --- /dev/null +++ b/app/views/admin/invites/index.html.haml @@ -0,0 +1,30 @@ +- content_for :page_title do + = t('admin.invites.title') + +.filters + .filter-subset + %strong= t('admin.invites.filter.title') + %ul + %li= filter_link_to t('admin.invites.filter.all'), available: nil, expired: nil + %li= filter_link_to t('admin.invites.filter.available'), available: 1, expired: nil + %li= filter_link_to t('admin.invites.filter.expired'), available: nil, expired: 1 + +- if policy(:invite).create? + %p= t('invites.prompt') + + = render 'invites/form' + + %hr/ + +%table.table + %thead + %tr + %th + %th= t('invites.table.uses') + %th= t('invites.table.expires_at') + %th + %th + %tbody + = render @invites + += paginate @invites diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index 468166035..c7c25f528 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -19,6 +19,9 @@ = f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html') .fields-group + = f.input :show_staff_badge, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_staff_badge.title'), hint: t('admin.settings.show_staff_badge.desc_html') + + .fields-group = f.input :open_registrations, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.open.title'), hint: t('admin.settings.registrations.open.desc_html') .fields-group @@ -30,6 +33,11 @@ %hr/ .fields-group + = f.input :min_invite_role, wrapper: :with_label, collection: %i(disabled user moderator admin), label: t('admin.settings.registrations.min_invite_role.title'), label_method: lambda { |role| role == :disabled ? t('admin.settings.registrations.min_invite_role.disabled') : t("admin.accounts.roles.#{role}") }, as: :radio_buttons, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' + + %hr/ + + .fields-group = f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } = f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 } diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml index f71675df0..2d4c0f5ac 100644 --- a/app/views/auth/registrations/new.html.haml +++ b/app/views/auth/registrations/new.html.haml @@ -16,6 +16,7 @@ = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' } = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' } = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' } + = f.input :invite_code, as: :hidden .actions = f.button :button, t('auth.register'), type: :submit diff --git a/app/views/invites/_form.html.haml b/app/views/invites/_form.html.haml new file mode 100644 index 000000000..a01cf5946 --- /dev/null +++ b/app/views/invites/_form.html.haml @@ -0,0 +1,9 @@ += simple_form_for(@invite, url: controller.is_a?(Admin::InvitesController) ? admin_invites_path : invites_path) do |f| + = render 'shared/error_messages', object: @invite + + .fields-group + = f.input :max_uses, wrapper: :with_label, collection: [1, 5, 10, 25, 50, 100], label_method: lambda { |num| I18n.t('invites.max_uses', count: num) }, prompt: I18n.t('invites.max_uses_prompt') + = f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day].map(&:to_i), label_method: lambda { |i| I18n.t("invites.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt') + + .actions + = f.button :button, t('invites.generate'), type: :submit diff --git a/app/views/invites/_invite.html.haml b/app/views/invites/_invite.html.haml new file mode 100644 index 000000000..81d67eb7d --- /dev/null +++ b/app/views/invites/_invite.html.haml @@ -0,0 +1,17 @@ +%tr + %td + = invite.uses + = " / #{invite.max_uses}" unless invite.max_uses.nil? + %td + - if invite.expired? + = t('invites.expired') + - else + - if invite.expires_at.nil? + ∞ + - else + %time.formatted{ datetime: invite.expires_at.iso8601, title: l(invite.expires_at) } + = l invite.expires_at + %td= table_link_to 'link', public_invite_url(invite_code: invite.code), public_invite_url(invite_code: invite.code) + %td + - if invite.expired? && policy(invite).destroy? + = table_link_to 'times', t('invites.delete'), invite_path(invite), method: :delete diff --git a/app/views/invites/index.html.haml b/app/views/invites/index.html.haml new file mode 100644 index 000000000..f4c5047fa --- /dev/null +++ b/app/views/invites/index.html.haml @@ -0,0 +1,19 @@ +- content_for :page_title do + = t('invites.title') + +- if policy(:invite).create? + %p= t('invites.prompt') + + = render 'form' + + %hr/ + +%table.table + %thead + %tr + %th= t('invites.table.uses') + %th= t('invites.table.expires_at') + %th + %th + %tbody + = render @invites diff --git a/app/views/settings/migrations/show.html.haml b/app/views/settings/migrations/show.html.haml new file mode 100644 index 000000000..b7c34761f --- /dev/null +++ b/app/views/settings/migrations/show.html.haml @@ -0,0 +1,17 @@ +- content_for :page_title do + = t('settings.migrate') + += simple_form_for @migration, as: :migration, url: settings_migration_path, html: { method: :put } do |f| + - if @migration.account + %p.hint= t('migrations.currently_redirecting') + + .fields-group + = render partial: 'authorize_follows/card', locals: { account: @migration.account } + + = render 'shared/error_messages', object: @migration + + .fields-group + = f.input :acct, placeholder: t('migrations.acct') + + .actions + = f.button :button, t('migrations.proceed'), type: :submit, class: 'negative' diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index 551a7ca49..be7bd0ba0 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -21,3 +21,8 @@ .actions = f.button :button, t('generic.save_changes'), type: :submit + +%hr/ + +%h6= t('auth.migrate_account') +%p.muted-hint= t('auth.migrate_account_html', path: settings_migration_path) diff --git a/app/views/stream_entries/show.html.haml b/app/views/stream_entries/show.html.haml index 428069931..895a61247 100644 --- a/app/views/stream_entries/show.html.haml +++ b/app/views/stream_entries/show.html.haml @@ -8,7 +8,7 @@ = opengraph 'og:site_name', site_title = opengraph 'og:type', 'article' - = opengraph 'og:title', "#{@account.username} on #{site_hostname}" + = opengraph 'og:title', "#{@account.display_name.presence || @account.username} on #{site_hostname}" = opengraph 'og:url', account_stream_entry_url(@account, @stream_entry) = render 'stream_entries/og_description', activity: @stream_entry.activity diff --git a/app/workers/activitypub/raw_distribution_worker.rb b/app/workers/activitypub/raw_distribution_worker.rb index d73466f6e..41e61132f 100644 --- a/app/workers/activitypub/raw_distribution_worker.rb +++ b/app/workers/activitypub/raw_distribution_worker.rb @@ -5,10 +5,10 @@ class ActivityPub::RawDistributionWorker sidekiq_options queue: 'push' - def perform(json, source_account_id) + def perform(json, source_account_id, exclude_inboxes = []) @account = Account.find(source_account_id) - ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url| + ActivityPub::DeliveryWorker.push_bulk(inboxes - exclude_inboxes) do |inbox_url| [json, @account.id, inbox_url] end rescue ActiveRecord::RecordNotFound |