diff options
Diffstat (limited to 'app')
306 files changed, 7415 insertions, 2852 deletions
diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb new file mode 100644 index 000000000..8bf5b4af7 --- /dev/null +++ b/app/chewy/statuses_index.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +class StatusesIndex < Chewy::Index + settings index: { refresh_interval: '15m' }, analysis: { + filter: { + english_stop: { + type: 'stop', + stopwords: '_english_', + }, + english_stemmer: { + type: 'stemmer', + language: 'english', + }, + english_possessive_stemmer: { + type: 'stemmer', + language: 'possessive_english', + }, + }, + analyzer: { + content: { + tokenizer: 'uax_url_email', + filter: %w( + english_possessive_stemmer + lowercase + asciifolding + cjk_width + english_stop + english_stemmer + ), + }, + }, + } + + define_type ::Status.without_reblogs do + crutch :mentions do |collection| + data = ::Mention.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id) + data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } + end + + crutch :favourites do |collection| + data = ::Favourite.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id) + data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } + end + + crutch :reblogs do |collection| + data = ::Status.where(reblog_of_id: collection.map(&:id)).pluck(:reblog_of_id, :account_id) + data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } + end + + root date_detection: false do + field :account_id, type: 'long' + + field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].join("\n\n") } do + field :stemmed, type: 'text', analyzer: 'content' + end + + field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) } + field :created_at, type: 'date' + end + end +end diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 8785df14e..7b46b2228 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -36,7 +36,7 @@ class AboutController < ApplicationController def initial_state_params { - settings: {}, + settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token, } end diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index c9725ed00..1efaf619b 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class AccountsController < ApplicationController + PAGE_SIZE = 20 + include AccountControllerConcern before_action :set_cache_headers @@ -17,13 +19,16 @@ class AccountsController < ApplicationController end @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses? - @statuses = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id]) + @statuses = filtered_status_page(params) @statuses = cache_collection(@statuses, Status) - @next_url = next_url unless @statuses.empty? + unless @statuses.empty? + @older_url = older_url if @statuses.last.id > filtered_statuses.last.id + @newer_url = newer_url if @statuses.first.id < filtered_statuses.first.id + end end format.atom do - @entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id]) + @entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]) render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? })) end @@ -70,13 +75,22 @@ class AccountsController < ApplicationController @account = Account.find_local!(params[:username]) end - def next_url + def older_url + ::Rails.logger.info("older: max_id #{@statuses.last.id}, url #{pagination_url(max_id: @statuses.last.id)}") + pagination_url(max_id: @statuses.last.id) + end + + def newer_url + pagination_url(min_id: @statuses.first.id) + end + + def pagination_url(max_id: nil, min_id: nil) if media_requested? - short_account_media_url(@account, max_id: @statuses.last.id) + short_account_media_url(@account, max_id: max_id, min_id: min_id) elsif replies_requested? - short_account_with_replies_url(@account, max_id: @statuses.last.id) + short_account_with_replies_url(@account, max_id: max_id, min_id: min_id) else - short_account_url(@account, max_id: @statuses.last.id) + short_account_url(@account, max_id: max_id, min_id: min_id) end end @@ -87,4 +101,12 @@ class AccountsController < ApplicationController def replies_requested? request.path.ends_with?('/with_replies') end + + def filtered_status_page(params) + if params[:min_id].present? + filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse + else + filtered_statuses.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]).to_a + end + end end diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb new file mode 100644 index 000000000..081914016 --- /dev/null +++ b/app/controllers/activitypub/collections_controller.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +class ActivityPub::CollectionsController < Api::BaseController + include SignatureVerification + + before_action :set_account + before_action :set_size + before_action :set_statuses + + def show + render json: collection_presenter, + serializer: ActivityPub::CollectionSerializer, + adapter: ActivityPub::Adapter, + content_type: 'application/activity+json', + skip_activities: true + end + + private + + def set_account + @account = Account.find_local!(params[:account_username]) + end + + def set_statuses + @statuses = scope_for_collection.paginate_by_max_id(20, params[:max_id], params[:since_id]) + @statuses = cache_collection(@statuses, Status) + end + + def set_size + case params[:id] + when 'featured' + @account.pinned_statuses.count + else + raise ActiveRecord::NotFound + end + end + + def scope_for_collection + case params[:id] + when 'featured' + @account.statuses.permitted_for(@account, signed_request_account).tap do |scope| + scope.merge!(@account.pinned_statuses) + end + else + raise ActiveRecord::NotFound + end + end + + def collection_presenter + ActivityPub::CollectionPresenter.new( + id: account_collection_url(@account, params[:id]), + type: :ordered, + size: @size, + items: @statuses + ) + end +end diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index 76553a162..af51e32d5 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -11,7 +11,7 @@ class ActivityPub::InboxesController < Api::BaseController process_payload head 202 else - [signature_verification_failure_reason, 401] + render plain: signature_verification_failure_reason, status: 401 end end @@ -28,7 +28,7 @@ class ActivityPub::InboxesController < Api::BaseController def upgrade_account if signed_request_account.ostatus? signed_request_account.update(last_webfingered_at: nil) - ResolveRemoteAccountWorker.perform_async(signed_request_account.acct) + ResolveAccountWorker.perform_async(signed_request_account.acct) end Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed? diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 9f97ff622..9ed700c1e 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -1,13 +1,15 @@ # frozen_string_literal: true class ActivityPub::OutboxesController < Api::BaseController + include SignatureVerification + before_action :set_account def show - @statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(20, params[:max_id], params[:since_id]) + @statuses = @account.statuses.permitted_for(@account, signed_request_account).paginate_by_max_id(20, params[:max_id], params[:since_id]) @statuses = cache_collection(@statuses, Status) - render json: outbox_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' + render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end private diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 487282dc3..ce3208209 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -16,9 +16,11 @@ module Admin show_staff_badge bootstrap_timeline_accounts thumbnail + hero min_invite_role activity_api_enabled peers_api_enabled + show_known_fediverse_at_about_page ).freeze BOOLEAN_SETTINGS = %w( @@ -28,10 +30,12 @@ module Admin show_staff_badge activity_api_enabled peers_api_enabled + show_known_fediverse_at_about_page ).freeze UPLOAD_SETTINGS = %w( thumbnail + hero ).freeze def edit diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 52e68ab35..7b5168b31 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -51,6 +51,10 @@ class Api::BaseController < ApplicationController [params[:limit].to_i.abs, default_limit * 2].min end + def truthy_param?(key) + ActiveModel::Type::Boolean.new.cast(params[key]) + end + def current_resource_owner @current_user ||= User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token end diff --git a/app/controllers/api/salmon_controller.rb b/app/controllers/api/salmon_controller.rb index 143e9d3cd..ac5f3268d 100644 --- a/app/controllers/api/salmon_controller.rb +++ b/app/controllers/api/salmon_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Api::SalmonController < Api::BaseController + include SignatureVerification + before_action :set_account respond_to :txt @@ -9,7 +11,7 @@ class Api::SalmonController < Api::BaseController process_salmon head 202 elsif payload.present? - [signature_verification_failure_reason, 401] + render plain: signature_verification_failure_reason, status: 401 else head 400 end diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index da534d960..68af22529 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -20,6 +20,6 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController private def account_params - params.permit(:display_name, :note, :avatar, :header) + params.permit(:display_name, :note, :avatar, :header, :locked) end end diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb index 91a942d75..70236d1a8 100644 --- a/app/controllers/api/v1/accounts/relationships_controller.rb +++ b/app/controllers/api/v1/accounts/relationships_controller.rb @@ -10,7 +10,7 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController accounts = Account.where(id: account_ids).select('id') # .where doesn't guarantee that our results are in the same order # we requested them, so return the "right" order to the requestor. - @accounts = accounts.index_by(&:id).values_at(*account_ids) + @accounts = accounts.index_by(&:id).values_at(*account_ids).compact render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships end @@ -21,6 +21,6 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController end def account_ids - @_account_ids ||= Array(params[:id]).map(&:to_i) + Array(params[:id]).map(&:to_i) end end diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb index 11e647c3c..7649da433 100644 --- a/app/controllers/api/v1/accounts/search_controller.rb +++ b/app/controllers/api/v1/accounts/search_controller.rb @@ -22,8 +22,4 @@ class Api::V1::Accounts::SearchController < Api::BaseController following: truthy_param?(:following) ) end - - def truthy_param?(key) - params[key] == 'true' - end end diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 095f6937b..1e1511a7b 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -28,9 +28,9 @@ class Api::V1::Accounts::StatusesController < Api::BaseController def account_statuses default_statuses.tap do |statuses| - statuses.merge!(only_media_scope) if params[:only_media] - statuses.merge!(pinned_scope) if params[:pinned] - statuses.merge!(no_replies_scope) if params[:exclude_replies] + statuses.merge!(only_media_scope) if truthy_param?(:only_media) + statuses.merge!(pinned_scope) if truthy_param?(:pinned) + statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies) end end @@ -51,7 +51,13 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def account_media_status_ids - @account.media_attachments.attached.reorder(nil).select(:status_id).distinct + # `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids. + # Also, Avoid getting slow by not narrowing down by `statuses.account_id`. + # When narrowing down by `statuses.account_id`, `index_statuses_20180106` will be used + # and the table will be joined by `Merge Semi Join`, so the query will be slow. + Status.joins(:media_attachments).merge(@account.media_attachments).permitted_for(@account, current_account) + .paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) + .reorder(id: :desc).distinct(:id).pluck(:id) end def pinned_scope diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 4e73e9e8b..d64325944 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -13,9 +13,9 @@ class Api::V1::AccountsController < Api::BaseController end def follow - FollowService.new.call(current_user.account, @account.acct, reblogs: params[:reblogs]) + FollowService.new.call(current_user.account, @account.acct, reblogs: truthy_param?(:reblogs)) - options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: params[:reblogs] } }, requested_map: { @account.id => false } } + options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } } render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options) end @@ -26,7 +26,7 @@ class Api::V1::AccountsController < Api::BaseController end def mute - MuteService.new.call(current_user.account, @account, notifications: params[:notifications]) + MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications)) render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb index 9f330f0df..d4e6337e7 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -27,7 +27,7 @@ class Api::V1::MediaController < Api::BaseController private def media_params - params.permit(:file, :description) + params.permit(:file, :description, :focus) end def file_type_error diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb index 22828217d..f5095e073 100644 --- a/app/controllers/api/v1/reports_controller.rb +++ b/app/controllers/api/v1/reports_controller.rb @@ -13,14 +13,14 @@ class Api::V1::ReportsController < Api::BaseController end def create - @report = current_account.reports.create!( - target_account: reported_account, + @report = ReportService.new.call( + current_account, + reported_account, status_ids: reported_status_ids, - comment: report_params[:comment] + comment: report_params[:comment], + forward: report_params[:forward] ) - User.staff.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later } - render json: @report, serializer: REST::ReportSerializer end @@ -39,6 +39,6 @@ class Api::V1::ReportsController < Api::BaseController end def report_params - params.permit(:account_id, :comment, status_ids: []) + params.permit(:account_id, :comment, :forward, status_ids: []) end end diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb index d1b4e0402..99b635ad9 100644 --- a/app/controllers/api/v1/search_controller.rb +++ b/app/controllers/api/v1/search_controller.rb @@ -33,12 +33,8 @@ class Api::V1::SearchController < Api::BaseController SearchService.new.call( params[:q], RESULTS_LIMIT, - resolving_search?, + truthy_param?(:resolve), current_account ) end - - def resolving_search? - params[:resolve] == 'true' - end end diff --git a/app/controllers/api/v1/statuses/pins_controller.rb b/app/controllers/api/v1/statuses/pins_controller.rb index 3de1009b8..bba6a6f48 100644 --- a/app/controllers/api/v1/statuses/pins_controller.rb +++ b/app/controllers/api/v1/statuses/pins_controller.rb @@ -11,12 +11,18 @@ class Api::V1::Statuses::PinsController < Api::BaseController def create StatusPin.create!(account: current_account, status: @status) + distribute_add_activity! render json: @status, serializer: REST::StatusSerializer end def destroy pin = StatusPin.find_by(account: current_account, status: @status) - pin&.destroy! + + if pin + pin.destroy! + distribute_remove_activity! + end + render json: @status, serializer: REST::StatusSerializer end @@ -25,4 +31,24 @@ class Api::V1::Statuses::PinsController < Api::BaseController def set_status @status = Status.find(params[:status_id]) end + + def distribute_add_activity! + json = ActiveModelSerializers::SerializableResource.new( + @status, + serializer: ActivityPub::AddSerializer, + adapter: ActivityPub::Adapter + ).as_json + + ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account) + end + + def distribute_remove_activity! + json = ActiveModelSerializers::SerializableResource.new( + @status, + serializer: ActivityPub::RemoveSerializer, + adapter: ActivityPub::Adapter + ).as_json + + ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account) + end end diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index 49887778e..d7d70b94d 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -21,15 +21,23 @@ class Api::V1::Timelines::PublicController < Api::BaseController end def public_statuses - public_timeline_statuses.paginate_by_max_id( + statuses = public_timeline_statuses.paginate_by_max_id( limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id] ) + + if truthy_param?(:only_media) + # `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids. + status_ids = statuses.joins(:media_attachments).distinct(:id).pluck(:id) + statuses.where(id: status_ids) + else + statuses + end end def public_timeline_statuses - Status.as_public_timeline(current_account, params[:local]) + Status.as_public_timeline(current_account, truthy_param?(:local)) end def insert_pagination_headers @@ -37,7 +45,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController end def pagination_params(core_params) - params.permit(:local, :limit).merge(core_params) + params.permit(:local, :limit, :only_media).merge(core_params) end def next_path diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index 08db04a39..eb32611ad 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -29,16 +29,24 @@ class Api::V1::Timelines::TagController < Api::BaseController if @tag.nil? [] else - tag_timeline_statuses.paginate_by_max_id( + statuses = tag_timeline_statuses.paginate_by_max_id( limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id] ) + + if truthy_param?(:only_media) + # `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids. + status_ids = statuses.joins(:media_attachments).distinct(:id).pluck(:id) + statuses.where(id: status_ids) + else + statuses + end end end def tag_timeline_statuses - Status.as_tag_timeline(@tag, current_account, params[:local]) + Status.as_tag_timeline(@tag, current_account, truthy_param?(:local)) end def insert_pagination_headers @@ -46,7 +54,7 @@ class Api::V1::Timelines::TagController < Api::BaseController end def pagination_params(core_params) - params.permit(:local, :limit).merge(core_params) + params.permit(:local, :limit, :only_media).merge(core_params) end def next_path diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 276c6b012..fc745eaec 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -15,6 +15,7 @@ class ApplicationController < ActionController::Base helper_method :current_flavour helper_method :current_skin helper_method :single_user_mode? + helper_method :use_seamless_external_login? rescue_from ActionController::RoutingError, with: :not_found rescue_from ActiveRecord::RecordNotFound, with: :not_found @@ -35,7 +36,7 @@ class ApplicationController < ActionController::Base end def store_current_location - store_location_for(:user, request.url) + store_location_for(:user, request.url) unless request.format == :json end def require_admin! @@ -145,6 +146,10 @@ class ApplicationController < ActionController::Base @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.exists? end + def use_seamless_external_login? + Devise.pam_authentication || Devise.ldap_authentication + end + def current_account @current_account ||= current_user.try(:account) end diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index 72b8e9dd8..f3e0ae257 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -3,6 +3,7 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController layout 'auth' + before_action :set_user, only: [:finish_signup] before_action :set_pack private @@ -10,4 +11,26 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController def set_pack use_pack 'auth' end + + # GET/PATCH /users/:id/finish_signup + def finish_signup + return unless request.patch? && params[:user] + if @user.update(user_params) + @user.skip_reconfirmation! + sign_in(@user, bypass: true) + redirect_to root_path, notice: I18n.t('devise.confirmations.send_instructions') + else + @show_errors = true + end + end + + private + + def set_user + @user = current_user + end + + def user_params + params.require(:user).permit(:email) + end end diff --git a/app/controllers/auth/omniauth_callbacks_controller.rb b/app/controllers/auth/omniauth_callbacks_controller.rb new file mode 100644 index 000000000..bbf63bed3 --- /dev/null +++ b/app/controllers/auth/omniauth_callbacks_controller.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController + skip_before_action :verify_authenticity_token + + def self.provides_callback_for(provider) + provider_id = provider.to_s.chomp '_oauth2' + + define_method provider do + @user = User.find_for_oauth(request.env['omniauth.auth'], current_user) + + if @user.persisted? + sign_in_and_redirect @user, event: :authentication + set_flash_message(:notice, :success, kind: provider_id.capitalize) if is_navigational_format? + else + session["devise.#{provider}_data"] = request.env['omniauth.auth'] + redirect_to new_user_registration_url + end + end + end + + Devise.omniauth_configs.each_key do |provider| + provides_callback_for provider + end + + def after_sign_in_path_for(resource) + if resource.email_verified? + root_path + else + finish_signup_path + end + end +end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 2b6a1bdbc..9b3ea4f27 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -15,6 +15,11 @@ class Auth::RegistrationsController < Devise::RegistrationsController protected + def update_resource(resource, params) + params[:password] = nil if Devise.pam_authentication && resource.encrypted_password.blank? + super + end + def build_resource(hash = nil) super(hash) diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index f45d77b88..62f3b2eb6 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -11,6 +11,14 @@ class Auth::SessionsController < Devise::SessionsController prepend_before_action :set_pack before_action :set_instance_presenter, only: [:new] + def new + Devise.omniauth_configs.each do |provider, config| + return redirect_to(omniauth_authorize_path(resource_name, provider)) if config.strategy.redirect_at_sign_in + end + + super + end + def create super do |resource| remember_me(resource) @@ -29,7 +37,11 @@ class Auth::SessionsController < Devise::SessionsController if session[:otp_user_id] User.find(session[:otp_user_id]) elsif user_params[:email] - User.find_for_authentication(email: user_params[:email]) + if use_seamless_external_login? && Devise.check_at_sign && user_params[:email].index('@').nil? + User.joins(:account).find_by(accounts: { username: user_params[:email] }) + else + User.find_for_authentication(email: user_params[:email]) + end end end @@ -47,6 +59,14 @@ class Auth::SessionsController < Devise::SessionsController end end + def after_sign_out_path_for(_resource_or_scope) + Devise.omniauth_configs.each_value do |config| + return root_path if config.strategy.redirect_at_sign_in + end + + super + end + def two_factor_enabled? find_user.try(:otp_required_for_login?) end diff --git a/app/controllers/authorize_follows_controller.rb b/app/controllers/authorize_follows_controller.rb index eda50e07d..95052df7c 100644 --- a/app/controllers/authorize_follows_controller.rb +++ b/app/controllers/authorize_follows_controller.rb @@ -46,7 +46,7 @@ class AuthorizeFollowsController < ApplicationController end def account_from_remote_follow - ResolveRemoteAccountService.new.call(acct_without_prefix) + ResolveAccountService.new.call(acct_without_prefix) end def acct_param_is_url? diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb index a9ea60f7d..e697284a8 100644 --- a/app/controllers/concerns/localized.rb +++ b/app/controllers/concerns/localized.rb @@ -17,11 +17,7 @@ module Localized end def default_locale - request_locale || env_locale || I18n.default_locale - end - - def env_locale - ENV['DEFAULT_LOCALE'] + request_locale || I18n.default_locale end def request_locale @@ -29,12 +25,10 @@ module Localized end def preferred_locale - http_accept_language.preferred_language_from([env_locale]) || - http_accept_language.preferred_language_from(I18n.available_locales) + http_accept_language.preferred_language_from(I18n.available_locales) end def compatible_locale - http_accept_language.compatible_language_from([env_locale]) || - http_accept_language.compatible_language_from(I18n.available_locales) + http_accept_language.compatible_language_from(I18n.available_locales) end end diff --git a/app/controllers/concerns/signature_authentication.rb b/app/controllers/concerns/signature_authentication.rb new file mode 100644 index 000000000..beec93223 --- /dev/null +++ b/app/controllers/concerns/signature_authentication.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module SignatureAuthentication + extend ActiveSupport::Concern + + include SignatureVerification + + def current_account + super || signed_request_account + end +end diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 2baafb5bf..f289228d3 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -114,7 +114,7 @@ module SignatureVerification def account_from_key_id(key_id) if key_id.start_with?('acct:') - ResolveRemoteAccountService.new.call(key_id.gsub(/\Aacct:/, '')) + ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) elsif !ActivityPub::TagManager.instance.local_uri?(key_id) account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account) account ||= ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) diff --git a/app/controllers/concerns/user_tracking_concern.rb b/app/controllers/concerns/user_tracking_concern.rb index a2510e55f..be10705fc 100644 --- a/app/controllers/concerns/user_tracking_concern.rb +++ b/app/controllers/concerns/user_tracking_concern.rb @@ -3,7 +3,6 @@ module UserTrackingConcern extend ActiveSupport::Concern - REGENERATE_FEED_DAYS = 14 UPDATE_SIGN_IN_HOURS = 24 included do @@ -14,25 +13,10 @@ module UserTrackingConcern def set_user_activity return unless user_needs_sign_in_update? - - # Mark as signed-in today current_user.update_tracked_fields!(request) - ActivityTracker.record('activity:logins', current_user.id) - - # Regenerate feed if needed - regenerate_feed! if user_needs_feed_update? end def user_needs_sign_in_update? user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_HOURS.hours.ago) end - - def user_needs_feed_update? - current_user.last_sign_in_at < REGENERATE_FEED_DAYS.days.ago - end - - def regenerate_feed! - Redis.current.setnx("account:#{current_user.account_id}:regeneration", true) && Redis.current.expire("account:#{current_user.account_id}:regeneration", 1.day.seconds) - RegenerationWorker.perform_async(current_user.account_id) - end end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 080cbde11..c74d3f86d 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -9,6 +9,8 @@ class FollowerAccountsController < ApplicationController respond_to do |format| format.html do use_pack 'public' + + @relationships = AccountRelationshipsPresenter.new(@follows.map(&:account_id), current_user.account_id) if user_signed_in? end format.json do diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 74e83ad81..4c1e3f327 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -9,6 +9,8 @@ class FollowingAccountsController < ApplicationController respond_to do |format| format.html do use_pack 'public' + + @relationships = AccountRelationshipsPresenter.new(@follows.map(&:target_account_id), current_user.account_id) if user_signed_in? end format.json do diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 7437a647e..a8ec0dcc9 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -35,7 +35,8 @@ class HomeController < ApplicationController end end - redirect_to(default_redirect_path) + matches = request.path.match(%r{\A/web/timelines/tag/(?<tag>.+)\z}) + redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path) end def set_pack diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index f652f5ace..88c7232dd 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -3,20 +3,26 @@ class MediaController < ApplicationController include Authorization - before_action :verify_permitted_status + before_action :set_media_attachment + before_action :verify_permitted_status! def show - redirect_to media_attachment.file.url(:original) + redirect_to @media_attachment.file.url(:original) + end + + def player + @body_classes = 'player' + raise ActiveRecord::RecordNotFound unless @media_attachment.video? || @media_attachment.gifv? end private - def media_attachment - MediaAttachment.attached.find_by!(shortcode: params[:id]) + def set_media_attachment + @media_attachment = MediaAttachment.attached.find_by!(shortcode: params[:id] || params[:medium_id]) end - def verify_permitted_status - authorize media_attachment.status, :show? + def verify_permitted_status! + authorize @media_attachment.status, :show? rescue Mastodon::NotPermittedError # Reraise in order to get a 404 instead of a 403 error code raise ActiveRecord::RecordNotFound diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index 9c03ece86..cf8745576 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -1,7 +1,19 @@ # frozen_string_literal: true class Settings::ExportsController < Settings::BaseController + include Authorization + def show - @export = Export.new(current_account) + @export = Export.new(current_account) + @backups = current_user.backups + end + + def create + authorize :backup, :create? + + backup = current_user.backups.create! + BackupWorker.perform_async(backup.id) + + redirect_to settings_export_path end end diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 7cd1abe0c..c853b5ab7 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -36,6 +36,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_favourite_modal, :setting_delete_modal, :setting_auto_play_gif, + :setting_display_sensitive_media, :setting_reduce_motion, :setting_system_font_ui, :setting_noindex, diff --git a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb index f1fa03f0a..8518c61ee 100644 --- a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb +++ b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb @@ -3,6 +3,8 @@ module Settings module TwoFactorAuthentication class ConfirmationsController < BaseController + before_action :ensure_otp_secret + def new prepare_two_factor_form end @@ -34,6 +36,10 @@ module Settings @provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Rails.configuration.x.local_domain) @qrcode = RQRCode::QRCode.new(@provision_url) end + + def ensure_otp_secret + redirect_to settings_two_factor_authentication_path unless current_user.otp_secret + end end end end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index d67fac0e5..61ffb97d9 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class StatusesController < ApplicationController + include SignatureAuthentication include Authorization layout 'public' diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb index b597ba4bb..e2ea45c83 100644 --- a/app/controllers/stream_entries_controller.rb +++ b/app/controllers/stream_entries_controller.rb @@ -10,6 +10,7 @@ class StreamEntriesController < ApplicationController before_action :set_stream_entry before_action :set_link_headers before_action :check_account_suspension + before_action :set_cache_headers def show respond_to do |format| @@ -20,6 +21,10 @@ class StreamEntriesController < ApplicationController end format.atom do + unless @stream_entry.hidden? + skip_session! + expires_in 3.minutes, public: true + end render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true)) end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 8ed5c8bda..bab4615a1 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -22,10 +22,18 @@ module ApplicationHelper end def add_rtl_body_class(other_classes) - other_classes = "#{other_classes} rtl" if [:ar, :fa, :he].include?(I18n.locale) + other_classes = "#{other_classes} rtl" if locale_direction == 'rtl' other_classes end + def locale_direction + if [:ar, :fa, :he].include?(I18n.locale) + 'rtl' + else + 'ltr' + end + end + def favicon_path env_suffix = Rails.env.production? ? '' : '-dev' "/favicon#{env_suffix}.ico" diff --git a/app/helpers/instance_helper.rb b/app/helpers/instance_helper.rb index 22a19c52b..dd0b25f3e 100644 --- a/app/helpers/instance_helper.rb +++ b/app/helpers/instance_helper.rb @@ -2,7 +2,7 @@ module InstanceHelper def site_title - Setting.site_title.presence || site_hostname + Setting.site_title end def site_hostname diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index a63eb5e43..a2f5917f9 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -16,6 +16,7 @@ module SettingsHelper he: 'עברית', hr: 'Hrvatski', hu: 'Magyar', + hy: 'Հայերեն', id: 'Bahasa Indonesia', io: 'Ido', it: 'Italiano', diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb index 445114985..54b92bdf4 100644 --- a/app/helpers/stream_entries_helper.rb +++ b/app/helpers/stream_entries_helper.rb @@ -8,6 +8,27 @@ module StreamEntriesHelper account.display_name.presence || account.username end + def account_description(account) + prepend_str = [ + [ + number_to_human(account.statuses_count, strip_insignificant_zeros: true), + t('accounts.posts'), + ].join(' '), + + [ + number_to_human(account.following_count, strip_insignificant_zeros: true), + t('accounts.following'), + ].join(' '), + + [ + number_to_human(account.followers_count, strip_insignificant_zeros: true), + t('accounts.followers'), + ].join(' '), + ].join(', ') + + [prepend_str, account.note].join(' · ') + end + def stream_link_target embedded_view? ? '_blank' : nil end diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js index c0bd09bdd..b4125e84e 100644 --- a/app/javascript/core/admin.js +++ b/app/javascript/core/admin.js @@ -30,13 +30,13 @@ delegate(document, batchCheckboxClassName, 'change', () => { }); delegate(document, '.media-spoiler-show-button', 'click', () => { - [].forEach.call(document.querySelectorAll('.activity-stream .media-spoiler-wrapper'), (content) => { - content.classList.add('media-spoiler-wrapper__visible'); + [].forEach.call(document.querySelectorAll('button.media-spoiler'), (element) => { + element.click(); }); }); delegate(document, '.media-spoiler-hide-button', 'click', () => { - [].forEach.call(document.querySelectorAll('.activity-stream .media-spoiler-wrapper'), (content) => { - content.classList.remove('media-spoiler-wrapper__visible'); + [].forEach.call(document.querySelectorAll('.spoiler-button.spoiler-button--visible button'), (element) => { + element.click(); }); }); diff --git a/app/javascript/flavours/glitch/components/intersection_observer_article.js b/app/javascript/flavours/glitch/components/intersection_observer_article.js index 8b06f9a8c..f7f6b0a53 100644 --- a/app/javascript/flavours/glitch/components/intersection_observer_article.js +++ b/app/javascript/flavours/glitch/components/intersection_observer_article.js @@ -1,15 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; +import ImmutablePureComponent from 'react-immutable-pure-component'; import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task'; import getRectFromEntry from 'flavours/glitch/util/get_rect_from_entry'; -import { is } from 'immutable'; -// Diff these props in the "rendered" state -const updateOnPropsForRendered = ['id', 'index', 'listLength']; -// Diff these props in the "unrendered" state -const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight']; - -export default class IntersectionObserverArticle extends React.Component { +export default class IntersectionObserverArticle extends ImmutablePureComponent { static propTypes = { intersectionObserverWrapper: PropTypes.object.isRequired, @@ -27,15 +22,18 @@ export default class IntersectionObserverArticle extends React.Component { } shouldComponentUpdate (nextProps, nextState) { - const isUnrendered = !this.state.isIntersecting && (this.state.isHidden || this.props.cachedHeight); - const willBeUnrendered = !nextState.isIntersecting && (nextState.isHidden || nextProps.cachedHeight); - if (!!isUnrendered !== !!willBeUnrendered) { - // If we're going from rendered to unrendered (or vice versa) then update + if (!nextState.isIntersecting && nextState.isHidden) { + // It's only if we're not intersecting (i.e. offscreen) and isHidden is true + // that either "isIntersecting" or "isHidden" matter, and then they're + // the only things that matter (and updated ARIA attributes). + return this.state.isIntersecting || !this.state.isHidden || nextProps.listLength !== this.props.listLength; + } else if (nextState.isIntersecting && !this.state.isIntersecting) { + // If we're going from a non-intersecting state to an intersecting state, + // (i.e. offscreen to onscreen), then we definitely need to re-render return true; } - // Otherwise, diff based on props - const propsToDiff = isUnrendered ? updateOnPropsForUnrendered : updateOnPropsForRendered; - return !propsToDiff.every(prop => is(nextProps[prop], this.props[prop])); + // Otherwise, diff based on "updateOnProps" and "updateOnStates" + return super.shouldComponentUpdate(nextProps, nextState); } componentDidMount () { diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index feffc2d59..d63b6a639 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -47,7 +47,6 @@ export default class Status extends ImmutablePureComponent { state = { isExpanded: null, - markedForDelete: false, } // Avoid checking props that are functions (and whose equality will always @@ -67,7 +66,6 @@ export default class Status extends ImmutablePureComponent { updateOnStates = [ 'isExpanded', - 'markedForDelete', ] // If our settings have changed to disable collapsed statuses, then we @@ -382,7 +380,6 @@ export default class Status extends ImmutablePureComponent { const computedClass = classNames('status', `status-${status.get('visibility')}`, { collapsed: isExpanded === false, 'has-background': isExpanded === false && background, - 'marked-for-delete': this.state.markedForDelete, muted, }, 'focusable'); diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index 767e4da47..f346bd108 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -15,6 +15,7 @@ const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, + unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, }); @injectIntl @@ -23,6 +24,7 @@ export default class Header extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map, onFollow: PropTypes.func.isRequired, + onBlock: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; @@ -35,11 +37,21 @@ export default class Header extends ImmutablePureComponent { let displayName = account.get('display_name_html'); let info = ''; + let mutingInfo = ''; let actionBtn = ''; if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) { info = <span className='account--follows-info'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>; } + else if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) { + info = <span className='account--follows-info'><FormattedMessage id='account.blocked' defaultMessage='Blocked' /></span>; + } + + if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) { + mutingInfo = <span className='account--muting-info'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>; + } else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) { + mutingInfo = <span className='account--muting-info'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain hidden' /></span>; + } if (me !== account.get('id')) { if (account.getIn(['relationship', 'requested'])) { @@ -54,6 +66,12 @@ export default class Header extends ImmutablePureComponent { <IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} /> </div> ); + } else if (account.getIn(['relationship', 'blocking'])) { + actionBtn = ( + <div className='account--action-button'> + <IconButton size={26} icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} /> + </div> + ); } } @@ -78,6 +96,7 @@ export default class Header extends ImmutablePureComponent { <div className='account__header__content' dangerouslySetInnerHTML={{ __html: emojify(text) }} /> {info} + {mutingInfo} {actionBtn} </div> </div> diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js index 4ad677fbe..092034664 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js @@ -76,6 +76,7 @@ export default class Header extends ImmutablePureComponent { <InnerHeader account={account} onFollow={this.handleFollow} + onBlock={this.handleBlock} /> <ActionBar diff --git a/app/javascript/flavours/glitch/styles/about.scss b/app/javascript/flavours/glitch/styles/about.scss index 31c079cc5..d97e23696 100644 --- a/app/javascript/flavours/glitch/styles/about.scss +++ b/app/javascript/flavours/glitch/styles/about.scss @@ -1,4 +1,183 @@ +$maximum-width: 1235px; +$fluid-breakpoint: $maximum-width + 20px; +$column-breakpoint: 700px; +$small-breakpoint: 960px; + +.container { + box-sizing: border-box; + max-width: $maximum-width; + margin: 0 auto; + position: relative; + + @media screen and (max-width: $fluid-breakpoint) { + width: 100%; + padding: 0 10px; + } +} + .landing-page { + .grid { + display: grid; + grid-gap: 10px; + grid-template-columns: 1fr 2fr; + grid-auto-columns: 25%; + grid-auto-rows: max-content; + + .column-0 { + display: none; + } + + .column-1 { + grid-column: 1; + grid-row: 1; + } + + .column-2 { + grid-column: 2; + grid-row: 1; + } + + .column-3 { + grid-column: 3; + grid-row: 1 / 3; + } + + .column-4 { + grid-column: 1 / 3; + grid-row: 2; + } + } + + @media screen and (max-width: $small-breakpoint) { + .grid { + grid-template-columns: 40% 60%; + + .column-0 { + display: none; + } + + .column-1 { + grid-column: 1; + grid-row: 1; + + &.non-preview .landing-page__forms { + height: 100%; + } + } + + .column-2 { + grid-column: 2; + grid-row: 1 / 3; + + &.non-preview { + grid-column: 2; + grid-row: 1; + } + } + + .column-3 { + grid-column: 1; + grid-row: 2 / 4; + } + + .column-4 { + grid-column: 2; + grid-row: 3; + + &.non-preview { + grid-column: 1 / 3; + grid-row: 2; + } + } + } + } + + @media screen and (max-width: $column-breakpoint) { + .grid { + grid-template-columns: auto; + + .column-0 { + display: block; + grid-column: 1; + grid-row: 1; + } + + .column-1 { + grid-column: 1; + grid-row: 3; + + .brand { + display: none; + } + } + + .column-2 { + grid-column: 1; + grid-row: 2; + + .landing-page__logo, + .landing-page__call-to-action { + display: none; + } + + &.non-preview { + grid-column: 1; + grid-row: 2; + } + } + + .column-3 { + grid-column: 1; + grid-row: 5; + } + + .column-4 { + grid-column: 1; + grid-row: 4; + + &.non-preview { + grid-column: 1; + grid-row: 4; + } + } + } + } + + .column-flex { + display: flex; + flex-direction: column; + } + + .separator-or { + position: relative; + margin: 40px 0; + text-align: center; + + &::before { + content: ""; + display: block; + width: 100%; + height: 0; + border-bottom: 1px solid rgba($ui-base-lighter-color, .6); + position: absolute; + top: 50%; + left: 0; + } + + span { + display: inline-block; + background: $ui-base-color; + font-size: 12px; + font-weight: 500; + color: $ui-primary-color; + text-transform: uppercase; + position: relative; + z-index: 1; + padding: 0 8px; + cursor: default; + } + } + p, li { font-family: 'mastodon-font-sans-serif', sans-serif; @@ -116,10 +295,14 @@ } hr { - border-color: rgba($ui-base-lighter-color, .6); + width: 100%; + height: 0; + border: 0; + border-bottom: 1px solid rgba($ui-base-lighter-color, .6); + margin: 20px 0; } - .container { + .container-alt { width: 100%; box-sizing: border-box; max-width: 800px; @@ -152,24 +335,20 @@ } } } + } - .mascot-container { - max-width: 800px; - margin: 0 auto; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 100%; + .brand { + a { + padding-left: 0; + padding-right: 0; + color: $white; } - .mascot { - position: absolute; - bottom: -14px; - width: auto; - height: auto; - left: 60px; - z-index: 3; + img { + height: 32px; + position: relative; + top: 4px; + left: -10px; } } @@ -177,7 +356,7 @@ line-height: 30px; overflow: hidden; - .container { + .container-alt { display: flex; justify-content: space-between; } @@ -203,21 +382,6 @@ } } - .brand { - a { - padding-left: 0; - padding-right: 0; - color: $white; - } - - img { - height: 32px; - position: relative; - top: 4px; - left: -10px; - } - } - ul { list-style: none; margin: 0; @@ -243,53 +407,6 @@ align-items: center; position: relative; - .floats { - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - - div { - position: absolute; - transition: all 0.1s linear; - animation-name: floating; - animation-iteration-count: infinite; - animation-direction: alternate; - animation-timing-function: ease-in-out; - z-index: 2; - } - - .float-1 { - width: 324px; - height: 170px; - right: -120px; - bottom: 0; - animation-duration: 3s; - background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 447.1875 234.375" height="170" width="324"><path fill="#{hex-color($ui-base-lighter-color)}" d="M21.69 233.366c-6.45-1.268-13.347-5.63-16.704-10.564-10.705-15.734-1.513-37.724 18.632-44.57l4.8-1.632.173-17.753c.146-14.77.515-19.063 2.2-25.55 6.736-25.944 24.46-46.032 47.766-54.137 11.913-4.143 19.558-5.366 34.178-5.47l13.828-.096V71.12c0-4.755 2.853-17.457 5.238-23.327 8.588-21.137 26.735-35.957 52.153-42.593 23.248-6.07 50.153-6.415 71.863-.923 11.14 2.82 25.686 9.957 33.857 16.615 19.335 15.756 31.82 41.05 35.183 71.275.59 5.305.672 5.435 3.11 4.926 11.833-2.474 30.4-3.132 40.065-1.42 24.388 4.32 40.568 19.076 47.214 43.058 2.16 7.8 3.953 23.894 3.59 32.237l-.24 5.498 5.156 1.317c6.392 1.633 14.55 7.098 18.003 12.062 1.435 2.062 3.305 6.597 4.156 10.078 1.428 5.84 1.43 6.8.04 12.44-1.807 7.318-5.672 13.252-10.872 16.694-8.508 5.63 3.756 5.33-211.916 5.216-108.56-.056-199.22-.464-201.47-.906z"/></svg>'); - } - - .float-2 { - width: 241px; - height: 100px; - right: 210px; - bottom: 0; - animation-duration: 3.5s; - animation-delay: 0.2s; - background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 536.25 222.1875" height="100" width="241"><path fill="#{hex-color($ui-base-lighter-color)}" d="M42.626 221.23c-14.104-1.174-26.442-5.133-32.825-10.534-4.194-3.548-7.684-10.66-8.868-18.075-1.934-12.102.633-22.265 7.528-29.81 7.61-8.328 19.998-12.76 39.855-14.257l8.47-.638-2.08-6.223c-4.826-14.422-6.357-24.813-6.37-43.255-.012-14.923.28-18.513 2.1-25.724 2.283-9.048 8.483-23.034 13.345-30.1 14.76-21.45 43.505-38.425 70.535-41.65 30.628-3.655 64.47 12.073 89.668 41.673l5.955 6.995 2.765-4.174c1.52-2.296 5.74-6.93 9.376-10.295 18.382-17.02 43.436-20.676 73.352-10.705 12.158 4.052 21.315 9.53 29.64 17.733 12.752 12.562 18.16 25.718 18.19 44.26l.02 10.98 2.312-3.01c15.64-20.365 42.29-20.485 62.438-.28 3.644 3.653 7.558 8.593 8.697 10.976 4.895 10.24 5.932 25.688 2.486 37.046-.76 2.507-1.388 4.816-1.393 5.13-.006.316 6.845.87 15.224 1.234 53.06 2.297 76.356 12.98 81.817 37.526 3.554 15.973-3.71 28.604-19.566 34.02-4.554 1.555-17.922 1.655-234.517 1.757-126.327.06-233.497-.21-238.154-.597z"/></svg>'); - } - - .float-3 { - width: 267px; - height: 140px; - right: 110px; - top: -30px; - animation-duration: 4s; - animation-delay: 0.5s; - background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 388.125 202.5" height="140" width="267"><path fill="#{hex-color($ui-base-lighter-color)}" d="M181.37 201.458c-17.184-1.81-36.762-8.944-49.523-18.05l-5.774-4.12-8.074 2.63c-11.468 3.738-21.382 4.962-35.815 4.422-14.79-.554-24.577-2.845-36.716-8.594-15.483-7.332-28.498-19.98-35.985-34.968C2.44 128.675-.94 108.435.9 91.356c3.362-31.234 18.197-53.698 43.63-66.074 12.803-6.23 22.384-8.55 37.655-9.122 14.433-.54 24.347.684 35.814 4.42l8.073 2.633 5.635-4.01c24.81-17.656 60.007-23.332 92.914-14.985 10.11 2.565 25.498 9.62 33.102 15.178l5.068 3.704 7.632-2.564c10.89-3.66 21.086-4.916 35.516-4.376 45.816 1.716 76.422 30.03 81.285 75.196 1.84 17.08-1.54 37.32-8.585 51.422-7.487 14.99-20.502 27.636-35.984 34.968-12.14 5.75-21.926 8.04-36.716 8.593-14.43.54-24.626-.716-35.516-4.376l-7.632-2.564-5.068 3.704c-12.844 9.387-32.714 16.488-51.545 18.42-10.607 1.09-13.916 1.08-24.81-.066z"/></svg>'); - } - } - .heading { position: relative; z-index: 4; @@ -346,18 +463,18 @@ background: darken($ui-base-color, 4%); padding: 20px 0; - .container { + .container-alt { position: relative; padding-right: 280px + 15px; } - .information-board-sections { + &__sections { display: flex; justify-content: space-between; flex-wrap: wrap; } - .section { + &__section { flex: 1 0 0; font-family: 'mastodon-font-sans-serif', sans-serif; font-size: 16px; @@ -382,6 +499,10 @@ font-size: 32px; line-height: 48px; } + + @media screen and (max-width: $column-breakpoint) { + text-align: center; + } } .panel { @@ -428,15 +549,12 @@ height: 80px; margin: 0 auto; margin-bottom: 15px; - @include avatar-size(80px); img { display: block; width: 80px; height: 80px; border-radius: 48px; - @include avatar-radius(); - @include avatar-size(80px); } } @@ -463,111 +581,298 @@ } } - .features { - padding: 50px 0; + &.alternative { + padding: 10px 0; - .container { - display: flex; - } + .brand { + text-align: center; + padding: 30px 0; + margin-bottom: 10px; - #mastodon-timeline { - display: flex; - -webkit-overflow-scrolling: touch; - -ms-overflow-style: -ms-autohiding-scrollbar; - font-family: 'mastodon-font-sans-serif', sans-serif; - font-size: 13px; - line-height: 18px; - font-weight: 400; - color: $primary-text-color; - width: 330px; - margin-right: 30px; - flex: 0 0 auto; - background: $ui-base-color; - overflow: hidden; - border-radius: 4px; - box-shadow: 0 0 6px rgba($black, 0.1); + img { + position: static; + padding: 10px 0; + } - .column-header { - color: inherit; - font-family: inherit; - font-size: 16px; - line-height: inherit; - font-weight: inherit; - margin: 0; - padding: 15px; + @media screen and (max-width: $small-breakpoint) { + padding: 15px 0; } - .column { + @media screen and (max-width: $column-breakpoint) { padding: 0; - border-radius: 4px; - overflow: hidden; + margin-bottom: -10px; } + } + } - .scrollable { - height: 400px; + &__information, + &__forms { + padding: 20px; + } + + &__call-to-action { + background: darken($ui-base-color, 4%); + border-radius: 4px; + padding: 25px 40px; + overflow: hidden; + + .row { + display: flex; + flex-direction: row-reverse; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + } + + .row__information-board { + display: flex; + justify-content: flex-end; + align-items: flex-end; + + .information-board__section { + flex: 1 0 auto; + padding: 0 10px; } + } + + .row__mascot { + flex: 1; + margin: 10px -50px 0 0; + } + } + + &__logo { + margin-right: 20px; + + img { + height: 50px; + width: auto; + mix-blend-mode: lighten; + } + } + + &__information { + padding: 45px 40px; + margin-bottom: 10px; + + &:last-child { + margin-bottom: 0; + } + + @media screen and (max-width: $column-breakpoint) { + padding: 25px 20px; + } + } + + &__information, + &__forms, + #mastodon-timeline { + box-sizing: border-box; + background: $ui-base-color; + border-radius: 4px; + box-shadow: 0 0 6px rgba($black, 0.1); + } + + &__mascot { + height: 104px; + position: relative; + left: -40px; + bottom: 25px; + + img { + height: 190px; + width: auto; + } + } + + &__short-description { + .row { + display: flex; + flex-wrap: wrap; + align-items: center; + margin-bottom: 40px; + } - p { - font-size: inherit; - line-height: inherit; - font-weight: inherit; - color: $primary-text-color; + @media screen and (max-width: $column-breakpoint) { + .row { margin-bottom: 20px; + } + } - &:last-child { - margin-bottom: 0; - } + p a { + color: $ui-secondary-color; + } - a { + h1 { + font-weight: 500; + color: $primary-text-color; + margin-bottom: 0; + + small { + color: $ui-primary-color; + + span { color: $ui-secondary-color; - text-decoration: none; } } } - .about-mastodon { - max-width: 675px; + p:last-child { + margin-bottom: 0; + } + } - p { - margin-bottom: 20px; + &__hero { + margin-bottom: 10px; + + img { + display: block; + margin: 0; + max-width: 100%; + height: auto; + border-radius: 4px; + } + } + + &__forms { + height: 100%; + + @media screen and (max-width: $small-breakpoint) { + height: auto; + } + + @media screen and (max-width: $column-breakpoint) { + background: transparent; + box-shadow: none; + padding: 0 20px; + margin-top: 30px; + margin-bottom: 40px; + + .separator-or { + span { + background: darken($ui-base-color, 8%); + } } + } - .features-list { - margin-top: 20px; + hr { + margin: 40px 0; + } - .features-list__row { - display: flex; - padding: 10px 0; - justify-content: space-between; + .button { + display: block; + } - &:first-child { - padding-top: 0; - } + .subtle-hint a { + text-decoration: none; - .visual { - flex: 0 0 auto; - display: flex; - align-items: center; - margin-left: 15px; + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } + } - .fa { - display: block; - color: $ui-primary-color; - font-size: 48px; - } - } + #mastodon-timeline { + display: flex; + -webkit-overflow-scrolling: touch; + -ms-overflow-style: -ms-autohiding-scrollbar; + font-family: 'mastodon-font-sans-serif', sans-serif; + font-size: 13px; + line-height: 18px; + font-weight: 400; + color: $primary-text-color; + width: 100%; + flex: 1 1 auto; + overflow: hidden; + height: 100%; - .text { - font-size: 16px; - line-height: 30px; - color: $ui-primary-color; + .column-header { + color: inherit; + font-family: inherit; + font-size: 16px; + line-height: inherit; + font-weight: inherit; + } - h6 { - font-size: inherit; - line-height: inherit; - margin-bottom: 0; - } - } + .column { + padding: 0; + border-radius: 4px; + overflow: hidden; + width: 100%; + } + + .scrollable { + height: 400px; + } + + p { + font-size: inherit; + line-height: inherit; + font-weight: inherit; + color: $primary-text-color; + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; + } + + a { + color: $ui-secondary-color; + text-decoration: none; + } + } + + @media screen and (max-width: $column-breakpoint) { + height: 90vh; + } + } + + &__features { + .features-list { + margin: 40px 0 !important; + } + + &__action { + text-align: center; + } + } + + .features-list { + margin-top: 20px; + + .features-list__row { + display: flex; + padding: 10px 0; + justify-content: space-between; + + &:first-child { + padding-top: 0; + } + + .visual { + flex: 0 0 auto; + display: flex; + align-items: center; + margin-left: 15px; + + .fa { + display: block; + color: $ui-primary-color; + font-size: 48px; + } + } + + .text { + font-size: 16px; + line-height: 30px; + color: $ui-primary-color; + + h6 { + font-size: inherit; + line-height: inherit; + margin-bottom: 0; } } } @@ -603,21 +908,31 @@ } } + &__footer { + margin-top: 10px; + text-align: center; + color: $ui-base-lighter-color; + + p { + font-size: 14px; + + a { + color: inherit; + text-decoration: underline; + } + } + } + @media screen and (max-width: 840px) { - .container { + .container-alt { padding: 0 20px; } .information-board { - - .container { + .container-alt { padding-right: 20px; } - .section { - text-align: center; - } - .panel { position: static; margin-top: 20px; @@ -629,16 +944,6 @@ } } } - - .header-wrapper .mascot { - left: 20px; - } - } - - @media screen and (max-width: 689px) { - .header-wrapper .mascot { - display: none; - } } @media screen and (max-width: 675px) { @@ -654,13 +959,12 @@ } } - .header .container, - .features .container { + .header .container-alt, + .features .container-alt { display: block; } .header { - .links { padding-top: 15px; background: darken($ui-base-color, 4%); @@ -685,10 +989,6 @@ margin-top: 30px; padding: 0; - .floats { - display: none; - } - .heading { padding: 30px 20px; text-align: center; @@ -703,16 +1003,6 @@ } } } - - .features #mastodon-timeline { - height: 70vh; - width: 100%; - margin-bottom: 50px; - - .column { - width: 100%; - } - } } .cta { @@ -720,108 +1010,55 @@ } &.tag-page { - .features { - padding: 30px 0; - - .container { - max-width: 820px; - - #mastodon-timeline { - margin-right: 0; - border-top-right-radius: 0; - } - - .about-mastodon { - .about-hashtag { - background: darken($ui-base-color, 4%); - padding: 0 20px 20px 30px; - border-radius: 0 5px 5px 0; - - .brand { - padding-top: 20px; - margin-bottom: 20px; - - img { - height: 48px; - width: auto; - } - } - - p { - strong { - color: $ui-secondary-color; - font-weight: 700; - } - } - - .cta { - margin: 0; - - .button { - margin-right: 4px; - } - } - } + .grid { + @media screen and (min-width: $small-breakpoint) { + grid-template-columns: 33% 67%; + } - .features-list { - margin-left: 30px; - margin-right: 10px; - } - } + .column-2 { + grid-column: 2; + grid-row: 1; } } - @media screen and (max-width: 675px) { - .features { - padding: 10px 0; + .brand { + text-align: unset; + padding: 0; - .container { - display: flex; - flex-direction: column; - - #mastodon-timeline { - order: 2; - flex: 0 0 auto; - height: 60vh; - margin-bottom: 20px; - border-top-right-radius: 4px; - } + img { + height: 48px; + width: auto; + } + } - .about-mastodon { - order: 1; - flex: 0 0 auto; - max-width: 100%; + .cta { + margin: 0; - .about-hashtag { - background: unset; - padding: 0; - border-radius: 0; + .button { + margin-right: 4px; + } + } - .cta { - margin: 20px 0; - } - } + @media screen and (max-width: $column-breakpoint) { + .grid { + .column-1 { + grid-column: 1; + grid-row: 2; + } - .features-list { - display: none; - } - } + .column-2 { + grid-column: 1; + grid-row: 1; } } - } - } -} -@keyframes floating { - from { - transform: translate(0, 0); - } - - 65% { - transform: translate(0, 4px); - } + .brand { + margin: 0; + } - to { - transform: translate(0, -0); + .landing-page__features { + display: none; + } + } } } diff --git a/app/javascript/flavours/glitch/styles/basics.scss b/app/javascript/flavours/glitch/styles/basics.scss index b5d77ff63..15fbb1c89 100644 --- a/app/javascript/flavours/glitch/styles/basics.scss +++ b/app/javascript/flavours/glitch/styles/basics.scss @@ -118,5 +118,6 @@ button { height: 100%; align-items: center; justify-content: center; + outline: 0 !important; } } diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index 67096018f..2bc894d25 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -357,6 +357,22 @@ border-radius: 4px; } +.account--muting-info { + color: $primary-text-color; + position: absolute; + top: 40px; + left: 10px; + opacity: 0.7; + display: inline-block; + vertical-align: top; + background-color: rgba($base-overlay-background, 0.4); + text-transform: uppercase; + font-size: 11px; + font-weight: 500; + padding: 4px; + border-radius: 4px; +} + .account--action-button { position: absolute; top: 10px; diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss index 6e03650ed..5dcfab4d1 100644 --- a/app/javascript/flavours/glitch/styles/components/columns.scss +++ b/app/javascript/flavours/glitch/styles/components/columns.scss @@ -242,14 +242,29 @@ .column-header { display: flex; - padding: 15px; font-size: 16px; + padding: 15px; background: lighten($ui-base-color, 4%); flex: 0 0 auto; cursor: pointer; position: relative; z-index: 2; outline: 0; + overflow: hidden; + + & > button { + margin: 0; + border: none; + padding: 15px 0 15px 15px; + color: inherit; + background: transparent; + font: inherit; + text-align: left; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + flex: 1; + } &.active { box-shadow: 0 1px 0 rgba($ui-highlight-color, 0.3); diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index ebc4ee383..3d16c856d 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -40,14 +40,20 @@ cursor: default; } - &.button-alternative { + &.button-primary, + &.button-alternative, + &.button-secondary, + &.button-alternative-2 { font-size: 16px; line-height: 36px; height: auto; - color: $ui-base-color; - background: $ui-primary-color; text-transform: none; padding: 4px 16px; + } + + &.button-alternative { + color: $ui-base-color; + background: $ui-primary-color; &:active, &:focus, @@ -56,6 +62,16 @@ } } + &.button-alternative-2 { + background: $ui-base-lighter-color; + + &:active, + &:focus, + &:hover { + background-color: lighten($ui-base-lighter-color, 4%); + } + } + &.button-secondary { font-size: 16px; line-height: 36px; diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss index 81d4692a8..4dd748227 100644 --- a/app/javascript/flavours/glitch/styles/components/media.scss +++ b/app/javascript/flavours/glitch/styles/components/media.scss @@ -84,8 +84,8 @@ height: 110px; .detailed-status & { - margin-left: -22px; - width: calc(100% + 44px); + margin-left: -14px; + width: calc(100% + 28px); height: 250px; } diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss index af2589e23..6fa1fa38f 100644 --- a/app/javascript/flavours/glitch/styles/containers.scss +++ b/app/javascript/flavours/glitch/styles/containers.scss @@ -1,4 +1,4 @@ -.container { +.container-alt { width: 700px; margin: 0 auto; margin-top: 40px; diff --git a/app/javascript/images/icon_file_download.svg b/app/javascript/images/icon_file_download.svg new file mode 100644 index 000000000..53e97e4f8 --- /dev/null +++ b/app/javascript/images/icon_file_download.svg @@ -0,0 +1,4 @@ +<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> + <path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/> + <path d="M0 0h24v24H0z" fill="none"/> +</svg> \ No newline at end of file diff --git a/app/javascript/images/mailer/icon_cached.png b/app/javascript/images/mailer/icon_cached.png index e62c064c8..210833d34 100644 --- a/app/javascript/images/mailer/icon_cached.png +++ b/app/javascript/images/mailer/icon_cached.png Binary files differdiff --git a/app/javascript/images/mailer/icon_file_download.png b/app/javascript/images/mailer/icon_file_download.png new file mode 100644 index 000000000..8a6a8673b --- /dev/null +++ b/app/javascript/images/mailer/icon_file_download.png Binary files differdiff --git a/app/javascript/images/reticle.png b/app/javascript/images/reticle.png new file mode 100644 index 000000000..998994f5c --- /dev/null +++ b/app/javascript/images/reticle.png Binary files differdiff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 8a35049b3..130b4af23 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -1,6 +1,7 @@ import api from '../api'; import { throttle } from 'lodash'; import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light'; +import { tagHistory } from '../settings'; import { useEmoji } from './emojis'; import { @@ -27,6 +28,9 @@ export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO'; export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR'; export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY'; export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT'; +export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE'; + +export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE'; export const COMPOSE_MOUNT = 'COMPOSE_MOUNT'; export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT'; @@ -92,8 +96,9 @@ export function mentionCompose(account, router) { export function submitCompose() { return function (dispatch, getState) { const status = getState().getIn(['compose', 'text'], ''); + const media = getState().getIn(['compose', 'media_attachments']); - if (!status || !status.length) { + if ((!status || !status.length) && media.size === 0) { return; } @@ -102,7 +107,7 @@ export function submitCompose() { api(getState).post('/api/v1/statuses', { status, in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), - media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')), + media_ids: media.map(item => item.get('id')), sensitive: getState().getIn(['compose', 'sensitive']), spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''), visibility: getState().getIn(['compose', 'privacy']), @@ -111,6 +116,7 @@ export function submitCompose() { 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), }, }).then(function (response) { + dispatch(insertIntoTagHistory(response.data.tags)); dispatch(submitComposeSuccess({ ...response.data })); // To make the app more responsive, immediately get the status into the columns @@ -178,11 +184,11 @@ export function uploadCompose(files) { }; }; -export function changeUploadCompose(id, description) { +export function changeUploadCompose(id, params) { return (dispatch, getState) => { dispatch(changeUploadComposeRequest()); - api(getState).put(`/api/v1/media/${id}`, { description }).then(response => { + api(getState).put(`/api/v1/media/${id}`, params).then(response => { dispatch(changeUploadComposeSuccess(response.data)); }).catch(error => { dispatch(changeUploadComposeFail(id, error)); @@ -273,12 +279,22 @@ const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => { dispatch(readyComposeSuggestionsEmojis(token, results)); }; +const fetchComposeSuggestionsTags = (dispatch, getState, token) => { + dispatch(updateSuggestionTags(token)); +}; + export function fetchComposeSuggestions(token) { return (dispatch, getState) => { - if (token[0] === ':') { + switch (token[0]) { + case ':': fetchComposeSuggestionsEmojis(dispatch, getState, token); - } else { + break; + case '#': + fetchComposeSuggestionsTags(dispatch, getState, token); + break; + default: fetchComposeSuggestionsAccounts(dispatch, getState, token); + break; } }; }; @@ -308,6 +324,9 @@ export function selectComposeSuggestion(position, token, suggestion) { startPosition = position - 1; dispatch(useEmoji(suggestion)); + } else if (suggestion[0] === '#') { + completion = suggestion; + startPosition = position - 1; } else { completion = getState().getIn(['accounts', suggestion, 'acct']); startPosition = position; @@ -322,6 +341,48 @@ export function selectComposeSuggestion(position, token, suggestion) { }; }; +export function updateSuggestionTags(token) { + return { + type: COMPOSE_SUGGESTION_TAGS_UPDATE, + token, + }; +} + +export function updateTagHistory(tags) { + return { + type: COMPOSE_TAG_HISTORY_UPDATE, + tags, + }; +} + +export function hydrateCompose() { + return (dispatch, getState) => { + const me = getState().getIn(['meta', 'me']); + const history = tagHistory.get(me); + + if (history !== null) { + dispatch(updateTagHistory(history)); + } + }; +} + +function insertIntoTagHistory(tags) { + return (dispatch, getState) => { + const state = getState(); + const oldHistory = state.getIn(['compose', 'tagHistory']); + const me = state.getIn(['meta', 'me']); + const names = tags.map(({ name }) => name); + const intersectedOldHistory = oldHistory.filter(name => !names.includes(name)); + + names.push(...intersectedOldHistory.toJS()); + + const newHistory = names.slice(0, 1000); + + tagHistory.set(me, newHistory); + dispatch(updateTagHistory(newHistory)); + }; +} + export function mountCompose() { return { type: COMPOSE_MOUNT, diff --git a/app/javascript/mastodon/actions/dropdown_menu.js b/app/javascript/mastodon/actions/dropdown_menu.js new file mode 100644 index 000000000..217ba4e74 --- /dev/null +++ b/app/javascript/mastodon/actions/dropdown_menu.js @@ -0,0 +1,10 @@ +export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN'; +export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE'; + +export function openDropdownMenu(id, placement) { + return { type: DROPDOWN_MENU_OPEN, id, placement }; +} + +export function closeDropdownMenu(id) { + return { type: DROPDOWN_MENU_CLOSE, id }; +} diff --git a/app/javascript/mastodon/actions/push_notifications/registerer.js b/app/javascript/mastodon/actions/push_notifications/registerer.js index 5f47a5501..51e68cad1 100644 --- a/app/javascript/mastodon/actions/push_notifications/registerer.js +++ b/app/javascript/mastodon/actions/push_notifications/registerer.js @@ -1,6 +1,7 @@ import api from '../../api'; import { pushNotificationsSetting } from '../../settings'; import { setBrowserSupport, setSubscription, clearSubscription } from './setter'; +import { me } from '../../initial_state'; // Taken from https://www.npmjs.com/package/web-push const urlBase64ToUint8Array = (base64String) => { @@ -35,7 +36,7 @@ const subscribe = (registration) => const unsubscribe = ({ registration, subscription }) => subscription ? subscription.unsubscribe().then(() => registration) : registration; -const sendSubscriptionToBackend = (getState, subscription, me) => { +const sendSubscriptionToBackend = (getState, subscription) => { const params = { subscription }; if (me) { @@ -54,7 +55,6 @@ const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' export function register () { return (dispatch, getState) => { dispatch(setBrowserSupport(supportsPushNotifications)); - const me = getState().getIn(['meta', 'me']); if (me && !pushNotificationsSetting.get(me)) { const alerts = getState().getIn(['push_notifications', 'alerts']); @@ -85,13 +85,13 @@ export function register () { } else { // Something went wrong, try to subscribe again return unsubscribe({ registration, subscription }).then(subscribe).then( - subscription => sendSubscriptionToBackend(getState, subscription, me)); + subscription => sendSubscriptionToBackend(getState, subscription)); } } // No subscription, try to subscribe return subscribe(registration).then( - subscription => sendSubscriptionToBackend(getState, subscription, me)); + subscription => sendSubscriptionToBackend(getState, subscription)); }) .then(subscription => { // If we got a PushSubscription (and not a subscription object from the backend) @@ -140,7 +140,6 @@ export function saveSettings() { api(getState).put(`/api/web/push_subscriptions/${subscription.get('id')}`, { data, }).then(() => { - const me = getState().getIn(['meta', 'me']); if (me) { pushNotificationsSetting.set(me, data); } diff --git a/app/javascript/mastodon/actions/reports.js b/app/javascript/mastodon/actions/reports.js index b19a07285..afa0c3412 100644 --- a/app/javascript/mastodon/actions/reports.js +++ b/app/javascript/mastodon/actions/reports.js @@ -10,6 +10,7 @@ export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL'; export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE'; export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE'; +export const REPORT_FORWARD_CHANGE = 'REPORT_FORWARD_CHANGE'; export function initReport(account, status) { return dispatch => { @@ -45,6 +46,7 @@ export function submitReport() { account_id: getState().getIn(['reports', 'new', 'account_id']), status_ids: getState().getIn(['reports', 'new', 'status_ids']), comment: getState().getIn(['reports', 'new', 'comment']), + forward: getState().getIn(['reports', 'new', 'forward']), }).then(response => { dispatch(closeModal()); dispatch(submitReportSuccess(response.data)); @@ -78,3 +80,10 @@ export function changeReportComment(comment) { comment, }; }; + +export function changeReportForward(forward) { + return { + type: REPORT_FORWARD_CHANGE, + forward, + }; +}; diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js index 78c6109f7..73cb106ec 100644 --- a/app/javascript/mastodon/actions/search.js +++ b/app/javascript/mastodon/actions/search.js @@ -1,4 +1,5 @@ import api from '../api'; +import { fetchRelationships } from './accounts'; export const SEARCH_CHANGE = 'SEARCH_CHANGE'; export const SEARCH_CLEAR = 'SEARCH_CLEAR'; @@ -38,6 +39,7 @@ export function submitSearch() { }, }).then(response => { dispatch(fetchSearchSuccess(response.data)); + dispatch(fetchRelationships(response.data.accounts.map(item => item.id))); }).catch(error => { dispatch(fetchSearchFail(error)); }); diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 2204e0b14..073f09883 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -23,6 +23,9 @@ export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST'; export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS'; export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL'; +export const STATUS_REVEAL = 'STATUS_REVEAL'; +export const STATUS_HIDE = 'STATUS_HIDE'; + export function fetchStatusRequest(id, skipLoading) { return { type: STATUS_FETCH_REQUEST, @@ -215,3 +218,25 @@ export function unmuteStatusFail(id, error) { error, }; }; + +export function hideStatus(ids) { + if (!Array.isArray(ids)) { + ids = [ids]; + } + + return { + type: STATUS_HIDE, + ids, + }; +}; + +export function revealStatus(ids) { + if (!Array.isArray(ids)) { + ids = [ids]; + } + + return { + type: STATUS_REVEAL, + ids, + }; +}; diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js index a1db0fdd5..2dd94a998 100644 --- a/app/javascript/mastodon/actions/store.js +++ b/app/javascript/mastodon/actions/store.js @@ -1,4 +1,5 @@ import { Iterable, fromJS } from 'immutable'; +import { hydrateCompose } from './compose'; export const STORE_HYDRATE = 'STORE_HYDRATE'; export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; @@ -8,10 +9,14 @@ const convertState = rawState => Iterable.isIndexed(v) ? v.toList() : v.toMap()); export function hydrateStore(rawState) { - const state = convertState(rawState); + return dispatch => { + const state = convertState(rawState); - return { - type: STORE_HYDRATE, - state, + dispatch({ + type: STORE_HYDRATE, + state, + }); + + dispatch(hydrateCompose()); }; }; diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index df6a36379..f0ab16a2d 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -117,13 +117,14 @@ export function refreshTimeline(timelineId, path, params = {}) { }; }; -export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home'); -export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public'); -export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true }); -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 const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home'); +export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public'); +export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true }); +export const refreshAccountTimeline = (accountId, withReplies) => refreshTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies }); +export const refreshAccountFeaturedTimeline = accountId => refreshTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); +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 { @@ -161,7 +162,7 @@ export function expandTimeline(timelineId, path, params = {}) { export const expandHomeTimeline = () => expandTimeline('home', '/api/v1/timelines/home'); export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public'); export const expandCommunityTimeline = () => expandTimeline('community', '/api/v1/timelines/public', { local: true }); -export const expandAccountTimeline = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`); +export const expandAccountTimeline = (accountId, withReplies) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies }); 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}`); diff --git a/app/javascript/mastodon/base_polyfills.js b/app/javascript/mastodon/base_polyfills.js index 7856b26f9..8fbb17785 100644 --- a/app/javascript/mastodon/base_polyfills.js +++ b/app/javascript/mastodon/base_polyfills.js @@ -3,6 +3,7 @@ import 'intl/locale-data/jsonp/en'; import 'es6-symbol/implement'; import includes from 'array-includes'; import assign from 'object-assign'; +import values from 'object.values'; import isNaN from 'is-nan'; if (!Array.prototype.includes) { @@ -13,6 +14,10 @@ if (!Object.assign) { Object.assign = assign; } +if (!Object.values) { + values.shim(); +} + if (!Number.isNaN) { Number.isNaN = isNaN; } diff --git a/app/javascript/mastodon/components/attachment_list.js b/app/javascript/mastodon/components/attachment_list.js index 9f2d46ddd..8e5bb0e0b 100644 --- a/app/javascript/mastodon/components/attachment_list.js +++ b/app/javascript/mastodon/components/attachment_list.js @@ -1,5 +1,6 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; @@ -8,10 +9,29 @@ export default class AttachmentList extends ImmutablePureComponent { static propTypes = { media: ImmutablePropTypes.list.isRequired, + compact: PropTypes.bool, }; render () { - const { media } = this.props; + const { media, compact } = this.props; + + if (compact) { + return ( + <div className='attachment-list compact'> + <ul className='attachment-list__list'> + {media.map(attachment => { + const displayUrl = attachment.get('remote_url') || attachment.get('url'); + + return ( + <li key={attachment.get('id')}> + <a href={displayUrl} target='_blank' rel='noopener'><i className='fa fa-link' /> {filename(displayUrl)}</a> + </li> + ); + })} + </ul> + </div> + ); + } return ( <div className='attachment-list'> @@ -20,11 +40,15 @@ export default class AttachmentList extends ImmutablePureComponent { </div> <ul className='attachment-list__list'> - {media.map(attachment => ( - <li key={attachment.get('id')}> - <a href={attachment.get('remote_url')} target='_blank' rel='noopener'>{filename(attachment.get('remote_url'))}</a> - </li> - ))} + {media.map(attachment => { + const displayUrl = attachment.get('remote_url') || attachment.get('url'); + + return ( + <li key={attachment.get('id')}> + <a href={displayUrl} target='_blank' rel='noopener'>{filename(displayUrl)}</a> + </li> + ); + })} </ul> </div> ); diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js index 6a16e2fc7..34904194f 100644 --- a/app/javascript/mastodon/components/autosuggest_textarea.js +++ b/app/javascript/mastodon/components/autosuggest_textarea.js @@ -20,7 +20,7 @@ const textAtCursorMatchesToken = (str, caretPosition) => { word = str.slice(left, right + caretPosition); } - if (!word || word.trim().length < 3 || ['@', ':'].indexOf(word[0]) === -1) { + if (!word || word.trim().length < 3 || ['@', ':', '#'].indexOf(word[0]) === -1) { return [null, null]; } @@ -170,6 +170,9 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { if (typeof suggestion === 'object') { inner = <AutosuggestEmoji emoji={suggestion} />; key = suggestion.id; + } else if (suggestion[0] === '#') { + inner = suggestion; + key = suggestion; } else { inner = <AutosuggestAccountContainer id={suggestion} />; key = suggestion; diff --git a/app/javascript/mastodon/components/column_back_button_slim.js b/app/javascript/mastodon/components/column_back_button_slim.js index 3b4f46d99..964c100be 100644 --- a/app/javascript/mastodon/components/column_back_button_slim.js +++ b/app/javascript/mastodon/components/column_back_button_slim.js @@ -1,17 +1,8 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; +import ColumnBackButton from './column_back_button'; -export default class ColumnBackButtonSlim extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - handleClick = () => { - if (window.history && window.history.length === 1) this.context.router.history.push('/'); - else this.context.router.history.goBack(); - } +export default class ColumnBackButtonSlim extends ColumnBackButton { render () { return ( diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js index c300db89b..56453aeac 100644 --- a/app/javascript/mastodon/components/column_header.js +++ b/app/javascript/mastodon/components/column_header.js @@ -19,10 +19,11 @@ export default class ColumnHeader extends React.PureComponent { static propTypes = { intl: PropTypes.object.isRequired, - title: PropTypes.node.isRequired, - icon: PropTypes.string.isRequired, + title: PropTypes.node, + icon: PropTypes.string, active: PropTypes.bool, multiColumn: PropTypes.bool, + extraButton: PropTypes.node, showBackButton: PropTypes.bool, children: PropTypes.node, pinned: PropTypes.bool, @@ -63,7 +64,7 @@ export default class ColumnHeader extends React.PureComponent { } render () { - const { title, icon, active, children, pinned, onPin, multiColumn, showBackButton, intl: { formatMessage } } = this.props; + const { title, icon, active, children, pinned, onPin, multiColumn, extraButton, showBackButton, intl: { formatMessage } } = this.props; const { collapsed, animating } = this.state; const wrapperClassName = classNames('column-header__wrapper', { @@ -125,21 +126,26 @@ export default class ColumnHeader extends React.PureComponent { } if (children || multiColumn) { - collapseButton = <button className={collapsibleButtonClassName} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>; + collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>; } + const hasTitle = icon && title; + return ( <div className={wrapperClassName}> <h1 className={buttonClassName}> - <button onClick={this.handleTitleClick}> - <i className={`fa fa-fw fa-${icon} column-header__icon`} /> - <span className='column-header__title'> + {hasTitle && ( + <button onClick={this.handleTitleClick}> + <i className={`fa fa-fw fa-${icon} column-header__icon`} /> {title} - </span> - </button> + </button> + )} + + {!hasTitle && backButton} <div className='column-header__buttons'> - {backButton} + {hasTitle && backButton} + {extraButton} {collapseButton} </div> </h1> diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index 43dc0d6e3..c5c6f73b3 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -8,6 +8,7 @@ import spring from 'react-motion/lib/spring'; import detectPassiveEvents from 'detect-passive-events'; const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; +let id = 0; class DropdownMenu extends React.PureComponent { @@ -29,6 +30,10 @@ class DropdownMenu extends React.PureComponent { placement: 'bottom', }; + state = { + mounted: false, + }; + handleDocumentClick = e => { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); @@ -38,6 +43,7 @@ class DropdownMenu extends React.PureComponent { componentDidMount () { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + this.setState({ mounted: true }); } componentWillUnmount () { @@ -82,11 +88,15 @@ class DropdownMenu extends React.PureComponent { render () { const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props; + const { mounted } = this.state; return ( <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> {({ opacity, scaleX, scaleY }) => ( - <div className='dropdown-menu' style={{ ...style, opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }} ref={this.setRef}> + // It should not be transformed when mounting because the resulting + // size will be used to determine the coordinate of the menu by + // react-overlays + <div className='dropdown-menu' style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}> <div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} /> <ul> @@ -115,8 +125,10 @@ export default class Dropdown extends React.PureComponent { status: ImmutablePropTypes.map, isUserTouching: PropTypes.func, isModalOpen: PropTypes.bool.isRequired, - onModalOpen: PropTypes.func, - onModalClose: PropTypes.func, + onOpen: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + dropdownPlacement: PropTypes.string, + openDropdownId: PropTypes.number, }; static defaultProps = { @@ -124,37 +136,28 @@ export default class Dropdown extends React.PureComponent { }; state = { - expanded: false, + id: id++, }; - handleClick = () => { - if (!this.state.expanded && this.props.isUserTouching() && this.props.onModalOpen) { - const { status, items } = this.props; - - this.props.onModalOpen({ - status, - actions: items, - onClick: this.handleItemClick, - }); + handleClick = ({ target }) => { + if (this.state.id === this.props.openDropdownId) { + this.handleClose(); + } else { + const { top } = target.getBoundingClientRect(); + const placement = top * 2 < innerHeight ? 'bottom' : 'top'; - return; + this.props.onOpen(this.state.id, this.handleItemClick, placement); } - - this.setState({ expanded: !this.state.expanded }); } handleClose = () => { - if (this.props.onModalClose) { - this.props.onModalClose(); - } - - this.setState({ expanded: false }); + this.props.onClose(this.state.id); } handleKeyDown = e => { switch(e.key) { case 'Enter': - this.handleClick(); + this.handleClick(e); break; case 'Escape': this.handleClose(); @@ -186,22 +189,22 @@ export default class Dropdown extends React.PureComponent { } render () { - const { icon, items, size, title, disabled } = this.props; - const { expanded } = this.state; + const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId } = this.props; + const open = this.state.id === openDropdownId; return ( <div onKeyDown={this.handleKeyDown}> <IconButton icon={icon} title={title} - active={expanded} + active={open} disabled={disabled} size={size} ref={this.setTargetRef} onClick={this.handleClick} /> - <Overlay show={expanded} placement='bottom' target={this.findTarget}> + <Overlay show={open} placement={dropdownPlacement} target={this.findTarget}> <DropdownMenu items={items} onClose={this.handleClose} /> </Overlay> </div> diff --git a/app/javascript/mastodon/components/extended_video_player.js b/app/javascript/mastodon/components/extended_video_player.js index f8bd067e8..9e2f6835a 100644 --- a/app/javascript/mastodon/components/extended_video_player.js +++ b/app/javascript/mastodon/components/extended_video_player.js @@ -11,6 +11,7 @@ export default class ExtendedVideoPlayer extends React.PureComponent { time: PropTypes.number, controls: PropTypes.bool.isRequired, muted: PropTypes.bool.isRequired, + onClick: PropTypes.func, }; handleLoadedData = () => { @@ -31,6 +32,12 @@ export default class ExtendedVideoPlayer extends React.PureComponent { this.video = c; } + handleClick = e => { + e.stopPropagation(); + const handler = this.props.onClick; + if (handler) handler(); + } + render () { const { src, muted, controls, alt } = this.props; @@ -46,6 +53,7 @@ export default class ExtendedVideoPlayer extends React.PureComponent { muted={muted} controls={controls} loop={!controls} + onClick={this.handleClick} /> </div> ); diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index 20febdb16..9310e7c96 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -6,12 +6,32 @@ import IconButton from './icon_button'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { isIOS } from '../is_mobile'; import classNames from 'classnames'; -import { autoPlayGif } from '../initial_state'; +import { autoPlayGif, displaySensitiveMedia } from '../initial_state'; const messages = defineMessages({ toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }, }); +const shiftToPoint = (containerToImageRatio, containerSize, imageSize, focusSize, toMinus) => { + const containerCenter = Math.floor(containerSize / 2); + const focusFactor = (focusSize + 1) / 2; + const scaledImage = Math.floor(imageSize / containerToImageRatio); + + let focus = Math.floor(focusFactor * scaledImage); + + if (toMinus) focus = scaledImage - focus; + + let focusOffset = focus - containerCenter; + + const remainder = scaledImage - focus; + const containerRemainder = containerSize - containerCenter; + + if (remainder < containerRemainder) focusOffset -= containerRemainder - remainder; + if (focusOffset < 0) focusOffset = 0; + + return (focusOffset * -100 / containerSize) + '%'; +}; + class Item extends React.PureComponent { static contextTypes = { @@ -24,6 +44,8 @@ class Item extends React.PureComponent { index: PropTypes.number.isRequired, size: PropTypes.number.isRequired, onClick: PropTypes.func.isRequired, + containerWidth: PropTypes.number, + containerHeight: PropTypes.number, }; static defaultProps = { @@ -62,7 +84,7 @@ class Item extends React.PureComponent { } render () { - const { attachment, index, size, standalone } = this.props; + const { attachment, index, size, standalone, containerWidth, containerHeight } = this.props; let width = 50; let height = 100; @@ -116,16 +138,50 @@ class Item extends React.PureComponent { let thumbnail = ''; if (attachment.get('type') === 'image') { - const previewUrl = attachment.get('preview_url'); + const previewUrl = attachment.get('preview_url'); const previewWidth = attachment.getIn(['meta', 'small', 'width']); - const originalUrl = attachment.get('url'); - const originalWidth = attachment.getIn(['meta', 'original', 'width']); + const originalUrl = attachment.get('url'); + const originalWidth = attachment.getIn(['meta', 'original', 'width']); + const originalHeight = attachment.getIn(['meta', 'original', 'height']); const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number'; const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null; - const sizes = hasSize ? `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw` : null; + const sizes = hasSize ? `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw` : null; + + const focusX = attachment.getIn(['meta', 'focus', 'x']); + const focusY = attachment.getIn(['meta', 'focus', 'y']); + const imageStyle = {}; + + if (originalWidth && originalHeight && containerWidth && containerHeight && focusX && focusY) { + const widthRatio = originalWidth / (containerWidth * (width / 100)); + const heightRatio = originalHeight / (containerHeight * (height / 100)); + + let hShift = 0; + let vShift = 0; + + if (widthRatio > heightRatio) { + hShift = shiftToPoint(heightRatio, (containerWidth * (width / 100)), originalWidth, focusX); + } else if(widthRatio < heightRatio) { + vShift = shiftToPoint(widthRatio, (containerHeight * (height / 100)), originalHeight, focusY, true); + } + + if (originalWidth > originalHeight) { + imageStyle.height = '100%'; + imageStyle.width = 'auto'; + imageStyle.minWidth = '100%'; + } else { + imageStyle.height = 'auto'; + imageStyle.width = '100%'; + imageStyle.minHeight = '100%'; + } + + imageStyle.top = vShift; + imageStyle.left = hShift; + } else { + imageStyle.height = '100%'; + } thumbnail = ( <a @@ -134,7 +190,14 @@ class Item extends React.PureComponent { onClick={this.handleClick} target='_blank' > - <img src={previewUrl} srcSet={srcSet} sizes={sizes} alt={attachment.get('description')} title={attachment.get('description')} /> + <img + src={previewUrl} + srcSet={srcSet} + sizes={sizes} + alt={attachment.get('description')} + title={attachment.get('description')} + style={imageStyle} + /> </a> ); } else if (attachment.get('type') === 'gifv') { @@ -187,7 +250,7 @@ export default class MediaGallery extends React.PureComponent { }; state = { - visible: !this.props.sensitive, + visible: !this.props.sensitive || displaySensitiveMedia, }; componentWillReceiveProps (nextProps) { @@ -205,7 +268,7 @@ export default class MediaGallery extends React.PureComponent { } handleRef = (node) => { - if (node && this.isStandaloneEligible()) { + if (node /*&& this.isStandaloneEligible()*/) { // offsetWidth triggers a layout, so only calculate when we need to this.setState({ width: node.offsetWidth, @@ -227,15 +290,12 @@ export default class MediaGallery extends React.PureComponent { const style = {}; if (this.isStandaloneEligible()) { - if (!visible && width) { - // only need to forcibly set the height in "sensitive" mode + if (width) { style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']); - } else { - // layout automatically, using image's natural aspect ratio - style.height = ''; } + } else if (width) { + style.height = width / (16/9); } else { - // crop the image style.height = height; } @@ -249,7 +309,7 @@ export default class MediaGallery extends React.PureComponent { } children = ( - <button className='media-spoiler' onClick={this.handleOpen} style={style} ref={this.handleRef}> + <button type='button' className='media-spoiler' onClick={this.handleOpen} style={style} ref={this.handleRef}> <span className='media-spoiler__warning'>{warning}</span> <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> </button> @@ -260,12 +320,12 @@ export default class MediaGallery extends React.PureComponent { if (this.isStandaloneEligible()) { children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} />; } else { - children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} />); + children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} containerWidth={width} containerHeight={style.height} />); } } return ( - <div className='media-gallery' style={style}> + <div className='media-gallery' style={style} ref={this.handleRef}> <div className={classNames('spoiler-button', { 'spoiler-button--visible': visible })}> <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} /> </div> diff --git a/app/javascript/mastodon/components/permalink.js b/app/javascript/mastodon/components/permalink.js index d726d37a2..b369e9812 100644 --- a/app/javascript/mastodon/components/permalink.js +++ b/app/javascript/mastodon/components/permalink.js @@ -12,9 +12,15 @@ export default class Permalink extends React.PureComponent { href: PropTypes.string.isRequired, to: PropTypes.string.isRequired, children: PropTypes.node, + onInterceptClick: PropTypes.func, }; - handleClick = (e) => { + handleClick = e => { + if (this.props.onInterceptClick && this.props.onInterceptClick()) { + e.preventDefault(); + return; + } + if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); this.context.router.history.push(this.props.to); @@ -22,7 +28,7 @@ export default class Permalink extends React.PureComponent { } render () { - const { href, children, className, ...other } = this.props; + const { href, children, className, onInterceptClick, ...other } = this.props; return ( <a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}> diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js index 71228ca6c..ac3e404df 100644 --- a/app/javascript/mastodon/components/scrollable_list.js +++ b/app/javascript/mastodon/components/scrollable_list.js @@ -17,7 +17,7 @@ export default class ScrollableList extends PureComponent { static propTypes = { scrollKey: PropTypes.string.isRequired, - onScrollToBottom: PropTypes.func, + onLoadMore: PropTypes.func.isRequired, onScrollToTop: PropTypes.func, onScroll: PropTypes.func, trackScroll: PropTypes.bool, @@ -45,9 +45,11 @@ export default class ScrollableList extends PureComponent { const offset = scrollHeight - scrollTop - clientHeight; this._oldScrollPosition = scrollHeight - scrollTop; - if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) { - this.props.onScrollToBottom(); - } else if (scrollTop < 100 && this.props.onScrollToTop) { + if (400 > offset && this.props.onLoadMore && !this.props.isLoading) { + this.props.onLoadMore(); + } + + if (scrollTop < 100 && this.props.onScrollToTop) { this.props.onScrollToTop(); } else if (this.props.onScroll) { this.props.onScroll(); @@ -138,7 +140,7 @@ export default class ScrollableList extends PureComponent { handleLoadMore = (e) => { e.preventDefault(); - this.props.onScrollToBottom(); + this.props.onLoadMore(); } _recentlyMoved () { diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index c030510a0..a918a94f8 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -7,6 +7,7 @@ import RelativeTimestamp from './relative_timestamp'; import DisplayName from './display_name'; import StatusContent from './status_content'; import StatusActionBar from './status_action_bar'; +import AttachmentList from './attachment_list'; import { FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { MediaGallery, Video } from '../features/ui/util/async-components'; @@ -36,16 +37,13 @@ export default class Status extends ImmutablePureComponent { onBlock: PropTypes.func, onEmbed: PropTypes.func, onHeightChange: PropTypes.func, + onToggleHidden: PropTypes.func, muted: PropTypes.bool, hidden: PropTypes.bool, onMoveUp: PropTypes.func, onMoveDown: PropTypes.func, }; - state = { - isExpanded: false, - } - // Avoid checking props that are functions (and whose equality will always // evaluate to false. See react-immutable-pure-component for usage. updateOnProps = [ @@ -55,8 +53,6 @@ export default class Status extends ImmutablePureComponent { 'hidden', ] - updateOnStates = ['isExpanded'] - handleClick = () => { if (!this.context.router) { return; @@ -75,7 +71,7 @@ export default class Status extends ImmutablePureComponent { } handleExpandedToggle = () => { - this.setState({ isExpanded: !this.state.isExpanded }); + this.props.onToggleHidden(this._properStatus()); }; renderLoadingMediaGallery () { @@ -138,8 +134,7 @@ export default class Status extends ImmutablePureComponent { let media = null; let statusAvatar, prepend; - const { hidden } = this.props; - const { isExpanded } = this.state; + const { hidden, featured } = this.props; let { status, account, ...other } = this.props; @@ -156,7 +151,14 @@ export default class Status extends ImmutablePureComponent { ); } - if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { + if (featured) { + prepend = ( + <div className='status__prepend'> + <div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-thumb-tack status__prepend-icon' /></div> + <FormattedMessage id='status.pinned' defaultMessage='Pinned toot' /> + </div> + ); + } else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { const display_name_html = { __html: status.getIn(['account', 'display_name_html']) }; prepend = ( @@ -170,9 +172,14 @@ export default class Status extends ImmutablePureComponent { status = status.get('reblog'); } - if (status.get('media_attachments').size > 0 && !this.props.muted) { - if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { - + if (status.get('media_attachments').size > 0) { + if (this.props.muted || status.get('media_attachments').some(item => item.get('type') === 'unknown')) { + media = ( + <AttachmentList + compact + media={status.get('media_attachments')} + /> + ); } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { const video = status.getIn(['media_attachments', 0]); @@ -184,6 +191,7 @@ export default class Status extends ImmutablePureComponent { src={video.get('url')} width={239} height={110} + inline sensitive={status.get('sensitive')} onOpenVideo={this.handleOpenVideo} /> @@ -234,7 +242,7 @@ export default class Status extends ImmutablePureComponent { </a> </div> - <StatusContent status={status} onClick={this.handleClick} expanded={isExpanded} onExpandedToggle={this.handleExpandedToggle} /> + <StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} /> {media} diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js index 3b8155632..701b5702c 100644 --- a/app/javascript/mastodon/components/status_content.js +++ b/app/javascript/mastodon/components/status_content.js @@ -24,7 +24,12 @@ export default class StatusContent extends React.PureComponent { }; _updateStatusLinks () { - const node = this.node; + const node = this.node; + + if (!node) { + return; + } + const links = node.querySelectorAll('a'); for (var i = 0; i < links.length; ++i) { @@ -115,6 +120,10 @@ export default class StatusContent extends React.PureComponent { render () { const { status } = this.props; + if (status.get('content').length === 0) { + return null; + } + const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; const content = { __html: status.get('contentHtml') }; diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js index 5acaf714e..3bebf702c 100644 --- a/app/javascript/mastodon/components/status_list.js +++ b/app/javascript/mastodon/components/status_list.js @@ -11,7 +11,8 @@ export default class StatusList extends ImmutablePureComponent { static propTypes = { scrollKey: PropTypes.string.isRequired, statusIds: ImmutablePropTypes.list.isRequired, - onScrollToBottom: PropTypes.func, + featuredStatusIds: ImmutablePropTypes.list, + onLoadMore: PropTypes.func, onScrollToTop: PropTypes.func, onScroll: PropTypes.func, trackScroll: PropTypes.bool, @@ -50,7 +51,7 @@ export default class StatusList extends ImmutablePureComponent { } render () { - const { statusIds, ...other } = this.props; + const { statusIds, featuredStatusIds, ...other } = this.props; const { isLoading, isPartial } = other; if (isPartial) { @@ -68,8 +69,8 @@ export default class StatusList extends ImmutablePureComponent { ); } - const scrollableContent = (isLoading || statusIds.size > 0) ? ( - statusIds.map((statusId) => ( + let scrollableContent = (isLoading || statusIds.size > 0) ? ( + statusIds.map(statusId => ( <StatusContainer key={statusId} id={statusId} @@ -79,6 +80,18 @@ export default class StatusList extends ImmutablePureComponent { )) ) : null; + if (scrollableContent && featuredStatusIds) { + scrollableContent = featuredStatusIds.map(statusId => ( + <StatusContainer + key={`f-${statusId}`} + id={statusId} + featured + onMoveUp={this.handleMoveUp} + onMoveDown={this.handleMoveDown} + /> + )).concat(scrollableContent); + } + return ( <ScrollableList {...other} ref={this.setRef}> {scrollableContent} diff --git a/app/javascript/mastodon/containers/dropdown_menu_container.js b/app/javascript/mastodon/containers/dropdown_menu_container.js index 151f25390..7cbcdcd35 100644 --- a/app/javascript/mastodon/containers/dropdown_menu_container.js +++ b/app/javascript/mastodon/containers/dropdown_menu_container.js @@ -1,3 +1,4 @@ +import { openDropdownMenu, closeDropdownMenu } from '../actions/dropdown_menu'; import { openModal, closeModal } from '../actions/modal'; import { connect } from 'react-redux'; import DropdownMenu from '../components/dropdown_menu'; @@ -5,12 +6,22 @@ import { isUserTouching } from '../is_mobile'; const mapStateToProps = state => ({ isModalOpen: state.get('modal').modalType === 'ACTIONS', + dropdownPlacement: state.getIn(['dropdown_menu', 'placement']), + openDropdownId: state.getIn(['dropdown_menu', 'openId']), }); -const mapDispatchToProps = dispatch => ({ - isUserTouching, - onModalOpen: props => dispatch(openModal('ACTIONS', props)), - onModalClose: () => dispatch(closeModal()), +const mapDispatchToProps = (dispatch, { status, items }) => ({ + onOpen(id, onItemClick, dropdownPlacement) { + dispatch(isUserTouching() ? openModal('ACTIONS', { + status, + actions: items, + onClick: onItemClick, + }) : openDropdownMenu(id, dropdownPlacement)); + }, + onClose(id) { + dispatch(closeModal()); + dispatch(closeDropdownMenu(id)); + }, }); export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu); diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index b22540204..8ba1015b5 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -15,7 +15,13 @@ import { unpin, } from '../actions/interactions'; import { blockAccount } from '../actions/accounts'; -import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses'; +import { + muteStatus, + unmuteStatus, + deleteStatus, + hideStatus, + revealStatus, +} from '../actions/statuses'; import { initMuteModal } from '../actions/mutes'; import { initReport } from '../actions/reports'; import { openModal } from '../actions/modal'; @@ -128,6 +134,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, + onToggleHidden (status) { + if (status.get('hidden')) { + dispatch(revealStatus(status.get('id'))); + } else { + dispatch(hideStatus(status.get('id'))); + } + }, + }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status)); diff --git a/app/javascript/mastodon/containers/timeline_container.js b/app/javascript/mastodon/containers/timeline_container.js index e84c921ee..8719bb5c9 100644 --- a/app/javascript/mastodon/containers/timeline_container.js +++ b/app/javascript/mastodon/containers/timeline_container.js @@ -6,6 +6,7 @@ import { hydrateStore } from '../actions/store'; import { IntlProvider, addLocaleData } from 'react-intl'; import { getLocale } from '../locales'; import PublicTimeline from '../features/standalone/public_timeline'; +import CommunityTimeline from '../features/standalone/community_timeline'; import HashtagTimeline from '../features/standalone/hashtag_timeline'; import initialState from '../initial_state'; @@ -23,17 +24,24 @@ export default class TimelineContainer extends React.PureComponent { static propTypes = { locale: PropTypes.string.isRequired, hashtag: PropTypes.string, + showPublicTimeline: PropTypes.bool.isRequired, + }; + + static defaultProps = { + showPublicTimeline: initialState.settings.known_fediverse, }; render () { - const { locale, hashtag } = this.props; + const { locale, hashtag, showPublicTimeline } = this.props; let timeline; if (hashtag) { timeline = <HashtagTimeline hashtag={hashtag} />; - } else { + } else if (showPublicTimeline) { timeline = <PublicTimeline />; + } else { + timeline = <CommunityTimeline />; } return ( diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js index cb849fa5d..b538fa5fc 100644 --- a/app/javascript/mastodon/features/account/components/action_bar.js +++ b/app/javascript/mastodon/features/account/components/action_bar.js @@ -53,11 +53,11 @@ export default class ActionBar extends React.PureComponent { let extraInfo = ''; menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); + if ('share' in navigator) { menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); } - menu.push(null); - menu.push({ text: intl.formatMessage(messages.media), to: `/accounts/${account.get('id')}/media` }); + menu.push(null); if (account.get('id') === me) { @@ -122,7 +122,7 @@ export default class ActionBar extends React.PureComponent { <div className='account__action-bar-links'> <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}> - <span><FormattedMessage id='account.posts' defaultMessage='Posts' /></span> + <span><FormattedMessage id='account.posts' defaultMessage='Toots' /></span> <strong><FormattedNumber value={account.get('statuses_count')} /></strong> </Link> diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index b8605d11f..bb7b3b632 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -13,6 +13,7 @@ const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, + unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, }); class Avatar extends ImmutablePureComponent { @@ -69,6 +70,7 @@ export default class Header extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map, onFollow: PropTypes.func.isRequired, + onBlock: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; @@ -80,11 +82,20 @@ export default class Header extends ImmutablePureComponent { } let info = ''; + let mutingInfo = ''; let actionBtn = ''; let lockedIcon = ''; if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) { info = <span className='account--follows-info'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>; + } else if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) { + info = <span className='account--follows-info'><FormattedMessage id='account.blocked' defaultMessage='Blocked' /></span>; + } + + if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) { + mutingInfo = <span className='account--muting-info'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>; + } else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) { + mutingInfo = <span className='account--muting-info'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain hidden' /></span>; } if (me !== account.get('id')) { @@ -100,6 +111,12 @@ export default class Header extends ImmutablePureComponent { <IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} /> </div> ); + } else if (account.getIn(['relationship', 'blocking'])) { + actionBtn = ( + <div className='account--action-button'> + <IconButton size={26} icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} /> + </div> + ); } } @@ -124,6 +141,7 @@ export default class Header extends ImmutablePureComponent { <div className='account__header__content' dangerouslySetInnerHTML={content} /> {info} + {mutingInfo} {actionBtn} </div> </div> diff --git a/app/javascript/mastodon/features/account_gallery/components/media_item.js b/app/javascript/mastodon/features/account_gallery/components/media_item.js index dda3d4e37..f7a802dc7 100644 --- a/app/javascript/mastodon/features/account_gallery/components/media_item.js +++ b/app/javascript/mastodon/features/account_gallery/components/media_item.js @@ -2,6 +2,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Permalink from '../../../components/permalink'; +import { displaySensitiveMedia } from '../../../initial_state'; export default class MediaItem extends ImmutablePureComponent { @@ -9,28 +10,51 @@ export default class MediaItem extends ImmutablePureComponent { media: ImmutablePropTypes.map.isRequired, }; + state = { + visible: !this.props.media.getIn(['status', 'sensitive']) || displaySensitiveMedia, + }; + + handleClick = () => { + if (!this.state.visible) { + this.setState({ visible: true }); + return true; + } + + return false; + } + render () { const { media } = this.props; + const { visible } = this.state; const status = media.get('status'); + const focusX = media.getIn(['meta', 'focus', 'x']); + const focusY = media.getIn(['meta', 'focus', 'y']); + const x = ((focusX / 2) + .5) * 100; + const y = ((focusY / -2) + .5) * 100; + const style = {}; - let content, style; + let label, icon; if (media.get('type') === 'gifv') { - content = <span className='media-gallery__gifv__label'>GIF</span>; + label = <span className='media-gallery__gifv__label'>GIF</span>; } - if (!status.get('sensitive')) { - style = { backgroundImage: `url(${media.get('preview_url')})` }; + if (visible) { + style.backgroundImage = `url(${media.get('preview_url')})`; + style.backgroundPosition = `${x}% ${y}%`; + } else { + icon = ( + <span className='account-gallery__item__icons'> + <i className='fa fa-eye-slash' /> + </span> + ); } return ( <div className='account-gallery__item'> - <Permalink - to={`/statuses/${status.get('id')}`} - href={status.get('url')} - style={style} - > - {content} + <Permalink to={`/statuses/${status.get('id')}`} href={status.get('url')} style={style} onInterceptClick={this.handleClick}> + {icon} + {label} </Permalink> </div> ); diff --git a/app/javascript/mastodon/features/account_gallery/index.js b/app/javascript/mastodon/features/account_gallery/index.js index ece219a3d..4b408256a 100644 --- a/app/javascript/mastodon/features/account_gallery/index.js +++ b/app/javascript/mastodon/features/account_gallery/index.js @@ -11,7 +11,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { getAccountGallery } from '../../selectors'; import MediaItem from './components/media_item'; import HeaderContainer from '../account_timeline/containers/header_container'; -import { FormattedMessage } from 'react-intl'; import { ScrollContainer } from 'react-router-scroll-4'; import LoadMore from '../../components/load_more'; @@ -89,10 +88,6 @@ export default class AccountGallery extends ImmutablePureComponent { <div className='scrollable' onScroll={this.handleScroll}> <HeaderContainer accountId={this.props.params.accountId} /> - <div className='account-section-headline'> - <FormattedMessage id='account.media' defaultMessage='Media' /> - </div> - <div className='account-gallery__container'> {medias.map(media => ( <MediaItem diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js index 0ddb6b6c1..9d594fb0c 100644 --- a/app/javascript/mastodon/features/account_timeline/components/header.js +++ b/app/javascript/mastodon/features/account_timeline/components/header.js @@ -6,6 +6,8 @@ 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'; +import { FormattedMessage } from 'react-intl'; +import { NavLink } from 'react-router-dom'; export default class Header extends ImmutablePureComponent { @@ -19,6 +21,7 @@ export default class Header extends ImmutablePureComponent { onMute: PropTypes.func.isRequired, onBlockDomain: PropTypes.func.isRequired, onUnblockDomain: PropTypes.func.isRequired, + hideTabs: PropTypes.bool, }; static contextTypes = { @@ -66,7 +69,7 @@ export default class Header extends ImmutablePureComponent { } render () { - const { account } = this.props; + const { account, hideTabs } = this.props; if (account === null) { return <MissingIndicator />; @@ -79,6 +82,7 @@ export default class Header extends ImmutablePureComponent { <InnerHeader account={account} onFollow={this.handleFollow} + onBlock={this.handleBlock} /> <ActionBar @@ -91,6 +95,14 @@ export default class Header extends ImmutablePureComponent { onBlockDomain={this.handleBlockDomain} onUnblockDomain={this.handleUnblockDomain} /> + + {!hideTabs && ( + <div className='account__section-headline'> + <NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink> + <NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink> + <NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink> + </div> + )} </div> ); } diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js index f8c85c296..f5f2475ea 100644 --- a/app/javascript/mastodon/features/account_timeline/index.js +++ b/app/javascript/mastodon/features/account_timeline/index.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { fetchAccount } from '../../actions/accounts'; -import { refreshAccountTimeline, expandAccountTimeline } from '../../actions/timelines'; +import { refreshAccountTimeline, refreshAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines'; import StatusList from '../../components/status_list'; import LoadingIndicator from '../../components/loading_indicator'; import Column from '../ui/components/column'; @@ -12,11 +12,16 @@ import ColumnBackButton from '../../components/column_back_button'; import { List as ImmutableList } from 'immutable'; import ImmutablePureComponent from 'react-immutable-pure-component'; -const mapStateToProps = (state, props) => ({ - statusIds: state.getIn(['timelines', `account:${props.params.accountId}`, 'items'], ImmutableList()), - isLoading: state.getIn(['timelines', `account:${props.params.accountId}`, 'isLoading']), - hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}`, 'next']), -}); +const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => { + const path = withReplies ? `${accountId}:with_replies` : accountId; + + return { + statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()), + featuredStatusIds: state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()), + isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']), + hasMore: !!state.getIn(['timelines', `account:${path}`, 'next']), + }; +}; @connect(mapStateToProps) export default class AccountTimeline extends ImmutablePureComponent { @@ -25,30 +30,36 @@ export default class AccountTimeline extends ImmutablePureComponent { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, statusIds: ImmutablePropTypes.list, + featuredStatusIds: ImmutablePropTypes.list, isLoading: PropTypes.bool, hasMore: PropTypes.bool, + withReplies: PropTypes.bool, }; componentWillMount () { - this.props.dispatch(fetchAccount(this.props.params.accountId)); - this.props.dispatch(refreshAccountTimeline(this.props.params.accountId)); + const { params: { accountId }, withReplies } = this.props; + + this.props.dispatch(fetchAccount(accountId)); + this.props.dispatch(refreshAccountFeaturedTimeline(accountId)); + this.props.dispatch(refreshAccountTimeline(accountId, withReplies)); } componentWillReceiveProps (nextProps) { - if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { + if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) { this.props.dispatch(fetchAccount(nextProps.params.accountId)); - this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId)); + this.props.dispatch(refreshAccountFeaturedTimeline(nextProps.params.accountId)); + this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId, nextProps.params.withReplies)); } } - handleScrollToBottom = () => { + handleLoadMore = () => { if (!this.props.isLoading && this.props.hasMore) { - this.props.dispatch(expandAccountTimeline(this.props.params.accountId)); + this.props.dispatch(expandAccountTimeline(this.props.params.accountId, this.props.withReplies)); } } render () { - const { statusIds, isLoading, hasMore } = this.props; + const { statusIds, featuredStatusIds, isLoading, hasMore } = this.props; if (!statusIds && isLoading) { return ( @@ -66,9 +77,10 @@ export default class AccountTimeline extends ImmutablePureComponent { prepend={<HeaderContainer accountId={this.props.params.accountId} />} scrollKey='account_timeline' statusIds={statusIds} + featuredStatusIds={featuredStatusIds} isLoading={isLoading} hasMore={hasMore} - onScrollToBottom={this.handleScrollToBottom} + onLoadMore={this.handleLoadMore} /> </Column> ); diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index a876c5197..663ccfb8e 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -50,6 +50,7 @@ export default class ComposeForm extends ImmutablePureComponent { onPaste: PropTypes.func.isRequired, onPickEmoji: PropTypes.func.isRequired, showSearch: PropTypes.bool, + anyMedia: PropTypes.bool, }; static defaultProps = { @@ -142,10 +143,10 @@ export default class ComposeForm extends ImmutablePureComponent { } render () { - const { intl, onPaste, showSearch } = this.props; + const { intl, onPaste, showSearch, anyMedia } = this.props; const disabled = this.props.is_submitting; const text = [this.props.spoiler_text, countableText(this.props.text)].join(''); - + const disabledButton = disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0 && !anyMedia); let publishText = ''; if (this.props.privacy === 'private' || this.props.privacy === 'direct') { @@ -203,7 +204,7 @@ export default class ComposeForm extends ImmutablePureComponent { </div> <div className='compose-form__publish'> - <div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div> + <div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabledButton} block /></div> </div> </div> ); diff --git a/app/javascript/mastodon/features/compose/components/search.js b/app/javascript/mastodon/features/compose/components/search.js index 398fc44ce..71c0a203f 100644 --- a/app/javascript/mastodon/features/compose/components/search.js +++ b/app/javascript/mastodon/features/compose/components/search.js @@ -4,6 +4,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import Overlay from 'react-overlays/lib/Overlay'; import Motion from '../../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; +import { searchEnabled } from '../../../initial_state'; const messages = defineMessages({ placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, @@ -17,7 +18,7 @@ class SearchPopout extends React.PureComponent { render () { const { style } = this.props; - + const extraInformation = searchEnabled ? <FormattedMessage id='search_popout.tips.full_text' defaultMessage='Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.' /> : <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' />; return ( <div style={{ ...style, position: 'absolute', width: 285 }}> <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> @@ -32,7 +33,7 @@ class SearchPopout extends React.PureComponent { <li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='status' /></li> </ul> - <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' /> + {extraInformation} </div> )} </Motion> diff --git a/app/javascript/mastodon/features/compose/components/search_results.js b/app/javascript/mastodon/features/compose/components/search_results.js index d16f7fce7..84455563c 100644 --- a/app/javascript/mastodon/features/compose/components/search_results.js +++ b/app/javascript/mastodon/features/compose/components/search_results.js @@ -22,6 +22,8 @@ export default class SearchResults extends ImmutablePureComponent { count += results.get('accounts').size; accounts = ( <div className='search-results__section'> + <h5><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5> + {results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)} </div> ); @@ -31,6 +33,8 @@ export default class SearchResults extends ImmutablePureComponent { count += results.get('statuses').size; statuses = ( <div className='search-results__section'> + <h5><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5> + {results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)} </div> ); @@ -40,6 +44,8 @@ export default class SearchResults extends ImmutablePureComponent { count += results.get('hashtags').size; hashtags = ( <div className='search-results__section'> + <h5><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5> + {results.get('hashtags').map(hashtag => ( <Link key={hashtag} className='search-results__hashtag' to={`/timelines/tag/${hashtag}`}> #{hashtag} diff --git a/app/javascript/mastodon/features/compose/components/upload.js b/app/javascript/mastodon/features/compose/components/upload.js index 3a3d17710..61b2d19e0 100644 --- a/app/javascript/mastodon/features/compose/components/upload.js +++ b/app/javascript/mastodon/features/compose/components/upload.js @@ -1,15 +1,13 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import IconButton from '../../../components/icon_button'; import Motion from '../../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; const messages = defineMessages({ - undo: { id: 'upload_form.undo', defaultMessage: 'Undo' }, description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' }, }); @@ -21,6 +19,7 @@ export default class Upload extends ImmutablePureComponent { intl: PropTypes.object.isRequired, onUndo: PropTypes.func.isRequired, onDescriptionChange: PropTypes.func.isRequired, + onOpenFocalPoint: PropTypes.func.isRequired, }; state = { @@ -33,6 +32,10 @@ export default class Upload extends ImmutablePureComponent { this.props.onUndo(this.props.media.get('id')); } + handleFocalPointClick = () => { + this.props.onOpenFocalPoint(this.props.media.get('id')); + } + handleInputChange = e => { this.setState({ dirtyDescription: e.target.value }); } @@ -63,13 +66,20 @@ export default class Upload extends ImmutablePureComponent { const { intl, media } = this.props; const active = this.state.hovered || this.state.focused; const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || ''; + const focusX = media.getIn(['meta', 'focus', 'x']); + const focusY = media.getIn(['meta', 'focus', 'y']); + const x = ((focusX / 2) + .5) * 100; + const y = ((focusY / -2) + .5) * 100; return ( <div className='compose-form__upload' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}> {({ scale }) => ( - <div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})` }}> - <IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.handleUndoClick} /> + <div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}> + <div className={classNames('compose-form__upload__actions', { active })}> + <button className='icon-button' onClick={this.handleUndoClick}><i className='fa fa-times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Undo' /></button> + {media.get('type') === 'image' && <button className='icon-button' onClick={this.handleFocalPointClick}><i className='fa fa-crosshairs' /> <FormattedMessage id='upload_form.focus' defaultMessage='Crop' /></button>} + </div> <div className={classNames('compose-form__upload-description', { active })}> <label> diff --git a/app/javascript/mastodon/features/compose/containers/compose_form_container.js b/app/javascript/mastodon/features/compose/containers/compose_form_container.js index 5f5509dbe..ede23d361 100644 --- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js +++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js @@ -23,6 +23,7 @@ const mapStateToProps = state => ({ is_submitting: state.getIn(['compose', 'is_submitting']), is_uploading: state.getIn(['compose', 'is_uploading']), showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), + anyMedia: state.getIn(['compose', 'media_attachments']).size > 0, }); const mapDispatchToProps = (dispatch) => ({ diff --git a/app/javascript/mastodon/features/compose/containers/upload_container.js b/app/javascript/mastodon/features/compose/containers/upload_container.js index ca9c3b704..d6b57e5ff 100644 --- a/app/javascript/mastodon/features/compose/containers/upload_container.js +++ b/app/javascript/mastodon/features/compose/containers/upload_container.js @@ -1,6 +1,7 @@ import { connect } from 'react-redux'; import Upload from '../components/upload'; import { undoUploadCompose, changeUploadCompose } from '../../../actions/compose'; +import { openModal } from '../../../actions/modal'; const mapStateToProps = (state, { id }) => ({ media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id), @@ -13,7 +14,11 @@ const mapDispatchToProps = dispatch => ({ }, onDescriptionChange: (id, description) => { - dispatch(changeUploadCompose(id, description)); + dispatch(changeUploadCompose(id, { description })); + }, + + onOpenFocalPoint: id => { + dispatch(openModal('FOCAL_POINT', { id })); }, }); diff --git a/app/javascript/mastodon/features/compose/containers/warning_container.js b/app/javascript/mastodon/features/compose/containers/warning_container.js index b9f280958..dbb80dfb0 100644 --- a/app/javascript/mastodon/features/compose/containers/warning_container.js +++ b/app/javascript/mastodon/features/compose/containers/warning_container.js @@ -5,7 +5,7 @@ import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import { me } from '../../../initial_state'; -const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\S+)/i; +const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\w*[a-zA-Z]\w*)/i; const mapStateToProps = state => ({ needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']), diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index 84e3a2338..138bc4e2e 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -12,6 +12,7 @@ import Motion from '../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import SearchResultsContainer from './containers/search_results_container'; import { changeComposing } from '../../actions/compose'; +import elephantUIPlane from '../../../images/elephant_ui_plane.svg'; const messages = defineMessages({ start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, @@ -94,7 +95,11 @@ export default class Compose extends React.PureComponent { <div className='drawer__inner' onFocus={this.onFocus}> <NavigationContainer onClose={this.onBlur} /> <ComposeFormContainer /> - {multiColumn && <div className='mastodon' />} + {multiColumn && ( + <div className='drawer__inner__mastodon'> + <img alt='' src={elephantUIPlane} /> + </div> + )} </div> <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}> diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js index 67b107bc8..6f1c863b4 100644 --- a/app/javascript/mastodon/features/favourited_statuses/index.js +++ b/app/javascript/mastodon/features/favourited_statuses/index.js @@ -62,7 +62,7 @@ export default class Favourites extends ImmutablePureComponent { this.column = c; } - handleScrollToBottom = debounce(() => { + handleLoadMore = debounce(() => { this.props.dispatch(expandFavouritedStatuses()); }, 300, { leading: true }) @@ -89,7 +89,7 @@ export default class Favourites extends ImmutablePureComponent { scrollKey={`favourited_statuses-${columnId}`} hasMore={hasMore} isLoading={isLoading} - onScrollToBottom={this.handleScrollToBottom} + onLoadMore={this.handleLoadMore} /> </Column> ); diff --git a/app/javascript/mastodon/features/followers/index.js b/app/javascript/mastodon/features/followers/index.js index f64ed7948..919a89332 100644 --- a/app/javascript/mastodon/features/followers/index.js +++ b/app/javascript/mastodon/features/followers/index.js @@ -80,7 +80,7 @@ export default class Followers extends ImmutablePureComponent { <ScrollContainer scrollKey='followers'> <div className='scrollable' onScroll={this.handleScroll}> <div className='followers'> - <HeaderContainer accountId={this.props.params.accountId} /> + <HeaderContainer accountId={this.props.params.accountId} hideTabs /> {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} {loadMore} </div> diff --git a/app/javascript/mastodon/features/following/index.js b/app/javascript/mastodon/features/following/index.js index a0c0fac05..5719259d1 100644 --- a/app/javascript/mastodon/features/following/index.js +++ b/app/javascript/mastodon/features/following/index.js @@ -80,7 +80,7 @@ export default class Following extends ImmutablePureComponent { <ScrollContainer scrollKey='following'> <div className='scrollable' onScroll={this.handleScroll}> <div className='following'> - <HeaderContainer accountId={this.props.params.accountId} /> + <HeaderContainer accountId={this.props.params.accountId} hideTabs /> {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} {loadMore} </div> diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index ee789e180..3a875169e 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -8,6 +8,8 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { me } from '../../initial_state'; +import { fetchFollowRequests } from '../../actions/accounts'; +import { List as ImmutableList } from 'immutable'; const messages = defineMessages({ heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, @@ -32,9 +34,25 @@ const messages = defineMessages({ const mapStateToProps = state => ({ myAccount: state.getIn(['accounts', me]), columns: state.getIn(['settings', 'columns']), + unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size, + unreadNotifications: state.getIn(['notifications', 'unread']), }); -@connect(mapStateToProps) +const mapDispatchToProps = dispatch => ({ + fetchFollowRequests: () => dispatch(fetchFollowRequests()), +}); + +const badgeDisplay = (number, limit) => { + if (number === 0) { + return undefined; + } else if (limit && number >= limit) { + return `${limit}+`; + } else { + return number; + } +}; + +@connect(mapStateToProps, mapDispatchToProps) @injectIntl export default class GettingStarted extends ImmutablePureComponent { @@ -43,10 +61,21 @@ export default class GettingStarted extends ImmutablePureComponent { myAccount: ImmutablePropTypes.map.isRequired, columns: ImmutablePropTypes.list, multiColumn: PropTypes.bool, + fetchFollowRequests: PropTypes.func.isRequired, + unreadFollowRequests: PropTypes.number, + unreadNotifications: PropTypes.number, }; + componentDidMount () { + const { myAccount, fetchFollowRequests } = this.props; + + if (myAccount.get('locked')) { + fetchFollowRequests(); + } + } + render () { - const { intl, myAccount, columns, multiColumn } = this.props; + const { intl, myAccount, columns, multiColumn, unreadFollowRequests, unreadNotifications } = this.props; const navItems = []; @@ -56,7 +85,7 @@ export default class GettingStarted extends ImmutablePureComponent { } if (!columns.find(item => item.get('id') === 'NOTIFICATIONS')) { - navItems.push(<ColumnLink key='1' icon='bell' text={intl.formatMessage(messages.notifications)} to='/notifications' />); + navItems.push(<ColumnLink key='1' icon='bell' text={intl.formatMessage(messages.notifications)} badge={badgeDisplay(unreadNotifications)} to='/notifications' />); } if (!columns.find(item => item.get('id') === 'COMMUNITY')) { @@ -74,7 +103,7 @@ export default class GettingStarted extends ImmutablePureComponent { ); if (myAccount.get('locked')) { - navItems.push(<ColumnLink key='6' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />); + navItems.push(<ColumnLink key='6' icon='users' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />); } if (multiColumn) { diff --git a/app/javascript/mastodon/features/notifications/components/clear_column_button.js b/app/javascript/mastodon/features/notifications/components/clear_column_button.js index 22a10753f..e0bf4c82d 100644 --- a/app/javascript/mastodon/features/notifications/components/clear_column_button.js +++ b/app/javascript/mastodon/features/notifications/components/clear_column_button.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; -export default class ClearColumnButton extends React.Component { +export default class ClearColumnButton extends React.PureComponent { static propTypes = { onClick: PropTypes.func.isRequired, diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index 35b430bfb..cb9d025ea 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -50,8 +50,14 @@ export default class Notifications extends React.PureComponent { trackScroll: true, }; - handleScrollToBottom = debounce(() => { + componentWillUnmount () { + this.handleLoadMore.cancel(); + this.handleScrollToTop.cancel(); + this.handleScroll.cancel(); this.props.dispatch(scrollTopNotifications(false)); + } + + handleLoadMore = debounce(() => { this.props.dispatch(expandNotifications()); }, 300, { leading: true }); @@ -136,7 +142,7 @@ export default class Notifications extends React.PureComponent { isLoading={isLoading} hasMore={hasMore} emptyMessage={emptyMessage} - onScrollToBottom={this.handleScrollToBottom} + onLoadMore={this.handleLoadMore} onScrollToTop={this.handleScrollToTop} onScroll={this.handleScroll} shouldUpdateScroll={shouldUpdateScroll} diff --git a/app/javascript/mastodon/features/report/components/status_check_box.js b/app/javascript/mastodon/features/report/components/status_check_box.js index cc9232201..9ff75a082 100644 --- a/app/javascript/mastodon/features/report/components/status_check_box.js +++ b/app/javascript/mastodon/features/report/components/status_check_box.js @@ -2,6 +2,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Toggle from 'react-toggle'; +import noop from 'lodash/noop'; +import StatusContent from '../../../components/status_content'; +import { MediaGallery, Video } from '../../ui/util/async-components'; +import Bundle from '../../ui/components/bundle'; export default class StatusCheckBox extends React.PureComponent { @@ -14,18 +18,48 @@ export default class StatusCheckBox extends React.PureComponent { render () { const { status, checked, onToggle, disabled } = this.props; - const content = { __html: status.get('contentHtml') }; + let media = null; if (status.get('reblog')) { return null; } + if (status.get('media_attachments').size > 0) { + if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { + + } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { + const video = status.getIn(['media_attachments', 0]); + + media = ( + <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} > + {Component => ( + <Component + preview={video.get('preview_url')} + src={video.get('url')} + width={239} + height={110} + inline + sensitive={status.get('sensitive')} + onOpenVideo={noop} + /> + )} + </Bundle> + ); + } else { + media = ( + <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} > + {Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={noop} />} + </Bundle> + ); + } + } + return ( <div className='status-check-box'> - <div - className='status__content' - dangerouslySetInnerHTML={content} - /> + <div className='status-check-box__status'> + <StatusContent status={status} /> + {media} + </div> <div className='status-check-box-toggle'> <Toggle checked={checked} onChange={onToggle} disabled={disabled} /> diff --git a/app/javascript/mastodon/features/standalone/community_timeline/index.js b/app/javascript/mastodon/features/standalone/community_timeline/index.js new file mode 100644 index 000000000..51e50e1f5 --- /dev/null +++ b/app/javascript/mastodon/features/standalone/community_timeline/index.js @@ -0,0 +1,74 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import StatusListContainer from '../../ui/containers/status_list_container'; +import { + refreshCommunityTimeline, + expandCommunityTimeline, +} from '../../../actions/timelines'; +import Column from '../../../components/column'; +import ColumnHeader from '../../../components/column_header'; +import { defineMessages, injectIntl } from 'react-intl'; +import { connectCommunityStream } from '../../../actions/streaming'; + +const messages = defineMessages({ + title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' }, +}); + +@connect() +@injectIntl +export default class CommunityTimeline extends React.PureComponent { + + static propTypes = { + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + handleHeaderClick = () => { + this.column.scrollTop(); + } + + setRef = c => { + this.column = c; + } + + componentDidMount () { + const { dispatch } = this.props; + + dispatch(refreshCommunityTimeline()); + this.disconnect = dispatch(connectCommunityStream()); + } + + componentWillUnmount () { + if (this.disconnect) { + this.disconnect(); + this.disconnect = null; + } + } + + handleLoadMore = () => { + this.props.dispatch(expandCommunityTimeline()); + } + + render () { + const { intl } = this.props; + + return ( + <Column ref={this.setRef}> + <ColumnHeader + icon='users' + title={intl.formatMessage(messages.title)} + onClick={this.handleHeaderClick} + /> + + <StatusListContainer + timelineId='community' + loadMore={this.handleLoadMore} + scrollKey='standalone_public_timeline' + trackScroll={false} + /> + </Column> + ); + } + +} diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js index 2f6a7831e..b52f3c4fa 100644 --- a/app/javascript/mastodon/features/status/components/card.js +++ b/app/javascript/mastodon/features/status/components/card.js @@ -20,6 +20,39 @@ const getHostname = url => { return parser.hostname; }; +const trim = (text, len) => { + const cut = text.indexOf(' ', len); + + if (cut === -1) { + return text; + } + + return text.substring(0, cut) + (text.length > len ? '…' : ''); +}; + +const domParser = new DOMParser(); + +const addAutoPlay = html => { + const document = domParser.parseFromString(html, 'text/html').documentElement; + const iframe = document.querySelector('iframe'); + + if (iframe) { + if (iframe.src.indexOf('?') !== -1) { + iframe.src += '&'; + } else { + iframe.src += '?'; + } + + iframe.src += 'autoplay=1&auto_play=1'; + + // DOM parser creates html/body elements around original HTML fragment, + // so we need to get innerHTML out of the body and not the entire document + return document.querySelector('body').innerHTML; + } + + return html; +}; + export default class Card extends React.PureComponent { static propTypes = { @@ -33,9 +66,16 @@ export default class Card extends React.PureComponent { }; state = { - width: 0, + width: 280, + embedded: false, }; + componentWillReceiveProps (nextProps) { + if (this.props.card !== nextProps.card) { + this.setState({ embedded: false }); + } + } + handlePhotoClick = () => { const { card, onOpenMedia } = this.props; @@ -57,56 +97,14 @@ export default class Card extends React.PureComponent { ); }; - renderLink () { - const { card, maxDescription } = this.props; - const { width } = this.state; - const horizontal = card.get('width') > card.get('height') && (card.get('width') + 100 >= width); - - let image = ''; - let provider = card.get('provider_name'); - - if (card.get('image')) { - image = ( - <div className='status-card__image'> - <img src={card.get('image')} alt={card.get('title')} className='status-card__image-image' width={card.get('width')} height={card.get('height')} /> - </div> - ); - } - - if (provider.length < 1) { - provider = decodeIDNA(getHostname(card.get('url'))); - } - - const className = classnames('status-card', { horizontal }); - - return ( - <a href={card.get('url')} className={className} target='_blank' rel='noopener' ref={this.setRef}> - {image} - - <div className='status-card__content'> - <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong> - {!horizontal && <p className='status-card__description'>{(card.get('description') || '').substring(0, maxDescription)}</p>} - <span className='status-card__host'>{provider}</span> - </div> - </a> - ); - } - - renderPhoto () { + handleEmbedClick = () => { const { card } = this.props; - return ( - <img - className='status-card-photo' - onClick={this.handlePhotoClick} - role='button' - tabIndex='0' - src={card.get('embed_url')} - alt={card.get('title')} - width={card.get('width')} - height={card.get('height')} - /> - ); + if (card.get('type') === 'photo') { + this.handlePhotoClick(); + } else { + this.setState({ embedded: true }); + } } setRef = c => { @@ -117,7 +115,7 @@ export default class Card extends React.PureComponent { renderVideo () { const { card } = this.props; - const content = { __html: card.get('html') }; + const content = { __html: addAutoPlay(card.get('html')) }; const { width } = this.state; const ratio = card.get('width') / card.get('height'); const height = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio); @@ -125,7 +123,7 @@ export default class Card extends React.PureComponent { return ( <div ref={this.setRef} - className='status-card-video' + className='status-card__image status-card-video' dangerouslySetInnerHTML={content} style={{ height }} /> @@ -133,23 +131,76 @@ export default class Card extends React.PureComponent { } render () { - const { card } = this.props; + const { card, maxDescription } = this.props; + const { width, embedded } = this.state; if (card === null) { return null; } - switch(card.get('type')) { - case 'link': - return this.renderLink(); - case 'photo': - return this.renderPhoto(); - case 'video': - return this.renderVideo(); - case 'rich': - default: - return null; + const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name'); + const horizontal = card.get('width') > card.get('height') && (card.get('width') + 100 >= width) || card.get('type') !== 'link'; + const className = classnames('status-card', { horizontal }); + const interactive = card.get('type') !== 'link'; + const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>; + const ratio = card.get('width') / card.get('height'); + const height = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio); + + const description = ( + <div className='status-card__content'> + {title} + {!horizontal && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>} + <span className='status-card__host'>{provider}</span> + </div> + ); + + let embed = ''; + let thumbnail = <div style={{ backgroundImage: `url(${card.get('image')})`, width: horizontal ? width : null, height: horizontal ? height : null }} className='status-card__image-image' />; + + if (interactive) { + if (embedded) { + embed = this.renderVideo(); + } else { + let iconVariant = 'play'; + + if (card.get('type') === 'photo') { + iconVariant = 'search-plus'; + } + + embed = ( + <div className='status-card__image'> + {thumbnail} + + <div className='status-card__actions'> + <div> + <button onClick={this.handleEmbedClick}><i className={`fa fa-${iconVariant}`} /></button> + <a href={card.get('url')} target='_blank' rel='noopener'><i className='fa fa-external-link' /></a> + </div> + </div> + </div> + ); + } + + return ( + <div className={className} ref={this.setRef}> + {embed} + {description} + </div> + ); + } else if (card.get('image')) { + embed = ( + <div className='status-card__image'> + {thumbnail} + </div> + ); } + + return ( + <a href={card.get('url')} className={className} target='_blank' rel='noopener' ref={this.setRef}> + {embed} + {description} + </a> + ); } } diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index abdb9a3f6..b5f516032 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -22,6 +22,7 @@ export default class DetailedStatus extends ImmutablePureComponent { status: ImmutablePropTypes.map.isRequired, onOpenMedia: PropTypes.func.isRequired, onOpenVideo: PropTypes.func.isRequired, + onToggleHidden: PropTypes.func.isRequired, }; handleAccountClick = (e) => { @@ -37,6 +38,10 @@ export default class DetailedStatus extends ImmutablePureComponent { this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime); } + handleExpandedToggle = () => { + this.props.onToggleHidden(this.props.status); + } + render () { const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status; @@ -57,6 +62,7 @@ export default class DetailedStatus extends ImmutablePureComponent { src={video.get('url')} width={300} height={150} + inline onOpenVideo={this.handleOpenVideo} sensitive={status.get('sensitive')} /> @@ -104,7 +110,7 @@ export default class DetailedStatus extends ImmutablePureComponent { <DisplayName account={status.get('account')} /> </a> - <StatusContent status={status} /> + <StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} /> {media} diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index 73ea9321d..2f482b292 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -21,12 +21,19 @@ import { mentionCompose, } from '../../actions/compose'; import { blockAccount } from '../../actions/accounts'; -import { muteStatus, unmuteStatus, deleteStatus } from '../../actions/statuses'; +import { + muteStatus, + unmuteStatus, + deleteStatus, + hideStatus, + revealStatus, +} from '../../actions/statuses'; import { initMuteModal } from '../../actions/mutes'; import { initReport } from '../../actions/reports'; import { makeGetStatus } from '../../selectors'; import { ScrollContainer } from 'react-router-scroll-4'; import ColumnBackButton from '../../components/column_back_button'; +import ColumnHeader from '../../components/column_header'; import StatusContainer from '../../containers/status_container'; import { openModal } from '../../actions/modal'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; @@ -39,6 +46,8 @@ const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, + revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' }, + hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' }, }); const makeMapStateToProps = () => { @@ -163,6 +172,25 @@ export default class Status extends ImmutablePureComponent { } } + handleToggleHidden = (status) => { + if (status.get('hidden')) { + this.props.dispatch(revealStatus(status.get('id'))); + } else { + this.props.dispatch(hideStatus(status.get('id'))); + } + } + + handleToggleAll = () => { + const { status, ancestorsIds, descendantsIds } = this.props; + const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS()); + + if (status.get('hidden')) { + this.props.dispatch(revealStatus(statusIds)); + } else { + this.props.dispatch(hideStatus(statusIds)); + } + } + handleBlockClick = (account) => { const { dispatch, intl } = this.props; @@ -293,7 +321,7 @@ export default class Status extends ImmutablePureComponent { render () { let ancestors, descendants; - const { status, ancestorsIds, descendantsIds } = this.props; + const { status, ancestorsIds, descendantsIds, intl } = this.props; const { fullscreen } = this.state; if (status === null) { @@ -325,7 +353,12 @@ export default class Status extends ImmutablePureComponent { return ( <Column> - <ColumnBackButton /> + <ColumnHeader + showBackButton + extraButton={( + <button className='column-header__button' title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={status.get('hidden') ? 'false' : 'true'}><i className={`fa fa-${status.get('hidden') ? 'eye-slash' : 'eye'}`} /></button> + )} + /> <ScrollContainer scrollKey='thread'> <div className={classNames('scrollable', 'detailed-status__wrapper', { fullscreen })} ref={this.setRef}> @@ -337,6 +370,7 @@ export default class Status extends ImmutablePureComponent { status={status} onOpenVideo={this.handleOpenVideo} onOpenMedia={this.handleOpenMedia} + onToggleHidden={this.handleToggleHidden} /> <ActionBar diff --git a/app/javascript/mastodon/features/ui/components/__tests__/column-test.js b/app/javascript/mastodon/features/ui/components/__tests__/column-test.js index 1e5e1d8dc..11cc1b6e8 100644 --- a/app/javascript/mastodon/features/ui/components/__tests__/column-test.js +++ b/app/javascript/mastodon/features/ui/components/__tests__/column-test.js @@ -21,13 +21,13 @@ describe('<Column />', () => { <div className='scrollable' /> </Column> ); - wrapper.find(ColumnHeader).simulate('click'); + wrapper.find(ColumnHeader).find('button').simulate('click'); expect(global.requestAnimationFrame.mock.calls.length).toEqual(1); }); it('does not try to scroll if there is no scrollable content', () => { const wrapper = mount(<Column heading='notifications' />); - wrapper.find(ColumnHeader).simulate('click'); + wrapper.find(ColumnHeader).find('button').simulate('click'); expect(global.requestAnimationFrame.mock.calls.length).toEqual(0); }); }); diff --git a/app/javascript/mastodon/features/ui/components/bundle.js b/app/javascript/mastodon/features/ui/components/bundle.js index fc88e0c70..e7d935251 100644 --- a/app/javascript/mastodon/features/ui/components/bundle.js +++ b/app/javascript/mastodon/features/ui/components/bundle.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; const emptyComponent = () => null; const noop = () => { }; -class Bundle extends React.Component { +class Bundle extends React.PureComponent { static propTypes = { fetchComponent: PropTypes.func.isRequired, @@ -26,7 +26,7 @@ class Bundle extends React.Component { onFetchFail: noop, } - static cache = {} + static cache = new Map state = { mod: undefined, @@ -51,13 +51,12 @@ class Bundle extends React.Component { load = (props) => { const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props; + const cachedMod = Bundle.cache.get(fetchComponent); onFetch(); - if (Bundle.cache[fetchComponent.name]) { - const mod = Bundle.cache[fetchComponent.name]; - - this.setState({ mod: mod.default }); + if (cachedMod) { + this.setState({ mod: cachedMod.default }); onFetchSuccess(); return Promise.resolve(); } @@ -71,7 +70,7 @@ class Bundle extends React.Component { return fetchComponent() .then((mod) => { - Bundle.cache[fetchComponent.name] = mod; + Bundle.cache.set(fetchComponent, mod); this.setState({ mod: mod.default }); onFetchSuccess(); }) diff --git a/app/javascript/mastodon/features/ui/components/bundle_column_error.js b/app/javascript/mastodon/features/ui/components/bundle_column_error.js index cd124746a..f39ebd900 100644 --- a/app/javascript/mastodon/features/ui/components/bundle_column_error.js +++ b/app/javascript/mastodon/features/ui/components/bundle_column_error.js @@ -13,7 +13,7 @@ const messages = defineMessages({ retry: { id: 'bundle_column_error.retry', defaultMessage: 'Try again' }, }); -class BundleColumnError extends React.Component { +class BundleColumnError extends React.PureComponent { static propTypes = { onRetry: PropTypes.func.isRequired, diff --git a/app/javascript/mastodon/features/ui/components/bundle_modal_error.js b/app/javascript/mastodon/features/ui/components/bundle_modal_error.js index 928bfe1f7..f9365b95b 100644 --- a/app/javascript/mastodon/features/ui/components/bundle_modal_error.js +++ b/app/javascript/mastodon/features/ui/components/bundle_modal_error.js @@ -10,7 +10,7 @@ const messages = defineMessages({ close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' }, }); -class BundleModalError extends React.Component { +class BundleModalError extends React.PureComponent { static propTypes = { onRetry: PropTypes.func.isRequired, diff --git a/app/javascript/mastodon/features/ui/components/column_header.js b/app/javascript/mastodon/features/ui/components/column_header.js index fdf9aab1a..e8bdd8054 100644 --- a/app/javascript/mastodon/features/ui/components/column_header.js +++ b/app/javascript/mastodon/features/ui/components/column_header.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; export default class ColumnHeader extends React.PureComponent { @@ -16,19 +17,20 @@ export default class ColumnHeader extends React.PureComponent { } render () { - const { type, active, columnHeaderId } = this.props; + const { icon, type, active, columnHeaderId } = this.props; + let iconElement = ''; - let icon = ''; - - if (this.props.icon) { - icon = <i className={`fa fa-fw fa-${this.props.icon} column-header__icon`} />; + if (icon) { + iconElement = <i className={`fa fa-fw fa-${icon} column-header__icon`} />; } return ( - <div role='button heading' tabIndex='0' className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick} id={columnHeaderId || null}> - {icon} - {type} - </div> + <h1 className={classNames('column-header', { active })} id={columnHeaderId || null}> + <button onClick={this.handleClick}> + {iconElement} + {type} + </button> + </h1> ); } diff --git a/app/javascript/mastodon/features/ui/components/column_link.js b/app/javascript/mastodon/features/ui/components/column_link.js index a90616213..25c2d1cf8 100644 --- a/app/javascript/mastodon/features/ui/components/column_link.js +++ b/app/javascript/mastodon/features/ui/components/column_link.js @@ -2,12 +2,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; -const ColumnLink = ({ icon, text, to, href, method }) => { +const ColumnLink = ({ icon, text, to, href, method, badge }) => { + const badgeElement = typeof badge !== 'undefined' ? <span className='column-link__badge'>{badge}</span> : null; + if (href) { return ( <a href={href} className='column-link' data-method={method}> <i className={`fa fa-fw fa-${icon} column-link__icon`} /> {text} + {badgeElement} </a> ); } else { @@ -15,6 +18,7 @@ const ColumnLink = ({ icon, text, to, href, method }) => { <Link to={to} className='column-link'> <i className={`fa fa-fw fa-${icon} column-link__icon`} /> {text} + {badgeElement} </Link> ); } @@ -26,6 +30,7 @@ ColumnLink.propTypes = { to: PropTypes.string, href: PropTypes.string, method: PropTypes.string, + badge: PropTypes.node, }; export default ColumnLink; diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index a01e5a390..e82c46402 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -6,6 +6,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import ReactSwipeableViews from 'react-swipeable-views'; import { links, getIndex, getLink } from './tabs_bar'; +import { Link } from 'react-router-dom'; import BundleContainer from '../containers/bundle_container'; import ColumnLoading from './column_loading'; @@ -152,11 +153,19 @@ export default class ColumnsArea extends ImmutablePureComponent { this.pendingIndex = null; if (singleColumn) { - return columnIndex !== -1 ? ( - <ReactSwipeableViews index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}> + const floatingActionButton = this.context.router.history.location.pathname === '/statuses/new' ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button'><i className='fa fa-pencil' /></Link>; + + return columnIndex !== -1 ? [ + <ReactSwipeableViews key='content' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}> {links.map(this.renderView)} - </ReactSwipeableViews> - ) : <div className='columns-area'>{children}</div>; + </ReactSwipeableViews>, + + floatingActionButton, + ] : [ + <div className='columns-area'>{children}</div>, + + floatingActionButton, + ]; } return ( diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.js new file mode 100644 index 000000000..1038e1864 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.js @@ -0,0 +1,122 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; +import ImageLoader from './image_loader'; +import classNames from 'classnames'; +import { changeUploadCompose } from '../../../actions/compose'; +import { getPointerPosition } from '../../video'; + +const mapStateToProps = (state, { id }) => ({ + media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id), +}); + +const mapDispatchToProps = (dispatch, { id }) => ({ + + onSave: (x, y) => { + dispatch(changeUploadCompose(id, { focus: `${x.toFixed(2)},${y.toFixed(2)}` })); + }, + +}); + +@connect(mapStateToProps, mapDispatchToProps) +export default class FocalPointModal extends ImmutablePureComponent { + + static propTypes = { + media: ImmutablePropTypes.map.isRequired, + }; + + state = { + x: 0, + y: 0, + focusX: 0, + focusY: 0, + dragging: false, + }; + + componentWillMount () { + this.updatePositionFromMedia(this.props.media); + } + + componentWillReceiveProps (nextProps) { + if (this.props.media.get('id') !== nextProps.media.get('id')) { + this.updatePositionFromMedia(nextProps.media); + } + } + + componentWillUnmount () { + document.removeEventListener('mousemove', this.handleMouseMove); + document.removeEventListener('mouseup', this.handleMouseUp); + } + + handleMouseDown = e => { + document.addEventListener('mousemove', this.handleMouseMove); + document.addEventListener('mouseup', this.handleMouseUp); + + this.updatePosition(e); + this.setState({ dragging: true }); + } + + handleMouseMove = e => { + this.updatePosition(e); + } + + handleMouseUp = () => { + document.removeEventListener('mousemove', this.handleMouseMove); + document.removeEventListener('mouseup', this.handleMouseUp); + + this.setState({ dragging: false }); + this.props.onSave(this.state.focusX, this.state.focusY); + } + + updatePosition = e => { + const { x, y } = getPointerPosition(this.node, e); + const focusX = (x - .5) * 2; + const focusY = (y - .5) * -2; + + this.setState({ x, y, focusX, focusY }); + } + + updatePositionFromMedia = media => { + const focusX = media.getIn(['meta', 'focus', 'x']); + const focusY = media.getIn(['meta', 'focus', 'y']); + + if (focusX && focusY) { + const x = (focusX / 2) + .5; + const y = (focusY / -2) + .5; + + this.setState({ x, y, focusX, focusY }); + } else { + this.setState({ x: 0.5, y: 0.5, focusX: 0, focusY: 0 }); + } + } + + setRef = c => { + this.node = c; + } + + render () { + const { media } = this.props; + const { x, y, dragging } = this.state; + + const width = media.getIn(['meta', 'original', 'width']) || null; + const height = media.getIn(['meta', 'original', 'height']) || null; + + return ( + <div className='modal-root__modal video-modal'> + <div className={classNames('focal-point', { dragging })} ref={this.setRef}> + <ImageLoader + previewSrc={media.get('preview_url')} + src={media.get('url')} + width={width} + height={height} + /> + + <div className='focal-point__reticle' style={{ top: `${y * 100}%`, left: `${x * 100}%` }} /> + <div className='focal-point__overlay' onMouseDown={this.handleMouseDown} /> + </div> + </div> + ); + } + +} diff --git a/app/javascript/mastodon/features/ui/components/image_loader.js b/app/javascript/mastodon/features/ui/components/image_loader.js index e3e7197c5..c7360a726 100644 --- a/app/javascript/mastodon/features/ui/components/image_loader.js +++ b/app/javascript/mastodon/features/ui/components/image_loader.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import ZoomableImage from './zoomable_image'; export default class ImageLoader extends React.PureComponent { @@ -10,6 +11,7 @@ export default class ImageLoader extends React.PureComponent { previewSrc: PropTypes.string, width: PropTypes.number, height: PropTypes.number, + onClick: PropTypes.func, } static defaultProps = { @@ -24,6 +26,7 @@ export default class ImageLoader extends React.PureComponent { } removers = []; + canvas = null; get canvasContext() { if (!this.canvas) { @@ -43,6 +46,10 @@ export default class ImageLoader extends React.PureComponent { } } + componentWillUnmount () { + this.removeEventListeners(); + } + loadImage (props) { this.removeEventListeners(); this.setState({ loading: true, error: false }); @@ -118,7 +125,7 @@ export default class ImageLoader extends React.PureComponent { } render () { - const { alt, src, width, height } = this.props; + const { alt, src, width, height, onClick } = this.props; const { loading } = this.state; const className = classNames('image-loader', { @@ -128,22 +135,19 @@ export default class ImageLoader extends React.PureComponent { return ( <div className={className}> - <canvas - className='image-loader__preview-canvas' - width={width} - height={height} - ref={this.setCanvasRef} - style={{ opacity: loading ? 1 : 0 }} - /> - - {!loading && ( - <img - alt={alt} - className='image-loader__img' - src={src} + {loading ? ( + <canvas + className='image-loader__preview-canvas' + ref={this.setCanvasRef} width={width} height={height} /> + ) : ( + <ZoomableImage + alt={alt} + src={src} + onClick={onClick} + /> )} </div> ); diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index 02591a51f..72ef32256 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -3,6 +3,7 @@ import ReactSwipeableViews from 'react-swipeable-views'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import ExtendedVideoPlayer from '../../../components/extended_video_player'; +import classNames from 'classnames'; import { defineMessages, injectIntl } from 'react-intl'; import IconButton from '../../../components/icon_button'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -26,6 +27,7 @@ export default class MediaModal extends ImmutablePureComponent { state = { index: null, + navigationHidden: false, }; handleSwipe = (index) => { @@ -68,14 +70,21 @@ export default class MediaModal extends ImmutablePureComponent { return this.state.index !== null ? this.state.index : this.props.index; } + toggleNavigation = () => { + this.setState(prevState => ({ + navigationHidden: !prevState.navigationHidden, + })); + }; + render () { const { media, intl, onClose } = this.props; + const { navigationHidden } = this.state; const index = this.getIndex(); let pagination = []; - const leftNav = media.size > 1 && <button tabIndex='0' className='modal-container__nav modal-container__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><i className='fa fa-fw fa-chevron-left' /></button>; - const rightNav = media.size > 1 && <button tabIndex='0' className='modal-container__nav modal-container__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><i className='fa fa-fw fa-chevron-right' /></button>; + const leftNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><i className='fa fa-fw fa-chevron-left' /></button>; + const rightNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><i className='fa fa-fw fa-chevron-right' /></button>; if (media.size > 1) { pagination = media.map((item, i) => { @@ -92,9 +101,30 @@ 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('url')} />; + return ( + <ImageLoader + previewSrc={image.get('preview_url')} + src={image.get('url')} + width={width} + height={height} + alt={image.get('description')} + key={image.get('url')} + onClick={this.toggleNavigation} + /> + ); } 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')} />; + return ( + <ExtendedVideoPlayer + src={image.get('url')} + muted + controls={false} + width={width} + height={height} + key={image.get('preview_url')} + alt={image.get('description')} + onClick={this.toggleNavigation} + /> + ); } return null; @@ -104,21 +134,43 @@ export default class MediaModal extends ImmutablePureComponent { alignItems: 'center', // center vertically }; + const navigationClassName = classNames('media-modal__navigation', { + 'media-modal__navigation--hidden': navigationHidden, + }); + return ( <div className='modal-root__modal media-modal'> - {leftNav} - - <div className='media-modal__content'> - <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} /> - <ReactSwipeableViews containerStyle={containerStyle} onChangeIndex={this.handleSwipe} index={index}> - {content} - </ReactSwipeableViews> + <div + className='media-modal__closer' + role='presentation' + onClick={onClose} + > + <div className='media-modal__content'> + <ReactSwipeableViews + style={{ + // you can't use 100vh, because the viewport height is taller + // than the visible part of the document in some mobile + // browsers when it's address bar is visible. + // https://developers.google.com/web/updates/2016/12/url-bar-resizing + height: `${document.body.clientHeight}px`, + }} + containerStyle={containerStyle} + onChangeIndex={this.handleSwipe} + onSwitching={this.handleSwitching} + index={index} + > + {content} + </ReactSwipeableViews> + </div> + </div> + <div className={navigationClassName}> + <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={40} /> + {leftNav} + {rightNav} + <ul className='media-modal__pagination'> + {pagination} + </ul> </div> - <ul className='media-modal__pagination'> - {pagination} - </ul> - - {rightNav} </div> ); } diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index 5839ba40a..20bf21153 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -8,6 +8,7 @@ import MediaModal from './media_modal'; import VideoModal from './video_modal'; import BoostModal from './boost_modal'; import ConfirmationModal from './confirmation_modal'; +import FocalPointModal from './focal_point_modal'; import { OnboardingModal, MuteModal, @@ -27,6 +28,7 @@ const MODAL_COMPONENTS = { 'ACTIONS': () => Promise.resolve({ default: ActionsModal }), 'EMBED': EmbedModal, 'LIST_EDITOR': ListEditor, + 'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }), }; export default class ModalRoot extends React.PureComponent { diff --git a/app/javascript/mastodon/features/ui/components/report_modal.js b/app/javascript/mastodon/features/ui/components/report_modal.js index b5dfa422e..3ae97646f 100644 --- a/app/javascript/mastodon/features/ui/components/report_modal.js +++ b/app/javascript/mastodon/features/ui/components/report_modal.js @@ -1,6 +1,6 @@ import React from 'react'; import { connect } from 'react-redux'; -import { changeReportComment, submitReport } from '../../../actions/reports'; +import { changeReportComment, changeReportForward, submitReport } from '../../../actions/reports'; import { refreshAccountTimeline } from '../../../actions/timelines'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; @@ -10,8 +10,11 @@ import StatusCheckBox from '../../report/containers/status_check_box_container'; import { OrderedSet } from 'immutable'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Button from '../../../components/button'; +import Toggle from 'react-toggle'; +import IconButton from '../../../components/icon_button'; const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' }, placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' }, submit: { id: 'report.submit', defaultMessage: 'Submit' }, }); @@ -26,6 +29,7 @@ const makeMapStateToProps = () => { isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']), account: getAccount(state, accountId), comment: state.getIn(['reports', 'new', 'comment']), + forward: state.getIn(['reports', 'new', 'forward']), statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])), }; }; @@ -42,14 +46,19 @@ export default class ReportModal extends ImmutablePureComponent { account: ImmutablePropTypes.map, statusIds: ImmutablePropTypes.orderedSet.isRequired, comment: PropTypes.string.isRequired, + forward: PropTypes.bool, dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; - handleCommentChange = (e) => { + handleCommentChange = e => { this.props.dispatch(changeReportComment(e.target.value)); } + handleForwardChange = e => { + this.props.dispatch(changeReportForward(e.target.checked)); + } + handleSubmit = () => { this.props.dispatch(submitReport()); } @@ -65,26 +74,25 @@ export default class ReportModal extends ImmutablePureComponent { } render () { - const { account, comment, intl, statusIds, isSubmitting } = this.props; + const { account, comment, intl, statusIds, isSubmitting, forward, onClose } = this.props; if (!account) { return null; } + const domain = account.get('acct').split('@')[1]; + return ( <div className='modal-root__modal report-modal'> <div className='report-modal__target'> + <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} /> <FormattedMessage id='report.target' defaultMessage='Report {target}' values={{ target: <strong>{account.get('acct')}</strong> }} /> </div> <div className='report-modal__container'> - <div className='report-modal__statuses'> - <div> - {statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)} - </div> - </div> - <div className='report-modal__comment'> + <p><FormattedMessage id='report.hint' defaultMessage='The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:' /></p> + <textarea className='setting-text light' placeholder={intl.formatMessage(messages.placeholder)} @@ -92,11 +100,26 @@ export default class ReportModal extends ImmutablePureComponent { onChange={this.handleCommentChange} disabled={isSubmitting} /> + + {domain && ( + <div> + <p><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' /></p> + + <div className='setting-toggle'> + <Toggle id='report-forward' checked={forward} disabled={isSubmitting} onChange={this.handleForwardChange} /> + <label htmlFor='report-forward' className='setting-toggle__label'><FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} /></label> + </div> + </div> + )} + + <Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} /> </div> - </div> - <div className='report-modal__action-bar'> - <Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} /> + <div className='report-modal__statuses'> + <div> + {statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)} + </div> + </div> </div> </div> ); diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js index 7694e5ab3..dba3be98b 100644 --- a/app/javascript/mastodon/features/ui/components/tabs_bar.js +++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js @@ -1,19 +1,18 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { NavLink } from 'react-router-dom'; +import { NavLink, withRouter } from 'react-router-dom'; import { FormattedMessage, injectIntl } from 'react-intl'; import { debounce } from 'lodash'; import { isUserTouching } from '../../../is_mobile'; export const links = [ - <NavLink className='tabs-bar__link primary' to='/statuses/new' data-preview-title-id='tabs_bar.compose' data-preview-icon='pencil' ><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></NavLink>, <NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>, <NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>, <NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>, <NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>, - <NavLink className='tabs-bar__link primary' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='asterisk' ><i className='fa fa-fw fa-asterisk' /></NavLink>, + <NavLink className='tabs-bar__link primary' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><i className='fa fa-fw fa-bars' /></NavLink>, ]; export function getIndex (path) { @@ -25,14 +24,12 @@ export function getLink (index) { } @injectIntl -export default class TabsBar extends React.Component { - - static contextTypes = { - router: PropTypes.object.isRequired, - } +@withRouter +export default class TabsBar extends React.PureComponent { static propTypes = { intl: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, } setRef = ref => { @@ -60,7 +57,7 @@ export default class TabsBar extends React.Component { const listener = debounce(() => { nextTab.removeEventListener('transitionend', listener); - this.context.router.history.push(to); + this.props.history.push(to); }, 50); nextTab.addEventListener('transitionend', listener); diff --git a/app/javascript/mastodon/features/ui/components/video_modal.js b/app/javascript/mastodon/features/ui/components/video_modal.js index 6a883759f..9ed4a43ad 100644 --- a/app/javascript/mastodon/features/ui/components/video_modal.js +++ b/app/javascript/mastodon/features/ui/components/video_modal.js @@ -16,7 +16,7 @@ export default class VideoModal extends ImmutablePureComponent { const { media, time, onClose } = this.props; return ( - <div className='modal-root__modal media-modal'> + <div className='modal-root__modal video-modal'> <div> <Video preview={media.get('preview_url')} diff --git a/app/javascript/mastodon/features/ui/components/zoomable_image.js b/app/javascript/mastodon/features/ui/components/zoomable_image.js new file mode 100644 index 000000000..0a0a4d41a --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/zoomable_image.js @@ -0,0 +1,151 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const MIN_SCALE = 1; +const MAX_SCALE = 4; + +const getMidpoint = (p1, p2) => ({ + x: (p1.clientX + p2.clientX) / 2, + y: (p1.clientY + p2.clientY) / 2, +}); + +const getDistance = (p1, p2) => + Math.sqrt(Math.pow(p1.clientX - p2.clientX, 2) + Math.pow(p1.clientY - p2.clientY, 2)); + +const clamp = (min, max, value) => Math.min(max, Math.max(min, value)); + +export default class ZoomableImage extends React.PureComponent { + + static propTypes = { + alt: PropTypes.string, + src: PropTypes.string.isRequired, + width: PropTypes.number, + height: PropTypes.number, + onClick: PropTypes.func, + } + + static defaultProps = { + alt: '', + width: null, + height: null, + }; + + state = { + scale: MIN_SCALE, + } + + removers = []; + container = null; + image = null; + lastTouchEndTime = 0; + lastDistance = 0; + + componentDidMount () { + let handler = this.handleTouchStart; + this.container.addEventListener('touchstart', handler); + this.removers.push(() => this.container.removeEventListener('touchstart', handler)); + handler = this.handleTouchMove; + // on Chrome 56+, touch event listeners will default to passive + // https://www.chromestatus.com/features/5093566007214080 + this.container.addEventListener('touchmove', handler, { passive: false }); + this.removers.push(() => this.container.removeEventListener('touchend', handler)); + } + + componentWillUnmount () { + this.removeEventListeners(); + } + + removeEventListeners () { + this.removers.forEach(listeners => listeners()); + this.removers = []; + } + + handleTouchStart = e => { + if (e.touches.length !== 2) return; + + this.lastDistance = getDistance(...e.touches); + } + + handleTouchMove = e => { + const { scrollTop, scrollHeight, clientHeight } = this.container; + if (e.touches.length === 1 && scrollTop !== scrollHeight - clientHeight) { + // prevent propagating event to MediaModal + e.stopPropagation(); + return; + } + if (e.touches.length !== 2) return; + + e.preventDefault(); + e.stopPropagation(); + + const distance = getDistance(...e.touches); + const midpoint = getMidpoint(...e.touches); + const scale = clamp(MIN_SCALE, MAX_SCALE, this.state.scale * distance / this.lastDistance); + + this.zoom(scale, midpoint); + + this.lastMidpoint = midpoint; + this.lastDistance = distance; + } + + zoom(nextScale, midpoint) { + const { scale } = this.state; + const { scrollLeft, scrollTop } = this.container; + + // math memo: + // x = (scrollLeft + midpoint.x) / scrollWidth + // x' = (nextScrollLeft + midpoint.x) / nextScrollWidth + // scrollWidth = clientWidth * scale + // scrollWidth' = clientWidth * nextScale + // Solve x = x' for nextScrollLeft + const nextScrollLeft = (scrollLeft + midpoint.x) * nextScale / scale - midpoint.x; + const nextScrollTop = (scrollTop + midpoint.y) * nextScale / scale - midpoint.y; + + this.setState({ scale: nextScale }, () => { + this.container.scrollLeft = nextScrollLeft; + this.container.scrollTop = nextScrollTop; + }); + } + + handleClick = e => { + // don't propagate event to MediaModal + e.stopPropagation(); + const handler = this.props.onClick; + if (handler) handler(); + } + + setContainerRef = c => { + this.container = c; + } + + setImageRef = c => { + this.image = c; + } + + render () { + const { alt, src } = this.props; + const { scale } = this.state; + const overflow = scale === 1 ? 'hidden' : 'scroll'; + + return ( + <div + className='zoomable-image' + ref={this.setContainerRef} + style={{ overflow }} + > + <img + role='presentation' + ref={this.setImageRef} + alt={alt} + src={src} + style={{ + transform: `scale(${scale})`, + transformOrigin: '0 0', + }} + onClick={this.handleClick} + /> + </div> + ); + } + +} diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js index 59b53d823..fc2867cf0 100644 --- a/app/javascript/mastodon/features/ui/containers/status_list_container.js +++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js @@ -56,10 +56,7 @@ const makeMapStateToProps = () => { const mapDispatchToProps = (dispatch, { timelineId, loadMore }) => ({ - onScrollToBottom: debounce(() => { - dispatch(scrollTopTimeline(timelineId, false)); - loadMore(); - }, 300, { leading: true }), + onLoadMore: debounce(loadMore, 300, { leading: true }), onScrollToTop: debounce(() => { dispatch(scrollTopTimeline(timelineId, true)); diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 5b0d7246a..6cf00222a 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -1,3 +1,4 @@ +import classNames from 'classnames'; import React from 'react'; import NotificationsContainer from './containers/notifications_container'; import PropTypes from 'prop-types'; @@ -55,6 +56,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ isComposing: state.getIn(['compose', 'is_composing']), hasComposingText: state.getIn(['compose', 'text']) !== '', + dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null, }); const keyMap = { @@ -84,10 +86,93 @@ const keyMap = { goToMuted: 'g m', }; +class SwitchingColumnsArea extends React.PureComponent { + + static propTypes = { + children: PropTypes.node, + location: PropTypes.object, + onLayoutChange: PropTypes.func.isRequired, + }; + + state = { + mobile: isMobile(window.innerWidth), + }; + + componentWillMount () { + window.addEventListener('resize', this.handleResize, { passive: true }); + } + + componentDidUpdate (prevProps) { + if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) { + this.node.handleChildrenContentChange(); + } + } + + componentWillUnmount () { + window.removeEventListener('resize', this.handleResize); + } + + handleResize = debounce(() => { + // The cached heights are no longer accurate, invalidate + this.props.onLayoutChange(); + + this.setState({ mobile: isMobile(window.innerWidth) }); + }, 500, { + trailing: true, + }); + + setRef = c => { + this.node = c.getWrappedInstance().getWrappedInstance(); + } + + render () { + const { children } = this.props; + const { mobile } = this.state; + + return ( + <ColumnsAreaContainer ref={this.setRef} singleColumn={mobile}> + <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} /> + <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} /> + + <WrappedRoute path='/statuses/new' component={Compose} content={children} /> + <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} /> + <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} /> + <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} /> + + <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} /> + <WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} /> + <WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} /> + <WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} /> + <WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} /> + + <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} /> + <WrappedRoute path='/blocks' component={Blocks} content={children} /> + <WrappedRoute path='/mutes' component={Mutes} content={children} /> + <WrappedRoute path='/lists' component={Lists} content={children} /> + + <WrappedRoute component={GenericNotFound} content={children} /> + </WrappedSwitch> + </ColumnsAreaContainer> + ); + } + +} + @connect(mapStateToProps) @injectIntl @withRouter -export default class UI extends React.Component { +export default class UI extends React.PureComponent { static contextTypes = { router: PropTypes.object.isRequired, @@ -100,10 +185,10 @@ export default class UI extends React.Component { hasComposingText: PropTypes.bool, location: PropTypes.object, intl: PropTypes.object.isRequired, + dropdownMenuIsOpen: PropTypes.bool, }; state = { - width: window.innerWidth, draggingOver: false, }; @@ -118,14 +203,10 @@ export default class UI extends React.Component { } } - handleResize = debounce(() => { + handleLayoutChange = () => { // The cached heights are no longer accurate, invalidate this.props.dispatch(clearHeight()); - - this.setState({ width: window.innerWidth }); - }, 500, { - trailing: true, - }); + } handleDragEnter = (e) => { e.preventDefault(); @@ -193,7 +274,6 @@ export default class UI extends React.Component { componentWillMount () { window.addEventListener('beforeunload', this.handleBeforeUnload, false); - window.addEventListener('resize', this.handleResize, { passive: true }); document.addEventListener('dragenter', this.handleDragEnter, false); document.addEventListener('dragover', this.handleDragOver, false); document.addEventListener('drop', this.handleDrop, false); @@ -214,28 +294,8 @@ export default class UI extends React.Component { }; } - shouldComponentUpdate (nextProps) { - if (nextProps.isComposing !== this.props.isComposing) { - // Avoid expensive update just to toggle a class - this.node.classList.toggle('is-composing', nextProps.isComposing); - - return false; - } - - // Why isn't this working?!? - // return super.shouldComponentUpdate(nextProps, nextState); - return true; - } - - componentDidUpdate (prevProps) { - if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) { - this.columnsAreaNode.handleChildrenContentChange(); - } - } - componentWillUnmount () { window.removeEventListener('beforeunload', this.handleBeforeUnload); - window.removeEventListener('resize', this.handleResize); document.removeEventListener('dragenter', this.handleDragEnter); document.removeEventListener('dragover', this.handleDragOver); document.removeEventListener('drop', this.handleDrop); @@ -247,10 +307,6 @@ export default class UI extends React.Component { this.node = c; } - setColumnsAreaRef = c => { - this.columnsAreaNode = c.getWrappedInstance().getWrappedInstance(); - } - handleHotkeyNew = e => { e.preventDefault(); @@ -350,8 +406,8 @@ export default class UI extends React.Component { } render () { - const { width, draggingOver } = this.state; - const { children } = this.props; + const { draggingOver } = this.state; + const { children, isComposing, location, dropdownMenuIsOpen } = this.props; const handlers = { help: this.handleHotkeyToggleHelp, @@ -374,42 +430,12 @@ export default class UI extends React.Component { return ( <HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef}> - <div className='ui' ref={this.setRef}> + <div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}> <TabsBar /> - <ColumnsAreaContainer ref={this.setColumnsAreaRef} singleColumn={isMobile(width)}> - <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} /> - <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} /> - - <WrappedRoute path='/statuses/new' component={Compose} content={children} /> - <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} /> - <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} /> - <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} /> - - <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} /> - <WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} /> - <WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} /> - <WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} /> - - <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} /> - <WrappedRoute path='/blocks' component={Blocks} content={children} /> - <WrappedRoute path='/mutes' component={Mutes} content={children} /> - <WrappedRoute path='/lists' component={Lists} content={children} /> - - <WrappedRoute component={GenericNotFound} content={children} /> - </WrappedSwitch> - </ColumnsAreaContainer> + <SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange}> + {children} + </SwitchingColumnsArea> <NotificationsContainer /> <LoadingBarContainer className='loading-bar' /> diff --git a/app/javascript/mastodon/features/ui/util/react_router_helpers.js b/app/javascript/mastodon/features/ui/util/react_router_helpers.js index 43007ddc3..32dfe320b 100644 --- a/app/javascript/mastodon/features/ui/util/react_router_helpers.js +++ b/app/javascript/mastodon/features/ui/util/react_router_helpers.js @@ -35,14 +35,19 @@ export class WrappedRoute extends React.Component { component: PropTypes.func.isRequired, content: PropTypes.node, multiColumn: PropTypes.bool, - } + componentParams: PropTypes.object, + }; + + static defaultProps = { + componentParams: {}, + }; renderComponent = ({ match }) => { - const { component, content, multiColumn } = this.props; + const { component, content, multiColumn, componentParams } = this.props; return ( <BundleContainer fetchComponent={component} loading={this.renderLoading} error={this.renderError}> - {Component => <Component params={match.params} multiColumn={multiColumn}>{content}</Component>} + {Component => <Component params={match.params} multiColumn={multiColumn} {...componentParams}>{content}</Component>} </BundleContainer> ); } diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index 0ee8bb6c8..98ebcb6f9 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -4,6 +4,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { throttle } from 'lodash'; import classNames from 'classnames'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; +import { displaySensitiveMedia } from '../../initial_state'; const messages = defineMessages({ play: { id: 'video.play', defaultMessage: 'Play' }, @@ -29,7 +30,7 @@ const formatTime = secondsNum => { return (hours === '00' ? '' : `${hours}:`) + `${minutes}:${seconds}`; }; -const findElementPosition = el => { +export const findElementPosition = el => { let box; if (el.getBoundingClientRect && el.parentNode) { @@ -60,7 +61,7 @@ const findElementPosition = el => { }; }; -const getPointerPosition = (el, event) => { +export const getPointerPosition = (el, event) => { const position = {}; const box = findElementPosition(el); const boxW = el.offsetWidth; @@ -76,7 +77,7 @@ const getPointerPosition = (el, event) => { pageY = event.changedTouches[0].pageY; } - position.y = Math.max(0, Math.min(1, ((boxY - pageY) + boxH) / boxH)); + position.y = Math.max(0, Math.min(1, (pageY - boxY) / boxH)); position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW)); return position; @@ -96,6 +97,7 @@ export default class Video extends React.PureComponent { onOpenVideo: PropTypes.func, onCloseVideo: PropTypes.func, detailed: PropTypes.bool, + inline: PropTypes.bool, intl: PropTypes.object.isRequired, }; @@ -104,14 +106,21 @@ export default class Video extends React.PureComponent { duration: 0, paused: true, dragging: false, + containerWidth: false, fullscreen: false, hovered: false, muted: false, - revealed: !this.props.sensitive, + revealed: !this.props.sensitive || displaySensitiveMedia, }; setPlayerRef = c => { this.player = c; + + if (c) { + this.setState({ + containerWidth: c.offsetWidth, + }); + } } setVideoRef = c => { @@ -245,12 +254,23 @@ export default class Video extends React.PureComponent { } render () { - const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed } = this.props; - const { currentTime, duration, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; + const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed } = this.props; + const { containerWidth, currentTime, duration, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; const progress = (currentTime / duration) * 100; + const playerStyle = {}; + + let { width, height } = this.props; + + if (inline && containerWidth) { + width = containerWidth; + height = containerWidth / (16/9); + + playerStyle.width = width; + playerStyle.height = height; + } return ( - <div className={classNames('video-player', { inactive: !revealed, detailed, inline: width && height && !fullscreen, fullscreen })} style={{ width, height }} ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> + <div className={classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen })} style={playerStyle} ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> <video ref={this.setVideoRef} src={src} @@ -270,7 +290,7 @@ export default class Video extends React.PureComponent { onProgress={this.handleProgress} /> - <button className={classNames('video-player__spoiler', { active: !revealed })} onClick={this.toggleReveal}> + <button type='button' className={classNames('video-player__spoiler', { active: !revealed })} onClick={this.toggleReveal}> <span className='video-player__spoiler__title'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span> <span className='video-player__spoiler__subtitle'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> </button> @@ -289,10 +309,10 @@ export default class Video extends React.PureComponent { <div className='video-player__buttons-bar'> <div className='video-player__buttons left'> - <button aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><i className={classNames('fa fa-fw', { 'fa-play': paused, 'fa-pause': !paused })} /></button> - <button aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><i className={classNames('fa fa-fw', { 'fa-volume-off': muted, 'fa-volume-up': !muted })} /></button> + <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><i className={classNames('fa fa-fw', { 'fa-play': paused, 'fa-pause': !paused })} /></button> + <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><i className={classNames('fa fa-fw', { 'fa-volume-off': muted, 'fa-volume-up': !muted })} /></button> - {!onCloseVideo && <button aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>} + {!onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>} {(detailed || fullscreen) && <span> @@ -304,9 +324,9 @@ export default class Video extends React.PureComponent { </div> <div className='video-player__buttons right'> - {(!fullscreen && onOpenVideo) && <button aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>} - {onCloseVideo && <button aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-compress' /></button>} - <button aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><i className={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen, 'fa-compress': fullscreen })} /></button> + {(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>} + {onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-compress' /></button>} + <button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><i className={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen, 'fa-compress': fullscreen })} /></button> </div> </div> </div> diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 3fc45077d..df310e7e1 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -5,9 +5,11 @@ const getMeta = (prop) => initialState && initialState.meta && initialState.meta export const reduceMotion = getMeta('reduce_motion'); export const autoPlayGif = getMeta('auto_play_gif'); +export const displaySensitiveMedia = getMeta('display_sensitive_media'); export const unfollowModal = getMeta('unfollow_modal'); export const boostModal = getMeta('boost_modal'); export const deleteModal = getMeta('delete_modal'); export const me = getMeta('me'); +export const searchEnabled = getMeta('search_enabled'); export default initialState; diff --git a/app/javascript/mastodon/load_polyfills.js b/app/javascript/mastodon/load_polyfills.js index 8927b7358..815e1905b 100644 --- a/app/javascript/mastodon/load_polyfills.js +++ b/app/javascript/mastodon/load_polyfills.js @@ -14,6 +14,7 @@ function loadPolyfills() { const needsBasePolyfills = !( window.Intl && Object.assign && + Object.values && Number.isNaN && window.Symbol && Array.prototype.includes diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 795b27707..33e223b2a 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -1,7 +1,9 @@ { "account.block": "حظر @{name}", "account.block_domain": "إخفاء كل شيئ قادم من إسم النطاق {domain}", + "account.blocked": "محظور", "account.disclaimer_full": "قد لا تعكس المعلومات أدناه الملف الشخصي الكامل للمستخدم.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "تعديل الملف الشخصي", "account.follow": "تابِع", "account.followers": "المتابعون", @@ -13,7 +15,9 @@ "account.moved_to": "{name} إنتقل إلى :", "account.mute": "أكتم @{name}", "account.mute_notifications": "كتم إخطارات @{name}", - "account.posts": "المشاركات", + "account.muted": "مكتوم", + "account.posts": "التبويقات", + "account.posts_with_replies": "تبويقات تحتوي على رُدود", "account.report": "أبلغ عن @{name}", "account.requested": "في انتظار الموافقة", "account.share": "مشاركة @{name}'s profile", @@ -50,7 +54,7 @@ "column_header.unpin": "فك التدبيس", "column_subheading.navigation": "التصفح", "column_subheading.settings": "الإعدادات", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.hashtag_warning": "هذا التبويق لن يُدرَج تحت أي وسم كان بما أنه غير مُدرَج. لا يُسمح بالبحث إلّا عن التبويقات العمومية عن طريق الوسوم.", "compose_form.lock_disclaimer": "حسابك ليس {locked}. يمكن لأي شخص متابعتك و عرض المنشورات.", "compose_form.lock_disclaimer.lock": "مقفل", "compose_form.placeholder": "فيمَ تفكّر؟", @@ -139,6 +143,7 @@ "loading_indicator.label": "تحميل ...", "media_gallery.toggle_visible": "عرض / إخفاء", "missing_indicator.label": "تعذر العثور عليه", + "missing_indicator.sublabel": "تعذر العثور على هذا المورد", "mute_modal.hide_notifications": "هل تود إخفاء الإخطارات القادمة من هذا المستخدم ؟", "navigation_bar.blocks": "الحسابات المحجوبة", "navigation_bar.community_timeline": "الخيط العام المحلي", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "تعرض الصفحة الرئيسية منشورات جميع الأشخاص الذين تتابعهم.", "onboarding.page_four.notifications": "فعندما يتفاعل شخص ما معك، عمود الإخطارات يخبرك.", "onboarding.page_one.federation": "ماستدون شبكة من خوادم مستقلة متلاحمة تهدف إلى إنشاء أكبر شبكة اجتماعية موحدة. تسمى هذه السرفيرات بمثيلات خوادم.", - "onboarding.page_one.handle": "أنت الآن على {domain}، واحد من مجموع مثيلات الخوادم المستقلة. اسم المستخدم الكامل الخاص بك هو {handle}", + "onboarding.page_one.full_handle": "عنوانك الكامل", + "onboarding.page_one.handle_hint": "هذا هو ما يجب عليك توصيله لأصدقائك للبحث عنه.", "onboarding.page_one.welcome": "مرحبا بك في ماستدون !", "onboarding.page_six.admin": "مدير(ة) مثيل الخادم هذا {admin}.", "onboarding.page_six.almost_done": "أنهيت تقريبا ...", @@ -197,21 +203,30 @@ "privacy.public.short": "للعامة", "privacy.unlisted.long": "لا تقم بإدراجه على الخيوط العامة", "privacy.unlisted.short": "غير مدرج", + "regeneration_indicator.label": "جارٍ التحميل …", + "regeneration_indicator.sublabel": "جارٍ تجهيز تغذية صفحتك الرئيسية !", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", "relative_time.just_now": "الآن", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "reply_indicator.cancel": "إلغاء", + "report.forward": "التحويل إلى {target}", + "report.forward_hint": "هذا الحساب ينتمي إلى خادوم آخَر. هل تودّ إرسال نسخة مجهولة مِن التقرير إلى هنالك أيضًا ؟", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "تعليقات إضافية", "report.submit": "إرسال", "report.target": "إبلاغ", "search.placeholder": "ابحث", "search_popout.search_format": "نمط البحث المتقدم", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "وسم", "search_popout.tips.status": "حالة", "search_popout.tips.text": "جملة قصيرة تُمكّنُك من عرض أسماء و حسابات و كلمات رمزية", "search_popout.tips.user": "مستخدِم", + "search_results.accounts": "أشخاص", + "search_results.hashtags": "الوُسوم", + "search_results.statuses": "التبويقات", "search_results.total": "{count, number} {count, plural, one {result} و {results}}", "standalone.public_title": "نظرة على ...", "status.block": "Block @{name}", @@ -223,10 +238,11 @@ "status.media_hidden": "الصورة مستترة", "status.mention": "أذكُر @{name}", "status.more": "المزيد", - "status.mute": "Mute @{name}", + "status.mute": "أكتم @{name}", "status.mute_conversation": "كتم المحادثة", "status.open": "وسع هذه المشاركة", "status.pin": "تدبيس على الملف الشخصي", + "status.pinned": "تبويق مثبَّت", "status.reblog": "رَقِّي", "status.reblogged_by": "{name} رقى", "status.reply": "ردّ", @@ -239,7 +255,6 @@ "status.show_more": "أظهر المزيد", "status.unmute_conversation": "فك الكتم عن المحادثة", "status.unpin": "فك التدبيس من الملف الشخصي", - "tabs_bar.compose": "تحرير", "tabs_bar.federated_timeline": "الموحَّد", "tabs_bar.home": "الرئيسية", "tabs_bar.local_timeline": "المحلي", @@ -248,6 +263,7 @@ "upload_area.title": "إسحب ثم أفلت للرفع", "upload_button.label": "إضافة وسائط", "upload_form.description": "وصف للمعاقين بصريا", + "upload_form.focus": "قص", "upload_form.undo": "إلغاء", "upload_progress.label": "يرفع...", "video.close": "إغلاق الفيديو", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index c0a24dacb..a84e6e9d1 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -1,7 +1,9 @@ { "account.block": "Блокирай", "account.block_domain": "Hide everything from {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Редактирай профила си", "account.follow": "Последвай", "account.followers": "Последователи", @@ -13,7 +15,9 @@ "account.moved_to": "{name} has moved to:", "account.mute": "Mute @{name}", "account.mute_notifications": "Mute notifications from @{name}", + "account.muted": "Muted", "account.posts": "Публикации", + "account.posts_with_replies": "Toots with replies", "account.report": "Report @{name}", "account.requested": "В очакване на одобрение", "account.share": "Share @{name}'s profile", @@ -139,6 +143,7 @@ "loading_indicator.label": "Зареждане...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", + "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Blocked users", "navigation_bar.community_timeline": "Local timeline", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "The home timeline shows posts from people you follow.", "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", - "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.full_handle": "Your full handle", + "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.", "onboarding.page_one.welcome": "Welcome to Mastodon!", "onboarding.page_six.admin": "Your instance's admin is {admin}.", "onboarding.page_six.almost_done": "Almost done...", @@ -197,21 +203,30 @@ "privacy.public.short": "Public", "privacy.unlisted.long": "Do not show in public timelines", "privacy.unlisted.short": "Unlisted", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", "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": "Отказ", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting", "search.placeholder": "Търсене", "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "status", "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "standalone.public_title": "A look inside...", "status.block": "Block @{name}", @@ -227,6 +242,7 @@ "status.mute_conversation": "Mute conversation", "status.open": "Expand this status", "status.pin": "Pin on profile", + "status.pinned": "Pinned toot", "status.reblog": "Споделяне", "status.reblogged_by": "{name} сподели", "status.reply": "Отговор", @@ -239,7 +255,6 @@ "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", "status.unpin": "Unpin from profile", - "tabs_bar.compose": "Съставяне", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Начало", "tabs_bar.local_timeline": "Local", @@ -248,6 +263,7 @@ "upload_area.title": "Drag & drop to upload", "upload_button.label": "Добави медия", "upload_form.description": "Describe for the visually impaired", + "upload_form.focus": "Crop", "upload_form.undo": "Отмяна", "upload_progress.label": "Uploading...", "video.close": "Close video", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 3eb0e3d26..bac807dbb 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -1,29 +1,33 @@ { - "account.block": "Bloquejar @{name}", - "account.block_domain": "Amagar tot de {domain}", + "account.block": "Bloca @{name}", + "account.block_domain": "Amaga-ho tot de {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "La informació següent pot reflectir incompleta el perfil de l'usuari.", - "account.edit_profile": "Editar perfil", - "account.follow": "Seguir", + "account.domain_blocked": "Domain hidden", + "account.edit_profile": "Edita el perfil", + "account.follow": "Segueix", "account.followers": "Seguidors", "account.follows": "Seguint", - "account.follows_you": "et segueix", + "account.follows_you": "Et segueix", "account.hide_reblogs": "Amaga els impulsos de @{name}", "account.media": "Media", "account.mention": "Esmentar @{name}", "account.moved_to": "{name} s'ha mogut a:", - "account.mute": "Silenciar @{name}", + "account.mute": "Silencia @{name}", "account.mute_notifications": "Notificacions desactivades de @{name}", - "account.posts": "Publicacions", + "account.muted": "Muted", + "account.posts": "Toots", + "account.posts_with_replies": "Toots amb respostes", "account.report": "Informe @{name}", "account.requested": "Esperant aprovació. Clic per a cancel·lar la petició de seguiment", - "account.share": "Compartir el perfil de @{name}", + "account.share": "Comparteix el perfil de @{name}", "account.show_reblogs": "Mostra els impulsos de @{name}", - "account.unblock": "Desbloquejar @{name}", + "account.unblock": "Desbloca @{name}", "account.unblock_domain": "Mostra {domain}", - "account.unfollow": "Deixar de seguir", + "account.unfollow": "Deixa de seguir", "account.unmute": "Treure silenci de @{name}", "account.unmute_notifications": "Activar notificacions de @{name}", - "account.view_full_profile": "Veure el perfil complet", + "account.view_full_profile": "Mostra 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.", "bundle_column_error.retry": "Torna-ho a provar", @@ -31,7 +35,7 @@ "bundle_modal_error.close": "Tanca", "bundle_modal_error.message": "S'ha produït un error en carregar aquest component.", "bundle_modal_error.retry": "Torna-ho a provar", - "column.blocks": "Usuaris bloquejats", + "column.blocks": "Usuaris blocats", "column.community": "Línia de temps local", "column.favourites": "Favorits", "column.follow_requests": "Peticions per seguir-te", @@ -45,46 +49,46 @@ "column_header.hide_settings": "Amaga la configuració", "column_header.moveLeft_settings": "Mou la columna cap a l'esquerra", "column_header.moveRight_settings": "Mou la columna cap a la dreta", - "column_header.pin": "Fixar", + "column_header.pin": "Fixa", "column_header.show_settings": "Mostra la configuració", - "column_header.unpin": "Deslligar", + "column_header.unpin": "No fixis", "column_subheading.navigation": "Navegació", "column_subheading.settings": "Configuració", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.hashtag_warning": "Aquest toot no es mostrarà en cap etiqueta ja que no està llistat. Només els toots públics poden ser cercats per etiqueta.", "compose_form.lock_disclaimer": "El teu compte no està bloquejat {locked}. Tothom pot seguir-te i veure els teus missatges a seguidors.", - "compose_form.lock_disclaimer.lock": "bloquejat", + "compose_form.lock_disclaimer.lock": "blocat", "compose_form.placeholder": "En què estàs pensant?", "compose_form.publish": "Toot", "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": "Escriu l'advertència aquí", - "confirmation_modal.cancel": "Cancel·lar", - "confirmations.block.confirm": "Bloquejar", - "confirmations.block.message": "Estàs segur que vols bloquejar {name}?", - "confirmations.delete.confirm": "Esborrar", - "confirmations.delete.message": "Estàs segur que vols esborrar aquest estat?", - "confirmations.delete_list.confirm": "Delete", - "confirmations.delete_list.message": "Estàs segur que vols esborrar permanenment aquesta llista?", - "confirmations.domain_block.confirm": "Amagar tot el domini", - "confirmations.domain_block.message": "Estàs realment, realment segur que vols bloquejar totalment {domain}? En la majoria dels casos bloquejar o silenciar és suficient i preferible.", - "confirmations.mute.confirm": "Silenciar", + "compose_form.sensitive": "Marca el contingut multimèdia com a sensible", + "compose_form.spoiler": "Amaga el text darrera darrere un avís", + "compose_form.spoiler_placeholder": "Escriu l'avís aquí", + "confirmation_modal.cancel": "Cancel·la", + "confirmations.block.confirm": "Bloca", + "confirmations.block.message": "Estàs segur que vols blocar {name}?", + "confirmations.delete.confirm": "Suprimeix", + "confirmations.delete.message": "Estàs segur que vols suprimir aquest estat?", + "confirmations.delete_list.confirm": "Suprimeix", + "confirmations.delete_list.message": "Estàs segur que vols suprimir permanentment aquesta llista?", + "confirmations.domain_block.confirm": "Amaga tot el domini", + "confirmations.domain_block.message": "Estàs realment, realment segur que vols blocar totalment {domain}? En la majoria dels casos blocar o silenciar uns pocs objectius és suficient i preferible.", + "confirmations.mute.confirm": "Silencia", "confirmations.mute.message": "Estàs segur que vols silenciar {name}?", - "confirmations.unfollow.confirm": "Deixar de seguir", + "confirmations.unfollow.confirm": "Deixa 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": "Aquí tenim quin aspecte tindrá:", "emoji_button.activity": "Activitat", "emoji_button.custom": "Personalitzat", - "emoji_button.flags": "Marques", - "emoji_button.food": "Menjar i Beure", - "emoji_button.label": "Inserir emoji", + "emoji_button.flags": "Banderes", + "emoji_button.food": "Menjar i beure", + "emoji_button.label": "Insereix un emoji", "emoji_button.nature": "Natura", "emoji_button.not_found": "Emojos no!! (╯°□°)╯︵ ┻━┻", "emoji_button.objects": "Objectes", "emoji_button.people": "Gent", - "emoji_button.recent": "Freqüentment utilitzat", - "emoji_button.search": "Cercar...", + "emoji_button.recent": "Usats freqüentment", + "emoji_button.search": "Cerca...", "emoji_button.search_results": "Resultats de la cerca", "emoji_button.symbols": "Símbols", "emoji_button.travel": "Viatges i Llocs", @@ -100,7 +104,7 @@ "getting_started.appsshort": "Aplicacions", "getting_started.faq": "PMF", "getting_started.heading": "Començant", - "getting_started.open_source_notice": "Mastodon és un programari de codi obert. Pots contribuir o informar de problemes a GitHub de {github}.", + "getting_started.open_source_notice": "Mastodon és un programari de codi obert. Pots contribuir o informar de problemes a GitHub a {github}.", "getting_started.userguide": "Guia de l'usuari", "home.column_settings.advanced": "Avançat", "home.column_settings.basic": "Bàsic", @@ -114,7 +118,7 @@ "keyboard_shortcuts.compose": "per centrar l'area de composició de text", "keyboard_shortcuts.description": "Description", "keyboard_shortcuts.down": "per baixar en la llista", - "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.enter": "ampliar estat", "keyboard_shortcuts.favourite": "afavorir", "keyboard_shortcuts.heading": "Keyboard Shortcuts", "keyboard_shortcuts.hotkey": "Tecla d'accés directe", @@ -133,12 +137,13 @@ "lists.delete": "Delete list", "lists.edit": "Editar llista", "lists.new.create": "Afegir llista", - "lists.new.title_placeholder": "Nou títol de llista", + "lists.new.title_placeholder": "Nova llista", "lists.search": "Cercar entre les persones que segueixes", "lists.subheading": "Les teves llistes", "loading_indicator.label": "Carregant...", "media_gallery.toggle_visible": "Alternar visibilitat", "missing_indicator.label": "No trobat", + "missing_indicator.sublabel": "Aquest recurs no pot ser trobat", "mute_modal.hide_notifications": "Amagar notificacions d'aquest usuari?", "navigation_bar.blocks": "Usuaris bloquejats", "navigation_bar.community_timeline": "Línia de temps Local", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "La línia de temps d'Inici mostra missatges de les persones que segueixes.", "onboarding.page_four.notifications": "La columna Notificacions mostra quan algú interactua amb tu.", "onboarding.page_one.federation": "Mastodon és una xarxa de servidors independents que s'uneixen per fer una més gran xarxa social. A aquests servidors els hi diem instàncies.", - "onboarding.page_one.handle": "Ets a {domain}, per tant el teu usuari complert és {handle}", + "onboarding.page_one.full_handle": "El teu usuari complet", + "onboarding.page_one.handle_hint": "Això és el que els hi diries als teus amics que cerquin.", "onboarding.page_one.welcome": "Benvingut a Mastodon!", "onboarding.page_six.admin": "L'administrador de la teva instància és {admin}.", "onboarding.page_six.almost_done": "Quasi fet...", @@ -197,21 +203,30 @@ "privacy.public.short": "Públic", "privacy.unlisted.long": "No publicar en línies de temps públiques", "privacy.unlisted.short": "No llistat", + "regeneration_indicator.label": "Carregant…", + "regeneration_indicator.sublabel": "S'està preparant la línia de temps Inici!", "relative_time.days": "fa {number} dies", "relative_time.hours": "fa {number} hores", "relative_time.just_now": "ara", "relative_time.minutes": "fa {number} minuts", "relative_time.seconds": "fa {number} segons", "reply_indicator.cancel": "Cancel·lar", + "report.forward": "Reenvia a {target}", + "report.forward_hint": "Aquest compte és d'un altre servidor. Enviar-hi també una copia anònima del informe?", + "report.hint": "El informe s'enviarà als moderadors de la teva instància. Pots explicar perquè vols informar d'aquest compte aquí:", "report.placeholder": "Comentaris addicionals", "report.submit": "Enviar", "report.target": "Informes", "search.placeholder": "Cercar", "search_popout.search_format": "Format de cerca avançada", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "etiqueta", "search_popout.tips.status": "status", "search_popout.tips.text": "El text simple retorna coincidències amb els noms de visualització, els noms d'usuari i els hashtags", "search_popout.tips.user": "usuari", + "search_results.accounts": "Gent", + "search_results.hashtags": "Etiquetes", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, un {result} altres {results}}", "standalone.public_title": "Una mirada a l'interior ...", "status.block": "Block @{name}", @@ -223,10 +238,11 @@ "status.media_hidden": "Multimèdia amagat", "status.mention": "Esmentar @{name}", "status.more": "Més", - "status.mute": "Mute @{name}", + "status.mute": "Silenciar @{name}", "status.mute_conversation": "Silenciar conversació", "status.open": "Ampliar aquest estat", "status.pin": "Fixat en el perfil", + "status.pinned": "Pinned toot", "status.reblog": "Impuls", "status.reblogged_by": "{name} ha retootejat", "status.reply": "Respondre", @@ -239,7 +255,6 @@ "status.show_more": "Mostra més", "status.unmute_conversation": "Activar conversació", "status.unpin": "Deslliga del perfil", - "tabs_bar.compose": "Compondre", "tabs_bar.federated_timeline": "Federada", "tabs_bar.home": "Inici", "tabs_bar.local_timeline": "Local", @@ -248,6 +263,7 @@ "upload_area.title": "Arrossega i deixa anar per carregar", "upload_button.label": "Afegir multimèdia", "upload_form.description": "Descriure els problemes visuals", + "upload_form.focus": "Retallar", "upload_form.undo": "Desfer", "upload_progress.label": "Pujant...", "video.close": "Tancar el vídeo", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 9b6c857e4..6267b1d71 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -1,7 +1,9 @@ { "account.block": "@{name} blocken", "account.block_domain": "Alles von {domain} verstecken", + "account.blocked": "Blockiert", "account.disclaimer_full": "Das Profil wird möglicherweise unvollständig wiedergegeben.", + "account.domain_blocked": "Domain versteckt", "account.edit_profile": "Profil bearbeiten", "account.follow": "Folgen", "account.followers": "Folgende", @@ -13,7 +15,9 @@ "account.moved_to": "{name} ist umgezogen auf:", "account.mute": "@{name} stummschalten", "account.mute_notifications": "Benachrichtigungen von @{name} verbergen", + "account.muted": "Stummgeschaltet", "account.posts": "Beiträge", + "account.posts_with_replies": "Beiträge mit Antworten", "account.report": "@{name} melden", "account.requested": "Warte auf Erlaubnis. Klicke zum Abbrechen", "account.share": "Profil von @{name} teilen", @@ -36,7 +40,7 @@ "column.favourites": "Favoriten", "column.follow_requests": "Folgeanfragen", "column.home": "Startseite", - "column.lists": "Lists", + "column.lists": "Listen", "column.mutes": "Stummgeschaltete Profile", "column.notifications": "Mitteilungen", "column.pins": "Angeheftete Beiträge", @@ -50,7 +54,7 @@ "column_header.unpin": "Lösen", "column_subheading.navigation": "Navigation", "column_subheading.settings": "Einstellungen", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.hashtag_warning": "Dieser Beitrag wird nicht unter einen dieser Hashtags sichtbar sein, solange er ungelistet ist. Bei einer Suche kann er nicht gefunden werden.", "compose_form.lock_disclaimer": "Dein Profil ist nicht {locked}. Wer dir folgen will, kann das jederzeit tun und dann auch deine privaten Beiträge sehen.", "compose_form.lock_disclaimer.lock": "gesperrt", "compose_form.placeholder": "Worüber möchtest du schreiben?", @@ -65,7 +69,7 @@ "confirmations.delete.confirm": "Löschen", "confirmations.delete.message": "Bist du dir sicher, dass du diesen Beitrag löschen möchtest?", "confirmations.delete_list.confirm": "Delete", - "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", + "confirmations.delete_list.message": "Bist du dir sicher, dass du diese Liste permanent löschen möchtest?", "confirmations.domain_block.confirm": "Die ganze Domain verbergen", "confirmations.domain_block.message": "Bist du dir wirklich sicher, dass du die ganze Domain {domain} verbergen willst? In den meisten Fällen reichen ein paar gezielte Blocks aus.", "confirmations.mute.confirm": "Stummschalten", @@ -92,7 +96,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": "Diese Liste ist derzeit leer.", + "empty_column.list": "Diese Liste ist derzeit leer. Wenn Wesen auf dieser Liste neue Beiträge veröffentlichen werden sie hier erscheinen.", "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", @@ -111,34 +115,35 @@ "keyboard_shortcuts.back": "zurück navigieren", "keyboard_shortcuts.boost": "boosten", "keyboard_shortcuts.column": "einen Status in einer der Spalten fokussieren", - "keyboard_shortcuts.compose": "to focus the compose textarea", - "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.compose": "um das Textfeld zu fokussieren", + "keyboard_shortcuts.description": "Beschreibung", "keyboard_shortcuts.down": "sich in der Liste hinunter bewegen", - "keyboard_shortcuts.enter": "to open status", - "keyboard_shortcuts.favourite": "favorisieren", - "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.enter": "um den Status zu öffnen", + "keyboard_shortcuts.favourite": "um zu favorisieren", + "keyboard_shortcuts.heading": "Tastenkombinationen", "keyboard_shortcuts.hotkey": "Hotkey", - "keyboard_shortcuts.legend": "diese Übersicht anzeigen", - "keyboard_shortcuts.mention": "Autor_in erwähnen", - "keyboard_shortcuts.reply": "antworten", - "keyboard_shortcuts.search": "die Suche fokussieren", - "keyboard_shortcuts.toot": "einen neuen Toot beginnen", - "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.legend": "um diese Übersicht anzuzeigen", + "keyboard_shortcuts.mention": "um Autor_in zu erwähnen", + "keyboard_shortcuts.reply": "um zu antworten", + "keyboard_shortcuts.search": "um die Suche zu fokussieren", + "keyboard_shortcuts.toot": "um einen neuen Toot zu beginnen", + "keyboard_shortcuts.unfocus": "um das Textfeld/die Suche nicht mehr zu fokussieren", "keyboard_shortcuts.up": "sich in der Liste hinauf bewegen", "lightbox.close": "Schließen", "lightbox.next": "Weiter", "lightbox.previous": "Zurück", - "lists.account.add": "Add to list", - "lists.account.remove": "Remove from list", + "lists.account.add": "Zur Liste hinzufügen", + "lists.account.remove": "Von der Liste entfernen", "lists.delete": "Delete list", - "lists.edit": "Edit list", - "lists.new.create": "Add list", - "lists.new.title_placeholder": "New list title", - "lists.search": "Search among people you follow", - "lists.subheading": "Your lists", + "lists.edit": "Liste bearbeiten", + "lists.new.create": "Liste hinzufügen", + "lists.new.title_placeholder": "Neuer Titel der Liste", + "lists.search": "Suche nach Leuten denen du folgst", + "lists.subheading": "Deine Listen", "loading_indicator.label": "Wird geladen …", "media_gallery.toggle_visible": "Sichtbarkeit umschalten", "missing_indicator.label": "Nicht gefunden", + "missing_indicator.sublabel": "Die Ressource konnte nicht gefunden werden", "mute_modal.hide_notifications": "Benachrichtigungen von diesem Account verbergen?", "navigation_bar.blocks": "Blockierte Profile", "navigation_bar.community_timeline": "Lokale Zeitleiste", @@ -147,7 +152,7 @@ "navigation_bar.follow_requests": "Folgeanfragen", "navigation_bar.info": "Über diese Instanz", "navigation_bar.keyboard_shortcuts": "Tastenkombinationen", - "navigation_bar.lists": "Lists", + "navigation_bar.lists": "Listen", "navigation_bar.logout": "Abmelden", "navigation_bar.mutes": "Stummgeschaltete Profile", "navigation_bar.pins": "Angeheftete Beiträge", @@ -170,11 +175,12 @@ "notifications.column_settings.sound": "Ton abspielen", "onboarding.done": "Fertig", "onboarding.next": "Weiter", - "onboarding.page_five.public_timelines": "Die lokale Zeitleiste zeigt alle Beiträge von Leuten, die auch auf {domain} sind. Das gesamte bekannte Netz zeigt Beiträge von allen, denen von Leuten auf {domain} gefolgt wird. Zusammen sind sie die öffentlichen Zeitleisten. In ihnen kannst du viel Neues entdecken!", + "onboarding.page_five.public_timelines": "Die lokale Zeitleiste zeigt alle Beiträge von Leuten, die auch auf {domain} sind. Das gesamte bekannte Netz zeigt Beiträge von allen, denen von Leuten auf {domain} gefolgt wird. Zusammen sind sie die öffentlichen Zeitleisten, ein guter Weg, um neue Leute zu finden.", "onboarding.page_four.home": "Die Startseite zeigt dir Beiträge von Leuten, denen du folgst.", "onboarding.page_four.notifications": "Wenn jemand mit dir interagiert, bekommst du eine Mitteilung.", "onboarding.page_one.federation": "Mastodon ist ein soziales Netzwerk, das aus unabhängigen Servern besteht. Diese Server nennen wir auch Instanzen.", - "onboarding.page_one.handle": "Du bist auf der Instanz {domain}, also ist dein vollständiger Profilname im Netzwerk {handle}", + "onboarding.page_one.full_handle": "Your full handle", + "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.", "onboarding.page_one.welcome": "Willkommen bei Mastodon!", "onboarding.page_six.admin": "Für deine Instanz ist {admin} zuständig.", "onboarding.page_six.almost_done": "Fast fertig …", @@ -197,21 +203,30 @@ "privacy.public.short": "Öffentlich", "privacy.unlisted.long": "Nicht in öffentlichen Zeitleisten anzeigen", "privacy.unlisted.short": "Nicht gelistet", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", "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": "Abbrechen", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Zusätzliche Kommentare", "report.submit": "Absenden", "report.target": "{target} melden", "search.placeholder": "Suche", "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "status", "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {Ergebnis} other {Ergebnisse}}", "standalone.public_title": "Ein kleiner Einblick …", "status.block": "Block @{name}", @@ -227,6 +242,7 @@ "status.mute_conversation": "Thread stummschalten", "status.open": "Diesen Beitrag öffnen", "status.pin": "Im Profil anheften", + "status.pinned": "Pinned toot", "status.reblog": "Teilen", "status.reblogged_by": "{name} teilte", "status.reply": "Antworten", @@ -239,7 +255,6 @@ "status.show_more": "Mehr anzeigen", "status.unmute_conversation": "Stummschaltung von Thread aufheben", "status.unpin": "Vom Profil lösen", - "tabs_bar.compose": "Schreiben", "tabs_bar.federated_timeline": "Föderation", "tabs_bar.home": "Startseite", "tabs_bar.local_timeline": "Lokal", @@ -248,6 +263,7 @@ "upload_area.title": "Zum Hochladen hereinziehen", "upload_button.label": "Mediendatei hinzufügen", "upload_form.description": "Für Menschen mit Sehbehinderung beschreiben", + "upload_form.focus": "Crop", "upload_form.undo": "Entfernen", "upload_progress.label": "Wird hochgeladen …", "video.close": "Video schließen", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index acf051de5..c8ebf4b87 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -136,6 +136,10 @@ { "defaultMessage": "Not found", "id": "missing_indicator.label" + }, + { + "defaultMessage": "This resource could not be found", + "id": "missing_indicator.sublabel" } ], "path": "app/javascript/mastodon/components/missing_indicator.json" @@ -258,6 +262,23 @@ { "descriptors": [ { + "defaultMessage": "Loading…", + "id": "regeneration_indicator.label" + }, + { + "defaultMessage": "Your home feed is being prepared!", + "id": "regeneration_indicator.sublabel" + } + ], + "path": "app/javascript/mastodon/components/status_list.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Pinned toot", + "id": "status.pinned" + }, + { "defaultMessage": "{name} boosted", "id": "status.reblogged_by" } @@ -301,11 +322,19 @@ { "descriptors": [ { + "defaultMessage": "Toots", + "id": "account.posts" + }, + { + "defaultMessage": "Toots with replies", + "id": "account.posts_with_replies" + }, + { "defaultMessage": "Media", "id": "account.media" } ], - "path": "app/javascript/mastodon/features/account_gallery/index.json" + "path": "app/javascript/mastodon/features/account_timeline/components/header.json" }, { "descriptors": [ @@ -416,7 +445,7 @@ "id": "account.view_full_profile" }, { - "defaultMessage": "Posts", + "defaultMessage": "Toots", "id": "account.posts" }, { @@ -445,8 +474,24 @@ "id": "account.requested" }, { + "defaultMessage": "Unblock @{name}", + "id": "account.unblock" + }, + { "defaultMessage": "Follows you", "id": "account.follows_you" + }, + { + "defaultMessage": "Blocked", + "id": "account.blocked" + }, + { + "defaultMessage": "Muted", + "id": "account.muted" + }, + { + "defaultMessage": "Domain hidden", + "id": "account.domain_blocked" } ], "path": "app/javascript/mastodon/features/account/components/header.json" @@ -634,6 +679,18 @@ { "descriptors": [ { + "defaultMessage": "People", + "id": "search_results.accounts" + }, + { + "defaultMessage": "Toots", + "id": "search_results.statuses" + }, + { + "defaultMessage": "Hashtags", + "id": "search_results.hashtags" + }, + { "defaultMessage": "{count, number} {count, plural, one {result} other {results}}", "id": "search_results.total" } @@ -647,6 +704,14 @@ "id": "search.placeholder" }, { + "defaultMessage": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "id": "search_popout.tips.full_text" + }, + { + "defaultMessage": "Simple text returns matching display names, usernames and hashtags", + "id": "search_popout.tips.text" + }, + { "defaultMessage": "Advanced search format", "id": "search_popout.search_format" }, @@ -661,10 +726,6 @@ { "defaultMessage": "status", "id": "search_popout.tips.status" - }, - { - "defaultMessage": "Simple text returns matching display names, usernames and hashtags", - "id": "search_popout.tips.text" } ], "path": "app/javascript/mastodon/features/compose/components/search.json" @@ -690,12 +751,16 @@ { "descriptors": [ { + "defaultMessage": "Describe for the visually impaired", + "id": "upload_form.description" + }, + { "defaultMessage": "Undo", "id": "upload_form.undo" }, { - "defaultMessage": "Describe for the visually impaired", - "id": "upload_form.description" + "defaultMessage": "Crop", + "id": "upload_form.focus" } ], "path": "app/javascript/mastodon/features/compose/components/upload.json" @@ -1220,6 +1285,15 @@ "id": "standalone.public_title" } ], + "path": "app/javascript/mastodon/features/standalone/community_timeline/index.json" + }, + { + "descriptors": [ + { + "defaultMessage": "A look inside...", + "id": "standalone.public_title" + } + ], "path": "app/javascript/mastodon/features/standalone/public_timeline/index.json" }, { @@ -1459,8 +1533,12 @@ "id": "onboarding.page_one.federation" }, { - "defaultMessage": "You are on {domain}, so your full handle is {handle}", - "id": "onboarding.page_one.handle" + "defaultMessage": "Your full handle", + "id": "onboarding.page_one.full_handle" + }, + { + "defaultMessage": "This is what you would tell your friends to search for.", + "id": "onboarding.page_one.handle_hint" }, { "defaultMessage": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", @@ -1536,6 +1614,10 @@ { "descriptors": [ { + "defaultMessage": "Close", + "id": "lightbox.close" + }, + { "defaultMessage": "Additional comments", "id": "report.placeholder" }, @@ -1546,6 +1628,18 @@ { "defaultMessage": "Report {target}", "id": "report.target" + }, + { + "defaultMessage": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", + "id": "report.hint" + }, + { + "defaultMessage": "The account is from another server. Send an anonymized copy of the report there as well?", + "id": "report.forward_hint" + }, + { + "defaultMessage": "Forward to {target}", + "id": "report.forward" } ], "path": "app/javascript/mastodon/features/ui/components/report_modal.json" @@ -1553,10 +1647,6 @@ { "descriptors": [ { - "defaultMessage": "Compose", - "id": "tabs_bar.compose" - }, - { "defaultMessage": "Home", "id": "tabs_bar.home" }, @@ -1642,4 +1732,4 @@ ], "path": "app/javascript/mastodon/features/video/index.json" } -] +] \ No newline at end of file diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index ac25a57a2..e6e0b012b 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -1,7 +1,9 @@ { "account.block": "Block @{name}", "account.block_domain": "Hide everything from {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Edit profile", "account.follow": "Follow", "account.followers": "Followers", @@ -13,7 +15,9 @@ "account.moved_to": "{name} has moved to:", "account.mute": "Mute @{name}", "account.mute_notifications": "Mute notifications from @{name}", - "account.posts": "Posts", + "account.muted": "Muted", + "account.posts": "Toots", + "account.posts_with_replies": "Toots with replies", "account.report": "Report @{name}", "account.requested": "Awaiting approval. Click to cancel follow request", "account.share": "Share @{name}'s profile", @@ -144,6 +148,7 @@ "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", + "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Blocked users", "navigation_bar.community_timeline": "Local timeline", @@ -205,21 +210,30 @@ "privacy.public.short": "Public", "privacy.unlisted.long": "Do not post to public timelines", "privacy.unlisted.short": "Unlisted", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", "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": "Cancel", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting {target}", "search.placeholder": "Search", "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "status", "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "standalone.public_title": "A look inside...", "status.block": "Block @{name}", @@ -235,6 +249,7 @@ "status.mute_conversation": "Mute conversation", "status.open": "Expand this status", "status.pin": "Pin on profile", + "status.pinned": "Pinned toot", "status.reblog": "Boost", "status.reblogged_by": "{name} boosted", "status.reply": "Reply", @@ -247,7 +262,6 @@ "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", "status.unpin": "Unpin from profile", - "tabs_bar.compose": "Compose", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Home", "tabs_bar.local_timeline": "Local", @@ -256,6 +270,7 @@ "upload_area.title": "Drag & drop to upload", "upload_button.label": "Add media", "upload_form.description": "Describe for the visually impaired", + "upload_form.focus": "Crop", "upload_form.undo": "Undo", "upload_progress.label": "Uploading...", "video.close": "Close video", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index eab8c09a6..4f465bf40 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -1,232 +1,248 @@ { "account.block": "Bloki @{name}", - "account.block_domain": "Kaŝi ĉion el {domain}", - "account.disclaimer_full": "La ĉi-subaj informoj povas ne plene reflekti la profilon de la uzanto.", - "account.edit_profile": "Redakti la profilon", + "account.block_domain": "Kaŝi ĉion de {domain}", + "account.blocked": "Blokita", + "account.disclaimer_full": "Subaj informoj povas reflekti la profilon de la uzanto nekomplete.", + "account.domain_blocked": "Domajno kaŝita", + "account.edit_profile": "Redakti profilon", "account.follow": "Sekvi", "account.followers": "Sekvantoj", "account.follows": "Sekvatoj", "account.follows_you": "Sekvas vin", - "account.hide_reblogs": "Hide boosts from @{name}", - "account.media": "Sonbildaĵoj", + "account.hide_reblogs": "Kaŝi diskonigojn de @{name}", + "account.media": "Aŭdovidaĵoj", "account.mention": "Mencii @{name}", - "account.moved_to": "{name} has moved to:", + "account.moved_to": "{name} moviĝis al:", "account.mute": "Silentigi @{name}", - "account.mute_notifications": "Mute notifications from @{name}", + "account.mute_notifications": "Silentigi sciigojn el @{name}", + "account.muted": "Silentigita", "account.posts": "Mesaĝoj", + "account.posts_with_replies": "Mesaĝoj kun respondoj", "account.report": "Signali @{name}", - "account.requested": "Atendas aprobon", + "account.requested": "Atendo de aprobo. Alklaku por nuligi peton de sekvado", "account.share": "Diskonigi la profilon de @{name}", - "account.show_reblogs": "Show boosts from @{name}", + "account.show_reblogs": "Montri diskonigojn de @{name}", "account.unblock": "Malbloki @{name}", "account.unblock_domain": "Malkaŝi {domain}", - "account.unfollow": "Ne plus sekvi", + "account.unfollow": "Ne plu sekvi", "account.unmute": "Malsilentigi @{name}", - "account.unmute_notifications": "Unmute notifications from @{name}", + "account.unmute_notifications": "Malsilentigi sciigojn de @{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.", + "boost_modal.combo": "Vi povas premi {combo} por preterpasi sekvafoje", + "bundle_column_error.body": "Io misfunkciis en la ŝargado de ĉi tiu elemento.", "bundle_column_error.retry": "Bonvolu reprovi", "bundle_column_error.title": "Reta eraro", "bundle_modal_error.close": "Fermi", - "bundle_modal_error.message": "Io malfunkciis ŝargante tiun ĉi komponanton.", + "bundle_modal_error.message": "Io misfunkciis en la ŝargado de ĉi tiu elemento.", "bundle_modal_error.retry": "Bonvolu reprovi", "column.blocks": "Blokitaj uzantoj", "column.community": "Loka tempolinio", - "column.favourites": "Favoritoj", - "column.follow_requests": "Abonpetoj", + "column.favourites": "Stelumoj", + "column.follow_requests": "Petoj de sekvado", "column.home": "Hejmo", - "column.lists": "Lists", + "column.lists": "Listoj", "column.mutes": "Silentigitaj uzantoj", "column.notifications": "Sciigoj", - "column.pins": "Alpinglitaj pepoj", + "column.pins": "Alpinglitaj mesaĝoj", "column.public": "Fratara tempolinio", "column_back_button.label": "Reveni", "column_header.hide_settings": "Kaŝi agordojn", "column_header.moveLeft_settings": "Movi kolumnon maldekstren", "column_header.moveRight_settings": "Movi kolumnon dekstren", "column_header.pin": "Alpingli", - "column_header.show_settings": "Malkaŝi agordojn", + "column_header.show_settings": "Montri agordojn", "column_header.unpin": "Depingli", "column_subheading.navigation": "Navigado", - "column_subheading.settings": "Agordoj", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", - "compose_form.lock_disclaimer": "Via konta ne estas ŝlosita. Iu ajn povas sekvi vin por vidi viajn privatajn pepojn.", + "column_subheading.settings": "Agordado", + "compose_form.hashtag_warning": "Ĉi tiu mesaĝo ne estos listigita per ajna kradvorto. Nur publikaj mesaĝoj estas serĉeblaj per kradvortoj.", + "compose_form.lock_disclaimer": "Via konta ne estas {locked}. Iu ajn povas sekvi vin por vidi viajn mesaĝojn nur por sekvantoj.", "compose_form.lock_disclaimer.lock": "ŝlosita", "compose_form.placeholder": "Pri kio vi pensas?", "compose_form.publish": "Hup", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive": "Marki ke la enhavo estas tikla", - "compose_form.spoiler": "Kaŝi la tekston malantaŭ averto", - "compose_form.spoiler_placeholder": "Skribu tie vian averton", - "confirmation_modal.cancel": "Malfari", + "compose_form.sensitive": "Marki aŭdovidaĵon tikla", + "compose_form.spoiler": "Kaŝi tekston malantaŭ averto", + "compose_form.spoiler_placeholder": "Skribu vian averton ĉi tie", + "confirmation_modal.cancel": "Nuligi", "confirmations.block.confirm": "Bloki", - "confirmations.block.message": "Ĉu vi konfirmas la blokadon de {name}?", - "confirmations.delete.confirm": "Malaperigi", - "confirmations.delete.message": "Ĉu vi konfirmas la malaperigon de tiun pepon?", - "confirmations.delete_list.confirm": "Delete", - "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", - "confirmations.domain_block.confirm": "Kaŝi la tutan reton", - "confirmations.domain_block.message": "Ĉu vi vere, vere certas, ke vi volas bloki {domain} tute? Plej ofte, kelkaj celitaj blokadoj aŭ silentigoj estas sufiĉaj kaj preferindaj.", + "confirmations.block.message": "Ĉu vi certas, ke vi volas bloki {name}?", + "confirmations.delete.confirm": "Forigi", + "confirmations.delete.message": "Ĉu vi certas, ke vi volas forigi ĉi tiun mesaĝon?", + "confirmations.delete_list.confirm": "Forigi", + "confirmations.delete_list.message": "Ĉu vi certas, ke vi volas porĉiame forigi ĉi tiun liston?", + "confirmations.domain_block.confirm": "Kaŝi la tutan domajnon", + "confirmations.domain_block.message": "Ĉu vi vere, vere certas, ke vi volas tute bloki {domain}? Plej ofte, trafa blokado kaj silentigado sufiĉas kaj preferindas.", "confirmations.mute.confirm": "Silentigi", - "confirmations.mute.message": "Ĉu vi konfirmas la silentigon de {name}?", + "confirmations.mute.message": "Ĉu vi certas, ke vi volas silentigi {name}?", "confirmations.unfollow.confirm": "Ne plu sekvi", - "confirmations.unfollow.message": "Ĉu vi volas ĉesi sekvi {name}?", - "embed.instructions": "Enmetu tiun statkonigon ĉe vian retejon kopiante la ĉi-suban kodon.", + "confirmations.unfollow.message": "Ĉu vi certas, ke vi volas ĉesi sekvi {name}?", + "embed.instructions": "Enkorpigu ĉi tiun mesaĝon en vian retejon per kopio de la suba kodo.", "embed.preview": "Ĝi aperos tiel:", - "emoji_button.activity": "Aktivecoj", - "emoji_button.custom": "Personaj", + "emoji_button.activity": "Agadoj", + "emoji_button.custom": "Propraj", "emoji_button.flags": "Flagoj", "emoji_button.food": "Manĝi kaj trinki", - "emoji_button.label": "Enmeti mieneton", + "emoji_button.label": "Enmeti emoĝion", "emoji_button.nature": "Naturo", - "emoji_button.not_found": "Neniuj mienetoj!! (╯°□°)╯︵ ┻━┻", - "emoji_button.objects": "Objektoj", + "emoji_button.not_found": "Neniu emoĝio!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Aĵoj", "emoji_button.people": "Homoj", "emoji_button.recent": "Ofte uzataj", "emoji_button.search": "Serĉo…", - "emoji_button.search_results": "Rezultatoj de serĉo", + "emoji_button.search_results": "Serĉaj rezultoj", "emoji_button.symbols": "Simboloj", - "emoji_button.travel": "Vojaĝoj & lokoj", + "emoji_button.travel": "Vojaĝoj kaj lokoj", "empty_column.community": "La loka tempolinio estas malplena. Skribu ion por plenigi ĝin!", - "empty_column.hashtag": "Ĝise, neniu enhavo estas asociita kun tiu kradvorto.", + "empty_column.hashtag": "Ankoraŭ estas nenio per ĉi 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", + "empty_column.list": "Ankoraŭ estas nenio en ĉi tiu listo. Kiam membroj de ĉi tiu listo afiŝos novajn mesaĝojn, ili aperos ĉi tie.", + "empty_column.notifications": "Vi ankoraŭ ne havas sciigojn. Interagu kun aliaj por komenci konversacion.", + "empty_column.public": "Estas nenio ĉi tie! Publike skribu ion, aŭ mane sekvu uzantojn de aliaj nodoj por plenigi la publikan tempolinion", + "follow_request.authorize": "Rajtigi", "follow_request.reject": "Rifuzi", "getting_started.appsshort": "Aplikaĵoj", "getting_started.faq": "Oftaj demandoj", "getting_started.heading": "Por komenci", - "getting_started.open_source_notice": "Mastodono estas malfermkoda programo. Vi povas kontribui aŭ raporti problemojn en GitHub je {github}.", + "getting_started.open_source_notice": "Mastodon estas malfermitkoda programo. Vi povas kontribui aŭ raporti problemojn en GitHub je {github}.", "getting_started.userguide": "Gvidilo de uzo", "home.column_settings.advanced": "Precizaj agordoj", "home.column_settings.basic": "Bazaj agordoj", - "home.column_settings.filter_regex": "Forfiltri per regulesprimo", + "home.column_settings.filter_regex": "Filtri per regulesprimoj", "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", + "home.settings": "Kolumnaj agordoj", + "keyboard_shortcuts.back": "por reveni", + "keyboard_shortcuts.boost": "por diskonigi", + "keyboard_shortcuts.column": "por fokusigi mesaĝon en unu el la kolumnoj", + "keyboard_shortcuts.compose": "por fokusigi la tekstujon", + "keyboard_shortcuts.description": "Priskribo", + "keyboard_shortcuts.down": "por iri suben en la listo", + "keyboard_shortcuts.enter": "por malfermi mesaĝon", + "keyboard_shortcuts.favourite": "por stelumi", + "keyboard_shortcuts.heading": "Klavaraj mallongigoj", + "keyboard_shortcuts.hotkey": "Rapidklavo", + "keyboard_shortcuts.legend": "por montri ĉi tiun noton", + "keyboard_shortcuts.mention": "por mencii la aŭtoron", + "keyboard_shortcuts.reply": "por respondi", + "keyboard_shortcuts.search": "por fokusigi la serĉilon", + "keyboard_shortcuts.toot": "por komenci tute novan mesaĝon", + "keyboard_shortcuts.unfocus": "por malfokusigi la tekstujon aŭ la serĉilon", + "keyboard_shortcuts.up": "por iri supren en la listo", "lightbox.close": "Fermi", - "lightbox.next": "Malantaŭa", + "lightbox.next": "Sekva", "lightbox.previous": "Antaŭa", - "lists.account.add": "Add to list", - "lists.account.remove": "Remove from list", - "lists.delete": "Delete list", - "lists.edit": "Edit list", - "lists.new.create": "Add list", - "lists.new.title_placeholder": "New list title", - "lists.search": "Search among people you follow", - "lists.subheading": "Your lists", - "loading_indicator.label": "Ŝarganta…", - "media_gallery.toggle_visible": "Baskuli videblecon", + "lists.account.add": "Aldoni al la listo", + "lists.account.remove": "Forigi de la listo", + "lists.delete": "Forigi la liston", + "lists.edit": "Redakti la liston", + "lists.new.create": "Aldoni liston", + "lists.new.title_placeholder": "Titolo de la nova listo", + "lists.search": "Serĉi inter la homoj, kiujn vi sekvas", + "lists.subheading": "Viaj listoj", + "loading_indicator.label": "Ŝargado…", + "media_gallery.toggle_visible": "Baskuligi videblecon", "missing_indicator.label": "Ne trovita", - "mute_modal.hide_notifications": "Hide notifications from this user?", + "missing_indicator.sublabel": "Ĉi tiu rimedo ne estis trovita", + "mute_modal.hide_notifications": "Ĉu kaŝi sciigojn el ĉi tiu uzanto?", "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.lists": "Lists", + "navigation_bar.edit_profile": "Redakti profilon", + "navigation_bar.favourites": "Stelumoj", + "navigation_bar.follow_requests": "Petoj de sekvado", + "navigation_bar.info": "Pri ĉi tiu nodo", + "navigation_bar.keyboard_shortcuts": "Klavaraj mallongigoj", + "navigation_bar.lists": "Listoj", "navigation_bar.logout": "Elsaluti", "navigation_bar.mutes": "Silentigitaj uzantoj", - "navigation_bar.pins": "Alpinglitaj pepoj", + "navigation_bar.pins": "Alpinglitaj mesaĝoj", "navigation_bar.preferences": "Preferoj", "navigation_bar.public_timeline": "Fratara tempolinio", - "notification.favourite": "{name} favoris vian mesaĝon", - "notification.follow": "{name} sekvis vin", + "notification.favourite": "{name} stelumis vian mesaĝon", + "notification.follow": "{name} eksekvis vin", "notification.mention": "{name} menciis vin", "notification.reblog": "{name} diskonigis vian mesaĝon", - "notifications.clear": "Forviŝi la sciigojn", - "notifications.clear_confirmation": "Ĉu vi certe volas malaperigi ĉiujn viajn sciigojn?", - "notifications.column_settings.alert": "Retumilaj atentigoj", - "notifications.column_settings.favourite": "Favoritoj:", + "notifications.clear": "Forviŝi sciigojn", + "notifications.clear_confirmation": "Ĉu vi certas, ke vi volas porĉiame forviŝi ĉiujn viajn sciigojn?", + "notifications.column_settings.alert": "Retumilaj sciigoj", + "notifications.column_settings.favourite": "Stelumoj:", "notifications.column_settings.follow": "Novaj sekvantoj:", "notifications.column_settings.mention": "Mencioj:", "notifications.column_settings.push": "Puŝsciigoj", - "notifications.column_settings.push_meta": "Tiu ĉi aparato", + "notifications.column_settings.push_meta": "Ĉi tiu aparato", "notifications.column_settings.reblog": "Diskonigoj:", - "notifications.column_settings.show": "Montri en kolono", + "notifications.column_settings.show": "Montri en kolumno", "notifications.column_settings.sound": "Eligi sonon", "onboarding.done": "Farita", - "onboarding.next": "Malantaŭa", - "onboarding.page_five.public_timelines": "La loka tempolinio enhavas mesaĝojn de ĉiuj ĉe {domain}. La federacia tempolinio enhavas ĉiujn mesaĝojn de uzantoj, kiujn iu ĉe {domain} sekvas. Ambaŭ tre utilas por trovi novajn kunparolantojn.", - "onboarding.page_four.home": "La hejma tempolinio enhavas la mesaĝojn de ĉiuj uzantoj, kiuj vi sekvas.", - "onboarding.page_four.notifications": "La sciiga kolumno informas vin kiam iu interagas kun vi.", - "onboarding.page_one.federation": "Mastodono estas reto de nedependaj serviloj, unuiĝintaj por krei pligrandan socian retejon. Ni nomas tiujn servilojn instancoj.", - "onboarding.page_one.handle": "Vi estas ĉe {domain}, unu el la multaj instancoj de Mastodono. Via kompleta uznomo do estas {handle}", - "onboarding.page_one.welcome": "Bonvenon al Mastodono!", - "onboarding.page_six.admin": "Via instancestro estas {admin}.", - "onboarding.page_six.almost_done": "Estas preskaŭ finita…", - "onboarding.page_six.appetoot": "Bonan a‘pepi’ton!", - "onboarding.page_six.apps_available": "{apps} estas elŝuteblaj por iOS, Androido kaj alioj. Kaj nun… bonan a‘pepi’ton!", - "onboarding.page_six.github": "Mastodono estas libera, senpaga kaj malfermkoda programaro. Vi povas signali cimojn, proponi funkciojn aŭ kontribui al gîa kreskado ĉe {github}.", - "onboarding.page_six.guidelines": "komunreguloj", - "onboarding.page_six.read_guidelines": "Ni petas vin: ne forgesu legi la {guidelines}n de {domain}!", - "onboarding.page_six.various_app": "telefon-aplikaĵoj", - "onboarding.page_three.profile": "Redaktu vian profilon por ŝanĝi vian avataron, priskribon kaj vian nomon. Vi tie trovos ankoraŭ aliajn agordojn.", - "onboarding.page_three.search": "Uzu la serĉokampo por trovi uzantojn kaj esplori kradvortojn tiel ke {illustration} kaj {introductions}. Por trovi iun, kiu ne estas ĉe ĉi tiu instanco, uzu ĝian kompletan uznomon.", - "onboarding.page_two.compose": "Skribu pepojn en la verkkolumno. Vi povas aldoni bildojn, ŝanĝi la agordojn de privateco kaj aldoni tiklavertojn (« content warning ») dank' al la piktogramoj malsupre.", - "onboarding.skip": "Pasigi", - "privacy.change": "Alĝustigi la privateco de la mesaĝo", - "privacy.direct.long": "Vidigi nur al la menciitaj personoj", + "onboarding.next": "Sekva", + "onboarding.page_five.public_timelines": "La loka tempolinio montras publikajn mesaĝojn de ĉiuj en {domain}. La fratara tempolinio montras publikajn mesaĝojn de ĉiuj, kiuj estas sekvataj de homoj en {domain}. Tio estas la publikaj tempolinioj, kio estas bona maniero por malkovri novajn homojn.", + "onboarding.page_four.home": "La hejma tempolinio montras mesaĝojn de ĉiuj uzantoj, kiujn vi sekvas.", + "onboarding.page_four.notifications": "La sciiga kolumno montras kiam iu interagas kun vi.", + "onboarding.page_one.federation": "Mastodon estas reto de sendependaj serviloj, unuiĝintaj por krei pligrandan socian reton. Ni nomas tiujn servilojn nodoj.", + "onboarding.page_one.full_handle": "Via kompleta uzantnomo", + "onboarding.page_one.handle_hint": "Jen kion vi petus al viaj amikoj serĉi.", + "onboarding.page_one.welcome": "Bonvenon en Mastodon!", + "onboarding.page_six.admin": "Via noda administranto estas {admin}.", + "onboarding.page_six.almost_done": "Preskaŭ finita…", + "onboarding.page_six.appetoot": "Saĝan mesaĝadon!", + "onboarding.page_six.apps_available": "{apps} estas disponeblaj por iOS, Android kaj aliaj platformoj.", + "onboarding.page_six.github": "Mastodon estas libera, senpaga kaj malfermitkoda programo. Vi povas raporti cimojn, proponi funkciojn aŭ kontribui al la kodo en {github}.", + "onboarding.page_six.guidelines": "komunumaj gvidlinioj", + "onboarding.page_six.read_guidelines": "Bonvolu atenti pri la {guidelines} de {domain}!", + "onboarding.page_six.various_app": "telefonaj aplikaĵoj", + "onboarding.page_three.profile": "Redaktu vian profilon por ŝanĝi vian profilbildon, priskribon kaj nomon. Vi ankaŭ trovos tie aliajn agordojn.", + "onboarding.page_three.search": "Uzu la serĉilon por trovi uzantojn kaj esplori kradvortojn, tiel {illustration} kaj {introductions}. Por trovi iun, kiu ne estas en ĉi tiu nodo, uzu ties kompletan uzantnomon.", + "onboarding.page_two.compose": "Skribu mesaĝojn en la skriba kolumno. Vi povas alŝuti bildojn, ŝanĝi privatecajn agordojn, kaj aldoni avertojn pri la enhavo per la subaj bildetoj.", + "onboarding.skip": "Preterpasi", + "privacy.change": "Agordi mesaĝan privatecon", + "privacy.direct.long": "Afiŝi nur al menciitaj uzantoj", "privacy.direct.short": "Rekta", - "privacy.private.long": "Vidigi nur al viaj sekvantoj", - "privacy.private.short": "Nursekvanta", - "privacy.public.long": "Vidigi en publikaj tempolinioj", + "privacy.private.long": "Afiŝi nur al sekvantoj", + "privacy.private.short": "Nur por sekvantoj", + "privacy.public.long": "Afiŝi en publikaj tempolinioj", "privacy.public.short": "Publika", - "privacy.unlisted.long": "Ne vidigi en publikaj tempolinioj", + "privacy.unlisted.long": "Ne afiŝi en publikaj tempolinioj", "privacy.unlisted.short": "Nelistigita", + "regeneration_indicator.label": "Ŝargado…", + "regeneration_indicator.sublabel": "Via hejma fluo pretiĝas!", "relative_time.days": "{number}t", "relative_time.hours": "{number}h", "relative_time.just_now": "nun", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", - "reply_indicator.cancel": "Malfari", + "reply_indicator.cancel": "Nuligi", + "report.forward": "Plusendi al {target}", + "report.forward_hint": "La konto estas en alia servilo. Ĉu sendi sennomigitan kopion de la signalo ankaŭ tien?", + "report.hint": "La signalo estos sendita al la kontrolantoj de via nodo. Vi povas doni klarigon pri kial vi signalas ĉi tiun konton sube:", "report.placeholder": "Pliaj komentoj", "report.submit": "Sendi", - "report.target": "Signalaĵo", + "report.target": "Signali {target}", "search.placeholder": "Serĉi", "search_popout.search_format": "Detala serĉo", + "search_popout.tips.full_text": "Simplaj tekstoj montras la mesaĝojn, kiujn vi skribis, stelumis, diskonigis, aŭ en kiuj vi estis menciita, sed ankaŭ kongruajn uzantnomojn, montratajn nomojn, kaj kradvortojn.", "search_popout.tips.hashtag": "kradvorto", - "search_popout.tips.status": "statkonigo", - "search_popout.tips.text": "Simpla teksto eligas la kongruajn afiŝnomojn, uznomojn kaj kradvortojn.", + "search_popout.tips.status": "mesaĝoj", + "search_popout.tips.text": "Simpla teksto montras la kongruajn afiŝitajn nomojn, uzantnomojn kaj kradvortojn", "search_popout.tips.user": "uzanto", - "search_results.total": "{count, number} {count, plural, one {rezultato} other {rezultatoj}}", - "standalone.public_title": "Rigardeti…", - "status.block": "Block @{name}", - "status.cannot_reblog": "Tiun publikaĵon oni ne povas diskonigi", + "search_results.accounts": "Homoj", + "search_results.hashtags": "Kradvortoj", + "search_results.statuses": "Mesaĝoj", + "search_results.total": "{count, number} {count, plural, one {rezulto} other {rezultoj}}", + "standalone.public_title": "Enrigardo…", + "status.block": "Bloki @{name}", + "status.cannot_reblog": "Ĉi tiu mesaĝo ne diskonigeblas", "status.delete": "Forigi", - "status.embed": "Enmeti", - "status.favourite": "Favori", - "status.load_more": "Ŝargi plie", - "status.media_hidden": "Sonbildaĵo kaŝita", + "status.embed": "Enkorpigi", + "status.favourite": "Stelumi", + "status.load_more": "Ŝargi pli", + "status.media_hidden": "Aŭdovidaĵo kaŝita", "status.mention": "Mencii @{name}", "status.more": "Pli", - "status.mute": "Mute @{name}", + "status.mute": "Silentigi @{name}", "status.mute_conversation": "Silentigi konversacion", - "status.open": "Disfaldi statkonigon", - "status.pin": "Pingli al la profilo", + "status.open": "Grandigi ĉi tiun mesaĝon", + "status.pin": "Alpingli en la profilo", + "status.pinned": "Alpinglita mesaĝo", "status.reblog": "Diskonigi", "status.reblogged_by": "{name} diskonigis", "status.reply": "Respondi", @@ -235,28 +251,28 @@ "status.sensitive_toggle": "Alklaki por vidi", "status.sensitive_warning": "Tikla enhavo", "status.share": "Diskonigi", - "status.show_less": "Refaldi", - "status.show_more": "Disfaldi", + "status.show_less": "Malgrandigi", + "status.show_more": "Grandigi", "status.unmute_conversation": "Malsilentigi konversacion", "status.unpin": "Depingli de profilo", - "tabs_bar.compose": "Ekskribi", - "tabs_bar.federated_timeline": "Federacia tempolinio", + "tabs_bar.federated_timeline": "Fratara tempolinio", "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", + "ui.beforeunload": "Via malneto perdiĝos se vi eliras de Mastodon.", + "upload_area.title": "Altreni kaj lasi por alŝuti", + "upload_button.label": "Aldoni aŭdovidaĵon", + "upload_form.description": "Priskribi por misvidantaj homoj", + "upload_form.focus": "Stuci", "upload_form.undo": "Malfari", - "upload_progress.label": "Alŝutanta…", + "upload_progress.label": "Alŝutado…", "video.close": "Fermi videon", - "video.exit_fullscreen": "Eliri el plenekrano", - "video.expand": "Vastigi videon", - "video.fullscreen": "Igi plenekrane", + "video.exit_fullscreen": "Eksigi plenekrana", + "video.expand": "Grandigi videon", + "video.fullscreen": "Igi plenekrana", "video.hide": "Kaŝi videon", "video.mute": "Silentigi", "video.pause": "Paŭzi", - "video.play": "Legi", + "video.play": "Ekigi", "video.unmute": "Malsilentigi" } diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 8a8110b1e..d172dff1c 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -1,7 +1,9 @@ { "account.block": "Bloquear", "account.block_domain": "Ocultar todo de {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "La siguiente información del usuario puede estar incompleta.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Editar perfil", "account.follow": "Seguir", "account.followers": "Seguidores", @@ -13,7 +15,9 @@ "account.moved_to": "{name} se ha mudado a:", "account.mute": "Silenciar a @{name}", "account.mute_notifications": "Silenciar notificaciones de @{name}", + "account.muted": "Muted", "account.posts": "Publicaciones", + "account.posts_with_replies": "Toots with replies", "account.report": "Reportar a @{name}", "account.requested": "Esperando aprobación", "account.share": "Compartir el perfil de @{name}", @@ -36,7 +40,7 @@ "column.favourites": "Favoritos", "column.follow_requests": "Solicitudes de seguimiento", "column.home": "Inicio", - "column.lists": "Lists", + "column.lists": "Listas", "column.mutes": "Usuarios silenciados", "column.notifications": "Notificaciones", "column.pins": "Toot fijado", @@ -92,7 +96,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": "No hay nada en esta lista aún.", + "empty_column.list": "No hay nada en esta lista aún. Cuando miembros de esta lista publiquen nuevos estatus, estos aparecerán qui.", "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", @@ -139,6 +143,7 @@ "loading_indicator.label": "Cargando…", "media_gallery.toggle_visible": "Cambiar visibilidad", "missing_indicator.label": "No encontrado", + "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Ocultar notificaciones de este usuario?", "navigation_bar.blocks": "Usuarios bloqueados", "navigation_bar.community_timeline": "Historia local", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "La línea de tiempo principal muestra toots de gente que sigues.", "onboarding.page_four.notifications": "Las notificaciones se muestran cuando alguien interactúa contigo.", "onboarding.page_one.federation": "Mastodon es una red de servidores federados que conforman una red social aún más grande. Llamamos a estos servidores instancias.", - "onboarding.page_one.handle": "Estás en {domain}, así que tu nombre de usuario completo es {handle}", + "onboarding.page_one.full_handle": "Your full handle", + "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.", "onboarding.page_one.welcome": "¡Bienvenido a Mastodon!", "onboarding.page_six.admin": "El administrador de tu instancia es {admin}.", "onboarding.page_six.almost_done": "Ya casi…", @@ -197,21 +203,30 @@ "privacy.public.short": "Público", "privacy.unlisted.long": "No mostrar en la historia federada", "privacy.unlisted.short": "Sin federar", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", "relative_time.just_now": "ahora", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "reply_indicator.cancel": "Cancelar", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Comentarios adicionales", "report.submit": "Publicar", "report.target": "Reportando", "search.placeholder": "Buscar", "search_popout.search_format": "Formato de búsqueda avanzada", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "etiqueta", "search_popout.tips.status": "status", "search_popout.tips.text": "El texto simple devuelve correspondencias de nombre, usuario y hashtag", "search_popout.tips.user": "usuario", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", "standalone.public_title": "Un pequeño vistazo...", "status.block": "Block @{name}", @@ -227,6 +242,7 @@ "status.mute_conversation": "Silenciar conversación", "status.open": "Expandir estado", "status.pin": "Fijar", + "status.pinned": "Pinned toot", "status.reblog": "Retootear", "status.reblogged_by": "Retooteado por {name}", "status.reply": "Responder", @@ -239,7 +255,6 @@ "status.show_more": "Mostrar más", "status.unmute_conversation": "Dejar de silenciar conversación", "status.unpin": "Dejar de fijar", - "tabs_bar.compose": "Redactar", "tabs_bar.federated_timeline": "Federado", "tabs_bar.home": "Inicio", "tabs_bar.local_timeline": "Local", @@ -248,6 +263,7 @@ "upload_area.title": "Arrastra y suelta para subir", "upload_button.label": "Subir multimedia", "upload_form.description": "Describir para los usuarios con dificultad visual", + "upload_form.focus": "Crop", "upload_form.undo": "Deshacer", "upload_progress.label": "Subiendo…", "video.close": "Cerrar video", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index 8c52ffdb4..b5b81bff9 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -1,7 +1,9 @@ { "account.block": "مسدودسازی @{name}", "account.block_domain": "پنهانسازی همه چیز از سرور {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "اطلاعات زیر ممکن است نمایهٔ این کاربر را به تمامی نشان ندهد.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "ویرایش نمایه", "account.follow": "پی بگیرید", "account.followers": "پیگیران", @@ -13,7 +15,9 @@ "account.moved_to": "{name} منتقل شده است به:", "account.mute": "بیصدا کردن @{name}", "account.mute_notifications": "بیصداکردن اعلانها از طرف @{name}", + "account.muted": "Muted", "account.posts": "نوشتهها", + "account.posts_with_replies": "Toots with replies", "account.report": "گزارش @{name}", "account.requested": "در انتظار پذیرش", "account.share": "همرسانی نمایهٔ @{name}", @@ -139,6 +143,7 @@ "loading_indicator.label": "بارگیری...", "media_gallery.toggle_visible": "تغییر پیدایی", "missing_indicator.label": "پیدا نشد", + "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "اعلانهای این کاربر پنهان شود؟", "navigation_bar.blocks": "کاربران مسدودشده", "navigation_bar.community_timeline": "نوشتههای محلی", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "ستون «خانه» نوشتههای کسانی را نشان میدهد که شما پی میگیرید.", "onboarding.page_four.notifications": "ستون «اعلانها» ارتباطهای شما با دیگران را نشان میدهد.", "onboarding.page_one.federation": "ماستدون شبکهای از سرورهای مستقل است که با پیوستن به یکدیگر یک شبکهٔ اجتماعی بزرگ را تشکیل میدهند.", - "onboarding.page_one.handle": "شما روی سرور {domain} هستید، بنابراین شناسهٔ کامل شما {handle} است", + "onboarding.page_one.full_handle": "Your full handle", + "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.", "onboarding.page_one.welcome": "به ماستدون خوش آمدید!", "onboarding.page_six.admin": "نشانی مسئول سرور شما {admin} است.", "onboarding.page_six.almost_done": "الان تقریباً آمادهاید...", @@ -197,21 +203,30 @@ "privacy.public.short": "عمومی", "privacy.unlisted.long": "عمومی، ولی فهرست نکن", "privacy.unlisted.short": "فهرستنشده", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", "relative_time.just_now": "الان", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "reply_indicator.cancel": "لغو", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "توضیح اضافه", "report.submit": "بفرست", "report.target": "گزارشدادن", "search.placeholder": "جستجو", "search_popout.search_format": "راهنمای جستجوی پیشرفته", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "هشتگ", "search_popout.tips.status": "نوشته", "search_popout.tips.text": "جستجوی متنی ساده برای نامها، نامهای کاربری، و هشتگها", "search_popout.tips.user": "کاربر", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}", "standalone.public_title": "نگاهی به کاربران این سرور...", "status.block": "Block @{name}", @@ -227,6 +242,7 @@ "status.mute_conversation": "بیصداکردن گفتگو", "status.open": "این نوشته را باز کن", "status.pin": "نوشتهٔ ثابت نمایه", + "status.pinned": "Pinned toot", "status.reblog": "بازبوقیدن", "status.reblogged_by": "{name} بازبوقید", "status.reply": "پاسخ", @@ -239,7 +255,6 @@ "status.show_more": "نمایش", "status.unmute_conversation": "باصداکردن گفتگو", "status.unpin": "برداشتن نوشتهٔ ثابت نمایه", - "tabs_bar.compose": "بنویسید", "tabs_bar.federated_timeline": "همگانی", "tabs_bar.home": "خانه", "tabs_bar.local_timeline": "محلی", @@ -248,6 +263,7 @@ "upload_area.title": "برای بارگذاری به اینجا بکشید", "upload_button.label": "افزودن تصویر", "upload_form.description": "نوشتهٔ توضیحی برای کمبینایان و نابینایان", + "upload_form.focus": "Crop", "upload_form.undo": "واگردانی", "upload_progress.label": "بارگذاری...", "video.close": "بستن ویدیو", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 74ab699c4..aa97aae84 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -1,262 +1,278 @@ { "account.block": "Estä @{name}", - "account.block_domain": "Hide everything from {domain}", - "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", + "account.block_domain": "Piilota kaikki sisältö verkkotunnuksesta {domain}", + "account.blocked": "Blocked", + "account.disclaimer_full": "Alla olevat käyttäjän profiilitiedot saattavat olla epätäydellisiä.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Muokkaa", "account.follow": "Seuraa", "account.followers": "Seuraajia", "account.follows": "Seuraa", "account.follows_you": "Seuraa sinua", - "account.hide_reblogs": "Hide boosts from @{name}", + "account.hide_reblogs": "Piilota buustaukset käyttäjältä @{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.moved_to": "{name} on muuttanut instanssiin:", + "account.mute": "Mykistä @{name}", + "account.mute_notifications": "Mykistä ilmoitukset käyttäjältä @{name}", + "account.muted": "Muted", + "account.posts": "Töötit", + "account.posts_with_replies": "Toots with replies", "account.report": "Report @{name}", - "account.requested": "Odottaa hyväksyntää", - "account.share": "Share @{name}'s profile", - "account.show_reblogs": "Show boosts from @{name}", + "account.requested": "Odottaa hyväksyntää. Klikkaa peruuttaaksesi seurauspyynnön", + "account.share": "Jaa käyttäjän @{name} profiili", + "account.show_reblogs": "Näytä boostaukset käyttäjältä @{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.", - "bundle_column_error.retry": "Try again", + "account.unblock_domain": "Näytä {domain}", + "account.unfollow": "Lakkaa seuraamasta", + "account.unmute": "Poista mykistys käyttäjältä @{name}", + "account.unmute_notifications": "Poista mykistys käyttäjän @{name} ilmoituksilta", + "account.view_full_profile": "Näytä koko profiili", + "boost_modal.combo": "Voit painaa näppäimiä {combo} ohittaaksesi tämän ensi kerralla", + "bundle_column_error.body": "Jokin meni vikaan tätä komponenttia ladatessa.", + "bundle_column_error.retry": "Yritä uudestaan", "bundle_column_error.title": "Network error", - "bundle_modal_error.close": "Close", - "bundle_modal_error.message": "Something went wrong while loading this component.", - "bundle_modal_error.retry": "Try again", - "column.blocks": "Blocked users", + "bundle_modal_error.close": "Sulje", + "bundle_modal_error.message": "Jokin meni vikaan tätä komponenttia ladatessa.", + "bundle_modal_error.retry": "Yritä uudestaan", + "column.blocks": "Estetyt käyttäjät", "column.community": "Paikallinen aikajana", - "column.favourites": "Favourites", - "column.follow_requests": "Follow requests", + "column.favourites": "Suosikit", + "column.follow_requests": "Seurauspyynnöt", "column.home": "Koti", - "column.lists": "Lists", - "column.mutes": "Muted users", + "column.lists": "Listat", + "column.mutes": "Mykistetyt käyttäjät", "column.notifications": "Ilmoitukset", "column.pins": "Pinned toot", "column.public": "Yleinen aikajana", "column_back_button.label": "Takaisin", - "column_header.hide_settings": "Hide settings", - "column_header.moveLeft_settings": "Move column to the left", - "column_header.moveRight_settings": "Move column to the right", - "column_header.pin": "Pin", - "column_header.show_settings": "Show settings", - "column_header.unpin": "Unpin", - "column_subheading.navigation": "Navigation", - "column_subheading.settings": "Settings", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", - "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", - "compose_form.lock_disclaimer.lock": "locked", + "column_header.hide_settings": "Piilota asetukset", + "column_header.moveLeft_settings": "Siirrä saraketta vasemmalle", + "column_header.moveRight_settings": "Siirrä saraketta oikealle", + "column_header.pin": "Kiinnitä", + "column_header.show_settings": "Näytä asetukset", + "column_header.unpin": "Poista kiinnitys", + "column_subheading.navigation": "Navigaatio", + "column_subheading.settings": "Asetukset", + "compose_form.hashtag_warning": "Tämä töötti ei tule näkymään hashtag-hauissa, koska se ei näy julkisilla aikajanoilla. Vain julkisia tööttejä voi hakea hashtageilla.", + "compose_form.lock_disclaimer": "Tilisi ei ole {locked}. Kuka tahansa voi seurata tiliäsi ja nähdä vain seuraajille -postauksesi.", + "compose_form.lock_disclaimer.lock": "lukittu", "compose_form.placeholder": "Mitä sinulla on mielessä?", "compose_form.publish": "Toot", "compose_form.publish_loud": "{publish}!", "compose_form.sensitive": "Merkitse media herkäksi", "compose_form.spoiler": "Piiloita teksti varoituksen taakse", "compose_form.spoiler_placeholder": "Content warning", - "confirmation_modal.cancel": "Cancel", - "confirmations.block.confirm": "Block", - "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmation_modal.cancel": "Peruuta", + "confirmations.block.confirm": "Estä", + "confirmations.block.message": "Oletko varma, että haluat estää käyttäjän {name}?", "confirmations.delete.confirm": "Delete", - "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.delete.message": "Oletko varma, että haluat poistaa tämän statuspäivityksen?", "confirmations.delete_list.confirm": "Delete", - "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", - "confirmations.domain_block.confirm": "Hide entire domain", - "confirmations.domain_block.message": "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.", - "confirmations.mute.confirm": "Mute", - "confirmations.mute.message": "Are you sure you want to mute {name}?", - "confirmations.unfollow.confirm": "Unfollow", - "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", - "embed.instructions": "Embed this status on your website by copying the code below.", - "embed.preview": "Here is what it will look like:", - "emoji_button.activity": "Activity", - "emoji_button.custom": "Custom", - "emoji_button.flags": "Flags", - "emoji_button.food": "Food & Drink", - "emoji_button.label": "Insert emoji", - "emoji_button.nature": "Nature", - "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", - "emoji_button.objects": "Objects", - "emoji_button.people": "People", - "emoji_button.recent": "Frequently used", - "emoji_button.search": "Search...", - "emoji_button.search_results": "Search results", - "emoji_button.symbols": "Symbols", - "emoji_button.travel": "Travel & Places", - "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", - "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", - "follow_request.reject": "Reject", - "getting_started.appsshort": "Apps", + "confirmations.delete_list.message": "Oletko varma, että haluat poistaa tämän listan pysyvästi?", + "confirmations.domain_block.confirm": "Piilota koko verkko-osoite", + "confirmations.domain_block.message": "Oletko aivan oikeasti varma että haluat estää koko verkko-osoitteen {domain}? Useimmissa tapauksissa muutamat kohdistetut estot ja mykistykset ovat riittäviä ja suositeltavampia.", + "confirmations.mute.confirm": "Mykistä", + "confirmations.mute.message": "Oletko varma että haluat mykistää käyttäjän {name}?", + "confirmations.unfollow.confirm": "Lakkaa seuraamasta", + "confirmations.unfollow.message": "Oletko varma, että haluat lakata seuraamasta käyttäjää {name}?", + "embed.instructions": "Upota tämä statuspäivitys sivullesi kopioimalla alla oleva koodi.", + "embed.preview": "Tältä se tulee näyttämään:", + "emoji_button.activity": "Aktiviteetit", + "emoji_button.custom": "Mukautetut", + "emoji_button.flags": "Liput", + "emoji_button.food": "Ruoka ja juoma", + "emoji_button.label": "Lisää emoji", + "emoji_button.nature": "Luonto", + "emoji_button.not_found": "Ei emojeja!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Objektit", + "emoji_button.people": "Ihmiset", + "emoji_button.recent": "Usein käytetyt", + "emoji_button.search": "Etsi...", + "emoji_button.search_results": "Hakutulokset", + "emoji_button.symbols": "Symbolit", + "emoji_button.travel": "Matkailu", + "empty_column.community": "Paikallinen aikajana on tyhjä. Kirjoita jotain julkista saadaksesi pyörät pyörimään!", + "empty_column.hashtag": "Tässä hashtagissa ei ole vielä mitään.", + "empty_column.home": "Kotiaikajanasi on tyhjä! Käy vierailemassa {public}ssa tai käytä hakutoimintoa aloittaaksesi ja tavataksesi muita käyttäjiä.", + "empty_column.home.public_timeline": "yleinen aikajana", + "empty_column.list": "Tämä lista on vielä tyhjä. Kun listan jäsenet julkaisevat statuspäivityksiä, ne näkyvät tässä.", + "empty_column.notifications": "Sinulle ei ole vielä ilmoituksia. Juttele muille aloittaaksesi keskustelun.", + "empty_column.public": "Täällä ei ole mitään! Kirjoita jotain julkisesti, tai käy manuaalisesti seuraamassa käyttäjiä muista instansseista saadaksesi sisältöä", + "follow_request.authorize": "Valtuuta", + "follow_request.reject": "Hylkää", + "getting_started.appsshort": "Sovellukset", "getting_started.faq": "FAQ", "getting_started.heading": "Aloitus", - "getting_started.open_source_notice": "Mastodon Mastodon on avoimen lähdekoodin ohjelma. Voit avustaa tai raportoida ongelmia GitHub palvelussa {github}.", - "getting_started.userguide": "User Guide", - "home.column_settings.advanced": "Advanced", - "home.column_settings.basic": "Basic", - "home.column_settings.filter_regex": "Filter out by regular expressions", - "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", + "getting_started.open_source_notice": "Mastodon on avoimen lähdekoodin ohjelma. Voit avustaa tai raportoida ongelmia GitHub palvelussa {github}.", + "getting_started.userguide": "Käyttöopas", + "home.column_settings.advanced": "Tarkemmat asetukset", + "home.column_settings.basic": "Perusasetukset", + "home.column_settings.filter_regex": "Suodata säännöllisten lauseiden avulla", + "home.column_settings.show_reblogs": "Näytä buustaukset", + "home.column_settings.show_replies": "Näytä vastaukset", + "home.settings": "Sarakeasetukset", + "keyboard_shortcuts.back": "liikkuaksesi taaksepäin", + "keyboard_shortcuts.boost": "buustataksesi", + "keyboard_shortcuts.column": "keskittääksesi statuspäivitykseen yhdessä sarakkeista", + "keyboard_shortcuts.compose": "aktivoidaksesi tekstinkirjoitusalueen", "keyboard_shortcuts.description": "Description", - "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.down": "liikkuaksesi listassa alaspäin", "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", + "keyboard_shortcuts.favourite": "tykätäksesi", + "keyboard_shortcuts.heading": "Näppäinoikotiet", + "keyboard_shortcuts.hotkey": "Pikanäppäin", + "keyboard_shortcuts.legend": "näyttääksesi tämän selitteen", + "keyboard_shortcuts.mention": "mainitaksesi julkaisijan", + "keyboard_shortcuts.reply": "vastataksesi", + "keyboard_shortcuts.search": "aktivoidaksesi hakukentän", + "keyboard_shortcuts.toot": "aloittaaksesi uuden töötin kirjoittamisen", + "keyboard_shortcuts.unfocus": "poistaaksesi aktivoinnin tekstikentästä/hakukentästä", + "keyboard_shortcuts.up": "liikkuaksesi listassa ylöspäin", "lightbox.close": "Sulje", - "lightbox.next": "Next", - "lightbox.previous": "Previous", - "lists.account.add": "Add to list", - "lists.account.remove": "Remove from list", + "lightbox.next": "Seuraava", + "lightbox.previous": "Edellinen", + "lists.account.add": "Lisää listaan", + "lists.account.remove": "Poista listalta", "lists.delete": "Delete list", - "lists.edit": "Edit list", - "lists.new.create": "Add list", - "lists.new.title_placeholder": "New list title", - "lists.search": "Search among people you follow", - "lists.subheading": "Your lists", + "lists.edit": "Muokkaa listaa", + "lists.new.create": "Lisää lista", + "lists.new.title_placeholder": "Uuden listan otsikko", + "lists.search": "Etsi seuraamiesi henkilöiden joukosta", + "lists.subheading": "Omat listat", "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", + "media_gallery.toggle_visible": "Säädä näkyvyyttä", + "missing_indicator.label": "Ei löydetty", + "missing_indicator.sublabel": "Tätä resurssia ei löytynyt", + "mute_modal.hide_notifications": "Piilota ilmoitukset tältä käyttäjältä?", + "navigation_bar.blocks": "Estetyt käyttäjät", "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.lists": "Lists", + "navigation_bar.favourites": "Suosikit", + "navigation_bar.follow_requests": "Seurauspyynnöt", + "navigation_bar.info": "Tietoa tästä instanssista", + "navigation_bar.keyboard_shortcuts": "Näppäinoikotiet", + "navigation_bar.lists": "Listat", "navigation_bar.logout": "Kirjaudu ulos", - "navigation_bar.mutes": "Muted users", - "navigation_bar.pins": "Pinned toots", + "navigation_bar.mutes": "Mykistetyt käyttäjät", + "navigation_bar.pins": "Kiinnitetyt töötit", "navigation_bar.preferences": "Ominaisuudet", "navigation_bar.public_timeline": "Yleinen aikajana", "notification.favourite": "{name} tykkäsi statuksestasi", "notification.follow": "{name} seurasi sinua", "notification.mention": "{name} mainitsi sinut", "notification.reblog": "{name} buustasi statustasi", - "notifications.clear": "Clear notifications", - "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.clear": "Tyhjennä ilmoitukset", + "notifications.clear_confirmation": "Oletko varma, että haluat lopullisesti tyhjentää kaikki ilmoituksesi?", "notifications.column_settings.alert": "Työpöytä ilmoitukset", "notifications.column_settings.favourite": "Tykkäyksiä:", "notifications.column_settings.follow": "Uusia seuraajia:", "notifications.column_settings.mention": "Mainintoja:", - "notifications.column_settings.push": "Push notifications", - "notifications.column_settings.push_meta": "This device", + "notifications.column_settings.push": "Push-ilmoitukset", + "notifications.column_settings.push_meta": "Tämä laite", "notifications.column_settings.reblog": "Buusteja:", "notifications.column_settings.show": "Näytä sarakkeessa", - "notifications.column_settings.sound": "Play sound", - "onboarding.done": "Done", - "onboarding.next": "Next", - "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", - "onboarding.page_four.home": "The home timeline shows posts from people you follow.", - "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", - "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", - "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", - "onboarding.page_one.welcome": "Welcome to Mastodon!", - "onboarding.page_six.admin": "Your instance's admin is {admin}.", - "onboarding.page_six.almost_done": "Almost done...", - "onboarding.page_six.appetoot": "Bon Appetoot!", - "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", + "notifications.column_settings.sound": "Soita ääni", + "onboarding.done": "Valmis", + "onboarding.next": "Seuraava", + "onboarding.page_five.public_timelines": "Paikallinen aikajana näyttää kaikki julkiset julkaisut kaikilta, jotka ovat verkko-osoitteessa {domain}. Yleinen aikajana näyttää julkiset julkaisut kaikilta niiltä, joita käyttäjät verkko-osoitteessa {domain} seuraavat. Nämä ovat julkiset aikajanat, ja ne ovat hyviä tapoja löytää uusia ihmisiä.", + "onboarding.page_four.home": "Kotiaikajana näyttää julkaisut ihmisiltä joita seuraat.", + "onboarding.page_four.notifications": "Ilmoitukset-sarake näyttää sinulle, kun joku on viestii kanssasi.", + "onboarding.page_one.federation": "Mastodon on yhteisöpalvelu, joka toimii monen itsenäisen palvelimen muodostamassa verkossa. Me kutsumme näitä palvelimia instansseiksi.", + "onboarding.page_one.full_handle": "Koko käyttäjänimesi", + "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.", + "onboarding.page_one.welcome": "Tervetuloa Mastodoniin!", + "onboarding.page_six.admin": "Instanssisi ylläpitäjä on {admin}.", + "onboarding.page_six.almost_done": "Melkein valmista...", + "onboarding.page_six.appetoot": "Bon Appetööt!", + "onboarding.page_six.apps_available": "{apps} on saatavilla iOS:lle, Androidille ja muille alustoille.", "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", - "onboarding.page_six.guidelines": "community guidelines", - "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", - "onboarding.page_six.various_app": "mobile apps", - "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", - "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", - "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", - "onboarding.skip": "Skip", - "privacy.change": "Adjust status privacy", - "privacy.direct.long": "Post to mentioned users only", - "privacy.direct.short": "Direct", - "privacy.private.long": "Post to followers only", - "privacy.private.short": "Followers-only", - "privacy.public.long": "Post to public timelines", - "privacy.public.short": "Public", - "privacy.unlisted.long": "Do not show in public timelines", - "privacy.unlisted.short": "Unlisted", + "onboarding.page_six.guidelines": "yhteisön säännöt", + "onboarding.page_six.read_guidelines": "Ole hyvä ja lue {domain}:n {guidelines}!", + "onboarding.page_six.various_app": "mobiilisovellukset", + "onboarding.page_three.profile": "Muokkaa profiiliasi muuttaaksesi kuvakettasi, esittelyäsi ja nimimerkkiäsi. Löydät sieltä myös muita henkilökohtaisia asetuksia.", + "onboarding.page_three.search": "Käytä hakukenttää löytääksesi ihmisiä ja etsiäksesi hashtageja, kuten {illustration} tai {introductions}. Hakeaksesi henkilöä joka on toisessa instanssissa, käytä hänen käyttäjänimeään kokonaisuudessaan.", + "onboarding.page_two.compose": "Kirjoita postauksia kirjoita-sarakkeessa. Voit ladata kuvia, vaihtaa yksityisyysasetuksia ja lisätä sisältövaroituksia alla olevista painikkeista.", + "onboarding.skip": "Ohita", + "privacy.change": "Säädä töötin yksityisyysasetuksia", + "privacy.direct.long": "Julkaise vain mainituille käyttäjille", + "privacy.direct.short": "Yksityisviesti", + "privacy.private.long": "Julkaise vain seuraajille", + "privacy.private.short": "Vain seuraajat", + "privacy.public.long": "Julkaise julkisille aikajanoille", + "privacy.public.short": "Julkinen", + "privacy.unlisted.long": "Älä julkaise yleisillä aikajanoilla", + "privacy.unlisted.short": "Julkinen, mutta älä näytä julkisella aikajanalla", + "regeneration_indicator.label": "Ladataan…", + "regeneration_indicator.sublabel": "Kotinäkymääsi valmistellaan!", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", - "relative_time.just_now": "now", + "relative_time.just_now": "nyt", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "reply_indicator.cancel": "Peruuta", - "report.placeholder": "Additional comments", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", + "report.placeholder": "Lisäkommentit", "report.submit": "Submit", "report.target": "Reporting", "search.placeholder": "Hae", - "search_popout.search_format": "Advanced search format", - "search_popout.tips.hashtag": "hashtag", + "search_popout.search_format": "Tarkennettu haku", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.tips.hashtag": "hashtagi", "search_popout.tips.status": "status", - "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", - "search_popout.tips.user": "user", + "search_popout.tips.text": "Pelkkä tekstihaku palauttaa hakua vastaavat nimimerkit, käyttäjänimet ja hastagit", + "search_popout.tips.user": "käyttäjä", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", - "standalone.public_title": "A look inside...", + "standalone.public_title": "Kurkistus sisälle...", "status.block": "Block @{name}", - "status.cannot_reblog": "This post cannot be boosted", + "status.cannot_reblog": "Tätä postausta ei voi buustata", "status.delete": "Poista", - "status.embed": "Embed", + "status.embed": "Upota", "status.favourite": "Tykkää", - "status.load_more": "Load more", - "status.media_hidden": "Media hidden", + "status.load_more": "Lataa lisää", + "status.media_hidden": "Media piilotettu", "status.mention": "Mainitse @{name}", - "status.more": "More", - "status.mute": "Mute @{name}", - "status.mute_conversation": "Mute conversation", - "status.open": "Expand this status", - "status.pin": "Pin on profile", + "status.more": "Lisää", + "status.mute": "Mykistä @{name}", + "status.mute_conversation": "Mykistä keskustelu", + "status.open": "Laajenna statuspäivitys", + "status.pin": "Kiinnitä profiiliin", + "status.pinned": "Pinned toot", "status.reblog": "Buustaa", "status.reblogged_by": "{name} buustasi", "status.reply": "Vastaa", - "status.replyAll": "Reply to thread", + "status.replyAll": "Vastaa ketjuun", "status.report": "Report @{name}", "status.sensitive_toggle": "Klikkaa nähdäksesi", "status.sensitive_warning": "Arkaluontoista sisältöä", - "status.share": "Share", - "status.show_less": "Show less", - "status.show_more": "Show more", - "status.unmute_conversation": "Unmute conversation", - "status.unpin": "Unpin from profile", - "tabs_bar.compose": "Luo", + "status.share": "Jaa", + "status.show_less": "Näytä vähemmän", + "status.show_more": "Näytä lisää", + "status.unmute_conversation": "Poista mykistys keskustelulta", + "status.unpin": "Irrota profiilista", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Koti", - "tabs_bar.local_timeline": "Local", + "tabs_bar.local_timeline": "Paikallinen", "tabs_bar.notifications": "Ilmoitukset", - "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", - "upload_area.title": "Drag & drop to upload", + "ui.beforeunload": "Luonnoksesi menetetään, jos poistut Mastodonista.", + "upload_area.title": "Raahaa ja pudota tähän ladataksesi", "upload_button.label": "Lisää mediaa", - "upload_form.description": "Describe for the visually impaired", + "upload_form.description": "Anna kuvaus näkörajoitteisia varten", + "upload_form.focus": "Crop", "upload_form.undo": "Peru", - "upload_progress.label": "Uploading...", - "video.close": "Close video", - "video.exit_fullscreen": "Exit full screen", - "video.expand": "Expand video", + "upload_progress.label": "Ladataan...", + "video.close": "Sulje video", + "video.exit_fullscreen": "Poistu koko näytön tilasta", + "video.expand": "Laajenna video", "video.fullscreen": "Full screen", - "video.hide": "Hide video", - "video.mute": "Mute sound", - "video.pause": "Pause", - "video.play": "Play", - "video.unmute": "Unmute sound" + "video.hide": "Piilota video", + "video.mute": "Mykistä ääni", + "video.pause": "Keskeytä", + "video.play": "Toista", + "video.unmute": "Poista mykistys ääneltä" } diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index e77107fc5..4df1ef30f 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -1,7 +1,9 @@ { - "account.block": "Bloquer", - "account.block_domain": "Tout masquer de {domain}", + "account.block": "Bloquer @{name}", + "account.block_domain": "Tout masquer venant de {domain}", + "account.blocked": "Bloqué", "account.disclaimer_full": "Les données ci-dessous peuvent ne pas refléter ce profil dans sa totalité.", + "account.domain_blocked": "Domaine caché", "account.edit_profile": "Modifier le profil", "account.follow": "Suivre", "account.followers": "Abonné⋅e⋅s", @@ -11,9 +13,11 @@ "account.media": "Média", "account.mention": "Mentionner", "account.moved_to": "{name} a déménagé vers :", - "account.mute": "Masquer", + "account.mute": "Masquer @{name}", "account.mute_notifications": "Ignorer les notifications de @{name}", - "account.posts": "Statuts", + "account.muted": "Silencé", + "account.posts": "Pouets", + "account.posts_with_replies": "Pouets avec réponses", "account.report": "Signaler", "account.requested": "Invitation envoyée", "account.share": "Partager le profil de @{name}", @@ -36,7 +40,7 @@ "column.favourites": "Favoris", "column.follow_requests": "Demandes de suivi", "column.home": "Accueil", - "column.lists": "Lists", + "column.lists": "Listes", "column.mutes": "Comptes masqués", "column.notifications": "Notifications", "column.pins": "Pouets épinglés", @@ -50,7 +54,7 @@ "column_header.unpin": "Retirer", "column_subheading.navigation": "Navigation", "column_subheading.settings": "Paramètres", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.hashtag_warning": "Ce pouet ne sera pas listé dans les recherches par hashtag car sa visibilité est réglée sur \"non-listé\". Seuls les pouets avec une visibilité \"publique\" peuvent être recherchés par hashtag.", "compose_form.lock_disclaimer": "Votre compte n’est pas {locked}. Tout le monde peut vous suivre et voir vos pouets privés.", "compose_form.lock_disclaimer.lock": "verrouillé", "compose_form.placeholder": "Qu’avez-vous en tête ?", @@ -73,7 +77,7 @@ "confirmations.unfollow.confirm": "Ne plus suivre", "confirmations.unfollow.message": "Voulez-vous arrêter de suivre {name} ?", "embed.instructions": "Intégrez ce statut à votre site en copiant le code ci-dessous.", - "embed.preview": "Il apparaîtra comme cela :", + "embed.preview": "Il apparaîtra comme cela :", "emoji_button.activity": "Activités", "emoji_button.custom": "Personnalisés", "emoji_button.flags": "Drapeaux", @@ -89,12 +93,12 @@ "emoji_button.symbols": "Symboles", "emoji_button.travel": "Lieux et voyages", "empty_column.community": "Le fil public local est vide. Écrivez donc quelque chose pour le remplir !", - "empty_column.hashtag": "Il n’y a encore aucun contenu associé à ce hashtag", + "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": "Il n'y a rien dans cette liste pour l'instant. Dès que des personnes de cette liste publierons de nouveaux statuts, ils apparaîtront ici.", "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.", + "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", "follow_request.reject": "Rejeter", "getting_started.appsshort": "Applications", @@ -121,10 +125,10 @@ "keyboard_shortcuts.legend": "pour afficher cette légende", "keyboard_shortcuts.mention": "pour mentionner l'auteur", "keyboard_shortcuts.reply": "pour répondre", - "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.search": "pour cibler la recherche", "keyboard_shortcuts.toot": "pour démarrer un tout nouveau pouet", - "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", - "keyboard_shortcuts.up": "to move up in the list", + "keyboard_shortcuts.unfocus": "pour recentrer composer textarea/search", + "keyboard_shortcuts.up": "pour remonter dans la liste", "lightbox.close": "Fermer", "lightbox.next": "Suivant", "lightbox.previous": "Précédent", @@ -139,6 +143,7 @@ "loading_indicator.label": "Chargement…", "media_gallery.toggle_visible": "Modifier la visibilité", "missing_indicator.label": "Non trouvé", + "missing_indicator.sublabel": "Ressource introuvable", "mute_modal.hide_notifications": "Masquer les notifications de cet utilisateur ?", "navigation_bar.blocks": "Comptes bloqués", "navigation_bar.community_timeline": "Fil public local", @@ -154,15 +159,15 @@ "navigation_bar.preferences": "Préférences", "navigation_bar.public_timeline": "Fil public global", "notification.favourite": "{name} a ajouté à ses favoris :", - "notification.follow": "{name} vous suit.", + "notification.follow": "{name} vous suit", "notification.mention": "{name} vous a mentionné⋅e :", "notification.reblog": "{name} a partagé votre statut :", "notifications.clear": "Nettoyer", "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?", "notifications.column_settings.alert": "Notifications locales", "notifications.column_settings.favourite": "Favoris :", - "notifications.column_settings.follow": "Nouveaux⋅elles abonné⋅e⋅s :", - "notifications.column_settings.mention": "Mentions :", + "notifications.column_settings.follow": "Nouveaux⋅elles abonné⋅e·s :", + "notifications.column_settings.mention": "Mentions :", "notifications.column_settings.push": "Notifications push", "notifications.column_settings.push_meta": "Cet appareil", "notifications.column_settings.reblog": "Partages :", @@ -171,15 +176,16 @@ "onboarding.done": "Effectué", "onboarding.next": "Suivant", "onboarding.page_five.public_timelines": "Le fil public global affiche les posts de tou⋅te⋅s les utilisateur⋅ice⋅s suivi⋅es par les membres de {domain}. Le fil public local est identique mais se limite aux utilisateur⋅ice⋅s de {domain}.", - "onboarding.page_four.home": "L’Accueil affiche les posts de tou⋅te⋅s les utilisateur⋅ice⋅s que vous suivez", - "onboarding.page_four.notifications": "Les Notifications vous informent lorsque quelqu’un interagit avec vous", + "onboarding.page_four.home": "L’Accueil affiche les posts de tou⋅te·s les utilisateur⋅ice·s que vous suivez.", + "onboarding.page_four.notifications": "Les Notifications vous informent lorsque quelqu’un interagit avec vous.", "onboarding.page_one.federation": "Mastodon est un réseau social qui appartient à tou⋅te⋅s.", - "onboarding.page_one.handle": "Vous êtes sur {domain}, une des nombreuses instances indépendantes de Mastodon. Votre nom d’utilisateur⋅ice complet est {handle}", + "onboarding.page_one.full_handle": "Votre pleine maîtrise", + "onboarding.page_one.handle_hint": "C'est ce que vous diriez à vos amis de rechercher.", "onboarding.page_one.welcome": "Bienvenue sur Mastodon !", - "onboarding.page_six.admin": "L’administrateur⋅ice de votre instance est {admin}", + "onboarding.page_six.admin": "L’administrateur⋅ice de votre instance est {admin}.", "onboarding.page_six.almost_done": "Nous y sommes presque…", "onboarding.page_six.appetoot": "Bon appouétit !", - "onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres. Et maintenant… Bon appouétit !", + "onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres.", "onboarding.page_six.github": "Mastodon est un logiciel libre, gratuit et open-source. Vous pouvez rapporter des bogues, suggérer des fonctionnalités, ou contribuer à son développement sur {github}.", "onboarding.page_six.guidelines": "règles de la communauté", "onboarding.page_six.read_guidelines": "S’il vous plaît, n’oubliez pas de lire les {guidelines} !", @@ -189,29 +195,38 @@ "onboarding.page_two.compose": "Écrivez depuis la colonne de composition. Vous pouvez ajouter des images, changer les réglages de confidentialité, et ajouter des avertissements de contenu (Content Warning) grâce aux icônes en dessous.", "onboarding.skip": "Passer", "privacy.change": "Ajuster la confidentialité du message", - "privacy.direct.long": "N’afficher que pour les personnes mentionnées", + "privacy.direct.long": "N'envoyer qu'aux personnes mentionnées", "privacy.direct.short": "Direct", - "privacy.private.long": "N’afficher que pour vos abonné⋅e⋅s", + "privacy.private.long": "N'envoyer qu'à vos abonné⋅e⋅s", "privacy.private.short": "Privé", "privacy.public.long": "Afficher dans les fils publics", "privacy.public.short": "Public", "privacy.unlisted.long": "Ne pas afficher dans les fils publics", "privacy.unlisted.short": "Non-listé", - "relative_time.days": "{number} j", - "relative_time.hours": "{number} h", + "regeneration_indicator.label": "Chargement…", + "regeneration_indicator.sublabel": "Le flux de votre page principale est en cours de préparation !", + "relative_time.days": "{number} j", + "relative_time.hours": "{number} h", "relative_time.just_now": "à l’instant", "relative_time.minutes": "{number} min", "relative_time.seconds": "{number} s", "reply_indicator.cancel": "Annuler", + "report.forward": "Transférer à {target}", + "report.forward_hint": "Le compte provient d'un autre serveur. Envoyez également une copie anonyme du rapport ?", + "report.hint": "Le rapport sera envoyé aux modérateurs de votre instance. Vous pouvez expliquer pourquoi vous signalez ce compte ci-dessous :", "report.placeholder": "Commentaires additionnels", "report.submit": "Envoyer", "report.target": "Signalement", "search.placeholder": "Rechercher", "search_popout.search_format": "Recherche avancée", + "search_popout.tips.full_text": "Les textes simples retournent les pouets que vous avez écris, mis en favori, épinglés, ou ayant été mentionnés, ainsi que les noms d'utilisateurs, les noms affichés, et les hashtags correspondant.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "statuts", "search_popout.tips.text": "Un texte simple renvoie les noms affichés, les noms d’utilisateur⋅ice et les hashtags correspondants", "search_popout.tips.user": "utilisateur⋅ice", + "search_results.accounts": "Personnes", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Pouets", "search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}", "standalone.public_title": "Jeter un coup d’œil…", "status.block": "Block @{name}", @@ -223,10 +238,11 @@ "status.media_hidden": "Média caché", "status.mention": "Mentionner", "status.more": "Plus", - "status.mute": "Mute @{name}", + "status.mute": "Masquer @{name}", "status.mute_conversation": "Masquer la conversation", "status.open": "Déplier ce statut", "status.pin": "Épingler sur le profil", + "status.pinned": "Pouet épinglé", "status.reblog": "Partager", "status.reblogged_by": "{name} a partagé :", "status.reply": "Répondre", @@ -239,7 +255,6 @@ "status.show_more": "Déplier", "status.unmute_conversation": "Ne plus masquer la conversation", "status.unpin": "Retirer du profil", - "tabs_bar.compose": "Composer", "tabs_bar.federated_timeline": "Fil public global", "tabs_bar.home": "Accueil", "tabs_bar.local_timeline": "Fil public local", @@ -248,6 +263,7 @@ "upload_area.title": "Glissez et déposez pour envoyer", "upload_button.label": "Joindre un média", "upload_form.description": "Décrire pour les malvoyants", + "upload_form.focus": "Recadrer", "upload_form.undo": "Annuler", "upload_progress.label": "Envoi en cours…", "video.close": "Fermer la vidéo", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 77f6b82ab..e222ddaea 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -1,7 +1,9 @@ { "account.block": "Bloquear @{name}", "account.block_domain": "Ocultar calquer contido de {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "A información inferior podería mostrar un perfil incompleto da usuaria.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Editar perfil", "account.follow": "Seguir", "account.followers": "Seguidoras", @@ -13,7 +15,9 @@ "account.moved_to": "{name} marchou a:", "account.mute": "Acalar @{name}", "account.mute_notifications": "Acalar as notificacións de @{name}", - "account.posts": "Publicacións", + "account.muted": "Muted", + "account.posts": "Toots", + "account.posts_with_replies": "Toots with replies", "account.report": "Informar sobre @{name}", "account.requested": "Agardando aceptación. Pulse para cancelar a solicitude de seguimento", "account.share": "Compartir o perfil de @{name}", @@ -50,7 +54,7 @@ "column_header.unpin": "Soltar", "column_subheading.navigation": "Navegación", "column_subheading.settings": "Axustes", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.hashtag_warning": "Esta mensaxe non será listada baixo ningunha etiqueta xa que está marcada como non listada. Só os toots públicos poden buscarse por etiquetas.", "compose_form.lock_disclaimer": "A súa conta non está {locked}. Calquera pode seguila para ver as súas mensaxes só-para-seguidoras.", "compose_form.lock_disclaimer.lock": "bloqueado", "compose_form.placeholder": "A qué andas?", @@ -139,8 +143,9 @@ "loading_indicator.label": "Cargando...", "media_gallery.toggle_visible": "Dar visibilidade", "missing_indicator.label": "Non atopado", + "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Esconder notificacións deste usuario?", - "navigation_bar.blocks": "Usuarios bloqueados", + "navigation_bar.blocks": "Usuarias bloqueadas", "navigation_bar.community_timeline": "Liña temporal local", "navigation_bar.edit_profile": "Editar perfil", "navigation_bar.favourites": "Favoritas", @@ -156,7 +161,7 @@ "notification.favourite": "{name} marcou como favorito o seu estado", "notification.follow": "{name} está a seguila", "notification.mention": "{name} mencionoute", - "notification.reblog": "{name} promocionou o seu estado", + "notification.reblog": "{name} promoveu o seu estado", "notifications.clear": "Limpar notificacións", "notifications.clear_confirmation": "Estás seguro de que queres limpar permanentemente todas as túas notificacións?", "notifications.column_settings.alert": "Notificacións de escritorio", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "A liña de tempo local mostra as publicacións das persoas que segues.", "onboarding.page_four.notifications": "A columna de notificacións mostra cando alguén interactúa contigo.", "onboarding.page_one.federation": "Mastodon é unha rede de servidores independentes que se unen para facer unha rede social máis grande. Chamamos instancias a estes servidores.", - "onboarding.page_one.handle": "Estás en {domain}, polo que o teu nome de usuario completo é {handle}", + "onboarding.page_one.full_handle": "O seu alcume completo", + "onboarding.page_one.handle_hint": "Esto é o que lle debe dicir a quen queira seguila.", "onboarding.page_one.welcome": "Benvido a Mastodon!", "onboarding.page_six.admin": "O administrador da túa instancia é {admin}.", "onboarding.page_six.almost_done": "Case feito...", @@ -197,21 +203,30 @@ "privacy.public.short": "Pública", "privacy.unlisted.long": "Non publicar en liñas temporais públicas", "privacy.unlisted.short": "Non listada", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", "relative_time.just_now": "agora", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "reply_indicator.cancel": "Cancelar", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Comentarios adicionais", "report.submit": "Enviar", "report.target": "Informar {target}", "search.placeholder": "Buscar", "search_popout.search_format": "Formato de busca avanzada", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "etiqueta", "search_popout.tips.status": "estado", "search_popout.tips.text": "Texto simple devolve coincidencias con nomes públicos, nomes de usuaria e etiquetas", "search_popout.tips.user": "usuaria", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count,plural,one {result} outros {results}}", "standalone.public_title": "Ollada dentro...", "status.block": "Block @{name}", @@ -223,10 +238,11 @@ "status.media_hidden": "Medios ocultos", "status.mention": "Mencionar @{name}", "status.more": "Máis", - "status.mute": "Mute @{name}", + "status.mute": "Acalar @{name}", "status.mute_conversation": "Acalar conversa", "status.open": "Expandir este estado", "status.pin": "Fixar no perfil", + "status.pinned": "Pinned toot", "status.reblog": "Promover", "status.reblogged_by": "{name} promoveu", "status.reply": "Resposta", @@ -239,7 +255,6 @@ "status.show_more": "Mostrar máis", "status.unmute_conversation": "Non acalar a conversa", "status.unpin": "Despegar do perfil", - "tabs_bar.compose": "Compoñer", "tabs_bar.federated_timeline": "Federado", "tabs_bar.home": "Inicio", "tabs_bar.local_timeline": "Local", @@ -248,6 +263,7 @@ "upload_area.title": "Arrastre e solte para subir", "upload_button.label": "Engadir medios", "upload_form.description": "Describa para deficientes visuais", + "upload_form.focus": "Crop", "upload_form.undo": "Desfacer", "upload_progress.label": "Subindo...", "video.close": "Pechar video", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 2eb186173..b31976c42 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -1,7 +1,9 @@ { "account.block": "חסימת @{name}", "account.block_domain": "להסתיר הכל מהקהילה {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "המידע להלן עשוי להיות לא עדכני או לא שלם.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "עריכת פרופיל", "account.follow": "מעקב", "account.followers": "עוקבים", @@ -13,7 +15,9 @@ "account.moved_to": "החשבון {name} הועבר אל:", "account.mute": "להשתיק את @{name}", "account.mute_notifications": "להסתיר התראות מאת @{name}", + "account.muted": "Muted", "account.posts": "הודעות", + "account.posts_with_replies": "Toots with replies", "account.report": "לדווח על @{name}", "account.requested": "בהמתנה לאישור", "account.share": "לשתף את אודות @{name}", @@ -139,6 +143,7 @@ "loading_indicator.label": "טוען...", "media_gallery.toggle_visible": "נראה\\בלתי נראה", "missing_indicator.label": "לא נמצא", + "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "להסתיר הודעות מחשבון זה?", "navigation_bar.blocks": "חסימות", "navigation_bar.community_timeline": "ציר זמן מקומי", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "ציר זמן הבית מראה הודעות מהנעקבים שלך.", "onboarding.page_four.notifications": "טור ההתראות מראה כשמישהו מתייחס להודעות שלך.", "onboarding.page_one.federation": "מסטודון היא רשת של שרתים עצמאיים מצורפים ביחד לכדי רשת חברתית אחת גדולה. אנחנו מכנים את השרתים האלו קהילות.", - "onboarding.page_one.handle": "אתם בקהילה {domain}, ולכן מזהה המשתמש המלא שלכם הוא {handle}", + "onboarding.page_one.full_handle": "Your full handle", + "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.", "onboarding.page_one.welcome": "ברוכים הבאים למסטודון!", "onboarding.page_six.admin": "הקהילה מנוהלת בידי {admin}.", "onboarding.page_six.almost_done": "כמעט סיימנו...", @@ -197,21 +203,30 @@ "privacy.public.short": "פומבי", "privacy.unlisted.long": "לא יופיע בפידים הציבוריים המשותפים", "privacy.unlisted.short": "לא לפיד הכללי", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", "relative_time.just_now": "כרגע", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "reply_indicator.cancel": "ביטול", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "הערות נוספות", "report.submit": "שליחה", "report.target": "דיווח", "search.placeholder": "חיפוש", "search_popout.search_format": "מבנה חיפוש מתקדם", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "האשתג", "search_popout.tips.status": "status", "search_popout.tips.text": "טקסט פשוט מחזיר כינויים, שמות משתמש והאשתגים", "search_popout.tips.user": "משתמש(ת)", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {תוצאה} other {תוצאות}}", "standalone.public_title": "הצצה פנימה...", "status.block": "Block @{name}", @@ -227,6 +242,7 @@ "status.mute_conversation": "השתקת שיחה", "status.open": "הרחבת הודעה", "status.pin": "לקבע באודות", + "status.pinned": "Pinned toot", "status.reblog": "הדהוד", "status.reblogged_by": "הודהד על ידי {name}", "status.reply": "תגובה", @@ -239,7 +255,6 @@ "status.show_more": "הראה יותר", "status.unmute_conversation": "הסרת השתקת שיחה", "status.unpin": "לשחרר מקיבוע באודות", - "tabs_bar.compose": "חיבור", "tabs_bar.federated_timeline": "ציר זמן בין-קהילתי", "tabs_bar.home": "בבית", "tabs_bar.local_timeline": "ציר זמן מקומי", @@ -248,6 +263,7 @@ "upload_area.title": "ניתן להעלות על ידי Drag & drop", "upload_button.label": "הוספת מדיה", "upload_form.description": "תיאור לכבדי ראיה", + "upload_form.focus": "Crop", "upload_form.undo": "ביטול", "upload_progress.label": "עולה...", "video.close": "סגירת וידאו", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index 00dea67f7..d176a5df6 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -1,7 +1,9 @@ { "account.block": "Blokiraj @{name}", "account.block_domain": "Sakrij sve sa {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "Ovaj korisnik je sa druge instance. Ovaj broj bi mogao biti veći.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Uredi profil", "account.follow": "Slijedi", "account.followers": "Sljedbenici", @@ -13,7 +15,9 @@ "account.moved_to": "{name} has moved to:", "account.mute": "Utišaj @{name}", "account.mute_notifications": "Mute notifications from @{name}", + "account.muted": "Muted", "account.posts": "Postovi", + "account.posts_with_replies": "Toots with replies", "account.report": "Prijavi @{name}", "account.requested": "Čeka pristanak", "account.share": "Share @{name}'s profile", @@ -139,6 +143,7 @@ "loading_indicator.label": "Učitavam...", "media_gallery.toggle_visible": "Preklopi vidljivost", "missing_indicator.label": "Nije nađen", + "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Blokirani korisnici", "navigation_bar.community_timeline": "Lokalni timeline", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "The home timeline prikazuje postove ljudi koje slijediš.", "onboarding.page_four.notifications": "Stupac za notifikacije pokazuje poruke drugih upućene tebi.", "onboarding.page_one.federation": "Mastodon čini mreža neovisnih servera udruženih u jednu veću socialnu mrežu. Te servere nazivamo instancama.", - "onboarding.page_one.handle": "Ti si na {domain}, i tvoja puna handle je {handle}", + "onboarding.page_one.full_handle": "Your full handle", + "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.", "onboarding.page_one.welcome": "Dobro došli na Mastodon!", "onboarding.page_six.admin": "Administrator tvoje instance je {admin}.", "onboarding.page_six.almost_done": "Još malo pa gotovo...", @@ -197,21 +203,30 @@ "privacy.public.short": "Javno", "privacy.unlisted.long": "Ne prikazuj u javnim timelineovima", "privacy.unlisted.short": "Unlisted", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", "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": "Otkaži", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Dodatni komentari", "report.submit": "Pošalji", "report.target": "Prijavljivanje", "search.placeholder": "Traži", "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "status", "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "standalone.public_title": "A look inside...", "status.block": "Block @{name}", @@ -227,6 +242,7 @@ "status.mute_conversation": "Utišaj razgovor", "status.open": "Proširi ovaj status", "status.pin": "Pin on profile", + "status.pinned": "Pinned toot", "status.reblog": "Podigni", "status.reblogged_by": "{name} je podigao", "status.reply": "Odgovori", @@ -239,7 +255,6 @@ "status.show_more": "Pokaži više", "status.unmute_conversation": "Poništi utišavanje razgovora", "status.unpin": "Unpin from profile", - "tabs_bar.compose": "Sastavi", "tabs_bar.federated_timeline": "Federalni", "tabs_bar.home": "Dom", "tabs_bar.local_timeline": "Lokalno", @@ -248,6 +263,7 @@ "upload_area.title": "Povuci i spusti kako bi uploadao", "upload_button.label": "Dodaj media", "upload_form.description": "Describe for the visually impaired", + "upload_form.focus": "Crop", "upload_form.undo": "Poništi", "upload_progress.label": "Uploadam...", "video.close": "Close video", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index e1048519b..a4d2091ef 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -1,253 +1,269 @@ { - "account.block": "Blokkolás", - "account.block_domain": "Hide everything from {domain}", - "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", + "account.block": "@{name} letiltása", + "account.block_domain": "Minden elrejtése innen: {domain}", + "account.blocked": "Blocked", + "account.disclaimer_full": "Az alul található információk hiányosan mutathatják be a felhasználót.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Profil szerkesztése", "account.follow": "Követés", "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.hide_reblogs": "Rejtsd el a tülkölést @{name}-tól/től", + "account.media": "Média", + "account.mention": "@{name} említése", + "account.moved_to": "{name} átköltözött:", + "account.mute": "@{name} némítása", + "account.mute_notifications": "@{name} értesítések némítása", + "account.muted": "Muted", + "account.posts": "Státuszok", + "account.posts_with_replies": "Toots with replies", + "account.report": "@{name} jelentése", + "account.requested": "Engedélyre vár. Kattintson a követési kérés visszavonására", + "account.share": "@{name} profiljának megosztása", + "account.show_reblogs": "@{name} kedvenceinek mutatása", + "account.unblock": "@{name} kiblokkolása", + "account.unblock_domain": "{domain} mutatása", "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.", - "bundle_column_error.retry": "Try again", - "bundle_column_error.title": "Network error", - "bundle_modal_error.close": "Close", - "bundle_modal_error.message": "Something went wrong while loading this component.", - "bundle_modal_error.retry": "Try again", - "column.blocks": "Blocked users", - "column.community": "Local timeline", - "column.favourites": "Favourites", - "column.follow_requests": "Follow requests", + "account.unmute": "@{name} kinémítása", + "account.unmute_notifications": "@{name} értesítéseinek kinémítása", + "account.view_full_profile": "Teljes profil megtekintése", + "boost_modal.combo": "Megnyomhatod {combo}, hogy átugord következő alkalommal", + "bundle_column_error.body": "Hiba történt a komponens betöltése közben.", + "bundle_column_error.retry": "Próbálja újra", + "bundle_column_error.title": "Hálózati hiba", + "bundle_modal_error.close": "Bezár", + "bundle_modal_error.message": "Hiba történt a komponens betöltésekor.", + "bundle_modal_error.retry": "Próbálja újra", + "column.blocks": "Letiltott felhasználók", + "column.community": "Helyi idővonal", + "column.favourites": "Kedvencek", + "column.follow_requests": "Követési kérések", "column.home": "Kezdőlap", - "column.lists": "Lists", - "column.mutes": "Muted users", + "column.lists": "Listák", + "column.mutes": "Némított felhasználók", "column.notifications": "Értesítések", - "column.pins": "Pinned toot", - "column.public": "Nyilvános", + "column.pins": "Kitűzött tülkölések", + "column.public": "Nyilvános idővonal", "column_back_button.label": "Vissza", - "column_header.hide_settings": "Hide settings", - "column_header.moveLeft_settings": "Move column to the left", - "column_header.moveRight_settings": "Move column to the right", - "column_header.pin": "Pin", - "column_header.show_settings": "Show settings", - "column_header.unpin": "Unpin", - "column_subheading.navigation": "Navigation", - "column_subheading.settings": "Settings", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", - "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", - "compose_form.lock_disclaimer.lock": "locked", + "column_header.hide_settings": "Beállítások elrejtése", + "column_header.moveLeft_settings": "Oszlop elmozdítása balra", + "column_header.moveRight_settings": "oszlop elmozdítása jobbra", + "column_header.pin": "Kitűz", + "column_header.show_settings": "Beállítások mutatása", + "column_header.unpin": "Kitűzés eltávolítása", + "column_subheading.navigation": "Navigáció", + "column_subheading.settings": "Beállítások", + "compose_form.hashtag_warning": "Ezen tülkölés nem fog megjelenni semmilyen hashtag alatt mivel listázatlan. Csak a publikus tülkölések kereshetőek hashtag-el.", + "compose_form.lock_disclaimer": "Az ön fiókja nincs {locked}. Bárki követni tud, hogy megtekintse a kizárt követőknek szánt üzeneteid.", + "compose_form.lock_disclaimer.lock": "lezárva", "compose_form.placeholder": "Mire gondolsz?", - "compose_form.publish": "Tülk!", + "compose_form.publish": "Tülk", "compose_form.publish_loud": "{publish}!", "compose_form.sensitive": "Tartalom érzékenynek jelölése", - "compose_form.spoiler": "Hide text behind warning", - "compose_form.spoiler_placeholder": "Content warning", - "confirmation_modal.cancel": "Cancel", - "confirmations.block.confirm": "Block", - "confirmations.block.message": "Are you sure you want to block {name}?", - "confirmations.delete.confirm": "Delete", - "confirmations.delete.message": "Are you sure you want to delete this status?", - "confirmations.delete_list.confirm": "Delete", - "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", - "confirmations.domain_block.confirm": "Hide entire domain", - "confirmations.domain_block.message": "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.", - "confirmations.mute.confirm": "Mute", - "confirmations.mute.message": "Are you sure you want to mute {name}?", - "confirmations.unfollow.confirm": "Unfollow", - "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", - "embed.instructions": "Embed this status on your website by copying the code below.", - "embed.preview": "Here is what it will look like:", - "emoji_button.activity": "Activity", - "emoji_button.custom": "Custom", - "emoji_button.flags": "Flags", - "emoji_button.food": "Food & Drink", - "emoji_button.label": "Insert emoji", - "emoji_button.nature": "Nature", - "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", - "emoji_button.objects": "Objects", - "emoji_button.people": "People", - "emoji_button.recent": "Frequently used", - "emoji_button.search": "Search...", - "emoji_button.search_results": "Search results", - "emoji_button.symbols": "Symbols", - "emoji_button.travel": "Travel & Places", - "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", - "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", - "follow_request.reject": "Reject", - "getting_started.appsshort": "Apps", - "getting_started.faq": "FAQ", + "compose_form.spoiler": "Szöveg figyelmeztetés mögé rejtése", + "compose_form.spoiler_placeholder": "Figyelmeztetését írja ide", + "confirmation_modal.cancel": "Bezár", + "confirmations.block.confirm": "Letilt", + "confirmations.block.message": "Biztos benne, hogy le szeretné tiltani {name}?", + "confirmations.delete.confirm": "Töröl", + "confirmations.delete.message": "Biztos benne, hogy törölni szeretné ezt a státuszt?", + "confirmations.delete_list.confirm": "Töröl", + "confirmations.delete_list.message": "Biztos benne, hogy véglegesen törölni szeretné ezt a listát?", + "confirmations.domain_block.confirm": "Egész domain elrejtése", + "confirmations.domain_block.message": "Nagyon biztos abban, hogy le szeretné tiltani az egész {domain}-t? A legtöbb esetben néhány célszerű tiltás vagy némítás elegendő és kívánatosabb megoldás.", + "confirmations.mute.confirm": "Némít", + "confirmations.mute.message": "Biztos benne, hogy némítani szeretné {name}?", + "confirmations.unfollow.confirm": "Követés visszavonása", + "confirmations.unfollow.message": "Biztos benne, hogy vissza szeretné vonni {name} követését?", + "embed.instructions": "Ágyazza be ezen státuszt weboldalába az alábbi kód másolásával.", + "embed.preview": "Így fog kinézni:", + "emoji_button.activity": "Aktivitás", + "emoji_button.custom": "Egyéni", + "emoji_button.flags": "Zászlók", + "emoji_button.food": "Étel és Ital", + "emoji_button.label": "Emoji beszúrása", + "emoji_button.nature": "Természet", + "emoji_button.not_found": "Nincsenek emojok!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Tárgyak", + "emoji_button.people": "Emberek", + "emoji_button.recent": "Gyakran használt", + "emoji_button.search": "Keresés...", + "emoji_button.search_results": "Keresési találatok", + "emoji_button.symbols": "Szimbólumok", + "emoji_button.travel": "Utazás és Helyek", + "empty_column.community": "A helyi idővonal üres. Írj egy publikus stástuszt, hogy elindítsd a labdát!", + "empty_column.hashtag": "Jelenleg nem található semmi ezen hashtaggel.", + "empty_column.home": "A hazai idővonala üres! Látogasd meg a {public} vagy használd a keresőt, hogy ismerj meg más felhasználókat.", + "empty_column.home.public_timeline": "publikus idővonal", + "empty_column.list": "A lista jelenleg üres. Mikor a listatagok új státuszt posztolnak itt meg fognak jelenni.", + "empty_column.notifications": "Jelenleg nincsenek értesítései. Lépj kapcsolatba másokkal, hogy indítsd el a beszélgetést.", + "empty_column.public": "Jelenleg semmi nincs itt! Írj valamit publikusan vagy kövess más szervereken levő felhasználókat, hogy megtöltsd", + "follow_request.authorize": "Engedélyez", + "follow_request.reject": "Visszautasít", + "getting_started.appsshort": "Applikációk", + "getting_started.faq": "GYIK", "getting_started.heading": "Első lépések", - "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.", - "getting_started.userguide": "User Guide", - "home.column_settings.advanced": "Advanced", - "home.column_settings.basic": "Basic", - "home.column_settings.filter_regex": "Filter out by regular expressions", - "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", + "getting_started.open_source_notice": "Mastodon egy nyílt forráskódú szoftver. Hozzájárulás vagy problémák jelentése a GitHub-on {github}.", + "getting_started.userguide": "Használati Útmutató", + "home.column_settings.advanced": "Fejlett", + "home.column_settings.basic": "Alap", + "home.column_settings.filter_regex": "Szűrje ki reguláris kifejezésekkel", + "home.column_settings.show_reblogs": "Ismétlések mutatása", + "home.column_settings.show_replies": "Válaszok mutatása", + "home.settings": "Oszlop beállításai", + "keyboard_shortcuts.back": "vissza navigálás", + "keyboard_shortcuts.boost": "ismétlés", + "keyboard_shortcuts.column": "összpontosítson egy státuszra az egyik oszlopban", + "keyboard_shortcuts.compose": "fókuszálja a szerkesztési szövegdobozt", + "keyboard_shortcuts.description": "Leírás", + "keyboard_shortcuts.down": "lefele navigálás a listában", + "keyboard_shortcuts.enter": "státusz megnyitása", + "keyboard_shortcuts.favourite": "kedvenccé tétel", + "keyboard_shortcuts.heading": "Billentyű rövidítések", + "keyboard_shortcuts.hotkey": "Gyorsbillentyű", + "keyboard_shortcuts.legend": "jelmagyarázat megjelenítése", + "keyboard_shortcuts.mention": "szerző megjelenítése", + "keyboard_shortcuts.reply": "válaszolás", + "keyboard_shortcuts.search": "kereső kiemelése", + "keyboard_shortcuts.toot": "új tülk megkezdése", + "keyboard_shortcuts.unfocus": "tülk szerkesztés/keresés fókuszpontból való kivétele", + "keyboard_shortcuts.up": "fennebb helyezés a listában", "lightbox.close": "Bezárás", - "lightbox.next": "Next", - "lightbox.previous": "Previous", - "lists.account.add": "Add to list", - "lists.account.remove": "Remove from list", - "lists.delete": "Delete list", - "lists.edit": "Edit list", - "lists.new.create": "Add list", - "lists.new.title_placeholder": "New list title", - "lists.search": "Search among people you follow", - "lists.subheading": "Your lists", + "lightbox.next": "Következő", + "lightbox.previous": "Előző", + "lists.account.add": "Hozzáadás a listához", + "lists.account.remove": "Eltávolít a listából", + "lists.delete": "Lista törlése", + "lists.edit": "Lista szerkesztése", + "lists.new.create": "Lista hozzáadása", + "lists.new.title_placeholder": "Új lista cím", + "lists.search": "Keresés a követtett személyek között", + "lists.subheading": "Listáid", "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", + "media_gallery.toggle_visible": "Láthatóság váltása", + "missing_indicator.label": "Nincs találat", + "missing_indicator.sublabel": "Ezen forrás nem található", + "mute_modal.hide_notifications": "Értesítések elrejtése ezen felhasználótól?", + "navigation_bar.blocks": "Tiltott felhasználók", + "navigation_bar.community_timeline": "Helyi idővonal", "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.lists": "Lists", + "navigation_bar.favourites": "Kedvencek", + "navigation_bar.follow_requests": "Követési kérések", + "navigation_bar.info": "Ezen szerverről", + "navigation_bar.keyboard_shortcuts": "Gyorsbillentyűk", + "navigation_bar.lists": "Listák", "navigation_bar.logout": "Kijelentkezés", - "navigation_bar.mutes": "Muted users", - "navigation_bar.pins": "Pinned toots", + "navigation_bar.mutes": "Némított felhasználók", + "navigation_bar.pins": "Kitűzött tülkök", "navigation_bar.preferences": "Beállítások", "navigation_bar.public_timeline": "Nyilvános időfolyam", "notification.favourite": "{name} kedvencnek jelölte az állapotod", "notification.follow": "{name} követ téged", "notification.mention": "{name} megemlített", - "notification.reblog": "{name} reblogolta az állapotod", - "notifications.clear": "Clear notifications", - "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", - "notifications.column_settings.alert": "Desktop notifications", - "notifications.column_settings.favourite": "Favourites:", - "notifications.column_settings.follow": "New followers:", - "notifications.column_settings.mention": "Mentions:", - "notifications.column_settings.push": "Push notifications", - "notifications.column_settings.push_meta": "This device", - "notifications.column_settings.reblog": "Boosts:", - "notifications.column_settings.show": "Show in column", - "notifications.column_settings.sound": "Play sound", - "onboarding.done": "Done", - "onboarding.next": "Next", - "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", - "onboarding.page_four.home": "The home timeline shows posts from people you follow.", - "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", - "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", - "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", - "onboarding.page_one.welcome": "Welcome to Mastodon!", - "onboarding.page_six.admin": "Your instance's admin is {admin}.", - "onboarding.page_six.almost_done": "Almost done...", - "onboarding.page_six.appetoot": "Bon Appetoot!", - "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", - "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", - "onboarding.page_six.guidelines": "community guidelines", - "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", - "onboarding.page_six.various_app": "mobile apps", - "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", + "notification.reblog": "{name} rebloggolta az állapotod", + "notifications.clear": "Értesítések törlése", + "notifications.clear_confirmation": "Biztos benne, hogy véglegesen törölni akarja az összes értesítését?", + "notifications.column_settings.alert": "Asztali gépi értesítések", + "notifications.column_settings.favourite": "Kedvencek:", + "notifications.column_settings.follow": "Új követők:", + "notifications.column_settings.mention": "Megemítéseim:", + "notifications.column_settings.push": "Push értesítések", + "notifications.column_settings.push_meta": "Ezen eszköz", + "notifications.column_settings.reblog": "Rebloggolások:", + "notifications.column_settings.show": "Oszlopban mutatás", + "notifications.column_settings.sound": "Hang lejátszása", + "onboarding.done": "Befejezve", + "onboarding.next": "Következő", + "onboarding.page_five.public_timelines": "A helyi idővonal mindenkinek a publikus posztját mutatja a(z) {domain}-n. A federált idővonal mindenki publikus posztját mutatja akit {domain} felhasználói követnek. Ezek a publikus idővonalak, nagyszerű mód új emberek megismerésére.", + "onboarding.page_four.home": "A hazai idővonal azon emberek posztjait mutatja akiket te követsz.", + "onboarding.page_four.notifications": "Az értesítések oszlop más felhasználók interakcióját veled tükrözi.", + "onboarding.page_one.federation": "Mastodon egy független szerverekből alkotott hálózat melyek együttműködése egy nagy szociális hálót képez. Ezeket a szervereket instanciáknak hívjuk.", + "onboarding.page_one.full_handle": "Teljes elérhetőséged", + "onboarding.page_one.handle_hint": "Ez az amit a barátaidnak mondasz ha meg akarnak keresni.", + "onboarding.page_one.welcome": "Üdvözölünk a Mastodon-on!", + "onboarding.page_six.admin": "Az instanciád adminisztrátora {admin}.", + "onboarding.page_six.almost_done": "Majdnem megvan...", + "onboarding.page_six.appetoot": "Bon Appetülk!", + "onboarding.page_six.apps_available": "Vannak {apps} iOS-re, Androidra és más platformokra is.", + "onboarding.page_six.github": "Mastodon egy szabad és nyílt-forráskódú szoftver. Jelentheted a bug-okat, kérhetsz új funkcionalitásokat vagy hozzájárulhatsz a kódhoz {github}-on.", + "onboarding.page_six.guidelines": "közösségi útmutató", + "onboarding.page_six.read_guidelines": "Kérjük olvassa el a(z) {domain}-nak a {guidelines}ját!", + "onboarding.page_six.various_app": "alkalmazások", + "onboarding.page_three.profile": "Módosítsa a profilját, hogy megváltoztassa az avatárt, bio-t vagy nevet. Ott megtalálja a többi beállítást is.", "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", - "onboarding.skip": "Skip", - "privacy.change": "Adjust status privacy", - "privacy.direct.long": "Post to mentioned users only", - "privacy.direct.short": "Direct", - "privacy.private.long": "Post to followers only", - "privacy.private.short": "Followers-only", - "privacy.public.long": "Post to public timelines", - "privacy.public.short": "Public", + "onboarding.skip": "Átugrás", + "privacy.change": "Státusz láthatóságának módosítása", + "privacy.direct.long": "Posztolás csak az említett felhasználóknak", + "privacy.direct.short": "Egyenesen", + "privacy.private.long": "Posztolás csak követőknek", + "privacy.private.short": "Csak követőknek", + "privacy.public.long": "Posztolás a publikus idővonalakra", + "privacy.public.short": "Publikus", "privacy.unlisted.long": "Do not show in public timelines", - "privacy.unlisted.short": "Unlisted", + "privacy.unlisted.short": "Listázatlan", + "regeneration_indicator.label": "Töltődik…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", - "relative_time.just_now": "now", + "relative_time.just_now": "most", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "reply_indicator.cancel": "Mégsem", - "report.placeholder": "Additional comments", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", + "report.placeholder": "További kommentek", "report.submit": "Submit", "report.target": "Reporting", "search.placeholder": "Keresés", - "search_popout.search_format": "Advanced search format", + "search_popout.search_format": "Fejlett keresés", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "status", "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", - "search_popout.tips.user": "user", + "search_popout.tips.user": "felhasználó", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", - "standalone.public_title": "A look inside...", + "standalone.public_title": "Betekintés...", "status.block": "Block @{name}", - "status.cannot_reblog": "This post cannot be boosted", + "status.cannot_reblog": "Ezen státusz nem rebloggolható", "status.delete": "Törlés", - "status.embed": "Embed", + "status.embed": "Beágyaz", "status.favourite": "Kedvenc", - "status.load_more": "Load more", - "status.media_hidden": "Media hidden", + "status.load_more": "Többet", + "status.media_hidden": "Média elrejtve", "status.mention": "Említés", - "status.more": "More", - "status.mute": "Mute @{name}", - "status.mute_conversation": "Mute conversation", - "status.open": "Expand this status", - "status.pin": "Pin on profile", + "status.more": "Többet", + "status.mute": "@{name} némítása", + "status.mute_conversation": "Beszélgetés némítása", + "status.open": "Státusz kinagyítása", + "status.pin": "Kitűzés a profilra", + "status.pinned": "Pinned toot", "status.reblog": "Reblog", "status.reblogged_by": "{name} reblogolta", "status.reply": "Válasz", - "status.replyAll": "Reply to thread", + "status.replyAll": "Válaszolj a beszélgetésre", "status.report": "Report @{name}", "status.sensitive_toggle": "Katt a megtekintéshez", "status.sensitive_warning": "Érzékeny tartalom", - "status.share": "Share", - "status.show_less": "Show less", - "status.show_more": "Show more", - "status.unmute_conversation": "Unmute conversation", - "status.unpin": "Unpin from profile", - "tabs_bar.compose": "Összeállítás", - "tabs_bar.federated_timeline": "Federated", + "status.share": "Megosztás", + "status.show_less": "Kevesebb", + "status.show_more": "Többet", + "status.unmute_conversation": "Beszélgetés némításának elvonása", + "status.unpin": "Kitűzés eltávolítása a profilról", + "tabs_bar.federated_timeline": "Federált", "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", + "tabs_bar.notifications": "Értesítések", + "ui.beforeunload": "A piszkozata el fog vesztődni ha elhagyja Mastodon-t.", + "upload_area.title": "Húzza ide a feltöltéshez", "upload_button.label": "Média hozzáadása", "upload_form.description": "Describe for the visually impaired", + "upload_form.focus": "Crop", "upload_form.undo": "Mégsem", "upload_progress.label": "Uploading...", "video.close": "Close video", @@ -256,7 +272,7 @@ "video.fullscreen": "Full screen", "video.hide": "Hide video", "video.mute": "Mute sound", - "video.pause": "Pause", - "video.play": "Play", - "video.unmute": "Unmute sound" + "video.pause": "Szünet", + "video.play": "Lejátszás", + "video.unmute": "Hang kinémítása" } diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json new file mode 100644 index 000000000..b5e9a2b5a --- /dev/null +++ b/app/javascript/mastodon/locales/hy.json @@ -0,0 +1,278 @@ +{ + "account.block": "Արգելափակել @{name}֊ին", + "account.block_domain": "Թաքցնել ամենը հետեւյալ տիրույթից՝ {domain}", + "account.blocked": "Blocked", + "account.disclaimer_full": "Ներքոհիշյալը կարող է ոչ ամբողջությամբ արտացոլել օգտատիրոջ էջի տվյալները։", + "account.domain_blocked": "Domain hidden", + "account.edit_profile": "Խմբագրել անձնական էջը", + "account.follow": "Հետեւել", + "account.followers": "Հետեւվողներ", + "account.follows": "Հետեւում է", + "account.follows_you": "Հետեւում է քեզ", + "account.hide_reblogs": "Թաքցնել @{name}֊ի տարածածները", + "account.media": "Մեդիա", + "account.mention": "Նշել @{name}֊ին", + "account.moved_to": "{name}֊ը տեղափոխվել է՝", + "account.mute": "Լռեցնել @{name}֊ին", + "account.mute_notifications": "Անջատել ծանուցումները @{name}֊ից", + "account.muted": "Muted", + "account.posts": "Գրառումներ", + "account.posts_with_replies": "Toots with replies", + "account.report": "Բողոքել @{name}֊ից", + "account.requested": "Հաստատման կարիք ունի։ Սեղմիր՝ հետեւելու հայցը չեղարկելու համար։", + "account.share": "Կիսվել @{name}֊ի էջով", + "account.show_reblogs": "Ցուցադրել @{name}֊ի տարածածները", + "account.unblock": "Ապաարգելափակել @{name}֊ին", + "account.unblock_domain": "Ցուցադրել {domain} թաքցված տիրույթի գրառումները", + "account.unfollow": "Չհետեւել", + "account.unmute": "Ապալռեցնել @{name}֊ին", + "account.unmute_notifications": "Միացնել ծանուցումները @{name}֊ից", + "account.view_full_profile": "Դիտել ամբողջական տարբերակը։", + "boost_modal.combo": "Կարող ես սեղմել {combo}՝ սա հաջորդ անգամ բաց թողնելու համար", + "bundle_column_error.body": "Այս բաղադրիչը բեռնելու ընթացքում ինչ֊որ բան խափանվեց։", + "bundle_column_error.retry": "Կրկին փորձել", + "bundle_column_error.title": "Ցանցային սխալ", + "bundle_modal_error.close": "Փակել", + "bundle_modal_error.message": "Այս բաղադրիչը բեռնելու ընթացքում ինչ֊որ բան խափանվեց։", + "bundle_modal_error.retry": "Կրկին փորձել", + "column.blocks": "Արգելափակված օգտատերեր", + "column.community": "Տեղական հոսք", + "column.favourites": "Հավանածներ", + "column.follow_requests": "Հետեւելու հայցեր", + "column.home": "Հիմնական", + "column.lists": "Ցանկեր", + "column.mutes": "Լռեցրած օգտատերեր", + "column.notifications": "Ծանուցումներ", + "column.pins": "Ամրացված թթեր", + "column.public": "Դաշնային հոսք", + "column_back_button.label": "Ետ", + "column_header.hide_settings": "Թաքցնել կարգավորումները", + "column_header.moveLeft_settings": "Տեղաշարժել սյունը ձախ", + "column_header.moveRight_settings": "Տեղաշարժել սյունը աջ", + "column_header.pin": "Ամրացնել", + "column_header.show_settings": "Ցուցադրել կարգավորումները", + "column_header.unpin": "Հանել", + "column_subheading.navigation": "Նավարկություն", + "column_subheading.settings": "Կարգավորումներ", + "compose_form.hashtag_warning": "Այս թութը չի հաշվառվի որեւէ պիտակի տակ, քանզի այն ծածուկ է։ Միայն հրապարակային թթերը հնարավոր է որոնել պիտակներով։", + "compose_form.lock_disclaimer": "Քո հաշիվը {locked} չէ։ Յուրաքանչյուր ոք կարող է հետեւել քեզ եւ տեսնել միայն հետեւողների համար նախատեսված գրառումները։", + "compose_form.lock_disclaimer.lock": "փակ", + "compose_form.placeholder": "Ի՞նչ կա մտքիդ", + "compose_form.publish": "Թթել", + "compose_form.publish_loud": "Թթե՜լ", + "compose_form.sensitive": "Նշել բովանդակությունը որպես կասկածելի", + "compose_form.spoiler": "Թաքցնել տեքստը նախազգուշացման ետեւում", + "compose_form.spoiler_placeholder": "Գրիր նախազգուշացումդ այստեղ", + "confirmation_modal.cancel": "Չեղարկել", + "confirmations.block.confirm": "Արգելափակել", + "confirmations.block.message": "Վստա՞հ ես, որ ուզում ես արգելափակել {name}֊ին։", + "confirmations.delete.confirm": "Ջնջել", + "confirmations.delete.message": "Վստա՞հ ես, որ ուզում ես ջնջել այս թութը։", + "confirmations.delete_list.confirm": "Ջնջել", + "confirmations.delete_list.message": "Վստա՞հ ես, որ ուզում ես մշտապես ջնջել այս ցանկը։", + "confirmations.domain_block.confirm": "Թաքցնել ամբողջ տիրույթը", + "confirmations.domain_block.message": "Հաստատ֊հաստա՞տ վստահ ես, որ ուզում ես արգելափակել ամբողջ {domain} տիրույթը։ Սովորաբար մի երկու թիրախավորված արգելափակում կամ լռեցում բավական է ու նախընտրելի։", + "confirmations.mute.confirm": "Լռեցնել", + "confirmations.mute.message": "Վստա՞հ ես, որ ուզում ես {name}֊ին լռեցնել։", + "confirmations.unfollow.confirm": "Ապահետեւել", + "confirmations.unfollow.message": "Վստա՞հ ես, որ ուզում ես այլեւս չհետեւել {name}֊ին։", + "embed.instructions": "Այս թութը քո կայքում ներդնելու համար կարող ես պատճենել ներքոհիշյալ կոդը։", + "embed.preview": "Ահա, թե ինչ տեսք կունենա այն՝", + "emoji_button.activity": "Զբաղմունքներ", + "emoji_button.custom": "Հատուկ", + "emoji_button.flags": "Դրոշներ", + "emoji_button.food": "Կերուխում", + "emoji_button.label": "Էմոջի ավելացնել", + "emoji_button.nature": "Բնություն", + "emoji_button.not_found": "Նման էմոջիներ դեռ չեն հայտնաբերվել։ (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Առարկաներ", + "emoji_button.people": "Մարդիկ", + "emoji_button.recent": "Հաճախ օգտագործվող", + "emoji_button.search": "Որոնել…", + "emoji_button.search_results": "Որոնման արդյունքներ", + "emoji_button.symbols": "Նշաններ", + "emoji_button.travel": "Ուղեւորություն եւ տեղանքներ", + "empty_column.community": "Տեղական հոսքը դատա՛րկ է։ Հրապարակային մի բան գրիր շարժիչը խոդ տալու համար։", + "empty_column.hashtag": "Այս պիտակով դեռ ոչինչ չկա։", + "empty_column.home": "Քո հիմնական հոսքը դատա՛րկ է։ Այցելի՛ր {public}ը կամ օգտվիր որոնումից՝ այլ մարդկանց հանդիպելու համար։", + "empty_column.home.public_timeline": "հրապարակային հոսք", + "empty_column.list": "Այս ցանկում դեռ ոչինչ չկա։ Երբ ցանկի անդամներից որեւէ մեկը նոր թութ գրի, այն կհայտնվի այստեղ։", + "empty_column.notifications": "Ոչ մի ծանուցում դեռ չունես։ Բզիր մյուսներին՝ խոսակցությունը սկսելու համար։", + "empty_column.public": "Այստեղ բան չկա՛։ Հրապարակային մի բան գրիր կամ հետեւիր այլ հանգույցներից էակների՝ այն լցնելու համար։", + "follow_request.authorize": "Վավերացնել", + "follow_request.reject": "Մերժել", + "getting_started.appsshort": "Հավելվածներ", + "getting_started.faq": "ՀՏՀ", + "getting_started.heading": "Ինչպես սկսել", + "getting_started.open_source_notice": "Մաստոդոնը բաց ելատեքստով ծրագրակազմ է։ Կարող ես ներդրում անել կամ վրեպներ զեկուցել ԳիթՀաբում՝ {github}։", + "getting_started.userguide": "Ձեռնարկ", + "home.column_settings.advanced": "Առաջադեմ", + "home.column_settings.basic": "Հիմնական", + "home.column_settings.filter_regex": "Զտել օրինաչափ արտահայտությամբ", + "home.column_settings.show_reblogs": "Ցուցադրել տարածածները", + "home.column_settings.show_replies": "Ցուցադրել պատասխանները", + "home.settings": "Սյան կարգավորումներ", + "keyboard_shortcuts.back": "ետ նավարկելու համար", + "keyboard_shortcuts.boost": "տարածելու համար", + "keyboard_shortcuts.column": "սյուներից մեկի վրա սեւեռվելու համար", + "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": "Նախորդ", + "lists.account.add": "Ավելացնել ցանկին", + "lists.account.remove": "Հանել ցանկից", + "lists.delete": "Ջնջել ցանկը", + "lists.edit": "Փոփոխել ցանկը", + "lists.new.create": "Ավելացնել ցանկ", + "lists.new.title_placeholder": "Նոր ցանկի վերնագիր", + "lists.search": "Փնտրել քո հետեւած մարդկանց մեջ", + "lists.subheading": "Քո ցանկերը", + "loading_indicator.label": "Բեռնվում է…", + "media_gallery.toggle_visible": "Ցուցադրել/թաքցնել", + "missing_indicator.label": "Չգտնվեց", + "missing_indicator.sublabel": "This resource could not be found", + "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.lists": "Ցանկեր", + "navigation_bar.logout": "Դուրս գալ", + "navigation_bar.mutes": "Լռեցրած օգտատերեր", + "navigation_bar.pins": "Ամրացված թթեր", + "navigation_bar.preferences": "Նախապատվություններ", + "navigation_bar.public_timeline": "Դաշնային հոսք", + "notification.favourite": "{name} հավանեց թութդ", + "notification.follow": "{name} սկսեց հետեւել քեզ", + "notification.mention": "{name} նշեց քեզ", + "notification.reblog": "{name} տարածեց թութդ", + "notifications.clear": "Մաքրել ծանուցումները", + "notifications.clear_confirmation": "Վստա՞հ ես, որ ուզում ես մշտապես մաքրել քո բոլոր ծանուցումները։", + "notifications.column_settings.alert": "Աշխատատիրույթի ծանուցումներ", + "notifications.column_settings.favourite": "Հավանածներից՝", + "notifications.column_settings.follow": "Նոր հետեւողներ՝", + "notifications.column_settings.mention": "Նշումներ՝", + "notifications.column_settings.push": "Հրելու ծանուցումներ", + "notifications.column_settings.push_meta": "Այս սարքը", + "notifications.column_settings.reblog": "Տարածածներից՝", + "notifications.column_settings.show": "Ցուցադրել սյունում", + "notifications.column_settings.sound": "Ձայն հանել", + "onboarding.done": "Պատրաստ է", + "onboarding.next": "Հաջորդ", + "onboarding.page_five.public_timelines": "Տեղական հոսքը ցույց է տալիս {domain} տիրույթից բոլորի հրապարակային թթերը։ Դաշնային հոսքը ցույց է տալիս հրապարակային թթերը բոլորից, ում {domain} տիրույթի մարդիկ հետեւում են։ Սրանք Հրապարակային հոսքերն են՝ նոր մարդկանց բացահայտելու հրաշալի միջոց։", + "onboarding.page_four.home": "Հիմնական հոսքը ցույց է տալիս այն մարդկանց թթերը, ում հետեւում ես։", + "onboarding.page_four.notifications": "Ծանուցումների սյունը ցույց է տալիս, երբ որեւէ մեկը փոխգործակցում է հետդ։", + "onboarding.page_one.federation": "Մաստոդոնը անկախ սպասարկիչների ցանց է, որոնք միասնական սոցիալական ցանց են կազմում։ Մենք կոչում ենք այդ սպասարկիչները հանգույցներ։", + "onboarding.page_one.full_handle": "Քո ամբողջական օգտանունը", + "onboarding.page_one.handle_hint": "Սա այն է, ինչ ասելու ես ընկերներիդ՝ քեզ փնտրելու համար։", + "onboarding.page_one.welcome": "Բարի գալուստ Մաստոդո՜ն", + "onboarding.page_six.admin": "Քո հանգույցի ադմինը նա է՝ {admin}։", + "onboarding.page_six.almost_done": "Գրեթե պատրաստ է…", + "onboarding.page_six.appetoot": "Հաջողութությո՜ւն", + "onboarding.page_six.apps_available": "Նաեւ կան այՕՍի, Անդրոիդի եւ այլ հարթակների համար {apps}։", + "onboarding.page_six.github": "Մաստոդոնն ազատ ու բաց ելատեքստով ծրագրակազմ է։ Կարող ես վրեպներ զեկուցել, նոր հատկություններ հայցել կամ ներդրում անել {github}֊ում։", + "onboarding.page_six.guidelines": "համայնքի կանոնակարգ", + "onboarding.page_six.read_guidelines": "Խնդրում ենք, կարդա {domain} տիրույթի {guidelines}ը։", + "onboarding.page_six.various_app": "հավելվածներ", + "onboarding.page_three.profile": "Թարմացրու անձնական էջդ՝ նկարդ, կենսագրությունդ ու անունդ փոխելու համար։ Այնտեղ նաեւ այլ նախապատվություններ կգտնես։", + "onboarding.page_three.search": "Օգտվիր որոնման դաշտից՝ մարդկանց գտնելու կամ պիտակներին՝ օրինակ {illustration} ու {introductions}, ծանոթանալու համար։ Ոչ այս հանգույցի բնակիչներին փնտրելու համար օգտագործիր նրանց ամբողջական օգտանունը։", + "onboarding.page_two.compose": "Գրիր թթերդ շարադրման սյունակում։ Կարող ես նկարներ վերբեռնել, փոփոխել գաղտնիության կարգավորումները եւ բովանդակության վերաբերյալ նախազգուշացումներ ավելացնել՝ օգտվելով ներքեւի պատկերակներից։", + "onboarding.skip": "Բաց թողնել", + "privacy.change": "Կարգավորել թթի գաղտնիությունը", + "privacy.direct.long": "Թթել միայն նշված օգտատերերի համար", + "privacy.direct.short": "Հասցեագրված", + "privacy.private.long": "Թթել միայն հետեւողների համար", + "privacy.private.short": "Միայն հետեւողներին", + "privacy.public.long": "Թթել հրապարակային հոսքերում", + "privacy.public.short": "Հրապարակային", + "privacy.unlisted.long": "Չթթել հրապարակային հոսքերում", + "privacy.unlisted.short": "Ծածուկ", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", + "relative_time.days": "{number}օր", + "relative_time.hours": "{number}ժ", + "relative_time.just_now": "նոր", + "relative_time.minutes": "{number}ր", + "relative_time.seconds": "{number}վ", + "reply_indicator.cancel": "Չեղարկել", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", + "report.placeholder": "Լրացուցիչ մեկնաբանություններ", + "report.submit": "Ուղարկել", + "report.target": "Բողոքել {target}֊ի մասին", + "search.placeholder": "Փնտրել", + "search_popout.search_format": "Փնտրելու առաջադեմ ձեւ", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.tips.hashtag": "պիտակ", + "search_popout.tips.status": "թութ", + "search_popout.tips.text": "Հասարակ տեքստը կվերադարձնի համընկնող անուններ, օգտանուններ ու պիտակներ", + "search_popout.tips.user": "օգտատեր", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "standalone.public_title": "Այս պահին…", + "status.block": "Արգելափակել @{name}֊ին", + "status.cannot_reblog": "Այս թութը չի կարող տարածվել", + "status.delete": "Ջնջել", + "status.embed": "Ներդնել", + "status.favourite": "Հավանել", + "status.load_more": "Բեռնել ավելին", + "status.media_hidden": "մեդիաբովանդակությունը թաքցված է", + "status.mention": "Նշել @{name}֊ին", + "status.more": "Ավելին", + "status.mute": "Լռեցնել @{name}֊ին", + "status.mute_conversation": "Լռեցնել խոսակցությունը", + "status.open": "Ընդարձակել այս թութը", + "status.pin": "Ամրացնել անձնական էջում", + "status.pinned": "Pinned toot", + "status.reblog": "Տարածել", + "status.reblogged_by": "{name} տարածել է", + "status.reply": "Պատասխանել", + "status.replyAll": "Պատասխանել թելին", + "status.report": "Բողոքել @{name}֊ից", + "status.sensitive_toggle": "Կտացրու՝ դիտելու համար", + "status.sensitive_warning": "Կասկածելի բովանդակություն", + "status.share": "Կիսվել", + "status.show_less": "Պակաս", + "status.show_more": "Ավելին", + "status.unmute_conversation": "Ապալռեցնել խոսակցությունը", + "status.unpin": "Հանել անձնական էջից", + "tabs_bar.federated_timeline": "Դաշնային", + "tabs_bar.home": "Հիմնական", + "tabs_bar.local_timeline": "Տեղական", + "tabs_bar.notifications": "Ծանուցումներ", + "ui.beforeunload": "Քո սեւագիրը կկորի, եթե լքես Մաստոդոնը։", + "upload_area.title": "Քաշիր ու նետիր՝ վերբեռնելու համար", + "upload_button.label": "Ավելացնել մեդիա", + "upload_form.description": "Նկարագրություն ավելացրու տեսողական խնդիրներ ունեցողների համար", + "upload_form.focus": "Crop", + "upload_form.undo": "Հետարկել", + "upload_progress.label": "Վերբեռնվում է…", + "video.close": "Փակել տեսագրությունը", + "video.exit_fullscreen": "Անջատել լիաէկրան դիտումը", + "video.expand": "Ընդարձակել տեսագրությունը", + "video.fullscreen": "Լիաէկրան", + "video.hide": "Թաքցնել տեսագրությունը", + "video.mute": "Լռեցնել ձայնը", + "video.pause": "Դադար տալ", + "video.play": "Նվագել", + "video.unmute": "Միացնել ձայնը" +} diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index 0942bc33c..596415cde 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -1,7 +1,9 @@ { "account.block": "Blokir @{name}", "account.block_domain": "Hide everything from {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Ubah profil", "account.follow": "Ikuti", "account.followers": "Pengikut", @@ -13,7 +15,9 @@ "account.moved_to": "{name} has moved to:", "account.mute": "Bisukan @{name}", "account.mute_notifications": "Mute notifications from @{name}", + "account.muted": "Muted", "account.posts": "Postingan", + "account.posts_with_replies": "Toots with replies", "account.report": "Laporkan @{name}", "account.requested": "Menunggu persetujuan", "account.share": "Share @{name}'s profile", @@ -139,6 +143,7 @@ "loading_indicator.label": "Tunggu sebentar...", "media_gallery.toggle_visible": "Tampil/Sembunyikan", "missing_indicator.label": "Tidak ditemukan", + "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Pengguna diblokir", "navigation_bar.community_timeline": "Linimasa lokal", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "Linimasa beranda menampilkan postingan dari orang-orang yang anda ikuti.", "onboarding.page_four.notifications": "Kolom notifikasi menampilkan ketika seseorang berinteraksi dengan anda.", "onboarding.page_one.federation": "Mastodon adalah jaringan dari beberapa server independen yang bergabung untuk membuat jejaring sosial yang besar.", - "onboarding.page_one.handle": "Ada berada dalam {domain}, jadi nama user lengkap anda adalah {handle}", + "onboarding.page_one.full_handle": "Your full handle", + "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.", "onboarding.page_one.welcome": "Selamat datang di Mastodon!", "onboarding.page_six.admin": "Admin serveer anda adalah {admin}.", "onboarding.page_six.almost_done": "Hampir selesei...", @@ -197,21 +203,30 @@ "privacy.public.short": "Publik", "privacy.unlisted.long": "Tidak ditampilkan di linimasa publik", "privacy.unlisted.short": "Tak Terdaftar", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", "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": "Batal", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Komentar tambahan", "report.submit": "Kirim", "report.target": "Melaporkan", "search.placeholder": "Pencarian", "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "status", "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count} {count, plural, one {hasil} other {hasil}}", "standalone.public_title": "A look inside...", "status.block": "Block @{name}", @@ -227,6 +242,7 @@ "status.mute_conversation": "Mute conversation", "status.open": "Tampilkan status ini", "status.pin": "Pin on profile", + "status.pinned": "Pinned toot", "status.reblog": "Boost", "status.reblogged_by": "di-boost {name}", "status.reply": "Balas", @@ -239,7 +255,6 @@ "status.show_more": "Tampilkan semua", "status.unmute_conversation": "Unmute conversation", "status.unpin": "Unpin from profile", - "tabs_bar.compose": "Tulis", "tabs_bar.federated_timeline": "Gabungan", "tabs_bar.home": "Beranda", "tabs_bar.local_timeline": "Lokal", @@ -248,6 +263,7 @@ "upload_area.title": "Seret & lepaskan untuk mengunggah", "upload_button.label": "Tambahkan media", "upload_form.description": "Describe for the visually impaired", + "upload_form.focus": "Crop", "upload_form.undo": "Undo", "upload_progress.label": "Mengunggah...", "video.close": "Close video", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index cfd8e299f..4f554b08f 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -1,7 +1,9 @@ { "account.block": "Blokusar @{name}", "account.block_domain": "Hide everything from {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Modifikar profilo", "account.follow": "Sequar", "account.followers": "Sequanti", @@ -13,7 +15,9 @@ "account.moved_to": "{name} has moved to:", "account.mute": "Celar @{name}", "account.mute_notifications": "Mute notifications from @{name}", + "account.muted": "Muted", "account.posts": "Mesaji", + "account.posts_with_replies": "Toots with replies", "account.report": "Denuncar @{name}", "account.requested": "Vartante aprobo", "account.share": "Share @{name}'s profile", @@ -139,6 +143,7 @@ "loading_indicator.label": "Kargante...", "media_gallery.toggle_visible": "Chanjar videbleso", "missing_indicator.label": "Ne trovita", + "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Blokusita uzeri", "navigation_bar.community_timeline": "Lokala tempolineo", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "The home timeline shows posts from people you follow.", "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", - "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.full_handle": "Your full handle", + "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.", "onboarding.page_one.welcome": "Welcome to Mastodon!", "onboarding.page_six.admin": "Your instance's admin is {admin}.", "onboarding.page_six.almost_done": "Almost done...", @@ -197,21 +203,30 @@ "privacy.public.short": "Publike", "privacy.unlisted.long": "Ne montrar en publika tempolinei", "privacy.unlisted.short": "Ne enlistigota", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", "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": "Nihiligar", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Plusa komenti", "report.submit": "Sendar", "report.target": "Denuncante", "search.placeholder": "Serchez", "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "status", "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {rezulto} other {rezulti}}", "standalone.public_title": "A look inside...", "status.block": "Block @{name}", @@ -227,6 +242,7 @@ "status.mute_conversation": "Mute conversation", "status.open": "Detaligar ca mesajo", "status.pin": "Pin on profile", + "status.pinned": "Pinned toot", "status.reblog": "Repetar", "status.reblogged_by": "{name} repetita", "status.reply": "Respondar", @@ -239,7 +255,6 @@ "status.show_more": "Montrar plue", "status.unmute_conversation": "Unmute conversation", "status.unpin": "Unpin from profile", - "tabs_bar.compose": "Kompozar", "tabs_bar.federated_timeline": "Federata", "tabs_bar.home": "Hemo", "tabs_bar.local_timeline": "Lokala", @@ -248,6 +263,7 @@ "upload_area.title": "Tranar faligar por kargar", "upload_button.label": "Adjuntar kontenajo", "upload_form.description": "Describe for the visually impaired", + "upload_form.focus": "Crop", "upload_form.undo": "Desfacar", "upload_progress.label": "Kargante...", "video.close": "Close video", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index e14fa410c..6b2532512 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -1,7 +1,9 @@ { "account.block": "Blocca @{name}", "account.block_domain": "Hide everything from {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Modifica profilo", "account.follow": "Segui", "account.followers": "Seguaci", @@ -13,7 +15,9 @@ "account.moved_to": "{name} has moved to:", "account.mute": "Silenzia @{name}", "account.mute_notifications": "Mute notifications from @{name}", + "account.muted": "Muted", "account.posts": "Posts", + "account.posts_with_replies": "Toots with replies", "account.report": "Segnala @{name}", "account.requested": "In attesa di approvazione", "account.share": "Share @{name}'s profile", @@ -139,6 +143,7 @@ "loading_indicator.label": "Carico...", "media_gallery.toggle_visible": "Imposta visibilità", "missing_indicator.label": "Non trovato", + "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Utenti bloccati", "navigation_bar.community_timeline": "Timeline locale", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "The home timeline shows posts from people you follow.", "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", - "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.full_handle": "Your full handle", + "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.", "onboarding.page_one.welcome": "Welcome to Mastodon!", "onboarding.page_six.admin": "Your instance's admin is {admin}.", "onboarding.page_six.almost_done": "Almost done...", @@ -197,21 +203,30 @@ "privacy.public.short": "Pubblico", "privacy.unlisted.long": "Non mostrare sulla timeline pubblica", "privacy.unlisted.short": "Non elencato", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", "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": "Annulla", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Commenti aggiuntivi", "report.submit": "Invia", "report.target": "Invio la segnalazione", "search.placeholder": "Cerca", "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "status", "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count} {count, plural, one {risultato} other {risultati}}", "standalone.public_title": "A look inside...", "status.block": "Block @{name}", @@ -227,6 +242,7 @@ "status.mute_conversation": "Mute conversation", "status.open": "Espandi questo post", "status.pin": "Pin on profile", + "status.pinned": "Pinned toot", "status.reblog": "Condividi", "status.reblogged_by": "{name} ha condiviso", "status.reply": "Rispondi", @@ -239,7 +255,6 @@ "status.show_more": "Mostra di più", "status.unmute_conversation": "Unmute conversation", "status.unpin": "Unpin from profile", - "tabs_bar.compose": "Scrivi", "tabs_bar.federated_timeline": "Federazione", "tabs_bar.home": "Home", "tabs_bar.local_timeline": "Locale", @@ -248,6 +263,7 @@ "upload_area.title": "Trascina per caricare", "upload_button.label": "Aggiungi file multimediale", "upload_form.description": "Describe for the visually impaired", + "upload_form.focus": "Crop", "upload_form.undo": "Annulla", "upload_progress.label": "Sto caricando...", "video.close": "Close video", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 19885056c..04374abc3 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -1,7 +1,9 @@ { - "account.block": "ブロック", + "account.block": "@{name}さんをブロック", "account.block_domain": "{domain}全体を非表示", + "account.blocked": "ブロック済み", "account.disclaimer_full": "以下の情報は不正確な可能性があります。", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "プロフィールを編集", "account.follow": "フォロー", "account.followers": "フォロワー", @@ -9,22 +11,24 @@ "account.follows_you": "フォローされています", "account.hide_reblogs": "@{name}さんからのブーストを非表示", "account.media": "メディア", - "account.mention": "返信", + "account.mention": "@{name}さんにトゥート", "account.moved_to": "{name}さんは引っ越しました:", - "account.mute": "ミュート", - "account.mute_notifications": "@{name}さんからの通知を受け取る", + "account.mute": "@{name}さんをミュート", + "account.mute_notifications": "@{name}さんからの通知を受け取らない", + "account.muted": "ミュート済み", "account.posts": "投稿", - "account.report": "通報", - "account.requested": "承認待ち", + "account.posts_with_replies": "投稿と返信", + "account.report": "@{name}さんを通報", + "account.requested": "フォロー承認待ちです。クリックしてキャンセル", "account.share": "@{name}さんのプロフィールを共有する", "account.show_reblogs": "@{name}さんからのブーストを表示", - "account.unblock": "ブロック解除", + "account.unblock": "@{name}さんのブロックを解除", "account.unblock_domain": "{domain}を表示", "account.unfollow": "フォロー解除", - "account.unmute": "ミュート解除", - "account.unmute_notifications": "@{name}さんからの通知を受け取らない", + "account.unmute": "@{name}さんのミュートを解除", + "account.unmute_notifications": "@{name}さんからの通知を受け取る", "account.view_full_profile": "全ての情報を見る", - "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます", + "boost_modal.combo": "次からは{combo}を押せばスキップできます", "bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。", "bundle_column_error.retry": "再試行", "bundle_column_error.title": "ネットワークエラー", @@ -69,13 +73,13 @@ "confirmations.delete.confirm": "削除", "confirmations.delete.message": "本当に削除しますか?", "confirmations.delete_list.confirm": "削除", - "confirmations.delete_list.message": "本当に削除しますか?", + "confirmations.delete_list.message": "本当にこのリストを完全に削除しますか?", "confirmations.domain_block.confirm": "ドメイン全体を非表示", - "confirmations.domain_block.message": "本当に{domain}全体を非表示にしますか? 多くの場合は個別にブロックやミュートするだけで充分であり、また好ましいです。", + "confirmations.domain_block.message": "本当に{domain}全体を非表示にしますか? 多くの場合は個別にブロックやミュートするだけで充分であり、また好ましいです。", "confirmations.mute.confirm": "ミュート", "confirmations.mute.message": "本当に{name}さんをミュートしますか?", "confirmations.unfollow.confirm": "フォロー解除", - "confirmations.unfollow.message": "本当に{name}さんをフォロー解除しますか?", + "confirmations.unfollow.message": "本当に{name}さんのフォローを解除しますか?", "embed.instructions": "下記のコードをコピーしてウェブサイトに埋め込みます。", "embed.preview": "表示例:", "emoji_button.activity": "活動", @@ -84,7 +88,7 @@ "emoji_button.food": "食べ物", "emoji_button.label": "絵文字を追加", "emoji_button.nature": "自然", - "emoji_button.not_found": "絵文字がない!! (╯°□°)╯︵ ┻━┻", + "emoji_button.not_found": "絵文字がなーい!! (╯°□°)╯︵ ┻━┻", "emoji_button.objects": "物", "emoji_button.people": "人々", "emoji_button.recent": "よく使う絵文字", @@ -107,7 +111,7 @@ "getting_started.heading": "スタート", "getting_started.open_source_notice": "Mastodonはオープンソースソフトウェアです。誰でもGitHub({github})から開発に参加したり、問題を報告したりできます。", "getting_started.userguide": "ユーザーガイド", - "home.column_settings.advanced": "上級者向け", + "home.column_settings.advanced": "高度な設定", "home.column_settings.basic": "基本設定", "home.column_settings.filter_regex": "正規表現でフィルター", "home.column_settings.show_reblogs": "ブースト表示", @@ -144,6 +148,7 @@ "loading_indicator.label": "読み込み中...", "media_gallery.toggle_visible": "表示切り替え", "missing_indicator.label": "見つかりません", + "missing_indicator.sublabel": "見つかりませんでした", "mute_modal.hide_notifications": "このユーザーからの通知を隠しますか?", "navigation_bar.blocks": "ブロックしたユーザー", "navigation_bar.community_timeline": "ローカルタイムライン", @@ -156,7 +161,7 @@ "navigation_bar.lists": "リスト", "navigation_bar.logout": "ログアウト", "navigation_bar.mutes": "ミュートしたユーザー", - "navigation_bar.pins": "固定されたトゥート", + "navigation_bar.pins": "固定したトゥート", "navigation_bar.preferences": "ユーザー設定", "navigation_bar.public_timeline": "連合タイムライン", "navigation_bar.misc": "その他", @@ -181,7 +186,8 @@ "onboarding.page_four.home": "「ホーム」タイムラインではあなたがフォローしている人の投稿を表示します。", "onboarding.page_four.notifications": "「通知」ではあなたへの他の人からの関わりを表示します。", "onboarding.page_one.federation": "Mastodonは誰でも参加できるSNSです。", - "onboarding.page_one.handle": "今あなたは数あるMastodonインスタンスの1つである{domain}にいます。あなたのフルハンドルは{handle}です", + "onboarding.page_one.full_handle": "あなたのフルハンドル", + "onboarding.page_one.handle_hint": "あなたを探している友達に伝えるといいでしょう。", "onboarding.page_one.welcome": "Mastodonへようこそ!", "onboarding.page_six.admin": "あなたのインスタンスの管理者は{admin}です。", "onboarding.page_six.almost_done": "以上です。", @@ -189,11 +195,11 @@ "onboarding.page_six.apps_available": "iOS、Androidあるいは他のプラットフォームで使える{apps}があります。", "onboarding.page_six.github": "MastodonはOSSです。バグ報告や機能要望あるいは貢献を{github}から行なえます。", "onboarding.page_six.guidelines": "コミュニティガイドライン", - "onboarding.page_six.read_guidelines": "{guidelines}を読むことを忘れないようにしてください!", - "onboarding.page_six.various_app": "様々なモバイルアプリ", + "onboarding.page_six.read_guidelines": "{domain}の{guidelines}を読むことを忘れないようにしてください!", + "onboarding.page_six.various_app": "モバイルアプリ", "onboarding.page_three.profile": "「プロフィールを編集」から、あなたの自己紹介や表示名を変更できます。またそこでは他の設定ができます。", "onboarding.page_three.search": "検索バーで、{illustration}や{introductions}のように特定のハッシュタグの投稿を見たり、ユーザーを探したりできます。", - "onboarding.page_two.compose": "フォームから投稿できます。イメージや、公開範囲の設定や、表示時の警告の設定は下部のアイコンから行なえます。", + "onboarding.page_two.compose": "フォームから投稿できます。イメージや、公開範囲の設定や、表示時の警告の設定は下部のアイコンから行えます。", "onboarding.skip": "スキップ", "privacy.change": "投稿のプライバシーを変更", "privacy.direct.long": "メンションしたユーザーだけに公開", @@ -204,41 +210,51 @@ "privacy.public.short": "公開", "privacy.unlisted.long": "公開TLで表示しない", "privacy.unlisted.short": "未収載", + "regeneration_indicator.label": "読み込み中…", + "regeneration_indicator.sublabel": "ホームタイムラインは準備中です!", "relative_time.days": "{number}日前", "relative_time.hours": "{number}時間前", "relative_time.just_now": "今", "relative_time.minutes": "{number}分前", "relative_time.seconds": "{number}秒前", "reply_indicator.cancel": "キャンセル", - "report.placeholder": "コメント", + "report.forward": "{target} に転送する", + "report.forward_hint": "このアカウントは別のインスタンスに所属しています。通報内容を匿名で転送しますか?", + "report.hint": "通報内容はあなたのインスタンスのモデレーターへ送信されます。通報理由を入力してください。:", + "report.placeholder": "追加コメント", "report.submit": "通報する", - "report.target": "{target} を通報する", + "report.target": "{target}さんを通報する", "search.placeholder": "検索", "search_popout.search_format": "高度な検索フォーマット", + "search_popout.tips.full_text": "表示名やユーザー名、ハッシュタグのほか、あなたのトゥートやお気に入り、ブーストしたトゥート、返信に一致する単純なテキスト。", "search_popout.tips.hashtag": "ハッシュタグ", "search_popout.tips.status": "トゥート", "search_popout.tips.text": "表示名やユーザー名、ハッシュタグに一致する単純なテキスト", "search_popout.tips.user": "ユーザー", + "search_results.accounts": "人々", + "search_results.hashtags": "ハッシュタグ", + "search_results.statuses": "トゥート", "search_results.total": "{count, number}件の結果", "standalone.public_title": "今こんな話をしています...", - "status.block": "@{name}をブロック", + "status.block": "@{name}さんをブロック", "status.cannot_reblog": "この投稿はブーストできません", "status.delete": "削除", "status.embed": "埋め込み", "status.favourite": "お気に入り", "status.load_more": "もっと見る", "status.media_hidden": "非表示のメディア", - "status.mention": "返信", + "status.mention": "@{name}さんにトゥート", "status.more": "もっと見る", - "status.mute": "@{name}をミュート", + "status.mute": "@{name}さんをミュート", "status.mute_conversation": "会話をミュート", "status.open": "詳細を表示", "status.pin": "プロフィールに固定表示", + "status.pinned": "固定されたトゥート", "status.reblog": "ブースト", - "status.reblogged_by": "{name}さんにブーストされました", + "status.reblogged_by": "{name}さんがブースト", "status.reply": "返信", "status.replyAll": "全員に返信", - "status.report": "通報", + "status.report": "@{name}さんを通報", "status.sensitive_toggle": "クリックして表示", "status.sensitive_warning": "閲覧注意", "status.share": "共有", @@ -246,15 +262,15 @@ "status.show_more": "もっと見る", "status.unmute_conversation": "会話のミュートを解除", "status.unpin": "プロフィールの固定表示を解除", - "tabs_bar.compose": "投稿", "tabs_bar.federated_timeline": "連合", "tabs_bar.home": "ホーム", "tabs_bar.local_timeline": "ローカル", "tabs_bar.notifications": "通知", - "ui.beforeunload": "Mastodonから離れるとあなたのドラフトは失われます。", + "ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。", "upload_area.title": "ドラッグ&ドロップでアップロード", "upload_button.label": "メディアを追加", "upload_form.description": "視覚障害者のための説明", + "upload_form.focus": "焦点", "upload_form.undo": "やり直す", "upload_progress.label": "アップロード中...", "video.close": "動画を閉じる", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 31e5e377e..05c84fd37 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -1,7 +1,9 @@ { - "account.block": "차단", + "account.block": "@{name}을 차단", "account.block_domain": "{domain} 전체를 숨김", + "account.blocked": "차단 됨", "account.disclaimer_full": "여기 있는 정보는 유저의 프로파일을 정확히 반영하지 못 할 수도 있습니다.", + "account.domain_blocked": "도메인 숨겨짐", "account.edit_profile": "프로필 편집", "account.follow": "팔로우", "account.followers": "팔로워", @@ -11,11 +13,13 @@ "account.media": "미디어", "account.mention": "답장", "account.moved_to": "{name}는 계정을 이동했습니다:", - "account.mute": "뮤트", + "account.mute": "@{name} 뮤트", "account.mute_notifications": "@{name}의 알림을 뮤트", - "account.posts": "포스트", - "account.report": "신고", - "account.requested": "승인 대기 중", + "account.muted": "뮤트 됨", + "account.posts": "게시물", + "account.posts_with_replies": "툿과 답장", + "account.report": "@{name} 신고", + "account.requested": "승인 대기 중. 클릭해서 취소하기", "account.share": "@{name}의 프로파일 공유", "account.show_reblogs": "@{name}의 부스트 보기", "account.unblock": "차단 해제", @@ -139,6 +143,7 @@ "loading_indicator.label": "불러오는 중...", "media_gallery.toggle_visible": "표시 전환", "missing_indicator.label": "찾을 수 없습니다", + "missing_indicator.sublabel": "이 리소스를 찾을 수 없었습니다", "mute_modal.hide_notifications": "이 사용자로부터의 알림을 뮤트하시겠습니까?", "navigation_bar.blocks": "차단한 사용자", "navigation_bar.community_timeline": "로컬 타임라인", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "홈 타임라인에서는 내가 팔로우 중인 사람들의 포스트를 표시합니다.", "onboarding.page_four.notifications": "알림에서는 다른 사람들과의 연결을 표시합니다.", "onboarding.page_one.federation": "Mastodon은 누구나 참가할 수 있는 SNS입니다.", - "onboarding.page_one.handle": "여러분은 지금 수많은 Mastodon 인스턴스 중 하나인 {domain}에 있습니다. 당신의 유저 이름은 {handle} 입니다.", + "onboarding.page_one.full_handle": "당신의 풀 핸들", + "onboarding.page_one.handle_hint": "이것을 검색하여 친구들이 당신을 찾을 수 있습니다.", "onboarding.page_one.welcome": "Mastodon에 어서 오세요!", "onboarding.page_six.admin": "이 인스턴스의 관리자는 {admin}입니다.", "onboarding.page_six.almost_done": "이상입니다.", @@ -197,21 +203,30 @@ "privacy.public.short": "공개", "privacy.unlisted.long": "공개 타임라인에 표시하지 않음", "privacy.unlisted.short": "타임라인에 비표시", + "regeneration_indicator.label": "불러오는 중…", + "regeneration_indicator.sublabel": "당신의 홈 피드가 준비되는 중입니다!", "relative_time.days": "{number}일 전", "relative_time.hours": "{number}시간 전", "relative_time.just_now": "방금", "relative_time.minutes": "{number}분 전", "relative_time.seconds": "{number}초 전", "reply_indicator.cancel": "취소", + "report.forward": "{target}에 포워드 됨", + "report.forward_hint": "이 계정은 다른 서버에 있습니다. 익명화 된 사본을 해당 서버에도 전송할까요?", + "report.hint": "신고는 당신의 서버 스태프에게 전송 됩니다. 왜 이 계정을 신고하는 지에 대한 설명을 아래에 작성할 수 있습니다:", "report.placeholder": "코멘트", "report.submit": "신고하기", "report.target": "문제가 된 사용자", "search.placeholder": "검색", "search_popout.search_format": "고급 검색 방법", + "search_popout.tips.full_text": "단순한 텍스트 검색은 당신이 작성했거나, 관심글로 지정했거나, 부스트했거나, 멘션을 받은 게시글, 그리고 유저네임, 디스플레이네임, 해시태그를 반환합니다.", "search_popout.tips.hashtag": "해시태그", "search_popout.tips.status": "툿", "search_popout.tips.text": "단순한 텍스트 검색은 관계된 프로필 이름, 유저 이름 그리고 해시태그를 표시합니다", "search_popout.tips.user": "유저", + "search_results.accounts": "사람", + "search_results.hashtags": "해시태그", + "search_results.statuses": "툿", "search_results.total": "{count, number}건의 결과", "standalone.public_title": "지금 이런 이야기를 하고 있습니다…", "status.block": "@{name} 차단", @@ -227,6 +242,7 @@ "status.mute_conversation": "이 대화를 뮤트", "status.open": "상세 정보 표시", "status.pin": "고정", + "status.pinned": "고정 된 툿", "status.reblog": "부스트", "status.reblogged_by": "{name}님이 부스트 했습니다", "status.reply": "답장", @@ -239,7 +255,6 @@ "status.show_more": "더 보기", "status.unmute_conversation": "이 대화의 뮤트 해제하기", "status.unpin": "고정 해제", - "tabs_bar.compose": "포스트", "tabs_bar.federated_timeline": "연합", "tabs_bar.home": "홈", "tabs_bar.local_timeline": "로컬", @@ -248,6 +263,7 @@ "upload_area.title": "드래그 & 드롭으로 업로드", "upload_button.label": "미디어 추가", "upload_form.description": "시각장애인을 위한 설명", + "upload_form.focus": "크롭", "upload_form.undo": "재시도", "upload_progress.label": "업로드 중...", "video.close": "동영상 닫기", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 6dc7292f1..509382670 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -1,7 +1,9 @@ { "account.block": "Blokkeer @{name}", "account.block_domain": "Negeer alles van {domain}", + "account.blocked": "Geblokkeerd", "account.disclaimer_full": "De informatie hieronder kan mogelijk een incompleet beeld geven van dit gebruikersprofiel.", + "account.domain_blocked": "Domein verborgen", "account.edit_profile": "Profiel bewerken", "account.follow": "Volgen", "account.followers": "Volgers", @@ -13,7 +15,9 @@ "account.moved_to": "{name} is verhuisd naar:", "account.mute": "Negeer @{name}", "account.mute_notifications": "Negeer meldingen van @{name}", + "account.muted": "Genegeerd", "account.posts": "Toots", + "account.posts_with_replies": "Toots met reacties", "account.report": "Rapporteer @{name}", "account.requested": "Wacht op goedkeuring. Klik om het volgverzoek te annuleren", "account.share": "Profiel van @{name} delen", @@ -50,7 +54,7 @@ "column_header.unpin": "Losmaken", "column_subheading.navigation": "Navigatie", "column_subheading.settings": "Instellingen", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.hashtag_warning": "Deze toot valt niet onder een hashtag te bekijken, omdat deze niet op openbare tijdlijnen wordt getoond. Alleen openbare toots kunnen via hashtags gevonden worden.", "compose_form.lock_disclaimer": "Jouw account is niet {locked}. Iedereen kan jou volgen en toots zien die je alleen aan volgers hebt gericht.", "compose_form.lock_disclaimer.lock": "besloten", "compose_form.placeholder": "Wat wil je kwijt?", @@ -139,6 +143,7 @@ "loading_indicator.label": "Laden…", "media_gallery.toggle_visible": "Media wel/niet tonen", "missing_indicator.label": "Niet gevonden", + "missing_indicator.sublabel": "Deze hulpbron kan niet gevonden worden", "mute_modal.hide_notifications": "Verberg meldingen van deze persoon?", "navigation_bar.blocks": "Geblokkeerde gebruikers", "navigation_bar.community_timeline": "Lokale tijdlijn", @@ -170,13 +175,14 @@ "notifications.column_settings.sound": "Geluid afspelen", "onboarding.done": "Klaar", "onboarding.next": "Volgende", - "onboarding.page_five.public_timelines": "De lokale tijdlijn toont openbare toots van iedereen op {domain}. De globale tijdlijn toont openbare toots van iedereen die door gebruikers van {domain} worden gevolgd, dus ook mensen van andere Mastodon-servers. Dit zijn de openbare tijdlijnen en vormen een uitstekende manier om nieuwe mensen te ontdekken.", + "onboarding.page_five.public_timelines": "De lokale tijdlijn toont openbare toots van iedereen op {domain}. De globale tijdlijn toont openbare toots van iedereen die door gebruikers van {domain} worden gevolgd, dus ook mensen van andere Mastodonservers. Dit zijn de openbare tijdlijnen en vormen een uitstekende manier om nieuwe mensen te ontdekken.", "onboarding.page_four.home": "Deze tijdlijn laat toots zien van mensen die jij volgt.", - "onboarding.page_four.notifications": "De kolom met meldingen toont alle interacties die je met andere Mastodon-gebruikers hebt.", + "onboarding.page_four.notifications": "De kolom met meldingen toont alle interacties die je met andere Mastodongebruikers hebt.", "onboarding.page_one.federation": "Mastodon is een netwerk van onafhankelijke servers die samen een groot sociaal netwerk vormen.", - "onboarding.page_one.handle": "Je bevindt je nu op {domain}, dus is jouw volledige Mastodon-adres {handle}", + "onboarding.page_one.full_handle": "Jouw volledige Mastodonadres", + "onboarding.page_one.handle_hint": "Dit is waarmee jouw vrienden je kunnen vinden.", "onboarding.page_one.welcome": "Welkom op Mastodon!", - "onboarding.page_six.admin": "De beheerder van jouw Mastodon-server is {admin}.", + "onboarding.page_six.admin": "De beheerder van jouw Mastodonserver is {admin}.", "onboarding.page_six.almost_done": "Bijna klaar...", "onboarding.page_six.appetoot": "Veel succes!", "onboarding.page_six.apps_available": "Er zijn {apps} beschikbaar voor iOS, Android en andere platformen.", @@ -185,7 +191,7 @@ "onboarding.page_six.read_guidelines": "Vergeet niet de {guidelines} van {domain} te lezen!", "onboarding.page_six.various_app": "mobiele apps", "onboarding.page_three.profile": "Bewerk jouw profiel om jouw avatar, bio en weergavenaam te veranderen. Daar vind je ook andere instellingen.", - "onboarding.page_three.search": "Gebruik de zoekbalk linksboven om andere mensen op Mastodon te vinden en om te zoeken op hashtags, zoals {illustration} en {introductions}. Om iemand te vinden die niet op deze Mastodon-server zit, moet je het volledige Mastodon-adres van deze persoon invoeren.", + "onboarding.page_three.search": "Gebruik de zoekbalk linksboven om andere mensen op Mastodon te vinden en om te zoeken op hashtags, zoals {illustration} en {introductions}. Om iemand te vinden die niet op deze Mastodonserver zit, moet je het volledige Mastodonadres van deze persoon invoeren.", "onboarding.page_two.compose": "Schrijf berichten (wij noemen dit toots) in het tekstvak in de linkerkolom. Je kan met de pictogrammen daaronder afbeeldingen uploaden, privacy-instellingen veranderen en je tekst een waarschuwing meegeven.", "onboarding.skip": "Overslaan", "privacy.change": "Zichtbaarheid toot aanpassen", @@ -197,24 +203,33 @@ "privacy.public.short": "Openbaar", "privacy.unlisted.long": "Niet op openbare tijdlijnen tonen", "privacy.unlisted.short": "Minder openbaar", + "regeneration_indicator.label": "Aan het laden…", + "regeneration_indicator.sublabel": "Jouw tijdlijn wordt aangemaakt!", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", "relative_time.just_now": "nu", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "reply_indicator.cancel": "Annuleren", + "report.forward": "Doorsturen naar {target}", + "report.forward_hint": "Het account bevindt zich op een andere server. Stuur daar eveneens een geanonimiseerde kopie van de gerapporteerde toot(s) naartoe?", + "report.hint": "De gerapporteerde toot(s) worden naar de moderatoren van jouw server gestuurd. Je kunt hieronder een uitleg geven waarom je dit account rapporteert:", "report.placeholder": "Extra opmerkingen", "report.submit": "Verzenden", "report.target": "Rapporteer {target}", "search.placeholder": "Zoeken", "search_popout.search_format": "Geavanceerd zoeken", + "search_popout.tips.full_text": "Gebruik gewone tekst om te zoeken naar toots die jij hebt geschreven, als favoriet markeerde, hebt geboost of waarin jij bent vermeldt, en ook om te zoeken naar gebruikersnamen, weergavenamen en hashtags.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "toot", "search_popout.tips.text": "Gebruik gewone tekst om te zoeken op weergavenamen, gebruikersnamen en hashtags", "search_popout.tips.user": "gebruiker", + "search_results.accounts": "Gebruikers", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}", "standalone.public_title": "Een kijkje binnenin...", - "status.block": "Block @{name}", + "status.block": "Blokkeer @{name}", "status.cannot_reblog": "Deze toot kan niet geboost worden", "status.delete": "Verwijderen", "status.embed": "Embed", @@ -223,10 +238,11 @@ "status.media_hidden": "Media verborgen", "status.mention": "Vermeld @{name}", "status.more": "Meer", - "status.mute": "Mute @{name}", + "status.mute": "Negeer @{name}", "status.mute_conversation": "Negeer conversatie", "status.open": "Toot volledig tonen", "status.pin": "Aan profielpagina vastmaken", + "status.pinned": "Vastgemaakte toot", "status.reblog": "Boost", "status.reblogged_by": "{name} boostte", "status.reply": "Reageren", @@ -239,7 +255,6 @@ "status.show_more": "Meer tonen", "status.unmute_conversation": "Conversatie niet meer negeren", "status.unpin": "Van profielpagina losmaken", - "tabs_bar.compose": "Schrijven", "tabs_bar.federated_timeline": "Globaal", "tabs_bar.home": "Start", "tabs_bar.local_timeline": "Lokaal", @@ -248,6 +263,7 @@ "upload_area.title": "Hierin slepen om te uploaden", "upload_button.label": "Media toevoegen", "upload_form.description": "Omschrijf dit voor mensen met een visuele beperking", + "upload_form.focus": "Bijsnijden", "upload_form.undo": "Ongedaan maken", "upload_progress.label": "Uploaden...", "video.close": "Video sluiten", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index 5fbc51ff3..b7ceb9f73 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -1,19 +1,23 @@ { "account.block": "Blokkér @{name}", "account.block_domain": "Skjul alt fra {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "Informasjonen nedenfor kan gi et ufullstendig bilde av brukerens profil.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Rediger profil", "account.follow": "Følg", "account.followers": "Følgere", "account.follows": "Følger", "account.follows_you": "Følger deg", - "account.hide_reblogs": "Hide boosts from @{name}", + "account.hide_reblogs": "Skjul fremhevinger fra @{name}", "account.media": "Media", "account.mention": "Nevn @{name}", "account.moved_to": "{name} har flyttet til:", "account.mute": "Demp @{name}", "account.mute_notifications": "Ignorer varsler fra @{name}", + "account.muted": "Muted", "account.posts": "Innlegg", + "account.posts_with_replies": "Toots with replies", "account.report": "Rapportér @{name}", "account.requested": "Venter på godkjennelse", "account.share": "Del @{name}s profil", @@ -23,7 +27,7 @@ "account.unfollow": "Avfølg", "account.unmute": "Avdemp @{name}", "account.unmute_notifications": "Vis varsler fra @{name}", - "account.view_full_profile": "Vis full profil", + "account.view_full_profile": "Vis hele profilen", "boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang", "bundle_column_error.body": "Noe gikk galt mens denne komponenten lastet.", "bundle_column_error.retry": "Prøv igjen", @@ -36,7 +40,7 @@ "column.favourites": "Likt", "column.follow_requests": "Følgeforespørsler", "column.home": "Hjem", - "column.lists": "Lists", + "column.lists": "Lister", "column.mutes": "Dempede brukere", "column.notifications": "Varsler", "column.pins": "Pinned toot", @@ -50,7 +54,7 @@ "column_header.unpin": "Løsne", "column_subheading.navigation": "Navigasjon", "column_subheading.settings": "Innstillinger", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.hashtag_warning": "Denne tuten blir ikke listet under noen emneknagger da den er ulistet. Kun offentlige tuter kan søktes etter med emneknagg.", "compose_form.lock_disclaimer": "Din konto er ikke {locked}. Hvem som helst kan følge deg og se dine private poster.", "compose_form.lock_disclaimer.lock": "låst", "compose_form.placeholder": "Hva har du på hjertet?", @@ -65,7 +69,7 @@ "confirmations.delete.confirm": "Slett", "confirmations.delete.message": "Er du sikker på at du vil slette denne statusen?", "confirmations.delete_list.confirm": "Delete", - "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", + "confirmations.delete_list.message": "Er du sikker på at du vil slette denne listen permanent?", "confirmations.domain_block.confirm": "Skjul alt fra domenet", "confirmations.domain_block.message": "Er du sikker på at du vil skjule hele domenet {domain}? I de fleste tilfeller er det bedre med målrettet blokkering eller demping.", "confirmations.mute.confirm": "Demp", @@ -75,7 +79,7 @@ "embed.instructions": "Kopier koden under for å bygge inn denne statusen på hjemmesiden din.", "embed.preview": "Slik kommer det til å se ut:", "emoji_button.activity": "Aktivitet", - "emoji_button.custom": "Custom", + "emoji_button.custom": "Tilpasset", "emoji_button.flags": "Flagg", "emoji_button.food": "Mat og drikke", "emoji_button.label": "Sett inn emoji", @@ -92,7 +96,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": "Det er ikke noe i denne listen ennå.", + "empty_column.list": "Det er ingenting i denne listen ennå. Når medlemmene av denne listen legger ut nye statuser vil de dukke opp her.", "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", @@ -109,48 +113,49 @@ "home.column_settings.show_replies": "Vis svar", "home.settings": "Kolonneinnstillinger", "keyboard_shortcuts.back": "for å navigere tilbake", - "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.boost": "å fremheve", + "keyboard_shortcuts.column": "å fokusere en status i en av kolonnene", + "keyboard_shortcuts.compose": "å fokusere komponeringsfeltet", "keyboard_shortcuts.description": "Description", "keyboard_shortcuts.down": "for å flytte ned i listen", "keyboard_shortcuts.enter": "to open status", "keyboard_shortcuts.favourite": "for å favorittmarkere", "keyboard_shortcuts.heading": "Keyboard Shortcuts", "keyboard_shortcuts.hotkey": "Lyntast", - "keyboard_shortcuts.legend": "to display this legend", - "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.legend": "å vise denne forklaringen", + "keyboard_shortcuts.mention": "å nevne forfatter", "keyboard_shortcuts.reply": "for å svare", - "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", + "keyboard_shortcuts.search": "å fokusere søk", + "keyboard_shortcuts.toot": "å starte en helt ny tut", + "keyboard_shortcuts.unfocus": "å ufokusere komponerings-/søkefeltet", + "keyboard_shortcuts.up": "å flytte opp i listen", "lightbox.close": "Lukk", - "lightbox.next": "Next", - "lightbox.previous": "Previous", - "lists.account.add": "Add to list", - "lists.account.remove": "Remove from list", - "lists.delete": "Delete list", - "lists.edit": "Edit list", - "lists.new.create": "Add list", - "lists.new.title_placeholder": "New list title", - "lists.search": "Search among people you follow", - "lists.subheading": "Your lists", + "lightbox.next": "Neste", + "lightbox.previous": "Forrige", + "lists.account.add": "Legg til i listen", + "lists.account.remove": "Fjern fra listen", + "lists.delete": "Slett listen", + "lists.edit": "Rediger listen", + "lists.new.create": "Ligg til liste", + "lists.new.title_placeholder": "Ny listetittel", + "lists.search": "Søk blant personer du følger", + "lists.subheading": "Dine lister", "loading_indicator.label": "Laster...", "media_gallery.toggle_visible": "Veksle synlighet", "missing_indicator.label": "Ikke funnet", - "mute_modal.hide_notifications": "Hide notifications from this user?", + "missing_indicator.sublabel": "Denne ressursen ble ikke funnet", + "mute_modal.hide_notifications": "Skjul varslinger fra denne brukeren?", "navigation_bar.blocks": "Blokkerte brukere", "navigation_bar.community_timeline": "Lokal tidslinje", "navigation_bar.edit_profile": "Rediger profil", - "navigation_bar.favourites": "Likt", + "navigation_bar.favourites": "Favoritter", "navigation_bar.follow_requests": "Følgeforespørsler", "navigation_bar.info": "Utvidet informasjon", - "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", - "navigation_bar.lists": "Lists", + "navigation_bar.keyboard_shortcuts": "Tastatursnarveier", + "navigation_bar.lists": "Lister", "navigation_bar.logout": "Logg ut", "navigation_bar.mutes": "Dempede brukere", - "navigation_bar.pins": "Pinned toots", + "navigation_bar.pins": "Festa tuter", "navigation_bar.preferences": "Preferanser", "navigation_bar.public_timeline": "Felles tidslinje", "notification.favourite": "{name} likte din status", @@ -158,13 +163,13 @@ "notification.mention": "{name} nevnte deg", "notification.reblog": "{name} fremhevde din status", "notifications.clear": "Fjern varsler", - "notifications.clear_confirmation": "Er du sikker på at du vil fjerne alle dine varsler?", + "notifications.clear_confirmation": "Er du sikker på at du vil fjerne alle dine varsler permanent?", "notifications.column_settings.alert": "Skrivebordsvarslinger", "notifications.column_settings.favourite": "Likt:", "notifications.column_settings.follow": "Nye følgere:", "notifications.column_settings.mention": "Nevnt:", - "notifications.column_settings.push": "Push notifications", - "notifications.column_settings.push_meta": "This device", + "notifications.column_settings.push": "Push varsler", + "notifications.column_settings.push_meta": "Denne enheten", "notifications.column_settings.reblog": "Fremhevet:", "notifications.column_settings.show": "Vis i kolonne", "notifications.column_settings.sound": "Spill lyd", @@ -174,14 +179,15 @@ "onboarding.page_four.home": "Hjem er tidslinjen med alle brukere som du følger.", "onboarding.page_four.notifications": "Kolonnen med varsler viser når noen interakterer med deg.", "onboarding.page_one.federation": "Mastdodon er et nettverk med uavhengige servere som sammarbeider om å danne et stort sosialt nettverk. Vi kaller disse serverene instanser.", - "onboarding.page_one.handle": "Du er på {domain}, så ditt fulle brukernavn er {handle}", + "onboarding.page_one.full_handle": "Ditt fulle kallenavn", + "onboarding.page_one.handle_hint": "Dette er hva du ber dine venner å søke etter.", "onboarding.page_one.welcome": "Velkommen til Mastodon!", "onboarding.page_six.admin": "Administratoren på din instans er {admin}.", "onboarding.page_six.almost_done": "Snart ferdig...", "onboarding.page_six.appetoot": "Bon Appetut!", "onboarding.page_six.apps_available": "Det er {apps} tilgjengelig for iOS, Android og andre plattformer.", "onboarding.page_six.github": "Mastodon er programvare med fri og åpen kildekode. Du kan rapportere feil, be om hjelp eller foreslå endringer på {github}.", - "onboarding.page_six.guidelines": "samfunnets rettningslinjer", + "onboarding.page_six.guidelines": "samfunnets retningslinjer", "onboarding.page_six.read_guidelines": "Vennligst les {guidelines} for {domain}!", "onboarding.page_six.various_app": "mobilapper", "onboarding.page_three.profile": "Rediger profilen din for å endre din avatar, biografi, og visningsnavn. Der finner du også andre innstillinger.", @@ -197,36 +203,46 @@ "privacy.public.short": "Offentlig", "privacy.unlisted.long": "Ikke vis i offentlige tidslinjer", "privacy.unlisted.short": "Uoppført", + "regeneration_indicator.label": "Laster…", + "regeneration_indicator.sublabel": "Dine startside forberedes!", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", - "relative_time.just_now": "now", + "relative_time.just_now": "nå", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "reply_indicator.cancel": "Avbryt", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Tilleggskommentarer", "report.submit": "Send inn", "report.target": "Rapporterer", "search.placeholder": "Søk", - "search_popout.search_format": "Advanced search format", - "search_popout.tips.hashtag": "hashtag", + "search_popout.search_format": "Avansert søkeformat", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.tips.hashtag": "emneknagg", "search_popout.tips.status": "status", - "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", - "search_popout.tips.user": "user", + "search_popout.tips.text": "Enkel tekst returnerer matchende visningsnavn, brukernavn og emneknagger", + "search_popout.tips.user": "bruker", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {resultat} other {resultater}}", - "standalone.public_title": "A look inside...", + "standalone.public_title": "En titt inni...", "status.block": "Block @{name}", "status.cannot_reblog": "Denne posten kan ikke fremheves", "status.delete": "Slett", - "status.embed": "Embed", + "status.embed": "Bygge inn", "status.favourite": "Lik", "status.load_more": "Last mer", "status.media_hidden": "Media skjult", "status.mention": "Nevn @{name}", - "status.more": "More", - "status.mute": "Mute @{name}", + "status.more": "Mer", + "status.mute": "Demp @{name}", "status.mute_conversation": "Demp samtale", "status.open": "Utvid denne statusen", - "status.pin": "Pin on profile", + "status.pin": "Fest på profilen", + "status.pinned": "Pinned toot", "status.reblog": "Fremhev", "status.reblogged_by": "Fremhevd av {name}", "status.reply": "Svar", @@ -234,24 +250,24 @@ "status.report": "Rapporter @{name}", "status.sensitive_toggle": "Klikk for å vise", "status.sensitive_warning": "Følsomt innhold", - "status.share": "Share", + "status.share": "Del", "status.show_less": "Vis mindre", "status.show_more": "Vis mer", "status.unmute_conversation": "Ikke demp samtale", - "status.unpin": "Unpin from profile", - "tabs_bar.compose": "Komponer", + "status.unpin": "Angre festing på profilen", "tabs_bar.federated_timeline": "Felles", "tabs_bar.home": "Hjem", "tabs_bar.local_timeline": "Lokal", "tabs_bar.notifications": "Varslinger", - "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", + "ui.beforeunload": "Din kladd vil bli forkastet om du forlater Mastodon.", "upload_area.title": "Dra og slipp for å laste opp", "upload_button.label": "Legg til media", - "upload_form.description": "Describe for the visually impaired", + "upload_form.description": "Beskriv for synshemmede", + "upload_form.focus": "Crop", "upload_form.undo": "Angre", "upload_progress.label": "Laster opp...", - "video.close": "Close video", - "video.exit_fullscreen": "Exit full screen", + "video.close": "Lukk video", + "video.exit_fullscreen": "Lukk fullskjerm", "video.expand": "Utvid video", "video.fullscreen": "Full screen", "video.hide": "Skjul video", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 6ebd40f5b..c9a15c751 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -1,29 +1,33 @@ { "account.block": "Blocar @{name}", "account.block_domain": "Tot amagar del domeni {domain}", - "account.disclaimer_full": "Aquelas informacions de perfil pòdon èsser incompletas.", + "account.blocked": "Blocked", + "account.disclaimer_full": "Aquelas informacions de perfil pòdon èsser incomplètas.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Modificar lo perfil", "account.follow": "Sègre", "account.followers": "Seguidors", "account.follows": "Abonaments", "account.follows_you": "Vos sèc", - "account.hide_reblogs": "Rescondre los partages de @{name}", + "account.hide_reblogs": "Rescondre los partatges de @{name}", "account.media": "Mèdias", "account.mention": "Mencionar @{name}", "account.moved_to": "{name} a mudat los catons a :", "account.mute": "Rescondre @{name}", "account.mute_notifications": "Rescondre las notificacions de @{name}", + "account.muted": "Muted", "account.posts": "Estatuts", + "account.posts_with_replies": "Toots with replies", "account.report": "Senhalar @{name}", "account.requested": "Invitacion mandada. Clicatz per anullar", "account.share": "Partejar lo perfil a @{name}", - "account.show_reblogs": "Mostrar los partages de @{name}", + "account.show_reblogs": "Mostrar los partatges de @{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": "Mostrar las notificacions de @{name}", - "account.view_full_profile": "Veire lo perfil complet", + "account.view_full_profile": "Veire lo perfil complèt", "boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven", "bundle_column_error.body": "Quicòm a fach mèuca pendent lo cargament d’aqueste compausant.", "bundle_column_error.retry": "Tornar ensajar", @@ -67,7 +71,7 @@ "confirmations.delete_list.confirm": "Suprimir", "confirmations.delete_list.message": "Sètz segur de voler suprimir aquesta lista per totjorn ?", "confirmations.domain_block.confirm": "Amagar tot lo domeni", - "confirmations.domain_block.message": "Sètz segur segur de voler blocar completament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.", + "confirmations.domain_block.message": "Sètz segur segur de voler blocar complètament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.", "confirmations.mute.confirm": "Rescondre", "confirmations.mute.message": "Sètz segur de voler rescondre {name} ?", "confirmations.unfollow.confirm": "Quitar de sègre", @@ -80,7 +84,7 @@ "emoji_button.food": "Beure e manjar", "emoji_button.label": "Inserir un emoji", "emoji_button.nature": "Natura", - "emoji_button.not_found": "Cap emoji ! (╯°□°)╯︵ ┻━┻", + "emoji_button.not_found": "Cap d’emoji ! (╯°□°)╯︵ ┻━┻", "emoji_button.objects": "Objèctes", "emoji_button.people": "Gents", "emoji_button.recent": "Sovent utilizats", @@ -108,23 +112,23 @@ "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": "per anar enrèire", - "keyboard_shortcuts.boost": "per partejar", - "keyboard_shortcuts.column": "per centrar un estatut a una colomna", - "keyboard_shortcuts.compose": "per anar al camp tèxte", + "keyboard_shortcuts.back": "anar enrèire", + "keyboard_shortcuts.boost": "partejar", + "keyboard_shortcuts.column": "centrar un estatut a una colomna", + "keyboard_shortcuts.compose": "anar al camp tèxte", "keyboard_shortcuts.description": "Descripcion", - "keyboard_shortcuts.down": "per far davalar dins la lista", - "keyboard_shortcuts.enter": "per dobrir los estatuts", - "keyboard_shortcuts.favourite": "per apondre als favorits", + "keyboard_shortcuts.down": "far davalar dins la lista", + "keyboard_shortcuts.enter": "dobrir los estatuts", + "keyboard_shortcuts.favourite": "apondre als favorits", "keyboard_shortcuts.heading": "Acorchis clavièr", "keyboard_shortcuts.hotkey": "Acorchis", - "keyboard_shortcuts.legend": "per mostrar aquesta legenda", - "keyboard_shortcuts.mention": "per mencionar l’autor", - "keyboard_shortcuts.reply": "per respondre", - "keyboard_shortcuts.search": "per anar a la recèrca", - "keyboard_shortcuts.toot": "per començar un estatut tot novèl", - "keyboard_shortcuts.unfocus": "per quitar lo camp tèxte/de recèrca", - "keyboard_shortcuts.up": "per far montar dins la lista", + "keyboard_shortcuts.legend": "mostrar aquesta legenda", + "keyboard_shortcuts.mention": "mencionar l’autor", + "keyboard_shortcuts.reply": "respondre", + "keyboard_shortcuts.search": "anar a la recèrca", + "keyboard_shortcuts.toot": "començar un estatut tot novèl", + "keyboard_shortcuts.unfocus": "quitar lo camp tèxte/de recèrca", + "keyboard_shortcuts.up": "far montar dins la lista", "lightbox.close": "Tampar", "lightbox.next": "Seguent", "lightbox.previous": "Precedent", @@ -139,6 +143,7 @@ "loading_indicator.label": "Cargament…", "media_gallery.toggle_visible": "Modificar la visibilitat", "missing_indicator.label": "Pas trobat", + "missing_indicator.sublabel": "Aquesta ressorsa es pas estada trobada", "mute_modal.hide_notifications": "Rescondre las notificacions d’aquesta persona ?", "navigation_bar.blocks": "Personas blocadas", "navigation_bar.community_timeline": "Flux public local", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "Lo flux d’acuèlh mòstra los estatuts del mond que seguètz.", "onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualqu’un interagís amb vos.", "onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per construire un malhum mai larg. Òm los apèla instàncias.", - "onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}", + "onboarding.page_one.full_handle": "Vòstre escais-nom complèt", + "onboarding.page_one.handle_hint": "Vos cal dire a vòstres amics de cercar aquò.", "onboarding.page_one.welcome": "Benvengut a Mastodon !", "onboarding.page_six.admin": "Vòstre administrator d’instància es {admin}.", "onboarding.page_six.almost_done": "Gaireben acabat…", @@ -185,7 +191,7 @@ "onboarding.page_six.read_guidelines": "Mercés de legir la {guidelines} de {domain} !", "onboarding.page_six.various_app": "aplicacions per mobil", "onboarding.page_three.profile": "Modificatz vòstre perfil per cambiar vòstre avatar, bio e escais-nom. I a enlà totas las preferéncias.", - "onboarding.page_three.search": "Emplegatz la barra de recèrca per trobar de mond e engachatz las etiquetas coma {illustration} e {introductions}. Per trobar una persona d’una autra instància, picatz son identificant complet.", + "onboarding.page_three.search": "Emplegatz la barra de recèrca per trobar de mond e engachatz las etiquetas coma {illustration} e {introductions}. Per trobar una persona d’una autra instància, picatz son identificant complèt.", "onboarding.page_two.compose": "Escrivètz un estatut dempuèi la colomna per compausar. Podètz mandar un imatge, cambiar la confidencialitat e ajustar un avertiment amb las icònas cai-jos.", "onboarding.skip": "Passar", "privacy.change": "Ajustar la confidencialitat del messatge", @@ -197,21 +203,30 @@ "privacy.public.short": "Public", "privacy.unlisted.long": "Mostrar pas dins los fluxes publics", "privacy.unlisted.short": "Pas-listat", - "relative_time.days": "fa {number} d", - "relative_time.hours": "fa {number} h", + "regeneration_indicator.label": "Cargament…", + "regeneration_indicator.sublabel": "Sèm a preparar vòstre flux d’acuèlh !", + "relative_time.days": "fa {number}d", + "relative_time.hours": "fa {number}h", "relative_time.just_now": "ara", - "relative_time.minutes": "fa {number} min", - "relative_time.seconds": "fa {number} s", + "relative_time.minutes": "fa {number}min", + "relative_time.seconds": "fa {number}s", "reply_indicator.cancel": "Anullar", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Comentaris addicionals", "report.submit": "Mandar", "report.target": "Senhalar {target}", "search.placeholder": "Recercar", "search_popout.search_format": "Format recèrca avançada", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "etiqueta", "search_popout.tips.status": "estatut", "search_popout.tips.text": "Lo tèxt brut tòrna escais, noms d’utilizaire e etiquetas correspondents", "search_popout.tips.user": "utilizaire", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}", "standalone.public_title": "Una ulhada dedins…", "status.block": "Blocar @{name}", @@ -227,6 +242,7 @@ "status.mute_conversation": "Rescondre la conversacion", "status.open": "Desplegar aqueste estatut", "status.pin": "Penjar al perfil", + "status.pinned": "Pinned toot", "status.reblog": "Partejar", "status.reblogged_by": "{name} a partejat", "status.reply": "Respondre", @@ -239,7 +255,6 @@ "status.show_more": "Desplegar", "status.unmute_conversation": "Tornar mostrar la conversacion", "status.unpin": "Tirar del perfil", - "tabs_bar.compose": "Compausar", "tabs_bar.federated_timeline": "Flux public global", "tabs_bar.home": "Acuèlh", "tabs_bar.local_timeline": "Flux public local", @@ -248,12 +263,13 @@ "upload_area.title": "Lisatz e depausatz per mandar", "upload_button.label": "Ajustar un mèdia", "upload_form.description": "Descripcion pels mal vesents", + "upload_form.focus": "Crop", "upload_form.undo": "Anullar", "upload_progress.label": "Mandadís…", "video.close": "Tampar la vidèo", "video.exit_fullscreen": "Sortir plen ecran", "video.expand": "Agrandir la vidèo", - "video.fullscreen": "Ecran complet", + "video.fullscreen": "Ecran complèt", "video.hide": "Amagar la vidèo", "video.mute": "Copar lo son", "video.pause": "Pausa", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index d36b1e6ed..391bc6a28 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -1,19 +1,23 @@ { "account.block": "Blokuj @{name}", "account.block_domain": "Blokuj wszystko z {domain}", + "account.blocked": "Zablokowany", "account.disclaimer_full": "Poniższe informacje mogą nie odwzorowywać bezbłędnie profilu użytkownika.", + "account.domain_blocked": "Ukryto domenę", "account.edit_profile": "Edytuj profil", "account.follow": "Śledź", "account.followers": "Śledzący", "account.follows": "Śledzeni", "account.follows_you": "Śledzi Cię", "account.hide_reblogs": "Ukryj podbicia od @{name}", - "account.media": "Media", + "account.media": "Zawartość multimedialna", "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.muted": "Wyciszony", "account.posts": "Wpisy", + "account.posts_with_replies": "Wpisy z odpowiedziami", "account.report": "Zgłoś @{name}", "account.requested": "Oczekująca prośba, kliknij aby anulować", "account.share": "Udostępnij profil @{name}", @@ -99,7 +103,7 @@ "empty_column.home.public_timeline": "publiczna oś czasu", "empty_column.list": "Nie ma nic na tej liście. Kiedy członkowie listy dodadzą nowe wpisy, pojawia się one tutaj.", "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ć.", + "empty_column.public": "Tu nic nie ma! Napisz coś publicznie, lub dodaj ludzi z innych instancji, aby to wyświetlić", "follow_request.authorize": "Autoryzuj", "follow_request.reject": "Odrzuć", "getting_started.appsshort": "Aplikacje", @@ -134,16 +138,17 @@ "lightbox.next": "Następne", "lightbox.previous": "Poprzednie", "lists.account.add": "Dodaj do listy", - "lists.account.remove": "Remove from list", + "lists.account.remove": "Usunąć z listy", "lists.delete": "Usuń listę", "lists.edit": "Edytuj listę", "lists.new.create": "Utwórz listę", - "lists.new.title_placeholder": "Wprowadź tytuł listy…", + "lists.new.title_placeholder": "Wprowadź tytuł listy", "lists.search": "Szukaj wśród osób które śledzisz", "lists.subheading": "Twoje listy", "loading_indicator.label": "Ładowanie…", "media_gallery.toggle_visible": "Przełącz widoczność", "missing_indicator.label": "Nie znaleziono", + "missing_indicator.sublabel": "Nie można odnaleźć tego zasobu", "mute_modal.hide_notifications": "Chcesz ukryć powiadomienia od tego użytkownika?", "navigation_bar.blocks": "Zablokowani użytkownicy", "navigation_bar.community_timeline": "Lokalna oś czasu", @@ -181,7 +186,8 @@ "onboarding.page_four.home": "Główna oś czasu wyświetla publiczne wpisy.", "onboarding.page_four.notifications": "Kolumna powiadomień wyświetla, gdy ktoś dokonuje interakcji z tobą.", "onboarding.page_one.federation": "Mastodon jest siecią niezależnych serwerów połączonych w jeden portal społecznościowy. Nazywamy te serwery instancjami.", - "onboarding.page_one.handle": "Jesteś na domenie {domain}, więc Twój pełny adres to {handle}", + "onboarding.page_one.full_handle": "Twój pełny adres", + "onboarding.page_one.handle_hint": "Należy go podać znajomym, aby mogli Cię odnaleźć.", "onboarding.page_one.welcome": "Witamy w Mastodon!", "onboarding.page_six.admin": "Administratorem tej instancji jest {admin}.", "onboarding.page_six.almost_done": "Prawie gotowe…", @@ -204,21 +210,30 @@ "privacy.public.short": "Publiczny", "privacy.unlisted.long": "Niewidoczny na publicznych osiach czasu", "privacy.unlisted.short": "Niewidoczny", + "regeneration_indicator.label": "Ładuję…", + "regeneration_indicator.sublabel": "Twoja oś czasu jest przygotowywana!", "relative_time.days": "{number} dni", "relative_time.hours": "{number} godz.", "relative_time.just_now": "teraz", "relative_time.minutes": "{number} min.", "relative_time.seconds": "{number} s.", "reply_indicator.cancel": "Anuluj", + "report.forward": "Przekaż na {target}", + "report.forward_hint": "To konto znajduje się na innej instancji. Czy chcesz wysłać anonimową kopię zgłoszenia rnież na nią?", + "report.hint": "Zgłoszenie zostanie wysłane moderatorom Twojej instancji. Poniżej możesz też umieścić wyjaśnieni dlaczego zgłaszasz to konto:", "report.placeholder": "Dodatkowe komentarze", "report.submit": "Wyślij", "report.target": "Zgłaszanie {target}", "search.placeholder": "Szukaj", "search_popout.search_format": "Zaawansowane wyszukiwanie", + "search_popout.tips.full_text": "Pozwala na wyszukiwanie wpisów które napisałeś, dodałeś do ulubionych, podbiłeś w których o Tobie wspomniano, oraz pasujące nazwy użytkowników, pełne nazwy i hashtagi.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "wpis", "search_popout.tips.text": "Proste wyszukiwanie pasujących pseudonimów, nazw użytkowników i hashtagów", "search_popout.tips.user": "użytkownik", + "search_results.accounts": "Ludzie", + "search_results.hashtags": "Hashtagi", + "search_results.statuses": "Wpisy", "search_results.total": "{count, number} {count, plural, one {wynik} few {wyniki} many {wyników} more {wyników}}", "standalone.public_title": "Spojrzenie w głąb…", "status.block": "Zablokuj @{name}", @@ -234,6 +249,7 @@ "status.mute_conversation": "Wycisz konwersację", "status.open": "Rozszerz ten wpis", "status.pin": "Przypnij do profilu", + "status.pinned": "Przypięty wpis", "status.reblog": "Podbij", "status.reblogged_by": "{name} podbił", "status.reply": "Odpowiedz", @@ -246,7 +262,6 @@ "status.show_more": "Pokaż więcej", "status.unmute_conversation": "Cofnij wyciszenie konwersacji", "status.unpin": "Odepnij z profilu", - "tabs_bar.compose": "Napisz", "tabs_bar.federated_timeline": "Globalne", "tabs_bar.home": "Strona główna", "tabs_bar.local_timeline": "Lokalne", @@ -255,6 +270,7 @@ "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", + "upload_form.focus": "Przytnij", "upload_form.undo": "Cofnij", "upload_progress.label": "Wysyłanie", "video.close": "Zamknij film", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index 381b80639..6406dbea6 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -1,7 +1,9 @@ { "account.block": "Bloquear @{name}", "account.block_domain": "Esconder tudo de {domain}", + "account.blocked": "Bloqueado", "account.disclaimer_full": "As informações abaixo podem refletir o perfil do usuário de maneira incompleta.", + "account.domain_blocked": "Domínio escondido", "account.edit_profile": "Editar perfil", "account.follow": "Seguir", "account.followers": "Seguidores", @@ -13,7 +15,9 @@ "account.moved_to": "{name} se mudou para:", "account.mute": "Silenciar @{name}", "account.mute_notifications": "Silenciar notificações de @{name}", - "account.posts": "Posts", + "account.muted": "Silenciado", + "account.posts": "Toots", + "account.posts_with_replies": "Toots e respostas", "account.report": "Denunciar @{name}", "account.requested": "Aguardando aprovação. Clique para cancelar a solicitação", "account.share": "Compartilhar perfil de @{name}", @@ -90,7 +94,7 @@ "emoji_button.travel": "Viagens & Lugares", "empty_column.community": "A timeline local está vazia. Escreva algo publicamente para começar!", "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": "Você ainda não segue usuário algum. Visite a timeline {public} ou use o buscador para procurar e conhecer outros usuários.", "empty_column.home.public_timeline": "global", "empty_column.list": "Ainda não há nada nesta lista. Quando membros dessa lista fizerem novas postagens, elas aparecerão aqui.", "empty_column.notifications": "Você ainda não possui notificações. Interaja com outros usuários para começar a conversar.", @@ -139,6 +143,7 @@ "loading_indicator.label": "Carregando...", "media_gallery.toggle_visible": "Esconder/Mostrar", "missing_indicator.label": "Não encontrado", + "missing_indicator.sublabel": "Esse recurso não pôde ser encontrado", "mute_modal.hide_notifications": "Esconder notificações deste usuário?", "navigation_bar.blocks": "Usuários bloqueados", "navigation_bar.community_timeline": "Local", @@ -173,8 +178,9 @@ "onboarding.page_five.public_timelines": "A timeline local mostra postagens públicas de todos os usuários no {domain}. A timeline federada mostra todas as postagens de todas as pessoas que pessoas no {domain} seguem. Estas são as timelines públicas, uma ótima maneira de conhecer novas pessoas.", "onboarding.page_four.home": "A página inicial mostra postagens de pessoas que você segue.", "onboarding.page_four.notifications": "A coluna de notificações te mostra quando alguém interage com você.", - "onboarding.page_one.federation": "Mastodon é uma rede d servidores independentes se juntando para fazer uma grande rede social. Nós chamamos estes servidores de instâncias.", - "onboarding.page_one.handle": "Você está no {domain}, então o seu nome de usuário completo é {handle}", + "onboarding.page_one.federation": "Mastodon é uma rede de servidores independentes que se juntam para fazer uma grande rede social. Nós chamamos estes servidores de instâncias.", + "onboarding.page_one.full_handle": "Seu nome de usuário completo", + "onboarding.page_one.handle_hint": "Isso é o que você diz aos seus amigos para que eles possam te mandar mensagens ou te seguir a partir de outra instância.", "onboarding.page_one.welcome": "Seja bem-vindo(a) ao Mastodon!", "onboarding.page_six.admin": "O administrador de sua instância é {admin}.", "onboarding.page_six.almost_done": "Quase acabando...", @@ -197,21 +203,30 @@ "privacy.public.short": "Pública", "privacy.unlisted.long": "Não publicar em feeds públicos", "privacy.unlisted.short": "Não listada", + "regeneration_indicator.label": "Carregando…", + "regeneration_indicator.sublabel": "Sua página inicial está sendo preparada!", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", "relative_time.just_now": "agora", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "reply_indicator.cancel": "Cancelar", + "report.forward": "Encaminhar para {target}", + "report.forward_hint": "Essa conta pertence à um outro servidor. Encaminhar uma cópia da denúncia com seus dados tornados anônimos para esse servidor?", + "report.hint": "A sua denúncia será enviada aos moderadores da instância. Você pode adicionar uma explicação de porque você está denunciando essa conta abaixo:", "report.placeholder": "Comentários adicionais", "report.submit": "Enviar", "report.target": "Denunciar", "search.placeholder": "Pesquisar", "search_popout.search_format": "Formato de busca avançado", + "search_popout.tips.full_text": "Texto simples retorna status que você escreveu, favoritou, compartilhou ou em que tenha sido mencionado; também retorna nomes de exibição, usuários e hashtags correspondentes.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "status", "search_popout.tips.text": "Texto simples retorna nomes de exibição, usuários e hashtags correspondentes", "search_popout.tips.user": "usuário", + "search_results.accounts": "Pessoas", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", "standalone.public_title": "Dê uma espiada...", "status.block": "Block @{name}", @@ -227,6 +242,7 @@ "status.mute_conversation": "Silenciar conversa", "status.open": "Expandir", "status.pin": "Fixar no perfil", + "status.pinned": "Toot fixado", "status.reblog": "Compartilhar", "status.reblogged_by": "{name} compartilhou", "status.reply": "Responder", @@ -239,7 +255,6 @@ "status.show_more": "Mostrar mais", "status.unmute_conversation": "Desativar silêncio desta conversa", "status.unpin": "Desafixar do perfil", - "tabs_bar.compose": "Criar", "tabs_bar.federated_timeline": "Global", "tabs_bar.home": "Página inicial", "tabs_bar.local_timeline": "Local", @@ -248,6 +263,7 @@ "upload_area.title": "Arraste e solte para enviar", "upload_button.label": "Adicionar mídia", "upload_form.description": "Descreva a imagem para deficientes visuais", + "upload_form.focus": "Recortar", "upload_form.undo": "Desfazer", "upload_progress.label": "Salvando...", "video.close": "Fechar vídeo", diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json index f566f551b..f059e7c20 100644 --- a/app/javascript/mastodon/locales/pt.json +++ b/app/javascript/mastodon/locales/pt.json @@ -1,7 +1,9 @@ { "account.block": "Bloquear @{name}", "account.block_domain": "Esconder tudo do domínio {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "As informações abaixo podem refletir o perfil do usuário de forma incompleta.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Editar perfil", "account.follow": "Seguir", "account.followers": "Seguidores", @@ -13,7 +15,9 @@ "account.moved_to": "{name} mudou a sua conta para:", "account.mute": "Silenciar @{name}", "account.mute_notifications": "Silenciar notificações de @{name}", + "account.muted": "Muted", "account.posts": "Posts", + "account.posts_with_replies": "Toots with replies", "account.report": "Denunciar @{name}", "account.requested": "A aguardar aprovação", "account.share": "Partilhar o perfil @{name}", @@ -50,7 +54,7 @@ "column_header.unpin": "Desafixar", "column_subheading.navigation": "Navegação", "column_subheading.settings": "Preferências", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.hashtag_warning": "Esta pulbicacção não será listada em nenhuma hashtag por ser não listada. Somente publicações públicas podem ser pesquisadas por hashtag.", "compose_form.lock_disclaimer": "A tua conta não está {locked}. Qualquer pessoa pode seguir-te e ver as publicações direcionadas apenas a seguidores.", "compose_form.lock_disclaimer.lock": "bloqueada", "compose_form.placeholder": "Em que estás a pensar?", @@ -108,7 +112,7 @@ "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": "para navegar de volta", + "keyboard_shortcuts.back": "para voltar", "keyboard_shortcuts.boost": "para partilhar", "keyboard_shortcuts.column": "para focar uma publicação numa das colunas", "keyboard_shortcuts.compose": "para focar na área de publicação", @@ -139,6 +143,7 @@ "loading_indicator.label": "A carregar...", "media_gallery.toggle_visible": "Esconder/Mostrar", "missing_indicator.label": "Não encontrado", + "missing_indicator.sublabel": "Este recurso não foi encontrado", "mute_modal.hide_notifications": "Esconder notificações deste utilizador?", "navigation_bar.blocks": "Utilizadores bloqueados", "navigation_bar.community_timeline": "Local", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "A timeline home mostra as publicações de pessoas que tu segues.", "onboarding.page_four.notifications": "A coluna de notificações mostra-te quando alguém interage contigo.", "onboarding.page_one.federation": "Mastodon é uma rede de servidores independentes ligados entre si para fazer uma grande rede social. Nós chamamos instâncias a estes servidores.", - "onboarding.page_one.handle": "Tu estás em {domain}, por isso o teu endereço completo de utilizador é {handle}", + "onboarding.page_one.full_handle": "O teu nome de utilizador completo", + "onboarding.page_one.handle_hint": "Isto é o que dizes aos teus amigos para pesquisar.", "onboarding.page_one.welcome": "Bem-vindo(a) ao Mastodon!", "onboarding.page_six.admin": "O administrador da tua instância é {admin}.", "onboarding.page_six.almost_done": "Quase pronto...", @@ -197,21 +203,30 @@ "privacy.public.short": "Público", "privacy.unlisted.long": "Não publicar nos feeds públicos", "privacy.unlisted.short": "Não listar", + "regeneration_indicator.label": "A carregar…", + "regeneration_indicator.sublabel": "A tua home está a ser preparada!", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", "relative_time.just_now": "agora", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "reply_indicator.cancel": "Cancelar", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Comentários adicionais", "report.submit": "Enviar", "report.target": "Denunciar", "search.placeholder": "Pesquisar", "search_popout.search_format": "Formato avançado de pesquisa", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "status", "search_popout.tips.text": "O texto simples retorna a correspondência de nomes, utilizadores e hashtags", "search_popout.tips.user": "utilizador", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", "standalone.public_title": "Espreitar lá dentro...", "status.block": "Block @{name}", @@ -223,10 +238,11 @@ "status.media_hidden": "Media escondida", "status.mention": "Mencionar @{name}", "status.more": "Mais", - "status.mute": "Mute @{name}", + "status.mute": "Silenciar @{name}", "status.mute_conversation": "Silenciar conversa", "status.open": "Expandir", "status.pin": "Fixar no perfil", + "status.pinned": "Pinned toot", "status.reblog": "Partilhar", "status.reblogged_by": "{name} partilhou", "status.reply": "Responder", @@ -239,7 +255,6 @@ "status.show_more": "Mostrar mais", "status.unmute_conversation": "Deixar de silenciar esta conversa", "status.unpin": "Não fixar no perfil", - "tabs_bar.compose": "Criar", "tabs_bar.federated_timeline": "Global", "tabs_bar.home": "Home", "tabs_bar.local_timeline": "Local", @@ -248,6 +263,7 @@ "upload_area.title": "Arraste e solte para enviar", "upload_button.label": "Adicionar media", "upload_form.description": "Descrição da imagem para pessoas com dificuldades visuais", + "upload_form.focus": "Crop", "upload_form.undo": "Anular", "upload_progress.label": "A gravar...", "video.close": "Fechar vídeo", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index 0fec70df0..06a7d732b 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -1,28 +1,32 @@ { "account.block": "Блокировать", "account.block_domain": "Блокировать все с {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "Нижеуказанная информация может не полностью отражать профиль пользователя.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Изменить профиль", "account.follow": "Подписаться", "account.followers": "Подписаны", "account.follows": "Подписки", "account.follows_you": "Подписан(а) на Вас", - "account.hide_reblogs": "Hide boosts from @{name}", + "account.hide_reblogs": "Скрыть продвижения от @{name}", "account.media": "Медиаконтент", "account.mention": "Упомянуть", - "account.moved_to": "{name} has moved to:", + "account.moved_to": "Ищите {name} здесь:", "account.mute": "Заглушить", - "account.mute_notifications": "Mute notifications from @{name}", + "account.mute_notifications": "Скрыть уведомления от @{name}", + "account.muted": "Muted", "account.posts": "Посты", + "account.posts_with_replies": "Toots with replies", "account.report": "Пожаловаться", "account.requested": "Ожидает подтверждения", "account.share": "Поделиться профилем @{name}", - "account.show_reblogs": "Show boosts from @{name}", + "account.show_reblogs": "Показывать продвижения от @{name}", "account.unblock": "Разблокировать", "account.unblock_domain": "Разблокировать {domain}", "account.unfollow": "Отписаться", "account.unmute": "Снять глушение", - "account.unmute_notifications": "Unmute notifications from @{name}", + "account.unmute_notifications": "Показывать уведомления от @{name}", "account.view_full_profile": "Показать полный профиль", "boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз", "bundle_column_error.body": "Что-то пошло не так при загрузке этого компонента.", @@ -36,10 +40,10 @@ "column.favourites": "Понравившееся", "column.follow_requests": "Запросы на подписку", "column.home": "Главная", - "column.lists": "Lists", + "column.lists": "Списки", "column.mutes": "Список глушения", "column.notifications": "Уведомления", - "column.pins": "Pinned toot", + "column.pins": "Закреплённый пост", "column.public": "Глобальная лента", "column_back_button.label": "Назад", "column_header.hide_settings": "Скрыть настройки", @@ -50,7 +54,7 @@ "column_header.unpin": "Открепить", "column_subheading.navigation": "Навигация", "column_subheading.settings": "Настройки", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.hashtag_warning": "Этот пост не будет показывается в поиске по хэштегу, т.к. он непубличный. Только публичные посты можно найти в поиске по хэштегу.", "compose_form.lock_disclaimer": "Ваш аккаунт не {locked}. Любой человек может подписаться на Вас и просматривать посты для подписчиков.", "compose_form.lock_disclaimer.lock": "закрыт", "compose_form.placeholder": "О чем Вы думаете?", @@ -64,8 +68,8 @@ "confirmations.block.message": "Вы уверены, что хотите заблокировать {name}?", "confirmations.delete.confirm": "Удалить", "confirmations.delete.message": "Вы уверены, что хотите удалить этот статус?", - "confirmations.delete_list.confirm": "Delete", - "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", + "confirmations.delete_list.confirm": "Удалить", + "confirmations.delete_list.message": "Вы действительно хотите навсегда удалить этот список?", "confirmations.domain_block.confirm": "Блокировать весь домен", "confirmations.domain_block.message": "Вы на самом деле уверены, что хотите блокировать весь {domain}? В большинстве случаев нескольких отдельных блокировок или глушений достаточно.", "confirmations.mute.confirm": "Заглушить", @@ -92,7 +96,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.list": "В этом списке пока ничего нет.", "empty_column.notifications": "У Вас еще нет уведомлений. Заведите знакомство с другими пользователями, чтобы начать разговор.", "empty_column.public": "Здесь ничего нет! Опубликуйте что-нибудь или подпишитесь на пользователей с других узлов, чтобы заполнить ленту.", "follow_request.authorize": "Авторизовать", @@ -108,49 +112,50 @@ "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", + "keyboard_shortcuts.back": "перейти назад", + "keyboard_shortcuts.boost": "продвинуть пост", + "keyboard_shortcuts.column": "фокус на одном из столбцов", + "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": "Назад", - "lists.account.add": "Add to list", - "lists.account.remove": "Remove from list", - "lists.delete": "Delete list", - "lists.edit": "Edit list", - "lists.new.create": "Add list", - "lists.new.title_placeholder": "New list title", - "lists.search": "Search among people you follow", - "lists.subheading": "Your lists", + "lists.account.add": "Добавить в список", + "lists.account.remove": "Убрать из списка", + "lists.delete": "Удалить список", + "lists.edit": "Изменить список", + "lists.new.create": "Новый список", + "lists.new.title_placeholder": "Заголовок списка", + "lists.search": "Искать из ваших подписок", + "lists.subheading": "Ваши списки", "loading_indicator.label": "Загрузка...", "media_gallery.toggle_visible": "Показать/скрыть", "missing_indicator.label": "Не найдено", - "mute_modal.hide_notifications": "Hide notifications from this user?", + "missing_indicator.sublabel": "Запрашиваемый ресурс не найден", + "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.lists": "Lists", + "navigation_bar.keyboard_shortcuts": "Сочетания клавиш", + "navigation_bar.lists": "Списки", "navigation_bar.logout": "Выйти", "navigation_bar.mutes": "Список глушения", - "navigation_bar.pins": "Pinned toots", + "navigation_bar.pins": "Закреплённые посты", "navigation_bar.preferences": "Опции", "navigation_bar.public_timeline": "Глобальная лента", "notification.favourite": "{name} понравился Ваш статус", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "Домашняя лента показывает посты от тех, на кого Вы подписаны.", "onboarding.page_four.notifications": "Колонка уведомлений сообщает о взаимодействии с Вами других людей.", "onboarding.page_one.federation": "Mastodon - это сеть независимых серверов, которые вместе образуют единую социальную сеть. Мы называем эти сервера узлами.", - "onboarding.page_one.handle": "Вы находитесь на {domain}, поэтому Ваше полное имя пользователя - {handle}", + "onboarding.page_one.full_handle": "Всё в ваших руках", + "onboarding.page_one.handle_hint": "Это то, что вы посоветуете искать своим друзьям.", "onboarding.page_one.welcome": "Добро пожаловать в Mastodon!", "onboarding.page_six.admin": "Админ Вашего узла - {admin}.", "onboarding.page_six.almost_done": "Почти готово...", @@ -197,24 +203,33 @@ "privacy.public.short": "Публичный", "privacy.unlisted.long": "Не показывать в лентах", "privacy.unlisted.short": "Скрытый", + "regeneration_indicator.label": "Загрузка…", + "regeneration_indicator.sublabel": "Ваша домашняя лента готовится!", "relative_time.days": "{number}д", "relative_time.hours": "{number}ч", "relative_time.just_now": "только что", "relative_time.minutes": "{number}м", "relative_time.seconds": "{number}с", "reply_indicator.cancel": "Отмена", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Комментарий", "report.submit": "Отправить", "report.target": "Жалуемся на", "search.placeholder": "Поиск", "search_popout.search_format": "Продвинутый формат поиска", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "хэштег", "search_popout.tips.status": "статус", "search_popout.tips.text": "Простой ввод текста покажет совпадающие имена пользователей, отображаемые имена и хэштеги", "search_popout.tips.user": "пользователь", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {результат} few {результата} many {результатов} other {результатов}}", "standalone.public_title": "Прямо сейчас", - "status.block": "Block @{name}", + "status.block": "Заблокировать @{name}", "status.cannot_reblog": "Этот статус не может быть продвинут", "status.delete": "Удалить", "status.embed": "Встроить", @@ -223,10 +238,11 @@ "status.media_hidden": "Медиаконтент скрыт", "status.mention": "Упомянуть @{name}", "status.more": "Больше", - "status.mute": "Mute @{name}", + "status.mute": "Заглушить @{name}", "status.mute_conversation": "Заглушить тред", "status.open": "Развернуть статус", "status.pin": "Закрепить в профиле", + "status.pinned": "Pinned toot", "status.reblog": "Продвинуть", "status.reblogged_by": "{name} продвинул(а)", "status.reply": "Ответить", @@ -239,15 +255,15 @@ "status.show_more": "Развернуть", "status.unmute_conversation": "Снять глушение с треда", "status.unpin": "Открепить от профиля", - "tabs_bar.compose": "Написать", "tabs_bar.federated_timeline": "Глобальная", "tabs_bar.home": "Главная", "tabs_bar.local_timeline": "Локальная", "tabs_bar.notifications": "Уведомления", - "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", + "ui.beforeunload": "Ваш черновик будет утерян, если вы покинете Mastodon.", "upload_area.title": "Перетащите сюда, чтобы загрузить", "upload_button.label": "Добавить медиаконтент", "upload_form.description": "Описать для людей с нарушениями зрения", + "upload_form.focus": "Crop", "upload_form.undo": "Отменить", "upload_progress.label": "Загрузка...", "video.close": "Закрыть видео", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index c38bfd3c6..bb86165ad 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -1,32 +1,36 @@ { "account.block": "Blokovať @{name}", - "account.block_domain": "Blokovať všetko z {domain}", - "account.disclaimer_full": "Inofrmácie nižšie nemusia reflektovať použivateľský účet kompletne.", + "account.block_domain": "Ukryť všetko z {domain}", + "account.blocked": "Blokovaný/á", + "account.disclaimer_full": "Inofrmácie nižšie nemusia byť úplným odrazom uživateľovho účtu.", + "account.domain_blocked": "Doména ukrytá", "account.edit_profile": "Upraviť profil", - "account.follow": "Sledovať", + "account.follow": "Následovať", "account.followers": "Sledujúci", - "account.follows": "Sledovaní", - "account.follows_you": "Sleduje teba", - "account.hide_reblogs": "Hide boosts from @{name}", - "account.media": "Média", - "account.mention": "Napísať @{name}", - "account.moved_to": "{name} has moved to:", + "account.follows": "Sledujete", + "account.follows_you": "Následuje ťa", + "account.hide_reblogs": "Skryť povýšenia od @{name}", + "account.media": "Médiá", + "account.mention": "Spomeňte @{name}", + "account.moved_to": "{name} sa presunul/a na:", "account.mute": "Ignorovať @{name}", - "account.mute_notifications": "Mute notifications from @{name}", - "account.posts": "Správ", + "account.mute_notifications": "Stĺmiť notifikácie od @{name}", + "account.muted": "Utíšený/á", + "account.posts": "Hlášky", + "account.posts_with_replies": "Príspevky s odpoveďami", "account.report": "Nahlásiť @{name}", - "account.requested": "Čaká na schválenie. Klikni na zrušenie žiadosti", + "account.requested": "Čaká na schválenie. Kliknite pre zrušenie žiadosti", "account.share": "Zdieľať @{name} profil", - "account.show_reblogs": "Show boosts from @{name}", + "account.show_reblogs": "Zobraziť povýšenia od @{name}", "account.unblock": "Odblokovať @{name}", "account.unblock_domain": "Prestať blokovať {domain}", "account.unfollow": "Prestať nasledovať", "account.unmute": "Prestať ignorovať @{name}", - "account.unmute_notifications": "Unmute notifications from @{name}", + "account.unmute_notifications": "Odtĺmiť notifikácie od @{name}", "account.view_full_profile": "Pozri celý profil", - "boost_modal.combo": "Nabudúce môžeš kliknúť {combo} a preskočiť", + "boost_modal.combo": "Nabudúce môžete kliknúť {combo} aby ste preskočili", "bundle_column_error.body": "Nastala chyba pri načítaní tohto komponentu.", - "bundle_column_error.retry": "Skús znova", + "bundle_column_error.retry": "Skúste znova", "bundle_column_error.title": "Chyba siete", "bundle_modal_error.close": "Zatvoriť", "bundle_modal_error.message": "Nastala chyba pri načítaní tohto komponentu.", @@ -34,9 +38,9 @@ "column.blocks": "Blokovaní používatelia", "column.community": "Lokálna časová os", "column.favourites": "Obľúbené", - "column.follow_requests": "Žiadosti", - "column.home": "Moja časová os", - "column.lists": "Lists", + "column.follow_requests": "Žiadosti o sledovaní", + "column.home": "Domov", + "column.lists": "Zoznamy", "column.mutes": "Ignorovaní používatelia", "column.notifications": "Notifikácie", "column.pins": "Pripnuté toots", @@ -50,210 +54,222 @@ "column_header.unpin": "Odopnúť", "column_subheading.navigation": "Navigácia", "column_subheading.settings": "Nastavenia", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", - "compose_form.lock_disclaimer": "Tvoj účet nie je zamknutý. Ktokoľvek ťa môže nasledovať a vidieť tvoje správy pre sledujúcich.", + "compose_form.hashtag_warning": "Tento toot nebude zobrazený pod žiadným haštagom lebo nieje listovaný. Iba verejné toots môžu byť nájdené podľa haštagu.", + "compose_form.lock_disclaimer": "Váš účet nie je zamknutý. Ktokoľvek ťa môže nasledovať a vidieť tvoje správy pre sledujúcich.", "compose_form.lock_disclaimer.lock": "zamknutý", - "compose_form.placeholder": "Čo máš na mysli?", + "compose_form.placeholder": "Na čo myslíš?", "compose_form.publish": "Toot", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive": "Označ súbor ako chúlostivý", + "compose_form.sensitive": "Označ médiá ako chúlostivé", "compose_form.spoiler": "Skryť text za varovanie", - "compose_form.spoiler_placeholder": "Napíš sem tvoje varovanie", + "compose_form.spoiler_placeholder": "Sem napíšte vaše varovanie", "confirmation_modal.cancel": "Zrušiť", "confirmations.block.confirm": "Blokovať", - "confirmations.block.message": "Naozaj chceš blokovať {name}?", + "confirmations.block.message": "Ste si istý, že chcete blokovať {name}?", "confirmations.delete.confirm": "Zmazať", - "confirmations.delete.message": "Naozaj chceš zmazať túto správu?", - "confirmations.delete_list.confirm": "Delete", - "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", + "confirmations.delete.message": "Naozaj chcete vymazať túto správu?", + "confirmations.delete_list.confirm": "Vymazať", + "confirmations.delete_list.message": "Ste si istý/á, že chceťe navždy vymazať tento zoznam?", "confirmations.domain_block.confirm": "Skryť celú doménu", - "confirmations.domain_block.message": "Si si naozaj istý, že chceš blokovať celú {domain}? Vo väčšine prípadov stačí blokovať alebo ignorovať daných používateľov.", + "confirmations.domain_block.message": "Ste si naozaj istý, že chcete blokovať celú {domain}? Vo väčšine prípadov stačí blokovať alebo ignorovať daných používateľov, čiže to sa doporučuje.", "confirmations.mute.confirm": "Ignoruj", - "confirmations.mute.message": "Naozaj chceš ignorovať {name}?", + "confirmations.mute.message": "Naozaj chcete ignorovať {name}?", "confirmations.unfollow.confirm": "Nesledovať", - "confirmations.unfollow.message": "Naozaj chceš prestať sledovať {name}?", - "embed.instructions": "Skopíruj kód nižšie a ridaj tento status na tvoju web stránku.", - "embed.preview": "Tu je ukážka ako to bude vyzerať:", - "emoji_button.activity": "Aktivity", + "confirmations.unfollow.message": "Naozaj chcete prestať sledovať {name}?", + "embed.instructions": "Umiestnite kód uvedený nižšie pre pridanie tohto statusu na vašu web stránku.", + "embed.preview": "Tu je ako to bude vyzerať:", + "emoji_button.activity": "Aktivita", "emoji_button.custom": "Vlastné", "emoji_button.flags": "Vlajky", "emoji_button.food": "Jedlá a nápoje", - "emoji_button.label": "Vlož emoji", - "emoji_button.nature": "Zvieratká", - "emoji_button.not_found": "Nenájdené", + "emoji_button.label": "Vložiť emotikony", + "emoji_button.nature": "Prírodné", + "emoji_button.not_found": "Nie emotikony!! (╯°□°)╯︵ ┻━┻", "emoji_button.objects": "Predmety", "emoji_button.people": "Ľudia", "emoji_button.recent": "Často používané", - "emoji_button.search": "Hľadaj...", + "emoji_button.search": "Hľadať...", "emoji_button.search_results": "Nájdené", "emoji_button.symbols": "Symboly", "emoji_button.travel": "Cestovanie a miesta", - "empty_column.community": "Lokálna časová os je prázdna. Napíš niečo aby sa to začalo hýbať!", - "empty_column.hashtag": "Ešte nič nie je v tomto hashtag-u.", - "empty_column.home": "Ešte nesleduješ nikoho. Pre začiatok pozri {public} alebo použi vyhľadávanie aby si našiel ostatných používateľov.", - "empty_column.home.public_timeline": "verejnú časovú os", - "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", - "empty_column.notifications": "Nemáš žiadne notifikácie. Napíš niekomu, nasleduj niekoho alebo komunikuj s ostatnými.", - "empty_column.public": "Ešte tu nič nie je. Napíš niečo verejne alebo začni sledovať používateľov z iných Mastodon serverov aby tu niečo bolo", - "follow_request.authorize": "Potvrdiť", + "empty_column.community": "Lokálna časová os je prázdna. Napíšte niečo, aby sa to tu začalo hýbať!", + "empty_column.hashtag": "Pod týmto hashtagom sa ešte nič nenachádza.", + "empty_column.home": "Vaša lokálna osa je zatiaľ prázdna! Pre začiatok pozrite {public} alebo použite vyhľadávanie a nájdite tak ostatných používateľov.", + "empty_column.home.public_timeline": "verejná časová os", + "empty_column.list": "Tento zoznam je ešte prázdny. Keď ale členovia tohoto zoznamu napíšu nové správy, tak tie sa objavia priamo tu.", + "empty_column.notifications": "Nemáte ešte žiadne notifikácie. Napíšte niekomu, následujte niekoho a komunikujte s ostatnými aby diskusia mohla začať.", + "empty_column.public": "Ešte tu nič nie je. Napíšte niečo verejne alebo začnite sledovať používateľov z iných Mastodon serverov aby tu niečo pribudlo", + "follow_request.authorize": "Povoliť prístup", "follow_request.reject": "Odmietnúť", "getting_started.appsshort": "Aplikácie", "getting_started.faq": "FAQ", "getting_started.heading": "Začíname", - "getting_started.open_source_notice": "Mastodon má otvorený kód. Reportovať chyby alebo prispievať vlastným kódom môžeš na GitHube v {github}.", + "getting_started.open_source_notice": "Mastodon má otvorený kód. Nahlásiť chyby, alebo prispievať vlastným kódom môžete na GitHube v {github}.", "getting_started.userguide": "Používateľská príručka", "home.column_settings.advanced": "Rozšírené", "home.column_settings.basic": "Základné", "home.column_settings.filter_regex": "Filtrovať použitím regulárnych výrazov", - "home.column_settings.show_reblogs": "Zobraziť boosts", + "home.column_settings.show_reblogs": "Zobraziť povýšené", "home.column_settings.show_replies": "Zobraziť odpovede", "home.settings": "Nastavenia stĺpcov", - "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": "Zavrieť", - "lightbox.next": "Ďalší", + "keyboard_shortcuts.back": "dostať sa naspäť", + "keyboard_shortcuts.boost": "vyzdvihnúť", + "keyboard_shortcuts.column": "zamerať sa na status v jednom zo stĺpcov", + "keyboard_shortcuts.compose": "zamerať sa na písaciu plochu", + "keyboard_shortcuts.description": "Popis", + "keyboard_shortcuts.down": "posunúť sa dole v zozname", + "keyboard_shortcuts.enter": "otvoriť správu", + "keyboard_shortcuts.favourite": "pridať do obľúbených", + "keyboard_shortcuts.heading": "Klávesové skratky", + "keyboard_shortcuts.hotkey": "Klávesa", + "keyboard_shortcuts.legend": "zobraziť túto legendu", + "keyboard_shortcuts.mention": "spomenúť autora", + "keyboard_shortcuts.reply": "odpovedať", + "keyboard_shortcuts.search": "zamerať sa na vyhľadávanie", + "keyboard_shortcuts.toot": "začať úplne novú hlášku", + "keyboard_shortcuts.unfocus": "nesústrediť sa na písaciu plochu, alebo hľadanie", + "keyboard_shortcuts.up": "posunúť sa vyššie v zozname", + "lightbox.close": "Zatvoriť", + "lightbox.next": "Ďalšie", "lightbox.previous": "Predchádzajúci", - "lists.account.add": "Add to list", - "lists.account.remove": "Remove from list", - "lists.delete": "Delete list", - "lists.edit": "Edit list", - "lists.new.create": "Add list", - "lists.new.title_placeholder": "New list title", - "lists.search": "Search among people you follow", - "lists.subheading": "Your lists", + "lists.account.add": "Pridať do zoznamu", + "lists.account.remove": "Odobrať zo zoznamu", + "lists.delete": "Vymazať list", + "lists.edit": "Upraviť zoznam", + "lists.new.create": "Pridať zoznam", + "lists.new.title_placeholder": "Názov nového zoznamu", + "lists.search": "Vyhľadávajte medzi užívateľmi ktorých sledujete", + "lists.subheading": "Vaše zoznamy", "loading_indicator.label": "Nahrávam...", "media_gallery.toggle_visible": "Zapnúť/Vypnúť viditeľnosť", "missing_indicator.label": "Nenájdené", - "mute_modal.hide_notifications": "Hide notifications from this user?", + "missing_indicator.sublabel": "Tento zdroj sa nepodarilo nájsť", + "mute_modal.hide_notifications": "Skryť notifikácie od tohoto užívateľa?", "navigation_bar.blocks": "Blokovaní používatelia", "navigation_bar.community_timeline": "Lokálna časová os", "navigation_bar.edit_profile": "Upraviť profil", "navigation_bar.favourites": "Obľúbené", - "navigation_bar.follow_requests": "Žiadosti", + "navigation_bar.follow_requests": "Žiadosti o sledovanie", "navigation_bar.info": "O tomto Mastodon serveri", - "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", - "navigation_bar.lists": "Lists", + "navigation_bar.keyboard_shortcuts": "Klávesové skratky", + "navigation_bar.lists": "Zoznamy", "navigation_bar.logout": "Odhlásiť", "navigation_bar.mutes": "Ignorovaní používatelia", "navigation_bar.pins": "Pripnuté toots", "navigation_bar.preferences": "Možnosti", "navigation_bar.public_timeline": "Federovaná časová os", "notification.favourite": "{name} sa páči tvoj status", - "notification.follow": "{name} ťa začal(a) sledovať", - "notification.mention": "{name} ťa zmienil", + "notification.follow": "{name} ťa začal/a následovať", + "notification.mention": "{name} ťa spomenul/a", "notification.reblog": "{name} re-tootol tvoj status", - "notifications.clear": "Vymazať notifikácie", - "notifications.clear_confirmation": "Naozaj chceš vymazať všetky tvoje notifikácie?", - "notifications.column_settings.alert": "Bublinové notifikácie", + "notifications.clear": "Vyčistiť zoznam notifikácii", + "notifications.clear_confirmation": "Naozaj chcete nenávratne prečistiť všetky vaše notifikácie?", + "notifications.column_settings.alert": "Notifikácie na ploche", "notifications.column_settings.favourite": "Obľúbené:", - "notifications.column_settings.follow": "Nový nasledujúci:", + "notifications.column_settings.follow": "Noví následujúci:", "notifications.column_settings.mention": "Zmienenia:", "notifications.column_settings.push": "Push notifikácie", "notifications.column_settings.push_meta": "Toto zariadenie", - "notifications.column_settings.reblog": "Re-toots:", + "notifications.column_settings.reblog": "Boosty:", "notifications.column_settings.show": "Zobraziť v stĺpci", "notifications.column_settings.sound": "Prehrať zvuk", "onboarding.done": "Koniec", "onboarding.next": "Ďalej", - "onboarding.page_five.public_timelines": "Lokálna časová os zobrazuje verejné správy od všetkých na {domain}. Federovaná časová os zobrazuje verejné správy od všetkých ľudí ktoré {domain} nasleduje. Tieto sú takzvané Verejné Časové Osi, výborná možnosť ako nájsť a spoznať nových ľudí.", - "onboarding.page_four.home": "Domovská časová os zobrazí správy od ľudí ktorých sleduješ.", - "onboarding.page_four.notifications": "Stĺpec s notifikáciami zobrazí keď budeš s niekým komunikovať.", - "onboarding.page_one.federation": "Mastodon je sieť nezávislých serverov spojením ktorých vzniká jedna veľká federovaná sociálna sieť.", - "onboarding.page_one.handle": "Ty si na {domain}, takže tvoje celý nickname je {handle}", - "onboarding.page_one.welcome": "Vitajte v Mastodon!", + "onboarding.page_five.public_timelines": "Lokálna časová os zobrazuje verejné správy od všetkých na {domain}. Federovaná časová os zobrazuje verejné správy od všetkých tých, čo následujú užívatrľov {domain} z iných serverov. Tieto sú takzvané Verejné Časové Osi, výborná možnosť ako nájsť a spoznať nových ľudí.", + "onboarding.page_four.home": "Domovská časová os zobrazí správy od ľudí ktorých sledujete.", + "onboarding.page_four.notifications": "Stĺpec s notifikáciami zobrazí keď budete s niekým komunikovať.", + "onboarding.page_one.federation": "Mastodon je sieť nezávislých serverov, spojením ktorých vzniká jedna veľká federovaná sociálna sieť.", + "onboarding.page_one.full_handle": "Vaša celá prezývka aj s adresou", + "onboarding.page_one.handle_hint": "Toto je čo by ste povedali vaším priateľom že majú hľadať.", + "onboarding.page_one.welcome": "Vitajte na Mastodone!", "onboarding.page_six.admin": "Správca tohto servera je {admin}.", "onboarding.page_six.almost_done": "Takmer hotovo...", "onboarding.page_six.appetoot": "Bon Appetoot!", "onboarding.page_six.apps_available": "Aplikácie {apps} sú dostupné na pre iOS, Android and ďalšie platformy.", - "onboarding.page_six.github": "Mastodon je free open-source software. Chyby, nové funkcie alebo prispievať svojím kódom mǒžeš na {github}.", + "onboarding.page_six.github": "Mastodon je free open-source software. Nahlásiť chyby, zaujímať sa o nové funkcie, alebo prispievať svojím kódom mǒžeete na {github}.", "onboarding.page_six.guidelines": "pravidlá komunity", "onboarding.page_six.read_guidelines": "Prosím prečítajte si {domain} pravidlá {guidelines}!", "onboarding.page_six.various_app": "mobilné applikácie", - "onboarding.page_three.profile": "Uprav svoj profile a zmeň svoj avatar, bio a meno ktoré bude zobrazené. V nastaveniach nájdeš ďalšie možnosti.", - "onboarding.page_three.search": "Použi vyhľadávacie políčko na nájdenie ľudí a hashtagov, ako napríklad {slovensko}, {slovakia} alebo {pivo}. Na nájdenie človeka ktorý je registrovaný na inom Mastodon serveri použi jeho celý nickname.", - "onboarding.page_two.compose": "Správy píš zo stĺpca na komponovanie. Môžeš nahrávať obrázky, meniť nastavenia súkromia správ a pridávať varovania ikonkami nižšie.", + "onboarding.page_three.profile": "Upravte svoj profil ak chcete zmeňiť svoj avatar, popis profilu a meno ktoré bude zobrazené. V nastaveniach nájdete ďalšie možnosti.", + "onboarding.page_three.search": "Použite vyhľadávacie políčko na nájdenie ľudí a hashtagov, ako napríklad {slovensko}, {slovakia} alebo {pivo}. Na nájdenie človeka ktorý je registrovaný na inom Mastodon serveri použi jeho celý nickname.", + "onboarding.page_two.compose": "Správy píšte zo stĺpca na komponovanie. Je možné nahrávať obrázky, meniť nastavenia súkromia správ a pridávať varovania ikonkami nižšie.", "onboarding.skip": "Preskočiť", - "privacy.change": "Zmeň viditeľnosť statusu", - "privacy.direct.long": "Pošli priamo iba spomenutým používateľom", + "privacy.change": "Zmeňiť viditeľnosť statusu", + "privacy.direct.long": "Poslať priamo iba spomenutým používateľom", "privacy.direct.short": "Súkromne", - "privacy.private.long": "Pošli iba sledujúcim", - "privacy.private.short": "Iba sledujúci", - "privacy.public.long": "Pošli všetkým", - "privacy.public.short": "Verejne", + "privacy.private.long": "Poslať iba následovateľom", + "privacy.private.short": "Iba pre sledujúcich", + "privacy.public.long": "Poslať všetkým verejne", + "privacy.public.short": "Verejné", "privacy.unlisted.long": "Neposielať do verejných časových osí", - "privacy.unlisted.short": "Verejne mimo osí", + "privacy.unlisted.short": "Verejne, ale nezobraziť v osi", + "regeneration_indicator.label": "Načítava sa…", + "regeneration_indicator.sublabel": "Vaša domovská nástenka sa pripravuje!", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", - "relative_time.just_now": "now", + "relative_time.just_now": "teraz", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "reply_indicator.cancel": "Zrušiť", + "report.forward": "Posuň ku {target}", + "report.forward_hint": "Tento účet je z iného serveru. Chceš poslať anonymnú kópiu reportu aj tam?", + "report.hint": "Toto nahlásenie bude zaslané správcom servera. Môžeš napísať odvôvodnenie prečo si nahlásil/a tento účet:", "report.placeholder": "Ďalšie komentáre", "report.submit": "Poslať", - "report.target": "Reportovať {target}", - "search.placeholder": "Hľadaj", - "search_popout.search_format": "Advanced search format", - "search_popout.tips.hashtag": "hashtag", + "report.target": "Nahlásenie {target}", + "search.placeholder": "Hľadať", + "search_popout.search_format": "Pokročilý formát vyhľadávania", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.tips.hashtag": "haštag", "search_popout.tips.status": "status", - "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", - "search_popout.tips.user": "user", - "search_results.total": "{count, number} nájdených", - "standalone.public_title": "Čo tam nájdeš...", - "status.block": "Block @{name}", + "search_popout.tips.text": "Jednoduchý text vráti zhodujúce sa mená, prezývky a hashtagy", + "search_popout.tips.user": "používateľ", + "search_results.accounts": "Ľudia", + "search_results.hashtags": "Haštagy", + "search_results.statuses": "Príspevky", + "search_results.total": "{count, number} {count, plural, one {result} ostatné {results}}", + "standalone.public_title": "Pohľad dovnútra...", + "status.block": "Blokovať @{name}", "status.cannot_reblog": "Tento príspevok nemôže byť re-tootnutý", "status.delete": "Zmazať", - "status.embed": "Embed", + "status.embed": "Vložiť", "status.favourite": "Páči sa mi", - "status.load_more": "Zobraziť viac", + "status.load_more": "Zobraz viac", "status.media_hidden": "Skryté médiá", "status.mention": "Napísať @{name}", - "status.more": "More", - "status.mute": "Mute @{name}", + "status.more": "Viac", + "status.mute": "Utíšiť @{name}", "status.mute_conversation": "Ignorovať konverzáciu", - "status.open": "Otvoriť", + "status.open": "Otvoriť tento status", "status.pin": "Pripnúť na profil", - "status.reblog": "Re-toot", - "status.reblogged_by": "{name} re-tootol", + "status.pinned": "Pripnutý príspevok", + "status.reblog": "Povýšiť", + "status.reblogged_by": "{name} povýšil", "status.reply": "Odpovedať", - "status.replyAll": "Odpovedať všetkým", + "status.replyAll": "Odpovedať na diskusiu", "status.report": "Nahlásiť @{name}", - "status.sensitive_toggle": "Klikni pre zobrazenie", + "status.sensitive_toggle": "Kliknite pre zobrazenie", "status.sensitive_warning": "Chúlostivý obsah", "status.share": "Zdieľať", - "status.show_less": "Zobraziť menej", - "status.show_more": "Zobraziť viac", + "status.show_less": "Zobraz menej", + "status.show_more": "Zobraz viac", "status.unmute_conversation": "Prestať ignorovať konverzáciu", "status.unpin": "Odopnúť z profilu", - "tabs_bar.compose": "Napísať", "tabs_bar.federated_timeline": "Federovaná", "tabs_bar.home": "Domov", - "tabs_bar.local_timeline": "Local", + "tabs_bar.local_timeline": "Lokálna", "tabs_bar.notifications": "Notifikácie", - "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", + "ui.beforeunload": "Čo máte rozpísané sa stratí, ak opustíte Mastodon.", "upload_area.title": "Ťahaj a pusti pre nahratie", - "upload_button.label": "Pridať", - "upload_form.description": "Describe for the visually impaired", - "upload_form.undo": "Späť", - "upload_progress.label": "Nahrávam...", + "upload_button.label": "Pridať médiá", + "upload_form.description": "Opis pre slabo vidiacich", + "upload_form.focus": "Vystrihni", + "upload_form.undo": "Navrátiť", + "upload_progress.label": "Nahráva sa...", "video.close": "Zavrieť video", "video.exit_fullscreen": "Vpnúť zobrazenie na celú obrazovku", "video.expand": "Zväčšiť video", - "video.fullscreen": "Zapnúť zobrazenie na celú obrazovku", + "video.fullscreen": "Zobraziť na celú obrazovku", "video.hide": "Skryť video", "video.mute": "Vypnúť zvuk", "video.pause": "Pauza", diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json index 88631e332..a672ae6ca 100644 --- a/app/javascript/mastodon/locales/sr-Latn.json +++ b/app/javascript/mastodon/locales/sr-Latn.json @@ -1,7 +1,9 @@ { "account.block": "Blokiraj korisnika @{name}", "account.block_domain": "Sakrij sve sa domena {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "Navedene informacije možda ne odslikavaju korisnički profil u potpunosti.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Izmeni profil", "account.follow": "Zaprati", "account.followers": "Pratioca", @@ -13,7 +15,9 @@ "account.moved_to": "{name} se pomerio na:", "account.mute": "Ućutkaj korisnika @{name}", "account.mute_notifications": "Isključi obaveštenja od korisnika @{name}", + "account.muted": "Muted", "account.posts": "Statusa", + "account.posts_with_replies": "Toots with replies", "account.report": "Prijavi @{name}", "account.requested": "Čekam odobrenje. Kliknite da poništite zahtev za praćenje", "account.share": "Podeli profil korisnika @{name}", @@ -139,6 +143,7 @@ "loading_indicator.label": "Učitavam...", "media_gallery.toggle_visible": "Uključi/isključi vidljivost", "missing_indicator.label": "Nije pronađeno", + "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Sakrij obaveštenja od ovog korisnika?", "navigation_bar.blocks": "Blokirani korisnici", "navigation_bar.community_timeline": "Lokalna lajna", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "Početna lajna prikazuje statuse ljudi koje Vi pratite.", "onboarding.page_four.notifications": "Kolona sa obaveštenjima Vam prikazuje kada neko priča sa Vama.", "onboarding.page_one.federation": "Mastodont je mreža nezavisnih servera koji se uvezuju da naprave jednu veću društvenu mrežu. Ove servere zovemo instancama.", - "onboarding.page_one.handle": "Vi ste na domenu {domain}, pa je Vaša puna identifikacija {handle}", + "onboarding.page_one.full_handle": "Your full handle", + "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.", "onboarding.page_one.welcome": "Dobrodošli na Mastodont!", "onboarding.page_six.admin": "Administrator Vaše instance je {admin}.", "onboarding.page_six.almost_done": "Još malo, pa gotovo...", @@ -197,21 +203,30 @@ "privacy.public.short": "Javno", "privacy.unlisted.long": "Ne objavljuj na javnim lajnama", "privacy.unlisted.short": "Neizlistano", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", "relative_time.just_now": "sada", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "reply_indicator.cancel": "Poništi", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Dodatni komentari", "report.submit": "Pošalji", "report.target": "Prijavljujem {target}", "search.placeholder": "Pretraga", "search_popout.search_format": "Napredni format pretrage", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "hešteg", "search_popout.tips.status": "status", "search_popout.tips.text": "Traženjem običnog teksta ćete dobiti sva pronađena imena, sva korisnička imena i sve nađene heštegove", "search_popout.tips.user": "korisnik", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {rezultat} few {rezultata} other {rezultata}}", "standalone.public_title": "Pogled iznutra...", "status.block": "Block @{name}", @@ -227,6 +242,7 @@ "status.mute_conversation": "Ućutkaj prepisku", "status.open": "Proširi ovaj status", "status.pin": "Prikači na profil", + "status.pinned": "Pinned toot", "status.reblog": "Podrži", "status.reblogged_by": "{name} podržao(la)", "status.reply": "Odgovori", @@ -239,7 +255,6 @@ "status.show_more": "Prikaži više", "status.unmute_conversation": "Uključi prepisku", "status.unpin": "Otkači sa profila", - "tabs_bar.compose": "Napiši", "tabs_bar.federated_timeline": "Federisano", "tabs_bar.home": "Početna", "tabs_bar.local_timeline": "Lokalno", @@ -248,6 +263,7 @@ "upload_area.title": "Prevucite ovde da otpremite", "upload_button.label": "Dodaj multimediju", "upload_form.description": "Opiši za slabovide osobe", + "upload_form.focus": "Crop", "upload_form.undo": "Opozovi", "upload_progress.label": "Otpremam...", "video.close": "Zatvori video", diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json index e65c02ab7..1e3a3ce2b 100644 --- a/app/javascript/mastodon/locales/sr.json +++ b/app/javascript/mastodon/locales/sr.json @@ -1,7 +1,9 @@ { "account.block": "Блокирај корисника @{name}", "account.block_domain": "Сакриј све са домена {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "Наведене информације можда не одсликавају кориснички профил у потпуности.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Измени профил", "account.follow": "Запрати", "account.followers": "Пратиоца", @@ -13,7 +15,9 @@ "account.moved_to": "{name} се померио на:", "account.mute": "Ућуткај корисника @{name}", "account.mute_notifications": "Искључи обавештења од корисника @{name}", + "account.muted": "Muted", "account.posts": "Статуса", + "account.posts_with_replies": "Toots with replies", "account.report": "Пријави @{name}", "account.requested": "Чекам одобрење. Кликните да поништите захтев за праћење", "account.share": "Подели профил корисника @{name}", @@ -139,6 +143,7 @@ "loading_indicator.label": "Учитавам...", "media_gallery.toggle_visible": "Укључи/искључи видљивост", "missing_indicator.label": "Није пронађено", + "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Сакриј обавештења од овог корисника?", "navigation_bar.blocks": "Блокирани корисници", "navigation_bar.community_timeline": "Локална лајна", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "Почетна лајна приказује статусе људи које Ви пратите.", "onboarding.page_four.notifications": "Колона са обавештењима Вам приказује када неко прича са Вама.", "onboarding.page_one.federation": "Мастодонт је мрежа независних сервера који се увезују да направе једну већу друштвену мрежу. Ове сервере зовемо инстанцама.", - "onboarding.page_one.handle": "Ви сте на домену {domain}, па је Ваша пуна идентификација {handle}", + "onboarding.page_one.full_handle": "Your full handle", + "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.", "onboarding.page_one.welcome": "Добродошли на Мастодонт!", "onboarding.page_six.admin": "Администратор Ваше инстанце је {admin}.", "onboarding.page_six.almost_done": "Још мало, па готово...", @@ -197,21 +203,30 @@ "privacy.public.short": "Јавно", "privacy.unlisted.long": "Не објављуј на јавним лајнама", "privacy.unlisted.short": "Неизлистано", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", "relative_time.just_now": "сада", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "reply_indicator.cancel": "Поништи", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Додатни коментари", "report.submit": "Пошаљи", "report.target": "Пријављујем {target}", "search.placeholder": "Претрага", "search_popout.search_format": "Напредни формат претраге", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "хештег", "search_popout.tips.status": "статус", "search_popout.tips.text": "Тражењем обичног текста ћете добити сва пронађена имена, сва корисничка имена и све нађене хештегове", "search_popout.tips.user": "корисник", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {резултат} few {резултата} other {резултата}}", "standalone.public_title": "Поглед изнутра...", "status.block": "Block @{name}", @@ -227,6 +242,7 @@ "status.mute_conversation": "Ућуткај преписку", "status.open": "Прошири овај статус", "status.pin": "Прикачи на профил", + "status.pinned": "Pinned toot", "status.reblog": "Подржи", "status.reblogged_by": "{name} подржао(ла)", "status.reply": "Одговори", @@ -239,7 +255,6 @@ "status.show_more": "Прикажи више", "status.unmute_conversation": "Укључи преписку", "status.unpin": "Откачи са профила", - "tabs_bar.compose": "Напиши", "tabs_bar.federated_timeline": "Федерисано", "tabs_bar.home": "Почетна", "tabs_bar.local_timeline": "Локално", @@ -248,6 +263,7 @@ "upload_area.title": "Превуците овде да отпремите", "upload_button.label": "Додај мултимедију", "upload_form.description": "Опиши за слабовиде особе", + "upload_form.focus": "Crop", "upload_form.undo": "Опозови", "upload_progress.label": "Отпремам...", "video.close": "Затвори видео", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index edfa9b8c2..9c51d5b36 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -1,28 +1,32 @@ { "account.block": "Blockera @{name}", "account.block_domain": "Dölj allt från {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "Informationen nedan kan spegla användarens profil ofullständigt.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Redigera profil", "account.follow": "Följ", "account.followers": "Följare", "account.follows": "Följer", "account.follows_you": "Följer dig", - "account.hide_reblogs": "Hide boosts from @{name}", + "account.hide_reblogs": "Dölj knuffar från @{name}", "account.media": "Media", "account.mention": "Nämna @{name}", - "account.moved_to": "{name} has moved to:", + "account.moved_to": "{name} har flyttat till:", "account.mute": "Tysta @{name}", - "account.mute_notifications": "Mute notifications from @{name}", + "account.mute_notifications": "Stäng av notifieringar från @{name}", + "account.muted": "Muted", "account.posts": "Inlägg", + "account.posts_with_replies": "Toots with replies", "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.show_reblogs": "Visa knuffar från @{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.unmute_notifications": "Återaktivera notifikationer från @{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.", @@ -36,7 +40,7 @@ "column.favourites": "Favoriter", "column.follow_requests": "Följ förfrågningar", "column.home": "Hem", - "column.lists": "Lists", + "column.lists": "Listor", "column.mutes": "Tystade användare", "column.notifications": "Meddelanden", "column.pins": "Nålade toots", @@ -50,7 +54,7 @@ "column_header.unpin": "Ångra fäst", "column_subheading.navigation": "Navigation", "column_subheading.settings": "Inställningar", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.hashtag_warning": "Denna toot kommer inte att listas under någon hashtag eftersom den är onoterad. Endast offentliga toots kan sökas med hashtag.", "compose_form.lock_disclaimer": "Ditt konto är inte {locked}. Vemsomhelst kan följa dig och även se dina inlägg skrivna för endast dina följare.", "compose_form.lock_disclaimer.lock": "låst", "compose_form.placeholder": "Vad funderar du på?", @@ -65,7 +69,7 @@ "confirmations.delete.confirm": "Ta bort", "confirmations.delete.message": "Är du säker att du vill ta bort denna status?", "confirmations.delete_list.confirm": "Delete", - "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", + "confirmations.delete_list.message": "Är du säker på att du vill radera denna lista permanent?", "confirmations.domain_block.confirm": "Blockera hela domänen", "confirmations.domain_block.message": "Är du verkligen, verkligen säker på att du vill blockera hela {domain}? I de flesta fall är några riktade blockeringar eller nedtystade tillräckligt och föredras.", "confirmations.mute.confirm": "Tysta", @@ -92,7 +96,7 @@ "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.public_timeline": "den publika tidslinjen", - "empty_column.list": "There is nothing in this list yet.", + "empty_column.list": "Det finns inget i denna lista än. När medlemmar i denna lista lägger till nya statusar kommer de att visas här.", "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", @@ -108,46 +112,47 @@ "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.back": "att navigera tillbaka", + "keyboard_shortcuts.boost": "att knuffa", + "keyboard_shortcuts.column": "att fokusera en status i en av kolumnerna", + "keyboard_shortcuts.compose": "att fokusera komponera text fältet", "keyboard_shortcuts.description": "Description", - "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.down": "att flytta ner i listan", "keyboard_shortcuts.enter": "to open status", - "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.favourite": "att favorisera", "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", + "keyboard_shortcuts.hotkey": "Snabbvalstangent", + "keyboard_shortcuts.legend": "att visa denna översikt", + "keyboard_shortcuts.mention": "att nämna författaren", + "keyboard_shortcuts.reply": "att svara", + "keyboard_shortcuts.search": "att fokusera sökfältet", + "keyboard_shortcuts.toot": "att börja en helt ny toot", + "keyboard_shortcuts.unfocus": "att avfokusera komponera text fält / sökfält", + "keyboard_shortcuts.up": "att flytta upp i listan", "lightbox.close": "Stäng", "lightbox.next": "Nästa", "lightbox.previous": "Tidigare", - "lists.account.add": "Add to list", - "lists.account.remove": "Remove from list", - "lists.delete": "Delete list", - "lists.edit": "Edit list", - "lists.new.create": "Add list", - "lists.new.title_placeholder": "New list title", - "lists.search": "Search among people you follow", - "lists.subheading": "Your lists", + "lists.account.add": "Lägg till i lista", + "lists.account.remove": "Ta bort från lista", + "lists.delete": "Radera lista", + "lists.edit": "Redigera lista", + "lists.new.create": "Lägg till lista", + "lists.new.title_placeholder": "Ny listrubrik", + "lists.search": "Sök bland personer du följer", + "lists.subheading": "Dina listor", "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?", + "missing_indicator.sublabel": "Den här resursen kunde inte hittas", + "mute_modal.hide_notifications": "Dölj notifikationer från denna användare?", "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.lists": "Lists", + "navigation_bar.keyboard_shortcuts": "Tangentbordsgenvägar", + "navigation_bar.lists": "Listor", "navigation_bar.logout": "Logga ut", "navigation_bar.mutes": "Tystade användare", "navigation_bar.pins": "Nålade inlägg (toots)", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "Hemmatidslinjen visar inlägg från personer du följer.", "onboarding.page_four.notifications": "Meddelandekolumnen visar när någon interagerar med dig.", "onboarding.page_one.federation": "Mastodon är ett nätverk av oberoende servrar som ansluter för att skapa ett större socialt nätverk. Vi kallar dessa servrar instanser.", - "onboarding.page_one.handle": "Du är på {domain}, så din fulla hantering är {handle}", + "onboarding.page_one.full_handle": "Ditt fullständiga användarnamn/mastodonadress", + "onboarding.page_one.handle_hint": "Det här är vad du skulle berätta för dina vänner att söka efter.", "onboarding.page_one.welcome": "Välkommen till Mastodon!", "onboarding.page_six.admin": "Din instansadmin är {admin}.", "onboarding.page_six.almost_done": "Snart klart...", @@ -197,21 +203,30 @@ "privacy.public.short": "Publik", "privacy.unlisted.long": "Skicka inte till publik tidslinje", "privacy.unlisted.short": "Olistad", + "regeneration_indicator.label": "Laddar…", + "regeneration_indicator.sublabel": "Ditt hemmaflöde förbereds!", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", - "relative_time.just_now": "now", + "relative_time.just_now": "nu", "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "reply_indicator.cancel": "Ångra", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Ytterligare kommentarer", "report.submit": "Skicka", "report.target": "Rapporterar {target}", "search.placeholder": "Sök", "search_popout.search_format": "Avancerat sökformat", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "status", "search_popout.tips.text": "Enkel text returnerar matchande visningsnamn, användarnamn och hashtags", "search_popout.tips.user": "användare", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, ett {result} andra {results}}", "standalone.public_title": "En titt inuti...", "status.block": "Block @{name}", @@ -222,11 +237,12 @@ "status.load_more": "Ladda fler", "status.media_hidden": "Media dold", "status.mention": "Omnämn @{name}", - "status.more": "More", - "status.mute": "Mute @{name}", + "status.more": "Mer", + "status.mute": "Tysta @{name}", "status.mute_conversation": "Tysta konversation", "status.open": "Utvidga denna status", "status.pin": "Fäst i profil", + "status.pinned": "Pinned toot", "status.reblog": "Knuff", "status.reblogged_by": "{name} knuffade", "status.reply": "Svara", @@ -239,15 +255,15 @@ "status.show_more": "Visa mer", "status.unmute_conversation": "Öppna konversation", "status.unpin": "Ångra fäst i profil", - "tabs_bar.compose": "Skriv", "tabs_bar.federated_timeline": "Förenad", "tabs_bar.home": "Hem", "tabs_bar.local_timeline": "Lokal", "tabs_bar.notifications": "Meddelanden", - "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", + "ui.beforeunload": "Ditt utkast kommer att förloras om du lämnar 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", + "upload_form.focus": "Crop", "upload_form.undo": "Ångra", "upload_progress.label": "Laddar upp...", "video.close": "Stäng video", @@ -255,7 +271,7 @@ "video.expand": "Expandera video", "video.fullscreen": "Helskärm", "video.hide": "Dölj video", - "video.mute": "Tysta ljud", + "video.mute": "Stäng av ljud", "video.pause": "Pause", "video.play": "Spela upp", "video.unmute": "Spela upp ljud" diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index 06323ebfc..cab2ce089 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -1,7 +1,9 @@ { "account.block": "Block @{name}", "account.block_domain": "Hide everything from {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Edit profile", "account.follow": "Follow", "account.followers": "Followers", @@ -13,7 +15,9 @@ "account.moved_to": "{name} has moved to:", "account.mute": "Mute @{name}", "account.mute_notifications": "Mute notifications from @{name}", + "account.muted": "Muted", "account.posts": "Posts", + "account.posts_with_replies": "Toots with replies", "account.report": "Report @{name}", "account.requested": "Awaiting approval", "account.share": "Share @{name}'s profile", @@ -139,6 +143,7 @@ "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", + "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Blocked users", "navigation_bar.community_timeline": "Local timeline", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "The home timeline shows posts from people you follow.", "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", - "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", + "onboarding.page_one.full_handle": "Your full handle", + "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.", "onboarding.page_one.welcome": "Welcome to Mastodon!", "onboarding.page_six.admin": "Your instance's admin is {admin}.", "onboarding.page_six.almost_done": "Almost done...", @@ -197,21 +203,30 @@ "privacy.public.short": "Public", "privacy.unlisted.long": "Do not post to public timelines", "privacy.unlisted.short": "Unlisted", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", "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": "Cancel", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting", "search.placeholder": "Search", "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "status", "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "standalone.public_title": "A look inside...", "status.block": "Block @{name}", @@ -227,6 +242,7 @@ "status.mute_conversation": "Mute conversation", "status.open": "Expand this status", "status.pin": "Pin on profile", + "status.pinned": "Pinned toot", "status.reblog": "Boost", "status.reblogged_by": "{name} boosted", "status.reply": "Reply", @@ -239,7 +255,6 @@ "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", "status.unpin": "Unpin from profile", - "tabs_bar.compose": "Compose", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Home", "tabs_bar.local_timeline": "Local", @@ -248,6 +263,7 @@ "upload_area.title": "Drag & drop to upload", "upload_button.label": "Add media", "upload_form.description": "Describe for the visually impaired", + "upload_form.focus": "Crop", "upload_form.undo": "Undo", "upload_progress.label": "Uploading...", "video.close": "Close video", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index ce6434ca6..83c10de34 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -1,7 +1,9 @@ { "account.block": "Engelle @{name}", "account.block_domain": "Hide everything from {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Profili düzenle", "account.follow": "Takip et", "account.followers": "Takipçiler", @@ -13,7 +15,9 @@ "account.moved_to": "{name} has moved to:", "account.mute": "Sustur @{name}", "account.mute_notifications": "Mute notifications from @{name}", + "account.muted": "Muted", "account.posts": "Gönderiler", + "account.posts_with_replies": "Toots with replies", "account.report": "Rapor et @{name}", "account.requested": "Onay bekleniyor", "account.share": "Share @{name}'s profile", @@ -139,6 +143,7 @@ "loading_indicator.label": "Yükleniyor...", "media_gallery.toggle_visible": "Görünürlüğü değiştir", "missing_indicator.label": "Bulunamadı", + "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Engellenen kullanıcılar", "navigation_bar.community_timeline": "Yerel zaman tüneli", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "Takip ettiğiniz insanlardan gelen gönderileri gosteren zaman tünelidir", "onboarding.page_four.notifications": "Herkimse sizinle iletişime geçtiğinde gelen bildirimleri gösterir.", "onboarding.page_one.federation": "Mastodon, geniş bir sosyal ağ kurmak için birleşen bağımsız sunuculardan oluşan bir ağdır.", - "onboarding.page_one.handle": "{domain} sunucundasınız, bu yüzden tüm kontrol {handle}", + "onboarding.page_one.full_handle": "Your full handle", + "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.", "onboarding.page_one.welcome": "Mastodon'a hoş geldiniz.", "onboarding.page_six.admin": "{admin}, şu anda bulunduğunuz sunucunun yöneticisidir.", "onboarding.page_six.almost_done": "Neredeyse tamam...", @@ -197,21 +203,30 @@ "privacy.public.short": "Herkese açık", "privacy.unlisted.long": "Herkese açık zaman tüneline gönderme", "privacy.unlisted.short": "Listelenmemiş", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", "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": "İptal", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Ek yorumlar", "report.submit": "Gönder", "report.target": "Raporlama", "search.placeholder": "Ara", "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "status", "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {sonuç} other {sonuçlar}}", "standalone.public_title": "A look inside...", "status.block": "Block @{name}", @@ -227,6 +242,7 @@ "status.mute_conversation": "Mute conversation", "status.open": "Bu gönderiyi genişlet", "status.pin": "Pin on profile", + "status.pinned": "Pinned toot", "status.reblog": "Boost'la", "status.reblogged_by": "{name} boost etti", "status.reply": "Cevapla", @@ -239,7 +255,6 @@ "status.show_more": "Daha fazlası", "status.unmute_conversation": "Unmute conversation", "status.unpin": "Unpin from profile", - "tabs_bar.compose": "Oluştur", "tabs_bar.federated_timeline": "Federe", "tabs_bar.home": "Ana sayfa", "tabs_bar.local_timeline": "Yerel", @@ -248,6 +263,7 @@ "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", + "upload_form.focus": "Crop", "upload_form.undo": "Geri al", "upload_progress.label": "Yükleniyor...", "video.close": "Close video", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 46d22ac83..b49f707e1 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -1,7 +1,9 @@ { "account.block": "Заблокувати", "account.block_domain": "Заглушити {domain}", + "account.blocked": "Blocked", "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "Налаштування профілю", "account.follow": "Підписатися", "account.followers": "Підписники", @@ -13,7 +15,9 @@ "account.moved_to": "{name} has moved to:", "account.mute": "Заглушити", "account.mute_notifications": "Mute notifications from @{name}", + "account.muted": "Muted", "account.posts": "Пости", + "account.posts_with_replies": "Toots with replies", "account.report": "Поскаржитися", "account.requested": "Очікує підтвердження", "account.share": "Share @{name}'s profile", @@ -139,6 +143,7 @@ "loading_indicator.label": "Завантаження...", "media_gallery.toggle_visible": "Показати/приховати", "missing_indicator.label": "Не знайдено", + "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "Заблоковані користувачі", "navigation_bar.community_timeline": "Локальна стрічка", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "Домашня стрічка показує пости користувачів, на яких Ви підписані.", "onboarding.page_four.notifications": "Колонка сповіщень показує моменти, коли хтось звертається до Вас.", "onboarding.page_one.federation": "Mastodon - це мережа незалежних серверів, які разом образовують єдину соціальну мережу. Ми называємо ці сервери інстанціями.", - "onboarding.page_one.handle": "Ви знаходитесь на домені {domain}, тому Ваш повний нік - {handle}", + "onboarding.page_one.full_handle": "Your full handle", + "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.", "onboarding.page_one.welcome": "Ласкаво просимо до Mastodon!", "onboarding.page_six.admin": "Адміністратором Вашої інстанції є {admin}.", "onboarding.page_six.almost_done": "Майже готово...", @@ -197,21 +203,30 @@ "privacy.public.short": "Публічний", "privacy.unlisted.long": "Не показувати у публічних стрічках", "privacy.unlisted.short": "Прихований", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", "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": "Відмінити", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Додаткові коментарі", "report.submit": "Відправити", "report.target": "Скаржимося на", "search.placeholder": "Пошук", "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "status", "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} {count, plural, one {результат} few {результати} many {результатів} other {результатів}}", "standalone.public_title": "A look inside...", "status.block": "Block @{name}", @@ -227,6 +242,7 @@ "status.mute_conversation": "Заглушити діалог", "status.open": "Розгорнути допис", "status.pin": "Pin on profile", + "status.pinned": "Pinned toot", "status.reblog": "Передмухнути", "status.reblogged_by": "{name} передмухнув(-ла)", "status.reply": "Відповісти", @@ -239,7 +255,6 @@ "status.show_more": "Розгорнути", "status.unmute_conversation": "Зняти глушення з діалогу", "status.unpin": "Unpin from profile", - "tabs_bar.compose": "Написати", "tabs_bar.federated_timeline": "Глобальна", "tabs_bar.home": "Головна", "tabs_bar.local_timeline": "Локальна", @@ -248,6 +263,7 @@ "upload_area.title": "Перетягніть сюди, щоб завантажити", "upload_button.label": "Додати медіаконтент", "upload_form.description": "Describe for the visually impaired", + "upload_form.focus": "Crop", "upload_form.undo": "Відмінити", "upload_progress.label": "Завантаження...", "video.close": "Close video", diff --git a/app/javascript/mastodon/locales/whitelist_hy.json b/app/javascript/mastodon/locales/whitelist_hy.json new file mode 100644 index 000000000..0d4f101c7 --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_hy.json @@ -0,0 +1,2 @@ +[ +] diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index a02211b8a..5ccfbc4f4 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -1,7 +1,9 @@ { "account.block": "屏蔽 @{name}", "account.block_domain": "隐藏来自 {domain} 的内容", + "account.blocked": "Blocked", "account.disclaimer_full": "此处显示的信息可能不是全部内容。", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "修改个人资料", "account.follow": "关注", "account.followers": "关注者", @@ -13,7 +15,9 @@ "account.moved_to": "{name} 已经迁移到:", "account.mute": "隐藏 @{name}", "account.mute_notifications": "隐藏来自 @{name} 的通知", + "account.muted": "Muted", "account.posts": "嘟文", + "account.posts_with_replies": "Toots with replies", "account.report": "举报 @{name}", "account.requested": "正在等待对方同意。点击以取消发送关注请求", "account.share": "分享 @{name} 的个人资料", @@ -207,15 +211,22 @@ "relative_time.minutes": "{number}分", "relative_time.seconds": "{number}秒", "reply_indicator.cancel": "取消", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "附言", "report.submit": "提交", "report.target": "举报 {target}", "search.placeholder": "搜索", "search_popout.search_format": "高级搜索格式", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "话题标签", "search_popout.tips.status": "嘟文", "search_popout.tips.text": "使用普通字符进行搜索将会返回昵称、用户名和话题标签", "search_popout.tips.user": "用户", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "共 {count, number} 个结果", "standalone.public_title": "大家都在干啥?", "status.block": "屏蔽 @{name}", @@ -231,6 +242,7 @@ "status.mute_conversation": "隐藏此对话", "status.open": "展开嘟文", "status.pin": "在个人资料页面置顶", + "status.pinned": "Pinned toot", "status.reblog": "转嘟", "status.reblogged_by": "{name} 转嘟了", "status.reply": "回复", @@ -243,7 +255,6 @@ "status.show_more": "显示内容", "status.unmute_conversation": "不再隐藏此对话", "status.unpin": "在个人资料页面取消置顶", - "tabs_bar.compose": "撰写", "tabs_bar.federated_timeline": "跨站", "tabs_bar.home": "主页", "tabs_bar.local_timeline": "本站", @@ -252,6 +263,7 @@ "upload_area.title": "将文件拖放到此处开始上传", "upload_button.label": "上传媒体文件", "upload_form.description": "为视觉障碍人士添加文字说明", + "upload_form.focus": "Crop", "upload_form.undo": "取消上传", "upload_progress.label": "上传中…", "video.close": "关闭视频", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index 7745da622..b105fe426 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -1,7 +1,9 @@ { "account.block": "封鎖 @{name}", "account.block_domain": "隱藏來自 {domain} 的一切文章", + "account.blocked": "Blocked", "account.disclaimer_full": "下列資料不一定完整。", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "修改個人資料", "account.follow": "關注", "account.followers": "關注的人", @@ -13,7 +15,9 @@ "account.moved_to": "{name} has moved to:", "account.mute": "將 @{name} 靜音", "account.mute_notifications": "Mute notifications from @{name}", + "account.muted": "Muted", "account.posts": "文章", + "account.posts_with_replies": "Toots with replies", "account.report": "舉報 @{name}", "account.requested": "等候審批", "account.share": "分享 @{name} 的個人資料", @@ -139,6 +143,7 @@ "loading_indicator.label": "載入中...", "media_gallery.toggle_visible": "打開或關上", "missing_indicator.label": "找不到內容", + "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "被你封鎖的用戶", "navigation_bar.community_timeline": "本站時間軸", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "「主頁」顯示你所關注用戶的文章", "onboarding.page_four.notifications": "「通知」欄顯示你和其他人的互動。", "onboarding.page_one.federation": "Mastodon(萬象社交)是由一批獨立網站組成的龐大網絡,我們將這些獨立又互連網站稱為「服務站」(instance)", - "onboarding.page_one.handle": "你的帳戶在 {domain} 上面,由 {handle} 代理", + "onboarding.page_one.full_handle": "Your full handle", + "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.", "onboarding.page_one.welcome": "歡迎使用 Mastodon(萬象社交)", "onboarding.page_six.admin": "你服務站的管理員是{admin}", "onboarding.page_six.almost_done": "差不多了……", @@ -197,21 +203,30 @@ "privacy.public.short": "公共", "privacy.unlisted.long": "公開,但不在公共時間軸顯示", "privacy.unlisted.short": "公開", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", "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": "取消", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "額外訊息", "report.submit": "提交", "report.target": "舉報", "search.placeholder": "搜尋", "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "status", "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} 項結果", "standalone.public_title": "站點一瞥…", "status.block": "Block @{name}", @@ -227,6 +242,7 @@ "status.mute_conversation": "靜音對話", "status.open": "展開文章", "status.pin": "置頂到資料頁", + "status.pinned": "Pinned toot", "status.reblog": "轉推", "status.reblogged_by": "{name} 轉推", "status.reply": "回應", @@ -239,7 +255,6 @@ "status.show_more": "顯示更多", "status.unmute_conversation": "解禁對話", "status.unpin": "解除置頂", - "tabs_bar.compose": "撰寫", "tabs_bar.federated_timeline": "跨站", "tabs_bar.home": "主頁", "tabs_bar.local_timeline": "本站", @@ -248,6 +263,7 @@ "upload_area.title": "將檔案拖放至此上載", "upload_button.label": "上載媒體檔案", "upload_form.description": "Describe for the visually impaired", + "upload_form.focus": "Crop", "upload_form.undo": "還原", "upload_progress.label": "上載中……", "video.close": "關閉影片", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 65b174ab5..b0a94f67b 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -1,7 +1,9 @@ { "account.block": "封鎖 @{name}", "account.block_domain": "隱藏來自 {domain} 的一切貼文", + "account.blocked": "Blocked", "account.disclaimer_full": "下列資料不一定完整。", + "account.domain_blocked": "Domain hidden", "account.edit_profile": "編輯用者資訊", "account.follow": "關注", "account.followers": "專注者", @@ -13,7 +15,9 @@ "account.moved_to": "{name} has moved to:", "account.mute": "消音 @{name}", "account.mute_notifications": "Mute notifications from @{name}", + "account.muted": "Muted", "account.posts": "貼文", + "account.posts_with_replies": "Toots with replies", "account.report": "檢舉 @{name}", "account.requested": "正在等待許可", "account.share": "分享 @{name} 的用者資訊", @@ -139,6 +143,7 @@ "loading_indicator.label": "讀取中...", "media_gallery.toggle_visible": "切換可見性", "missing_indicator.label": "找不到", + "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", "navigation_bar.blocks": "封鎖的使用者", "navigation_bar.community_timeline": "本地時間軸", @@ -174,7 +179,8 @@ "onboarding.page_four.home": "家時間軸顯示所有你關注的人的貼文。", "onboarding.page_four.notifications": "通知欄顯示別人和你的互動。", "onboarding.page_one.federation": "Mastodon 是由獨立的伺服器連結起來,形成的大社群網路。我們把這些伺服器稱為副本。", - "onboarding.page_one.handle": "你在 {domain} 上,所以你的帳號全名是 {handle}", + "onboarding.page_one.full_handle": "Your full handle", + "onboarding.page_one.handle_hint": "This is what you would tell your friends to search for.", "onboarding.page_one.welcome": "歡迎來到 Mastodon !", "onboarding.page_six.admin": "你的副本的管理員是 {admin} 。", "onboarding.page_six.almost_done": "快好了…", @@ -197,21 +203,30 @@ "privacy.public.short": "公開貼", "privacy.unlisted.long": "不要貼到公開時間軸", "privacy.unlisted.short": "不列出來", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", "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": "取消", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "更多訊息", "report.submit": "送出", "report.target": "通報中", "search.placeholder": "搜尋", "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "status", "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", "search_results.total": "{count, number} 項結果", "standalone.public_title": "站點一瞥…", "status.block": "Block @{name}", @@ -227,6 +242,7 @@ "status.mute_conversation": "消音對話", "status.open": "展開這個狀態", "status.pin": "置頂到個人資訊頁", + "status.pinned": "Pinned toot", "status.reblog": "轉推", "status.reblogged_by": "{name} 轉推了", "status.reply": "回應", @@ -239,7 +255,6 @@ "status.show_more": "看更多", "status.unmute_conversation": "不消音對話", "status.unpin": "解除置頂", - "tabs_bar.compose": "編輯", "tabs_bar.federated_timeline": "聯盟", "tabs_bar.home": "家", "tabs_bar.local_timeline": "本地", @@ -248,6 +263,7 @@ "upload_area.title": "拖放來上傳", "upload_button.label": "增加媒體", "upload_form.description": "Describe for the visually impaired", + "upload_form.focus": "Crop", "upload_form.undo": "復原", "upload_progress.label": "上傳中...", "video.close": "關閉影片", diff --git a/app/javascript/mastodon/reducers/accounts.js b/app/javascript/mastodon/reducers/accounts.js index f77061dfa..47e6d2330 100644 --- a/app/javascript/mastodon/reducers/accounts.js +++ b/app/javascript/mastodon/reducers/accounts.js @@ -102,7 +102,7 @@ const initialState = ImmutableMap(); export default function accounts(state = initialState, action) { switch(action.type) { case STORE_HYDRATE: - return state.merge(action.state.get('accounts')); + return normalizeAccounts(state, Object.values(action.state.get('accounts').toJS())); case ACCOUNT_FETCH_SUCCESS: case NOTIFICATIONS_UPDATE: return normalizeAccount(state, action.account); diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index c709fb88c..532f4b2a7 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -16,6 +16,8 @@ import { COMPOSE_SUGGESTIONS_CLEAR, COMPOSE_SUGGESTIONS_READY, COMPOSE_SUGGESTION_SELECT, + COMPOSE_SUGGESTION_TAGS_UPDATE, + COMPOSE_TAG_HISTORY_UPDATE, COMPOSE_SENSITIVITY_CHANGE, COMPOSE_SPOILERNESS_CHANGE, COMPOSE_SPOILER_TEXT_CHANGE, @@ -34,7 +36,7 @@ import uuid from '../uuid'; import { me } from '../initial_state'; const initialState = ImmutableMap({ - mounted: false, + mounted: 0, sensitive: false, spoiler: false, spoiler_text: '', @@ -54,6 +56,7 @@ const initialState = ImmutableMap({ default_sensitive: false, resetFileKey: Math.floor((Math.random() * 0x10000)), idempotencyKey: null, + tagHistory: ImmutableList(), }); function statusToTextMentions(state, status) { @@ -87,7 +90,6 @@ function appendMedia(state, media) { map.update('media_attachments', list => list.push(media)); map.set('is_uploading', false); map.set('resetFileKey', Math.floor((Math.random() * 0x10000))); - map.update('text', oldText => `${oldText.trim()} ${media.get('text_url')}`); map.set('focusDate', new Date()); map.set('idempotencyKey', uuid()); @@ -98,12 +100,10 @@ function appendMedia(state, media) { }; function removeMedia(state, mediaId) { - const media = state.get('media_attachments').find(item => item.get('id') === mediaId); const prevSize = state.get('media_attachments').size; return state.withMutations(map => { map.update('media_attachments', list => list.filterNot(item => item.get('id') === mediaId)); - map.update('text', text => text.replace(media.get('text_url'), '').trim()); map.set('idempotencyKey', uuid()); if (prevSize === 1) { @@ -122,6 +122,18 @@ const insertSuggestion = (state, position, token, completion) => { }); }; +const updateSuggestionTags = (state, token) => { + const prefix = token.slice(1); + + return state.merge({ + suggestions: state.get('tagHistory') + .filter(tag => tag.startsWith(prefix)) + .slice(0, 4) + .map(tag => '#' + tag), + suggestion_token: token, + }); +}; + const insertEmoji = (state, position, emojiData) => { const emoji = emojiData.native; @@ -159,10 +171,10 @@ export default function compose(state = initialState, action) { case STORE_HYDRATE: return hydrate(state, action.state.get('compose')); case COMPOSE_MOUNT: - return state.set('mounted', true); + return state.set('mounted', state.get('mounted') + 1); case COMPOSE_UNMOUNT: return state - .set('mounted', false) + .set('mounted', Math.max(state.get('mounted') - 1, 0)) .set('is_composing', false); case COMPOSE_SENSITIVITY_CHANGE: return state.withMutations(map => { @@ -252,6 +264,10 @@ export default function compose(state = initialState, action) { return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map(item => item.id) : action.emojis)).set('suggestion_token', action.token); case COMPOSE_SUGGESTION_SELECT: return insertSuggestion(state, action.position, action.token, action.completion); + case COMPOSE_SUGGESTION_TAGS_UPDATE: + return updateSuggestionTags(state, action.token); + case COMPOSE_TAG_HISTORY_UPDATE: + return state.set('tagHistory', fromJS(action.tags)); case TIMELINE_DELETE: if (action.id === state.get('in_reply_to')) { return state.set('in_reply_to', null); @@ -265,7 +281,7 @@ export default function compose(state = initialState, action) { .set('is_submitting', false) .update('media_attachments', list => list.map(item => { if (item.get('id') === action.media.id) { - return item.set('description', action.media.description); + return fromJS(action.media); } return item; diff --git a/app/javascript/mastodon/reducers/dropdown_menu.js b/app/javascript/mastodon/reducers/dropdown_menu.js new file mode 100644 index 000000000..5449884cc --- /dev/null +++ b/app/javascript/mastodon/reducers/dropdown_menu.js @@ -0,0 +1,18 @@ +import Immutable from 'immutable'; +import { + DROPDOWN_MENU_OPEN, + DROPDOWN_MENU_CLOSE, +} from '../actions/dropdown_menu'; + +const initialState = Immutable.Map({ openId: null, placement: null }); + +export default function dropdownMenu(state = initialState, action) { + switch (action.type) { + case DROPDOWN_MENU_OPEN: + return state.merge({ openId: action.id, placement: action.placement }); + case DROPDOWN_MENU_CLOSE: + return state.get('openId') === action.id ? state.set('openId', null) : state; + default: + return state; + } +} diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index a028e989c..b84b2d18a 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -1,4 +1,5 @@ import { combineReducers } from 'redux-immutable'; +import dropdown_menu from './dropdown_menu'; import timelines from './timelines'; import meta from './meta'; import alerts from './alerts'; @@ -26,6 +27,7 @@ import lists from './lists'; import listEditor from './list_editor'; const reducers = { + dropdown_menu, timelines, meta, alerts, diff --git a/app/javascript/mastodon/reducers/reports.js b/app/javascript/mastodon/reducers/reports.js index a08bbec38..21ae6f93f 100644 --- a/app/javascript/mastodon/reducers/reports.js +++ b/app/javascript/mastodon/reducers/reports.js @@ -6,6 +6,7 @@ import { REPORT_CANCEL, REPORT_STATUS_TOGGLE, REPORT_COMMENT_CHANGE, + REPORT_FORWARD_CHANGE, } from '../actions/reports'; import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable'; @@ -15,6 +16,7 @@ const initialState = ImmutableMap({ account_id: null, status_ids: ImmutableSet(), comment: '', + forward: false, }), }); @@ -42,6 +44,8 @@ export default function reports(state = initialState, action) { }); case REPORT_COMMENT_CHANGE: return state.setIn(['new', 'comment'], action.comment); + case REPORT_FORWARD_CHANGE: + return state.setIn(['new', 'forward'], action.forward); case REPORT_SUBMIT_REQUEST: return state.setIn(['new', 'isSubmitting'], true); case REPORT_SUBMIT_FAIL: diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index 5120b2b67..5a47e7272 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -15,6 +15,8 @@ import { CONTEXT_FETCH_SUCCESS, STATUS_MUTE_SUCCESS, STATUS_UNMUTE_SUCCESS, + STATUS_REVEAL, + STATUS_HIDE, } from '../actions/statuses'; import { TIMELINE_REFRESH_SUCCESS, @@ -54,7 +56,7 @@ const normalizeStatus = (state, status) => { normalStatus.reblog = status.reblog.id; } - const searchContent = [status.spoiler_text, status.content].join('\n\n').replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n'); + const searchContent = [status.spoiler_text, status.content].join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n'); const emojiMap = normalStatus.emojis.reduce((obj, emoji) => { obj[`:${emoji.shortcode}:`] = emoji; @@ -62,8 +64,9 @@ const normalizeStatus = (state, status) => { }, {}); normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; - normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); - normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''), emojiMap); + normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); + normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''), emojiMap); + normalStatus.hidden = normalStatus.sensitive; return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus))); }; @@ -111,6 +114,14 @@ export default function statuses(state = initialState, action) { return state.setIn([action.id, 'muted'], true); case STATUS_UNMUTE_SUCCESS: return state.setIn([action.id, 'muted'], false); + case STATUS_REVEAL: + return state.withMutations(map => { + action.ids.forEach(id => map.setIn([id, 'hidden'], false)); + }); + case STATUS_HIDE: + return state.withMutations(map => { + action.ids.forEach(id => map.setIn([id, 'hidden'], true)); + }); case TIMELINE_REFRESH_SUCCESS: case TIMELINE_EXPAND_SUCCESS: case CONTEXT_FETCH_SUCCESS: diff --git a/app/javascript/mastodon/settings.js b/app/javascript/mastodon/settings.js index dbd969cb1..7643a508e 100644 --- a/app/javascript/mastodon/settings.js +++ b/app/javascript/mastodon/settings.js @@ -44,3 +44,4 @@ export default class Settings { } export const pushNotificationsSetting = new Settings('mastodon_push_notification_data'); +export const tagHistory = new Settings('mastodon_tag_history'); diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss index e45fc03d3..c484f074b 100644 --- a/app/javascript/styles/mastodon/about.scss +++ b/app/javascript/styles/mastodon/about.scss @@ -1,4 +1,183 @@ +$maximum-width: 1235px; +$fluid-breakpoint: $maximum-width + 20px; +$column-breakpoint: 700px; +$small-breakpoint: 960px; + +.container { + box-sizing: border-box; + max-width: $maximum-width; + margin: 0 auto; + position: relative; + + @media screen and (max-width: $fluid-breakpoint) { + width: 100%; + padding: 0 10px; + } +} + .landing-page { + .grid { + display: grid; + grid-gap: 10px; + grid-template-columns: 1fr 2fr; + grid-auto-columns: 25%; + grid-auto-rows: max-content; + + .column-0 { + display: none; + } + + .column-1 { + grid-column: 1; + grid-row: 1; + } + + .column-2 { + grid-column: 2; + grid-row: 1; + } + + .column-3 { + grid-column: 3; + grid-row: 1 / 3; + } + + .column-4 { + grid-column: 1 / 3; + grid-row: 2; + } + } + + @media screen and (max-width: $small-breakpoint) { + .grid { + grid-template-columns: 40% 60%; + + .column-0 { + display: none; + } + + .column-1 { + grid-column: 1; + grid-row: 1; + + &.non-preview .landing-page__forms { + height: 100%; + } + } + + .column-2 { + grid-column: 2; + grid-row: 1 / 3; + + &.non-preview { + grid-column: 2; + grid-row: 1; + } + } + + .column-3 { + grid-column: 1; + grid-row: 2 / 4; + } + + .column-4 { + grid-column: 2; + grid-row: 3; + + &.non-preview { + grid-column: 1 / 3; + grid-row: 2; + } + } + } + } + + @media screen and (max-width: $column-breakpoint) { + .grid { + grid-template-columns: auto; + + .column-0 { + display: block; + grid-column: 1; + grid-row: 1; + } + + .column-1 { + grid-column: 1; + grid-row: 3; + + .brand { + display: none; + } + } + + .column-2 { + grid-column: 1; + grid-row: 2; + + .landing-page__logo, + .landing-page__call-to-action { + display: none; + } + + &.non-preview { + grid-column: 1; + grid-row: 2; + } + } + + .column-3 { + grid-column: 1; + grid-row: 5; + } + + .column-4 { + grid-column: 1; + grid-row: 4; + + &.non-preview { + grid-column: 1; + grid-row: 4; + } + } + } + } + + .column-flex { + display: flex; + flex-direction: column; + } + + .separator-or { + position: relative; + margin: 40px 0; + text-align: center; + + &::before { + content: ""; + display: block; + width: 100%; + height: 0; + border-bottom: 1px solid rgba($ui-base-lighter-color, .6); + position: absolute; + top: 50%; + left: 0; + } + + span { + display: inline-block; + background: $ui-base-color; + font-size: 12px; + font-weight: 500; + color: $ui-primary-color; + text-transform: uppercase; + position: relative; + z-index: 1; + padding: 0 8px; + cursor: default; + } + } + p, li { font-family: 'mastodon-font-sans-serif', sans-serif; @@ -15,6 +194,28 @@ } } + .closed-registrations-message { + margin-top: 20px; + + &, + p { + text-align: center; + font-size: 12px; + line-height: 18px; + color: $ui-primary-color; + margin-bottom: 0; + + a { + color: $ui-highlight-color; + text-decoration: underline; + } + } + + p:last-child { + margin-bottom: 0; + } + } + em { display: inline; margin: 0; @@ -116,10 +317,14 @@ } hr { - border-color: rgba($ui-base-lighter-color, .6); + width: 100%; + height: 0; + border: 0; + border-bottom: 1px solid rgba($ui-base-lighter-color, .6); + margin: 20px 0; } - .container { + .container-alt { width: 100%; box-sizing: border-box; max-width: 800px; @@ -152,24 +357,20 @@ } } } + } - .mascot-container { - max-width: 800px; - margin: 0 auto; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 100%; + .brand { + a { + padding-left: 0; + padding-right: 0; + color: $white; } - .mascot { - position: absolute; - bottom: -14px; - width: auto; - height: auto; - left: 60px; - z-index: 3; + img { + height: 32px; + position: relative; + top: 4px; + left: -10px; } } @@ -177,7 +378,7 @@ line-height: 30px; overflow: hidden; - .container { + .container-alt { display: flex; justify-content: space-between; } @@ -203,21 +404,6 @@ } } - .brand { - a { - padding-left: 0; - padding-right: 0; - color: $white; - } - - img { - height: 32px; - position: relative; - top: 4px; - left: -10px; - } - } - ul { list-style: none; margin: 0; @@ -243,53 +429,6 @@ align-items: center; position: relative; - .floats { - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - - div { - position: absolute; - transition: all 0.1s linear; - animation-name: floating; - animation-iteration-count: infinite; - animation-direction: alternate; - animation-timing-function: ease-in-out; - z-index: 2; - } - - .float-1 { - width: 324px; - height: 170px; - right: -120px; - bottom: 0; - animation-duration: 3s; - background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 447.1875 234.375" height="170" width="324"><path fill="#{hex-color($ui-base-lighter-color)}" d="M21.69 233.366c-6.45-1.268-13.347-5.63-16.704-10.564-10.705-15.734-1.513-37.724 18.632-44.57l4.8-1.632.173-17.753c.146-14.77.515-19.063 2.2-25.55 6.736-25.944 24.46-46.032 47.766-54.137 11.913-4.143 19.558-5.366 34.178-5.47l13.828-.096V71.12c0-4.755 2.853-17.457 5.238-23.327 8.588-21.137 26.735-35.957 52.153-42.593 23.248-6.07 50.153-6.415 71.863-.923 11.14 2.82 25.686 9.957 33.857 16.615 19.335 15.756 31.82 41.05 35.183 71.275.59 5.305.672 5.435 3.11 4.926 11.833-2.474 30.4-3.132 40.065-1.42 24.388 4.32 40.568 19.076 47.214 43.058 2.16 7.8 3.953 23.894 3.59 32.237l-.24 5.498 5.156 1.317c6.392 1.633 14.55 7.098 18.003 12.062 1.435 2.062 3.305 6.597 4.156 10.078 1.428 5.84 1.43 6.8.04 12.44-1.807 7.318-5.672 13.252-10.872 16.694-8.508 5.63 3.756 5.33-211.916 5.216-108.56-.056-199.22-.464-201.47-.906z"/></svg>'); - } - - .float-2 { - width: 241px; - height: 100px; - right: 210px; - bottom: 0; - animation-duration: 3.5s; - animation-delay: 0.2s; - background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 536.25 222.1875" height="100" width="241"><path fill="#{hex-color($ui-base-lighter-color)}" d="M42.626 221.23c-14.104-1.174-26.442-5.133-32.825-10.534-4.194-3.548-7.684-10.66-8.868-18.075-1.934-12.102.633-22.265 7.528-29.81 7.61-8.328 19.998-12.76 39.855-14.257l8.47-.638-2.08-6.223c-4.826-14.422-6.357-24.813-6.37-43.255-.012-14.923.28-18.513 2.1-25.724 2.283-9.048 8.483-23.034 13.345-30.1 14.76-21.45 43.505-38.425 70.535-41.65 30.628-3.655 64.47 12.073 89.668 41.673l5.955 6.995 2.765-4.174c1.52-2.296 5.74-6.93 9.376-10.295 18.382-17.02 43.436-20.676 73.352-10.705 12.158 4.052 21.315 9.53 29.64 17.733 12.752 12.562 18.16 25.718 18.19 44.26l.02 10.98 2.312-3.01c15.64-20.365 42.29-20.485 62.438-.28 3.644 3.653 7.558 8.593 8.697 10.976 4.895 10.24 5.932 25.688 2.486 37.046-.76 2.507-1.388 4.816-1.393 5.13-.006.316 6.845.87 15.224 1.234 53.06 2.297 76.356 12.98 81.817 37.526 3.554 15.973-3.71 28.604-19.566 34.02-4.554 1.555-17.922 1.655-234.517 1.757-126.327.06-233.497-.21-238.154-.597z"/></svg>'); - } - - .float-3 { - width: 267px; - height: 140px; - right: 110px; - top: -30px; - animation-duration: 4s; - animation-delay: 0.5s; - background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 388.125 202.5" height="140" width="267"><path fill="#{hex-color($ui-base-lighter-color)}" d="M181.37 201.458c-17.184-1.81-36.762-8.944-49.523-18.05l-5.774-4.12-8.074 2.63c-11.468 3.738-21.382 4.962-35.815 4.422-14.79-.554-24.577-2.845-36.716-8.594-15.483-7.332-28.498-19.98-35.985-34.968C2.44 128.675-.94 108.435.9 91.356c3.362-31.234 18.197-53.698 43.63-66.074 12.803-6.23 22.384-8.55 37.655-9.122 14.433-.54 24.347.684 35.814 4.42l8.073 2.633 5.635-4.01c24.81-17.656 60.007-23.332 92.914-14.985 10.11 2.565 25.498 9.62 33.102 15.178l5.068 3.704 7.632-2.564c10.89-3.66 21.086-4.916 35.516-4.376 45.816 1.716 76.422 30.03 81.285 75.196 1.84 17.08-1.54 37.32-8.585 51.422-7.487 14.99-20.502 27.636-35.984 34.968-12.14 5.75-21.926 8.04-36.716 8.593-14.43.54-24.626-.716-35.516-4.376l-7.632-2.564-5.068 3.704c-12.844 9.387-32.714 16.488-51.545 18.42-10.607 1.09-13.916 1.08-24.81-.066z"/></svg>'); - } - } - .heading { position: relative; z-index: 4; @@ -346,18 +485,18 @@ background: darken($ui-base-color, 4%); padding: 20px 0; - .container { + .container-alt { position: relative; padding-right: 280px + 15px; } - .information-board-sections { + &__sections { display: flex; justify-content: space-between; flex-wrap: wrap; } - .section { + &__section { flex: 1 0 0; font-family: 'mastodon-font-sans-serif', sans-serif; font-size: 16px; @@ -382,6 +521,10 @@ font-size: 32px; line-height: 48px; } + + @media screen and (max-width: $column-breakpoint) { + text-align: center; + } } .panel { @@ -460,114 +603,310 @@ } } - .features { - padding: 50px 0; + &.alternative { + padding: 10px 0; - .container { - display: flex; - } + .brand { + text-align: center; + padding: 30px 0; + margin-bottom: 10px; - #mastodon-timeline { - display: flex; - -webkit-overflow-scrolling: touch; - -ms-overflow-style: -ms-autohiding-scrollbar; - font-family: 'mastodon-font-sans-serif', sans-serif; - font-size: 13px; - line-height: 18px; - font-weight: 400; - color: $primary-text-color; - width: 330px; - margin-right: 30px; - flex: 0 0 auto; - background: $ui-base-color; - overflow: hidden; - border-radius: 4px; - box-shadow: 0 0 6px rgba($black, 0.1); + img { + position: static; + padding: 10px 0; + } - .column-header { - color: inherit; - font-family: inherit; - font-size: 16px; - line-height: inherit; - font-weight: inherit; - margin: 0; - padding: 15px; + @media screen and (max-width: $small-breakpoint) { + padding: 15px 0; } - .column { + @media screen and (max-width: $column-breakpoint) { padding: 0; - border-radius: 4px; - overflow: hidden; + margin-bottom: -10px; } + } + } + + &__information, + &__forms { + padding: 20px; + } + + &__call-to-action { + background: darken($ui-base-color, 4%); + border-radius: 4px; + padding: 25px 40px; + overflow: hidden; - .scrollable { - height: 400px; + .row { + display: flex; + flex-direction: row-reverse; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + } + + .row__information-board { + display: flex; + justify-content: flex-end; + align-items: flex-end; + + .information-board__section { + flex: 1 0 auto; + padding: 0 10px; } + } + + .row__mascot { + flex: 1; + margin: 10px -50px 0 0; + } + } + + &__logo { + margin-right: 20px; + + img { + height: 50px; + width: auto; + mix-blend-mode: lighten; + } + } + + &__information { + padding: 45px 40px; + margin-bottom: 10px; + + &:last-child { + margin-bottom: 0; + } + + @media screen and (max-width: $column-breakpoint) { + padding: 25px 20px; + } + } + + &__information, + &__forms, + #mastodon-timeline { + box-sizing: border-box; + background: $ui-base-color; + border-radius: 4px; + box-shadow: 0 0 6px rgba($black, 0.1); + } + + &__mascot { + height: 104px; + position: relative; + left: -40px; + bottom: 25px; + + img { + height: 190px; + width: auto; + } + } - p { - font-size: inherit; - line-height: inherit; - font-weight: inherit; - color: $primary-text-color; + &__short-description { + .row { + display: flex; + flex-wrap: wrap; + align-items: center; + margin-bottom: 40px; + } + + @media screen and (max-width: $column-breakpoint) { + .row { margin-bottom: 20px; + } + } - &:last-child { - margin-bottom: 0; - } + p a { + color: $ui-secondary-color; + } - a { + h1 { + font-weight: 500; + color: $primary-text-color; + margin-bottom: 0; + + small { + color: $ui-primary-color; + + span { color: $ui-secondary-color; - text-decoration: none; } } } - .about-mastodon { - max-width: 675px; + p:last-child { + margin-bottom: 0; + } + } + + &__hero { + margin-bottom: 10px; - p { - margin-bottom: 20px; + img { + display: block; + margin: 0; + max-width: 100%; + height: auto; + border-radius: 4px; + } + } + + &__forms { + height: 100%; + + @media screen and (max-width: $small-breakpoint) { + height: auto; + } + + @media screen and (max-width: $column-breakpoint) { + background: transparent; + box-shadow: none; + padding: 0 20px; + margin-top: 30px; + margin-bottom: 40px; + + .separator-or { + span { + background: darken($ui-base-color, 8%); + } } + } - .features-list { - margin-top: 20px; + hr { + margin: 40px 0; + } - .features-list__row { - display: flex; - padding: 10px 0; - justify-content: space-between; + .button { + display: block; + } - &:first-child { - padding-top: 0; - } + .subtle-hint a { + text-decoration: none; - .visual { - flex: 0 0 auto; - display: flex; - align-items: center; - margin-left: 15px; + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } + } - .fa { - display: block; - color: $ui-primary-color; - font-size: 48px; - } - } + #mastodon-timeline { + display: flex; + -webkit-overflow-scrolling: touch; + -ms-overflow-style: -ms-autohiding-scrollbar; + font-family: 'mastodon-font-sans-serif', sans-serif; + font-size: 13px; + line-height: 18px; + font-weight: 400; + color: $primary-text-color; + width: 100%; + flex: 1 1 auto; + overflow: hidden; + height: 100%; - .text { - font-size: 16px; - line-height: 30px; - color: $ui-primary-color; + .column-header { + color: inherit; + font-family: inherit; + font-size: 16px; + line-height: inherit; + font-weight: inherit; + margin: 0; + padding: 0; + } - h6 { - font-size: inherit; - line-height: inherit; - margin-bottom: 0; - } - } + .column { + padding: 0; + border-radius: 4px; + overflow: hidden; + width: 100%; + } + + .scrollable { + height: 400px; + } + + p { + font-size: inherit; + line-height: inherit; + font-weight: inherit; + color: $primary-text-color; + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; + } + + a { + color: $ui-secondary-color; + text-decoration: none; + } + } + + @media screen and (max-width: $column-breakpoint) { + height: 90vh; + } + } + + &__features { + & > p { + padding-right: 60px; + } + + .features-list { + margin: 40px 0; + margin-top: 30px; + } + + &__action { + text-align: center; + } + } + + .features-list { + .features-list__row { + display: flex; + padding: 10px 0; + justify-content: space-between; + + .visual { + flex: 0 0 auto; + display: flex; + align-items: center; + margin-left: 15px; + + .fa { + display: block; + color: $ui-primary-color; + font-size: 48px; + } + } + + .text { + font-size: 16px; + line-height: 30px; + color: $ui-primary-color; + + h6 { + font-size: inherit; + line-height: inherit; + margin-bottom: 0; } } } + + @media screen and (min-width: $small-breakpoint) { + display: grid; + grid-gap: 30px; + grid-template-columns: 1fr 1fr; + grid-auto-columns: 50%; + grid-auto-rows: max-content; + } } .extended-description { @@ -600,21 +939,31 @@ } } + &__footer { + margin-top: 10px; + text-align: center; + color: $ui-base-lighter-color; + + p { + font-size: 14px; + + a { + color: inherit; + text-decoration: underline; + } + } + } + @media screen and (max-width: 840px) { - .container { + .container-alt { padding: 0 20px; } .information-board { - - .container { + .container-alt { padding-right: 20px; } - .section { - text-align: center; - } - .panel { position: static; margin-top: 20px; @@ -626,16 +975,6 @@ } } } - - .header-wrapper .mascot { - left: 20px; - } - } - - @media screen and (max-width: 689px) { - .header-wrapper .mascot { - display: none; - } } @media screen and (max-width: 675px) { @@ -651,13 +990,12 @@ } } - .header .container, - .features .container { + .header .container-alt, + .features .container-alt { display: block; } .header { - .links { padding-top: 15px; background: darken($ui-base-color, 4%); @@ -682,10 +1020,6 @@ margin-top: 30px; padding: 0; - .floats { - display: none; - } - .heading { padding: 30px 20px; text-align: center; @@ -700,16 +1034,6 @@ } } } - - .features #mastodon-timeline { - height: 70vh; - width: 100%; - margin-bottom: 50px; - - .column { - width: 100%; - } - } } .cta { @@ -717,108 +1041,55 @@ } &.tag-page { - .features { - padding: 30px 0; - - .container { - max-width: 820px; - - #mastodon-timeline { - margin-right: 0; - border-top-right-radius: 0; - } - - .about-mastodon { - .about-hashtag { - background: darken($ui-base-color, 4%); - padding: 0 20px 20px 30px; - border-radius: 0 5px 5px 0; - - .brand { - padding-top: 20px; - margin-bottom: 20px; - - img { - height: 48px; - width: auto; - } - } - - p { - strong { - color: $ui-secondary-color; - font-weight: 700; - } - } - - .cta { - margin: 0; - - .button { - margin-right: 4px; - } - } - } + .grid { + @media screen and (min-width: $small-breakpoint) { + grid-template-columns: 33% 67%; + } - .features-list { - margin-left: 30px; - margin-right: 10px; - } - } + .column-2 { + grid-column: 2; + grid-row: 1; } } - @media screen and (max-width: 675px) { - .features { - padding: 10px 0; + .brand { + text-align: unset; + padding: 0; - .container { - display: flex; - flex-direction: column; - - #mastodon-timeline { - order: 2; - flex: 0 0 auto; - height: 60vh; - margin-bottom: 20px; - border-top-right-radius: 4px; - } + img { + height: 48px; + width: auto; + } + } - .about-mastodon { - order: 1; - flex: 0 0 auto; - max-width: 100%; + .cta { + margin: 0; - .about-hashtag { - background: unset; - padding: 0; - border-radius: 0; + .button { + margin-right: 4px; + } + } - .cta { - margin: 20px 0; - } - } + @media screen and (max-width: $column-breakpoint) { + .grid { + .column-1 { + grid-column: 1; + grid-row: 2; + } - .features-list { - display: none; - } - } + .column-2 { + grid-column: 1; + grid-row: 1; } } - } - } -} -@keyframes floating { - from { - transform: translate(0, 0); - } - - 65% { - transform: translate(0, 4px); - } + .brand { + margin: 0; + } - to { - transform: translate(0, -0); + .landing-page__features { + display: none; + } + } } } diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index 9015d04cb..873963c90 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -97,32 +97,6 @@ } } - .controls { - position: absolute; - top: 15px; - left: 15px; - z-index: 2; - - .icon-button { - color: rgba($white, 0.8); - text-decoration: none; - font-size: 13px; - line-height: 13px; - font-weight: 500; - - .fa { - font-weight: 400; - margin-right: 5px; - } - - &:hover, - &:active, - &:focus { - color: $white; - } - } - } - .roles { margin-bottom: 30px; padding: 0 15px; @@ -226,6 +200,40 @@ } } +.card, +.account-grid-card { + .controls { + position: absolute; + top: 15px; + left: 15px; + z-index: 2; + + .icon-button { + color: rgba($white, 0.8); + text-decoration: none; + font-size: 13px; + line-height: 13px; + font-weight: 500; + + .fa { + font-weight: 400; + margin-right: 5px; + } + + &:hover, + &:active, + &:focus { + color: $white; + } + } + } +} + +.account-grid-card .controls { + left: auto; + right: 15px; +} + .pagination { padding: 30px 0; text-align: center; @@ -233,8 +241,8 @@ a, .current, - .next, - .prev, + .newer, + .older, .page, .gap { font-size: 14px; @@ -257,13 +265,13 @@ cursor: default; } - .prev, - .next { + .older, + .newer { text-transform: uppercase; color: $ui-secondary-color; } - .prev { + .older { float: left; padding-left: 0; @@ -273,7 +281,7 @@ } } - .next { + .newer { float: right; padding-right: 0; @@ -295,8 +303,8 @@ display: none; } - .next, - .prev { + .newer, + .older { display: inline-block; } } @@ -411,13 +419,14 @@ font-weight: 400; } - .note { + .account__header__content { padding: 10px 15px; padding-top: 15px; - box-sizing: border-box; color: lighten($ui-base-color, 26%); word-wrap: break-word; - min-height: 80px; + overflow: hidden; + text-overflow: ellipsis; + height: 5.5em; } } } diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 0c343e1df..e6bd0c717 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -105,6 +105,16 @@ margin-bottom: 30px; } + h4 { + text-transform: uppercase; + font-size: 13px; + font-weight: 500; + color: $ui-primary-color; + padding-bottom: 8px; + margin-bottom: 8px; + border-bottom: 1px solid lighten($ui-base-color, 8%); + } + h6 { font-size: 16px; color: $ui-secondary-color; diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss index b5d77ff63..bec0d4d91 100644 --- a/app/javascript/styles/mastodon/basics.scss +++ b/app/javascript/styles/mastodon/basics.scss @@ -47,6 +47,10 @@ body { padding-bottom: 0; } + &.player { + text-align: center; + } + &.embed { background: transparent; margin: 0; @@ -118,5 +122,6 @@ button { height: 100%; align-items: center; justify-content: center; + outline: 0 !important; } } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 75e37237b..cd0dbbab8 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -40,14 +40,20 @@ cursor: default; } - &.button-alternative { + &.button-primary, + &.button-alternative, + &.button-secondary, + &.button-alternative-2 { font-size: 16px; line-height: 36px; height: auto; - color: $ui-base-color; - background: $ui-primary-color; text-transform: none; padding: 4px 16px; + } + + &.button-alternative { + color: $ui-base-color; + background: $ui-primary-color; &:active, &:focus, @@ -56,15 +62,20 @@ } } + &.button-alternative-2 { + background: $ui-base-lighter-color; + + &:active, + &:focus, + &:hover { + background-color: lighten($ui-base-lighter-color, 4%); + } + } + &.button-secondary { - font-size: 16px; - line-height: 36px; - height: auto; color: $ui-primary-color; - text-transform: none; background: transparent; padding: 3px 15px; - border-radius: 4px; border: 1px solid $ui-primary-color; &:active, @@ -433,6 +444,34 @@ min-width: 40%; margin: 5px; + &__actions { + background: linear-gradient(180deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent); + display: flex; + align-items: flex-start; + justify-content: space-between; + opacity: 0; + transition: opacity .1s ease; + + .icon-button { + flex: 0 1 auto; + color: $ui-secondary-color; + font-size: 14px; + font-weight: 500; + padding: 10px; + font-family: inherit; + + &:hover, + &:focus, + &:active { + color: lighten($ui-secondary-color, 4%); + } + } + + &.active { + opacity: 1; + } + } + &-description { position: absolute; z-index: 2; @@ -470,10 +509,6 @@ opacity: 1; } } - - .icon-button { - mix-blend-mode: difference; - } } .compose-form__upload-thumbnail { @@ -481,8 +516,9 @@ background-position: center; background-size: cover; background-repeat: no-repeat; - height: 100px; + height: 140px; width: 100%; + overflow: hidden; } } @@ -686,12 +722,13 @@ background: transparent; border: 0; color: lighten($ui-base-color, 8%); - font-weight: 500; + font-weight: 700; font-size: 11px; padding: 0 6px; text-transform: uppercase; - line-height: inherit; + line-height: 20px; cursor: pointer; + vertical-align: middle; } .status__prepend-icon-wrapper { @@ -825,12 +862,27 @@ border-bottom: 1px solid $ui-secondary-color; display: flex; - .status__content { - flex: 1 1 auto; - padding: 10px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + .status-check-box__status { + margin: 10px 0 10px 10px; + flex: 1; + + .media-gallery { + max-width: 250px; + } + + .status__content { + padding: 0; + white-space: normal; + } + + .video-player { + margin-top: 8px; + max-width: 250px; + } + + .media-gallery__item-thumbnail { + cursor: default; + } } } @@ -899,6 +951,11 @@ height: 24px; margin: -1px 0 0; } + + .status__content__spoiler-link { + line-height: 24px; + margin: -1px 0 0; + } } .video-player { @@ -1376,36 +1433,29 @@ .image-loader { position: relative; + width: 100%; + height: 100%; &.image-loader--loading { + display: flex; + align-content: center; + .image-loader__preview-canvas { filter: blur(2px); } } - .image-loader__img { - position: absolute; - top: 0; - left: 0; - right: 0; - max-width: 100%; - max-height: 100%; - background-image: none; + &.image-loader--amorphous .image-loader__preview-canvas { + display: none; } +} - &.image-loader--amorphous { - position: static; - - .image-loader__preview-canvas { - display: none; - } - - .image-loader__img { - position: static; - width: auto; - height: auto; - } - } +.zoomable-image { + position: relative; + width: 100%; + height: 100%; + display: flex; + align-content: center; } .navigation-bar { @@ -1766,7 +1816,7 @@ position: absolute; top: 0; left: 0; - background: lighten($ui-base-color, 13%) url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>') no-repeat bottom / 100% auto; + background: lighten($ui-base-color, 13%); box-sizing: border-box; padding: 0; display: flex; @@ -1779,10 +1829,19 @@ &.darker { background: $ui-base-color; } +} - > .mastodon { - background: url('~images/elephant_ui_plane.svg') no-repeat left bottom / contain; - flex: 1; +.drawer__inner__mastodon { + background: lighten($ui-base-color, 13%) url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>') no-repeat bottom / 100% auto; + flex: 1; + min-height: 47px; + + > img { + display: block; + object-fit: contain; + object-position: bottom left; + width: 100%; + height: 100%; } } @@ -1827,7 +1886,7 @@ font-size: 14px; font-weight: 500; border-bottom: 2px solid lighten($ui-base-color, 8%); - transition: all 200ms linear; + transition: all 50ms linear; .fa { font-weight: 400; @@ -1844,7 +1903,6 @@ &:active { @media screen and (min-width: 631px) { background: lighten($ui-base-color, 14%); - transition: all 100ms linear; } } @@ -1913,7 +1971,7 @@ font-family: inherit; color: $ui-highlight-color; cursor: pointer; - flex: 0 0 auto; + white-space: nowrap; font-size: 16px; padding: 0 5px 0 0; z-index: 3; @@ -2070,6 +2128,17 @@ margin-right: 5px; } +.column-link__badge { + display: inline-block; + border-radius: 4px; + font-size: 12px; + line-height: 19px; + font-weight: 500; + background: $ui-base-color; + padding: 4px 8px; + margin: -6px 10px; +} + .column-subheading { background: $ui-base-color; color: $ui-base-lighter-color; @@ -2188,7 +2257,6 @@ .status-card { display: flex; - cursor: pointer; font-size: 14px; border: 1px solid lighten($ui-base-color, 8%); border-radius: 4px; @@ -2197,20 +2265,58 @@ text-decoration: none; overflow: hidden; - &:hover { - background: lighten($ui-base-color, 8%); + &__actions { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + display: flex; + justify-content: center; + align-items: center; + + & > div { + background: rgba($base-shadow-color, 0.6); + border-radius: 4px; + padding: 12px 9px; + flex: 0 0 auto; + display: flex; + justify-content: center; + align-items: center; + } + + button, + a { + display: inline; + color: $primary-text-color; + background: transparent; + border: 0; + padding: 0 5px; + text-decoration: none; + opacity: 0.6; + font-size: 18px; + line-height: 18px; + + &:hover, + &:active, + &:focus { + opacity: 1; + } + } + + a { + font-size: 19px; + position: relative; + bottom: -1px; + } } } -.status-card-video, -.status-card-rich, -.status-card-photo { - margin-top: 14px; - overflow: hidden; +a.status-card { + cursor: pointer; - iframe { - width: 100%; - height: auto; + &:hover { + background: lighten($ui-base-color, 8%); } } @@ -2238,6 +2344,7 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + text-decoration: none; } .status-card__content { @@ -2259,6 +2366,7 @@ .status-card__image { flex: 0 0 100px; background: lighten($ui-base-color, 8%); + position: relative; } .status-card.horizontal { @@ -2284,6 +2392,8 @@ width: 100%; height: 100%; object-fit: cover; + background-size: cover; + background-position: center center; } .load-more { @@ -2382,7 +2492,6 @@ .column-header { display: flex; - padding: 15px; font-size: 16px; background: lighten($ui-base-color, 4%); flex: 0 0 auto; @@ -2393,15 +2502,21 @@ overflow: hidden; & > button { - display: flex; - flex: auto; margin: 0; border: none; - padding: 0; + padding: 15px 0 15px 15px; color: inherit; background: transparent; font: inherit; text-align: left; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + flex: 1; + } + + & > .column-header__back-button { + color: $ui-highlight-color; } &.active { @@ -2422,8 +2537,6 @@ .column-header__buttons { height: 48px; display: flex; - margin: -15px; - margin-left: 0; } .column-header__links .text-btn { @@ -2503,14 +2616,6 @@ } } -.column-header__title { - display: inline-block; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - flex: 1; -} - .text-btn { display: inline-block; padding: 0; @@ -2615,12 +2720,16 @@ background: $base-overlay-background; color: $ui-primary-color; border: 0; + padding: 0; width: 100%; height: 100%; + border-radius: 4px; + appearance: none; &:hover, &:active, &:focus { + padding: 0; color: lighten($ui-primary-color, 8%); } } @@ -2633,7 +2742,7 @@ .media-spoiler__trigger { display: block; font-size: 11px; - font-weight: 500; + font-weight: 700; } .spoiler-button { @@ -2687,33 +2796,26 @@ } } -.modal-container__nav { - align-items: center; - background: rgba($base-overlay-background, 0.5); - box-sizing: border-box; - border: 0; +.account--follows-info { color: $primary-text-color; - cursor: pointer; - display: flex; - font-size: 24px; - height: 100%; - padding: 30px 15px; position: absolute; - top: 0; -} - -.modal-container__nav--left { - left: -61px; -} - -.modal-container__nav--right { - right: -61px; + top: 10px; + left: 10px; + opacity: 0.7; + display: inline-block; + vertical-align: top; + background-color: rgba($base-overlay-background, 0.4); + text-transform: uppercase; + font-size: 11px; + font-weight: 500; + padding: 4px; + border-radius: 4px; } -.account--follows-info { +.account--muting-info { color: $primary-text-color; position: absolute; - top: 10px; + top: 40px; left: 10px; opacity: 0.7; display: inline-block; @@ -3218,6 +3320,43 @@ font-weight: 500; } +.search-results__section { + margin-bottom: 20px; + + h5 { + position: relative; + + &::before { + content: ""; + display: block; + position: absolute; + left: 0; + right: 0; + top: 50%; + width: 100%; + height: 0; + border-top: 1px solid lighten($ui-base-color, 8%); + } + + span { + display: inline-block; + background: $ui-base-color; + color: $ui-primary-color; + font-size: 14px; + font-weight: 500; + padding: 10px; + position: relative; + z-index: 1; + cursor: default; + } + } + + .account:last-child, + & > div:last-child .status { + border-bottom: 0; + } +} + .search-results__hashtag { display: block; padding: 10px; @@ -3269,29 +3408,27 @@ z-index: 9999; } +.video-modal { + max-width: 100vw; + max-height: 100vh; + position: relative; +} + .media-modal { - max-width: 80vw; - max-height: 80vh; + width: 100%; + height: 100%; position: relative; - .extended-video-player, img, canvas, video { - max-width: 80vw; - max-height: 80vh; + max-width: 100vw; + max-height: 100vh; width: auto; height: auto; margin: auto; } - .extended-video-player, - video { - display: flex; - width: 80vw; - height: 80vh; - } - img, canvas { display: block; @@ -3300,12 +3437,65 @@ } .react-swipeable-view-container { - max-width: 80vw; + width: 100vw; + height: 100%; } } -.media-modal__content { - background: $base-overlay-background; +.media-modal__closer { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.media-modal__navigation { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + transition: opacity 0.3s linear; + will-change: opacity; + + * { + pointer-events: auto; + } + + &.media-modal__navigation--hidden { + opacity: 0; + + * { + pointer-events: none; + } + } +} + +.media-modal__nav { + background: rgba($base-overlay-background, 0.5); + box-sizing: border-box; + border: 0; + color: $primary-text-color; + cursor: pointer; + display: flex; + align-items: center; + font-size: 24px; + height: 20vmax; + margin: auto 0; + padding: 30px 15px; + position: absolute; + top: 0; + bottom: 0; +} + +.media-modal__nav--left { + left: 0; +} + +.media-modal__nav--right { + right: 0; } .media-modal__pagination { @@ -3313,7 +3503,8 @@ text-align: center; position: absolute; left: 0; - bottom: -40px; + bottom: 20px; + pointer-events: none; } .media-modal__page-dot { @@ -3337,8 +3528,8 @@ .media-modal__close { position: absolute; - right: 4px; - top: 4px; + right: 8px; + top: 8px; z-index: 100; } @@ -3756,8 +3947,7 @@ .boost-modal__action-bar, .confirmation-modal__action-bar, -.mute-modal__action-bar, -.report-modal__action-bar { +.mute-modal__action-bar { display: flex; justify-content: space-between; background: $ui-secondary-color; @@ -3801,21 +3991,94 @@ vertical-align: middle; } +.report-modal { + width: 90vw; + max-width: 700px; +} + +.report-modal__container { + display: flex; + border-top: 1px solid $ui-secondary-color; + + @media screen and (max-width: 480px) { + flex-wrap: wrap; + overflow-y: auto; + } +} + .report-modal__statuses, .report-modal__comment { - padding: 10px; + box-sizing: border-box; + width: 50%; + + @media screen and (max-width: 480px) { + width: 100%; + } } .report-modal__statuses { + flex: 1 1 auto; min-height: 20vh; max-height: 40vh; overflow-y: auto; overflow-x: hidden; + + @media screen and (max-width: 480px) { + max-height: 10vh; + } } .report-modal__comment { + padding: 20px; + border-right: 1px solid $ui-secondary-color; + max-width: 320px; + + p { + font-size: 14px; + line-height: 20px; + margin-bottom: 20px; + } + .setting-text { - margin-top: 10px; + display: block; + box-sizing: border-box; + width: 100%; + margin: 0; + color: $ui-base-color; + background: $white; + padding: 10px; + font-family: inherit; + font-size: 14px; + resize: vertical; + border: 0; + outline: 0; + border-radius: 4px; + border: 1px solid $ui-secondary-color; + margin-bottom: 20px; + + &:focus { + border: 1px solid darken($ui-secondary-color, 8%); + } + } + + .setting-toggle { + margin-top: 20px; + margin-bottom: 24px; + + &__label { + color: $ui-base-color; + font-size: 14px; + } + } + + @media screen and (max-width: 480px) { + padding: 10px; + max-width: 100%; + order: 2; + + .setting-toggle { + margin-bottom: 4px; + } } } @@ -3908,6 +4171,15 @@ } } +.report-modal__target { + padding: 20px; + + .media-modal__close { + top: 19px; + right: 15px; + } +} + .loading-bar { background-color: $ui-highlight-color; height: 3px; @@ -3954,45 +4226,59 @@ border-radius: 4px; margin-top: 14px; overflow: hidden; -} -.attachment-list__icon { - flex: 0 0 auto; - color: $ui-base-lighter-color; - padding: 8px 18px; - cursor: default; - border-right: 1px solid lighten($ui-base-color, 8%); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: 26px; + &__icon { + flex: 0 0 auto; + color: $ui-base-lighter-color; + padding: 8px 18px; + cursor: default; + border-right: 1px solid lighten($ui-base-color, 8%); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: 26px; - .fa { - display: block; + .fa { + display: block; + } } -} - -.attachment-list__list { - list-style: none; - padding: 4px 0; - padding-left: 8px; - display: flex; - flex-direction: column; - justify-content: center; - li { - display: block; + &__list { + list-style: none; padding: 4px 0; + padding-left: 8px; + display: flex; + flex-direction: column; + justify-content: center; + + li { + display: block; + padding: 4px 0; + } + + a { + text-decoration: none; + color: $ui-base-lighter-color; + font-weight: 500; + + &:hover { + text-decoration: underline; + } + } } - a { - text-decoration: none; - color: $ui-base-lighter-color; - font-weight: 500; + &.compact { + border: 0; + margin-top: 4px; - &:hover { - text-decoration: underline; + .attachment-list__list { + padding: 0; + display: block; + } + + .fa { + color: $ui-base-lighter-color; } } } @@ -4002,6 +4288,7 @@ box-sizing: border-box; margin-top: 8px; overflow: hidden; + border-radius: 4px; position: relative; width: 100%; } @@ -4012,10 +4299,13 @@ display: block; float: left; position: relative; + border-radius: 4px; + overflow: hidden; &.standalone { .media-gallery__item-gifv-thumbnail { transform: none; + top: 0; } } } @@ -4024,14 +4314,19 @@ cursor: zoom-in; display: block; text-decoration: none; + color: $ui-secondary-color; height: 100%; line-height: 0; &, img { width: 100%; - height: 100%; + } + + img { + position: relative; object-fit: cover; + height: auto; } } @@ -4140,7 +4435,7 @@ &.inline { video { - object-fit: cover; + object-fit: contain; position: relative; top: 50%; transform: translateY(-50%); @@ -4360,64 +4655,105 @@ /* End Video Player */ .account-gallery__container { - margin: -2px; - padding: 4px; display: flex; + justify-content: center; flex-wrap: wrap; + padding: 2px; } .account-gallery__item { - flex: 1 1 auto; - width: calc(100% / 3 - 4px); - height: 95px; - margin: 2px; + flex-grow: 1; + width: 50%; + overflow: hidden; + position: relative; + + &::before { + content: ""; + display: block; + padding-top: 100%; + } a { display: block; - width: 100%; - height: 100%; + width: calc(100% - 4px); + height: calc(100% - 4px); + margin: 2px; + top: 0; + left: 0; background-color: $base-overlay-background; background-size: cover; background-position: center; - position: relative; - color: inherit; + position: absolute; + color: $ui-primary-color; text-decoration: none; + border-radius: 4px; &:hover, &:active, &:focus { outline: 0; + color: $ui-secondary-color; + + &::before { + content: ""; + display: block; + width: 100%; + height: 100%; + background: rgba($base-overlay-background, 0.3); + border-radius: 4px; + } } } + + &__icons { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 24px; + } } -.account-section-headline { - color: $ui-base-lighter-color; - background: lighten($ui-base-color, 2%); - border-bottom: 1px solid lighten($ui-base-color, 4%); - padding: 15px 10px; - font-size: 14px; - font-weight: 500; - position: relative; +.account__section-headline { + background: darken($ui-base-color, 4%); + border-bottom: 1px solid lighten($ui-base-color, 8%); cursor: default; + display: flex; - &::before, - &::after { + a { display: block; - content: ""; - position: absolute; - bottom: 0; - left: 18px; - width: 0; - height: 0; - border-style: solid; - border-width: 0 10px 10px; - border-color: transparent transparent lighten($ui-base-color, 4%); - } + flex: 1 1 auto; + color: $ui-primary-color; + padding: 15px 0; + font-size: 14px; + font-weight: 500; + text-align: center; + text-decoration: none; + position: relative; - &::after { - bottom: -1px; - border-color: transparent transparent $ui-base-color; + &.active { + color: $ui-secondary-color; + + &::before, + &::after { + display: block; + content: ""; + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 0; + transform: translateX(-50%); + border-style: solid; + border-width: 0 10px 10px; + border-color: transparent transparent lighten($ui-base-color, 8%); + } + + &::after { + bottom: -1px; + border-color: transparent transparent $ui-base-color; + } + } } } @@ -4739,3 +5075,55 @@ noscript { margin-bottom: 0; } } + +.focal-point { + position: relative; + cursor: pointer; + overflow: hidden; + + &.dragging { + cursor: move; + } + + &__reticle { + position: absolute; + width: 100px; + height: 100px; + transform: translate(-50%, -50%); + background: url('~/images/reticle.png') no-repeat 0 0; + border-radius: 50%; + box-shadow: 0 0 0 9999em rgba($base-shadow-color, 0.35); + } + + &__overlay { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + } +} + +.floating-action-button { + position: fixed; + display: flex; + justify-content: center; + align-items: center; + width: 3.9375rem; + height: 3.9375rem; + bottom: 1.3125rem; + right: 1.3125rem; + background: darken($ui-highlight-color, 3%); + color: $white; + border-radius: 50%; + font-size: 21px; + line-height: 21px; + text-decoration: none; + box-shadow: 2px 3px 9px rgba($base-shadow-color, 0.4); + + &:hover, + &:focus, + &:active { + background: lighten($ui-highlight-color, 7%); + } +} diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index af2589e23..6fa1fa38f 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -1,4 +1,4 @@ -.container { +.container-alt { width: 700px; margin: 0 auto; margin-top: 40px; diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 2bef53cff..d74c5a4fd 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -22,16 +22,6 @@ code { margin-top: 4px; } - h4 { - text-transform: uppercase; - font-size: 13px; - font-weight: 500; - color: $ui-primary-color; - padding-bottom: 8px; - margin-bottom: 8px; - border-bottom: 1px solid lighten($ui-base-color, 8%); - } - p.hint { margin-bottom: 15px; color: $ui-primary-color; @@ -278,6 +268,11 @@ code { .actions { margin-top: 30px; display: flex; + + &.actions--top { + margin-top: 0; + margin-bottom: 30px; + } } button, @@ -568,3 +563,21 @@ code { margin-bottom: 4px; } } + +.alternative-login { + margin-top: 20px; + margin-bottom: 20px; + + h4 { + font-size: 16px; + color: $ui-base-lighter-color; + text-align: center; + margin-bottom: 20px; + border: 0; + padding: 0; + } + + .button { + display: block; + } +} diff --git a/app/javascript/styles/mastodon/stream_entries.scss b/app/javascript/styles/mastodon/stream_entries.scss index a35bbee79..442b143a0 100644 --- a/app/javascript/styles/mastodon/stream_entries.scss +++ b/app/javascript/styles/mastodon/stream_entries.scss @@ -65,6 +65,10 @@ } } + .media-gallery__gifv__label { + bottom: 9px; + } + .status.light { padding: 14px 14px 14px (48px + 14px * 2); position: relative; @@ -142,10 +146,10 @@ a.status__content__spoiler-link { color: $primary-text-color; - background: $ui-primary-color; + background: $ui-base-color; &:hover { - background: lighten($ui-primary-color, 8%); + background: lighten($ui-base-color, 8%); } } } @@ -210,10 +214,10 @@ a.status__content__spoiler-link { color: $primary-text-color; - background: $ui-primary-color; + background: $ui-base-color; &:hover { - background: lighten($ui-primary-color, 8%); + background: lighten($ui-base-color, 8%); } } } @@ -256,16 +260,8 @@ } .media-spoiler { - background: $ui-primary-color; - color: $white; - transition: all 100ms linear; - - &:hover, - &:active, - &:focus { - background: darken($ui-primary-color, 5%); - color: unset; - } + background: $ui-base-color; + color: $ui-primary-color; } .pre-header { diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 0f9e4f263..9b00f0f52 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -44,6 +44,12 @@ class ActivityPub::Activity ActivityPub::Activity::Accept when 'Reject' ActivityPub::Activity::Reject + when 'Flag' + ActivityPub::Activity::Flag + when 'Add' + ActivityPub::Activity::Add + when 'Remove' + ActivityPub::Activity::Remove end end end @@ -74,7 +80,7 @@ class ActivityPub::Activity # Only continue if the status is supposed to have # arrived in real-time - return unless @options[:override_timestamps] + return unless @options[:override_timestamps] || status.within_realtime_window? distribute_to_followers(status) end diff --git a/app/lib/activitypub/activity/add.rb b/app/lib/activitypub/activity/add.rb new file mode 100644 index 000000000..ea94d2f98 --- /dev/null +++ b/app/lib/activitypub/activity/add.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ActivityPub::Activity::Add < ActivityPub::Activity + def perform + return unless @json['target'].present? && value_or_id(@json['target']) == @account.featured_collection_url + + status = status_from_uri(object_uri) + + return unless status.account_id == @account.id && !@account.pinned?(status) + + StatusPin.create!(account: @account, status: status) + end +end diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb index abf2b9b80..c8a358195 100644 --- a/app/lib/activitypub/activity/announce.rb +++ b/app/lib/activitypub/activity/announce.rb @@ -15,7 +15,8 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity account: @account, reblog: original_status, uri: @json['id'], - created_at: @options[:override_timestamps] ? nil : @json['published'] + created_at: @options[:override_timestamps] ? nil : @json['published'], + visibility: original_status.visibility ) distribute(status) @@ -35,6 +36,6 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity end def announceable?(status) - status.public_visibility? || status.unlisted_visibility? + status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility? end end diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 64c429420..5a1c13d67 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -20,13 +20,12 @@ class ActivityPub::Activity::Create < ActivityPub::Activity private def process_status - media_attachments = process_attachments + status_params = process_status_params ApplicationRecord.transaction do @status = Status.create!(status_params) process_tags(@status) - attach_media(@status, media_attachments) end resolve_thread(@status) @@ -40,7 +39,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity status end - def status_params + def process_status_params { uri: @object['id'], url: object_url || @object['id'], @@ -54,6 +53,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity visibility: visibility_from_audience, thread: replied_to_status, conversation: conversation_from_uri(@object['conversation']), + media_attachments: process_attachments.take(4), } end @@ -108,7 +108,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def process_attachments - return if @object['attachment'].nil? + return [] if @object['attachment'].nil? media_attachments = [] @@ -116,7 +116,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity next if unsupported_media_type?(attachment['mediaType']) || attachment['url'].blank? href = Addressable::URI.parse(attachment['url']).normalize.to_s - media_attachment = MediaAttachment.create(account: @account, remote_url: href, description: attachment['name'].presence) + media_attachment = MediaAttachment.create(account: @account, remote_url: href, description: attachment['name'].presence, focus: attachment['focalPoint']) media_attachments << media_attachment next if skip_download? @@ -132,13 +132,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity media_attachments end - def attach_media(status, media_attachments) - return if media_attachments.blank? - - media = MediaAttachment.where(status_id: nil, id: media_attachments.take(4).map(&:id)) - media.update(status_id: status.id) - end - def resolve_thread(status) return unless status.reply? && status.thread.nil? ThreadResolveWorker.perform_async(status.id, in_reply_to_uri) diff --git a/app/lib/activitypub/activity/flag.rb b/app/lib/activitypub/activity/flag.rb new file mode 100644 index 000000000..36d3c5730 --- /dev/null +++ b/app/lib/activitypub/activity/flag.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class ActivityPub::Activity::Flag < ActivityPub::Activity + def perform + target_accounts = object_uris.map { |uri| account_from_uri(uri) }.compact.select(&:local?) + target_statuses_by_account = object_uris.map { |uri| status_from_uri(uri) }.compact.select(&:local?).group_by(&:account_id) + + target_accounts.each do |target_account| + next if Report.where(account: @account, target_account: target_account).exists? + + target_statuses = target_statuses_by_account[target_account.id] + + ReportService.new.call( + @account, + target_account, + status_ids: target_statuses.nil? ? [] : target_statuses.map(&:id), + comment: @json['content'] || '' + ) + end + end + + def object_uris + @object_uris ||= Array(@object.is_a?(Array) ? @object.map { |item| value_or_id(item) } : value_or_id(@object)) + end +end diff --git a/app/lib/activitypub/activity/reject.rb b/app/lib/activitypub/activity/reject.rb index d815feeb6..28d472883 100644 --- a/app/lib/activitypub/activity/reject.rb +++ b/app/lib/activitypub/activity/reject.rb @@ -17,6 +17,8 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity follow_request = FollowRequest.find_by(account: target_account, target_account: @account) follow_request&.reject! + + UnfollowService.new.call(target_account, @account) if target_account.following?(@account) end def target_uri diff --git a/app/lib/activitypub/activity/remove.rb b/app/lib/activitypub/activity/remove.rb new file mode 100644 index 000000000..62a1e3196 --- /dev/null +++ b/app/lib/activitypub/activity/remove.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class ActivityPub::Activity::Remove < ActivityPub::Activity + def perform + return unless @json['target'].present? && value_or_id(@json['target']) == @account.featured_collection_url + + status = status_from_uri(object_uri) + + return unless status.account_id == @account.id + + pin = StatusPin.find_by(account: @account, status: status) + pin&.destroy! + end +end diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index 90d589d90..f19b04ae6 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -17,6 +17,8 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base 'conversation' => 'ostatus:conversation', 'toot' => 'http://joinmastodon.org/ns#', 'Emoji' => 'toot:Emoji', + 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' }, + 'featured' => 'toot:featured', }, ], }.freeze diff --git a/app/lib/exceptions.rb b/app/lib/exceptions.rb index b2489711d..95e3365c2 100644 --- a/app/lib/exceptions.rb +++ b/app/lib/exceptions.rb @@ -4,6 +4,7 @@ module Mastodon class Error < StandardError; end class NotPermittedError < Error; end class ValidationError < Error; end + class HostValidationError < ValidationError; end class RaceConditionError < Error; end class UnexpectedResponseError < Error diff --git a/app/lib/fast_geometry_parser.rb b/app/lib/fast_geometry_parser.rb new file mode 100644 index 000000000..5209c2bc5 --- /dev/null +++ b/app/lib/fast_geometry_parser.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class FastGeometryParser + def self.from_file(file) + width, height = FastImage.size(file.path) + + raise Paperclip::Errors::NotIdentifiedByImageMagickError if width.nil? + + Paperclip::Geometry.new(width, height) + end +end diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index fe5ebfc36..700fd61c4 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -178,22 +178,7 @@ class FeedManager end 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? - 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 + Glitch::KeywordMuteHelper.new(receiver_id).matches?(status) end def filter_from_mentions?(status, receiver_id) @@ -243,6 +228,14 @@ class FeedManager return false end else + # A reblog may reach earlier than the original status because of the + # delay of the worker deliverying the original status, the late addition + # by merging timelines, and other reasons. + # If such a reblog already exists, just do not re-insert it into the feed. + rank = redis.zrevrank(reblog_key, status.id) + + return false unless rank.nil? + redis.zadd(timeline_key, status.id, status.id) end diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 8c0f8cebc..1df4ff8d4 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -19,6 +19,8 @@ class Formatter raw_content = status.text + return '' if raw_content.blank? + unless status.local? html = reformat(raw_content) html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify] diff --git a/app/lib/frontmatter_handler.rb b/app/lib/frontmatter_handler.rb index 83e5f465e..a95a1e294 100644 --- a/app/lib/frontmatter_handler.rb +++ b/app/lib/frontmatter_handler.rb @@ -189,27 +189,6 @@ class FrontmatterHandler case str[0] when '"' str[1..-2] - .gsub(/\\0/, "\u{00}") - .gsub(/\\a/, "\u{07}") - .gsub(/\\b/, "\u{08}") - .gsub(/\\t/, "\u{09}") - .gsub(/\\\u{09}/, "\u{09}") - .gsub(/\\n/, "\u{0a}") - .gsub(/\\v/, "\u{0b}") - .gsub(/\\f/, "\u{0c}") - .gsub(/\\r/, "\u{0d}") - .gsub(/\\e/, "\u{1b}") - .gsub(/\\ /, "\u{20}") - .gsub(/\\"/, "\u{22}") - .gsub(/\\\//, "\u{2f}") - .gsub(/\\\\/, "\u{5c}") - .gsub(/\\N/, "\u{85}") - .gsub(/\\_/, "\u{a0}") - .gsub(/\\L/, "\u{2028}") - .gsub(/\\P/, "\u{2029}") - .gsub(/\\x([0-9a-fA-F]{2})/mu) {|s| $1.to_i.chr Encoding::UTF_8} - .gsub(/\\u([0-9a-fA-F]{4})/mu) {|s| $1.to_i.chr Encoding::UTF_8} - .gsub(/\\U([0-9a-fA-F]{8})/mu) {|s| $1.to_i.chr Encoding::UTF_8} when "'" str[1..-2].gsub(/''/, "'") else diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb index b38407cd3..aa46267dc 100644 --- a/app/lib/ostatus/activity/creation.rb +++ b/app/lib/ostatus/activity/creation.rb @@ -29,7 +29,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base # Skip if the reblogged status is not public return if cached_reblog && !(cached_reblog.public_visibility? || cached_reblog.unlisted_visibility?) - media_attachments = save_media + media_attachments = save_media.take(4) ApplicationRecord.transaction do status = Status.create!( @@ -44,12 +44,12 @@ class OStatus::Activity::Creation < OStatus::Activity::Base language: content_language, visibility: visibility_scope, conversation: find_or_create_conversation, - thread: thread? ? find_status(thread.first) || find_activitypub_status(thread.first, thread.second) : nil + thread: thread? ? find_status(thread.first) || find_activitypub_status(thread.first, thread.second) : nil, + media_attachments: media_attachments ) save_mentions(status) save_hashtags(status) - attach_media(status, media_attachments) save_emojis(status) end @@ -61,7 +61,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base Rails.logger.debug "Queuing remote status #{status.id} (#{id}) for distribution" LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text? - DistributionWorker.perform_async(status.id) if @options[:override_timestamps] + DistributionWorker.perform_async(status.id) if @options[:override_timestamps] || status.within_realtime_window? status end @@ -159,13 +159,6 @@ class OStatus::Activity::Creation < OStatus::Activity::Base media_attachments end - def attach_media(parent, media_attachments) - return if media_attachments.blank? - - media = MediaAttachment.where(status_id: nil, id: media_attachments.take(4).map(&:id)) - media.update(status_id: parent.id) - end - def save_emojis(parent) do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media? diff --git a/app/lib/request.rb b/app/lib/request.rb index 7671f4ffc..5776b3d78 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require 'ipaddr' +require 'socket' + class Request REQUEST_TARGET = '(request-target)' @@ -8,7 +11,7 @@ class Request def initialize(verb, url, **options) @verb = verb @url = Addressable::URI.parse(url).normalize - @options = options + @options = options.merge(socket_class: Socket) @headers = {} set_common_headers! @@ -87,4 +90,18 @@ class Request def http_client HTTP.timeout(:per_operation, timeout).follow(max_hops: 2) end + + class Socket < TCPSocket + class << self + def open(host, *args) + address = IPSocket.getaddress(host) + raise Mastodon::HostValidationError if PrivateAddressCheck.private_address? IPAddr.new(address) + super address, *args + end + + alias new open + end + end + + private_constant :Socket end diff --git a/app/lib/sidekiq_error_handler.rb b/app/lib/sidekiq_error_handler.rb new file mode 100644 index 000000000..23785cf05 --- /dev/null +++ b/app/lib/sidekiq_error_handler.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class SidekiqErrorHandler + def call(*) + yield + rescue Mastodon::HostValidationError => e + Rails.logger.error "#{e.class}: #{e.message}" + Rails.logger.error e.backtrace.join("\n") + # Do not retry + end +end diff --git a/app/lib/status_filter.rb b/app/lib/status_filter.rb index a6a050ce1..41d4381e5 100644 --- a/app/lib/status_filter.rb +++ b/app/lib/status_filter.rb @@ -9,6 +9,7 @@ class StatusFilter end def filtered? + return false if !account.nil? && account.id == status.account_id blocked_by_policy? || (account_present? && filtered_status?) || silenced_account? end diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 5f0176f27..c7afdacc2 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -15,20 +15,21 @@ class UserSettingsDecorator private def process_update - user.settings['notification_emails'] = merged_notification_emails if change?('notification_emails') - user.settings['interactions'] = merged_interactions if change?('interactions') - user.settings['default_privacy'] = default_privacy_preference if change?('setting_default_privacy') - user.settings['default_sensitive'] = default_sensitive_preference if change?('setting_default_sensitive') - user.settings['unfollow_modal'] = unfollow_modal_preference if change?('setting_unfollow_modal') - user.settings['boost_modal'] = boost_modal_preference if change?('setting_boost_modal') + user.settings['notification_emails'] = merged_notification_emails if change?('notification_emails') + user.settings['interactions'] = merged_interactions if change?('interactions') + user.settings['default_privacy'] = default_privacy_preference if change?('setting_default_privacy') + user.settings['default_sensitive'] = default_sensitive_preference if change?('setting_default_sensitive') + user.settings['unfollow_modal'] = unfollow_modal_preference if change?('setting_unfollow_modal') + user.settings['boost_modal'] = boost_modal_preference if change?('setting_boost_modal') user.settings['favourite_modal'] = favourite_modal_preference if change?('setting_favourite_modal') - user.settings['delete_modal'] = delete_modal_preference if change?('setting_delete_modal') - user.settings['auto_play_gif'] = auto_play_gif_preference if change?('setting_auto_play_gif') - user.settings['reduce_motion'] = reduce_motion_preference if change?('setting_reduce_motion') - user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui') - user.settings['noindex'] = noindex_preference if change?('setting_noindex') - user.settings['flavour'] = flavour_preference if change?('setting_flavour') - user.settings['skin'] = skin_preference if change?('setting_skin') + user.settings['delete_modal'] = delete_modal_preference if change?('setting_delete_modal') + user.settings['auto_play_gif'] = auto_play_gif_preference if change?('setting_auto_play_gif') + user.settings['display_sensitive_media'] = display_sensitive_media_preference if change?('setting_display_sensitive_media') + user.settings['reduce_motion'] = reduce_motion_preference if change?('setting_reduce_motion') + user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui') + user.settings['noindex'] = noindex_preference if change?('setting_noindex') + user.settings['flavour'] = flavour_preference if change?('setting_flavour') + user.settings['skin'] = skin_preference if change?('setting_skin') end def merged_notification_emails @@ -71,6 +72,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_auto_play_gif' end + def display_sensitive_media_preference + boolean_cast_setting 'setting_display_sensitive_media' + end + def reduce_motion_preference boolean_cast_setting 'setting_reduce_motion' end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 683b60c86..bf161b9be 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -3,6 +3,7 @@ class ApplicationMailer < ActionMailer::Base layout 'mailer' + helper :application helper :instance helper :mailer diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 9fed4a636..b45844296 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -9,7 +9,7 @@ class NotificationMailer < ApplicationMailer @me = recipient @status = notification.target_status - return if @me.user.disabled? + return if @me.user.disabled? || @status.nil? locale_for_account(@me) do thread_by_conversation(@status.conversation) @@ -33,7 +33,7 @@ class NotificationMailer < ApplicationMailer @account = notification.from_account @status = notification.target_status - return if @me.user.disabled? + return if @me.user.disabled? || @status.nil? locale_for_account(@me) do thread_by_conversation(@status.conversation) @@ -46,7 +46,7 @@ class NotificationMailer < ApplicationMailer @account = notification.from_account @status = notification.target_status - return if @me.user.disabled? + return if @me.user.disabled? || @status.nil? locale_for_account(@me) do thread_by_conversation(@status.conversation) diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 4104f6cd2..9848c34a2 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -3,6 +3,7 @@ class UserMailer < Devise::Mailer layout 'mailer' + helper :application helper :instance add_template_helper RoutingHelper @@ -65,4 +66,16 @@ class UserMailer < Devise::Mailer mail to: @resource.email, subject: I18n.t('user_mailer.welcome.subject') end end + + def backup_ready(user, backup) + @resource = user + @instance = Rails.configuration.x.local_domain + @backup = backup + + return if @resource.disabled? + + I18n.with_locale(@resource.locale || I18n.default_locale) do + mail to: @resource.email, subject: I18n.t('user_mailer.backup_ready.subject') + end + end end diff --git a/app/models/account.rb b/app/models/account.rb index 6df9668d5..61f81ab70 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -43,6 +43,7 @@ # protocol :integer default("ostatus"), not null # memorial :boolean default(FALSE), not null # moved_to_account_id :integer +# featured_collection_url :string # class Account < ApplicationRecord @@ -165,7 +166,7 @@ class Account < ApplicationRecord def refresh! return if local? - ResolveRemoteAccountService.new.call(acct) + ResolveAccountService.new.call(acct) end def unsuspend! diff --git a/app/models/account_domain_block.rb b/app/models/account_domain_block.rb index abcc923b3..bc00b4f32 100644 --- a/app/models/account_domain_block.rb +++ b/app/models/account_domain_block.rb @@ -16,12 +16,16 @@ class AccountDomainBlock < ApplicationRecord belongs_to :account validates :domain, presence: true, uniqueness: { scope: :account_id } - after_create :remove_blocking_cache - after_destroy :remove_blocking_cache + after_commit :remove_blocking_cache + after_commit :remove_relationship_cache private def remove_blocking_cache Rails.cache.delete("exclude_domains_for:#{account_id}") end + + def remove_relationship_cache + Rails.cache.delete_matched("relationship:#{account_id}:*") + end end diff --git a/app/models/backup.rb b/app/models/backup.rb new file mode 100644 index 000000000..5a7e6a14d --- /dev/null +++ b/app/models/backup.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: backups +# +# id :integer not null, primary key +# user_id :integer +# dump_file_name :string +# dump_content_type :string +# dump_file_size :integer +# dump_updated_at :datetime +# processed :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class Backup < ApplicationRecord + belongs_to :user, inverse_of: :backups + + has_attached_file :dump + do_not_validate_attachment_file_type :dump +end diff --git a/app/models/block.rb b/app/models/block.rb index 441e6bca3..d6ecabd3b 100644 --- a/app/models/block.rb +++ b/app/models/block.rb @@ -12,14 +12,14 @@ class Block < ApplicationRecord include Paginable + include RelationshipCacheable belongs_to :account belongs_to :target_account, class_name: 'Account' validates :account_id, uniqueness: { scope: :target_account_id } - after_create :remove_blocking_cache - after_destroy :remove_blocking_cache + after_commit :remove_blocking_cache private diff --git a/app/models/concerns/account_avatar.rb b/app/models/concerns/account_avatar.rb index 8a5c9a22c..9e34a9461 100644 --- a/app/models/concerns/account_avatar.rb +++ b/app/models/concerns/account_avatar.rb @@ -7,8 +7,8 @@ module AccountAvatar class_methods do def avatar_styles(file) - styles = { original: '120x120#' } - styles[:static] = { format: 'png', convert_options: '-coalesce' } if file.content_type == 'image/gif' + styles = { original: { geometry: '400x400#', file_geometry_parser: FastGeometryParser } } + styles[:static] = { geometry: '400x400#', format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif' styles end @@ -17,7 +17,7 @@ module AccountAvatar included do # Avatar upload - has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, convert_options: { all: '-quality 80 -strip' } + has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, convert_options: { all: '-strip' }, processors: [:lazy_thumbnail] validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES validates_attachment_size :avatar, less_than: 2.megabytes end diff --git a/app/models/concerns/account_header.rb b/app/models/concerns/account_header.rb index aff2aa3f9..04c576b28 100644 --- a/app/models/concerns/account_header.rb +++ b/app/models/concerns/account_header.rb @@ -7,8 +7,8 @@ module AccountHeader class_methods do def header_styles(file) - styles = { original: '700x335#' } - styles[:static] = { format: 'png', convert_options: '-coalesce' } if file.content_type == 'image/gif' + styles = { original: { geometry: '700x335#', file_geometry_parser: FastGeometryParser } } + styles[:static] = { geometry: '700x335#', format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif' styles end @@ -17,7 +17,7 @@ module AccountHeader included do # Header upload - has_attached_file :header, styles: ->(f) { header_styles(f) }, convert_options: { all: '-quality 80 -strip' } + has_attached_file :header, styles: ->(f) { header_styles(f) }, convert_options: { all: '-strip' }, processors: [:lazy_thumbnail] validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES validates_attachment_size :header, less_than: 2.megabytes end diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb new file mode 100644 index 000000000..50288e700 --- /dev/null +++ b/app/models/concerns/omniauthable.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +module Omniauthable + extend ActiveSupport::Concern + + TEMP_EMAIL_PREFIX = 'change@me' + TEMP_EMAIL_REGEX = /\Achange@me/ + + included do + def omniauth_providers + Devise.omniauth_configs.keys + end + + def email_verified? + email && email !~ TEMP_EMAIL_REGEX + end + end + + class_methods do + def find_for_oauth(auth, signed_in_resource = nil) + # EOLE-SSO Patch + auth.uid = (auth.uid[0][:uid] || auth.uid[0][:user]) if auth.uid.is_a? Hashie::Array + identity = Identity.find_for_oauth(auth) + + # If a signed_in_resource is provided it always overrides the existing user + # to prevent the identity being locked with accidentally created accounts. + # Note that this may leave zombie accounts (with no associated identity) which + # can be cleaned up at a later date. + user = signed_in_resource ? signed_in_resource : identity.user + user = create_for_oauth(auth) if user.nil? + + if identity.user.nil? + identity.user = user + identity.save! + end + + user + end + + def create_for_oauth(auth) + # Check if the user exists with provided email if the provider gives us a + # verified email. If no verified email was provided or the user already + # exists, we assign a temporary email and ask the user to verify it on + # the next step via Auth::ConfirmationsController.finish_signup + + user = User.new(user_params_from_auth(auth)) + user.account.avatar_remote_url = auth.info.image if auth.info.image =~ /\A#{URI.regexp(%w(http https))}\z/ + user.skip_confirmation! + user.save! + user + end + + private + + def user_params_from_auth(auth) + strategy = Devise.omniauth_configs[auth.provider.to_sym].strategy + assume_verified = strategy.try(:security).try(:assume_email_is_verified) + email_is_verified = auth.info.verified || auth.info.verified_email || assume_verified + email = auth.info.verified_email || auth.info.email + email = email_is_verified && !User.exists?(email: auth.info.email) && email + display_name = auth.info.full_name || [auth.info.first_name, auth.info.last_name].join(' ') + + { + email: email ? email : "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com", + password: Devise.friendly_token[0, 20], + account_attributes: { + username: ensure_unique_username(auth.uid), + display_name: display_name, + }, + } + end + + def ensure_unique_username(starting_username) + username = starting_username + i = 0 + + while Account.exists?(username: username) + i += 1 + username = "#{starting_username}_#{i}" + end + + username + end + end +end diff --git a/app/models/concerns/paginable.rb b/app/models/concerns/paginable.rb index 6061bf9bd..66695677e 100644 --- a/app/models/concerns/paginable.rb +++ b/app/models/concerns/paginable.rb @@ -10,5 +10,14 @@ module Paginable query = query.where(arel_table[:id].gt(since_id)) if since_id.present? query } + + # Differs from :paginate_by_max_id in that it gives the results immediately following min_id, + # whereas since_id gives the items with largest id, but with since_id as a cutoff. + # Results will be in ascending order by id. + scope :paginate_by_min_id, ->(limit, min_id = nil) { + query = reorder(arel_table[:id]).limit(limit) + query = query.where(arel_table[:id].gt(min_id)) if min_id.present? + query + } end end diff --git a/app/models/concerns/relationship_cacheable.rb b/app/models/concerns/relationship_cacheable.rb new file mode 100644 index 000000000..0d9359f7e --- /dev/null +++ b/app/models/concerns/relationship_cacheable.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module RelationshipCacheable + extend ActiveSupport::Concern + + included do + after_commit :remove_relationship_cache + end + + private + + def remove_relationship_cache + Rails.cache.delete("relationship:#{account_id}:#{target_account_id}") + Rails.cache.delete("relationship:#{target_account_id}:#{account_id}") + end +end diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb index 990035b34..020303a2f 100644 --- a/app/models/concerns/remotable.rb +++ b/app/models/concerns/remotable.rb @@ -28,7 +28,11 @@ module Remotable matches = response.headers['content-disposition']&.match(/filename="([^"]*)"/) filename = matches.nil? ? parsed_url.path.split('/').last : matches[1] basename = SecureRandom.hex(8) - extname = File.extname(filename) + extname = if filename.nil? + '' + else + File.extname(filename) + end send("#{attachment_name}=", StringIO.new(response.to_s)) send("#{attachment_name}_file_name=", basename + extname) diff --git a/app/models/favourite.rb b/app/models/favourite.rb index 2b1271f31..fa1884b86 100644 --- a/app/models/favourite.rb +++ b/app/models/favourite.rb @@ -13,6 +13,8 @@ class Favourite < ApplicationRecord include Paginable + update_index('statuses#status', :status) if Chewy.enabled? + belongs_to :account, inverse_of: :favourites belongs_to :status, inverse_of: :favourites, counter_cache: true diff --git a/app/models/follow.rb b/app/models/follow.rb index f953b8e3e..8e6fe537a 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -13,6 +13,7 @@ class Follow < ApplicationRecord include Paginable + include RelationshipCacheable belongs_to :account, counter_cache: :following_count diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index bd6c4a0b9..cde26ceed 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -13,6 +13,7 @@ class FollowRequest < ApplicationRecord include Paginable + include RelationshipCacheable belongs_to :account belongs_to :target_account, class_name: 'Account' diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index dd629279c..32922e7f1 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -34,6 +34,8 @@ class Form::AdminSettings :activity_api_enabled=, :peers_api_enabled, :peers_api_enabled=, + :show_known_fediverse_at_about_page, + :show_known_fediverse_at_about_page=, to: Setting ) end diff --git a/app/models/form/migration.rb b/app/models/form/migration.rb index b74987337..c2a8655e1 100644 --- a/app/models/form/migration.rb +++ b/app/models/form/migration.rb @@ -20,6 +20,6 @@ class Form::Migration private def set_account - self.account = (ResolveRemoteAccountService.new.call(acct) if account.nil? && acct.present?) + self.account = (ResolveAccountService.new.call(acct) if account.nil? && acct.present?) end end diff --git a/app/models/glitch/keyword_mute_helper.rb b/app/models/glitch/keyword_mute_helper.rb new file mode 100644 index 000000000..6d067947f --- /dev/null +++ b/app/models/glitch/keyword_mute_helper.rb @@ -0,0 +1,27 @@ +require 'html2text' + +class Glitch::KeywordMuteHelper + attr_reader :text_matcher + attr_reader :tag_matcher + + def initialize(receiver_id) + @text_matcher = Glitch::KeywordMute.text_matcher_for(receiver_id) + @tag_matcher = Glitch::KeywordMute.tag_matcher_for(receiver_id) + end + + def matches?(status) + matchers_match?(status) || (status.reblog? && matchers_match?(status.reblog)) + end + + private + + def matchers_match?(status) + text_matcher.matches?(prepare_text(status.text)) || + text_matcher.matches?(prepare_text(status.spoiler_text)) || + tag_matcher.matches?(status.tags) + end + + def prepare_text(text) + Html2Text.convert(text) + end +end diff --git a/app/models/identity.rb b/app/models/identity.rb new file mode 100644 index 000000000..a5e0c09ec --- /dev/null +++ b/app/models/identity.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: identities +# +# id :integer not null, primary key +# user_id :integer +# provider :string default(""), not null +# uid :string default(""), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class Identity < ApplicationRecord + belongs_to :user, dependent: :destroy + validates :uid, presence: true, uniqueness: { scope: :provider } + validates :provider, presence: true + + def self.find_for_oauth(auth) + find_or_create_by(uid: auth.uid, provider: auth.provider) + end +end diff --git a/app/models/import.rb b/app/models/import.rb index ba88435bf..fdb4c6b80 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -26,7 +26,7 @@ class Import < ApplicationRecord validates :type, presence: true - has_attached_file :data, url: '/system/:hash.:extension', hash_secret: ENV['PAPERCLIP_SECRET'] + has_attached_file :data validates_attachment_content_type :data, content_type: FILE_TYPES validates_attachment_presence :data end diff --git a/app/models/invite.rb b/app/models/invite.rb index b87a3b722..4ba5432d2 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -4,7 +4,7 @@ # Table name: invites # # id :integer not null, primary key -# user_id :integer +# user_id :integer not null # code :string default(""), not null # expires_at :datetime # max_uses :integer diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 25b7fd085..283d0e714 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -34,7 +34,18 @@ class MediaAttachment < ApplicationRecord VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze AUDIO_MIME_TYPES = ['audio/mpeg', 'audio/mp4', 'audio/vnd.wav', 'audio/wav', 'audio/x-wav', 'audio/x-wave', 'audio/ogg',].freeze - IMAGE_STYLES = { original: '1280x1280>', small: '400x400>' }.freeze + IMAGE_STYLES = { + original: { + geometry: '1280x1280>', + file_geometry_parser: FastGeometryParser, + }, + + small: { + geometry: '400x400>', + file_geometry_parser: FastGeometryParser, + }, + }.freeze + AUDIO_STYLES = { original: { format: 'mp4', @@ -50,6 +61,7 @@ class MediaAttachment < ApplicationRecord }, }, }.freeze + VIDEO_STYLES = { small: { convert_options: { @@ -97,6 +109,24 @@ class MediaAttachment < ApplicationRecord shortcode end + def focus=(point) + return if point.blank? + + x, y = (point.is_a?(Enumerable) ? point : point.split(',')).map(&:to_f) + + meta = file.instance_read(:meta) || {} + meta['focus'] = { 'x' => x, 'y' => y } + + file.instance_write(:meta, meta) + end + + def focus + x = file.meta['focus']['x'] + y = file.meta['focus']['y'] + + "#{x},#{y}" + end + before_create :prepare_description, unless: :local? before_create :set_shortcode before_post_process :set_type_and_extension @@ -178,26 +208,42 @@ class MediaAttachment < ApplicationRecord end def populate_meta - meta = {} + meta = file.instance_read(:meta) || {} file.queued_for_write.each do |style, file| - begin - geo = Paperclip::Geometry.from_file file - - meta[style] = { - width: geo.width.to_i, - height: geo.height.to_i, - size: "#{geo.width.to_i}x#{geo.height.to_i}", - aspect: geo.width.to_f / geo.height.to_f, - } - rescue Paperclip::Errors::NotIdentifiedByImageMagickError - meta[style] = {} - end + meta[style] = style == :small || image? ? image_geometry(file) : video_metadata(file) end meta end + def image_geometry(file) + width, height = FastImage.size(file.path) + + return {} if width.nil? + + { + width: width, + height: height, + size: "#{width}x#{height}", + aspect: width.to_f / height.to_f, + } + end + + def video_metadata(file) + movie = FFMPEG::Movie.new(file.path) + + return {} unless movie.valid? + + { + width: movie.width, + height: movie.height, + frame_rate: movie.frame_rate, + duration: movie.duration, + bitrate: movie.bitrate, + } + end + def appropriate_extension mime_type = MIME::Types[file.content_type] diff --git a/app/models/mute.rb b/app/models/mute.rb index da4787179..ebb3818c7 100644 --- a/app/models/mute.rb +++ b/app/models/mute.rb @@ -13,14 +13,14 @@ class Mute < ApplicationRecord include Paginable + include RelationshipCacheable belongs_to :account belongs_to :target_account, class_name: 'Account' validates :account_id, uniqueness: { scope: :target_account_id } - after_create :remove_blocking_cache - after_destroy :remove_blocking_cache + after_commit :remove_blocking_cache private diff --git a/app/models/notification.rb b/app/models/notification.rb index 733f89cf7..7f8dae5ec 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -69,7 +69,7 @@ class Notification < ApplicationRecord class << self def reload_stale_associations!(cached_items) - account_ids = cached_items.map(&:from_account_id).uniq + account_ids = (cached_items.map(&:from_account_id) + cached_items.map { |item| item.target_status&.account_id }.compact).uniq return if account_ids.empty? @@ -77,6 +77,7 @@ class Notification < ApplicationRecord cached_items.each do |item| item.from_account = accounts[item.from_account_id] + item.target_status.account = accounts[item.target_status.account_id] if item.target_status end end diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb index 716b82243..86eecdfe5 100644 --- a/app/models/preview_card.rb +++ b/app/models/preview_card.rb @@ -33,7 +33,7 @@ class PreviewCard < ApplicationRecord has_and_belongs_to_many :statuses - has_attached_file :image, styles: { original: '400x400>' }, convert_options: { all: '-quality 80 -strip' } + has_attached_file :image, styles: { original: { geometry: '400x400>', file_geometry_parser: FastGeometryParser } }, convert_options: { all: '-quality 80 -strip' } include Attachmentable include Remotable @@ -58,10 +58,11 @@ class PreviewCard < ApplicationRecord return if file.nil? - geo = Paperclip::Geometry.from_file(file) - self.width = geo.width.to_i - self.height = geo.height.to_i - rescue Paperclip::Errors::NotIdentifiedByImageMagickError - nil + width, height = FastImage.size(file.path) + + return nil if width.nil? + + self.width = width + self.height = height end end diff --git a/app/models/report.rb b/app/models/report.rb index f55fb6d3e..dd123fc15 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -24,6 +24,10 @@ class Report < ApplicationRecord validates :comment, length: { maximum: 1000 } + def object_type + :flag + end + def statuses Status.where(id: status_ids).includes(:account, :media_attachments, :mentions) end diff --git a/app/models/site_upload.rb b/app/models/site_upload.rb index 8ffdc8313..641128adf 100644 --- a/app/models/site_upload.rb +++ b/app/models/site_upload.rb @@ -34,8 +34,8 @@ class SiteUpload < ApplicationRecord return if tempfile.nil? - geometry = Paperclip::Geometry.from_file(tempfile) - self.meta = { width: geometry.width.to_i, height: geometry.height.to_i } + width, height = FastImage.size(tempfile.path) + self.meta = { width: width, height: height } end def clear_cache diff --git a/app/models/status.rb b/app/models/status.rb index e927fb9dd..7e5ca09e4 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -32,6 +32,8 @@ class Status < ApplicationRecord include Cacheable include StatusThreadingConcern + update_index('statuses#status', :proper) if Chewy.enabled? + enum visibility: [:public, :unlisted, :private, :direct], _suffix: :visibility belongs_to :application, class_name: 'Doorkeeper::Application', optional: true @@ -56,7 +58,7 @@ class Status < ApplicationRecord has_one :stream_entry, as: :activity, inverse_of: :status validates :uri, uniqueness: true, presence: true, unless: :local? - validates :text, presence: true, unless: :reblog? + validates :text, presence: true, unless: -> { with_media? || reblog? } validates_with StatusLengthValidator validates :reblog, uniqueness: { scope: :account }, if: :reblog? @@ -77,10 +79,28 @@ class Status < ApplicationRecord scope :not_local_only, -> { where(local_only: [false, nil]) } - cache_associated :account, :application, :media_attachments, :tags, :stream_entry, mentions: :account, reblog: [:account, :application, :stream_entry, :tags, :media_attachments, mentions: :account], thread: :account + cache_associated :account, :application, :media_attachments, :conversation, :tags, :stream_entry, mentions: :account, reblog: [:account, :application, :stream_entry, :tags, :media_attachments, :conversation, mentions: :account], thread: :account delegate :domain, to: :account, prefix: true + REAL_TIME_WINDOW = 6.hours + + def searchable_by(preloaded = nil) + ids = [account_id] + + if preloaded.nil? + ids += mentions.pluck(:account_id) + ids += favourites.pluck(:account_id) + ids += reblogs.pluck(:account_id) + else + ids += preloaded.mentions[id] || [] + ids += preloaded.favourites[id] || [] + ids += preloaded.reblogs[id] || [] + end + + ids.uniq + end + def reply? !in_reply_to_id.nil? || attributes['reply'] end @@ -93,6 +113,10 @@ class Status < ApplicationRecord !reblog_of_id.nil? end + def within_realtime_window? + created_at >= REAL_TIME_WINDOW.ago + end + def verb if destroyed? :delete @@ -129,8 +153,12 @@ class Status < ApplicationRecord private_visibility? || direct_visibility? end + def with_media? + media_attachments.any? + end + def non_sensitive_with_media? - !sensitive? && media_attachments.any? + !sensitive? && with_media? end def emojis diff --git a/app/models/user.rb b/app/models/user.rb index 3cf9900bd..0346cf8ae 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -34,15 +34,17 @@ # disabled :boolean default(FALSE), not null # moderator :boolean default(FALSE), not null # invite_id :integer +# remember_token :string # class User < ApplicationRecord include Settings::Extend + include Omniauthable ACTIVE_DURATION = 14.days devise :two_factor_authenticatable, - otp_secret_encryption_key: ENV['OTP_SECRET'] + otp_secret_encryption_key: Rails.configuration.x.otp_secret devise :two_factor_backupable, otp_number_of_backup_codes: 10 @@ -50,11 +52,14 @@ class User < ApplicationRecord devise :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable + devise :omniauthable + belongs_to :account, inverse_of: :user belongs_to :invite, counter_cache: :uses, optional: true accepts_nested_attributes_for :account has_many :applications, class_name: 'Doorkeeper::Application', as: :owner + has_many :backups, inverse_of: :user validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale? validates_with BlacklistedEmailValidator, if: :email_changed? @@ -79,11 +84,44 @@ class User < ApplicationRecord has_many :session_activations, dependent: :destroy delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal, - :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, + :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_sensitive_media, to: :settings, prefix: :setting, allow_nil: false attr_accessor :invite_code + def pam_conflict(_) + # block pam login tries on traditional account + nil + end + + def pam_conflict? + return false unless Devise.pam_authentication + encrypted_password.present? && is_pam_account? + end + + def pam_get_name + return account.username if account.present? + super + end + + def pam_setup(_attributes) + acc = Account.new(username: pam_get_name) + acc.save!(validate: false) + + self.email = "#{acc.username}@#{find_pam_suffix}" if email.nil? && find_pam_suffix + self.confirmed_at = Time.now.utc + self.admin = false + self.account = acc + + acc.destroy! unless save + end + + def ldap_setup(_attributes) + self.confirmed_at = Time.now.utc + self.admin = false + save! + end + def confirmed? confirmed_at.present? end @@ -129,7 +167,7 @@ class User < ApplicationRecord new_user = !confirmed? super - update_statistics! if new_user + prepare_new_user! if new_user end def confirm! @@ -137,7 +175,12 @@ class User < ApplicationRecord skip_confirmation! save! - update_statistics! if new_user + prepare_new_user! if new_user + end + + def update_tracked_fields!(request) + super + prepare_returning_user! end def promote! @@ -208,6 +251,56 @@ class User < ApplicationRecord @invite_code = code end + def password_required? + return false if Devise.pam_authentication || Devise.ldap_authentication + super + end + + def send_reset_password_instructions + return false if encrypted_password.blank? && (Devise.pam_authentication || Devise.ldap_authentication) + super + end + + def reset_password!(new_password, new_password_confirmation) + return false if encrypted_password.blank? && (Devise.pam_authentication || Devise.ldap_authentication) + super + end + + def self.pam_get_user(attributes = {}) + if attributes[:email] + resource = + if Devise.check_at_sign && !attributes[:email].index('@') + joins(:account).find_by(accounts: { username: attributes[:email] }) + else + find_by(email: attributes[:email]) + end + + if resource.blank? + resource = new(email: attributes[:email]) + if Devise.check_at_sign && !resource[:email].index('@') + resource[:email] = "#{attributes[:email]}@#{resource.find_pam_suffix}" + end + end + resource + end + end + + def self.ldap_get_user(attributes = {}) + resource = joins(:account).find_by(accounts: { username: attributes[Devise.ldap_uid.to_sym].first }) + + if resource.blank? + resource = new(email: attributes[:mail].first, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first }) + resource.ldap_setup(attributes) + end + + resource + end + + def self.authenticate_with_pam(attributes = {}) + return nil unless Devise.pam_authentication + super + end + protected def send_devise_notification(notification, *args) @@ -220,9 +313,23 @@ class User < ApplicationRecord filtered_languages.reject!(&:blank?) end - def update_statistics! + def prepare_new_user! BootstrapTimelineWorker.perform_async(account_id) ActivityTracker.increment('activity:accounts:local') UserMailer.welcome(self).deliver_later end + + def prepare_returning_user! + ActivityTracker.record('activity:logins', id) + regenerate_feed! if needs_feed_update? + end + + def regenerate_feed! + Redis.current.setnx("account:#{account_id}:regeneration", true) && Redis.current.expire("account:#{account_id}:regeneration", 1.day.seconds) + RegenerationWorker.perform_async(account_id) + end + + def needs_feed_update? + last_sign_in_at < ACTIVE_DURATION.ago + end end diff --git a/app/models/web/setting.rb b/app/models/web/setting.rb index 12b9d1226..0a5129d17 100644 --- a/app/models/web/setting.rb +++ b/app/models/web/setting.rb @@ -7,7 +7,7 @@ # data :json # created_at :datetime not null # updated_at :datetime not null -# user_id :integer +# user_id :integer not null # class Web::Setting < ApplicationRecord diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb index 3e617001f..d1de5e81a 100644 --- a/app/policies/application_policy.rb +++ b/app/policies/application_policy.rb @@ -15,4 +15,8 @@ class ApplicationPolicy def current_user current_account&.user end + + def user_signed_in? + !current_user.nil? + end end diff --git a/app/policies/backup_policy.rb b/app/policies/backup_policy.rb new file mode 100644 index 000000000..0ef89a8d0 --- /dev/null +++ b/app/policies/backup_policy.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class BackupPolicy < ApplicationPolicy + MIN_AGE = 1.week + + def create? + user_signed_in? && current_user.backups.where('created_at >= ?', MIN_AGE.ago).count.zero? + end +end diff --git a/app/presenters/account_relationships_presenter.rb b/app/presenters/account_relationships_presenter.rb index bf1ba3716..b1e99b31b 100644 --- a/app/presenters/account_relationships_presenter.rb +++ b/app/presenters/account_relationships_presenter.rb @@ -5,11 +5,67 @@ class AccountRelationshipsPresenter :muting, :requested, :domain_blocking def initialize(account_ids, current_account_id, **options) - @following = Account.following_map(account_ids, current_account_id).merge(options[:following_map] || {}) - @followed_by = Account.followed_by_map(account_ids, current_account_id).merge(options[:followed_by_map] || {}) - @blocking = Account.blocking_map(account_ids, current_account_id).merge(options[:blocking_map] || {}) - @muting = Account.muting_map(account_ids, current_account_id).merge(options[:muting_map] || {}) - @requested = Account.requested_map(account_ids, current_account_id).merge(options[:requested_map] || {}) - @domain_blocking = Account.domain_blocking_map(account_ids, current_account_id).merge(options[:domain_blocking_map] || {}) + @account_ids = account_ids.map { |a| a.is_a?(Account) ? a.id : a } + @current_account_id = current_account_id + + @following = cached[:following].merge(Account.following_map(@uncached_account_ids, @current_account_id)) + @followed_by = cached[:followed_by].merge(Account.followed_by_map(@uncached_account_ids, @current_account_id)) + @blocking = cached[:blocking].merge(Account.blocking_map(@uncached_account_ids, @current_account_id)) + @muting = cached[:muting].merge(Account.muting_map(@uncached_account_ids, @current_account_id)) + @requested = cached[:requested].merge(Account.requested_map(@uncached_account_ids, @current_account_id)) + @domain_blocking = cached[:domain_blocking].merge(Account.domain_blocking_map(@uncached_account_ids, @current_account_id)) + + cache_uncached! + + @following.merge!(options[:following_map] || {}) + @followed_by.merge!(options[:followed_by_map] || {}) + @blocking.merge!(options[:blocking_map] || {}) + @muting.merge!(options[:muting_map] || {}) + @requested.merge!(options[:requested_map] || {}) + @domain_blocking.merge!(options[:domain_blocking_map] || {}) + end + + private + + def cached + return @cached if defined?(@cached) + + @cached = { + following: {}, + followed_by: {}, + blocking: {}, + muting: {}, + requested: {}, + domain_blocking: {}, + } + + @uncached_account_ids = [] + + @account_ids.each do |account_id| + maps_for_account = Rails.cache.read("relationship:#{@current_account_id}:#{account_id}") + + if maps_for_account.is_a?(Hash) + @cached.deep_merge!(maps_for_account) + else + @uncached_account_ids << account_id + end + end + + @cached + end + + def cache_uncached! + @uncached_account_ids.each do |account_id| + maps_for_account = { + following: { account_id => following[account_id] }, + followed_by: { account_id => followed_by[account_id] }, + blocking: { account_id => blocking[account_id] }, + muting: { account_id => muting[account_id] }, + requested: { account_id => requested[account_id] }, + domain_blocking: { account_id => domain_blocking[account_id] }, + } + + Rails.cache.write("relationship:#{@current_account_id}:#{account_id}", maps_for_account, expires_in: 1.day) + end end end diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb index 1c08fb3bc..db288d5db 100644 --- a/app/presenters/instance_presenter.rb +++ b/app/presenters/instance_presenter.rb @@ -48,4 +48,8 @@ class InstancePresenter def thumbnail @thumbnail ||= Rails.cache.fetch('site_uploads/thumbnail') { SiteUpload.find_by(var: 'thumbnail') } end + + def hero + @hero ||= Rails.cache.fetch('site_uploads/hero') { SiteUpload.find_by(var: 'hero') } + end end diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 622bdde0c..afcd37771 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -4,7 +4,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer include RoutingHelper attributes :id, :type, :following, :followers, - :inbox, :outbox, + :inbox, :outbox, :featured, :preferred_username, :name, :summary, :url, :manually_approves_followers @@ -53,6 +53,10 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer account_outbox_url(object) end + def featured + account_collection_url(object, :featured) + end + def endpoints object end diff --git a/app/serializers/activitypub/add_serializer.rb b/app/serializers/activitypub/add_serializer.rb new file mode 100644 index 000000000..c0906e8d0 --- /dev/null +++ b/app/serializers/activitypub/add_serializer.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class ActivityPub::AddSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :type, :actor, :target + attribute :proper_object, key: :object + + def type + 'Add' + end + + def actor + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def proper_object + ActivityPub::TagManager.instance.uri_for(object) + end + + def target + account_collection_url(object.account, :featured) + end +end diff --git a/app/serializers/activitypub/collection_serializer.rb b/app/serializers/activitypub/collection_serializer.rb index 9832133fc..1ae492945 100644 --- a/app/serializers/activitypub/collection_serializer.rb +++ b/app/serializers/activitypub/collection_serializer.rb @@ -2,7 +2,7 @@ class ActivityPub::CollectionSerializer < ActiveModel::Serializer def self.serializer_for(model, options) - return ActivityPub::ActivitySerializer if model.class.name == 'Status' + return ActivityPub::NoteSerializer if model.class.name == 'Status' return ActivityPub::CollectionSerializer if model.class.name == 'ActivityPub::CollectionPresenter' super end @@ -13,8 +13,8 @@ class ActivityPub::CollectionSerializer < ActiveModel::Serializer attribute :part_of, if: -> { object.part_of.present? } has_one :first, if: -> { object.first.present? } - has_many :items, key: :items, if: -> { (object.items.present? || page?) && !ordered? } - has_many :items, key: :ordered_items, if: -> { (object.items.present? || page?) && ordered? } + has_many :items, key: :items, if: -> { (!object.items.nil? || page?) && !ordered? } + has_many :items, key: :ordered_items, if: -> { (!object.items.nil? || page?) && ordered? } def type if page? diff --git a/app/serializers/activitypub/flag_serializer.rb b/app/serializers/activitypub/flag_serializer.rb new file mode 100644 index 000000000..53e8f726d --- /dev/null +++ b/app/serializers/activitypub/flag_serializer.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class ActivityPub::FlagSerializer < ActiveModel::Serializer + attributes :id, :type, :actor, :content + attribute :virtual_object, key: :object + + def id + # This is nil for now + ActivityPub::TagManager.instance.uri_for(object) + end + + def type + 'Flag' + end + + def actor + ActivityPub::TagManager.instance.uri_for(instance_options[:account] || object.account) + end + + def virtual_object + [ActivityPub::TagManager.instance.uri_for(object.target_account)] + object.statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) } + end + + def content + object.comment + end +end diff --git a/app/serializers/activitypub/image_serializer.rb b/app/serializers/activitypub/image_serializer.rb index a015c6b1b..3c08f77e8 100644 --- a/app/serializers/activitypub/image_serializer.rb +++ b/app/serializers/activitypub/image_serializer.rb @@ -4,6 +4,7 @@ class ActivityPub::ImageSerializer < ActiveModel::Serializer include RoutingHelper attributes :type, :media_type, :url + attribute :focal_point, if: :focal_point? def type 'Image' @@ -16,4 +17,12 @@ class ActivityPub::ImageSerializer < ActiveModel::Serializer def media_type object.content_type end + + def focal_point? + object.respond_to?(:meta) && object.meta.is_a?(Hash) && object.meta['focus'].is_a?(Hash) + end + + def focal_point + [object.meta['focus']['x'], object.meta['focus']['y']] + end end diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 24c39f3c9..d0e6290c1 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -90,6 +90,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer include RoutingHelper attributes :type, :media_type, :url, :name + attribute :focal_point, if: :focal_point? def type 'Document' @@ -106,6 +107,14 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer def url object.local? ? full_asset_url(object.file.url(:original, false)) : object.remote_url end + + def focal_point? + object.file.meta.is_a?(Hash) && object.file.meta['focus'].is_a?(Hash) + end + + def focal_point + [object.file.meta['focus']['x'], object.file.meta['focus']['y']] + end end class MentionSerializer < ActiveModel::Serializer diff --git a/app/serializers/activitypub/outbox_serializer.rb b/app/serializers/activitypub/outbox_serializer.rb new file mode 100644 index 000000000..48fbad0fd --- /dev/null +++ b/app/serializers/activitypub/outbox_serializer.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class ActivityPub::OutboxSerializer < ActivityPub::CollectionSerializer + def self.serializer_for(model, options) + return ActivityPub::ActivitySerializer if model.is_a?(Status) + super + end +end diff --git a/app/serializers/activitypub/remove_serializer.rb b/app/serializers/activitypub/remove_serializer.rb new file mode 100644 index 000000000..c2a5ae1b3 --- /dev/null +++ b/app/serializers/activitypub/remove_serializer.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class ActivityPub::RemoveSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :type, :actor, :target + attribute :proper_object, key: :object + + def type + 'Remove' + end + + def actor + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def proper_object + ActivityPub::TagManager.instance.uri_for(object) + end + + def target + account_collection_url(object.account, :featured) + end +end diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 904daa804..1d17e2b0a 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -22,16 +22,18 @@ class InitialStateSerializer < ActiveModel::Serializer locale: I18n.locale, domain: Rails.configuration.x.local_domain, admin: object.admin&.id&.to_s, + search_enabled: Chewy.enabled?, } if object.current_account - store[:me] = object.current_account.id.to_s - store[:unfollow_modal] = object.current_account.user.setting_unfollow_modal - store[:boost_modal] = object.current_account.user.setting_boost_modal - store[:favourite_modal] = object.current_account.user.setting_favourite_modal - store[:delete_modal] = object.current_account.user.setting_delete_modal - store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif - store[:reduce_motion] = object.current_account.user.setting_reduce_motion + store[:me] = object.current_account.id.to_s + store[:unfollow_modal] = object.current_account.user.setting_unfollow_modal + store[:boost_modal] = object.current_account.user.setting_boost_modal + store[:favourite_modal] = object.current_account.user.setting_favourite_modal + store[:delete_modal] = object.current_account.user.setting_delete_modal + store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif + store[:display_sensitive_media] = object.current_account.user.setting_display_sensitive_media + store[:reduce_motion] = object.current_account.user.setting_reduce_motion end store diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index 65907dad2..cab05e60a 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -4,7 +4,12 @@ class REST::InstanceSerializer < ActiveModel::Serializer include RoutingHelper attributes :uri, :title, :description, :email, - :version, :urls, :stats, :thumbnail, :max_toot_chars + :version, :urls, :stats, :thumbnail, :max_toot_chars, + :languages + + has_one :contact_account, serializer: REST::AccountSerializer + + delegate :contact_account, to: :instance_presenter def uri Rails.configuration.x.local_domain @@ -46,6 +51,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer { streaming_api: Rails.configuration.x.streaming_api_base_url } end + def languages + [I18n.default_locale] + end + private def instance_presenter diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb index 3be110665..3860a9cbd 100644 --- a/app/services/account_search_service.rb +++ b/app/services/account_search_service.rb @@ -18,7 +18,7 @@ class AccountSearchService < BaseService return [] if query_blank_or_hashtag? || limit < 1 if resolving_non_matching_remote_account? - [ResolveRemoteAccountService.new.call("#{query_username}@#{query_domain}")].compact + [ResolveAccountService.new.call("#{query_username}@#{query_domain}")].compact else search_results_and_exact_match.compact.uniq.slice(0, limit) end diff --git a/app/services/activitypub/fetch_featured_collection_service.rb b/app/services/activitypub/fetch_featured_collection_service.rb new file mode 100644 index 000000000..40714e980 --- /dev/null +++ b/app/services/activitypub/fetch_featured_collection_service.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +class ActivityPub::FetchFeaturedCollectionService < BaseService + include JsonLdHelper + + def call(account) + @account = account + @json = fetch_resource(@account.featured_collection_url, true) + + return unless supported_context? + return if @account.suspended? || @account.local? + + case @json['type'] + when 'Collection', 'CollectionPage' + process_items @json['items'] + when 'OrderedCollection', 'OrderedCollectionPage' + process_items @json['orderedItems'] + end + end + + private + + def process_items(items) + status_ids = items.map { |item| value_or_id(item) } + .reject { |uri| ActivityPub::TagManager.instance.local_uri?(uri) } + .map { |uri| ActivityPub::FetchRemoteStatusService.new.call(uri) } + .compact + .select { |status| status.account_id == @account.id } + .map(&:id) + + to_remove = [] + to_add = status_ids + + StatusPin.where(account: @account).pluck(:status_id).each do |status_id| + if status_ids.include?(status_id) + to_add.delete(status_id) + else + to_remove << status_id + end + end + + StatusPin.where(account: @account, status_id: to_remove).delete_all unless to_remove.empty? + + to_add.each do |status_id| + StatusPin.create!(account: @account, status_id: status_id) + end + end + + def supported_context? + super(@json) + end +end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index f43edafe7..68e9db766 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -27,6 +27,7 @@ class ActivityPub::ProcessAccountService < BaseService after_protocol_change! if protocol_changed? after_key_change! if key_changed? + check_featured_collection! if @account.featured_collection_url.present? @account rescue Oj::ParseError @@ -57,14 +58,15 @@ class ActivityPub::ProcessAccountService < BaseService end def set_immediate_attributes! - @account.inbox_url = @json['inbox'] || '' - @account.outbox_url = @json['outbox'] || '' - @account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || '' - @account.followers_url = @json['followers'] || '' - @account.url = url || @uri - @account.display_name = @json['name'] || '' - @account.note = @json['summary'] || '' - @account.locked = @json['manuallyApprovesFollowers'] || false + @account.inbox_url = @json['inbox'] || '' + @account.outbox_url = @json['outbox'] || '' + @account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || '' + @account.followers_url = @json['followers'] || '' + @account.featured_collection_url = @json['featured'] || '' + @account.url = url || @uri + @account.display_name = @json['name'] || '' + @account.note = @json['summary'] || '' + @account.locked = @json['manuallyApprovesFollowers'] || false end def set_fetchable_attributes! @@ -85,6 +87,10 @@ class ActivityPub::ProcessAccountService < BaseService RefollowWorker.perform_async(@account.id) end + def check_featured_collection! + ActivityPub::SynchronizeFeaturedCollectionWorker.perform_async(@account.id) + end + def image_url(key) value = first_of_value(@json[key]) diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb new file mode 100644 index 000000000..fadc24a82 --- /dev/null +++ b/app/services/backup_service.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require 'rubygems/package' + +class BackupService < BaseService + attr_reader :account, :backup, :collection + + def call(backup) + @backup = backup + @account = backup.user.account + + build_json! + build_archive! + end + + private + + def build_json! + @collection = serialize(collection_presenter, ActivityPub::CollectionSerializer) + + account.statuses.with_includes.find_in_batches do |statuses| + statuses.each do |status| + item = serialize(status, ActivityPub::ActivitySerializer) + item.delete(:'@context') + + unless item[:type] == 'Announce' || item[:object][:attachment].blank? + item[:object][:attachment].each do |attachment| + attachment[:url] = Addressable::URI.parse(attachment[:url]).path.gsub(/\A\/system\//, '') + end + end + + @collection[:orderedItems] << item + end + + GC.start + end + end + + def build_archive! + tmp_file = Tempfile.new(%w(archive .tar.gz)) + + File.open(tmp_file, 'wb') do |file| + Zlib::GzipWriter.wrap(file) do |gz| + Gem::Package::TarWriter.new(gz) do |tar| + dump_media_attachments!(tar) + dump_outbox!(tar) + dump_actor!(tar) + end + end + end + + archive_filename = ['archive', Time.now.utc.strftime('%Y%m%d%H%M%S'), SecureRandom.hex(2)].join('-') + '.tar.gz' + + @backup.dump = ActionDispatch::Http::UploadedFile.new(tempfile: tmp_file, filename: archive_filename) + @backup.processed = true + @backup.save! + ensure + tmp_file.close + tmp_file.unlink + end + + def dump_media_attachments!(tar) + MediaAttachment.attached.where(account: account).find_in_batches do |media_attachments| + media_attachments.each do |m| + download_to_tar(tar, m.file, m.file.path) + end + + GC.start + end + end + + def dump_outbox!(tar) + json = Oj.dump(collection) + + tar.add_file_simple('outbox.json', 0o444, json.bytesize) do |io| + io.write(json) + end + end + + def dump_actor!(tar) + actor = serialize(account, ActivityPub::ActorSerializer) + + actor[:icon][:url] = 'avatar' + File.extname(actor[:icon][:url]) if actor[:icon] + actor[:image][:url] = 'header' + File.extname(actor[:image][:url]) if actor[:image] + + download_to_tar(tar, account.avatar, 'avatar' + File.extname(account.avatar.path)) if account.avatar.exists? + download_to_tar(tar, account.header, 'header' + File.extname(account.header.path)) if account.header.exists? + + json = Oj.dump(actor) + + tar.add_file_simple('actor.json', 0o444, json.bytesize) do |io| + io.write(json) + end + + tar.add_file_simple('key.pem', 0o444, account.private_key.bytesize) do |io| + io.write(account.private_key) + end + end + + def collection_presenter + ActivityPub::CollectionPresenter.new( + id: account_outbox_url(account), + type: :ordered, + size: account.statuses_count, + items: [] + ) + end + + def serialize(object, serializer) + ActiveModelSerializers::SerializableResource.new( + object, + serializer: serializer, + adapter: ActivityPub::Adapter + ).as_json + end + + CHUNK_SIZE = 1.megabyte + + def download_to_tar(tar, attachment, filename) + adapter = Paperclip.io_adapters.for(attachment) + + tar.add_file_simple(filename, 0o444, adapter.size) do |io| + while (buffer = adapter.read(CHUNK_SIZE)) + io.write(buffer) + end + end + end +end diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb index eefdc0dbf..d082de40b 100644 --- a/app/services/block_domain_service.rb +++ b/app/services/block_domain_service.rb @@ -5,13 +5,14 @@ class BlockDomainService < BaseService def call(domain_block) @domain_block = domain_block - process_domain_block + process_domain_block! end private - def process_domain_block + def process_domain_block! clear_media! if domain_block.reject_media? + if domain_block.silence? silence_accounts! elsif domain_block.suspend? @@ -19,14 +20,26 @@ class BlockDomainService < BaseService end end + def invalidate_association_caches! + # Normally, associated models of a status are immutable (except for accounts) + # so they are aggressively cached. After updating the media attachments to no + # longer point to a local file, we need to clear the cache to make those + # changes appear in the API and UI + @affected_status_ids.each { |id| Rails.cache.delete_matched("statuses/#{id}-*") } + end + def silence_accounts! blocked_domain_accounts.in_batches.update_all(silenced: true) end def clear_media! - clear_account_images - clear_account_attachments - clear_emojos + @affected_status_ids = [] + + clear_account_images! + clear_account_attachments! + clear_emojos! + + invalidate_association_caches! end def suspend_accounts! @@ -36,23 +49,25 @@ class BlockDomainService < BaseService end end - def clear_account_images + def clear_account_images! blocked_domain_accounts.find_each do |account| - account.avatar.destroy - account.header.destroy + account.avatar.destroy if account.avatar.exists? + account.header.destroy if account.header.exists? account.save end end - def clear_account_attachments + def clear_account_attachments! media_from_blocked_domain.find_each do |attachment| - attachment.file.destroy + @affected_status_ids << attachment.status_id if attachment.status_id.present? + + attachment.file.destroy if attachment.file.exists? attachment.type = :unknown attachment.save end end - def clear_emojos + def clear_emojos! emojis_from_blocked_domains.destroy_all end diff --git a/app/services/concerns/author_extractor.rb b/app/services/concerns/author_extractor.rb index c2366188a..1e00eb803 100644 --- a/app/services/concerns/author_extractor.rb +++ b/app/services/concerns/author_extractor.rb @@ -18,6 +18,6 @@ module AuthorExtractor acct = "#{username}@#{domain}" end - ResolveRemoteAccountService.new.call(acct, update_profile) + ResolveAccountService.new.call(acct, update_profile) end end diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index d0472a1d7..8f252e64c 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -91,17 +91,19 @@ class FetchLinkCardService < BaseService case @card.type when 'link' - @card.image = URI.parse(embed.thumbnail_url) if embed.respond_to?(:thumbnail_url) + @card.image_remote_url = embed.thumbnail_url if embed.respond_to?(:thumbnail_url) when 'photo' return false unless embed.respond_to?(:url) - @card.embed_url = embed.url - @card.image = URI.parse(embed.url) - @card.width = embed.width.presence || 0 - @card.height = embed.height.presence || 0 + + @card.embed_url = embed.url + @card.image_remote_url = embed.url + @card.width = embed.width.presence || 0 + @card.height = embed.height.presence || 0 when 'video' - @card.width = embed.width.presence || 0 - @card.height = embed.height.presence || 0 - @card.html = Formatter.instance.sanitize(embed.html, Sanitize::Config::MASTODON_OEMBED) + @card.width = embed.width.presence || 0 + @card.height = embed.height.presence || 0 + @card.html = Formatter.instance.sanitize(embed.html, Sanitize::Config::MASTODON_OEMBED) + @card.image_remote_url = embed.thumbnail_url if embed.respond_to?(:thumbnail_url) when 'rich' # Most providers rely on <script> tags, which is a no-no return false @@ -130,12 +132,12 @@ class FetchLinkCardService < BaseService scrolling: 'no', frameborder: '0') else - @card.type = :link - @card.image_remote_url = meta_property(page, 'og:image') if meta_property(page, 'og:image') + @card.type = :link end - @card.title = meta_property(page, 'og:title').presence || page.at_xpath('//title')&.content || '' - @card.description = meta_property(page, 'og:description').presence || meta_property(page, 'description') || '' + @card.title = meta_property(page, 'og:title').presence || page.at_xpath('//title')&.content || '' + @card.description = meta_property(page, 'og:description').presence || meta_property(page, 'description') || '' + @card.image_remote_url = meta_property(page, 'og:image') if meta_property(page, 'og:image') return if @card.title.blank? && @card.html.blank? diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index ac0207a0a..60a389afd 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -9,7 +9,7 @@ class FollowService < BaseService # @param [true, false, nil] reblogs Whether or not to show reblogs, defaults to true def call(source_account, uri, reblogs: nil) reblogs = true if reblogs.nil? - target_account = uri.is_a?(Account) ? uri : ResolveRemoteAccountService.new.call(uri) + target_account = uri.is_a?(Account) ? uri : ResolveAccountService.new.call(uri) raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended? raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 6b6a37676..74b4cba0c 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -21,17 +21,18 @@ class PostStatusService < BaseService media = validate_media!(options[:media_ids]) status = nil + text = options.delete(:spoiler_text) if text.blank? && options[:spoiler_text].present? + text = '.' if text.blank? && !media.empty? ApplicationRecord.transaction do status = account.statuses.create!(text: text, + media_attachments: media || [], thread: in_reply_to, sensitive: options[:sensitive], spoiler_text: options[:spoiler_text] || '', visibility: options[:visibility] || account.user&.setting_default_privacy, language: LanguageDetector.instance.detect(text, account), application: options[:application]) - - attach_media(status, media) end process_mentions_service.call(status) @@ -67,11 +68,6 @@ class PostStatusService < BaseService media end - def attach_media(status, media) - return if media.nil? - media.update(status_id: status.id) - end - def process_mentions_service ProcessMentionsService.new end diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index 46401f298..8e285e1f7 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -16,7 +16,7 @@ class ProcessMentionsService < BaseService if mention_undeliverable?(status, mentioned_account) begin - mentioned_account = resolve_remote_account_service.call($1) + mentioned_account = resolve_account_service.call($1) rescue Goldfinger::Error, HTTP::Error mentioned_account = nil end @@ -63,7 +63,7 @@ class ProcessMentionsService < BaseService ).as_json).sign!(status.account)) end - def resolve_remote_account_service - ResolveRemoteAccountService.new + def resolve_account_service + ResolveAccountService.new end end diff --git a/app/services/report_service.rb b/app/services/report_service.rb new file mode 100644 index 000000000..c06488a6d --- /dev/null +++ b/app/services/report_service.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +class ReportService < BaseService + def call(source_account, target_account, options = {}) + @source_account = source_account + @target_account = target_account + @status_ids = options.delete(:status_ids) || [] + @comment = options.delete(:comment) || '' + @options = options + + create_report! + notify_staff! + forward_to_origin! if !@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward]) + + @report + end + + private + + def create_report! + @report = @source_account.reports.create!( + target_account: @target_account, + status_ids: @status_ids, + comment: @comment + ) + end + + def notify_staff! + User.staff.includes(:account).each do |u| + AdminMailer.new_report(u.account, @report).deliver_later + end + end + + def forward_to_origin! + ActivityPub::DeliveryWorker.perform_async( + payload, + some_local_account.id, + @target_account.inbox_url + ) + end + + def payload + Oj.dump(ActiveModelSerializers::SerializableResource.new( + @report, + serializer: ActivityPub::FlagSerializer, + adapter: ActivityPub::Adapter, + account: some_local_account + ).as_json) + end + + def some_local_account + @some_local_account ||= Account.local.where(suspended: false).first + end +end diff --git a/app/services/resolve_remote_account_service.rb b/app/services/resolve_account_service.rb index d7d0be210..fd6d30605 100644 --- a/app/services/resolve_remote_account_service.rb +++ b/app/services/resolve_account_service.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ResolveRemoteAccountService < BaseService +class ResolveAccountService < BaseService include OStatus2::MagicKey include JsonLdHelper diff --git a/app/services/fetch_remote_resource_service.rb b/app/services/resolve_url_service.rb index 6d40796f2..1f2b24524 100644 --- a/app/services/fetch_remote_resource_service.rb +++ b/app/services/resolve_url_service.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class FetchRemoteResourceService < BaseService +class ResolveURLService < BaseService include JsonLdHelper attr_reader :url diff --git a/app/services/search_service.rb b/app/services/search_service.rb index 85ad94463..00a8b3dd7 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -1,21 +1,45 @@ # frozen_string_literal: true class SearchService < BaseService - attr_accessor :query + attr_accessor :query, :account, :limit, :resolve def call(query, limit, resolve = false, account = nil) - @query = query + @query = query + @account = account + @limit = limit + @resolve = resolve default_results.tap do |results| if url_query? - results.merge!(remote_resource_results) unless remote_resource.nil? + results.merge!(url_resource_results) unless url_resource.nil? elsif query.present? - results[:accounts] = AccountSearchService.new.call(query, limit, account, resolve: resolve) - results[:hashtags] = Tag.search_for(query.gsub(/\A#/, ''), limit) unless query.start_with?('@') + results[:accounts] = perform_accounts_search! if account_searchable? + results[:statuses] = perform_statuses_search! if full_text_searchable? + results[:hashtags] = perform_hashtags_search! if hashtag_searchable? end end end + private + + def perform_accounts_search! + AccountSearchService.new.call(query, limit, account, resolve: resolve) + end + + def perform_statuses_search! + statuses = StatusesIndex.filter(term: { searchable_by: account.id }) + .query(multi_match: { type: 'most_fields', query: query, operator: 'and', fields: %w(text text.stemmed) }) + .limit(limit) + .objects + .compact + + statuses.reject { |status| StatusFilter.new(status, account).filtered? } + end + + def perform_hashtags_search! + Tag.search_for(query.gsub(/\A#/, ''), limit) + end + def default_results { accounts: [], hashtags: [], statuses: [] } end @@ -24,15 +48,28 @@ class SearchService < BaseService query =~ /\Ahttps?:\/\// end - def remote_resource_results - { remote_resource_symbol => [remote_resource] } + def url_resource_results + { url_resource_symbol => [url_resource] } + end + + def url_resource + @_url_resource ||= ResolveURLService.new.call(query) + end + + def url_resource_symbol + url_resource.class.name.downcase.pluralize.to_sym + end + + def full_text_searchable? + return false unless Chewy.enabled? + !account.nil? && !((query.start_with?('#') || query.include?('@')) && !query.include?(' ')) end - def remote_resource - @_remote_resource ||= FetchRemoteResourceService.new.call(query) + def account_searchable? + !(query.include?('@') && query.include?(' ')) end - def remote_resource_symbol - remote_resource.class.name.downcase.pluralize.to_sym + def hashtag_searchable? + !query.include?('@') end end diff --git a/app/validators/status_length_validator.rb b/app/validators/status_length_validator.rb index 79d17742a..2db9b29f6 100644 --- a/app/validators/status_length_validator.rb +++ b/app/validators/status_length_validator.rb @@ -23,6 +23,8 @@ class StatusLengthValidator < ActiveModel::Validator end def countable_text(status) + return '' if status.text.nil? + status.text.dup.tap do |new_text| new_text.gsub!(FetchLinkCardService::URL_PATTERN, 'x' * 23) new_text.gsub!(Account::MENTION_RE, '@\2') diff --git a/app/validators/unreserved_username_validator.rb b/app/validators/unreserved_username_validator.rb index 44ea4359b..c2311a89a 100644 --- a/app/validators/unreserved_username_validator.rb +++ b/app/validators/unreserved_username_validator.rb @@ -8,7 +8,13 @@ class UnreservedUsernameValidator < ActiveModel::Validator private + def pam_controlled?(value) + return false unless Devise.pam_authentication && Devise.pam_controlled_service + Rpam2.account(Devise.pam_controlled_service, value).present? + end + def reserved_username?(value) + return true if pam_controlled?(value) return false unless Setting.reserved_usernames Setting.reserved_usernames.include?(value.downcase) end diff --git a/app/views/about/_forms.html.haml b/app/views/about/_forms.html.haml new file mode 100644 index 000000000..81f7173f7 --- /dev/null +++ b/app/views/about/_forms.html.haml @@ -0,0 +1,15 @@ +- if @instance_presenter.open_registrations + = render 'registration' +- else + = link_to t('auth.register_elsewhere'), 'https://joinmastodon.org', class: 'button button-primary' + + .closed-registrations-message + - if @instance_presenter.closed_registrations_message.blank? + %p= t('about.closed_registrations') + - else + = @instance_presenter.closed_registrations_message.html_safe + +.separator-or + %span= t('auth.or') + += link_to t('auth.login'), new_user_session_path, class: 'button button-alternative-2 webapp-btn' diff --git a/app/views/about/_links.html.haml b/app/views/about/_links.html.haml new file mode 100644 index 000000000..f79c37e65 --- /dev/null +++ b/app/views/about/_links.html.haml @@ -0,0 +1,16 @@ +.container-alt.links + .brand + = link_to root_url do + = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' + + %ul.nav + %li + - if user_signed_in? + = link_to t('settings.back'), root_url, class: 'webapp-btn' + - else + = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' + %li= link_to t('about.about_this'), about_more_path + %li + = link_to 'https://joinmastodon.org/' do + = "#{t('about.other_instances')}" + %i.fa.fa-external-link{ style: 'padding-left: 5px;' } diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml index 7a28f9738..6ca1d7129 100644 --- a/app/views/about/_registration.html.haml +++ b/app/views/about/_registration.html.haml @@ -10,6 +10,6 @@ = 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' } .actions - = f.button :button, t('auth.register'), type: :submit, class: 'button button-alternative' + = f.button :button, t('auth.register'), type: :submit, class: 'button button-primary' %p.hint.subtle-hint=t('auth.agreement_html', rules_path: about_more_path, terms_path: terms_path) diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml index d92362bd7..f86051fbf 100644 --- a/app/views/about/more.html.haml +++ b/app/views/about/more.html.haml @@ -7,51 +7,36 @@ .landing-page .header-wrapper.compact .header - .container.links - .brand - = link_to root_url do - = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' + = render 'links' - %ul.nav - %li - - if user_signed_in? - = link_to t('settings.back'), root_url, class: 'webapp-btn' - - else - = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' - %li= link_to t('about.about_this'), about_more_path - %li - = link_to 'https://joinmastodon.org/' do - = "#{t('about.other_instances')}" - %i.fa.fa-external-link{ style: 'padding-left: 5px;' } - - .container.hero + .container-alt.hero .heading %h3= t('about.description_headline', domain: site_hostname) %p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname) .information-board - .container - .information-board-sections - .section + .container-alt + .information-board__sections + .information-board__section %span= t 'about.user_count_before' %strong= number_with_delimiter @instance_presenter.user_count %span= t 'about.user_count_after' - .section + .information-board__section %span= t 'about.status_count_before' %strong= number_with_delimiter @instance_presenter.status_count %span= t 'about.status_count_after' - .section + .information-board__section %span= t 'about.domain_count_before' %strong= number_with_delimiter @instance_presenter.domain_count %span= t 'about.domain_count_after' = render 'contact', contact: @instance_presenter .extended-description - .container + .container-alt = @instance_presenter.site_extended_description.html_safe.presence || t('about.extended_description_html') .footer-links - .container + .container-alt %p = link_to t('about.source_code'), @instance_presenter.source_url - if @instance_presenter.commit_hash == "" diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml index 4f5b53470..2f0b31a9f 100644 --- a/app/views/about/show.html.haml +++ b/app/views/about/show.html.haml @@ -2,80 +2,134 @@ = site_hostname - content_for :header_tags do + %link{ rel: 'canonical', href: about_url }/ %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) = render partial: 'shared/og' -.landing-page - .header-wrapper - .mascot-container - = image_tag asset_pack_path('elephant-fren.png'), alt: '', role: 'presentation', class: 'mascot' - - .header - .container.links +.landing-page.alternative + .container + .grid + .column-0 .brand = link_to root_url do = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' - %ul.nav - %li - - if user_signed_in? - = link_to t('settings.back'), root_url, class: 'webapp-btn' - - else - = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' - %li= link_to t('about.about_this'), about_more_path - %li - = link_to 'https://joinmastodon.org/' do - = "#{t('about.other_instances')}" - %i.fa.fa-external-link{ style: 'padding-left: 5px;' } - - .container.hero - .floats - %div{ role: 'presentation', class: 'float-1' } - %div{ role: 'presentation', class: 'float-2' } - %div{ role: 'presentation', class: 'float-3' } - .heading - %h1 - = @instance_presenter.site_title - %small= t 'about.hosted_on', domain: site_hostname - - if @instance_presenter.open_registrations - = render 'registration' - - else - .closed-registrations-message - %div - - if @instance_presenter.closed_registrations_message.blank? - %p= t('about.closed_registrations') - - else - = @instance_presenter.closed_registrations_message.html_safe - - = simple_form_for(:user, html: { style: 'margin-left: -20px' }, url: session_path(:user)) do |f| - = f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } - = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' } - - .actions - = f.button :button, t('auth.login'), type: :submit - = link_to t('about.find_another_instance'), 'https://joinmastodon.org/', class: 'button button-alternative button--block' - - .about-short - .container - %h3= t('about.description_headline', domain: site_hostname) - %p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname) - - .features - .container - if Setting.timeline_preview - #mastodon-timeline{ data: { props: Oj.dump(default_props) } } - - .about-mastodon - %h3= t 'about.what_is_mastodon' - %p= t 'about.about_mastodon_html' - = link_to t('about.learn_more'), 'https://joinmastodon.org/', class: 'button button-secondary' - = render 'features' - .footer-links - .container - %p - = link_to t('about.source_code'), @instance_presenter.source_url - - if @instance_presenter.commit_hash == "" - %strong= " (#{@instance_presenter.version_number})" - - else - %strong= " (#{@instance_presenter.version_number}, " - %strong= " #{@instance_presenter.commit_hash})" + .column-1 + .landing-page__forms + .brand + = link_to root_url do + = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' + + = render 'forms' + + - else + .column-1.non-preview + .landing-page__forms + .brand + = link_to root_url do + = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' + + = render 'forms' + + - if Setting.timeline_preview + .column-2 + .landing-page__hero + = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('preview.jpg'), alt: @instance_presenter.site_title + + .landing-page__information + .landing-page__short-description + .row + .landing-page__logo + = image_tag asset_pack_path('logo_transparent.svg'), alt: 'Mastodon' + + %h1 + = @instance_presenter.site_title + %small!= t 'about.hosted_on', domain: content_tag(:span, site_hostname) + + %p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname) + + .landing-page__call-to-action + .row + .row__information-board + .information-board__section + %span= t 'about.user_count_before' + %strong= number_with_delimiter @instance_presenter.user_count + %span= t 'about.user_count_after' + .information-board__section + %span= t 'about.status_count_before' + %strong= number_with_delimiter @instance_presenter.status_count + %span= t 'about.status_count_after' + .row__mascot + .landing-page__mascot + = image_tag asset_pack_path('elephant_ui_plane.svg') + + - else + .column-2.non-preview + .landing-page__hero + = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('preview.jpg'), alt: @instance_presenter.site_title + + .landing-page__information + .landing-page__short-description + .row + .landing-page__logo + = image_tag asset_pack_path('logo_transparent.svg'), alt: 'Mastodon' + + %h1 + = @instance_presenter.site_title + %small!= t 'about.hosted_on', domain: content_tag(:span, site_hostname) + + %p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname) + + .landing-page__call-to-action + .row + .row__information-board + .information-board__section + %span= t 'about.user_count_before' + %strong= number_with_delimiter @instance_presenter.user_count + %span= t 'about.user_count_after' + .information-board__section + %span= t 'about.status_count_before' + %strong= number_with_delimiter @instance_presenter.status_count + %span= t 'about.status_count_after' + .row__mascot + .landing-page__mascot + = image_tag asset_pack_path('elephant_ui_plane.svg') + + - if Setting.timeline_preview + .column-3 + #mastodon-timeline{ data: { props: Oj.dump(default_props) } } + + - if Setting.timeline_preview + .column-4.landing-page__information + .landing-page__features + .features-list + %div + %h3= t 'about.what_is_mastodon' + %p= t 'about.about_mastodon_html' + + = render 'features' + + .landing-page__features__action + = link_to t('about.learn_more'), 'https://joinmastodon.org/', class: 'button button-alternative' + + .landing-page__footer + %p + = link_to t('about.source_code'), @instance_presenter.source_url + = " (#{@instance_presenter.version_number})" + + - else + .column-4.non-preview.landing-page__information + .landing-page__features + %h3= t 'about.what_is_mastodon' + %p= t 'about.about_mastodon_html' + + = render 'features' + + .landing-page__features__action + = link_to t('about.learn_more'), 'https://joinmastodon.org/', class: 'button button-alternative' + + .landing-page__footer + %p + = link_to t('about.source_code'), @instance_presenter.source_url + = " (#{@instance_presenter.version_number})" diff --git a/app/views/about/terms.html.haml b/app/views/about/terms.html.haml index 7004cb0b1..c7d36ed47 100644 --- a/app/views/about/terms.html.haml +++ b/app/views/about/terms.html.haml @@ -4,20 +4,8 @@ .landing-page .header-wrapper.compact .header - .container.links - .brand - = link_to root_url do - = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' - - %ul.nav - %li - - if user_signed_in? - = link_to t('settings.back'), root_url, class: 'webapp-btn' - - else - = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' - %li= link_to t('about.about_this'), about_more_path - %li= link_to t('about.other_instances'), 'https://joinmastodon.org/' + = render 'links' .extended-description - .container + .container-alt = @instance_presenter.site_terms.html_safe.presence || t('terms.body_html') diff --git a/app/views/accounts/_follow_button.html.haml b/app/views/accounts/_follow_button.html.haml new file mode 100644 index 000000000..e476e0aff --- /dev/null +++ b/app/views/accounts/_follow_button.html.haml @@ -0,0 +1,23 @@ +- relationships ||= nil + +- unless account.memorial? || account.moved? + - if user_signed_in? + - requested = relationships ? relationships.requested[account.id].present? : current_account.requested?(account) + - following = relationships ? relationships.following[account.id].present? : current_account.following?(account) + + - if user_signed_in? && current_account.id != account.id && !requested + .controls + - if following + = link_to account_unfollow_path(account), data: { method: :post }, class: 'icon-button' do + = fa_icon 'user-times' + = t('accounts.unfollow') + - else + = link_to account_follow_path(account), data: { method: :post }, class: 'icon-button' do + = fa_icon 'user-plus' + = t('accounts.follow') + - elsif !user_signed_in? + .controls + .remote-follow + = link_to account_remote_follow_path(account), class: 'icon-button' do + = fa_icon 'user-plus' + = t('accounts.remote_follow') diff --git a/app/views/accounts/_grid_card.html.haml b/app/views/accounts/_grid_card.html.haml index 305eb2c44..95acbd581 100644 --- a/app/views/accounts/_grid_card.html.haml +++ b/app/views/accounts/_grid_card.html.haml @@ -1,9 +1,12 @@ .account-grid-card .account-grid-card__header{ style: "background-image: url(#{account.header.url(:original)})" } + = render 'accounts/follow_button', account: account, relationships: @relationships .account-grid-card__avatar .avatar= image_tag account.avatar.url(:original) .name = link_to TagManager.instance.url_for(account) do %span.display_name.emojify= display_name(account) - %span.username @#{account.acct} - %p.note.emojify= truncate(strip_tags(account.note), length: 150) + %span.username + @#{account.local? ? account.local_username_and_domain : account.acct} + = fa_icon('lock') if account.locked? + .account__header__content.p-note.emojify= Formatter.instance.simplified_format(account) diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml index b0062752c..74251b923 100644 --- a/app/views/accounts/_header.html.haml +++ b/app/views/accounts/_header.html.haml @@ -1,24 +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? || account.moved? - - if user_signed_in? && current_account.id != account.id && !current_account.requested?(account) - .controls - - if current_account.following?(account) - = link_to account_unfollow_path(account), data: { method: :post }, class: 'icon-button' do - = fa_icon 'user-times' - = t('accounts.unfollow') - - else - = link_to account_follow_path(account), data: { method: :post }, class: 'icon-button' do - = fa_icon 'user-plus' - = t('accounts.follow') - - elsif !user_signed_in? - .controls - .remote-follow - = link_to account_remote_follow_path(account), class: 'icon-button' do - = fa_icon 'user-plus' - = t('accounts.remote_follow') - + = render 'accounts/follow_button', account: account .avatar= image_tag account.avatar.url(:original), class: 'u-photo' .card__bio diff --git a/app/views/accounts/_og.html.haml b/app/views/accounts/_og.html.haml index 1d16be590..26424a49c 100644 --- a/app/views/accounts/_og.html.haml +++ b/app/views/accounts/_og.html.haml @@ -1,7 +1,7 @@ = opengraph 'og:url', url = opengraph 'og:site_name', site_title = opengraph 'og:title', [yield(:page_title).strip.presence, site_title].compact.join(' - ') -= opengraph 'og:description', account.note += opengraph 'og:description', account_description(account) = opengraph 'og:image', full_asset_url(account.avatar.url(:original)) = opengraph 'og:image:width', '120' = opengraph 'og:image:height', '120' diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index accad5f78..c62a573b0 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -1,7 +1,9 @@ - content_for :page_title do - = display_name(@account) + = "#{display_name(@account)} (@#{@account.username})" - content_for :header_tags do + %meta{ name: 'description', content: account_description(@account) }/ + - if @account.user&.setting_noindex %meta{ name: 'robots', content: 'noindex' }/ @@ -9,6 +11,11 @@ %link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/ %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/ + - if @older_url + %link{ rel: 'next', href: @older_url }/ + - if @newer_url + %link{ rel: 'prev', href: @newer_url }/ + = opengraph 'og:type', 'profile' = render 'og', account: @account, url: short_account_url(@account, only_path: false) @@ -39,6 +46,9 @@ = render partial: 'stream_entries/status', collection: @statuses, as: :status - - if @statuses.size == 20 + - if @newer_url || @older_url .pagination - = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), @next_url, class: 'next', rel: 'next' + - if @older_url + = link_to safe_join([fa_icon('chevron-left'), t('pagination.older')], ' '), @older_url, class: 'older', rel: 'next' + - if @newer_url + = link_to safe_join([t('pagination.newer'), fa_icon('chevron-right')], ' '), @newer_url, class: 'newer', rel: 'prev' diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index 5f5d0995c..dbbf5fc09 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -18,7 +18,10 @@ %tr %th= t('admin.accounts.role') %td - = t("admin.accounts.roles.#{@account.user&.role}") + - if @account.user.nil? + = t("admin.accounts.moderation.suspended") + - else + = t("admin.accounts.roles.#{@account.user&.role}") = table_link_to 'angle-double-up', t('admin.accounts.promote'), promote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:promote, @account.user) = table_link_to 'angle-double-down', t('admin.accounts.demote'), demote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:demote, @account.user) %tr diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index 4f9115ed2..08d05d738 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -12,6 +12,7 @@ .fields-group = f.input :thumbnail, as: :file, wrapper: :with_block_label, label: t('admin.settings.thumbnail.title'), hint: t('admin.settings.thumbnail.desc_html') + = f.input :hero, as: :file, wrapper: :with_block_label, label: t('admin.settings.hero.title'), hint: t('admin.settings.hero.desc_html') %hr/ @@ -19,6 +20,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_known_fediverse_at_about_page, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_known_fediverse_at_about_page.title'), hint: t('admin.settings.show_known_fediverse_at_about_page.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 diff --git a/app/views/auth/confirmations/finish_signup.html.haml b/app/views/auth/confirmations/finish_signup.html.haml new file mode 100644 index 000000000..4b5161d6b --- /dev/null +++ b/app/views/auth/confirmations/finish_signup.html.haml @@ -0,0 +1,14 @@ +- content_for :page_title do + = t('auth.confirm_email') + += simple_form_for(current_user, as: 'user', url: finish_signup_path, html: { role: 'form'}) do |f| + - if @show_errors && current_user.errors.any? + #error_explanation + - current_user.errors.full_messages.each do |msg| + = msg + %br + + = f.input :email + + .actions + = f.submit t('auth.confirm_email'), class: 'button' diff --git a/app/views/auth/passwords/edit.html.haml b/app/views/auth/passwords/edit.html.haml index 5ef3de976..53d1769d6 100644 --- a/app/views/auth/passwords/edit.html.haml +++ b/app/views/auth/passwords/edit.html.haml @@ -3,12 +3,16 @@ = simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| = render 'shared/error_messages', object: resource - = f.input :reset_password_token, as: :hidden - = f.input :password, autofocus: true, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' } - = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' } + - if !use_seamless_external_login? || resource.encrypted_password.present? + = f.input :reset_password_token, as: :hidden - .actions - = f.button :button, t('auth.set_new_password'), type: :submit + = f.input :password, autofocus: true, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' } + = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' } + + .actions + = f.button :button, t('auth.set_new_password'), type: :submit + - else + %p.hint= t('users.seamless_external_login') .form-footer= render 'auth/shared/links' diff --git a/app/views/auth/registrations/_sessions.html.haml b/app/views/auth/registrations/_sessions.html.haml index 8424a8901..8586c0549 100644 --- a/app/views/auth/registrations/_sessions.html.haml +++ b/app/views/auth/registrations/_sessions.html.haml @@ -1,4 +1,4 @@ -%h6= t 'sessions.title' +%h4= t 'sessions.title' %p.muted-hint= t 'sessions.explanation' .table-wrapper diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml index 145f5cd9e..05fc7df31 100644 --- a/app/views/auth/registrations/edit.html.haml +++ b/app/views/auth/registrations/edit.html.haml @@ -1,23 +1,24 @@ - content_for :page_title do - = t('auth.change_password') + = t('auth.security') +%h4= t('auth.change_password') = simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'auth_edit' }) do |f| = render 'shared/error_messages', object: resource - = f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } - = f.input :password, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' } - = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' } - = f.input :current_password, placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' } + - if !use_seamless_external_login? || resource.encrypted_password.present? + = f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } + = f.input :password, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' } + = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' } + = f.input :current_password, placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' } - .actions - = f.button :button, t('generic.save_changes'), type: :submit - -%hr/ + .actions + = f.button :button, t('generic.save_changes'), type: :submit + - else + %p.hint= t('users.seamless_external_login') = render 'sessions' - if open_deletion? - %hr/ - %h6= t('auth.delete_account') + %h4= t('auth.delete_account') %p.muted-hint= t('auth.delete_account_html', path: settings_delete_path) diff --git a/app/views/auth/sessions/new.html.haml b/app/views/auth/sessions/new.html.haml index a52b0053b..0c9f9d5fe 100644 --- a/app/views/auth/sessions/new.html.haml +++ b/app/views/auth/sessions/new.html.haml @@ -5,10 +5,22 @@ = render partial: 'shared/og' = simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| - = f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } + - if use_seamless_external_login? + = f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.username_or_email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username_or_email') } + - else + = f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' } .actions = f.button :button, t('auth.login'), type: :submit +- if devise_mapping.omniauthable? and resource_class.omniauth_providers.any? + .simple_form.alternative-login + %h4= t('auth.or_log_in_with') + + .actions + - resource_class.omniauth_providers.each do |provider| + = link_to omniauth_authorize_path(resource_name, provider), class: "button button-#{provider}" do + = t("auth.providers.#{provider}", default: provider.to_s.chomp("_oauth2").capitalize) + .form-footer= render 'auth/shared/links' diff --git a/app/views/authorize_follows/success.html.haml b/app/views/authorize_follows/success.html.haml index f0b495689..63ff3bcf1 100644 --- a/app/views/authorize_follows/success.html.haml +++ b/app/views/authorize_follows/success.html.haml @@ -12,5 +12,5 @@ .post-follow-actions %div= link_to t('authorize_follow.post_follow.web'), web_url("accounts/#{@account.id}"), class: 'button button--block' - %div= link_to t('authorize_follow.post_follow.return'), @account.url, class: 'button button--block' + %div= link_to t('authorize_follow.post_follow.return'), TagManager.instance.url_for(@account), class: 'button button--block' %div= t('authorize_follow.post_follow.close') diff --git a/app/views/follower_accounts/index.html.haml b/app/views/follower_accounts/index.html.haml index 738b31638..a24e4ea20 100644 --- a/app/views/follower_accounts/index.html.haml +++ b/app/views/follower_accounts/index.html.haml @@ -2,9 +2,7 @@ = t('accounts.people_who_follow', name: display_name(@account)) - content_for :header_tags do - - if @account.user&.setting_noindex - %meta{ name: 'robots', content: 'noindex' }/ - + %meta{ name: 'robots', content: 'noindex' }/ = render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false) = render 'accounts/header', account: @account diff --git a/app/views/following_accounts/index.html.haml b/app/views/following_accounts/index.html.haml index 9637c689f..67f6cfede 100644 --- a/app/views/following_accounts/index.html.haml +++ b/app/views/following_accounts/index.html.haml @@ -2,9 +2,7 @@ = t('accounts.people_followed_by', name: display_name(@account)) - content_for :header_tags do - - if @account.user&.setting_noindex - %meta{ name: 'robots', content: 'noindex' }/ - + %meta{ name: 'robots', content: 'noindex' }/ = render 'accounts/og', account: @account, url: account_followers_url(@account, only_path: false) = render 'accounts/header', account: @account diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 322d7403e..9ede598b3 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -2,7 +2,7 @@ %html{ lang: I18n.locale } %head %meta{ charset: 'utf-8' }/ - %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1' }/ + %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1' }/ %link{ rel: 'icon', href: favicon_path, type: 'image/x-icon' }/ %link{ rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' }/ %link{ rel: 'mask-icon', href: '/mask-icon.svg', color: '#2B90D9' }/ @@ -11,11 +11,7 @@ %meta{ name: 'theme-color', content: '#282c37' }/ %meta{ name: 'apple-mobile-web-app-capable', content: 'yes' }/ - %title< - - if content_for?(:page_title) - = yield(:page_title) - = ' - ' - = title + %title= content_for?(:page_title) ? safe_join([yield(:page_title).chomp.html_safe, title], ' - ') : title = javascript_pack_tag "locales", integrity: true, crossorigin: 'anonymous' - if @theme diff --git a/app/views/layouts/auth.html.haml b/app/views/layouts/auth.html.haml index f4812ac6a..ca9c13945 100644 --- a/app/views/layouts/auth.html.haml +++ b/app/views/layouts/auth.html.haml @@ -1,5 +1,5 @@ - content_for :content do - .container + .container-alt .logo-container %h1 = link_to root_path do diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml index ad15754d5..6321fec61 100644 --- a/app/views/layouts/mailer.html.haml +++ b/app/views/layouts/mailer.html.haml @@ -7,7 +7,7 @@ %title/ = stylesheet_pack_tag 'core/mailer' - %body + %body{ dir: locale_direction } %table.email-table{ cellspacing: 0, cellpadding: 0 } %tbody %tr diff --git a/app/views/layouts/modal.html.haml b/app/views/layouts/modal.html.haml index a5d79f5c0..e808593cd 100644 --- a/app/views/layouts/modal.html.haml +++ b/app/views/layouts/modal.html.haml @@ -8,7 +8,7 @@ = link_to destroy_user_session_path, method: :delete, class: 'logout-link icon-button' do = fa_icon 'sign-out' - .container= yield + .container-alt= yield .modal-layout__mastodon %div diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml index b3795eaad..07441a77d 100644 --- a/app/views/layouts/public.html.haml +++ b/app/views/layouts/public.html.haml @@ -1,5 +1,5 @@ - content_for :content do - .container= yield + .container-alt= yield .footer - if !user_signed_in? && single_user_mode? %span.single-user-login diff --git a/app/views/media/player.html.haml b/app/views/media/player.html.haml new file mode 100644 index 000000000..ea868b3f6 --- /dev/null +++ b/app/views/media/player.html.haml @@ -0,0 +1,2 @@ +%video{ poster: @media_attachment.file.url(:small), preload: 'auto', autoplay: 'autoplay', muted: 'muted', loop: 'loop', controls: 'controls', style: "width: #{@media_attachment.file.meta.dig('original', 'width')}px; height: #{@media_attachment.file.meta.dig('original', 'height')}px" } + %source{ src: @media_attachment.file.url(:original), type: @media_attachment.file_content_type } diff --git a/app/views/notification_mailer/_status.html.haml b/app/views/notification_mailer/_status.html.haml index 85f9294e9..f82ada146 100644 --- a/app/views/notification_mailer/_status.html.haml +++ b/app/views/notification_mailer/_status.html.haml @@ -1,6 +1,6 @@ - i ||= 0 -%table.email-table{ cellspacing: 0, cellpadding: 0 } +%table.email-table{ cellspacing: 0, cellpadding: 0, dir: 'ltr' } %tbody %tr %td.email-body @@ -19,12 +19,13 @@ %tbody %tr %td{ align: 'left', width: 48 } - = image_tag full_asset_url(status.account.avatar), alt:'' + = image_tag full_asset_url(status.account.avatar.url), alt:'' %td{ align: 'left' } %bdi= display_name(status.account) = "@#{status.account.acct}" - = Formatter.instance.format(status) + %div{ dir: rtl_status?(status) ? 'rtl' : 'ltr' } + = Formatter.instance.format(status) %p.status-footer = link_to l(status.created_at), web_url("statuses/#{status.id}") diff --git a/app/views/settings/exports/show.html.haml b/app/views/settings/exports/show.html.haml index e0df1c480..89d768d3f 100644 --- a/app/views/settings/exports/show.html.haml +++ b/app/views/settings/exports/show.html.haml @@ -20,3 +20,26 @@ %th= t('exports.mutes') %td= @export.total_mutes %td= table_link_to 'download', t('exports.csv'), settings_exports_mutes_path(format: :csv) + +%p.muted-hint= t('exports.archive_takeout.hint_html') + +- if policy(:backup).create? + %p= link_to t('exports.archive_takeout.request'), settings_export_path, class: 'button', method: :post + +- unless @backups.empty? + .table-wrapper + %table.table + %thead + %tr + %th= t('exports.archive_takeout.date') + %th= t('exports.archive_takeout.size') + %th + %tbody + - @backups.each do |backup| + %tr + %td= l backup.created_at + - if backup.processed? + %td= number_to_human_size backup.dump_file_size + %td= table_link_to 'download', t('exports.archive_takeout.download'), backup.dump.url + - else + %td{ colspan: 2 }= t('exports.archive_takeout.in_progress') diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index d1459d93c..102e4d200 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -4,6 +4,9 @@ = simple_form_for current_user, url: settings_preferences_path, html: { method: :put } do |f| = render 'shared/error_messages', object: current_user + .actions.actions--top + = f.button :button, t('generic.save_changes'), type: :submit + %h4= t 'preferences.languages' .fields-group @@ -33,6 +36,7 @@ .fields-group = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label + = f.input :setting_display_sensitive_media, as: :boolean, wrapper: :with_label = f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label = f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index d88ec8280..e1122d5a2 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -22,9 +22,9 @@ - if !status.media_attachments.empty? - if status.media_attachments.first.video? - video = status.media_attachments.first - %div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive?, width: 670, height: 380, detailed: true) }}< + %div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, width: 670, height: 380, detailed: true, inline: true) }} - else - %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 380, sensitive: status.sensitive?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}< + %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 380, sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }} - elsif status.preview_cards.first %div{ data: { component: 'Card', props: Oj.dump('maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_cards.first, serializer: REST::PreviewCardSerializer).as_json) }} diff --git a/app/views/stream_entries/_og_description.html.haml b/app/views/stream_entries/_og_description.html.haml index d2fa99e63..9c24e0a61 100644 --- a/app/views/stream_entries/_og_description.html.haml +++ b/app/views/stream_entries/_og_description.html.haml @@ -1,4 +1 @@ -- if activity.is_a?(Status) && activity.spoiler_text? - = opengraph 'og:description', activity.spoiler_text -- else - = opengraph 'og:description', activity.content += opengraph 'og:description', [activity.spoiler_text, activity.text].reject(&:blank?).join("\n\n") diff --git a/app/views/stream_entries/_og_image.html.haml b/app/views/stream_entries/_og_image.html.haml index 1056c1744..526034faa 100644 --- a/app/views/stream_entries/_og_image.html.haml +++ b/app/views/stream_entries/_og_image.html.haml @@ -1,23 +1,34 @@ -- if activity.is_a?(Status) && activity.non_sensitive_with_media? +- if activity.is_a?(Status) && activity.media_attachments.any? + - player_card = false - activity.media_attachments.each do |media| - if media.image? = opengraph 'og:image', full_asset_url(media.file.url(:original)) = opengraph 'og:image:type', media.file_content_type - unless media.file.meta.nil? - = opengraph 'og:image:width', media.file.meta['original']['width'] - = opengraph 'og:image:height', media.file.meta['original']['height'] - - elsif media.video? + = opengraph 'og:image:width', media.file.meta.dig('original', 'width') + = opengraph 'og:image:height', media.file.meta.dig('original', 'height') + - elsif media.video? || media.gifv? + - player_card = true = opengraph 'og:image', full_asset_url(media.file.url(:small)) = opengraph 'og:image:type', 'image/png' - unless media.file.meta.nil? - = opengraph 'og:image:width', media.file.meta['small']['width'] - = opengraph 'og:image:height', media.file.meta['small']['height'] + = opengraph 'og:image:width', media.file.meta.dig('small', 'width') + = opengraph 'og:image:height', media.file.meta.dig('small', 'height') = opengraph 'og:video', full_asset_url(media.file.url(:original)) + = opengraph 'og:video:secure_url', full_asset_url(media.file.url(:original)) = opengraph 'og:video:type', media.file_content_type + = opengraph 'twitter:player', medium_player_url(media) + = opengraph 'twitter:player:stream', full_asset_url(media.file.url(:original)) + = opengraph 'twitter:player:stream:content_type', media.file_content_type - unless media.file.meta.nil? - = opengraph 'og:video:width', media.file.meta['small']['width'] - = opengraph 'og:video:height', media.file.meta['small']['height'] - = opengraph 'twitter:card', 'summary_large_image' + = opengraph 'og:video:width', media.file.meta.dig('original', 'width') + = opengraph 'og:video:height', media.file.meta.dig('original', 'height') + = opengraph 'twitter:player:width', media.file.meta.dig('original', 'width') + = opengraph 'twitter:player:height', media.file.meta.dig('original', 'height') + - if player_card + = opengraph 'twitter:card', 'player' + - else + = opengraph 'twitter:card', 'summary_large_image' - else = opengraph 'og:image', full_asset_url(account.avatar.url(:original)) = opengraph 'og:image:width', '120' diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index 0b45ff308..2ad1f5120 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -20,10 +20,9 @@ %a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more') .e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }< = Formatter.instance.format(status, custom_emojify: true) - - unless status.media_attachments.empty? - if status.media_attachments.first.video? - video = status.media_attachments.first - %div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive?, width: 610, height: 343) }}>< + %div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, width: 610, height: 343) }} - else - %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 343, sensitive: status.sensitive?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}>< + %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 343, sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }} diff --git a/app/views/stream_entries/show.html.haml b/app/views/stream_entries/show.html.haml index cf6671e67..a87c51952 100644 --- a/app/views/stream_entries/show.html.haml +++ b/app/views/stream_entries/show.html.haml @@ -1,5 +1,5 @@ - content_for :page_title do - = t('statuses.title', name: display_name(@account), quote: truncate(@stream_entry.activity.spoiler_text.presence || @stream_entry.activity.text, length: 50, omission: '…')) + = t('statuses.title', name: display_name(@account), quote: truncate(@stream_entry.activity.spoiler_text.presence || @stream_entry.activity.text, length: 50, omission: '…', escape: false)) - content_for :header_tags do - if @account.user&.setting_noindex diff --git a/app/views/tags/_features.html.haml b/app/views/tags/_features.html.haml new file mode 100644 index 000000000..8fbc6b760 --- /dev/null +++ b/app/views/tags/_features.html.haml @@ -0,0 +1,25 @@ +.features-list + .features-list__row + .text + %h6= t 'about.features.real_conversation_title' + = t 'about.features.real_conversation_body' + .visual + = fa_icon 'fw comments' + .features-list__row + .text + %h6= t 'about.features.not_a_product_title' + = t 'about.features.not_a_product_body' + .visual + = fa_icon 'fw users' + .features-list__row + .text + %h6= t 'about.features.within_reach_title' + = t 'about.features.within_reach_body' + .visual + = fa_icon 'fw mobile' + .features-list__row + .text + %h6= t 'about.features.humane_approach_title' + = t 'about.features.humane_approach_body' + .visual + = fa_icon 'fw leaf' diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml index 03f19e20a..000aa0c4d 100644 --- a/app/views/tags/show.html.haml +++ b/app/views/tags/show.html.haml @@ -5,48 +5,31 @@ %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) = render 'og' -.landing-page.tag-page +.landing-page.tag-page.alternative .features .container - #mastodon-timeline{ data: { props: Oj.dump(default_props.merge(hashtag: @tag.name)) } } + .grid + .column-1 + #mastodon-timeline{ data: { props: Oj.dump(default_props.merge(hashtag: @tag.name)) } } - .about-mastodon - .about-hashtag - .brand - = link_to root_url do - = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' + .column-2 + .about-mastodon + .about-hashtag.landing-page__information + .brand + = link_to root_url do + = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' - %p= t 'about.about_hashtag_html', hashtag: @tag.name + %p= t 'about.about_hashtag_html', hashtag: @tag.name - .cta - - if user_signed_in? - = link_to t('settings.back'), root_path, class: 'button button-secondary' - - else - = link_to t('auth.login'), new_user_session_path, class: 'button button-secondary' - = link_to t('about.learn_more'), about_path, class: 'button button-alternative' + .cta + - if user_signed_in? + = link_to t('settings.back'), root_path, class: 'button button-secondary' + - else + = link_to t('auth.login'), new_user_session_path, class: 'button button-secondary' + = link_to t('about.learn_more'), about_path, class: 'button button-alternative' - .features-list - .features-list__row - .text - %h6= t 'about.features.real_conversation_title' - = t 'about.features.real_conversation_body' - .visual - = fa_icon 'fw comments' - .features-list__row - .text - %h6= t 'about.features.not_a_product_title' - = t 'about.features.not_a_product_body' - .visual - = fa_icon 'fw users' - .features-list__row - .text - %h6= t 'about.features.within_reach_title' - = t 'about.features.within_reach_body' - .visual - = fa_icon 'fw mobile' - .features-list__row - .text - %h6= t 'about.features.humane_approach_title' - = t 'about.features.humane_approach_body' - .visual - = fa_icon 'fw leaf' + .landing-page__features.landing-page__information + %h3= t 'about.what_is_mastodon' + %p= t 'about.about_mastodon_html' + + = render 'features' diff --git a/app/views/user_mailer/backup_ready.html.haml b/app/views/user_mailer/backup_ready.html.haml new file mode 100644 index 000000000..d5a4b8b48 --- /dev/null +++ b/app/views/user_mailer/backup_ready.html.haml @@ -0,0 +1,59 @@ +%table.email-table{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.email-body + .email-container + %table.content-section{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.content-cell.hero + .email-row + .col-6 + %table.column{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.column-cell.text-center.padded + %table.hero-icon{ align: 'center', cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td + = image_tag full_pack_url('icon_file_download.png'), alt: '' + + %h1= t 'user_mailer.backup_ready.title' + +%table.email-table{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.email-body + .email-container + %table.content-section{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.content-cell.content-start + .email-row + .col-6 + %table.column{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.column-cell.text-center + %p= t 'user_mailer.backup_ready.explanation' + +%table.email-table{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.email-body + .email-container + %table.content-section{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.content-cell + %table.column{ cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.column-cell.button-cell + %table.button{ align: 'center', cellspacing: 0, cellpadding: 0 } + %tbody + %tr + %td.button-primary + = link_to full_asset_url(@backup.dump.url) do + %span= t 'exports.archive_takeout.download' diff --git a/app/views/user_mailer/backup_ready.text.erb b/app/views/user_mailer/backup_ready.text.erb new file mode 100644 index 000000000..eb89e7d74 --- /dev/null +++ b/app/views/user_mailer/backup_ready.text.erb @@ -0,0 +1,7 @@ +<%= t 'user_mailer.backup_ready.title' %> + +=== + +<%= t 'user_mailer.backup_ready.explanation' %> + +=> <%= full_asset_url(@backup.dump.url) %> diff --git a/app/workers/activitypub/synchronize_featured_collection_worker.rb b/app/workers/activitypub/synchronize_featured_collection_worker.rb new file mode 100644 index 000000000..dd676a3ee --- /dev/null +++ b/app/workers/activitypub/synchronize_featured_collection_worker.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ActivityPub::SynchronizeFeaturedCollectionWorker + include Sidekiq::Worker + + sidekiq_options queue: 'pull' + + def perform(account_id) + ActivityPub::FetchFeaturedCollectionService.new.call(Account.find(account_id)) + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/app/workers/backup_worker.rb b/app/workers/backup_worker.rb new file mode 100644 index 000000000..ec6db4e9e --- /dev/null +++ b/app/workers/backup_worker.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class BackupWorker + include Sidekiq::Worker + + sidekiq_options queue: 'pull' + + def perform(backup_id) + backup = Backup.find(backup_id) + user = backup.user + + BackupService.new.call(backup) + + user.backups.where.not(id: backup.id).destroy_all + UserMailer.backup_ready(user, backup).deliver_later + end +end diff --git a/app/workers/import/relationship_worker.rb b/app/workers/import/relationship_worker.rb index ed4c962c1..1dd8bf8fb 100644 --- a/app/workers/import/relationship_worker.rb +++ b/app/workers/import/relationship_worker.rb @@ -7,7 +7,7 @@ class Import::RelationshipWorker def perform(account_id, target_account_uri, relationship) from_account = Account.find(account_id) - target_account = ResolveRemoteAccountService.new.call(target_account_uri) + target_account = ResolveAccountService.new.call(target_account_uri) return if target_account.nil? diff --git a/app/workers/resolve_remote_account_worker.rb b/app/workers/resolve_account_worker.rb index 5dd84ccb6..cd7c4d7dd 100644 --- a/app/workers/resolve_remote_account_worker.rb +++ b/app/workers/resolve_account_worker.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -class ResolveRemoteAccountWorker +class ResolveAccountWorker include Sidekiq::Worker sidekiq_options queue: 'pull', unique: :until_executed def perform(uri) - ResolveRemoteAccountService.new.call(uri) + ResolveAccountService.new.call(uri) end end diff --git a/app/workers/scheduler/backup_cleanup_scheduler.rb b/app/workers/scheduler/backup_cleanup_scheduler.rb new file mode 100644 index 000000000..7a9d4f894 --- /dev/null +++ b/app/workers/scheduler/backup_cleanup_scheduler.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +require 'sidekiq-scheduler' + +class Scheduler::BackupCleanupScheduler + include Sidekiq::Worker + + def perform + old_backups.find_each(&:destroy!) + end + + private + + def old_backups + Backup.where('created_at < ?', 7.days.ago) + end +end |