diff --git a/.circleci/config.yml b/.circleci/config.yml index 70d03f6b94..8791965f0e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,6 +11,8 @@ aliases: RAILS_ENV: test PARALLEL_TEST_PROCESSORS: 4 ALLOW_NOPAM: true + CONTINUOUS_INTEGRATION: true + DISABLE_SIMPLECOV: true working_directory: ~/projects/mastodon/ - &attach_workspace @@ -90,7 +92,7 @@ aliases: command: ./bin/rails parallel:create parallel:load_schema parallel:prepare - run: name: Run Tests - command: bundle exec parallel_test ./spec/ --group-by filesize --type rspec + command: ./bin/retry bundle exec parallel_test ./spec/ --group-by filesize --type rspec jobs: install: @@ -150,7 +152,7 @@ jobs: - image: circleci/node:8.11.1-stretch steps: - *attach_workspace - - run: yarn test:jest + - run: ./bin/retry yarn test:jest check-i18n: <<: *defaults diff --git a/.env.production.sample b/.env.production.sample index eddd38d906..d88af6007b 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -88,6 +88,10 @@ SMTP_FROM_ADDRESS=notifications@example.com # CDN_HOST=https://assets.example.com # S3 (optional) +# The attachment host must allow cross origin request from WEB_DOMAIN or +# LOCAL_DOMAIN if WEB_DOMAIN is not set. For example, the server may have the +# following header field: +# Access-Control-Allow-Origin: https://192.168.1.123:9000/ # S3_ENABLED=true # S3_BUCKET= # AWS_ACCESS_KEY_ID= @@ -97,6 +101,8 @@ SMTP_FROM_ADDRESS=notifications@example.com # S3_HOSTNAME=192.168.1.123:9000 # S3 (Minio Config (optional) Please check Minio instance for details) +# The attachment host must allow cross origin request - see the description +# above. # S3_ENABLED=true # S3_BUCKET= # AWS_ACCESS_KEY_ID= @@ -108,11 +114,15 @@ SMTP_FROM_ADDRESS=notifications@example.com # S3_SIGNATURE_VERSION= # Swift (optional) +# The attachment host must allow cross origin request - see the description +# above. # SWIFT_ENABLED=true # SWIFT_USERNAME= # For Keystone V3, the value for SWIFT_TENANT should be the project name # SWIFT_TENANT= # SWIFT_PASSWORD= +# Some OpenStack V3 providers require PROJECT_ID (optional) +# SWIFT_PROJECT_ID= # Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid # issues with token rate-limiting during high load. # SWIFT_AUTH_URL= diff --git a/.eslintrc.yml b/.eslintrc.yml index 33115853d7..da12088b4b 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -7,6 +7,9 @@ env: es6: true jest: true +globals: + ATTACHMENT_HOST: false + parser: babel-eslint plugins: diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE/bug_report.md similarity index 80% rename from .github/ISSUE_TEMPLATE.md rename to .github/ISSUE_TEMPLATE/bug_report.md index c78bcb492a..602530db0e 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,3 +1,9 @@ +--- +name: Bug Report +about: Create a report to help us improve + +--- + [Issue text goes here]. * * * * diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..46602fd2c0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,11 @@ +--- +name: Feature Request +about: Suggest an idea for this project + +--- + +[Issue text goes here]. + +* * * * + +- [ ] I searched or browsed the repo’s other issues to ensure this is not a duplicate. diff --git a/.travis.yml b/.travis.yml index 238b9a3f60..1529c81fc6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,7 @@ addons: rvm: - 2.4.3 - - 2.5.0 + - 2.5.1 services: - redis-server @@ -47,6 +47,10 @@ install: - bundle install --path=vendor/bundle --with pam_authentication --without development production --retry=3 --jobs=16 - yarn install +# https://github.com/travis-ci/travis-ci/issues/9333 +before_install: + - gem install bundler + before_script: - travis_wait ./bin/rails parallel:create parallel:load_schema parallel:prepare assets:precompile diff --git a/Gemfile b/Gemfile index 500628ad1d..ecf9d22308 100644 --- a/Gemfile +++ b/Gemfile @@ -19,7 +19,6 @@ gem 'fog-local', '~> 0.5', require: false gem 'fog-openstack', '~> 0.1', require: false gem 'paperclip', '~> 6.0' gem 'paperclip-av-transcoder', '~> 0.6' -gem 'posix-spawn', '~> 0.3' gem 'streamio-ffmpeg', '~> 3.0' gem 'active_model_serializers', '~> 0.10' @@ -42,7 +41,7 @@ gem 'omniauth-cas', '~> 1.1' gem 'omniauth-saml', '~> 1.10' gem 'omniauth', '~> 1.2' -gem 'doorkeeper', '~> 4.3' +gem 'doorkeeper', '~> 4.2', '< 4.3' gem 'fast_blank', '~> 1.0' gem 'fastimage' gem 'goldfinger', '~> 2.1' @@ -52,6 +51,7 @@ gem 'html2text' gem 'htmlentities', '~> 4.3' gem 'http', '~> 3.2' gem 'http_accept_language', '~> 2.1' +gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2' gem 'httplog', '~> 1.0' gem 'idn-ruby', require: 'idn' gem 'kaminari', '~> 1.1' @@ -62,6 +62,7 @@ gem 'nsa', '~> 0.2' gem 'oj', '~> 3.5' gem 'ostatus2', '~> 2.0' gem 'ox', '~> 2.9' +gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c' gem 'pundit', '~> 1.1' gem 'premailer-rails' gem 'rack-attack', '~> 5.2' @@ -113,7 +114,6 @@ group :test do gem 'microformats', '~> 4.0' gem 'rails-controller-testing', '~> 1.0' gem 'rspec-sidekiq', '~> 3.0' - gem 'rspec-retry', '~> 0.5', require: false gem 'simplecov', '~> 0.16', require: false gem 'webmock', '~> 3.3' gem 'parallel_tests', '~> 2.21' diff --git a/Gemfile.lock b/Gemfile.lock index 36554c1ed1..5c1cee0a59 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,17 @@ +GIT + remote: https://github.com/rtomayko/posix-spawn + revision: 58465d2e213991f8afb13b984854a49fcdcc980c + ref: 58465d2e213991f8afb13b984854a49fcdcc980c + specs: + posix-spawn (0.3.13) + +GIT + remote: https://github.com/tmm1/http_parser.rb + revision: 54b17ba8c7d8d20a16dfc65d1775241833219cf2 + ref: 54b17ba8c7d8d20a16dfc65d1775241833219cf2 + specs: + http_parser.rb (0.6.1) + GEM remote: https://rubygems.org/ specs: @@ -167,7 +181,7 @@ GEM docile (1.3.0) domain_name (0.5.20180417) unf (>= 0.0.5, < 1.0.0) - doorkeeper (4.3.2) + doorkeeper (4.2.6) railties (>= 4.2) dotenv (2.2.2) dotenv-rails (2.2.2) @@ -254,7 +268,6 @@ GEM domain_name (~> 0.5) http-form_data (2.1.0) http_accept_language (2.1.1) - http_parser.rb (0.6.0) httplog (1.0.2) colorize (~> 0.8) rack (>= 1.0) @@ -383,7 +396,6 @@ GEM pghero (2.1.0) activerecord pkg-config (1.3.0) - posix-spawn (0.3.13) powerpack (0.1.1) premailer (1.11.1) addressable @@ -503,8 +515,6 @@ GEM rspec-expectations (~> 3.7.0) rspec-mocks (~> 3.7.0) rspec-support (~> 3.7.0) - rspec-retry (0.5.7) - rspec-core (> 3.3) rspec-sidekiq (3.0.3) rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) @@ -663,7 +673,7 @@ DEPENDENCIES devise (~> 4.4) devise-two-factor (~> 3.0) devise_pam_authenticatable2 (~> 9.1) - doorkeeper (~> 4.3) + doorkeeper (~> 4.2, < 4.3) dotenv-rails (~> 2.2, < 2.3) fabrication (~> 2.20) faker (~> 1.8) @@ -680,6 +690,7 @@ DEPENDENCIES htmlentities (~> 4.3) http (~> 3.2) http_accept_language (~> 2.1) + http_parser.rb (~> 0.6)! httplog (~> 1.0) i18n-tasks (~> 0.9) idn-ruby @@ -709,7 +720,7 @@ DEPENDENCIES pg (~> 1.0) pghero (~> 2.1) pkg-config (~> 1.3) - posix-spawn (~> 0.3) + posix-spawn! premailer-rails private_address_check (~> 0.4.1) pry-byebug (~> 3.6) @@ -729,7 +740,6 @@ DEPENDENCIES redis-rails (~> 5.0) rqrcode (~> 0.10) rspec-rails (~> 3.7) - rspec-retry (~> 0.5) rspec-sidekiq (~> 3.0) rubocop (~> 0.55) ruby-progressbar (~> 1.4) diff --git a/app/controllers/admin/confirmations_controller.rb b/app/controllers/admin/confirmations_controller.rb index 34dfb458ec..8d3477e660 100644 --- a/app/controllers/admin/confirmations_controller.rb +++ b/app/controllers/admin/confirmations_controller.rb @@ -3,6 +3,7 @@ module Admin class ConfirmationsController < BaseController before_action :set_user + before_action :check_confirmation, only: [:resend] def create authorize @user, :confirm? @@ -11,10 +12,28 @@ module Admin redirect_to admin_accounts_path end + def resend + authorize @user, :confirm? + + @user.resend_confirmation_instructions + + log_action :confirm, @user + + flash[:notice] = I18n.t('admin.accounts.resend_confirmation.success') + redirect_to admin_accounts_path + end + private def set_user @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) end + + def check_confirmation + if @user.confirmed? + flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed') + redirect_to admin_accounts_path + end + end end end diff --git a/app/controllers/admin/reported_statuses_controller.rb b/app/controllers/admin/reported_statuses_controller.rb index 522f68c98e..d3c2f5e9e9 100644 --- a/app/controllers/admin/reported_statuses_controller.rb +++ b/app/controllers/admin/reported_statuses_controller.rb @@ -3,7 +3,6 @@ module Admin class ReportedStatusesController < BaseController before_action :set_report - before_action :set_status, only: [:update, :destroy] def create authorize :status, :update? @@ -14,20 +13,6 @@ module Admin redirect_to admin_report_path(@report) end - def update - authorize @status, :update? - @status.update!(status_params) - log_action :update, @status - redirect_to admin_report_path(@report) - end - - def destroy - authorize @status, :destroy? - RemovalWorker.perform_async(@status.id) - log_action :destroy, @status - render json: @status - end - private def status_params @@ -51,9 +36,5 @@ module Admin def set_report @report = Report.find(params[:report_id]) end - - def set_status - @status = @report.statuses.find(params[:id]) - end end end diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb index d5787acfb9..382bfc4a23 100644 --- a/app/controllers/admin/statuses_controller.rb +++ b/app/controllers/admin/statuses_controller.rb @@ -5,7 +5,6 @@ module Admin helper_method :current_params before_action :set_account - before_action :set_status, only: [:update, :destroy] PER_PAGE = 20 @@ -26,40 +25,18 @@ module Admin def create authorize :status, :update? - @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account)) + @form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button)) flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save redirect_to admin_account_statuses_path(@account.id, current_params) end - def update - authorize @status, :update? - @status.update!(status_params) - log_action :update, @status - redirect_to admin_account_statuses_path(@account.id, current_params) - end - - def destroy - authorize @status, :destroy? - RemovalWorker.perform_async(@status.id) - log_action :destroy, @status - render json: @status - end - private - def status_params - params.require(:status).permit(:sensitive) - end - def form_status_batch_params params.require(:form_status_batch).permit(:action, status_ids: []) end - def set_status - @status = @account.statuses.find(params[:id]) - end - def set_account @account = Account.find(params[:account_id]) end @@ -72,5 +49,15 @@ module Admin page: page > 1 && page, }.select { |_, value| value.present? } end + + def action_from_button + if params[:nsfw_on] + 'nsfw_on' + elsif params[:nsfw_off] + 'nsfw_off' + elsif params[:delete] + 'delete' + end + end end end diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index a3c4008e64..259d07be87 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -21,7 +21,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController private def account_params - params.permit(:display_name, :note, :avatar, :header, :locked, fields_attributes: [:name, :value]) + params.permit(:display_name, :note, :avatar, :header, :locked, :bot, fields_attributes: [:name, :value]) end def user_settings_params diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index c4f600c54a..4578cf6ca6 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -19,6 +19,8 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController end def load_accounts + return [] if @account.user_hides_network? && current_account.id != @account.id + default_accounts.merge(paginated_follows).to_a end diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 90b1f7fc51..ce2bbda855 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -19,6 +19,8 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController end def load_accounts + return [] if @account.user_hides_network? && current_account.id != @account.id + default_accounts.merge(paginated_follows).to_a end diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index cbcc7ef046..c40155cb56 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -27,19 +27,17 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def account_statuses - default_statuses.tap do |statuses| - 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 - - def default_statuses - permitted_account_statuses.paginate_by_max_id( + statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses + statuses = statuses.paginate_by_max_id( limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id] ) + + statuses.merge!(only_media_scope) if truthy_param?(:only_media) + statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies) + + statuses end def permitted_account_statuses diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb new file mode 100644 index 0000000000..1a19bd0ef6 --- /dev/null +++ b/app/controllers/api/v1/push/subscriptions_controller.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class Api::V1::Push::SubscriptionsController < Api::BaseController + before_action -> { doorkeeper_authorize! :push } + before_action :require_user! + before_action :set_web_push_subscription + + def create + @web_subscription&.destroy! + + @web_subscription = ::Web::PushSubscription.create!( + endpoint: subscription_params[:endpoint], + key_p256dh: subscription_params[:keys][:p256dh], + key_auth: subscription_params[:keys][:auth], + data: data_params, + user_id: current_user.id, + access_token_id: doorkeeper_token.id + ) + + render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer + end + + def show + raise ActiveRecord::RecordNotFound if @web_subscription.nil? + + render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer + end + + def update + raise ActiveRecord::RecordNotFound if @web_subscription.nil? + + @web_subscription.update!(data: data_params) + + render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer + end + + def destroy + @web_subscription&.destroy! + render_empty + end + + private + + def set_web_push_subscription + @web_subscription = ::Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id) + end + + def subscription_params + params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh]) + end + + def data_params + return {} if params[:data].blank? + params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention]) + end +end diff --git a/app/controllers/api/v1/statuses/pins_controller.rb b/app/controllers/api/v1/statuses/pins_controller.rb index bba6a6f480..54f8be667d 100644 --- a/app/controllers/api/v1/statuses/pins_controller.rb +++ b/app/controllers/api/v1/statuses/pins_controller.rb @@ -39,7 +39,7 @@ class Api::V1::Statuses::PinsController < Api::BaseController adapter: ActivityPub::Adapter ).as_json - ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account) + ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account.id) end def distribute_remove_activity! @@ -49,6 +49,6 @@ class Api::V1::Statuses::PinsController < Api::BaseController adapter: ActivityPub::Adapter ).as_json - ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account) + ActivityPub::RawDistributionWorker.perform_async(Oj.dump(json), current_account.id) end end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 01880565c8..289d910454 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -10,6 +10,12 @@ class Api::V1::StatusesController < Api::BaseController respond_to :json + # This API was originally unlimited, pagination cannot be introduced without + # breaking backwards-compatibility. Arbitrarily high number to cover most + # conversations as quasi-unlimited, it would be too much work to render more + # than this anyway + CONTEXT_LIMIT = 4_096 + def show cached = Rails.cache.read(@status.cache_key) @status = cached unless cached.nil? @@ -17,8 +23,8 @@ class Api::V1::StatusesController < Api::BaseController end def context - ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(DEFAULT_STATUSES_LIMIT, current_account) - descendants_results = @status.descendants(DEFAULT_STATUSES_LIMIT, current_account) + ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(CONTEXT_LIMIT, current_account) + descendants_results = @status.descendants(CONTEXT_LIMIT, current_account) loaded_ancestors = cache_collection(ancestors_results, Status) loaded_descendants = cache_collection(descendants_results, Status) diff --git a/app/controllers/api/v1/timelines/direct_controller.rb b/app/controllers/api/v1/timelines/direct_controller.rb index d455227eb5..ef64078be8 100644 --- a/app/controllers/api/v1/timelines/direct_controller.rb +++ b/app/controllers/api/v1/timelines/direct_controller.rb @@ -23,15 +23,18 @@ class Api::V1::Timelines::DirectController < Api::BaseController end def direct_statuses - direct_timeline_statuses.paginate_by_max_id( - limit_param(DEFAULT_STATUSES_LIMIT), - params[:max_id], - params[:since_id] - ) + direct_timeline_statuses end def direct_timeline_statuses - Status.as_direct_timeline(current_account) + # this query requires built in pagination. + Status.as_direct_timeline( + current_account, + limit_param(DEFAULT_STATUSES_LIMIT), + params[:max_id], + params[:since_id], + true # returns array of cache_ids object + ) end def insert_pagination_headers diff --git a/app/controllers/api/v1/trends_controller.rb b/app/controllers/api/v1/trends_controller.rb new file mode 100644 index 0000000000..bcea9857e8 --- /dev/null +++ b/app/controllers/api/v1/trends_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Api::V1::TrendsController < Api::BaseController + before_action :set_tags + + respond_to :json + + def index + render json: @tags, each_serializer: REST::TagSerializer + end + + private + + def set_tags + @tags = TrendingTags.get(limit_param(10)) + end +end diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb new file mode 100644 index 0000000000..2e91d68ee3 --- /dev/null +++ b/app/controllers/api/v2/search_controller.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class Api::V2::SearchController < Api::V1::SearchController + def index + @search = Search.new(search) + render json: @search, serializer: REST::V2::SearchSerializer + end +end diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb index 249e7c1860..fe8e425808 100644 --- a/app/controllers/api/web/push_subscriptions_controller.rb +++ b/app/controllers/api/web/push_subscriptions_controller.rb @@ -31,22 +31,23 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController endpoint: subscription_params[:endpoint], key_p256dh: subscription_params[:keys][:p256dh], key_auth: subscription_params[:keys][:auth], - data: data + data: data, + user_id: active_session.user_id, + access_token_id: active_session.access_token_id ) active_session.update!(web_push_subscription: web_subscription) - render json: web_subscription.as_payload + render json: web_subscription, serializer: REST::WebPushSubscriptionSerializer end def update params.require([:id]) web_subscription = ::Web::PushSubscription.find(params[:id]) - web_subscription.update!(data: data_params) - render json: web_subscription.as_payload + render json: web_subscription, serializer: REST::WebPushSubscriptionSerializer end private @@ -56,6 +57,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController end def data_params - @data_params ||= params.require(:data).permit(:alerts) + @data_params ||= params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention]) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 158c0c10e9..cc92894a5c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base include Localized include UserTrackingConcern + include SessionTrackingConcern helper_method :current_account helper_method :current_session @@ -20,6 +21,7 @@ class ApplicationController < ActionController::Base rescue_from ActionController::RoutingError, with: :not_found rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity + rescue_from ActionController::UnknownFormat, with: :not_acceptable rescue_from Mastodon::NotPermittedError, with: :forbidden before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? @@ -142,6 +144,10 @@ class ApplicationController < ActionController::Base respond_with_error(422) end + def not_acceptable + respond_with_error(406) + end + def single_user_mode? @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.exists? end diff --git a/app/controllers/concerns/session_tracking_concern.rb b/app/controllers/concerns/session_tracking_concern.rb new file mode 100644 index 0000000000..45361b0190 --- /dev/null +++ b/app/controllers/concerns/session_tracking_concern.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module SessionTrackingConcern + extend ActiveSupport::Concern + + UPDATE_SIGN_IN_HOURS = 24 + + included do + before_action :set_session_activity + end + + private + + def set_session_activity + return unless session_needs_update? + current_session.touch + end + + def session_needs_update? + !current_session.nil? && current_session.updated_at < UPDATE_SIGN_IN_HOURS.hours.ago + end +end diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index f289228d3c..41aa1c8a64 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -107,9 +107,7 @@ module SignatureVerification def incompatible_signature?(signature_params) signature_params['keyId'].blank? || - signature_params['signature'].blank? || - signature_params['algorithm'].blank? || - signature_params['algorithm'] != 'rsa-sha256' + signature_params['signature'].blank? end def account_from_key_id(key_id) diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index c74d3f86d8..f5670c6bf8 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -4,16 +4,19 @@ class FollowerAccountsController < ApplicationController include AccountControllerConcern def index - @follows = Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account) - 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? + next if @account.user_hides_network? + + follows + @relationships = AccountRelationshipsPresenter.new(follows.map(&:account_id), current_user.account_id) if user_signed_in? end format.json do + raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? + render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, @@ -24,28 +27,31 @@ class FollowerAccountsController < ApplicationController private + def follows + @follows ||= Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account) + end + def page_url(page) account_followers_url(@account, page: page) unless page.nil? end def collection_presenter - page = ActivityPub::CollectionPresenter.new( - id: account_followers_url(@account, page: params.fetch(:page, 1)), - type: :ordered, - size: @account.followers_count, - items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) }, - part_of: account_followers_url(@account), - next: page_url(@follows.next_page), - prev: page_url(@follows.prev_page) - ) if params[:page].present? - page + ActivityPub::CollectionPresenter.new( + id: account_followers_url(@account, page: params.fetch(:page, 1)), + type: :ordered, + size: @account.followers_count, + items: follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) }, + part_of: account_followers_url(@account), + next: page_url(follows.next_page), + prev: page_url(follows.prev_page) + ) else ActivityPub::CollectionPresenter.new( id: account_followers_url(@account), type: :ordered, size: @account.followers_count, - first: page + first: page_url(1) ) end end diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 4c1e3f327b..098b2a20cb 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -4,16 +4,19 @@ class FollowingAccountsController < ApplicationController include AccountControllerConcern def index - @follows = Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account) - 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? + next if @account.user_hides_network? + + follows + @relationships = AccountRelationshipsPresenter.new(follows.map(&:target_account_id), current_user.account_id) if user_signed_in? end format.json do + raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? + render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, @@ -24,28 +27,31 @@ class FollowingAccountsController < ApplicationController private + def follows + @follows ||= Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account) + end + def page_url(page) account_following_index_url(@account, page: page) unless page.nil? end def collection_presenter - page = ActivityPub::CollectionPresenter.new( - id: account_following_index_url(@account, page: params.fetch(:page, 1)), - type: :ordered, - size: @account.following_count, - items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.target_account) }, - part_of: account_following_index_url(@account), - next: page_url(@follows.next_page), - prev: page_url(@follows.prev_page) - ) if params[:page].present? - page + ActivityPub::CollectionPresenter.new( + id: account_following_index_url(@account, page: params.fetch(:page, 1)), + type: :ordered, + size: @account.following_count, + items: follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.target_account) }, + part_of: account_following_index_url(@account), + next: page_url(follows.next_page), + prev: page_url(follows.prev_page) + ) else ActivityPub::CollectionPresenter.new( id: account_following_index_url(@account), type: :ordered, size: @account.following_count, - first: page + first: page_url(1) ) end end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 189e4072e9..70818610c2 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -11,7 +11,7 @@ class InvitesController < ApplicationController def index authorize :invite, :create? - @invites = Invite.where(user: current_user) + @invites = invites @invite = Invite.new(expires_in: 1.day.to_i) end @@ -24,13 +24,13 @@ class InvitesController < ApplicationController if @invite.save redirect_to invites_path else - @invites = Invite.where(user: current_user) + @invites = invites render :index end end def destroy - @invite = Invite.where(user: current_user).find(params[:id]) + @invite = invites.find(params[:id]) authorize @invite, :destroy? @invite.expire! redirect_to invites_path @@ -42,6 +42,10 @@ class InvitesController < ApplicationController use_pack 'settings' end + def invites + Invite.where(user: current_user) + end + def resource_params params.require(:invite).permit(:max_uses, :expires_in) end diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 155670837e..d820b257e0 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -8,6 +8,8 @@ class MediaProxyController < ApplicationController if lock.acquired? @media_attachment = MediaAttachment.remote.find(params[:id]) redownload! if @media_attachment.needs_redownload? && !reject_media? + else + raise Mastodon::RaceConditionError end end diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index f95d672ecd..1e420b3e7f 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -9,6 +9,11 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio include Localized + def destroy + Web::PushSubscription.unsubscribe_for(params[:id], current_resource_owner) + super + end + private def store_current_location diff --git a/app/controllers/oauth/tokens_controller.rb b/app/controllers/oauth/tokens_controller.rb new file mode 100644 index 0000000000..fa6d58f258 --- /dev/null +++ b/app/controllers/oauth/tokens_controller.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class Oauth::TokensController < Doorkeeper::TokensController + def revoke + unsubscribe_for_token if authorized? && token.accessible? + super + end + + private + + def unsubscribe_for_token + Web::PushSubscription.where(access_token_id: token.id).delete_all + end +end diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb index 35a6f7f9e7..03c890a506 100644 --- a/app/controllers/settings/applications_controller.rb +++ b/app/controllers/settings/applications_controller.rb @@ -6,7 +6,7 @@ class Settings::ApplicationsController < Settings::BaseController before_action :prepare_scopes, only: [:create, :update] def index - @applications = current_user.applications.page(params[:page]) + @applications = current_user.applications.order(id: :desc).page(params[:page]) end def new diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index c853b5ab7a..425664d496 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -40,6 +40,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_reduce_motion, :setting_system_font_ui, :setting_noindex, + :setting_hide_network, notification_emails: %i(follow follow_request reblog favourite mention digest), interactions: %i(must_be_follower must_be_following) ) diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index 2b8330f2e3..918dbc6c6b 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -17,6 +17,7 @@ class Settings::ProfilesController < Settings::BaseController ActivityPub::UpdateDistributionWorker.perform_async(@account.id) redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg') else + @account.build_fields render :show end end @@ -24,7 +25,7 @@ class Settings::ProfilesController < Settings::BaseController private def account_params - params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, fields_attributes: [:name, :value]) + params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, fields_attributes: [:name, :value]) end def set_account diff --git a/app/controllers/shares_controller.rb b/app/controllers/shares_controller.rb index 3cbaccb352..4624c29a65 100644 --- a/app/controllers/shares_controller.rb +++ b/app/controllers/shares_controller.rb @@ -16,6 +16,7 @@ class SharesController < ApplicationController def initial_state_params text = [params[:title], params[:text], params[:url]].compact.join(' ') + { settings: Web::Setting.find_by(user: current_user)&.data || {}, push_subscription: current_account.user.web_push_subscription(current_session), diff --git a/app/helpers/admin/account_moderation_notes_helper.rb b/app/helpers/admin/account_moderation_notes_helper.rb index fdfadef080..49e764cefd 100644 --- a/app/helpers/admin/account_moderation_notes_helper.rb +++ b/app/helpers/admin/account_moderation_notes_helper.rb @@ -10,10 +10,16 @@ module Admin::AccountModerationNotesHelper end end + def admin_account_inline_link_to(account) + link_to admin_account_path(account.id), class: name_tag_classes(account, true) do + content_tag(:span, account.acct, class: 'username') + end + end + private - def name_tag_classes(account) - classes = ['name-tag'] + def name_tag_classes(account, inline = false) + classes = [inline ? 'inline-name-tag' : 'name-tag'] classes << 'suspended' if account.suspended? classes.join(' ') end diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index e9056166c1..9d2b6cf00d 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -52,18 +52,22 @@ module JsonLdHelper graph.dump(:normalize) end - def fetch_resource(uri, id) + def fetch_resource(uri, id, on_behalf_of = nil) unless id - json = fetch_resource_without_id_validation(uri) + json = fetch_resource_without_id_validation(uri, on_behalf_of) return unless json uri = json['id'] end - json = fetch_resource_without_id_validation(uri) + json = fetch_resource_without_id_validation(uri, on_behalf_of) json.present? && json['id'] == uri ? json : nil end - def fetch_resource_without_id_validation(uri) + def fetch_resource_without_id_validation(uri, on_behalf_of = nil) + build_request(uri, on_behalf_of).perform do |response| + return body_to_json(response.body_with_limit) if response.code == 200 + end + # If request failed, retry without doing it on behalf of a user build_request(uri).perform do |response| response.code == 200 ? body_to_json(response.body_with_limit) : nil end @@ -85,8 +89,9 @@ module JsonLdHelper private - def build_request(uri) + def build_request(uri, on_behalf_of = nil) request = Request.new(:get, uri) + request.on_behalf_of(on_behalf_of) if on_behalf_of request.add_headers('Accept' => 'application/activity+json, application/ld+json') request end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index f78e5fbc3f..ba728eb32c 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -6,6 +6,7 @@ module SettingsHelper ar: 'العربية', bg: 'Български', ca: 'Català', + co: 'Corsu', de: 'Deutsch', el: 'Ελληνικά', eo: 'Esperanto', @@ -32,6 +33,7 @@ module SettingsHelper 'pt-BR': 'Português do Brasil', ru: 'Русский', sk: 'Slovensky', + sl: 'Slovenščina', sr: 'Српски', 'sr-Latn': 'Srpski (latinica)', sv: 'Svenska', diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb index c6f12ecd41..a91a289355 100644 --- a/app/helpers/stream_entries_helper.rb +++ b/app/helpers/stream_entries_helper.rb @@ -4,8 +4,12 @@ module StreamEntriesHelper EMBEDDED_CONTROLLER = 'statuses' EMBEDDED_ACTION = 'embed' - def display_name(account) - account.display_name.presence || account.username + def display_name(account, **options) + if options[:custom_emojify] + Formatter.instance.format_display_name(account, options) + else + account.display_name.presence || account.username + end end def account_description(account) diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 8a6ca3699d..b7f706a833 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -3,14 +3,9 @@ import { CancelToken } from 'axios'; import { throttle } from 'lodash'; import { search as emojiSearch } from 'flavours/glitch/util/emoji/emoji_mart_search_light'; import { useEmoji } from './emojis'; +import resizeImage from 'flavours/glitch/util/resize_image'; -import { - updateTimeline, - refreshHomeTimeline, - refreshCommunityTimeline, - refreshPublicTimeline, - refreshDirectTimeline, -} from './timelines'; +import { updateTimeline } from './timelines'; let cancelFetchComposeSuggestionsAccounts; @@ -21,6 +16,7 @@ export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS'; export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL'; export const COMPOSE_REPLY = 'COMPOSE_REPLY'; export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL'; +export const COMPOSE_DIRECT = 'COMPOSE_DIRECT'; export const COMPOSE_MENTION = 'COMPOSE_MENTION'; export const COMPOSE_RESET = 'COMPOSE_RESET'; export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST'; @@ -102,6 +98,19 @@ export function mentionCompose(account, router) { }; }; +export function directCompose(account, router) { + return (dispatch, getState) => { + dispatch({ + type: COMPOSE_DIRECT, + account: account, + }); + + if (!getState().getIn(['compose', 'mounted'])) { + router.push('/statuses/new'); + } + }; +}; + export function submitCompose() { return function (dispatch, getState) { let status = getState().getIn(['compose', 'text'], ''); @@ -136,21 +145,19 @@ export function submitCompose() { // To make the app more responsive, immediately get the status into the columns - const insertOrRefresh = (timelineId, refreshAction) => { - if (getState().getIn(['timelines', timelineId, 'online'])) { + const insertIfOnline = (timelineId) => { + if (getState().getIn(['timelines', timelineId, 'items', 0]) !== null) { dispatch(updateTimeline(timelineId, { ...response.data })); - } else if (getState().getIn(['timelines', timelineId, 'loaded'])) { - dispatch(refreshAction()); } }; - insertOrRefresh('home', refreshHomeTimeline); + insertIfOnline('home'); if (response.data.in_reply_to_id === null && response.data.visibility === 'public') { - insertOrRefresh('community', refreshCommunityTimeline); - insertOrRefresh('public', refreshPublicTimeline); + insertIfOnline('community'); + insertIfOnline('public'); } else if (response.data.visibility === 'direct') { - insertOrRefresh('direct', refreshDirectTimeline); + insertIfOnline('direct'); } }).catch(function (error) { dispatch(submitComposeFail(error)); @@ -193,18 +200,14 @@ export function uploadCompose(files) { dispatch(uploadComposeRequest()); - let data = new FormData(); - data.append('file', files[0]); + resizeImage(files[0]).then(file => { + const data = new FormData(); + data.append('file', file); - api(getState).post('/api/v1/media', data, { - onUploadProgress: function (e) { - dispatch(uploadComposeProgress(e.loaded, e.total)); - }, - }).then(function (response) { - dispatch(uploadComposeSuccess(response.data)); - }).catch(function (error) { - dispatch(uploadComposeFail(error)); - }); + return api(getState).post('/api/v1/media', data, { + onUploadProgress: ({ loaded, total }) => dispatch(uploadComposeProgress(loaded, total)), + }).then(({ data }) => dispatch(uploadComposeSuccess(data))); + }).catch(error => dispatch(uploadComposeFail(error))); }; }; diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index cf27eff90e..68c46a7329 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -1,8 +1,8 @@ import api, { getLinks } from 'flavours/glitch/util/api'; -import { List as ImmutableList } from 'immutable'; import IntlMessageFormat from 'intl-messageformat'; import { fetchRelationships } from './accounts'; import { defineMessages } from 'react-intl'; +import { unescapeHTML } from 'flavours/glitch/util/html'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; @@ -17,10 +17,6 @@ export const NOTIFICATIONS_UNMARK_ALL_FOR_DELETE = 'NOTIFICATIONS_UNMARK_ALL_FOR // Mark one for delete export const NOTIFICATION_MARK_FOR_DELETE = 'NOTIFICATION_MARK_FOR_DELETE'; -export const NOTIFICATIONS_REFRESH_REQUEST = 'NOTIFICATIONS_REFRESH_REQUEST'; -export const NOTIFICATIONS_REFRESH_SUCCESS = 'NOTIFICATIONS_REFRESH_SUCCESS'; -export const NOTIFICATIONS_REFRESH_FAIL = 'NOTIFICATIONS_REFRESH_FAIL'; - export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST'; export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS'; export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; @@ -40,13 +36,6 @@ const fetchRelatedRelationships = (dispatch, notifications) => { } }; -const unescapeHTML = (html) => { - const wrapper = document.createElement('div'); - html = html.replace(/||\n/g, ' '); - wrapper.innerHTML = html; - return wrapper.textContent; -}; - export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true); @@ -78,84 +67,37 @@ export function updateNotifications(notification, intlMessages, intlLocale) { const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); -export function refreshNotifications() { + +const noOp = () => {}; + +export function expandNotifications({ maxId } = {}, done = noOp) { return (dispatch, getState) => { - const params = {}; - const ids = getState().getIn(['notifications', 'items']); + const notifications = getState().get('notifications'); - let skipLoading = false; - - if (ids.size > 0) { - params.since_id = ids.first().get('id'); - } - - if (getState().getIn(['notifications', 'loaded'])) { - skipLoading = true; - } - - params.exclude_types = excludeTypesFromSettings(getState()); - - dispatch(refreshNotificationsRequest(skipLoading)); - - api(getState).get('/api/v1/notifications', { params }).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(refreshNotificationsSuccess(response.data, skipLoading, next ? next.uri : null)); - fetchRelatedRelationships(dispatch, response.data); - }).catch(error => { - dispatch(refreshNotificationsFail(error, skipLoading)); - }); - }; -}; - -export function refreshNotificationsRequest(skipLoading) { - return { - type: NOTIFICATIONS_REFRESH_REQUEST, - skipLoading, - }; -}; - -export function refreshNotificationsSuccess(notifications, skipLoading, next) { - return { - type: NOTIFICATIONS_REFRESH_SUCCESS, - notifications, - accounts: notifications.map(item => item.account), - statuses: notifications.map(item => item.status).filter(status => !!status), - skipLoading, - next, - }; -}; - -export function refreshNotificationsFail(error, skipLoading) { - return { - type: NOTIFICATIONS_REFRESH_FAIL, - error, - skipLoading, - }; -}; - -export function expandNotifications() { - return (dispatch, getState) => { - const items = getState().getIn(['notifications', 'items'], ImmutableList()); - - if (getState().getIn(['notifications', 'isLoading']) || items.size === 0) { + if (notifications.get('isLoading')) { + done(); return; } const params = { - max_id: items.last().get('id'), - limit: 20, + max_id: maxId, exclude_types: excludeTypesFromSettings(getState()), }; + if (!maxId && notifications.get('items').size > 0) { + params.since_id = notifications.getIn(['items', 0]); + } + dispatch(expandNotificationsRequest()); api(getState).get('/api/v1/notifications', { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null)); fetchRelatedRelationships(dispatch, response.data); + done(); }).catch(error => { dispatch(expandNotificationsFail(error)); + done(); }); }; }; diff --git a/app/javascript/flavours/glitch/actions/push_notifications/registerer.js b/app/javascript/flavours/glitch/actions/push_notifications/registerer.js index 5ad11f73ff..91f442415a 100644 --- a/app/javascript/flavours/glitch/actions/push_notifications/registerer.js +++ b/app/javascript/flavours/glitch/actions/push_notifications/registerer.js @@ -56,13 +56,6 @@ export function register () { dispatch(setBrowserSupport(supportsPushNotifications)); const me = getState().getIn(['meta', 'me']); - if (me && !pushNotificationsSetting.get(me)) { - const alerts = getState().getIn(['push_notifications', 'alerts']); - if (alerts) { - pushNotificationsSetting.set(me, { alerts: alerts }); - } - } - if (supportsPushNotifications) { if (!getApplicationServerKey()) { console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.'); diff --git a/app/javascript/flavours/glitch/actions/search.js b/app/javascript/flavours/glitch/actions/search.js index e86bd848e5..13885c6004 100644 --- a/app/javascript/flavours/glitch/actions/search.js +++ b/app/javascript/flavours/glitch/actions/search.js @@ -1,4 +1,5 @@ import api from 'flavours/glitch/util/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/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index ae51e83490..6e34d0be64 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -2,11 +2,10 @@ import { connectStream } from 'flavours/glitch/util/stream'; import { updateTimeline, deleteFromTimelines, - refreshHomeTimeline, - connectTimeline, + expandHomeTimeline, disconnectTimeline, } from './timelines'; -import { updateNotifications, refreshNotifications } from './notifications'; +import { updateNotifications, expandNotifications } from './notifications'; import { getLocale } from 'mastodon/locales'; const { messages } = getLocale(); @@ -16,10 +15,6 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null) return connectStream (path, pollingRefresh, (dispatch, getState) => { const locale = getState().getIn(['meta', 'locale']); return { - onConnect() { - dispatch(connectTimeline(timelineId)); - }, - onDisconnect() { dispatch(disconnectTimeline(timelineId)); }, @@ -41,10 +36,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null) }); } -function refreshHomeTimelineAndNotification (dispatch) { - dispatch(refreshHomeTimeline()); - dispatch(refreshNotifications()); -} +const refreshHomeTimelineAndNotification = (dispatch, done) => { + dispatch(expandHomeTimeline({}, () => dispatch(expandNotifications({}, done)))); +}; export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); export const connectCommunityStream = () => connectTimelineStream('community', 'public:local'); diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index d99c6d98bc..9597fe89dd 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -4,32 +4,16 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE'; -export const TIMELINE_REFRESH_REQUEST = 'TIMELINE_REFRESH_REQUEST'; -export const TIMELINE_REFRESH_SUCCESS = 'TIMELINE_REFRESH_SUCCESS'; -export const TIMELINE_REFRESH_FAIL = 'TIMELINE_REFRESH_FAIL'; - export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS'; export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; -export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; export const TIMELINE_CONTEXT_UPDATE = 'CONTEXT_UPDATE'; -export function refreshTimelineSuccess(timeline, statuses, skipLoading, next, partial) { - return { - type: TIMELINE_REFRESH_SUCCESS, - timeline, - statuses, - skipLoading, - next, - partial, - }; -}; - export function updateTimeline(timeline, status) { return (dispatch, getState) => { const references = status.reblog ? getState().get('statuses').filter((item, itemId) => (itemId === status.reblog.id || item.get('reblog') === status.reblog.id)).map((_, itemId) => itemId) : []; @@ -77,97 +61,43 @@ export function deleteFromTimelines(id) { }; }; -export function refreshTimelineRequest(timeline, skipLoading) { - return { - type: TIMELINE_REFRESH_REQUEST, - timeline, - skipLoading, - }; -}; +const noOp = () => {}; -export function refreshTimeline(timelineId, path, params = {}) { - return function (dispatch, getState) { - const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); - - if (timeline.get('isLoading') || (timeline.get('online') && !timeline.get('isPartial'))) { - return; - } - - const ids = timeline.get('items', ImmutableList()); - const newestId = ids.size > 0 ? ids.first() : null; - - let skipLoading = timeline.get('loaded'); - - if (newestId !== null) { - params.since_id = newestId; - } - - dispatch(refreshTimelineRequest(timelineId, skipLoading)); - - api(getState).get(path, { params }).then(response => { - if (response.status === 206) { - dispatch(refreshTimelineSuccess(timelineId, [], skipLoading, null, true)); - } else { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(refreshTimelineSuccess(timelineId, response.data, skipLoading, next ? next.uri : null, false)); - } - }).catch(error => { - dispatch(refreshTimelineFail(timelineId, error, skipLoading)); - }); - }; -}; - -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 refreshDirectTimeline = () => refreshTimeline('direct', '/api/v1/timelines/direct'); -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 { - type: TIMELINE_REFRESH_FAIL, - timeline, - error, - skipLoading, - skipAlert: error.response && error.response.status === 404, - }; -}; - -export function expandTimeline(timelineId, path, params = {}) { +export function expandTimeline(timelineId, path, params = {}, done = noOp) { return (dispatch, getState) => { const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); - const ids = timeline.get('items', ImmutableList()); - if (timeline.get('isLoading') || ids.size === 0) { + if (timeline.get('isLoading')) { + done(); return; } - params.max_id = ids.last(); - params.limit = 10; + if (!params.max_id && timeline.get('items', ImmutableList()).size > 0) { + params.since_id = timeline.getIn(['items', 0]); + } dispatch(expandTimelineRequest(timelineId)); api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null)); + dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206)); + done(); }).catch(error => { dispatch(expandTimelineFail(timelineId, error)); + done(); }); }; }; -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 expandDirectTimeline = () => expandTimeline('direct', '/api/v1/timelines/direct'); -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}`); +export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); +export const expandPublicTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('public', '/api/v1/timelines/public', { max_id: maxId }, done); +export const expandCommunityTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('community', '/api/v1/timelines/public', { local: true, max_id: maxId }, done); +export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); +export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); +export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); +export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true }); +export const expandHashtagTimeline = (hashtag, { maxId } = {}, done = noOp) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId }, done); +export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); export function expandTimelineRequest(timeline) { return { @@ -176,12 +106,13 @@ export function expandTimelineRequest(timeline) { }; }; -export function expandTimelineSuccess(timeline, statuses, next) { +export function expandTimelineSuccess(timeline, statuses, next, partial) { return { type: TIMELINE_EXPAND_SUCCESS, timeline, statuses, next, + partial, }; }; @@ -201,13 +132,6 @@ export function scrollTopTimeline(timeline, top) { }; }; -export function connectTimeline(timeline) { - return { - type: TIMELINE_CONNECT, - timeline, - }; -}; - export function disconnectTimeline(timeline) { return { type: TIMELINE_DISCONNECT, diff --git a/app/javascript/flavours/glitch/components/load_gap.js b/app/javascript/flavours/glitch/components/load_gap.js new file mode 100644 index 0000000000..012303ae1e --- /dev/null +++ b/app/javascript/flavours/glitch/components/load_gap.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { injectIntl, defineMessages } from 'react-intl'; + +const messages = defineMessages({ + load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, +}); + +@injectIntl +export default class LoadGap extends React.PureComponent { + + static propTypes = { + disabled: PropTypes.bool, + maxId: PropTypes.string, + onClick: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + handleClick = () => { + this.props.onClick(this.props.maxId); + } + + render () { + const { disabled, intl } = this.props; + + return ( + + + + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/load_more.js b/app/javascript/flavours/glitch/components/load_more.js index c4c8c94a2a..389c3e1e11 100644 --- a/app/javascript/flavours/glitch/components/load_more.js +++ b/app/javascript/flavours/glitch/components/load_more.js @@ -6,6 +6,7 @@ export default class LoadMore extends React.PureComponent { static propTypes = { onClick: PropTypes.func, + disabled: PropTypes.bool, visible: PropTypes.bool, } @@ -14,10 +15,10 @@ export default class LoadMore extends React.PureComponent { } render() { - const { visible } = this.props; + const { disabled, visible } = this.props; return ( - + ); diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js index 7f5150f7bc..d90f9bdb41 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.js +++ b/app/javascript/flavours/glitch/components/media_gallery.js @@ -40,6 +40,7 @@ class Item extends React.PureComponent { size: PropTypes.number.isRequired, letterbox: PropTypes.bool, onClick: PropTypes.func.isRequired, + displayWidth: PropTypes.number, }; static defaultProps = { @@ -78,7 +79,7 @@ class Item extends React.PureComponent { } render () { - const { attachment, index, size, standalone, letterbox } = this.props; + const { attachment, index, size, standalone, letterbox, displayWidth } = this.props; let width = 50; let height = 100; @@ -141,7 +142,7 @@ class Item extends React.PureComponent { 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 ? `${displayWidth * (width / 100)}px` : null; const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0; const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0; @@ -235,7 +236,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, @@ -272,9 +273,9 @@ export default class MediaGallery extends React.PureComponent { ); } else { if (this.isStandaloneEligible()) { - children = ; + children = ; } else { - children = media.take(4).map((attachment, i) => ); + children = media.take(4).map((attachment, i) => ); } } diff --git a/app/javascript/flavours/glitch/components/modal_root.js b/app/javascript/flavours/glitch/components/modal_root.js index 789e117c70..89f81f58ef 100644 --- a/app/javascript/flavours/glitch/components/modal_root.js +++ b/app/javascript/flavours/glitch/components/modal_root.js @@ -6,6 +6,7 @@ export default class ModalRoot extends React.PureComponent { static propTypes = { children: PropTypes.node, onClose: PropTypes.func.isRequired, + noEsc: PropTypes.bool, }; state = { @@ -16,7 +17,7 @@ export default class ModalRoot extends React.PureComponent { handleKeyUp = (e) => { if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) - && !!this.props.children && !this.props.props.noEsc) { + && !!this.props.children && !this.props.noEsc) { this.props.onClose(); } } diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js index df3ace4c19..b96b4dd980 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.js +++ b/app/javascript/flavours/glitch/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, onScrollToTop: PropTypes.func, onScroll: PropTypes.func, trackScroll: PropTypes.bool, @@ -44,9 +44,11 @@ export default class ScrollableList extends PureComponent { const { scrollTop, scrollHeight, clientHeight } = this.node; const offset = scrollHeight - scrollTop - clientHeight; - 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(); @@ -144,15 +146,15 @@ export default class ScrollableList extends PureComponent { handleLoadMore = (e) => { e.preventDefault(); - this.props.onScrollToBottom(); + this.props.onLoadMore(); } render () { - const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props; + const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage, onLoadMore } = this.props; const { fullscreen } = this.state; const childrenCount = React.Children.count(children); - const loadMore = (hasMore && childrenCount > 0) ? : null; + const loadMore = (hasMore && childrenCount > 0 && onLoadMore) ? : null; let scrollableArea = null; if (isLoading || childrenCount > 0 || !emptyMessage) { diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index bff396f04f..c937052661 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -32,6 +32,8 @@ export default class Status extends ImmutablePureComponent { onFavourite: PropTypes.func, onReblog: PropTypes.func, onDelete: PropTypes.func, + onDirect: PropTypes.func, + onMention: PropTypes.func, onPin: PropTypes.func, onOpenMedia: PropTypes.func, onOpenVideo: PropTypes.func, @@ -257,8 +259,8 @@ export default class Status extends ImmutablePureComponent { } }; - handleOpenVideo = startTime => { - this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime); + handleOpenVideo = (media, startTime) => { + this.props.onOpenVideo(media, startTime); } handleHotkeyReply = e => { diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index da6e4e6baf..6ae4bc08d6 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -10,6 +10,7 @@ import RelativeTimestamp from './relative_timestamp'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, + direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' }, mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, block: { id: 'account.block', defaultMessage: 'Block @{name}' }, @@ -44,6 +45,7 @@ export default class StatusActionBar extends ImmutablePureComponent { onFavourite: PropTypes.func, onReblog: PropTypes.func, onDelete: PropTypes.func, + onDirect: PropTypes.func, onMention: PropTypes.func, onMute: PropTypes.func, onBlock: PropTypes.func, @@ -98,6 +100,10 @@ export default class StatusActionBar extends ImmutablePureComponent { this.props.onMention(this.props.status.get('account'), this.context.router.history); } + handleDirectClick = () => { + this.props.onDirect(this.props.status.get('account'), this.context.router.history); + } + handleMuteClick = () => { this.props.onMute(this.props.status.get('account')); } @@ -157,6 +163,7 @@ export default class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); } else { menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); + menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); menu.push(null); menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index 32b0770cba..6542df65b2 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -98,7 +98,7 @@ export default class StatusContent extends React.PureComponent { const [ startX, startY ] = this.startXY; const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)]; - if (e.target.localName === 'button' || e.target.localName === 'a' || (e.target.parentNode && (e.target.parentNode.localName === 'button' || e.target.parentNode.localName === 'a'))) { + if (e.target.localName === 'button' || e.target.localName == 'video' || e.target.localName === 'a' || (e.target.parentNode && (e.target.parentNode.localName === 'button' || e.target.parentNode.localName === 'a'))) { return; } @@ -188,11 +188,9 @@ export default class StatusContent extends React.PureComponent { } return ( - + {' '} @@ -208,8 +206,6 @@ export default class StatusContent extends React.PureComponent { ref={this.setRef} style={directionStyle} tabIndex={!hidden ? 0 : null} - onMouseDown={this.handleMouseDown} - onMouseUp={this.handleMouseUp} dangerouslySetInnerHTML={content} /> {media} @@ -222,12 +218,12 @@ export default class StatusContent extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/components/status_list.js b/app/javascript/flavours/glitch/components/status_list.js index 2b35d6f3d5..33bc7a9592 100644 --- a/app/javascript/flavours/glitch/components/status_list.js +++ b/app/javascript/flavours/glitch/components/status_list.js @@ -1,8 +1,10 @@ +import { debounce } from 'lodash'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import StatusContainer from 'flavours/glitch/containers/status_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import LoadGap from './load_gap'; import ScrollableList from './scrollable_list'; import { FormattedMessage } from 'react-intl'; @@ -12,7 +14,7 @@ export default class StatusList extends ImmutablePureComponent { scrollKey: PropTypes.string.isRequired, statusIds: ImmutablePropTypes.list.isRequired, featuredStatusIds: ImmutablePropTypes.list, - onScrollToBottom: PropTypes.func, + onLoadMore: PropTypes.func, onScrollToTop: PropTypes.func, onScroll: PropTypes.func, trackScroll: PropTypes.bool, @@ -50,6 +52,10 @@ export default class StatusList extends ImmutablePureComponent { this._selectChild(elementIndex); } + handleLoadOlder = debounce(() => { + this.props.onLoadMore(this.props.statusIds.last()); + }, 300, { leading: true }) + _selectChild (index) { const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`); @@ -63,7 +69,7 @@ export default class StatusList extends ImmutablePureComponent { } render () { - const { statusIds, featuredStatusIds, ...other } = this.props; + const { statusIds, featuredStatusIds, onLoadMore, ...other } = this.props; const { isLoading, isPartial } = other; if (isPartial) { @@ -82,7 +88,14 @@ export default class StatusList extends ImmutablePureComponent { } let scrollableContent = (isLoading || statusIds.size > 0) ? ( - statusIds.map(statusId => ( + statusIds.map((statusId, index) => statusId === null ? ( + 0 ? statusIds.get(index - 1) : null} + onClick={onLoadMore} + /> + ) : ( + {scrollableContent} ); diff --git a/app/javascript/flavours/glitch/containers/card_container.js b/app/javascript/flavours/glitch/containers/card_container.js deleted file mode 100644 index dec7df5223..0000000000 --- a/app/javascript/flavours/glitch/containers/card_container.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Card from 'flavours/glitch/features/status/components/card'; -import { fromJS } from 'immutable'; - -export default class CardContainer extends React.PureComponent { - - static propTypes = { - locale: PropTypes.string, - card: PropTypes.array.isRequired, - }; - - render () { - const { card, ...props } = this.props; - return ; - } - -} diff --git a/app/javascript/flavours/glitch/containers/media_container.js b/app/javascript/flavours/glitch/containers/media_container.js new file mode 100644 index 0000000000..0e1904132e --- /dev/null +++ b/app/javascript/flavours/glitch/containers/media_container.js @@ -0,0 +1,90 @@ +import React, { PureComponent, Fragment } from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import { IntlProvider, addLocaleData } from 'react-intl'; +import { getLocale } from 'mastodon/locales'; +import MediaGallery from 'flavours/glitch/components/media_gallery'; +import Video from 'flavours/glitch/features/video'; +import Card from 'flavours/glitch/features/status/components/card'; +import ModalRoot from 'flavours/glitch/components/modal_root'; +import MediaModal from 'flavours/glitch/features/ui/components/media_modal'; +import { List as ImmutableList, fromJS } from 'immutable'; + +const { localeData, messages } = getLocale(); +addLocaleData(localeData); + +const MEDIA_COMPONENTS = { MediaGallery, Video, Card }; + +export default class MediaContainer extends PureComponent { + + static propTypes = { + locale: PropTypes.string.isRequired, + components: PropTypes.object.isRequired, + }; + + state = { + media: null, + index: null, + time: null, + }; + + handleOpenMedia = (media, index) => { + document.body.classList.add('media-standalone__body'); + this.setState({ media, index }); + } + + handleOpenVideo = (video, time) => { + const media = ImmutableList([video]); + + document.body.classList.add('media-standalone__body'); + this.setState({ media, time }); + } + + handleCloseMedia = () => { + document.body.classList.remove('media-standalone__body'); + this.setState({ media: null, index: null, time: null }); + } + + render () { + const { locale, components } = this.props; + + return ( + + + {[].map.call(components, (component, i) => { + const componentName = component.getAttribute('data-component'); + const Component = MEDIA_COMPONENTS[componentName]; + const { media, card, ...props } = JSON.parse(component.getAttribute('data-props')); + + Object.assign(props, { + ...(media ? { media: fromJS(media) } : {}), + ...(card ? { card: fromJS(card) } : {}), + + ...(componentName === 'Video' ? { + onOpenVideo: this.handleOpenVideo, + } : { + onOpenMedia: this.handleOpenMedia, + }), + }); + + return ReactDOM.createPortal( + , + component, + ); + })} + + {this.state.media && ( + + )} + + + + ); + } + +} diff --git a/app/javascript/flavours/glitch/containers/media_galleries_container.js b/app/javascript/flavours/glitch/containers/media_galleries_container.js deleted file mode 100644 index a694578821..0000000000 --- a/app/javascript/flavours/glitch/containers/media_galleries_container.js +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; -import { IntlProvider, addLocaleData } from 'react-intl'; -import { getLocale } from 'mastodon/locales'; -import MediaGallery from 'flavours/glitch/components/media_gallery'; -import ModalRoot from 'flavours/glitch/components/modal_root'; -import MediaModal from 'flavours/glitch/features/ui/components/media_modal'; -import { fromJS } from 'immutable'; - -const { localeData, messages } = getLocale(); -addLocaleData(localeData); - -export default class MediaGalleriesContainer extends React.PureComponent { - - static propTypes = { - locale: PropTypes.string.isRequired, - galleries: PropTypes.object.isRequired, - }; - - state = { - media: null, - index: null, - }; - - handleOpenMedia = (media, index) => { - document.body.classList.add('media-gallery-standalone__body'); - this.setState({ media, index }); - } - - handleCloseMedia = () => { - document.body.classList.remove('media-gallery-standalone__body'); - this.setState({ media: null, index: null }); - } - - render () { - const { locale, galleries } = this.props; - - return ( - - - {[].map.call(galleries, gallery => { - const { media, ...props } = JSON.parse(gallery.getAttribute('data-props')); - - return ReactDOM.createPortal( - , - gallery - ); - })} - - {this.state.media === null || this.state.index === null ? null : ( - - )} - - - - ); - } - -} diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 3fc6a6a795..acec00e124 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -5,6 +5,7 @@ import { makeGetStatus } from 'flavours/glitch/selectors'; import { replyCompose, mentionCompose, + directCompose, } from 'flavours/glitch/actions/compose'; import { reblog, @@ -131,6 +132,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, + onDirect (account, router) { + dispatch(directCompose(account, router)); + }, + onMention (account, router) { dispatch(mentionCompose(account, router)); }, diff --git a/app/javascript/flavours/glitch/containers/timeline_container.js b/app/javascript/flavours/glitch/containers/timeline_container.js index c5ffe1b633..56669a49aa 100644 --- a/app/javascript/flavours/glitch/containers/timeline_container.js +++ b/app/javascript/flavours/glitch/containers/timeline_container.js @@ -6,6 +6,7 @@ import { hydrateStore } from 'flavours/glitch/actions/store'; import { IntlProvider, addLocaleData } from 'react-intl'; import { getLocale } from 'mastodon/locales'; import PublicTimeline from 'flavours/glitch/features/standalone/public_timeline'; +import CommunityTimeline from 'flavours/glitch/features/standalone/community_timeline'; import HashtagTimeline from 'flavours/glitch/features/standalone/hashtag_timeline'; import initialState from 'flavours/glitch/util/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 = ; - } else { + } else if (showPublicTimeline) { timeline = ; + } else { + timeline = ; } return ( diff --git a/app/javascript/flavours/glitch/containers/video_container.js b/app/javascript/flavours/glitch/containers/video_container.js deleted file mode 100644 index b206e9a100..0000000000 --- a/app/javascript/flavours/glitch/containers/video_container.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { IntlProvider, addLocaleData } from 'react-intl'; -import { getLocale } from 'mastodon/locales'; -import Video from 'flavours/glitch/features/video'; - -const { localeData, messages } = getLocale(); -addLocaleData(localeData); - -export default class VideoContainer extends React.PureComponent { - - static propTypes = { - locale: PropTypes.string.isRequired, - }; - - render () { - const { locale, ...props } = this.props; - - return ( - - - - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/account/components/action_bar.js b/app/javascript/flavours/glitch/features/account/components/action_bar.js index fb90722f3d..8b95c08f27 100644 --- a/app/javascript/flavours/glitch/features/account/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/account/components/action_bar.js @@ -8,6 +8,7 @@ import { me } from 'flavours/glitch/util/initial_state'; const messages = defineMessages({ mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, + direct: { id: 'account.direct', defaultMessage: 'Direct message @{name}' }, edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, @@ -32,6 +33,7 @@ export default class ActionBar extends React.PureComponent { onFollow: PropTypes.func, onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, + onDirect: PropTypes.func.isRequired, onReblogToggle: PropTypes.func.isRequired, onReport: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired, @@ -53,6 +55,7 @@ export default class ActionBar extends React.PureComponent { let extraInfo = ''; menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); + menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect }); if ('share' in navigator) { menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index 7a0a2dfa96..464c73c9a2 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -38,6 +38,8 @@ export default class Header extends ImmutablePureComponent { let displayName = account.get('display_name_html'); let fields = account.get('fields'); + let badge = account.get('bot') ? () : null; + let info = ''; let mutingInfo = ''; let actionBtn = ''; @@ -99,38 +101,31 @@ export default class Header extends ImmutablePureComponent { @{account.get('acct')} {account.get('locked') ? : null} + + {badge} + {fields.size > 0 && ( - - - {fields.map((pair, i) => ( - - - - - ))} - - + + {fields.map((pair, i) => ( + + + + + ))} + )} {fields.size == 0 && metadata.length && ( - - - {(() => { - let data = []; - for (let i = 0; i < metadata.length; i++) { - data.push( - - - - - ); - } - return data; - })()} - - + + {metadata.map((pair, i) => ( + + + + + ))} + ) || null} {info} diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js index 63ff98debe..ebd23a9717 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/index.js +++ b/app/javascript/flavours/glitch/features/account_gallery/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 'flavours/glitch/actions/accounts'; -import { refreshAccountMediaTimeline, expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines'; +import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import Column from 'flavours/glitch/features/ui/components/column'; import ColumnBackButton from 'flavours/glitch/components/column_back_button'; @@ -17,9 +17,31 @@ import LoadMore from 'flavours/glitch/components/load_more'; const mapStateToProps = (state, props) => ({ medias: getAccountGallery(state, props.params.accountId), isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']), - hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}:media`, 'next']), + hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']), }); +class LoadMoreMedia extends ImmutablePureComponent { + + static propTypes = { + maxId: PropTypes.string, + onLoadMore: PropTypes.func.isRequired, + }; + + handleLoadMore = () => { + this.props.onLoadMore(this.props.maxId); + } + + render () { + return ( + + ); + } + +} + @connect(mapStateToProps) export default class AccountGallery extends ImmutablePureComponent { @@ -33,19 +55,19 @@ export default class AccountGallery extends ImmutablePureComponent { componentDidMount () { this.props.dispatch(fetchAccount(this.props.params.accountId)); - this.props.dispatch(refreshAccountMediaTimeline(this.props.params.accountId)); + this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId)); } componentWillReceiveProps (nextProps) { if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { this.props.dispatch(fetchAccount(nextProps.params.accountId)); - this.props.dispatch(refreshAccountMediaTimeline(this.props.params.accountId)); + this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId)); } } handleScrollToBottom = () => { if (this.props.hasMore) { - this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId)); + this.handleLoadMore(this.props.medias.last().getIn(['status', 'id'])); } } @@ -58,7 +80,11 @@ export default class AccountGallery extends ImmutablePureComponent { } } - handleLoadMore = (e) => { + handleLoadMore = maxId => { + this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId, { maxId })); + }; + + handleLoadOlder = (e) => { e.preventDefault(); this.handleScrollToBottom(); } @@ -66,7 +92,7 @@ export default class AccountGallery extends ImmutablePureComponent { render () { const { medias, isLoading, hasMore } = this.props; - let loadMore = null; + let loadOlder = null; if (!medias && isLoading) { return ( @@ -77,7 +103,7 @@ export default class AccountGallery extends ImmutablePureComponent { } if (!isLoading && medias.size > 0 && hasMore) { - loadMore = ; + loadOlder = ; } return ( @@ -89,13 +115,18 @@ export default class AccountGallery extends ImmutablePureComponent { - {medias.map(media => - ( media === null ? ( + 0 ? medias.getIn(index - 1, 'id') : null} + /> + ) : ( + ) - )} - {loadMore} + /> + ))} + {loadOlder} 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 39a1850d7a..a1434b8dd2 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js @@ -16,6 +16,7 @@ export default class Header extends ImmutablePureComponent { onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, + onDirect: PropTypes.func.isRequired, onReblogToggle: PropTypes.func.isRequired, onReport: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired, @@ -40,6 +41,10 @@ export default class Header extends ImmutablePureComponent { this.props.onMention(this.props.account, this.context.router.history); } + handleDirect = () => { + this.props.onDirect(this.props.account, this.context.router.history); + } + handleReport = () => { this.props.onReport(this.props.account); } @@ -89,6 +94,7 @@ export default class Header extends ImmutablePureComponent { account={account} onBlock={this.handleBlock} onMention={this.handleMention} + onDirect={this.handleDirect} onReblogToggle={this.handleReblogToggle} onReport={this.handleReport} onMute={this.handleMute} diff --git a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js index 848119c638..fb0edfa882 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js +++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js @@ -9,7 +9,10 @@ import { unblockAccount, unmuteAccount, } from 'flavours/glitch/actions/accounts'; -import { mentionCompose } from 'flavours/glitch/actions/compose'; +import { + mentionCompose, + directCompose +} from 'flavours/glitch/actions/compose'; import { initMuteModal } from 'flavours/glitch/actions/mutes'; import { initReport } from 'flavours/glitch/actions/reports'; import { openModal } from 'flavours/glitch/actions/modal'; @@ -67,6 +70,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(mentionCompose(account, router)); }, + onDirect (account, router) { + dispatch(directCompose(account, router)); + }, + + onDirect (account, router) { + dispatch(directCompose(account, router)); + }, + onReblogToggle (account) { if (account.getIn(['relationship', 'showing_reblogs'])) { dispatch(followAccount(account.get('id'), false)); diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js index fbb16dff9b..2216f91534 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.js +++ b/app/javascript/flavours/glitch/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 'flavours/glitch/actions/accounts'; -import { refreshAccountTimeline, refreshAccountFeaturedTimeline, expandAccountTimeline } from 'flavours/glitch/actions/timelines'; +import { expandAccountFeaturedTimeline, expandAccountTimeline } from 'flavours/glitch/actions/timelines'; import StatusList from '../../components/status_list'; import LoadingIndicator from '../../components/loading_indicator'; import Column from '../ui/components/column'; @@ -19,7 +19,7 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false }) statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()), featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()), isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']), - hasMore: !!state.getIn(['timelines', `account:${path}`, 'next']), + hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']), }; }; @@ -40,22 +40,24 @@ export default class AccountTimeline extends ImmutablePureComponent { const { params: { accountId }, withReplies } = this.props; this.props.dispatch(fetchAccount(accountId)); - this.props.dispatch(refreshAccountFeaturedTimeline(accountId)); - this.props.dispatch(refreshAccountTimeline(accountId, withReplies)); + if (!withReplies) { + this.props.dispatch(expandAccountFeaturedTimeline(accountId)); + } + this.props.dispatch(expandAccountTimeline(accountId, { withReplies })); } componentWillReceiveProps (nextProps) { 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(refreshAccountFeaturedTimeline(nextProps.params.accountId)); - this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId, nextProps.params.withReplies)); + if (!nextProps.withReplies) { + this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId)); + } + this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies })); } } - handleScrollToBottom = () => { - if (!this.props.isLoading && this.props.hasMore) { - this.props.dispatch(expandAccountTimeline(this.props.params.accountId, this.props.withReplies)); - } + handleLoadMore = maxId => { + this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies })); } render () { @@ -80,7 +82,7 @@ export default class AccountTimeline extends ImmutablePureComponent { featuredStatusIds={featuredStatusIds} isLoading={isLoading} hasMore={hasMore} - onScrollToBottom={this.handleScrollToBottom} + onLoadMore={this.handleLoadMore} /> ); diff --git a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js index dae5caf1dc..9468ad81d7 100644 --- a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js +++ b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js @@ -62,7 +62,7 @@ export default class Bookmarks extends ImmutablePureComponent { this.column = c; } - handleScrollToBottom = debounce(() => { + handleLoadMore = debounce(() => { this.props.dispatch(expandBookmarkedStatuses()); }, 300, { leading: true }) @@ -89,7 +89,7 @@ export default class Bookmarks extends ImmutablePureComponent { scrollKey={`bookmarked_statuses-${columnId}`} hasMore={hasMore} isLoading={isLoading} - onScrollToBottom={this.handleScrollToBottom} + onLoadMore={this.handleLoadMore} /> ); diff --git a/app/javascript/flavours/glitch/features/community_timeline/index.js b/app/javascript/flavours/glitch/features/community_timeline/index.js index 55355414f4..b5843ca164 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/index.js +++ b/app/javascript/flavours/glitch/features/community_timeline/index.js @@ -4,10 +4,7 @@ import PropTypes from 'prop-types'; import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; import Column from 'flavours/glitch/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; -import { - refreshCommunityTimeline, - expandCommunityTimeline, -} from 'flavours/glitch/actions/timelines'; +import { expandCommunityTimeline } from 'flavours/glitch/actions/timelines'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ColumnSettingsContainer from './containers/column_settings_container'; @@ -55,7 +52,7 @@ export default class CommunityTimeline extends React.PureComponent { componentDidMount () { const { dispatch } = this.props; - dispatch(refreshCommunityTimeline()); + dispatch(expandCommunityTimeline()); this.disconnect = dispatch(connectCommunityStream()); } @@ -70,8 +67,8 @@ export default class CommunityTimeline extends React.PureComponent { this.column = c; } - handleLoadMore = () => { - this.props.dispatch(expandCommunityTimeline()); + handleLoadMore = maxId => { + this.props.dispatch(expandCommunityTimeline({ maxId })); } render () { @@ -97,7 +94,7 @@ export default class CommunityTimeline extends React.PureComponent { trackScroll={!pinned} scrollKey={`community_timeline-${columnId}`} timelineId='community' - loadMore={this.handleLoadMore} + onLoadMore={this.handleLoadMore} emptyMessage={} /> diff --git a/app/javascript/flavours/glitch/features/composer/direct_warning/index.js b/app/javascript/flavours/glitch/features/composer/direct_warning/index.js new file mode 100644 index 0000000000..d1febdd1ba --- /dev/null +++ b/app/javascript/flavours/glitch/features/composer/direct_warning/index.js @@ -0,0 +1,53 @@ +import React from 'react'; +import Motion from 'flavours/glitch/util/optional_motion'; +import spring from 'react-motion/lib/spring'; +import { defineMessages, FormattedMessage } from 'react-intl'; + +// This is the spring used with our motion. +const motionSpring = spring(1, { damping: 35, stiffness: 400 }); + +// Messages. +const messages = defineMessages({ + disclaimer: { + defaultMessage: 'This toot will only be sent to all the mentioned users.', + id: 'compose_form.direct_message_warning', + }, + learn_more: { + defaultMessage: 'Learn more', + id: 'compose_form.direct_message_warning_learn_more' + } +}); + +// The component. +export default function ComposerDirectWarning () { + return ( + + {({ opacity, scaleX, scaleY }) => ( + + + + + + )} + + ); +} + +ComposerDirectWarning.propTypes = {}; diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js index 3aa283628e..21b03be390 100644 --- a/app/javascript/flavours/glitch/features/composer/index.js +++ b/app/javascript/flavours/glitch/features/composer/index.js @@ -39,6 +39,7 @@ import ComposerTextarea from './textarea'; import ComposerUploadForm from './upload_form'; import ComposerWarning from './warning'; import ComposerHashtagWarning from './hashtag_warning'; +import ComposerDirectWarning from './direct_warning'; // Utils. import { countableText } from 'flavours/glitch/util/counter'; @@ -55,6 +56,7 @@ function mapStateToProps (state) { advancedOptions: state.getIn(['compose', 'advanced_options']), amUnlocked: !state.getIn(['accounts', me, 'locked']), focusDate: state.getIn(['compose', 'focusDate']), + caretPosition: state.getIn(['compose', 'caretPosition']), isSubmitting: state.getIn(['compose', 'is_submitting']), isUploading: state.getIn(['compose', 'is_uploading']), layout: state.getIn(['local_settings', 'layout']), @@ -116,7 +118,6 @@ const handlers = { handleEmoji (data) { const { textarea: { selectionStart } } = this; const { onInsertEmoji } = this.props; - this.caretPos = selectionStart + data.native.length + 1; if (onInsertEmoji) { onInsertEmoji(selectionStart, data); } @@ -138,7 +139,6 @@ const handlers = { // Selects a suggestion from the autofill. handleSelect (tokenStart, token, value) { const { onSelectSuggestion } = this.props; - this.caretPos = null; if (onSelectSuggestion) { onSelectSuggestion(tokenStart, token, value); } @@ -190,20 +190,9 @@ class Composer extends React.Component { assignHandlers(this, handlers); // Instance variables. - this.caretPos = null; this.textarea = null; } - // If this is the update where we've finished uploading, - // save the last caret position so we can restore it below! - componentWillReceiveProps (nextProps) { - const { textarea } = this; - const { isUploading } = this.props; - if (textarea && isUploading && !nextProps.isUploading) { - this.caretPos = textarea.selectionStart; - } - } - // Tells our state the composer has been mounted. componentDidMount () { const { onMount } = this.props; @@ -227,17 +216,13 @@ class Composer extends React.Component { // - Replying to more than one user, selects any usernames past // the first; this provides a convenient shortcut to drop // everyone else from the conversation. - // - If we've just finished uploading an image, and have a saved - // caret position, restores the cursor to that position after the - // text changes. componentDidUpdate (prevProps) { const { - caretPos, textarea, } = this; const { focusDate, - isUploading, + caretPosition, isSubmitting, preselectDate, text, @@ -245,14 +230,14 @@ class Composer extends React.Component { let selectionEnd, selectionStart; // Caret/selection handling. - if (focusDate !== prevProps.focusDate || (prevProps.isUploading && !isUploading && !isNaN(caretPos) && caretPos !== null)) { + if (focusDate !== prevProps.focusDate) { switch (true) { case preselectDate !== prevProps.preselectDate: selectionStart = text.search(/\s/) + 1; selectionEnd = text.length; break; - case !isNaN(caretPos) && caretPos !== null: - selectionStart = selectionEnd = caretPos; + case !isNaN(caretPosition) && caretPosition !== null: + selectionStart = selectionEnd = caretPosition; break; default: selectionStart = selectionEnd = text.length; @@ -326,6 +311,7 @@ class Composer extends React.Component { onSubmit={handleSubmit} text={spoilerText} /> + {privacy === 'direct' ? : null} {privacy === 'private' && amUnlocked ? : null} {privacy !== 'public' && APPROX_HASHTAG_RE.test(text) ? : null} {replyContent ? ( @@ -408,6 +394,7 @@ Composer.propTypes = { advancedOptions: ImmutablePropTypes.map, amUnlocked: PropTypes.bool, focusDate: PropTypes.instanceOf(Date), + caretPosition: PropTypes.number, isSubmitting: PropTypes.bool, isUploading: PropTypes.bool, layout: PropTypes.string, diff --git a/app/javascript/flavours/glitch/features/composer/reply/index.js b/app/javascript/flavours/glitch/features/composer/reply/index.js index 0b8ceddeea..0500a75d0e 100644 --- a/app/javascript/flavours/glitch/features/composer/reply/index.js +++ b/app/javascript/flavours/glitch/features/composer/reply/index.js @@ -58,6 +58,7 @@ export default class ComposerReply extends React.PureComponent { icon='times' onClick={handleClick} title={intl.formatMessage(messages.cancel)} + inverted /> {account ? ( { - this.props.dispatch(expandDirectTimeline()); + handleLoadMore = maxId => { + this.props.dispatch(expandDirectTimeline({ maxId })); } render () { @@ -97,7 +94,7 @@ export default class DirectTimeline extends React.PureComponent { trackScroll={!pinned} scrollKey={`direct_timeline-${columnId}`} timelineId='direct' - loadMore={this.handleLoadMore} + onLoadMore={this.handleLoadMore} emptyMessage={} /> diff --git a/app/javascript/flavours/glitch/features/domain_blocks/index.js b/app/javascript/flavours/glitch/features/domain_blocks/index.js index b17c47e91a..3b29e2a267 100644 --- a/app/javascript/flavours/glitch/features/domain_blocks/index.js +++ b/app/javascript/flavours/glitch/features/domain_blocks/index.js @@ -52,7 +52,7 @@ export default class Blocks extends ImmutablePureComponent { } return ( - + {domains.map(domain => diff --git a/app/javascript/flavours/glitch/features/drawer/results/index.js b/app/javascript/flavours/glitch/features/drawer/results/index.js index f2a79eb592..23dc0e3cf5 100644 --- a/app/javascript/flavours/glitch/features/drawer/results/index.js +++ b/app/javascript/flavours/glitch/features/drawer/results/index.js @@ -68,6 +68,8 @@ export default function DrawerResults ({ {accounts && accounts.size ? ( + + {accounts.map( accountId => ( + + {statuses.map( statusId => ( + + {hashtags.map( hashtag => ( { + 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} /> ); diff --git a/app/javascript/flavours/glitch/features/getting_started_misc/index.js b/app/javascript/flavours/glitch/features/getting_started_misc/index.js index 77c44c273e..b67e6f97f6 100644 --- a/app/javascript/flavours/glitch/features/getting_started_misc/index.js +++ b/app/javascript/flavours/glitch/features/getting_started_misc/index.js @@ -50,7 +50,7 @@ export default class gettingStartedMisc extends ImmutablePureComponent { - + diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js index 9f3c9bec76..8f77ed42b5 100644 --- a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js @@ -4,10 +4,7 @@ import PropTypes from 'prop-types'; import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; import Column from 'flavours/glitch/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; -import { - refreshHashtagTimeline, - expandHashtagTimeline, -} from 'flavours/glitch/actions/timelines'; +import { expandHashtagTimeline } from 'flavours/glitch/actions/timelines'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import { FormattedMessage } from 'react-intl'; import { connectHashtagStream } from 'flavours/glitch/actions/streaming'; @@ -61,13 +58,13 @@ export default class HashtagTimeline extends React.PureComponent { const { dispatch } = this.props; const { id } = this.props.params; - dispatch(refreshHashtagTimeline(id)); + dispatch(expandHashtagTimeline(id)); this._subscribe(dispatch, id); } componentWillReceiveProps (nextProps) { if (nextProps.params.id !== this.props.params.id) { - this.props.dispatch(refreshHashtagTimeline(nextProps.params.id)); + this.props.dispatch(expandHashtagTimeline(nextProps.params.id)); this._unsubscribe(); this._subscribe(this.props.dispatch, nextProps.params.id); } @@ -81,8 +78,8 @@ export default class HashtagTimeline extends React.PureComponent { this.column = c; } - handleLoadMore = () => { - this.props.dispatch(expandHashtagTimeline(this.props.params.id)); + handleLoadMore = maxId => { + this.props.dispatch(expandHashtagTimeline(this.props.params.id, { maxId })); } render () { @@ -108,7 +105,7 @@ export default class HashtagTimeline extends React.PureComponent { trackScroll={!pinned} scrollKey={`hashtag_timeline-${columnId}`} timelineId={`hashtag:${id}`} - loadMore={this.handleLoadMore} + onLoadMore={this.handleLoadMore} emptyMessage={} /> diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.js b/app/javascript/flavours/glitch/features/home_timeline/index.js index c20c0244a6..3650ffc6d1 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/index.js +++ b/app/javascript/flavours/glitch/features/home_timeline/index.js @@ -1,6 +1,6 @@ import React from 'react'; import { connect } from 'react-redux'; -import { expandHomeTimeline, refreshHomeTimeline } from 'flavours/glitch/actions/timelines'; +import { expandHomeTimeline } from 'flavours/glitch/actions/timelines'; import PropTypes from 'prop-types'; import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; import Column from 'flavours/glitch/components/column'; @@ -16,7 +16,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0, - isPartial: state.getIn(['timelines', 'home', 'isPartial'], false), + isPartial: state.getIn(['timelines', 'home', 'items', 0], null) === null, }); @connect(mapStateToProps) @@ -55,8 +55,8 @@ export default class HomeTimeline extends React.PureComponent { this.column = c; } - handleLoadMore = () => { - this.props.dispatch(expandHomeTimeline()); + handleLoadMore = maxId => { + this.props.dispatch(expandHomeTimeline({ maxId })); } componentDidMount () { @@ -78,7 +78,7 @@ export default class HomeTimeline extends React.PureComponent { return; } else if (!wasPartial && isPartial) { this.polling = setInterval(() => { - dispatch(refreshHomeTimeline()); + dispatch(expandHomeTimeline()); }, 3000); } else if (wasPartial && !isPartial) { this._stopPolling(); @@ -114,7 +114,7 @@ export default class HomeTimeline extends React.PureComponent { }} />} /> diff --git a/app/javascript/flavours/glitch/features/list_timeline/index.js b/app/javascript/flavours/glitch/features/list_timeline/index.js index f9476d92d5..07edf45aad 100644 --- a/app/javascript/flavours/glitch/features/list_timeline/index.js +++ b/app/javascript/flavours/glitch/features/list_timeline/index.js @@ -8,7 +8,7 @@ import ColumnHeader from 'flavours/glitch/components/column_header'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { connectListStream } from 'flavours/glitch/actions/streaming'; -import { refreshListTimeline, expandListTimeline } from 'flavours/glitch/actions/timelines'; +import { expandListTimeline } from 'flavours/glitch/actions/timelines'; import { fetchList, deleteList } from 'flavours/glitch/actions/lists'; import { openModal } from 'flavours/glitch/actions/modal'; import MissingIndicator from 'flavours/glitch/components/missing_indicator'; @@ -67,7 +67,7 @@ export default class ListTimeline extends React.PureComponent { const { id } = this.props.params; dispatch(fetchList(id)); - dispatch(refreshListTimeline(id)); + dispatch(expandListTimeline(id)); this.disconnect = dispatch(connectListStream(id)); } @@ -83,9 +83,9 @@ export default class ListTimeline extends React.PureComponent { this.column = c; } - handleLoadMore = () => { + handleLoadMore = maxId => { const { id } = this.props.params; - this.props.dispatch(expandListTimeline(id)); + this.props.dispatch(expandListTimeline(id, { maxId })); } handleEditClick = () => { @@ -164,7 +164,7 @@ export default class ListTimeline extends React.PureComponent { trackScroll={!pinned} scrollKey={`list_timeline-${columnId}`} timelineId={`list:${id}`} - loadMore={this.handleLoadMore} + onLoadMore={this.handleLoadMore} emptyMessage={} /> diff --git a/app/javascript/flavours/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js index 12b0b5b834..266d6807d9 100644 --- a/app/javascript/flavours/glitch/features/notifications/index.js +++ b/app/javascript/flavours/glitch/features/notifications/index.js @@ -17,6 +17,7 @@ import { createSelector } from 'reselect'; import { List as ImmutableList } from 'immutable'; import { debounce } from 'lodash'; import ScrollableList from 'flavours/glitch/components/scrollable_list'; +import LoadGap from 'flavours/glitch/components/load_gap'; const messages = defineMessages({ title: { id: 'column.notifications', defaultMessage: 'Notifications' }, @@ -25,14 +26,14 @@ const messages = defineMessages({ const getNotifications = createSelector([ state => ImmutableList(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()), state => state.getIn(['notifications', 'items']), -], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type')))); +], (excludedTypes, notifications) => notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')))); const mapStateToProps = state => ({ notifications: getNotifications(state), localSettings: state.get('local_settings'), isLoading: state.getIn(['notifications', 'isLoading'], true), isUnread: state.getIn(['notifications', 'unread']) > 0, - hasMore: !!state.getIn(['notifications', 'next']), + hasMore: state.getIn(['notifications', 'hasMore']), notifCleaningActive: state.getIn(['notifications', 'cleaningMode']), }); @@ -67,9 +68,13 @@ export default class Notifications extends React.PureComponent { trackScroll: true, }; - handleScrollToBottom = debounce(() => { - this.props.dispatch(scrollTopNotifications(false)); - this.props.dispatch(expandNotifications()); + handleLoadGap = (maxId) => { + this.props.dispatch(expandNotifications({ maxId })); + }; + + handleLoadOlder = debounce(() => { + const last = this.props.notifications.last(); + this.props.dispatch(expandNotifications({ maxId: last && last.get('id') })); }, 300, { leading: true }); handleScrollToTop = debounce(() => { @@ -104,12 +109,12 @@ export default class Notifications extends React.PureComponent { } handleMoveUp = id => { - const elementIndex = this.props.notifications.findIndex(item => item.get('id') === id) - 1; + const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1; this._selectChild(elementIndex); } handleMoveDown = id => { - const elementIndex = this.props.notifications.findIndex(item => item.get('id') === id) + 1; + const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1; this._selectChild(elementIndex); } @@ -131,7 +136,14 @@ export default class Notifications extends React.PureComponent { if (isLoading && this.scrollableContent) { scrollableContent = this.scrollableContent; } else if (notifications.size > 0 || hasMore) { - scrollableContent = notifications.map((item) => ( + scrollableContent = notifications.map((item, index) => item === null ? ( + 0 ? notifications.getIn([index - 1, 'id']) : null} + onClick={this.handleLoadGap} + /> + ) : ( { - this.props.dispatch(expandPublicTimeline()); + handleLoadMore = maxId => { + this.props.dispatch(expandPublicTimeline({ maxId })); } render () { @@ -95,7 +92,7 @@ export default class PublicTimeline extends React.PureComponent { } diff --git a/app/javascript/flavours/glitch/features/standalone/community_timeline/index.js b/app/javascript/flavours/glitch/features/standalone/community_timeline/index.js new file mode 100644 index 0000000000..c488f95417 --- /dev/null +++ b/app/javascript/flavours/glitch/features/standalone/community_timeline/index.js @@ -0,0 +1,71 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; +import { expandCommunityTimeline } from 'flavours/glitch/actions/timelines'; +import Column from 'flavours/glitch/components/column'; +import ColumnHeader from 'flavours/glitch/components/column_header'; +import { defineMessages, injectIntl } from 'react-intl'; +import { connectCommunityStream } from 'flavours/glitch/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(expandCommunityTimeline()); + this.disconnect = dispatch(connectCommunityStream()); + } + + componentWillUnmount () { + if (this.disconnect) { + this.disconnect(); + this.disconnect = null; + } + } + + handleLoadMore = maxId => { + this.props.dispatch(expandCommunityTimeline({ maxId })); + } + + render () { + const { intl } = this.props; + + return ( + + + + + + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js index 0ad2cef80e..dc02f1c917 100644 --- a/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js +++ b/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js @@ -2,12 +2,10 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; -import { - refreshHashtagTimeline, - expandHashtagTimeline, -} from 'flavours/glitch/actions/timelines'; +import { expandHashtagTimeline } from 'flavours/glitch/actions/timelines'; import Column from 'flavours/glitch/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; +import { connectHashtagStream } from 'flavours/glitch/actions/streaming'; @connect() export default class HashtagTimeline extends React.PureComponent { @@ -28,22 +26,19 @@ export default class HashtagTimeline extends React.PureComponent { componentDidMount () { const { dispatch, hashtag } = this.props; - dispatch(refreshHashtagTimeline(hashtag)); - - this.polling = setInterval(() => { - dispatch(refreshHashtagTimeline(hashtag)); - }, 10000); + dispatch(expandHashtagTimeline(hashtag)); + this.disconnect = dispatch(connectHashtagStream(hashtag)); } componentWillUnmount () { - if (typeof this.polling !== 'undefined') { - clearInterval(this.polling); - this.polling = null; + if (this.disconnect) { + this.disconnect(); + this.disconnect = null; } } - handleLoadMore = () => { - this.props.dispatch(expandHashtagTimeline(this.props.hashtag)); + handleLoadMore = maxId => { + this.props.dispatch(expandHashtagTimeline(this.props.hashtag, { maxId })); } render () { @@ -61,7 +56,7 @@ export default class HashtagTimeline extends React.PureComponent { trackScroll={false} scrollKey='standalone_hashtag_timeline' timelineId={`hashtag:${hashtag}`} - loadMore={this.handleLoadMore} + onLoadMore={this.handleLoadMore} /> ); diff --git a/app/javascript/flavours/glitch/features/standalone/public_timeline/index.js b/app/javascript/flavours/glitch/features/standalone/public_timeline/index.js index 717f6fcafc..0b4238485b 100644 --- a/app/javascript/flavours/glitch/features/standalone/public_timeline/index.js +++ b/app/javascript/flavours/glitch/features/standalone/public_timeline/index.js @@ -2,13 +2,11 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; -import { - refreshPublicTimeline, - expandPublicTimeline, -} from 'flavours/glitch/actions/timelines'; +import { expandPublicTimeline } from 'flavours/glitch/actions/timelines'; import Column from 'flavours/glitch/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; import { defineMessages, injectIntl } from 'react-intl'; +import { connectPublicStream } from 'flavours/glitch/actions/streaming'; const messages = defineMessages({ title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' }, @@ -34,22 +32,19 @@ export default class PublicTimeline extends React.PureComponent { componentDidMount () { const { dispatch } = this.props; - dispatch(refreshPublicTimeline()); - - this.polling = setInterval(() => { - dispatch(refreshPublicTimeline()); - }, 3000); + dispatch(expandPublicTimeline()); + this.disconnect = dispatch(connectPublicStream()); } componentWillUnmount () { - if (typeof this.polling !== 'undefined') { - clearInterval(this.polling); - this.polling = null; + if (this.disconnect) { + this.disconnect(); + this.disconnect = null; } } - handleLoadMore = () => { - this.props.dispatch(expandPublicTimeline()); + handleLoadMore = maxId => { + this.props.dispatch(expandPublicTimeline({ maxId })); } render () { @@ -65,7 +60,7 @@ export default class PublicTimeline extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index 1ea0fa4214..ef8805377c 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -8,6 +8,7 @@ import { me } from 'flavours/glitch/util/initial_state'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, + direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' }, mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, reply: { id: 'status.reply', defaultMessage: 'Reply' }, reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, @@ -43,6 +44,7 @@ export default class ActionBar extends React.PureComponent { onMuteConversation: PropTypes.func, onBlock: PropTypes.func, onDelete: PropTypes.func.isRequired, + onDirect: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, onReport: PropTypes.func, onPin: PropTypes.func, @@ -70,6 +72,10 @@ export default class ActionBar extends React.PureComponent { this.props.onDelete(this.props.status); } + handleDirectClick = () => { + this.props.onDirect(this.props.status.get('account'), this.context.router.history); + } + handleMentionClick = () => { this.props.onMention(this.props.status.get('account'), this.context.router.history); } @@ -115,6 +121,7 @@ export default class ActionBar extends React.PureComponent { if (publicStatus) { menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); + menu.push(null); } if (me === status.getIn(['account', 'id'])) { @@ -128,6 +135,7 @@ export default class ActionBar extends React.PureComponent { menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); } else { menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); + menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); menu.push(null); menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); @@ -149,7 +157,7 @@ export default class ActionBar extends React.PureComponent { - + {shareButton} diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index 16f7ae830e..5cfc9dfaed 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -37,8 +37,8 @@ export default class DetailedStatus extends ImmutablePureComponent { e.stopPropagation(); } - handleOpenVideo = startTime => { - this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime); + handleOpenVideo = (media, startTime) => { + this.props.onOpenVideo(media, startTime); } render () { diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index 7e1658dbb4..6c9da8e3e6 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -21,6 +21,7 @@ import { import { replyCompose, mentionCompose, + directCompose, } from 'flavours/glitch/actions/compose'; import { blockAccount } from 'flavours/glitch/actions/accounts'; import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses'; @@ -170,6 +171,10 @@ export default class Status extends ImmutablePureComponent { } } + handleDirectClick = (account, router) => { + this.props.dispatch(directCompose(account, router)); + } + handleMentionClick = (account, router) => { this.props.dispatch(mentionCompose(account, router)); } @@ -399,6 +404,7 @@ export default class Status extends ImmutablePureComponent { onReblog={this.handleReblogClick} onBookmark={this.handleBookmarkClick} onDelete={this.handleDeleteClick} + onDirect={this.handleDirectClick} onMention={this.handleMentionClick} onMute={this.handleMuteClick} onMuteConversation={this.handleConversationMuteClick} diff --git a/app/javascript/flavours/glitch/features/ui/components/media_modal.js b/app/javascript/flavours/glitch/features/ui/components/media_modal.js index 6ab6770ed4..bffe3b1f73 100644 --- a/app/javascript/flavours/glitch/features/ui/components/media_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.js @@ -2,6 +2,7 @@ import React from 'react'; import ReactSwipeableViews from 'react-swipeable-views'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; +import Video from 'flavours/glitch/features/video'; import ExtendedVideoPlayer from 'flavours/glitch/components/extended_video_player'; import classNames from 'classnames'; import { defineMessages, injectIntl } from 'react-intl'; @@ -112,6 +113,22 @@ export default class MediaModal extends ImmutablePureComponent { onClick={this.toggleNavigation} /> ); + } else if (image.get('type') === 'video') { + const { time } = this.props; + + return ( + + ); } else if (image.get('type') === 'gifv') { return ( + {visible && ( {(SpecificComponent) => } diff --git a/app/javascript/flavours/glitch/features/ui/components/report_modal.js b/app/javascript/flavours/glitch/features/ui/components/report_modal.js index 3b7a5ff206..ff81522a8c 100644 --- a/app/javascript/flavours/glitch/features/ui/components/report_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/report_modal.js @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { changeReportComment, changeReportForward, submitReport } from 'flavours/glitch/actions/reports'; -import { refreshAccountTimeline } from 'flavours/glitch/actions/timelines'; +import { expandAccountTimeline } from 'flavours/glitch/actions/timelines'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { makeGetAccount } from 'flavours/glitch/selectors'; @@ -64,12 +64,12 @@ export default class ReportModal extends ImmutablePureComponent { } componentDidMount () { - this.props.dispatch(refreshAccountTimeline(this.props.account.get('id'), true)); + this.props.dispatch(expandAccountTimeline(this.props.account.get('id'), { withReplies: true })); } componentWillReceiveProps (nextProps) { if (this.props.account !== nextProps.account && nextProps.account) { - this.props.dispatch(refreshAccountTimeline(nextProps.account.get('id'), true)); + this.props.dispatch(expandAccountTimeline(nextProps.account.get('id'), { withReplies: true })); } } diff --git a/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js b/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js index f85a2eeb8a..e0c017f827 100644 --- a/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js +++ b/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js @@ -21,6 +21,8 @@ const makeGetStatusIds = () => createSelector([ } return statusIds.filter(id => { + if (id === null) return true; + const statusForId = statuses.get(id); let showStatus = true; @@ -52,18 +54,13 @@ const makeMapStateToProps = () => { statusIds: getStatusIds(state, { type: timelineId }), isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true), isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false), - hasMore: !!state.getIn(['timelines', timelineId, 'next']), + hasMore: state.getIn(['timelines', timelineId, 'hasMore']), }); return mapStateToProps; }; -const mapDispatchToProps = (dispatch, { timelineId, loadMore }) => ({ - - onScrollToBottom: debounce(() => { - dispatch(scrollTopTimeline(timelineId, false)); - loadMore(); - }, 300, { leading: true }), +const mapDispatchToProps = (dispatch, { timelineId }) => ({ onScrollToTop: debounce(() => { dispatch(scrollTopTimeline(timelineId, true)); diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index e4b69cb3ba..0e3a83bb6e 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -9,8 +9,8 @@ import { Redirect, withRouter } from 'react-router-dom'; import { isMobile } from 'flavours/glitch/util/is_mobile'; import { debounce } from 'lodash'; import { uploadCompose, resetCompose } from 'flavours/glitch/actions/compose'; -import { refreshHomeTimeline } from 'flavours/glitch/actions/timelines'; -import { refreshNotifications } from 'flavours/glitch/actions/notifications'; +import { expandHomeTimeline } from 'flavours/glitch/actions/timelines'; +import { expandNotifications } from 'flavours/glitch/actions/notifications'; import { clearHeight } from 'flavours/glitch/actions/height_cache'; import { WrappedSwitch, WrappedRoute } from 'flavours/glitch/util/react_router_helpers'; import UploadArea from './components/upload_area'; @@ -219,8 +219,8 @@ export default class UI extends React.Component { navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage); } - this.props.dispatch(refreshHomeTimeline()); - this.props.dispatch(refreshNotifications()); + this.props.dispatch(expandHomeTimeline()); + this.props.dispatch(expandNotifications()); } componentDidMount () { diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js index 3be6e19f78..e9e095e260 100644 --- a/app/javascript/flavours/glitch/features/video/index.js +++ b/app/javascript/flavours/glitch/features/video/index.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { fromJS } from 'immutable'; import { throttle } from 'lodash'; import classNames from 'classnames'; import { isFullscreen, requestFullscreen, exitFullscreen } from 'flavours/glitch/util/fullscreen'; @@ -133,6 +134,8 @@ export default class Video extends React.PureComponent { this.seek = c; } + handleClickRoot = e => e.stopPropagation(); + handlePlay = () => { this.setState({ paused: false }); } @@ -246,8 +249,17 @@ export default class Video extends React.PureComponent { } handleOpenVideo = () => { + const { src, preview, width, height } = this.props; + const media = fromJS({ + type: 'video', + url: src, + preview_url: preview, + width, + height, + }); + this.video.pause(); - this.props.onOpenVideo(this.video.currentTime); + this.props.onOpenVideo(media, this.video.currentTime); } handleCloseVideo = () => { @@ -279,7 +291,15 @@ export default class Video extends React.PureComponent { } return ( - + { - const props = JSON.parse(content.getAttribute('data-props')); - ReactDOM.render(, content); - }); - - [].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => { - const props = JSON.parse(content.getAttribute('data-props')); - ReactDOM.render(, content); - }); - - const mediaGalleries = document.querySelectorAll('[data-component="MediaGallery"]'); - - if (mediaGalleries.length > 0) { - const MediaGalleriesContainer = require('flavours/glitch/containers/media_galleries_container').default; - const content = document.createElement('div'); - - ReactDOM.render(, content); - document.body.appendChild(content); + const reactComponents = document.querySelectorAll('[data-component]'); + if (reactComponents.length > 0) { + import(/* webpackChunkName: "containers/media_container" */ 'flavours/glitch/containers/media_container') + .then(({ default: MediaContainer }) => { + const content = document.createElement('div'); + ReactDOM.render(, content); + document.body.appendChild(content); + }) + .catch(error => console.error(error)); } }); } diff --git a/app/javascript/flavours/glitch/reducers/accounts.js b/app/javascript/flavours/glitch/reducers/accounts.js index 23fbd999cd..86f4970c97 100644 --- a/app/javascript/flavours/glitch/reducers/accounts.js +++ b/app/javascript/flavours/glitch/reducers/accounts.js @@ -57,6 +57,12 @@ import { STORE_HYDRATE } from 'flavours/glitch/actions/store'; import emojify from 'flavours/glitch/util/emoji'; import { Map as ImmutableMap, fromJS } from 'immutable'; import escapeTextContentForBrowser from 'escape-html'; +import { unescapeHTML } from 'flavours/glitch/util/html'; + +const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { + obj[`:${emoji.shortcode}:`] = emoji; + return obj; +}, {}); const normalizeAccount = (state, account) => { account = { ...account }; @@ -65,15 +71,17 @@ const normalizeAccount = (state, account) => { delete account.following_count; delete account.statuses_count; + const emojiMap = makeEmojiMap(account); const displayName = account.display_name.length === 0 ? account.username : account.display_name; - account.display_name_html = emojify(escapeTextContentForBrowser(displayName)); - account.note_emojified = emojify(account.note); + account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap); + account.note_emojified = emojify(account.note, emojiMap); if (account.fields) { account.fields = account.fields.map(pair => ({ ...pair, name_emojified: emojify(escapeTextContentForBrowser(pair.name)), - value_emojified: emojify(pair.value), + value_emojified: emojify(pair.value, emojiMap), + value_plain: unescapeHTML(pair.value), })); } diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 8973c77135..24a8af86f1 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -5,6 +5,7 @@ import { COMPOSE_CYCLE_ELEFRIEND, COMPOSE_REPLY, COMPOSE_REPLY_CANCEL, + COMPOSE_DIRECT, COMPOSE_MENTION, COMPOSE_SUBMIT_REQUEST, COMPOSE_SUBMIT_SUCCESS, @@ -55,6 +56,7 @@ const initialState = ImmutableMap({ privacy: null, text: '', focusDate: null, + caretPosition: null, preselectDate: null, in_reply_to: null, is_submitting: false, @@ -147,6 +149,7 @@ function continueThread (state, status) { map.update('media_attachments', list => list.clear()); map.set('idempotencyKey', uuid()); map.set('focusDate', new Date()); + map.set('caretPosition', null); map.set('preselectDate', new Date()); }); } @@ -158,7 +161,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.set('focusDate', new Date()); map.set('idempotencyKey', uuid()); if (prevSize === 0 && (state.get('default_sensitive') || state.get('spoiler'))) { @@ -186,6 +188,7 @@ const insertSuggestion = (state, position, token, completion) => { map.set('suggestion_token', null); map.update('suggestions', ImmutableList(), list => list.clear()); map.set('focusDate', new Date()); + map.set('caretPosition', position + completion.length + 1); map.set('idempotencyKey', uuid()); }); }; @@ -196,6 +199,7 @@ const insertEmoji = (state, position, emojiData) => { return state.withMutations(map => { map.update('text', oldText => `${oldText.slice(0, position)}${emoji}\u200B${oldText.slice(position)}`); map.set('focusDate', new Date()); + map.set('caretPosition', position + emoji.length + 1); map.set('idempotencyKey', uuid()); }); }; @@ -277,6 +281,7 @@ export default function compose(state = initialState, action) { map => map.merge(new ImmutableMap({ do_not_federate: /👁\ufe0f?\u200b?(?:<\/p>)?$/.test(action.status.get('content')) })) ); map.set('focusDate', new Date()); + map.set('caretPosition', null); map.set('preselectDate', new Date()); map.set('idempotencyKey', uuid()); @@ -321,10 +326,20 @@ export default function compose(state = initialState, action) { case COMPOSE_UPLOAD_PROGRESS: return state.set('progress', Math.round((action.loaded / action.total) * 100)); case COMPOSE_MENTION: - return state - .update('text', text => `${text}@${action.account.get('acct')} `) - .set('focusDate', new Date()) - .set('idempotencyKey', uuid()); + return state.withMutations(map => { + map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' ')); + map.set('focusDate', new Date()); + map.set('caretPosition', null); + map.set('idempotencyKey', uuid()); + }); + case COMPOSE_DIRECT: + return state.withMutations(map => { + map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' ')); + map.set('privacy', 'direct'); + map.set('focusDate', new Date()); + map.set('caretPosition', null); + map.set('idempotencyKey', uuid()); + }); case COMPOSE_SUGGESTIONS_CLEAR: return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null); case COMPOSE_SUGGESTIONS_READY: diff --git a/app/javascript/flavours/glitch/reducers/domain_lists.js b/app/javascript/flavours/glitch/reducers/domain_lists.js index a9e3519f3d..eff97fbd60 100644 --- a/app/javascript/flavours/glitch/reducers/domain_lists.js +++ b/app/javascript/flavours/glitch/reducers/domain_lists.js @@ -6,7 +6,9 @@ import { import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; const initialState = ImmutableMap({ - blocks: ImmutableMap(), + blocks: ImmutableMap({ + items: ImmutableOrderedSet(), + }), }); export default function domainLists(state = initialState, action) { diff --git a/app/javascript/flavours/glitch/reducers/notifications.js b/app/javascript/flavours/glitch/reducers/notifications.js index fb2b3f5498..dc820b4766 100644 --- a/app/javascript/flavours/glitch/reducers/notifications.js +++ b/app/javascript/flavours/glitch/reducers/notifications.js @@ -1,10 +1,7 @@ import { NOTIFICATIONS_UPDATE, - NOTIFICATIONS_REFRESH_SUCCESS, NOTIFICATIONS_EXPAND_SUCCESS, - NOTIFICATIONS_REFRESH_REQUEST, NOTIFICATIONS_EXPAND_REQUEST, - NOTIFICATIONS_REFRESH_FAIL, NOTIFICATIONS_EXPAND_FAIL, NOTIFICATIONS_CLEAR, NOTIFICATIONS_SCROLL_TOP, @@ -19,16 +16,16 @@ import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS, } from 'flavours/glitch/actions/accounts'; -import { TIMELINE_DELETE } from 'flavours/glitch/actions/timelines'; +import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from 'flavours/glitch/actions/timelines'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import compareId from 'flavours/glitch/util/compare_id'; const initialState = ImmutableMap({ items: ImmutableList(), - next: null, + hasMore: true, top: true, unread: 0, - loaded: false, - isLoading: true, + isLoading: false, cleaningMode: false, // notification removal mark of new notifs loaded whilst cleaningMode is true. markNewForDelete: false, @@ -58,39 +55,38 @@ const normalizeNotification = (state, notification) => { }); }; -const normalizeNotifications = (state, notifications, next) => { - let items = ImmutableList(); - const loaded = state.get('loaded'); - - notifications.forEach((n, i) => { - items = items.set(i, notificationToMap(state, n)); - }); - - if (state.get('next') === null) { - state = state.set('next', next); - } - - return state - .update('items', list => loaded ? items.concat(list) : list.concat(items)) - .set('loaded', true) - .set('isLoading', false); -}; - -const appendNormalizedNotifications = (state, notifications, next) => { +const expandNormalizedNotifications = (state, notifications, next) => { let items = ImmutableList(); notifications.forEach((n, i) => { items = items.set(i, notificationToMap(state, n)); }); - return state - .update('items', list => list.concat(items)) - .set('next', next) - .set('isLoading', false); + return state.withMutations(mutable => { + if (!items.isEmpty()) { + mutable.update('items', list => { + const lastIndex = 1 + list.findLastIndex( + item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')) + ); + + const firstIndex = 1 + list.take(lastIndex).findLastIndex( + item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0 + ); + + return list.take(firstIndex).concat(items, list.skip(lastIndex)); + }); + } + + if (!next) { + mutable.set('hasMore', true); + } + + mutable.set('isLoading', false); + }); }; const filterNotifications = (state, relationship) => { - return state.update('items', list => list.filterNot(item => item.get('account') === relationship.id)); + return state.update('items', list => list.filterNot(item => item !== null && item.get('account') === relationship.id)); }; const updateTop = (state, top) => { @@ -102,7 +98,7 @@ const updateTop = (state, top) => { }; const deleteByStatus = (state, statusId) => { - return state.update('items', list => list.filterNot(item => item.get('status') === statusId)); + return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId)); }; const markForDelete = (state, notificationId, yes) => { @@ -137,29 +133,29 @@ export default function notifications(state = initialState, action) { let st; switch(action.type) { - case NOTIFICATIONS_REFRESH_REQUEST: case NOTIFICATIONS_EXPAND_REQUEST: case NOTIFICATIONS_DELETE_MARKED_REQUEST: return state.set('isLoading', true); case NOTIFICATIONS_DELETE_MARKED_FAIL: - case NOTIFICATIONS_REFRESH_FAIL: case NOTIFICATIONS_EXPAND_FAIL: return state.set('isLoading', false); case NOTIFICATIONS_SCROLL_TOP: return updateTop(state, action.top); case NOTIFICATIONS_UPDATE: return normalizeNotification(state, action.notification); - case NOTIFICATIONS_REFRESH_SUCCESS: - return normalizeNotifications(state, action.notifications, action.next); case NOTIFICATIONS_EXPAND_SUCCESS: - return appendNormalizedNotifications(state, action.notifications, action.next); + return expandNormalizedNotifications(state, action.notifications, action.next); case ACCOUNT_BLOCK_SUCCESS: case ACCOUNT_MUTE_SUCCESS: return filterNotifications(state, action.relationship); case NOTIFICATIONS_CLEAR: - return state.set('items', ImmutableList()).set('next', null); + return state.set('items', ImmutableList()).set('hasMore', false); case TIMELINE_DELETE: return deleteByStatus(state, action.id); + case TIMELINE_DISCONNECT: + return action.timeline === 'home' ? + state.update('items', items => items.first() ? items.unshift(null) : items) : + state; case NOTIFICATION_MARK_FOR_DELETE: return markForDelete(state, action.id, action.yes); diff --git a/app/javascript/flavours/glitch/reducers/search.js b/app/javascript/flavours/glitch/reducers/search.js index f9bf920980..dc6be97e24 100644 --- a/app/javascript/flavours/glitch/reducers/search.js +++ b/app/javascript/flavours/glitch/reducers/search.js @@ -4,7 +4,11 @@ import { SEARCH_FETCH_SUCCESS, SEARCH_SHOW, } from 'flavours/glitch/actions/search'; -import { COMPOSE_MENTION, COMPOSE_REPLY } from 'flavours/glitch/actions/compose'; +import { + COMPOSE_MENTION, + COMPOSE_REPLY, + COMPOSE_DIRECT, +} from 'flavours/glitch/actions/compose'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; const initialState = ImmutableMap({ @@ -29,6 +33,7 @@ export default function search(state = initialState, action) { return state.set('hidden', false); case COMPOSE_REPLY: case COMPOSE_MENTION: + case COMPOSE_DIRECT: return state.set('hidden', true); case SEARCH_FETCH_SUCCESS: return state.set('results', ImmutableMap({ diff --git a/app/javascript/flavours/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js index c4ae2bc978..19e400b194 100644 --- a/app/javascript/flavours/glitch/reducers/timelines.js +++ b/app/javascript/flavours/glitch/reducers/timelines.js @@ -1,14 +1,10 @@ import { - TIMELINE_REFRESH_REQUEST, - TIMELINE_REFRESH_SUCCESS, - TIMELINE_REFRESH_FAIL, TIMELINE_UPDATE, TIMELINE_DELETE, TIMELINE_EXPAND_SUCCESS, TIMELINE_EXPAND_REQUEST, TIMELINE_EXPAND_FAIL, TIMELINE_SCROLL_TOP, - TIMELINE_CONNECT, TIMELINE_DISCONNECT, } from 'flavours/glitch/actions/timelines'; import { @@ -17,42 +13,39 @@ import { ACCOUNT_UNFOLLOW_SUCCESS, } from 'flavours/glitch/actions/accounts'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; +import compareId from 'flavours/glitch/util/compare_id'; const initialState = ImmutableMap(); const initialTimeline = ImmutableMap({ unread: 0, - online: false, top: true, - loaded: false, isLoading: false, - next: false, + hasMore: true, items: ImmutableList(), }); -const normalizeTimeline = (state, timeline, statuses, next, isPartial) => { - const oldIds = state.getIn([timeline, 'items'], ImmutableList()); - const ids = ImmutableList(statuses.map(status => status.get('id'))).filter(newId => !oldIds.includes(newId)); - const wasLoaded = state.getIn([timeline, 'loaded']); - const hadNext = state.getIn([timeline, 'next']); - - return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { - mMap.set('loaded', true); - mMap.set('isLoading', false); - if (!hadNext) mMap.set('next', next); - mMap.set('items', wasLoaded ? ids.concat(oldIds) : oldIds.concat(ids)); - mMap.set('isPartial', isPartial); - })); -}; - -const appendNormalizedTimeline = (state, timeline, statuses, next) => { - const oldIds = state.getIn([timeline, 'items'], ImmutableList()); - const ids = ImmutableList(statuses.map(status => status.get('id'))).filter(newId => !oldIds.includes(newId)); - +const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial) => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { mMap.set('isLoading', false); - mMap.set('next', next); - mMap.set('items', oldIds.concat(ids)); + if (!next) mMap.set('hasMore', false); + + if (!statuses.isEmpty()) { + mMap.update('items', ImmutableList(), oldIds => { + const newIds = statuses.map(status => status.get('id')); + const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1; + const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0); + + if (firstIndex < 0) { + return (isPartial ? newIds.unshift(null) : newIds).concat(oldIds.skip(lastIndex)); + } + + return oldIds.take(firstIndex + 1).concat( + isPartial && oldIds.get(firstIndex) !== null ? newIds.unshift(null) : newIds, + oldIds.skip(lastIndex) + ); + }); + } })); }; @@ -119,16 +112,12 @@ const updateTop = (state, timeline, top) => { export default function timelines(state = initialState, action) { switch(action.type) { - case TIMELINE_REFRESH_REQUEST: case TIMELINE_EXPAND_REQUEST: return state.update(action.timeline, initialTimeline, map => map.set('isLoading', true)); - case TIMELINE_REFRESH_FAIL: case TIMELINE_EXPAND_FAIL: return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false)); - case TIMELINE_REFRESH_SUCCESS: - return normalizeTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial); case TIMELINE_EXPAND_SUCCESS: - return appendNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next); + return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial); case TIMELINE_UPDATE: return updateTimeline(state, action.timeline, fromJS(action.status), action.references); case TIMELINE_DELETE: @@ -140,10 +129,15 @@ export default function timelines(state = initialState, action) { return filterTimeline('home', state, action.relationship, action.statuses); case TIMELINE_SCROLL_TOP: return updateTop(state, action.timeline, action.top); - case TIMELINE_CONNECT: - return state.update(action.timeline, initialTimeline, map => map.set('online', true)); case TIMELINE_DISCONNECT: - return state.update(action.timeline, initialTimeline, map => map.set('online', false)); + return state.update( + action.timeline, + initialTimeline, + map => map.update( + 'items', + items => items.first() ? items.unshift(null) : items + ) + ); default: return state; } diff --git a/app/javascript/flavours/glitch/service_worker/entry.js b/app/javascript/flavours/glitch/service_worker/entry.js deleted file mode 100644 index eea4cfc3c2..0000000000 --- a/app/javascript/flavours/glitch/service_worker/entry.js +++ /dev/null @@ -1,10 +0,0 @@ -import './web_push_notifications'; - -// Cause a new version of a registered Service Worker to replace an existing one -// that is already installed, and replace the currently active worker on open pages. -self.addEventListener('install', function(event) { - event.waitUntil(self.skipWaiting()); -}); -self.addEventListener('activate', function(event) { - event.waitUntil(self.clients.claim()); -}); diff --git a/app/javascript/flavours/glitch/service_worker/web_push_notifications.js b/app/javascript/flavours/glitch/service_worker/web_push_notifications.js deleted file mode 100644 index f63cff335a..0000000000 --- a/app/javascript/flavours/glitch/service_worker/web_push_notifications.js +++ /dev/null @@ -1,159 +0,0 @@ -const MAX_NOTIFICATIONS = 5; -const GROUP_TAG = 'tag'; - -// Avoid loading intl-messageformat and dealing with locales in the ServiceWorker -const formatGroupTitle = (message, count) => message.replace('%{count}', count); - -const notify = options => - self.registration.getNotifications().then(notifications => { - if (notifications.length === MAX_NOTIFICATIONS) { - // Reached the maximum number of notifications, proceed with grouping - const group = { - title: formatGroupTitle(options.data.message, notifications.length + 1), - body: notifications - .sort((n1, n2) => n1.timestamp < n2.timestamp) - .map(notification => notification.title).join('\n'), - badge: '/badge.png', - icon: '/android-chrome-192x192.png', - tag: GROUP_TAG, - data: { - url: (new URL('/web/notifications', self.location)).href, - count: notifications.length + 1, - message: options.data.message, - }, - }; - - notifications.forEach(notification => notification.close()); - - return self.registration.showNotification(group.title, group); - } else if (notifications.length === 1 && notifications[0].tag === GROUP_TAG) { - // Already grouped, proceed with appending the notification to the group - const group = cloneNotification(notifications[0]); - - group.title = formatGroupTitle(group.data.message, group.data.count + 1); - group.body = `${options.title}\n${group.body}`; - group.data = { ...group.data, count: group.data.count + 1 }; - - return self.registration.showNotification(group.title, group); - } - - return self.registration.showNotification(options.title, options); - }); - -const handlePush = (event) => { - const options = event.data.json(); - - options.body = options.data.nsfw || options.data.content; - options.dir = options.data.dir; - options.image = options.image || undefined; // Null results in a network request (404) - options.timestamp = options.timestamp && new Date(options.timestamp); - - const expandAction = options.data.actions.find(action => action.todo === 'expand'); - - if (expandAction) { - options.actions = [expandAction]; - options.hiddenActions = options.data.actions.filter(action => action !== expandAction); - options.data.hiddenImage = options.image; - options.image = undefined; - } else { - options.actions = options.data.actions; - } - - event.waitUntil(notify(options)); -}; - -const cloneNotification = (notification) => { - const clone = { }; - - for(var k in notification) { - clone[k] = notification[k]; - } - - return clone; -}; - -const expandNotification = (notification) => { - const nextNotification = cloneNotification(notification); - - nextNotification.body = notification.data.content; - nextNotification.image = notification.data.hiddenImage; - nextNotification.actions = notification.data.actions.filter(action => action.todo !== 'expand'); - - return self.registration.showNotification(nextNotification.title, nextNotification); -}; - -const makeRequest = (notification, action) => - fetch(action.action, { - headers: { - 'Authorization': `Bearer ${notification.data.access_token}`, - 'Content-Type': 'application/json', - }, - method: action.method, - credentials: 'include', - }); - -const findBestClient = clients => { - const focusedClient = clients.find(client => client.focused); - const visibleClient = clients.find(client => client.visibilityState === 'visible'); - - return focusedClient || visibleClient || clients[0]; -}; - -const openUrl = url => - self.clients.matchAll({ type: 'window' }).then(clientList => { - if (clientList.length !== 0) { - const webClients = clientList.filter(client => /\/web\//.test(client.url)); - - if (webClients.length !== 0) { - const client = findBestClient(webClients); - const { pathname } = new URL(url); - - if (pathname.startsWith('/web/')) { - return client.focus().then(client => client.postMessage({ - type: 'navigate', - path: pathname.slice('/web/'.length - 1), - })); - } - } else if ('navigate' in clientList[0]) { // Chrome 42-48 does not support navigate - const client = findBestClient(clientList); - - return client.navigate(url).then(client => client.focus()); - } - } - - return self.clients.openWindow(url); - }); - -const removeActionFromNotification = (notification, action) => { - const actions = notification.actions.filter(act => act.action !== action.action); - const nextNotification = cloneNotification(notification); - - nextNotification.actions = actions; - - return self.registration.showNotification(nextNotification.title, nextNotification); -}; - -const handleNotificationClick = (event) => { - const reactToNotificationClick = new Promise((resolve, reject) => { - if (event.action) { - const action = event.notification.data.actions.find(({ action }) => action === event.action); - - if (action.todo === 'expand') { - resolve(expandNotification(event.notification)); - } else if (action.todo === 'request') { - resolve(makeRequest(event.notification, action) - .then(() => removeActionFromNotification(event.notification, action))); - } else { - reject(`Unknown action: ${action.todo}`); - } - } else { - event.notification.close(); - resolve(openUrl(event.notification.data.url)); - } - }); - - event.waitUntil(reactToNotificationClick); -}; - -self.addEventListener('push', handlePush); -self.addEventListener('notificationclick', handleNotificationClick); diff --git a/app/javascript/flavours/glitch/styles/about.scss b/app/javascript/flavours/glitch/styles/about.scss index 55f31266f5..c9c0e3081a 100644 --- a/app/javascript/flavours/glitch/styles/about.scss +++ b/app/javascript/flavours/glitch/styles/about.scss @@ -322,6 +322,11 @@ $small-breakpoint: 960px; border: 0; border-bottom: 1px solid rgba($ui-base-lighter-color, .6); margin: 20px 0; + + &.spacer { + height: 1px; + border: 0; + } } .container-alt { @@ -681,6 +686,54 @@ $small-breakpoint: 960px; margin-bottom: 0; } + .account { + border-bottom: 0; + padding: 0; + + &__display-name { + align-items: center; + display: flex; + margin-right: 5px; + } + + div.account__display-name { + &:hover { + .display-name strong { + text-decoration: none; + } + } + + .account__avatar { + cursor: default; + } + } + + &__avatar-wrapper { + margin-left: 0; + flex: 0 0 auto; + } + + &__avatar { + width: 44px; + height: 44px; + background-size: 44px 44px; + } + + .display-name { + font-size: 15px; + + &__account { + font-size: 14px; + } + } + } + + @media screen and (max-width: $small-breakpoint) { + .contact { + margin-top: 30px; + } + } + @media screen and (max-width: $column-breakpoint) { padding: 25px 20px; } @@ -816,6 +869,8 @@ $small-breakpoint: 960px; font-size: 16px; line-height: inherit; font-weight: inherit; + margin: 0; + padding: 0; } .column { @@ -852,8 +907,13 @@ $small-breakpoint: 960px; } &__features { + & > p { + padding-right: 60px; + } + .features-list { - margin: 40px 0 !important; + margin: 40px 0; + margin-top: 30px; } &__action { @@ -862,17 +922,11 @@ $small-breakpoint: 960px; } .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; @@ -898,6 +952,14 @@ $small-breakpoint: 960px; } } } + + @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 { diff --git a/app/javascript/flavours/glitch/styles/accounts.scss b/app/javascript/flavours/glitch/styles/accounts.scss index eff964e504..efff59ff68 100644 --- a/app/javascript/flavours/glitch/styles/accounts.scss +++ b/app/javascript/flavours/glitch/styles/accounts.scss @@ -326,6 +326,15 @@ z-index: 2; position: relative; + &.empty img { + position: absolute; + opacity: 0.2; + height: 200px; + left: 0; + bottom: 0; + pointer-events: none; + } + @media screen and (max-width: 740px) { border-radius: 0; box-shadow: none; @@ -445,8 +454,8 @@ font-size: 14px; font-weight: 500; text-align: center; - padding: 60px 0; - padding-top: 55px; + padding: 130px 0; + padding-top: 125px; margin: 0 auto; cursor: default; } diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index b077df1450..7fe5e4a197 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -141,14 +141,15 @@ } hr { - margin: 20px 0; + width: 100%; + height: 0; border: 0; - background: transparent; - border-bottom: 1px solid $ui-base-color; + border-bottom: 1px solid rgba($ui-base-lighter-color, .6); + margin: 20px 0; - &.section-break { - margin: 30px 0; - border-bottom: 2px solid $ui-base-lighter-color; + &.spacer { + height: 1px; + border: 0; } } @@ -351,34 +352,9 @@ } } -.report-note__comment { - margin-bottom: 20px; -} - -.report-note__form { - margin-bottom: 20px; - - .report-note__textarea { - box-sizing: border-box; - border: 0; - padding: 7px 4px; - margin-bottom: 10px; - font-size: 16px; - color: $inverted-text-color; - display: block; - width: 100%; - outline: 0; - font-family: inherit; - resize: vertical; - } - - .report-note__buttons { - text-align: right; - } - - .report-note__button { - margin: 0 0 5px 5px; - } +.simple_form.new_report_note, +.simple_form.new_account_moderation_note { + max-width: 100%; } .batch-form-box { @@ -406,13 +382,6 @@ } } -.batch-checkbox, -.batch-checkbox-all { - display: flex; - align-items: center; - margin-right: 5px; -} - .back-link { margin-bottom: 10px; font-size: 14px; @@ -432,7 +401,7 @@ } .log-entry { - margin-bottom: 8px; + margin-bottom: 20px; line-height: 20px; &__header { @@ -530,6 +499,31 @@ } } +a.name-tag, +.name-tag, +a.inline-name-tag, +.inline-name-tag { + text-decoration: none; + color: $secondary-text-color; + + .username { + font-weight: 500; + } + + &.suspended { + .username { + text-decoration: line-through; + color: lighten($error-red, 12%); + } + + .avatar { + filter: grayscale(100%); + opacity: 0.8; + } + } +} + +a.name-tag, .name-tag { display: flex; align-items: center; @@ -541,7 +535,46 @@ border-radius: 50%; } - .username { - font-weight: 500; + &.suspended { + .avatar { + filter: grayscale(100%); + opacity: 0.8; + } + } +} + +.speech-bubble { + margin-bottom: 20px; + border-left: 4px solid $ui-highlight-color; + + &.positive { + border-left-color: $success-green; + } + + &.negative { + border-left-color: lighten($error-red, 12%); + } + + &__bubble { + padding: 16px; + padding-left: 14px; + font-size: 15px; + line-height: 20px; + border-radius: 4px 4px 4px 0; + position: relative; + font-weight: 500; + + a { + color: $darker-text-color; + } + } + + &__owner { + padding: 8px; + padding-left: 12px; + } + + time { + color: $dark-text-color; } } diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index 84d3f6adef..dadfa6d573 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -32,7 +32,8 @@ .account__avatar-wrapper { float: left; - margin: 6px 16px 6px 6px; + margin-left: 12px; + margin-right: 12px; } .account__avatar { @@ -509,3 +510,9 @@ margin-bottom: 0; } } + +.account__header .roles { + margin-top: 20px; + margin-bottom: 20px; + padding: 0 15px; +} diff --git a/app/javascript/flavours/glitch/styles/components/drawer.scss b/app/javascript/flavours/glitch/styles/components/drawer.scss index df239dba79..0432b233ad 100644 --- a/app/javascript/flavours/glitch/styles/components/drawer.scss +++ b/app/javascript/flavours/glitch/styles/components/drawer.scss @@ -266,6 +266,40 @@ & > section { background: $ui-base-color; + 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: $darker-text-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; + } & > .hashtag { display: block; diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 6f3338605e..0fa940766d 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -848,6 +848,10 @@ } } +.load-gap { + border-bottom: 1px solid lighten($ui-base-color, 8%); +} + .missing-indicator { padding-top: 20px + 48px; @@ -894,7 +898,7 @@ width: 30px; height: 30px; font-size: 20px; - color: $inverted-text-color; + color: $darker-text-color; text-shadow: 0 0 5px black; display: flex; justify-content: center; diff --git a/app/javascript/flavours/glitch/styles/components/local_settings.scss b/app/javascript/flavours/glitch/styles/components/local_settings.scss index 9e1606e995..9cd4e1fbe1 100644 --- a/app/javascript/flavours/glitch/styles/components/local_settings.scss +++ b/app/javascript/flavours/glitch/styles/components/local_settings.scss @@ -35,8 +35,8 @@ display: block; padding: 15px 20px; color: inherit; - background: $primary-text-color; - border-bottom: 1px $ui-primary-color solid; + background: lighten($ui-secondary-color, 8%); + border-bottom: 1px $ui-secondary-color solid; cursor: pointer; text-decoration: none; outline: none; @@ -58,8 +58,7 @@ } .glitch.local-settings__navigation { - background: $simple-background-color; - color: $inverted-text-color; + background: lighten($ui-secondary-color, 8%); width: 200px; font-size: 15px; line-height: 20px; diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss index 90674612d3..5a49c07fa8 100644 --- a/app/javascript/flavours/glitch/styles/components/media.scss +++ b/app/javascript/flavours/glitch/styles/components/media.scss @@ -279,6 +279,10 @@ background: $base-shadow-color; max-width: 100%; + &:focus { + outline: 0; + } + .detailed-status & { width: 100%; height: 100%; diff --git a/app/javascript/flavours/glitch/styles/components/metadata.scss b/app/javascript/flavours/glitch/styles/components/metadata.scss index fa1a4bc342..29a6330e9c 100644 --- a/app/javascript/flavours/glitch/styles/components/metadata.scss +++ b/app/javascript/flavours/glitch/styles/components/metadata.scss @@ -2,7 +2,6 @@ font-size: 15px; line-height: 20px; overflow: hidden; - border-collapse: collapse; margin: 20px -10px -20px; border-bottom: 0; @@ -14,35 +13,36 @@ } } - tr { + dl { border-top: 1px solid lighten($ui-base-color, 8%); - text-align: center; + display: flex; } - th, td { + dt, + dd { + box-sizing: border-box; padding: 14px 20px; - vertical-align: middle; - - & > div { - max-height: 40px; - overflow-y: auto; - white-space: pre-wrap; - text-overflow: ellipsis; - } + text-align: center; + max-height: 48px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } - th { + dt { color: $darker-text-color; background: lighten($ui-base-color, 13%); - max-width: 120px; + width: 120px; + flex: 0 0 auto; + font-weight: 500; a { color: $primary-text-color; } } - td { - flex: auto; + dd { + flex: 1 1 auto; color: $primary-text-color; background: $ui-base-color; diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss index 9d5ab66a48..ac648c8680 100644 --- a/app/javascript/flavours/glitch/styles/containers.scss +++ b/app/javascript/flavours/glitch/styles/containers.scss @@ -60,7 +60,7 @@ } } -.media-gallery-standalone__body { +.media-standalone__body { overflow: hidden; } diff --git a/app/javascript/flavours/glitch/styles/footer.scss b/app/javascript/flavours/glitch/styles/footer.scss index ba2a06954e..fe2d40c0cc 100644 --- a/app/javascript/flavours/glitch/styles/footer.scss +++ b/app/javascript/flavours/glitch/styles/footer.scss @@ -4,7 +4,7 @@ font-size: 12px; color: $darker-text-color; - .domain { + .footer__domain { font-weight: 500; a { @@ -26,5 +26,13 @@ text-decoration: none; } } + + img { + margin: 0 4px; + position: relative; + bottom: -1px; + height: 18px; + vertical-align: top; + } } } diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss index 0b12742a98..f978901870 100644 --- a/app/javascript/flavours/glitch/styles/forms.scss +++ b/app/javascript/flavours/glitch/styles/forms.scss @@ -280,6 +280,11 @@ code { .actions { margin-top: 30px; display: flex; + + &.actions--top { + margin-top: 0; + margin-bottom: 30px; + } } button, @@ -563,9 +568,27 @@ code { .post-follow-actions { text-align: center; - color: $ui-primary-color; + color: $darker-text-color; div { margin-bottom: 4px; } } + +.alternative-login { + margin-top: 20px; + margin-bottom: 20px; + + h4 { + font-size: 16px; + color: $primary-text-color; + text-align: center; + margin-bottom: 20px; + border: 0; + padding: 0; + } + + .button { + display: block; + } +} diff --git a/app/javascript/flavours/glitch/styles/mastodon-light.scss b/app/javascript/flavours/glitch/styles/mastodon-light.scss new file mode 100644 index 0000000000..029d5dde23 --- /dev/null +++ b/app/javascript/flavours/glitch/styles/mastodon-light.scss @@ -0,0 +1,218 @@ +// Set variables +$ui-base-color: #d9e1e8; +$ui-base-lighter-color: darken($ui-base-color, 57%); +$ui-highlight-color: #2b90d9; +$ui-primary-color: darken($ui-highlight-color, 28%); +$ui-secondary-color: #282c37; + +$primary-text-color: black; +$base-overlay-background: $ui-base-color; + +$login-button-color: white; +$account-background-color: white; + +// Import defaults +@import 'index'; + +// Change the color of the log in button +.button { + &.button-alternative-2 { + color: $login-button-color; + } +} + +// Change columns' default background colors +.column { + > .scrollable { + background: lighten($ui-base-color, 13%); + } +} + +.status.collapsed .status__content:after { + background: linear-gradient(rgba(lighten($ui-base-color, 13%), 0), rgba(lighten($ui-base-color, 13%), 1)); +} + +.drawer__inner { + background: $ui-base-color; +} + +.drawer > .contents { + background: $ui-base-color url('data:image/svg+xml;utf8,') no-repeat bottom / 100% auto !important; + + .mastodon { + filter: contrast(75%) brightness(75%) !important; + } +} + +// Change the default appearance of the content warning button +.status__content { + + .status__content__spoiler-link { + + background: darken($ui-base-color, 30%); + + &:hover { + background: darken($ui-base-color, 35%); + text-decoration: none; + } + + } + +} + +// Change the default appearance of the action buttons +.icon-button { + + &:hover, + &:active, + &:focus { + color: darken($ui-base-color, 40%); + transition: color 200ms ease-out; + } + + &.disabled { + color: darken($ui-base-color, 30%); + } + +} + +.status { + &.status-direct { + .icon-button.disabled { + color: darken($ui-base-color, 30%); + } + } +} + +// Change the colors used in the dropdown menu +.dropdown-menu { + background: $ui-base-color; +} + +.dropdown-menu__arrow { + + &.left { + border-left-color: $ui-base-color; + } + + &.top { + border-top-color: $ui-base-color; + } + + &.bottom { + border-bottom-color: $ui-base-color; + } + + &.right { + border-right-color: $ui-base-color; + } + +} + +.dropdown-menu__item { + a { + background: $ui-base-color; + color: $ui-secondary-color; + } +} + +// Change the default color of several parts of the compose form +.composer { + + .composer--spoiler input, .composer--textarea textarea { + color: darken($ui-base-color, 80%); + + &:disabled { background: darken($simple-background-color, 10%) } + + &::placeholder { + color: darken($ui-base-color, 70%); + } + } + + strong { + color: lighten($ui-secondary-color, 65%); + } + + .composer--options { + background: darken($ui-base-color, 10%); + box-shadow: unset; + } + + .composer--options--dropdown--content--item { + color: $ui-primary-color; + + strong { + color: $ui-primary-color; + } + + } + +} + +// Change the default color used for the text in an empty column or on the error column +.empty-column-indicator, +.error-column { + color: darken($ui-base-color, 60%); +} + +// Change the default colors used on some parts of the profile pages +.activity-stream-tabs { + + background: $account-background-color; + + a { + &.active { + color: $ui-primary-color; + } + } + +} + +.activity-stream { + + .entry { + background: $account-background-color; + } + + .status.light { + + .status__content { + color: $primary-text-color; + } + + .display-name { + strong { + color: $primary-text-color; + } + } + + } + +} + +.accounts-grid { + .account-grid-card { + + .controls { + .icon-button { + color: $ui-secondary-color; + } + } + + .name { + a { + color: $primary-text-color; + } + } + + .username { + color: $ui-secondary-color; + } + + .account__header__content { + color: $primary-text-color; + } + + } +} + diff --git a/app/javascript/flavours/glitch/styles/metadata.scss b/app/javascript/flavours/glitch/styles/metadata.scss index b66cce3c1f..280848959e 100644 --- a/app/javascript/flavours/glitch/styles/metadata.scss +++ b/app/javascript/flavours/glitch/styles/metadata.scss @@ -1,43 +1,56 @@ .account__header__fields { $meta-table-border: lighten($ui-base-color, 8%); - - border-collapse: collapse; padding: 0; margin: 15px -15px -15px -15px; border: 0 none; border-top: 1px solid $meta-table-border; border-bottom: 1px solid $meta-table-border; + font-size: 14px; + line-height: 20px; - td, th { - padding: 15px; - border: 0 none; + dl { + display: flex; border-bottom: 1px solid $meta-table-border; - vertical-align: middle; } - tr:last-child { - td, th { - border-bottom: 0 none; - } - } - - td { - color: $ui-primary-color; + dt, + dd { + box-sizing: border-box; + padding: 14px; text-align: center; - width:100%; - padding-left: 0; + max-height: 48px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } - th { + dt { padding-left: 15px; - font-weight: bold; + font-weight: 500; text-align: center; - width: 94px; - color: $ui-secondary-color; + width: 120px; + flex: 0 0 auto; + color: $secondary-text-color; background: darken($ui-base-color, 8%); } + dd { + flex: 1 1 auto; + color: $darker-text-color; + } + a { - color: $classic-highlight-color; + color: $highlight-text-color; + text-decoration: none; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } + + dl:last-child { + border-bottom: 0; } } diff --git a/app/javascript/flavours/glitch/styles/rtl.scss b/app/javascript/flavours/glitch/styles/rtl.scss index 77420c84b9..e9099a9e97 100644 --- a/app/javascript/flavours/glitch/styles/rtl.scss +++ b/app/javascript/flavours/glitch/styles/rtl.scss @@ -1,6 +1,22 @@ body.rtl { direction: rtl; + .column-header > button { + text-align: right; + padding-left: 0; + padding-right: 15px; + } + + .landing-page__logo { + margin-right: 0; + margin-left: 20px; + } + + .landing-page .features-list .features-list__row .visual { + margin-left: 0; + margin-right: 15px; + } + .column-link__icon, .column-header__icon { margin-right: 0; diff --git a/app/javascript/flavours/glitch/styles/stream_entries.scss b/app/javascript/flavours/glitch/styles/stream_entries.scss index b505c15806..40963ae848 100644 --- a/app/javascript/flavours/glitch/styles/stream_entries.scss +++ b/app/javascript/flavours/glitch/styles/stream_entries.scss @@ -6,7 +6,8 @@ background: $simple-background-color; .detailed-status.light, - .status.light { + .status.light, + .more.light { border-bottom: 1px solid $ui-secondary-color; animation: none; } @@ -65,6 +66,10 @@ } } + .media-gallery__gifv__label { + bottom: 9px; + } + .status.light { padding: 14px 14px 14px (48px + 14px * 2); position: relative; @@ -145,10 +150,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%); } } } @@ -215,10 +220,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%); } } } @@ -261,16 +266,8 @@ } .media-spoiler { - background: $ui-primary-color; - color: $inverted-text-color; - transition: all 100ms linear; - - &:hover, - &:active, - &:focus { - background: darken($ui-primary-color, 5%); - color: unset; - } + background: $ui-base-color; + color: $darker-text-color; } .pre-header { @@ -299,6 +296,17 @@ text-decoration: underline; } } + + .more { + color: $darker-text-color; + display: block; + padding: 14px; + text-align: center; + + &:not(:hover) { + text-decoration: none; + } + } } .embed { diff --git a/app/javascript/flavours/glitch/styles/tables.scss b/app/javascript/flavours/glitch/styles/tables.scss index c12d84f1c0..982bfd9900 100644 --- a/app/javascript/flavours/glitch/styles/tables.scss +++ b/app/javascript/flavours/glitch/styles/tables.scss @@ -1,3 +1,9 @@ +@keyframes Swag { + 0% { background-position: 0% 0%; } + 50% { background-position: 100% 0%; } + 100% { background-position: 200% 0%; } +} + .table { width: 100%; max-width: 100%; @@ -11,6 +17,7 @@ vertical-align: top; border-top: 1px solid $ui-base-color; text-align: left; + background: darken($ui-base-color, 4%); } & > thead > tr > th { @@ -48,9 +55,38 @@ } } - &.inline-table > tbody > tr:nth-child(odd) > td, - &.inline-table > tbody > tr:nth-child(odd) > th { - background: transparent; + &.inline-table { + & > tbody > tr:nth-child(odd) { + & > td, + & > th { + background: transparent; + } + } + + & > tbody > tr:first-child { + & > td, + & > th { + border-top: 0; + } + } + } + + &.batch-table { + & > thead > tr > th { + background: $ui-base-color; + border-top: 1px solid darken($ui-base-color, 8%); + border-bottom: 1px solid darken($ui-base-color, 8%); + + &:first-child { + border-radius: 4px 0 0; + border-left: 1px solid darken($ui-base-color, 8%); + } + + &:last-child { + border-radius: 0 4px 0 0; + border-right: 1px solid darken($ui-base-color, 8%); + } + } } } @@ -63,6 +99,13 @@ samp { font-family: 'mastodon-font-monospace', monospace; } +button.table-action-link { + background: transparent; + border: 0; + font: inherit; +} + +button.table-action-link, a.table-action-link { text-decoration: none; display: inline-block; @@ -79,4 +122,82 @@ a.table-action-link { font-weight: 400; margin-right: 5px; } + + &:first-child { + padding-left: 0; + } +} + +.batch-table { + &__toolbar, + &__row { + display: flex; + + &__select { + box-sizing: border-box; + padding: 8px 16px; + cursor: pointer; + min-height: 100%; + + input { + margin-top: 8px; + } + } + + &__actions, + &__content { + padding: 8px 0; + padding-right: 16px; + flex: 1 1 auto; + } + } + + &__toolbar { + border: 1px solid darken($ui-base-color, 8%); + background: $ui-base-color; + border-radius: 4px 0 0; + height: 47px; + align-items: center; + + &__actions { + text-align: right; + padding-right: 16px - 5px; + } + } + + &__row { + border: 1px solid darken($ui-base-color, 8%); + border-top: 0; + background: darken($ui-base-color, 4%); + + &:hover { + background: darken($ui-base-color, 2%); + } + + &:nth-child(even) { + background: $ui-base-color; + + &:hover { + background: lighten($ui-base-color, 2%); + } + } + + &__content { + padding-top: 12px; + padding-bottom: 16px; + } + } + + .status__content { + padding-top: 0; + + strong { + font-weight: 700; + background: linear-gradient(to right, orange , yellow, green, cyan, blue, violet,orange , yellow, green, cyan, blue, violet); + background-size: 200% 100%; + background-clip: text; + color: transparent; + animation: Swag 2s linear 0s infinite; + } + } } diff --git a/app/javascript/flavours/glitch/util/compare_id.js b/app/javascript/flavours/glitch/util/compare_id.js new file mode 100644 index 0000000000..aaff66481a --- /dev/null +++ b/app/javascript/flavours/glitch/util/compare_id.js @@ -0,0 +1,10 @@ +export default function compareId(id1, id2) { + if (id1 === id2) { + return 0; + } + if (id1.length === id2.length) { + return id1 > id2 ? 1 : -1; + } else { + return id1.length > id2.length ? 1 : -1; + } +} diff --git a/app/javascript/flavours/glitch/util/html.js b/app/javascript/flavours/glitch/util/html.js new file mode 100644 index 0000000000..0b646ce58f --- /dev/null +++ b/app/javascript/flavours/glitch/util/html.js @@ -0,0 +1,6 @@ +export const unescapeHTML = (html) => { + const wrapper = document.createElement('div'); + html = html.replace(/||\n/g, ' '); + wrapper.innerHTML = html; + return wrapper.textContent; +}; diff --git a/app/javascript/flavours/glitch/util/resize_image.js b/app/javascript/flavours/glitch/util/resize_image.js new file mode 100644 index 0000000000..279a858cad --- /dev/null +++ b/app/javascript/flavours/glitch/util/resize_image.js @@ -0,0 +1,116 @@ +import EXIF from 'exif-js'; + +const MAX_IMAGE_DIMENSION = 1280; + +const getImageUrl = inputFile => new Promise((resolve, reject) => { + if (window.URL && URL.createObjectURL) { + try { + resolve(URL.createObjectURL(inputFile)); + } catch (error) { + reject(error); + } + return; + } + + const reader = new FileReader(); + reader.onerror = (...args) => reject(...args); + reader.onload = ({ target }) => resolve(target.result); + + reader.readAsDataURL(inputFile); +}); + +const loadImage = inputFile => new Promise((resolve, reject) => { + getImageUrl(inputFile).then(url => { + const img = new Image(); + + img.onerror = (...args) => reject(...args); + img.onload = () => resolve(img); + + img.src = url; + }).catch(reject); +}); + +const getOrientation = (img, type = 'image/png') => new Promise(resolve => { + if (type !== 'image/jpeg') { + resolve(1); + return; + } + + EXIF.getData(img, () => { + const orientation = EXIF.getTag(img, 'Orientation'); + resolve(orientation); + }); +}); + +const processImage = (img, { width, height, orientation, type = 'image/png' }) => new Promise(resolve => { + const canvas = document.createElement('canvas'); + + if (4 < orientation && orientation < 9) { + canvas.width = height; + canvas.height = width; + } else { + canvas.width = width; + canvas.height = height; + } + + const context = canvas.getContext('2d'); + + switch (orientation) { + case 2: context.transform(-1, 0, 0, 1, width, 0); break; + case 3: context.transform(-1, 0, 0, -1, width, height); break; + case 4: context.transform(1, 0, 0, -1, 0, height); break; + case 5: context.transform(0, 1, 1, 0, 0, 0); break; + case 6: context.transform(0, 1, -1, 0, height, 0); break; + case 7: context.transform(0, -1, -1, 0, height, width); break; + case 8: context.transform(0, -1, 1, 0, 0, width); break; + } + + context.drawImage(img, 0, 0, width, height); + + canvas.toBlob(resolve, type); +}); + +const resizeImage = (img, type = 'image/png') => new Promise((resolve, reject) => { + const { width, height } = img; + + let newWidth, newHeight; + + if (width > height) { + newHeight = height * MAX_IMAGE_DIMENSION / width; + newWidth = MAX_IMAGE_DIMENSION; + } else if (height > width) { + newWidth = width * MAX_IMAGE_DIMENSION / height; + newHeight = MAX_IMAGE_DIMENSION; + } else { + newWidth = MAX_IMAGE_DIMENSION; + newHeight = MAX_IMAGE_DIMENSION; + } + + getOrientation(img, type) + .then(orientation => processImage(img, { + width: newWidth, + height: newHeight, + orientation, + type, + })) + .then(resolve) + .catch(reject); +}); + +export default inputFile => new Promise((resolve, reject) => { + if (!inputFile.type.match(/image.*/) || inputFile.type === 'image/gif') { + resolve(inputFile); + return; + } + + loadImage(inputFile).then(img => { + if (img.width < MAX_IMAGE_DIMENSION && img.height < MAX_IMAGE_DIMENSION) { + resolve(inputFile); + return; + } + + resizeImage(img, inputFile.type) + .then(resolve) + .catch(() => resolve(inputFile)); + }).catch(reject); +}); diff --git a/app/javascript/flavours/glitch/util/stream.js b/app/javascript/flavours/glitch/util/stream.js index 36c68ffc5b..9928d0dd76 100644 --- a/app/javascript/flavours/glitch/util/stream.js +++ b/app/javascript/flavours/glitch/util/stream.js @@ -1,21 +1,24 @@ import WebSocketClient from 'websocket.js'; -export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onConnect() {}, onDisconnect() {}, onReceive() {} })) { +const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max)); + +export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onDisconnect() {}, onReceive() {} })) { return (dispatch, getState) => { const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']); const accessToken = getState().getIn(['meta', 'access_token']); - const { onConnect, onDisconnect, onReceive } = callbacks(dispatch, getState); + const { onDisconnect, onReceive } = callbacks(dispatch, getState); + let polling = null; const setupPolling = () => { - polling = setInterval(() => { - pollingRefresh(dispatch); - }, 20000); + pollingRefresh(dispatch, () => { + polling = setTimeout(() => setupPolling(), 20000 + randomIntUpTo(20000)); + }); }; const clearPolling = () => { if (polling) { - clearInterval(polling); + clearTimeout(polling); polling = null; } }; @@ -25,13 +28,13 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({ if (pollingRefresh) { clearPolling(); } - onConnect(); }, disconnected () { if (pollingRefresh) { - setupPolling(); + polling = setTimeout(() => setupPolling(), randomIntUpTo(40000)); } + onDisconnect(); }, @@ -44,7 +47,6 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({ clearPolling(); pollingRefresh(dispatch); } - onConnect(); }, }); @@ -53,6 +55,7 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({ if (subscription) { subscription.close(); } + clearPolling(); }; @@ -62,7 +65,13 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({ export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) { - const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`); + const params = [ `stream=${stream}` ]; + + if (accessToken !== null) { + params.push(`access_token=${accessToken}`); + } + + const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`); ws.onopen = connected; ws.onmessage = e => received(JSON.parse(e.data)); diff --git a/app/javascript/mastodon/actions/columns.js b/app/javascript/mastodon/actions/columns.js index bcb0cdf983..f550e6c48d 100644 --- a/app/javascript/mastodon/actions/columns.js +++ b/app/javascript/mastodon/actions/columns.js @@ -1,8 +1,9 @@ import { saveSettings } from './settings'; -export const COLUMN_ADD = 'COLUMN_ADD'; -export const COLUMN_REMOVE = 'COLUMN_REMOVE'; -export const COLUMN_MOVE = 'COLUMN_MOVE'; +export const COLUMN_ADD = 'COLUMN_ADD'; +export const COLUMN_REMOVE = 'COLUMN_REMOVE'; +export const COLUMN_MOVE = 'COLUMN_MOVE'; +export const COLUMN_PARAMS_CHANGE = 'COLUMN_PARAMS_CHANGE'; export function addColumn(id, params) { return dispatch => { @@ -38,3 +39,15 @@ export function moveColumn(uuid, direction) { dispatch(saveSettings()); }; }; + +export function changeColumnParams(uuid, params) { + return dispatch => { + dispatch({ + type: COLUMN_PARAMS_CHANGE, + uuid, + params, + }); + + dispatch(saveSettings()); + }; +} diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 5f1274fab6..c015d3a994 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -1,20 +1,29 @@ import escapeTextContentForBrowser from 'escape-html'; import emojify from '../../features/emoji/emoji'; +import { unescapeHTML } from '../../utils/html'; const domParser = new DOMParser(); +const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { + obj[`:${emoji.shortcode}:`] = emoji; + return obj; +}, {}); + export function normalizeAccount(account) { account = { ...account }; + const emojiMap = makeEmojiMap(account); const displayName = account.display_name.length === 0 ? account.username : account.display_name; - account.display_name_html = emojify(escapeTextContentForBrowser(displayName)); - account.note_emojified = emojify(account.note); + + account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap); + account.note_emojified = emojify(account.note, emojiMap); if (account.fields) { account.fields = account.fields.map(pair => ({ ...pair, name_emojified: emojify(escapeTextContentForBrowser(pair.name)), - value_emojified: emojify(pair.value), + value_emojified: emojify(pair.value, emojiMap), + value_plain: unescapeHTML(pair.value), })); } @@ -42,11 +51,7 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.hidden = normalOldStatus.get('hidden'); } else { const searchContent = [status.spoiler_text, status.content].join('\n\n').replace(//g, '\n').replace(/<\/p>/g, '\n\n'); - - const emojiMap = normalStatus.emojis.reduce((obj, emoji) => { - obj[`:${emoji.shortcode}:`] = emoji; - return obj; - }, {}); + const emojiMap = makeEmojiMap(normalStatus); normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 7aa070f569..3f95f6667f 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -8,6 +8,7 @@ import { importFetchedStatuses, } from './importer'; import { defineMessages } from 'react-intl'; +import { unescapeHTML } from '../utils/html'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; @@ -21,6 +22,7 @@ export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, + group: { id: 'notifications.group', defaultMessage: '{count} notifications' }, }); const fetchRelatedRelationships = (dispatch, notifications) => { @@ -31,13 +33,6 @@ const fetchRelatedRelationships = (dispatch, notifications) => { } }; -const unescapeHTML = (html) => { - const wrapper = document.createElement('div'); - html = html.replace(/||\n/g, ' '); - wrapper.innerHTML = html; - return wrapper.textContent; -}; - export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true); @@ -82,9 +77,14 @@ export function updateNotifications(notification, intlMessages, intlLocale) { const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); -export function expandNotifications({ maxId } = {}) { +const noOp = () => {}; + +export function expandNotifications({ maxId } = {}, done = noOp) { return (dispatch, getState) => { - if (getState().getIn(['notifications', 'isLoading'])) { + const notifications = getState().get('notifications'); + + if (notifications.get('isLoading')) { + done(); return; } @@ -93,6 +93,10 @@ export function expandNotifications({ maxId } = {}) { exclude_types: excludeTypesFromSettings(getState()), }; + if (!maxId && notifications.get('items').size > 0) { + params.since_id = notifications.getIn(['items', 0]); + } + dispatch(expandNotificationsRequest()); api(getState).get('/api/v1/notifications', { params }).then(response => { @@ -103,8 +107,10 @@ export function expandNotifications({ maxId } = {}) { dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null)); fetchRelatedRelationships(dispatch, response.data); + done(); }).catch(error => { dispatch(expandNotificationsFail(error)); + done(); }); }; }; diff --git a/app/javascript/mastodon/actions/push_notifications/registerer.js b/app/javascript/mastodon/actions/push_notifications/registerer.js index 82fe4519a2..b0f42b6a20 100644 --- a/app/javascript/mastodon/actions/push_notifications/registerer.js +++ b/app/javascript/mastodon/actions/push_notifications/registerer.js @@ -51,13 +51,6 @@ export function register () { return (dispatch, getState) => { dispatch(setBrowserSupport(supportsPushNotifications)); - if (me && !pushNotificationsSetting.get(me)) { - const alerts = getState().getIn(['push_notifications', 'alerts']); - if (alerts) { - pushNotificationsSetting.set(me, { alerts: alerts }); - } - } - if (supportsPushNotifications) { if (!getApplicationServerKey()) { console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.'); diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js index 882c1709e9..b670d25c3c 100644 --- a/app/javascript/mastodon/actions/search.js +++ b/app/javascript/mastodon/actions/search.js @@ -33,7 +33,7 @@ export function submitSearch() { dispatch(fetchSearchRequest()); - api(getState).get('/api/v1/search', { + api(getState).get('/api/v2/search', { params: { q: value, resolve: true, diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 14215ab6d5..f56853bffb 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -36,15 +36,13 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null) }); } -function refreshHomeTimelineAndNotification (dispatch) { - dispatch(expandHomeTimeline()); - dispatch(expandNotifications()); -} +const refreshHomeTimelineAndNotification = (dispatch, done) => { + dispatch(expandHomeTimeline({}, () => dispatch(expandNotifications({}, done)))); +}; -export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); -export const connectCommunityStream = () => connectTimelineStream('community', 'public:local'); -export const connectMediaStream = () => connectTimelineStream('community', 'public:local'); -export const connectPublicStream = () => connectTimelineStream('public', 'public'); -export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`); -export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); -export const connectListStream = (id) => connectTimelineStream(`list:${id}`, `list&list=${id}`); +export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); +export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`); +export const connectPublicStream = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`); +export const connectHashtagStream = tag => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`); +export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); +export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`); diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index eca847ee73..11a199db6a 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -1,6 +1,6 @@ import { importFetchedStatus, importFetchedStatuses } from './importer'; import api, { getLinks } from '../api'; -import { Map as ImmutableMap } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE'; @@ -13,21 +13,9 @@ export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; -export const TIMELINE_CONTEXT_UPDATE = 'CONTEXT_UPDATE'; - export function updateTimeline(timeline, status) { return (dispatch, getState) => { const references = status.reblog ? getState().get('statuses').filter((item, itemId) => (itemId === status.reblog.id || item.get('reblog') === status.reblog.id)).map((_, itemId) => itemId) : []; - const parents = []; - - if (status.in_reply_to_id) { - let parent = getState().getIn(['statuses', status.in_reply_to_id]); - - while (parent && parent.get('in_reply_to_id')) { - parents.push(parent.get('id')); - parent = getState().getIn(['statuses', parent.get('in_reply_to_id')]); - } - } dispatch(importFetchedStatus(status)); @@ -37,14 +25,6 @@ export function updateTimeline(timeline, status) { status, references, }); - - if (parents.length > 0) { - dispatch({ - type: TIMELINE_CONTEXT_UPDATE, - status, - references: parents, - }); - } }; }; @@ -64,35 +44,44 @@ export function deleteFromTimelines(id) { }; }; -export function expandTimeline(timelineId, path, params = {}) { +const noOp = () => {}; + +export function expandTimeline(timelineId, path, params = {}, done = noOp) { return (dispatch, getState) => { const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); if (timeline.get('isLoading')) { + done(); return; } + if (!params.max_id && timeline.get('items', ImmutableList()).size > 0) { + params.since_id = timeline.getIn(['items', 0]); + } + dispatch(expandTimelineRequest(timelineId)); api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206)); + done(); }).catch(error => { dispatch(expandTimelineFail(timelineId, error)); + done(); }); }; }; -export const expandHomeTimeline = ({ maxId } = {}) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }); -export const expandPublicTimeline = ({ maxId } = {}) => expandTimeline('public', '/api/v1/timelines/public', { max_id: maxId }); -export const expandCommunityTimeline = ({ maxId } = {}) => expandTimeline('community', '/api/v1/timelines/public', { local: true, max_id: maxId }); -export const expandDirectTimeline = ({ maxId } = {}) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }); -export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); +export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); +export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done); +export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); +export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); +export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); -export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true }); -export const expandHashtagTimeline = (hashtag, { maxId } = {}) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId }); -export const expandListTimeline = (id, { maxId } = {}) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }); +export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true }); +export const expandHashtagTimeline = (hashtag, { maxId } = {}, done = noOp) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId }, done); +export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); export function expandTimelineRequest(timeline) { return { diff --git a/app/javascript/mastodon/actions/trends.js b/app/javascript/mastodon/actions/trends.js new file mode 100644 index 0000000000..853e4f60ae --- /dev/null +++ b/app/javascript/mastodon/actions/trends.js @@ -0,0 +1,32 @@ +import api from '../api'; + +export const TRENDS_FETCH_REQUEST = 'TRENDS_FETCH_REQUEST'; +export const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS'; +export const TRENDS_FETCH_FAIL = 'TRENDS_FETCH_FAIL'; + +export const fetchTrends = () => (dispatch, getState) => { + dispatch(fetchTrendsRequest()); + + api(getState) + .get('/api/v1/trends') + .then(({ data }) => dispatch(fetchTrendsSuccess(data))) + .catch(err => dispatch(fetchTrendsFail(err))); +}; + +export const fetchTrendsRequest = () => ({ + type: TRENDS_FETCH_REQUEST, + skipLoading: true, +}); + +export const fetchTrendsSuccess = trends => ({ + type: TRENDS_FETCH_SUCCESS, + trends, + skipLoading: true, +}); + +export const fetchTrendsFail = error => ({ + type: TRENDS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, +}); diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index 982d34718e..0a6e7c6272 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -43,6 +43,7 @@ class DropdownMenu extends React.PureComponent { componentDidMount () { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + if (this.focusedItem) this.focusedItem.focus(); this.setState({ mounted: true }); } @@ -55,6 +56,46 @@ class DropdownMenu extends React.PureComponent { this.node = c; } + setFocusRef = c => { + this.focusedItem = c; + } + + handleKeyDown = e => { + const items = Array.from(this.node.getElementsByTagName('a')); + const index = items.indexOf(e.currentTarget); + let element; + + switch(e.key) { + case 'Enter': + this.handleClick(e); + break; + case 'ArrowDown': + element = items[index+1]; + if (element) { + element.focus(); + } + break; + case 'ArrowUp': + element = items[index-1]; + if (element) { + element.focus(); + } + break; + case 'Home': + element = items[0]; + if (element) { + element.focus(); + } + break; + case 'End': + element = items[items.length-1]; + if (element) { + element.focus(); + } + break; + } + } + handleClick = e => { const i = Number(e.currentTarget.getAttribute('data-index')); const { action, to } = this.props.items[i]; @@ -79,7 +120,7 @@ class DropdownMenu extends React.PureComponent { return ( - + {text} @@ -156,9 +197,6 @@ export default class Dropdown extends React.PureComponent { handleKeyDown = e => { switch(e.key) { - case 'Enter': - this.handleClick(e); - break; case 'Escape': this.handleClose(); break; diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index 13e1fcc524..7c4444e0e8 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -20,6 +20,7 @@ class Item extends React.PureComponent { index: PropTypes.number.isRequired, size: PropTypes.number.isRequired, onClick: PropTypes.func.isRequired, + displayWidth: PropTypes.number, }; static defaultProps = { @@ -58,7 +59,7 @@ class Item extends React.PureComponent { } render () { - const { attachment, index, size, standalone } = this.props; + const { attachment, index, size, standalone, displayWidth } = this.props; let width = 50; let height = 100; @@ -121,7 +122,7 @@ class Item extends React.PureComponent { 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 ? `${displayWidth * (width / 100)}px` : null; const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0; const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0; @@ -263,9 +264,9 @@ export default class MediaGallery extends React.PureComponent { const size = media.take(4).size; if (this.isStandaloneEligible()) { - children = ; + children = ; } else { - children = media.take(4).map((attachment, i) => ); + children = media.take(4).map((attachment, i) => ); } } diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js index 7cdd63910e..4b433f32c3 100644 --- a/app/javascript/mastodon/components/scrollable_list.js +++ b/app/javascript/mastodon/components/scrollable_list.js @@ -25,6 +25,7 @@ export default class ScrollableList extends PureComponent { isLoading: PropTypes.bool, hasMore: PropTypes.bool, prepend: PropTypes.node, + alwaysPrepend: PropTypes.bool, emptyMessage: PropTypes.node, children: PropTypes.node, }; @@ -35,7 +36,6 @@ export default class ScrollableList extends PureComponent { state = { fullscreen: null, - mouseOver: false, }; intersectionObserverWrapper = new IntersectionObserverWrapper(); @@ -72,7 +72,7 @@ export default class ScrollableList extends PureComponent { const someItemInserted = React.Children.count(prevProps.children) > 0 && React.Children.count(prevProps.children) < React.Children.count(this.props.children) && this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); - if (someItemInserted && this.node.scrollTop > 0 || (this.state.mouseOver && !prevProps.isLoading)) { + if (someItemInserted && this.node.scrollTop > 0) { return this.node.scrollHeight - this.node.scrollTop; } else { return null; @@ -140,16 +140,8 @@ export default class ScrollableList extends PureComponent { this.props.onLoadMore(); } - handleMouseEnter = () => { - this.setState({ mouseOver: true }); - } - - handleMouseLeave = () => { - this.setState({ mouseOver: false }); - } - render () { - const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage, onLoadMore } = this.props; + const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props; const { fullscreen } = this.state; const childrenCount = React.Children.count(children); @@ -158,7 +150,7 @@ export default class ScrollableList extends PureComponent { if (isLoading || childrenCount > 0 || !emptyMessage) { scrollableArea = ( - + {prepend} @@ -181,8 +173,12 @@ export default class ScrollableList extends PureComponent { ); } else { scrollableArea = ( - - {emptyMessage} + + {alwaysPrepend && prepend} + + + {emptyMessage} + ); } diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 402d558c4c..fd08ff3b7c 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -84,8 +84,8 @@ export default class Status extends ImmutablePureComponent { return ; } - handleOpenVideo = startTime => { - this.props.onOpenVideo(this._properStatus().getIn(['media_attachments', 0]), startTime); + handleOpenVideo = (media, startTime) => { + this.props.onOpenVideo(media, startTime); } handleHotkeyReply = e => { @@ -206,7 +206,7 @@ export default class Status extends ImmutablePureComponent { ); } else { media = ( - + {Component => } ); diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js index 0c971ceb00..1c34d06408 100644 --- a/app/javascript/mastodon/components/status_list.js +++ b/app/javascript/mastodon/components/status_list.js @@ -24,6 +24,7 @@ export default class StatusList extends ImmutablePureComponent { hasMore: PropTypes.bool, prepend: PropTypes.node, emptyMessage: PropTypes.node, + alwaysPrepend: PropTypes.bool, }; static defaultProps = { diff --git a/app/javascript/mastodon/containers/card_container.js b/app/javascript/mastodon/containers/card_container.js deleted file mode 100644 index 11b9f88d4c..0000000000 --- a/app/javascript/mastodon/containers/card_container.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Card from '../features/status/components/card'; -import { fromJS } from 'immutable'; - -export default class CardContainer extends React.PureComponent { - - static propTypes = { - locale: PropTypes.string, - card: PropTypes.array.isRequired, - }; - - render () { - const { card, ...props } = this.props; - return ; - } - -} diff --git a/app/javascript/mastodon/containers/media_container.js b/app/javascript/mastodon/containers/media_container.js new file mode 100644 index 0000000000..1700fba05a --- /dev/null +++ b/app/javascript/mastodon/containers/media_container.js @@ -0,0 +1,90 @@ +import React, { PureComponent, Fragment } from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import { IntlProvider, addLocaleData } from 'react-intl'; +import { getLocale } from '../locales'; +import MediaGallery from '../components/media_gallery'; +import Video from '../features/video'; +import Card from '../features/status/components/card'; +import ModalRoot from '../components/modal_root'; +import MediaModal from '../features/ui/components/media_modal'; +import { List as ImmutableList, fromJS } from 'immutable'; + +const { localeData, messages } = getLocale(); +addLocaleData(localeData); + +const MEDIA_COMPONENTS = { MediaGallery, Video, Card }; + +export default class MediaContainer extends PureComponent { + + static propTypes = { + locale: PropTypes.string.isRequired, + components: PropTypes.object.isRequired, + }; + + state = { + media: null, + index: null, + time: null, + }; + + handleOpenMedia = (media, index) => { + document.body.classList.add('media-standalone__body'); + this.setState({ media, index }); + } + + handleOpenVideo = (video, time) => { + const media = ImmutableList([video]); + + document.body.classList.add('media-standalone__body'); + this.setState({ media, time }); + } + + handleCloseMedia = () => { + document.body.classList.remove('media-standalone__body'); + this.setState({ media: null, index: null, time: null }); + } + + render () { + const { locale, components } = this.props; + + return ( + + + {[].map.call(components, (component, i) => { + const componentName = component.getAttribute('data-component'); + const Component = MEDIA_COMPONENTS[componentName]; + const { media, card, ...props } = JSON.parse(component.getAttribute('data-props')); + + Object.assign(props, { + ...(media ? { media: fromJS(media) } : {}), + ...(card ? { card: fromJS(card) } : {}), + + ...(componentName === 'Video' ? { + onOpenVideo: this.handleOpenVideo, + } : { + onOpenMedia: this.handleOpenMedia, + }), + }); + + return ReactDOM.createPortal( + , + component, + ); + })} + + {this.state.media && ( + + )} + + + + ); + } + +} diff --git a/app/javascript/mastodon/containers/media_galleries_container.js b/app/javascript/mastodon/containers/media_galleries_container.js deleted file mode 100644 index d77bd688bf..0000000000 --- a/app/javascript/mastodon/containers/media_galleries_container.js +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; -import { IntlProvider, addLocaleData } from 'react-intl'; -import { getLocale } from '../locales'; -import MediaGallery from '../components/media_gallery'; -import ModalRoot from '../components/modal_root'; -import MediaModal from '../features/ui/components/media_modal'; -import { fromJS } from 'immutable'; - -const { localeData, messages } = getLocale(); -addLocaleData(localeData); - -export default class MediaGalleriesContainer extends React.PureComponent { - - static propTypes = { - locale: PropTypes.string.isRequired, - galleries: PropTypes.object.isRequired, - }; - - state = { - media: null, - index: null, - }; - - handleOpenMedia = (media, index) => { - document.body.classList.add('media-gallery-standalone__body'); - this.setState({ media, index }); - } - - handleCloseMedia = () => { - document.body.classList.remove('media-gallery-standalone__body'); - this.setState({ media: null, index: null }); - } - - render () { - const { locale, galleries } = this.props; - - return ( - - - {[].map.call(galleries, gallery => { - const { media, ...props } = JSON.parse(gallery.getAttribute('data-props')); - - return ReactDOM.createPortal( - , - gallery - ); - })} - - {this.state.media === null || this.state.index === null ? null : ( - - )} - - - - ); - } - -} diff --git a/app/javascript/mastodon/containers/timeline_container.js b/app/javascript/mastodon/containers/timeline_container.js index 8719bb5c9e..a1a4bd024b 100644 --- a/app/javascript/mastodon/containers/timeline_container.js +++ b/app/javascript/mastodon/containers/timeline_container.js @@ -1,4 +1,5 @@ -import React from 'react'; +import React, { Fragment } from 'react'; +import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import PropTypes from 'prop-types'; import configureStore from '../store/configureStore'; @@ -8,6 +9,7 @@ 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 ModalContainer from '../features/ui/containers/modal_container'; import initialState from '../initial_state'; const { localeData, messages } = getLocale(); @@ -47,7 +49,13 @@ export default class TimelineContainer extends React.PureComponent { return ( - {timeline} + + {timeline} + {ReactDOM.createPortal( + , + document.getElementById('modal-container'), + )} + ); diff --git a/app/javascript/mastodon/containers/video_container.js b/app/javascript/mastodon/containers/video_container.js deleted file mode 100644 index 2fd353096d..0000000000 --- a/app/javascript/mastodon/containers/video_container.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { IntlProvider, addLocaleData } from 'react-intl'; -import { getLocale } from '../locales'; -import Video from '../features/video'; - -const { localeData, messages } = getLocale(); -addLocaleData(localeData); - -export default class VideoContainer extends React.PureComponent { - - static propTypes = { - locale: PropTypes.string.isRequired, - }; - - render () { - const { locale, ...props } = this.props; - - return ( - - - - ); - } - -} diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index bbf886dca8..7358053da0 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -131,6 +131,7 @@ export default class Header extends ImmutablePureComponent { const content = { __html: account.get('note_emojified') }; const displayNameHtml = { __html: account.get('display_name_html') }; const fields = account.get('fields'); + const badge = account.get('bot') ? () : null; return ( @@ -139,19 +140,20 @@ export default class Header extends ImmutablePureComponent { @{account.get('acct')} {lockedIcon} + + {badge} + {fields.size > 0 && ( - - - {fields.map((pair, i) => ( - - - - - ))} - - + + {fields.map((pair, i) => ( + + + + + ))} + )} {info} diff --git a/app/javascript/mastodon/features/community_timeline/components/section_headline.js b/app/javascript/mastodon/features/community_timeline/components/section_headline.js new file mode 100644 index 0000000000..c7176d04b8 --- /dev/null +++ b/app/javascript/mastodon/features/community_timeline/components/section_headline.js @@ -0,0 +1,59 @@ +import PropTypes from 'prop-types'; +import React, { Component, Fragment } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { NavLink } from 'react-router-dom'; + +export default class SectionHeadline extends Component { + + static propTypes = { + timelineId: PropTypes.string.isRequired, + to: PropTypes.string.isRequired, + pinned: PropTypes.bool.isRequired, + onlyMedia: PropTypes.bool.isRequired, + onClick: PropTypes.func, + }; + + shouldComponentUpdate (nextProps) { + return ( + this.props.onlyMedia !== nextProps.onlyMedia || + this.props.pinned !== nextProps.pinned || + this.props.to !== nextProps.to || + this.props.timelineId !== nextProps.timelineId + ); + } + + handleClick = e => { + const { onClick } = this.props; + + if (typeof onClick === 'function') { + e.preventDefault(); + + onClick.call(this, e); + } + } + + render () { + const { timelineId, to, pinned, onlyMedia } = this.props; + + return ( + + {pinned ? ( + + + + + + + + + ) : ( + + + + + )} + + ); + } + +} diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js index 870474ed58..d375edbd51 100644 --- a/app/javascript/mastodon/features/community_timeline/index.js +++ b/app/javascript/mastodon/features/community_timeline/index.js @@ -1,42 +1,48 @@ import React from 'react'; import { connect } from 'react-redux'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; import StatusListContainer from '../ui/containers/status_list_container'; import Column from '../../components/column'; import ColumnHeader from '../../components/column_header'; import { expandCommunityTimeline } from '../../actions/timelines'; -import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { addColumn, removeColumn, moveColumn, changeColumnParams } from '../../actions/columns'; import ColumnSettingsContainer from './containers/column_settings_container'; +import SectionHeadline from './components/section_headline'; import { connectCommunityStream } from '../../actions/streaming'; const messages = defineMessages({ title: { id: 'column.community', defaultMessage: 'Local timeline' }, }); -const mapStateToProps = state => ({ - hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0, +const mapStateToProps = (state, { onlyMedia }) => ({ + hasUnread: state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`, 'unread']) > 0, }); @connect(mapStateToProps) @injectIntl export default class CommunityTimeline extends React.PureComponent { + static defaultProps = { + onlyMedia: false, + }; + static propTypes = { dispatch: PropTypes.func.isRequired, columnId: PropTypes.string, intl: PropTypes.object.isRequired, hasUnread: PropTypes.bool, multiColumn: PropTypes.bool, + onlyMedia: PropTypes.bool, }; handlePin = () => { - const { columnId, dispatch } = this.props; + const { columnId, dispatch, onlyMedia } = this.props; if (columnId) { dispatch(removeColumn(columnId)); } else { - dispatch(addColumn('COMMUNITY', {})); + dispatch(addColumn('COMMUNITY', { other: { onlyMedia } })); } } @@ -50,10 +56,20 @@ export default class CommunityTimeline extends React.PureComponent { } componentDidMount () { - const { dispatch } = this.props; + const { dispatch, onlyMedia } = this.props; - dispatch(expandCommunityTimeline()); - this.disconnect = dispatch(connectCommunityStream()); + dispatch(expandCommunityTimeline({ onlyMedia })); + this.disconnect = dispatch(connectCommunityStream({ onlyMedia })); + } + + componentDidUpdate (prevProps) { + if (prevProps.onlyMedia !== this.props.onlyMedia) { + const { dispatch, onlyMedia } = this.props; + + this.disconnect(); + dispatch(expandCommunityTimeline({ onlyMedia })); + this.disconnect = dispatch(connectCommunityStream({ onlyMedia })); + } } componentWillUnmount () { @@ -68,13 +84,32 @@ export default class CommunityTimeline extends React.PureComponent { } handleLoadMore = maxId => { - this.props.dispatch(expandCommunityTimeline({ maxId })); + const { dispatch, onlyMedia } = this.props; + + dispatch(expandCommunityTimeline({ maxId, onlyMedia })); + } + + handleHeadlineLinkClick = e => { + const { columnId, dispatch } = this.props; + const onlyMedia = /\/media$/.test(e.currentTarget.href); + + dispatch(changeColumnParams(columnId, { other: { onlyMedia } })); } render () { - const { intl, hasUnread, columnId, multiColumn } = this.props; + const { intl, hasUnread, columnId, multiColumn, onlyMedia } = this.props; const pinned = !!columnId; + const headline = ( + + ); + return ( } /> diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index 39eb02362d..6cc594c889 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -40,6 +40,7 @@ export default class ComposeForm extends ImmutablePureComponent { privacy: PropTypes.string, spoiler_text: PropTypes.string, focusDate: PropTypes.instanceOf(Date), + caretPosition: PropTypes.number, preselectDate: PropTypes.instanceOf(Date), is_submitting: PropTypes.bool, is_uploading: PropTypes.bool, @@ -96,7 +97,6 @@ export default class ComposeForm extends ImmutablePureComponent { } onSuggestionSelected = (tokenStart, token, value) => { - this._restoreCaret = null; this.props.onSuggestionSelected(tokenStart, token, value); } @@ -104,31 +104,21 @@ export default class ComposeForm extends ImmutablePureComponent { this.props.onChangeSpoilerText(e.target.value); } - componentWillReceiveProps (nextProps) { - // If this is the update where we've finished uploading, - // save the last caret position so we can restore it below! - if (!nextProps.is_uploading && this.props.is_uploading) { - this._restoreCaret = this.autosuggestTextarea.textarea.selectionStart; - } - } - componentDidUpdate (prevProps) { // This statement does several things: // - If we're beginning a reply, and, // - Replying to zero or one users, places the cursor at the end of the textbox. // - Replying to more than one user, selects any usernames past the first; // this provides a convenient shortcut to drop everyone else from the conversation. - // - If we've just finished uploading an image, and have a saved caret position, - // restores the cursor to that position after the text changes! - if (this.props.focusDate !== prevProps.focusDate || (prevProps.is_uploading && !this.props.is_uploading && typeof this._restoreCaret === 'number')) { + if (this.props.focusDate !== prevProps.focusDate) { let selectionEnd, selectionStart; if (this.props.preselectDate !== prevProps.preselectDate) { selectionEnd = this.props.text.length; selectionStart = this.props.text.search(/\s/) + 1; - } else if (typeof this._restoreCaret === 'number') { - selectionStart = this._restoreCaret; - selectionEnd = this._restoreCaret; + } else if (typeof this.props.caretPosition === 'number') { + selectionStart = this.props.caretPosition; + selectionEnd = this.props.caretPosition; } else { selectionEnd = this.props.text.length; selectionStart = selectionEnd; @@ -148,10 +138,8 @@ export default class ComposeForm extends ImmutablePureComponent { handleEmojiPick = (data) => { const { text } = this.props; const position = this.autosuggestTextarea.textarea.selectionStart; - const emojiChar = data.native; const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]); - this._restoreCaret = position + emojiChar.length + 1 + (needsSpace ? 1 : 0); this.props.onPickEmoji(position, data, needsSpace); } diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js index 6b22ba84a5..a772c1c953 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js @@ -42,22 +42,65 @@ class PrivacyDropdownMenu extends React.PureComponent { } } - handleClick = e => { - if (e.key === 'Escape') { - this.props.onClose(); - } else if (!e.key || e.key === 'Enter') { - const value = e.currentTarget.getAttribute('data-index'); - - e.preventDefault(); + handleKeyDown = e => { + const { items } = this.props; + const value = e.currentTarget.getAttribute('data-index'); + const index = items.findIndex(item => { + return (item.value === value); + }); + let element; + switch(e.key) { + case 'Escape': this.props.onClose(); - this.props.onChange(value); + break; + case 'Enter': + this.handleClick(e); + break; + case 'ArrowDown': + element = this.node.childNodes[index + 1]; + if (element) { + element.focus(); + this.props.onChange(element.getAttribute('data-index')); + } + break; + case 'ArrowUp': + element = this.node.childNodes[index - 1]; + if (element) { + element.focus(); + this.props.onChange(element.getAttribute('data-index')); + } + break; + case 'Home': + element = this.node.firstChild; + if (element) { + element.focus(); + this.props.onChange(element.getAttribute('data-index')); + } + break; + case 'End': + element = this.node.lastChild; + if (element) { + element.focus(); + this.props.onChange(element.getAttribute('data-index')); + } + break; } } + handleClick = e => { + const value = e.currentTarget.getAttribute('data-index'); + + e.preventDefault(); + + this.props.onClose(); + this.props.onChange(value); + } + componentDidMount () { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + if (this.focusedItem) this.focusedItem.focus(); this.setState({ mounted: true }); } @@ -70,6 +113,10 @@ class PrivacyDropdownMenu extends React.PureComponent { this.node = c; } + setFocusRef = c => { + this.focusedItem = c; + } + render () { const { mounted } = this.state; const { style, items, value } = this.props; @@ -80,9 +127,9 @@ class PrivacyDropdownMenu extends React.PureComponent { // It should not be transformed when mounting because the resulting // size will be used to determine the coordinate of the menu by // react-overlays - + {items.map(item => ( - + @@ -147,9 +194,6 @@ export default class PrivacyDropdown extends React.PureComponent { handleKeyDown = e => { switch(e.key) { - case 'Enter': - this.handleToggle(e); - break; case 'Escape': this.handleClose(); break; diff --git a/app/javascript/mastodon/features/compose/components/search_results.js b/app/javascript/mastodon/features/compose/components/search_results.js index 84455563c4..445bf27bb1 100644 --- a/app/javascript/mastodon/features/compose/components/search_results.js +++ b/app/javascript/mastodon/features/compose/components/search_results.js @@ -1,28 +1,82 @@ import React from 'react'; +import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, FormattedNumber } from 'react-intl'; import AccountContainer from '../../../containers/account_container'; import StatusContainer from '../../../containers/status_container'; import { Link } from 'react-router-dom'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { Sparklines, SparklinesCurve } from 'react-sparklines'; + +const shortNumberFormat = number => { + if (number < 1000) { + return ; + } else { + return K; + } +}; + +const renderHashtag = hashtag => ( + + + + #{hashtag.get('name')} + + + {shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))} }} /> + + + + {shortNumberFormat(hashtag.getIn(['history', 0, 'uses']))} + + + + day.get('uses')).toArray()}> + + + + +); export default class SearchResults extends ImmutablePureComponent { static propTypes = { results: ImmutablePropTypes.map.isRequired, + trends: ImmutablePropTypes.list, + fetchTrends: PropTypes.func.isRequired, }; + componentDidMount () { + const { fetchTrends } = this.props; + fetchTrends(); + } + render () { - const { results } = this.props; + const { results, trends } = this.props; let accounts, statuses, hashtags; let count = 0; + if (results.isEmpty()) { + return ( + + + + + + + + {trends && trends.map(hashtag => renderHashtag(hashtag))} + + + ); + } + if (results.get('accounts') && results.get('accounts').size > 0) { count += results.get('accounts').size; accounts = ( - + {results.get('accounts').map(accountId => )} @@ -33,7 +87,7 @@ export default class SearchResults extends ImmutablePureComponent { count += results.get('statuses').size; statuses = ( - + {results.get('statuses').map(statusId => )} @@ -44,13 +98,9 @@ export default class SearchResults extends ImmutablePureComponent { count += results.get('hashtags').size; hashtags = ( - + - {results.get('hashtags').map(hashtag => ( - - #{hashtag} - - ))} + {results.get('hashtags').map(hashtag => renderHashtag(hashtag))} ); } @@ -58,6 +108,7 @@ export default class SearchResults extends ImmutablePureComponent { return ( + diff --git a/app/javascript/mastodon/features/compose/components/upload.js b/app/javascript/mastodon/features/compose/components/upload.js index 61b2d19e0d..bfa2b47271 100644 --- a/app/javascript/mastodon/features/compose/components/upload.js +++ b/app/javascript/mastodon/features/compose/components/upload.js @@ -77,7 +77,7 @@ export default class Upload extends ImmutablePureComponent { {({ scale }) => ( - + {media.get('type') === 'image' && } 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 c3aa580ee3..3822dd711f 100644 --- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js +++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js @@ -19,6 +19,7 @@ const mapStateToProps = state => ({ spoiler_text: state.getIn(['compose', 'spoiler_text']), privacy: state.getIn(['compose', 'privacy']), focusDate: state.getIn(['compose', 'focusDate']), + caretPosition: state.getIn(['compose', 'caretPosition']), preselectDate: state.getIn(['compose', 'preselectDate']), is_submitting: state.getIn(['compose', 'is_submitting']), is_uploading: state.getIn(['compose', 'is_uploading']), diff --git a/app/javascript/mastodon/features/compose/containers/search_results_container.js b/app/javascript/mastodon/features/compose/containers/search_results_container.js index 16d95d417e..7273460e28 100644 --- a/app/javascript/mastodon/features/compose/containers/search_results_container.js +++ b/app/javascript/mastodon/features/compose/containers/search_results_container.js @@ -1,8 +1,14 @@ import { connect } from 'react-redux'; import SearchResults from '../components/search_results'; +import { fetchTrends } from '../../../actions/trends'; const mapStateToProps = state => ({ results: state.getIn(['search', 'results']), + trends: state.get('trends'), }); -export default connect(mapStateToProps)(SearchResults); +const mapDispatchToProps = dispatch => ({ + fetchTrends: () => dispatch(fetchTrends()), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(SearchResults); diff --git a/app/javascript/mastodon/features/compose/containers/warning_container.js b/app/javascript/mastodon/features/compose/containers/warning_container.js index efaa02e9ea..8200a319f7 100644 --- a/app/javascript/mastodon/features/compose/containers/warning_container.js +++ b/app/javascript/mastodon/features/compose/containers/warning_container.js @@ -17,11 +17,19 @@ const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning if (needsLockWarning) { return }} />} />; } + if (hashtagWarning) { return } />; } + if (directMessageWarning) { - return } />; + const message = ( + + + + ); + + return ; } return null; diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index 67f0e79815..d8e9ad9ee0 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -43,11 +43,19 @@ export default class Compose extends React.PureComponent { }; componentDidMount () { - this.props.dispatch(mountCompose()); + const { isSearchPage } = this.props; + + if (!isSearchPage) { + this.props.dispatch(mountCompose()); + } } componentWillUnmount () { - this.props.dispatch(unmountCompose()); + const { isSearchPage } = this.props; + + if (!isSearchPage) { + this.props.dispatch(unmountCompose()); + } } onFocus = () => { @@ -93,7 +101,7 @@ export default class Compose extends React.PureComponent { {(multiColumn || isSearchPage) && } - + {!isSearchPage && {multiColumn && ( @@ -101,7 +109,7 @@ export default class Compose extends React.PureComponent { )} - + } {({ x }) => ( diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js index 5a88f76017..341af582ae 100644 --- a/app/javascript/mastodon/features/public_timeline/index.js +++ b/app/javascript/mastodon/features/public_timeline/index.js @@ -1,42 +1,48 @@ import React from 'react'; import { connect } from 'react-redux'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; import StatusListContainer from '../ui/containers/status_list_container'; import Column from '../../components/column'; import ColumnHeader from '../../components/column_header'; import { expandPublicTimeline } from '../../actions/timelines'; -import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { addColumn, removeColumn, moveColumn, changeColumnParams } from '../../actions/columns'; import ColumnSettingsContainer from './containers/column_settings_container'; +import SectionHeadline from '../community_timeline/components/section_headline'; import { connectPublicStream } from '../../actions/streaming'; const messages = defineMessages({ title: { id: 'column.public', defaultMessage: 'Federated timeline' }, }); -const mapStateToProps = state => ({ - hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0, +const mapStateToProps = (state, { onlyMedia }) => ({ + hasUnread: state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`, 'unread']) > 0, }); @connect(mapStateToProps) @injectIntl export default class PublicTimeline extends React.PureComponent { + static defaultProps = { + onlyMedia: false, + }; + static propTypes = { dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, columnId: PropTypes.string, multiColumn: PropTypes.bool, hasUnread: PropTypes.bool, + onlyMedia: PropTypes.bool, }; handlePin = () => { - const { columnId, dispatch } = this.props; + const { columnId, dispatch, onlyMedia } = this.props; if (columnId) { dispatch(removeColumn(columnId)); } else { - dispatch(addColumn('PUBLIC', {})); + dispatch(addColumn('PUBLIC', { other: { onlyMedia } })); } } @@ -50,10 +56,20 @@ export default class PublicTimeline extends React.PureComponent { } componentDidMount () { - const { dispatch } = this.props; + const { dispatch, onlyMedia } = this.props; - dispatch(expandPublicTimeline()); - this.disconnect = dispatch(connectPublicStream()); + dispatch(expandPublicTimeline({ onlyMedia })); + this.disconnect = dispatch(connectPublicStream({ onlyMedia })); + } + + componentDidUpdate (prevProps) { + if (prevProps.onlyMedia !== this.props.onlyMedia) { + const { dispatch, onlyMedia } = this.props; + + this.disconnect(); + dispatch(expandPublicTimeline({ onlyMedia })); + this.disconnect = dispatch(connectPublicStream({ onlyMedia })); + } } componentWillUnmount () { @@ -68,13 +84,32 @@ export default class PublicTimeline extends React.PureComponent { } handleLoadMore = maxId => { - this.props.dispatch(expandPublicTimeline({ maxId })); + const { dispatch, onlyMedia } = this.props; + + dispatch(expandPublicTimeline({ maxId, onlyMedia })); + } + + handleHeadlineLinkClick = e => { + const { columnId, dispatch } = this.props; + const onlyMedia = /\/media$/.test(e.currentTarget.href); + + dispatch(changeColumnParams(columnId, { other: { onlyMedia } })); } render () { - const { intl, columnId, hasUnread, multiColumn } = this.props; + const { intl, columnId, hasUnread, multiColumn, onlyMedia } = this.props; const pinned = !!columnId; + const headline = ( + + ); + return ( - + {shareButton} diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index b5f5160326..4177190044 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -34,8 +34,8 @@ export default class DetailedStatus extends ImmutablePureComponent { e.stopPropagation(); } - handleOpenVideo = startTime => { - this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime); + handleOpenVideo = (media, startTime) => { + this.props.onOpenVideo(media, startTime); } handleExpandedToggle = () => { diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index d5af2a459c..505a88a3ff 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -1,3 +1,4 @@ +import Immutable from 'immutable'; import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; @@ -54,11 +55,44 @@ const messages = defineMessages({ const makeMapStateToProps = () => { const getStatus = makeGetStatus(); - const mapStateToProps = (state, props) => ({ - status: getStatus(state, props.params.statusId), - ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]), - descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]), - }); + const mapStateToProps = (state, props) => { + const status = getStatus(state, props.params.statusId); + let ancestorsIds = Immutable.List(); + let descendantsIds = Immutable.List(); + + if (status) { + ancestorsIds = ancestorsIds.withMutations(mutable => { + let id = status.get('in_reply_to_id'); + + while (id) { + mutable.unshift(id); + id = state.getIn(['contexts', 'inReplyTos', id]); + } + }); + + descendantsIds = descendantsIds.withMutations(mutable => { + const ids = [status.get('id')]; + + while (ids.length > 0) { + let id = ids.shift(); + const replies = state.getIn(['contexts', 'replies', id]); + + if (replies) { + replies.forEach(reply => { + mutable.push(reply); + ids.unshift(reply); + }); + } + } + }); + } + + return { + status, + ancestorsIds, + descendantsIds, + }; + }; return mapStateToProps; }; diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 0a62cbbeb4..3ab867b5a7 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -175,10 +175,11 @@ export default class ColumnsArea extends ImmutablePureComponent { {columns.map(column => { const params = column.get('params', null) === null ? null : column.get('params').toJS(); + const other = params && params.other ? params.other : {}; return ( - {SpecificComponent => } + {SpecificComponent => } ); })} diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index fb76270fad..f4d6b5c4e6 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -2,6 +2,7 @@ import React from 'react'; import ReactSwipeableViews from 'react-swipeable-views'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; +import Video from '../../video'; import ExtendedVideoPlayer from '../../../components/extended_video_player'; import classNames from 'classnames'; import { defineMessages, injectIntl } from 'react-intl'; @@ -112,6 +113,22 @@ export default class MediaModal extends ImmutablePureComponent { onClick={this.toggleNavigation} /> ); + } else if (image.get('type') === 'video') { + const { time } = this.props; + + return ( + + ); } else if (image.get('type') === 'gifv') { return ( () => { return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? : null; } diff --git a/app/javascript/mastodon/features/ui/components/report_modal.js b/app/javascript/mastodon/features/ui/components/report_modal.js index 8a55c553c1..8616f0315c 100644 --- a/app/javascript/mastodon/features/ui/components/report_modal.js +++ b/app/javascript/mastodon/features/ui/components/report_modal.js @@ -30,7 +30,7 @@ const makeMapStateToProps = () => { 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'])), + statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])), }; }; @@ -64,12 +64,12 @@ export default class ReportModal extends ImmutablePureComponent { } componentDidMount () { - this.props.dispatch(expandAccountTimeline(this.props.account.get('id'))); + this.props.dispatch(expandAccountTimeline(this.props.account.get('id'), { withReplies: true })); } componentWillReceiveProps (nextProps) { if (this.props.account !== nextProps.account && nextProps.account) { - this.props.dispatch(expandAccountTimeline(nextProps.account.get('id'))); + this.props.dispatch(expandAccountTimeline(nextProps.account.get('id'), { withReplies: true })); } } 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 4efacda65e..e5b1edc4a0 100644 --- a/app/javascript/mastodon/features/ui/containers/status_list_container.js +++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js @@ -21,6 +21,8 @@ const makeGetStatusIds = () => createSelector([ } return statusIds.filter(id => { + if (id === null) return true; + const statusForId = statuses.get(id); let showStatus = true; diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index adca0d617d..adb856b927 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -141,7 +141,9 @@ class SwitchingColumnsArea extends React.PureComponent { - + + + diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index 98ebcb6f91..47a165e169 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { fromJS } from 'immutable'; import { throttle } from 'lodash'; import classNames from 'classnames'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; @@ -131,6 +132,8 @@ export default class Video extends React.PureComponent { this.seek = c; } + handleClickRoot = e => e.stopPropagation(); + handlePlay = () => { this.setState({ paused: false }); } @@ -244,8 +247,17 @@ export default class Video extends React.PureComponent { } handleOpenVideo = () => { + const { src, preview, width, height } = this.props; + const media = fromJS({ + type: 'video', + url: src, + preview_url: preview, + width, + height, + }); + this.video.pause(); - this.props.onOpenVideo(this.video.currentTime); + this.props.onOpenVideo(media, this.video.currentTime); } handleCloseVideo = () => { @@ -270,7 +282,16 @@ export default class Video extends React.PureComponent { } return ( - + list.push(media)); map.set('is_uploading', false); map.set('resetFileKey', Math.floor((Math.random() * 0x10000))); - map.set('focusDate', new Date()); map.set('idempotencyKey', uuid()); if (prevSize === 0 && (state.get('default_sensitive') || state.get('spoiler'))) { @@ -119,6 +119,7 @@ const insertSuggestion = (state, position, token, completion) => { map.set('suggestion_token', null); map.update('suggestions', ImmutableList(), list => list.clear()); map.set('focusDate', new Date()); + map.set('caretPosition', position + completion.length + 1); map.set('idempotencyKey', uuid()); }); }; @@ -142,6 +143,7 @@ const insertEmoji = (state, position, emojiData, needsSpace) => { return state.merge({ text: `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`, focusDate: new Date(), + caretPosition: position + emoji.length + 1, idempotencyKey: uuid(), }); }; @@ -216,6 +218,7 @@ export default function compose(state = initialState, action) { map.set('text', statusToTextMentions(state, action.status)); map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy'))); map.set('focusDate', new Date()); + map.set('caretPosition', null); map.set('preselectDate', new Date()); map.set('idempotencyKey', uuid()); @@ -259,6 +262,7 @@ export default function compose(state = initialState, action) { return state.withMutations(map => { map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' ')); map.set('focusDate', new Date()); + map.set('caretPosition', null); map.set('idempotencyKey', uuid()); }); case COMPOSE_DIRECT: @@ -266,6 +270,7 @@ export default function compose(state = initialState, action) { map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' ')); map.set('privacy', 'direct'); map.set('focusDate', new Date()); + map.set('caretPosition', null); map.set('idempotencyKey', uuid()); }); case COMPOSE_SUGGESTIONS_CLEAR: diff --git a/app/javascript/mastodon/reducers/contexts.js b/app/javascript/mastodon/reducers/contexts.js index ebd01e5327..53e70b58ef 100644 --- a/app/javascript/mastodon/reducers/contexts.js +++ b/app/javascript/mastodon/reducers/contexts.js @@ -3,38 +3,62 @@ import { ACCOUNT_MUTE_SUCCESS, } from '../actions/accounts'; import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; -import { TIMELINE_DELETE, TIMELINE_CONTEXT_UPDATE } from '../actions/timelines'; +import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; const initialState = ImmutableMap({ - ancestors: ImmutableMap(), - descendants: ImmutableMap(), + inReplyTos: ImmutableMap(), + replies: ImmutableMap(), }); -const normalizeContext = (state, id, ancestors, descendants) => { - const ancestorsIds = ImmutableList(ancestors.map(ancestor => ancestor.id)); - const descendantsIds = ImmutableList(descendants.map(descendant => descendant.id)); +const normalizeContext = (immutableState, id, ancestors, descendants) => immutableState.withMutations(state => { + state.update('inReplyTos', immutableAncestors => immutableAncestors.withMutations(inReplyTos => { + state.update('replies', immutableDescendants => immutableDescendants.withMutations(replies => { + function addReply({ id, in_reply_to_id }) { + if (in_reply_to_id) { + const siblings = replies.get(in_reply_to_id, ImmutableList()); - return state.withMutations(map => { - map.setIn(['ancestors', id], ancestorsIds); - map.setIn(['descendants', id], descendantsIds); - }); -}; + if (!siblings.includes(id)) { + const index = siblings.findLastIndex(sibling => sibling.id < id); + replies.set(in_reply_to_id, siblings.insert(index + 1, id)); + } + + inReplyTos.set(id, in_reply_to_id); + } + } + + if (ancestors[0]) { + addReply({ id, in_reply_to_id: ancestors[0].id }); + } + + if (descendants[0]) { + addReply({ id: descendants[0].id, in_reply_to_id: id }); + } + + [ancestors, descendants].forEach(statuses => statuses.forEach(addReply)); + })); + })); +}); const deleteFromContexts = (immutableState, ids) => immutableState.withMutations(state => { - state.update('ancestors', immutableAncestors => immutableAncestors.withMutations(ancestors => { - state.update('descendants', immutableDescendants => immutableDescendants.withMutations(descendants => { + state.update('inReplyTos', immutableAncestors => immutableAncestors.withMutations(inReplyTos => { + state.update('replies', immutableDescendants => immutableDescendants.withMutations(replies => { ids.forEach(id => { - descendants.get(id, ImmutableList()).forEach(descendantId => { - ancestors.update(descendantId, ImmutableList(), list => list.filterNot(itemId => itemId === id)); - }); + const inReplyToIdOfId = inReplyTos.get(id); + const repliesOfId = replies.get(id); + const siblings = replies.get(inReplyToIdOfId); - ancestors.get(id, ImmutableList()).forEach(ancestorId => { - descendants.update(ancestorId, ImmutableList(), list => list.filterNot(itemId => itemId === id)); - }); + if (siblings) { + replies.set(inReplyToIdOfId, siblings.filterNot(sibling => sibling === id)); + } - descendants.delete(id); - ancestors.delete(id); + + if (repliesOfId) { + repliesOfId.forEach(reply => inReplyTos.delete(reply)); + } + + inReplyTos.delete(id); + replies.delete(id); }); })); })); @@ -48,23 +72,23 @@ const filterContexts = (state, relationship, statuses) => { return deleteFromContexts(state, ownedStatusIds); }; -const updateContext = (state, status, references) => { - return state.update('descendants', map => { - references.forEach(parentId => { - map = map.update(parentId, ImmutableList(), list => { - if (list.includes(status.id)) { - return list; - } +const updateContext = (state, status) => { + if (status.in_reply_to_id) { + return state.withMutations(mutable => { + const replies = mutable.getIn(['replies', status.in_reply_to_id], ImmutableList()); - return list.push(status.id); - }); + mutable.setIn(['inReplyTos', status.id], status.in_reply_to_id); + + if (!replies.includes(status.id)) { + mutable.setIn(['replies', status.id], replies.push(status.id)); + } }); + } - return map; - }); + return state; }; -export default function contexts(state = initialState, action) { +export default function replies(state = initialState, action) { switch(action.type) { case ACCOUNT_BLOCK_SUCCESS: case ACCOUNT_MUTE_SUCCESS: @@ -73,8 +97,8 @@ export default function contexts(state = initialState, action) { return normalizeContext(state, action.id, action.ancestors, action.descendants); case TIMELINE_DELETE: return deleteFromContexts(state, [action.id]); - case TIMELINE_CONTEXT_UPDATE: - return updateContext(state, action.status, action.references); + case TIMELINE_UPDATE: + return updateContext(state, action.status); default: return state; } diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index 3d9a6a1329..019c1f466b 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -26,6 +26,7 @@ import height_cache from './height_cache'; import custom_emojis from './custom_emojis'; import lists from './lists'; import listEditor from './list_editor'; +import trends from './trends'; const reducers = { dropdown_menu, @@ -55,6 +56,7 @@ const reducers = { custom_emojis, lists, listEditor, + trends, }; export default combineReducers(reducers); diff --git a/app/javascript/mastodon/reducers/search.js b/app/javascript/mastodon/reducers/search.js index 56fd7226b4..4758defb1b 100644 --- a/app/javascript/mastodon/reducers/search.js +++ b/app/javascript/mastodon/reducers/search.js @@ -9,7 +9,7 @@ import { COMPOSE_REPLY, COMPOSE_DIRECT, } from '../actions/compose'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; const initialState = ImmutableMap({ value: '', @@ -39,7 +39,7 @@ export default function search(state = initialState, action) { return state.set('results', ImmutableMap({ accounts: ImmutableList(action.results.accounts.map(item => item.id)), statuses: ImmutableList(action.results.statuses.map(item => item.id)), - hashtags: ImmutableList(action.results.hashtags), + hashtags: fromJS(action.results.hashtags), })).set('submitted', true); default: return state; diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index 9ec52a7faf..de8865e433 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -1,5 +1,5 @@ import { SETTING_CHANGE, SETTING_SAVE } from '../actions/settings'; -import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE } from '../actions/columns'; +import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE, COLUMN_PARAMS_CHANGE } from '../actions/columns'; import { STORE_HYDRATE } from '../actions/store'; import { EMOJI_USE } from '../actions/emojis'; import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists'; @@ -89,6 +89,17 @@ const moveColumn = (state, uuid, direction) => { .set('saved', false); }; +const changeColumnParams = (state, uuid, params) => { + const columns = state.get('columns'); + const index = columns.findIndex(item => item.get('uuid') === uuid); + + const newColumns = columns.update(index, column => column.update('params', () => fromJS(params))); + + return state + .set('columns', newColumns) + .set('saved', false); +}; + const updateFrequentEmojis = (state, emoji) => state.update('frequentlyUsedEmojis', ImmutableMap(), map => map.update(emoji.id, 0, count => count + 1)).set('saved', false); const filterDeadListColumns = (state, listId) => state.update('columns', columns => columns.filterNot(column => column.get('id') === 'LIST' && column.get('params').get('id') === listId)); @@ -111,6 +122,8 @@ export default function settings(state = initialState, action) { .set('saved', false); case COLUMN_MOVE: return moveColumn(state, action.uuid, action.direction); + case COLUMN_PARAMS_CHANGE: + return changeColumnParams(state, action.uuid, action.params); case EMOJI_USE: return updateFrequentEmojis(state, action.emoji); case SETTING_SAVE: diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index dd675d78fb..916a091eb9 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -33,6 +33,11 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial) => if (!statuses.isEmpty()) { mMap.update('items', ImmutableList(), oldIds => { const newIds = statuses.map(status => status.get('id')); + + if (timeline.indexOf(':pinned') !== -1) { + return newIds; + } + const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1; const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0); @@ -134,7 +139,7 @@ export default function timelines(state = initialState, action) { initialTimeline, map => map.update( 'items', - items => items.first() ? items : items.unshift(null) + items => items.first() ? items.unshift(null) : items ) ); default: diff --git a/app/javascript/mastodon/reducers/trends.js b/app/javascript/mastodon/reducers/trends.js new file mode 100644 index 0000000000..95cf8f2841 --- /dev/null +++ b/app/javascript/mastodon/reducers/trends.js @@ -0,0 +1,13 @@ +import { TRENDS_FETCH_SUCCESS } from '../actions/trends'; +import { fromJS } from 'immutable'; + +const initialState = null; + +export default function trendsReducer(state = initialState, action) { + switch(action.type) { + case TRENDS_FETCH_SUCCESS: + return fromJS(action.trends); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/service_worker/entry.js b/app/javascript/mastodon/service_worker/entry.js index ba54ae9968..c1854c1cd2 100644 --- a/app/javascript/mastodon/service_worker/entry.js +++ b/app/javascript/mastodon/service_worker/entry.js @@ -1,4 +1,4 @@ -import { freeStorage } from '../storage/modifier'; +import { freeStorage, storageFreeable } from '../storage/modifier'; import './web_push_notifications'; function openSystemCache() { @@ -10,9 +10,12 @@ function openWebCache() { } function fetchRoot() { - return fetch('/', { credentials: 'include' }); + return fetch('/', { credentials: 'include', redirect: 'manual' }); } +const firefox = navigator.userAgent.match(/Firefox\/(\d+)/); +const invalidOnlyIfCached = firefox && firefox[1] < 60; + // Cause a new version of a registered Service Worker to replace an existing one // that is already installed, and replace the currently active worker on open pages. self.addEventListener('install', function(event) { @@ -28,48 +31,49 @@ self.addEventListener('fetch', function(event) { const asyncResponse = fetchRoot(); const asyncCache = openWebCache(); - event.respondWith(asyncResponse.then(async response => { - if (response.ok) { - const cache = await asyncCache; - await cache.put('/', response); - return response.clone(); - } - - throw null; - }).catch(() => asyncCache.then(cache => cache.match('/')))); + event.respondWith(asyncResponse.then( + response => asyncCache.then(cache => cache.put('/', response.clone())) + .then(() => response), + () => asyncCache.then(cache => cache.match('/')))); } else if (url.pathname === '/auth/sign_out') { const asyncResponse = fetch(event.request); const asyncCache = openWebCache(); - event.respondWith(asyncResponse.then(async response => { + event.respondWith(asyncResponse.then(response => { if (response.ok || response.type === 'opaqueredirect') { - await Promise.all([ + return Promise.all([ asyncCache.then(cache => cache.delete('/')), indexedDB.deleteDatabase('mastodon'), - ]); + ]).then(() => response); } return response; })); - } else if (process.env.CDN_HOST ? url.host === process.env.CDN_HOST : url.pathname.startsWith('/system/')) { - event.respondWith(openSystemCache().then(async cache => { - const cached = await cache.match(event.request.url); + } else if (storageFreeable && (ATTACHMENT_HOST ? url.host === ATTACHMENT_HOST : url.pathname.startsWith('/system/'))) { + event.respondWith(openSystemCache().then(cache => { + return cache.match(event.request.url).then(cached => { + if (cached === undefined) { + const asyncResponse = invalidOnlyIfCached && event.request.cache === 'only-if-cached' ? + fetch(event.request, { cache: 'no-cache' }) : fetch(event.request); - if (cached === undefined) { - const fetched = await fetch(event.request); + return asyncResponse.then(response => { + if (response.ok) { + const put = cache.put(event.request.url, response.clone()); - if (fetched.ok) { - try { - await cache.put(event.request.url, fetched.clone()); - } finally { - freeStorage(); - } + put.catch(() => freeStorage()); + + return put.then(() => { + freeStorage(); + return response; + }); + } + + return response; + }); } - return fetched; - } - - return cached; + return cached; + }); })); } }); diff --git a/app/javascript/mastodon/service_worker/web_push_locales.js b/app/javascript/mastodon/service_worker/web_push_locales.js new file mode 100644 index 0000000000..ce96ae297c --- /dev/null +++ b/app/javascript/mastodon/service_worker/web_push_locales.js @@ -0,0 +1,30 @@ +/* @preval */ + +const fs = require('fs'); +const path = require('path'); + +const filtered = {}; +const filenames = fs.readdirSync(path.resolve(__dirname, '../locales')); + +filenames.forEach(filename => { + if (!filename.match(/\.json$/) || filename.match(/defaultMessages|whitelist/)) return; + + const content = fs.readFileSync(path.resolve(__dirname, `../locales/${filename}`), 'utf-8'); + const full = JSON.parse(content); + const locale = filename.split('.')[0]; + + filtered[locale] = { + 'notification.favourite': full['notification.favourite'] || '', + 'notification.follow': full['notification.follow'] || '', + 'notification.mention': full['notification.mention'] || '', + 'notification.reblog': full['notification.reblog'] || '', + + 'status.show_more': full['status.show_more'] || '', + 'status.reblog': full['status.reblog'] || '', + 'status.favourite': full['status.favourite'] || '', + + 'notifications.group': full['notifications.group'] || '', + }; +}); + +module.exports = JSON.parse(JSON.stringify(filtered)); diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js index f63cff335a..3318bbadcc 100644 --- a/app/javascript/mastodon/service_worker/web_push_notifications.js +++ b/app/javascript/mastodon/service_worker/web_push_notifications.js @@ -1,36 +1,33 @@ +import IntlMessageFormat from 'intl-messageformat'; +import locales from './web_push_locales'; +import { unescape } from 'lodash'; + const MAX_NOTIFICATIONS = 5; const GROUP_TAG = 'tag'; -// Avoid loading intl-messageformat and dealing with locales in the ServiceWorker -const formatGroupTitle = (message, count) => message.replace('%{count}', count); - const notify = options => self.registration.getNotifications().then(notifications => { - if (notifications.length === MAX_NOTIFICATIONS) { - // Reached the maximum number of notifications, proceed with grouping + if (notifications.length >= MAX_NOTIFICATIONS) { // Reached the maximum number of notifications, proceed with grouping const group = { - title: formatGroupTitle(options.data.message, notifications.length + 1), - body: notifications - .sort((n1, n2) => n1.timestamp < n2.timestamp) - .map(notification => notification.title).join('\n'), + title: formatMessage('notifications.group', options.data.preferred_locale, { count: notifications.length + 1 }), + body: notifications.sort((n1, n2) => n1.timestamp < n2.timestamp).map(notification => notification.title).join('\n'), badge: '/badge.png', icon: '/android-chrome-192x192.png', tag: GROUP_TAG, data: { url: (new URL('/web/notifications', self.location)).href, count: notifications.length + 1, - message: options.data.message, + preferred_locale: options.data.preferred_locale, }, }; notifications.forEach(notification => notification.close()); return self.registration.showNotification(group.title, group); - } else if (notifications.length === 1 && notifications[0].tag === GROUP_TAG) { - // Already grouped, proceed with appending the notification to the group + } else if (notifications.length === 1 && notifications[0].tag === GROUP_TAG) { // Already grouped, proceed with appending the notification to the group const group = cloneNotification(notifications[0]); - group.title = formatGroupTitle(group.data.message, group.data.count + 1); + group.title = formatMessage('notifications.group', options.data.preferred_locale, { count: group.data.count + 1 }); group.body = `${options.title}\n${group.body}`; group.data = { ...group.data, count: group.data.count + 1 }; @@ -40,57 +37,102 @@ const notify = options => return self.registration.showNotification(options.title, options); }); -const handlePush = (event) => { - const options = event.data.json(); +const fetchFromApi = (path, method, accessToken) => { + const url = (new URL(path, self.location)).href; - options.body = options.data.nsfw || options.data.content; - options.dir = options.data.dir; - options.image = options.image || undefined; // Null results in a network request (404) - options.timestamp = options.timestamp && new Date(options.timestamp); + return fetch(url, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, - const expandAction = options.data.actions.find(action => action.todo === 'expand'); - - if (expandAction) { - options.actions = [expandAction]; - options.hiddenActions = options.data.actions.filter(action => action !== expandAction); - options.data.hiddenImage = options.image; - options.image = undefined; - } else { - options.actions = options.data.actions; - } - - event.waitUntil(notify(options)); + method: method, + credentials: 'include', + }).then(res => { + if (res.ok) { + return res; + } else { + throw new Error(res.status); + } + }).then(res => res.json()); }; -const cloneNotification = (notification) => { - const clone = { }; +const cloneNotification = notification => { + const clone = {}; + let k; - for(var k in notification) { + // Object.assign() does not work with notifications + for(k in notification) { clone[k] = notification[k]; } return clone; }; -const expandNotification = (notification) => { - const nextNotification = cloneNotification(notification); +const formatMessage = (messageId, locale, values = {}) => + (new IntlMessageFormat(locales[locale][messageId], locale)).format(values); - nextNotification.body = notification.data.content; - nextNotification.image = notification.data.hiddenImage; - nextNotification.actions = notification.data.actions.filter(action => action.todo !== 'expand'); +const htmlToPlainText = html => + unescape(html.replace(//g, '\n').replace(/<\/p>/g, '\n\n').replace(/<[^>]*>/g, '')); - return self.registration.showNotification(nextNotification.title, nextNotification); +const handlePush = (event) => { + const { access_token, notification_id, preferred_locale, title, body, icon } = event.data.json(); + + // Placeholder until more information can be loaded + event.waitUntil( + notify({ + title, + body, + icon, + tag: notification_id, + timestamp: new Date(), + badge: '/badge.png', + data: { access_token, preferred_locale, url: '/web/notifications' }, + }).then(() => fetchFromApi(`/api/v1/notifications/${notification_id}`, 'get', access_token)).then(notification => { + const options = {}; + + options.title = formatMessage(`notification.${notification.type}`, preferred_locale, { name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username }); + options.body = notification.status && htmlToPlainText(notification.status.content); + options.icon = notification.account.avatar_static; + options.timestamp = notification.created_at && new Date(notification.created_at); + options.tag = notification.id; + options.badge = '/badge.png'; + options.image = notification.status && notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url || undefined; + options.data = { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/web/statuses/${notification.status.id}` : `/web/accounts/${notification.account.id}` }; + + if (notification.status && notification.status.sensitive) { + options.data.hiddenBody = htmlToPlainText(notification.status.content); + options.data.hiddenImage = notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url; + + options.body = notification.status.spoiler_text; + options.image = undefined; + options.actions = [actionExpand(preferred_locale)]; + } else if (notification.type === 'mention') { + options.actions = [actionReblog(preferred_locale), actionFavourite(preferred_locale)]; + } + + return notify(options); + }) + ); }; -const makeRequest = (notification, action) => - fetch(action.action, { - headers: { - 'Authorization': `Bearer ${notification.data.access_token}`, - 'Content-Type': 'application/json', - }, - method: action.method, - credentials: 'include', - }); +const actionExpand = preferred_locale => ({ + action: 'expand', + icon: '/web-push-icon_expand.png', + title: formatMessage('status.show_more', preferred_locale), +}); + +const actionReblog = preferred_locale => ({ + action: 'reblog', + icon: '/web-push-icon_reblog.png', + title: formatMessage('status.reblog', preferred_locale), +}); + +const actionFavourite = preferred_locale => ({ + action: 'favourite', + icon: '/web-push-icon_favourite.png', + title: formatMessage('status.favourite', preferred_locale), +}); const findBestClient = clients => { const focusedClient = clients.find(client => client.focused); @@ -99,6 +141,24 @@ const findBestClient = clients => { return focusedClient || visibleClient || clients[0]; }; +const expandNotification = notification => { + const newNotification = cloneNotification(notification); + + newNotification.body = newNotification.data.hiddenBody; + newNotification.image = newNotification.data.hiddenImage; + newNotification.actions = [actionReblog(notification.data.preferred_locale), actionFavourite(notification.data.preferred_locale)]; + + return self.registration.showNotification(newNotification.title, newNotification); +}; + +const removeActionFromNotification = (notification, action) => { + const newNotification = cloneNotification(notification); + + newNotification.actions = newNotification.actions.filter(item => item.action !== action); + + return self.registration.showNotification(newNotification.title, newNotification); +}; + const openUrl = url => self.clients.matchAll({ type: 'window' }).then(clientList => { if (clientList.length !== 0) { @@ -124,27 +184,19 @@ const openUrl = url => return self.clients.openWindow(url); }); -const removeActionFromNotification = (notification, action) => { - const actions = notification.actions.filter(act => act.action !== action.action); - const nextNotification = cloneNotification(notification); - - nextNotification.actions = actions; - - return self.registration.showNotification(nextNotification.title, nextNotification); -}; - const handleNotificationClick = (event) => { const reactToNotificationClick = new Promise((resolve, reject) => { if (event.action) { - const action = event.notification.data.actions.find(({ action }) => action === event.action); - - if (action.todo === 'expand') { + if (event.action === 'expand') { resolve(expandNotification(event.notification)); - } else if (action.todo === 'request') { - resolve(makeRequest(event.notification, action) - .then(() => removeActionFromNotification(event.notification, action))); + } else if (event.action === 'reblog') { + const { data } = event.notification; + resolve(fetchFromApi(`/api/v1/statuses/${data.id}/reblog`, 'post', data.access_token).then(() => removeActionFromNotification(event.notification, 'reblog'))); + } else if (event.action === 'favourite') { + const { data } = event.notification; + resolve(fetchFromApi(`/api/v1/statuses/${data.id}/favourite`, 'post', data.access_token).then(() => removeActionFromNotification(event.notification, 'favourite'))); } else { - reject(`Unknown action: ${action.todo}`); + reject(`Unknown action: ${event.action}`); } } else { event.notification.close(); diff --git a/app/javascript/mastodon/storage/modifier.js b/app/javascript/mastodon/storage/modifier.js index c2ed6f807e..9fadabef44 100644 --- a/app/javascript/mastodon/storage/modifier.js +++ b/app/javascript/mastodon/storage/modifier.js @@ -4,6 +4,11 @@ const accountAssetKeys = ['avatar', 'avatar_static', 'header', 'header_static']; const storageMargin = 8388608; const storeLimit = 1024; +// navigator.storage is not present on: +// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.100 Safari/537.36 Edge/16.16299 +// estimate method is not present on Chrome 57.0.2987.98 on Linux. +export const storageFreeable = 'storage' in navigator && 'estimate' in navigator.storage; + function openCache() { // ServiceWorker and Cache API is not available on iOS 11 // https://webkit.org/status/#specification-service-workers @@ -182,7 +187,7 @@ export function putStatuses(records) { } export function freeStorage() { - return navigator.storage.estimate().then(({ quota, usage }) => { + return storageFreeable && navigator.storage.estimate().then(({ quota, usage }) => { if (usage + storageMargin < quota) { return null; } diff --git a/app/javascript/mastodon/stream.js b/app/javascript/mastodon/stream.js index 6c67ba2755..9928d0dd76 100644 --- a/app/javascript/mastodon/stream.js +++ b/app/javascript/mastodon/stream.js @@ -1,21 +1,24 @@ import WebSocketClient from 'websocket.js'; +const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max)); + export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onDisconnect() {}, onReceive() {} })) { return (dispatch, getState) => { const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']); const accessToken = getState().getIn(['meta', 'access_token']); const { onDisconnect, onReceive } = callbacks(dispatch, getState); + let polling = null; const setupPolling = () => { - polling = setInterval(() => { - pollingRefresh(dispatch); - }, 20000); + pollingRefresh(dispatch, () => { + polling = setTimeout(() => setupPolling(), 20000 + randomIntUpTo(20000)); + }); }; const clearPolling = () => { if (polling) { - clearInterval(polling); + clearTimeout(polling); polling = null; } }; @@ -29,8 +32,9 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({ disconnected () { if (pollingRefresh) { - setupPolling(); + polling = setTimeout(() => setupPolling(), randomIntUpTo(40000)); } + onDisconnect(); }, @@ -51,6 +55,7 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({ if (subscription) { subscription.close(); } + clearPolling(); }; diff --git a/app/javascript/mastodon/utils/html.js b/app/javascript/mastodon/utils/html.js new file mode 100644 index 0000000000..0b646ce58f --- /dev/null +++ b/app/javascript/mastodon/utils/html.js @@ -0,0 +1,6 @@ +export const unescapeHTML = (html) => { + const wrapper = document.createElement('div'); + html = html.replace(/||\n/g, ' '); + wrapper.innerHTML = html; + return wrapper.textContent; +}; diff --git a/app/javascript/mastodon/utils/resize_image.js b/app/javascript/mastodon/utils/resize_image.js index 6442eda38c..279a858cad 100644 --- a/app/javascript/mastodon/utils/resize_image.js +++ b/app/javascript/mastodon/utils/resize_image.js @@ -1,3 +1,5 @@ +import EXIF from 'exif-js'; + const MAX_IMAGE_DIMENSION = 1280; const getImageUrl = inputFile => new Promise((resolve, reject) => { @@ -28,6 +30,73 @@ const loadImage = inputFile => new Promise((resolve, reject) => { }).catch(reject); }); +const getOrientation = (img, type = 'image/png') => new Promise(resolve => { + if (type !== 'image/jpeg') { + resolve(1); + return; + } + + EXIF.getData(img, () => { + const orientation = EXIF.getTag(img, 'Orientation'); + resolve(orientation); + }); +}); + +const processImage = (img, { width, height, orientation, type = 'image/png' }) => new Promise(resolve => { + const canvas = document.createElement('canvas'); + + if (4 < orientation && orientation < 9) { + canvas.width = height; + canvas.height = width; + } else { + canvas.width = width; + canvas.height = height; + } + + const context = canvas.getContext('2d'); + + switch (orientation) { + case 2: context.transform(-1, 0, 0, 1, width, 0); break; + case 3: context.transform(-1, 0, 0, -1, width, height); break; + case 4: context.transform(1, 0, 0, -1, 0, height); break; + case 5: context.transform(0, 1, 1, 0, 0, 0); break; + case 6: context.transform(0, 1, -1, 0, height, 0); break; + case 7: context.transform(0, -1, -1, 0, height, width); break; + case 8: context.transform(0, -1, 1, 0, 0, width); break; + } + + context.drawImage(img, 0, 0, width, height); + + canvas.toBlob(resolve, type); +}); + +const resizeImage = (img, type = 'image/png') => new Promise((resolve, reject) => { + const { width, height } = img; + + let newWidth, newHeight; + + if (width > height) { + newHeight = height * MAX_IMAGE_DIMENSION / width; + newWidth = MAX_IMAGE_DIMENSION; + } else if (height > width) { + newWidth = width * MAX_IMAGE_DIMENSION / height; + newHeight = MAX_IMAGE_DIMENSION; + } else { + newWidth = MAX_IMAGE_DIMENSION; + newHeight = MAX_IMAGE_DIMENSION; + } + + getOrientation(img, type) + .then(orientation => processImage(img, { + width: newWidth, + height: newHeight, + orientation, + type, + })) + .then(resolve) + .catch(reject); +}); + export default inputFile => new Promise((resolve, reject) => { if (!inputFile.type.match(/image.*/) || inputFile.type === 'image/gif') { resolve(inputFile); @@ -35,32 +104,13 @@ export default inputFile => new Promise((resolve, reject) => { } loadImage(inputFile).then(img => { - const canvas = document.createElement('canvas'); - const { width, height } = img; - - let newWidth, newHeight; - - if (width < MAX_IMAGE_DIMENSION && height < MAX_IMAGE_DIMENSION) { + if (img.width < MAX_IMAGE_DIMENSION && img.height < MAX_IMAGE_DIMENSION) { resolve(inputFile); return; } - if (width > height) { - newHeight = height * MAX_IMAGE_DIMENSION / width; - newWidth = MAX_IMAGE_DIMENSION; - } else if (height > width) { - newWidth = width * MAX_IMAGE_DIMENSION / height; - newHeight = MAX_IMAGE_DIMENSION; - } else { - newWidth = MAX_IMAGE_DIMENSION; - newHeight = MAX_IMAGE_DIMENSION; - } - - canvas.width = newWidth; - canvas.height = newHeight; - - canvas.getContext('2d').drawImage(img, 0, 0, newWidth, newHeight); - - canvas.toBlob(resolve, inputFile.type); + resizeImage(img, inputFile.type) + .then(resolve) + .catch(() => resolve(inputFile)); }).catch(reject); }); diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index 3377c23291..1e6ee62af2 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -6,8 +6,6 @@ function main() { const emojify = require('../mastodon/features/emoji/emoji').default; const { getLocale } = require('../mastodon/locales'); const { localeData } = getLocale(); - const VideoContainer = require('../mastodon/containers/video_container').default; - const CardContainer = require('../mastodon/containers/card_container').default; const React = require('react'); const ReactDOM = require('react-dom'); @@ -52,24 +50,16 @@ function main() { }); }); - [].forEach.call(document.querySelectorAll('[data-component="Video"]'), (content) => { - const props = JSON.parse(content.getAttribute('data-props')); - ReactDOM.render(, content); - }); + const reactComponents = document.querySelectorAll('[data-component]'); + if (reactComponents.length > 0) { + import(/* webpackChunkName: "containers/media_container" */ '../mastodon/containers/media_container') + .then(({ default: MediaContainer }) => { + const content = document.createElement('div'); - [].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => { - const props = JSON.parse(content.getAttribute('data-props')); - ReactDOM.render(, content); - }); - - const mediaGalleries = document.querySelectorAll('[data-component="MediaGallery"]'); - - if (mediaGalleries.length > 0) { - const MediaGalleriesContainer = require('../mastodon/containers/media_galleries_container').default; - const content = document.createElement('div'); - - ReactDOM.render(, content); - document.body.appendChild(content); + ReactDOM.render(, content); + document.body.appendChild(content); + }) + .catch(error => console.error(error)); } }); } diff --git a/app/javascript/skins/glitch/mastodon-light/common.scss b/app/javascript/skins/glitch/mastodon-light/common.scss new file mode 100644 index 0000000000..c37f407b3e --- /dev/null +++ b/app/javascript/skins/glitch/mastodon-light/common.scss @@ -0,0 +1 @@ +@import 'flavours/glitch/styles/mastodon-light'; diff --git a/app/javascript/skins/glitch/mastodon-light/names.yml b/app/javascript/skins/glitch/mastodon-light/names.yml new file mode 100644 index 0000000000..f15424f2bb --- /dev/null +++ b/app/javascript/skins/glitch/mastodon-light/names.yml @@ -0,0 +1,5 @@ +en: + skins: + glitch: + mastodon-light: Mastodon (light) + diff --git a/app/javascript/styles/mastodon-light.scss b/app/javascript/styles/mastodon-light.scss index 6a22a78226..756a12d868 100644 --- a/app/javascript/styles/mastodon-light.scss +++ b/app/javascript/styles/mastodon-light.scss @@ -1,228 +1,3 @@ -// Set variables -$ui-base-color: #d9e1e8; -$ui-base-lighter-color: darken($ui-base-color, 57%); -$ui-highlight-color: #2b90d9; -$ui-primary-color: darken($ui-highlight-color, 28%); -$ui-secondary-color: #282c37; - -$primary-text-color: black; -$base-overlay-background: $ui-base-color; - -$login-button-color: white; -$account-background-color: white; - -// Import defaults +@import 'mastodon-light/variables'; @import 'application'; - -// Change the color of the log in button -.button { - &.button-alternative-2 { - color: $login-button-color; - } -} - -// Change columns' default background colors -.column { - > .scrollable { - background: lighten($ui-base-color, 13%); - } -} - -.drawer__inner { - background: $ui-base-color; -} - -.drawer__inner__mastodon { - background: $ui-base-color url('data:image/svg+xml;utf8,') no-repeat bottom / 100% auto; -} - -// Change the default appearance of the content warning button -.status__content, -.reply-indicator__content { - - .status__content__spoiler-link { - - background: darken($ui-base-color, 30%); - - &:hover { - background: darken($ui-base-color, 35%); - text-decoration: none; - } - - } - -} - -// Change the default appearance of the action buttons -.icon-button { - - &:hover, - &:active, - &:focus { - color: darken($ui-base-color, 40%); - transition: color 200ms ease-out; - } - - &.disabled { - color: darken($ui-base-color, 30%); - } - -} - -.status { - &.status-direct { - .icon-button.disabled { - color: darken($ui-base-color, 30%); - } - } -} - -button.icon-button i.fa-retweet { - &:hover { - background-image: url("data:image/svg+xml;utf8,"); - } -} - -button.icon-button.disabled i.fa-retweet { - background-image: url("data:image/svg+xml;utf8,"); -} - -// Change the colors used in the dropdown menu -.dropdown-menu { - background: $ui-base-color; -} - -.dropdown-menu__arrow { - - &.left { - border-left-color: $ui-base-color; - } - - &.top { - border-top-color: $ui-base-color; - } - - &.bottom { - border-bottom-color: $ui-base-color; - } - - &.right { - border-right-color: $ui-base-color; - } - -} - -.dropdown-menu__item { - a { - background: $ui-base-color; - color: $ui-secondary-color; - } -} - -// Change the default color of several parts of the compose form -.compose-form { - - .compose-form__warning { - color: lighten($ui-secondary-color, 65%); - } - - strong { - color: lighten($ui-secondary-color, 65%); - } - - .autosuggest-textarea__textarea, - .spoiler-input__input { - - color: darken($ui-base-color, 80%); - - &::placeholder { - color: darken($ui-base-color, 70%); - } - - } - - .compose-form__buttons-wrapper { - background: darken($ui-base-color, 10%); - } - - .privacy-dropdown__option { - color: $ui-primary-color; - } - - .privacy-dropdown__option__content { - - strong { - color: $ui-primary-color; - } - - } - -} - -// Change the default color used for the text in an empty column or on the error column -.empty-column-indicator, -.error-column { - color: darken($ui-base-color, 60%); -} - -// Change the default colors used on some parts of the profile pages -.activity-stream-tabs { - - background: $account-background-color; - - a { - &.active { - color: $ui-primary-color; - } - } - -} - -.activity-stream { - - .entry { - background: $account-background-color; - } - - .status.light { - - .status__content { - color: $primary-text-color; - } - - .display-name { - strong { - color: $primary-text-color; - } - } - - } - -} - -.accounts-grid { - .account-grid-card { - - .controls { - .icon-button { - color: $ui-secondary-color; - } - } - - .name { - a { - color: $primary-text-color; - } - } - - .username { - color: $ui-secondary-color; - } - - .account__header__content { - color: $primary-text-color; - } - - } -} - +@import 'mastodon-light/diff'; diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss new file mode 100644 index 0000000000..fe304317d9 --- /dev/null +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -0,0 +1,240 @@ +// Notes! +// Sass color functions, "darken" and "lighten" are automatically replaced. + +// Change the colors of button texts +.button { + color: $white; + + &.button-alternative-2 { + color: $white; + } +} + +// Change default background colors of columns +.column { + > .scrollable { + background: $white; + } +} + +.drawer__inner { + background: $ui-base-color; +} + +.drawer__inner__mastodon { + background: $ui-base-color url('data:image/svg+xml;utf8,') no-repeat bottom / 100% auto; +} + +.compose-form .compose-form__modifiers .compose-form__upload__actions .icon-button { + color: $ui-base-color; + + &:active, + &:focus, + &:hover { + color: darken($ui-base-color, 7%); + } +} + +.compose-form .compose-form__modifiers .compose-form__upload-description input { + color: $ui-base-color; + + &::placeholder { + color: $ui-base-color; + } +} + +.compose-form .compose-form__buttons-wrapper { + background: darken($ui-base-color, 6%); +} + +.focusable:focus { + background: $ui-base-color; +} + +.status.status-direct { + background: lighten($ui-base-color, 4%); +} + +.focusable:focus .status.status-direct { + background: lighten($ui-base-color, 8%); +} + +.detailed-status, +.detailed-status__action-bar { + background: darken($ui-base-color, 6%); +} + +// Change the background color of status__content__spoiler-link +.reply-indicator__content .status__content__spoiler-link, +.status__content .status__content__spoiler-link { + background: $ui-base-lighter-color; + + &:hover { + background: lighten($ui-base-lighter-color, 6%); + } +} + +// Change the colors used in the dropdown menu +.dropdown-menu { + background: $ui-base-color; +} + +.dropdown-menu__arrow { + &.left { + border-left-color: $ui-base-color; + } + + &.top { + border-top-color: $ui-base-color; + } + + &.bottom { + border-bottom-color: $ui-base-color; + } + + &.right { + border-right-color: $ui-base-color; + } +} + +.dropdown-menu__item { + a { + background: $ui-base-color; + color: $ui-secondary-color; + } +} + +// Change the text colors on inverted background +.privacy-dropdown__option.active .privacy-dropdown__option__content, +.privacy-dropdown__option.active .privacy-dropdown__option__content strong, +.privacy-dropdown__option:hover .privacy-dropdown__option__content, +.privacy-dropdown__option:hover .privacy-dropdown__option__content strong, +.dropdown-menu__item a:active, +.dropdown-menu__item a:focus, +.dropdown-menu__item a:hover, +.actions-modal ul li:not(:empty) a.active, +.actions-modal ul li:not(:empty) a.active button, +.actions-modal ul li:not(:empty) a:active, +.actions-modal ul li:not(:empty) a:active button, +.actions-modal ul li:not(:empty) a:focus, +.actions-modal ul li:not(:empty) a:focus button, +.actions-modal ul li:not(:empty) a:hover, +.actions-modal ul li:not(:empty) a:hover button, +.admin-wrapper .sidebar ul ul a.selected, +.simple_form .block-button, +.simple_form .button, +.simple_form button { + color: $white; +} + +// Change the background colors of modals +.actions-modal, +.boost-modal, +.confirmation-modal, +.mute-modal, +.report-modal, +.embed-modal, +.error-modal, +.onboarding-modal { + background: $ui-base-color; +} + +.boost-modal__action-bar, +.confirmation-modal__action-bar, +.mute-modal__action-bar, +.onboarding-modal__paginator, +.error-modal__footer { + background: darken($ui-base-color, 6%); + + .onboarding-modal__nav, + .error-modal__nav { + &:hover, + &:focus, + &:active { + background-color: darken($ui-base-color, 12%); + } + } +} + +.display-case__case { + background: $white; +} + +.embed-modal .embed-modal__container .embed-modal__html { + background: $white; + + &:focus { + background: darken($ui-base-color, 6%); + } +} + +.react-toggle-track { + background: $ui-secondary-color; +} + +.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track { + background: darken($ui-secondary-color, 10%); +} + +.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track { + background: lighten($ui-highlight-color, 10%); +} + +// Change the default color used for the text in an empty column or on the error column +.empty-column-indicator, +.error-column { + color: $primary-text-color; +} + +// Change the default colors used on some parts of the profile pages +.activity-stream-tabs { + background: $account-background-color; + + a { + &.active { + color: $ui-primary-color; + } + } +} + +.activity-stream { + .entry { + background: $account-background-color; + } + + .status.light { + .status__content { + color: $primary-text-color; + } + + .display-name { + strong { + color: $primary-text-color; + } + } + } +} + +.accounts-grid { + .account-grid-card { + .controls { + .icon-button { + color: $ui-secondary-color; + } + } + + .name { + a { + color: $primary-text-color; + } + } + + .username { + color: $ui-secondary-color; + } + + .account__header__content { + color: $primary-text-color; + } + } +} diff --git a/app/javascript/styles/mastodon-light/variables.scss b/app/javascript/styles/mastodon-light/variables.scss new file mode 100644 index 0000000000..9f6d470b13 --- /dev/null +++ b/app/javascript/styles/mastodon-light/variables.scss @@ -0,0 +1,41 @@ +// Dependent colors +$black: #000000; +$white: #ffffff; + +$classic-base-color: #282c37; +$classic-primary-color: #9baec8; +$classic-secondary-color: #d9e1e8; +$classic-highlight-color: #2b90d9; + +// Differences +$success-green: #3c754d; + +$base-overlay-background: $white !default; +$valid-value-color: $success-green !default; + +$ui-base-color: $classic-secondary-color !default; +$ui-base-lighter-color: #b0c0cf; +$ui-primary-color: #9bcbed; +$ui-secondary-color: $classic-base-color !default; +$ui-highlight-color: #2b5fd9; + +$primary-text-color: $black !default; +$darker-text-color: $classic-base-color !default; +$dark-text-color: #444b5d; +$action-button-color: #606984; + +$inverted-text-color: $black !default; +$lighter-text-color: $classic-base-color !default; +$light-text-color: #444b5d; + +//Newly added colors +$account-background-color: $white !default; + +//Invert darkened and lightened colors +@function darken($color, $amount) { + @return hsl(hue($color), saturation($color), lightness($color) + $amount); +} + +@function lighten($color, $amount) { + @return hsl(hue($color), saturation($color), lightness($color) - $amount); +} diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss index c9c0e3081a..77728995d1 100644 --- a/app/javascript/styles/mastodon/about.scss +++ b/app/javascript/styles/mastodon/about.scss @@ -396,7 +396,7 @@ $small-breakpoint: 960px; display: flex; justify-content: center; align-items: center; - color: $ui-primary-color; + color: $darker-text-color; text-decoration: none; padding: 12px 16px; line-height: 32px; diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index c2d0de4b99..3ccce383b0 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -1,5 +1,5 @@ .card { - background-color: lighten($ui-base-color, 4%); + background-color: $base-shadow-color; background-size: cover; background-position: center; border-radius: 4px 4px 0 0; @@ -79,6 +79,10 @@ font-weight: 400; overflow: hidden; text-overflow: ellipsis; + + .fa { + margin-left: 3px; + } } } @@ -322,6 +326,15 @@ z-index: 2; position: relative; + &.empty img { + position: absolute; + opacity: 0.2; + height: 200px; + left: 0; + bottom: 0; + pointer-events: none; + } + @media screen and (max-width: 740px) { border-radius: 0; box-shadow: none; @@ -438,8 +451,8 @@ font-size: 14px; font-weight: 500; text-align: center; - padding: 60px 0; - padding-top: 55px; + padding: 130px 0; + padding-top: 125px; margin: 0 auto; cursor: default; } @@ -565,36 +578,41 @@ } .account__header__fields { - border-collapse: collapse; padding: 0; margin: 15px -15px -15px; border: 0 none; border-top: 1px solid lighten($ui-base-color, 4%); border-bottom: 1px solid lighten($ui-base-color, 4%); + font-size: 14px; + line-height: 20px; - th, - td { - padding: 15px; - padding-left: 15px; - border: 0 none; + dl { + display: flex; border-bottom: 1px solid lighten($ui-base-color, 4%); - vertical-align: middle; } - th { - padding-left: 15px; - font-weight: 500; + dt, + dd { + box-sizing: border-box; + padding: 14px; text-align: center; - width: 94px; + max-height: 48px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + dt { + font-weight: 500; + width: 120px; + flex: 0 0 auto; color: $secondary-text-color; background: rgba(darken($ui-base-color, 8%), 0.5); } - td { + dd { + flex: 1 1 auto; color: $darker-text-color; - text-align: center; - width: 100%; - padding-left: 0; } a { @@ -608,12 +626,7 @@ } } - tr { - &:last-child { - th, - td { - border-bottom: 0; - } - } + dl:last-child { + border-bottom: 0; } } diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index a6cc8b62ba..560b11ddf4 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -336,7 +336,8 @@ } } -.simple_form.new_report_note { +.simple_form.new_report_note, +.simple_form.new_account_moderation_note { max-width: 100%; } @@ -483,19 +484,12 @@ } a.name-tag, -.name-tag { - display: flex; - align-items: center; +.name-tag, +a.inline-name-tag, +.inline-name-tag { text-decoration: none; color: $secondary-text-color; - .avatar { - display: block; - margin: 0; - margin-right: 5px; - border-radius: 50%; - } - .username { font-weight: 500; } @@ -513,6 +507,26 @@ a.name-tag, } } +a.name-tag, +.name-tag { + display: flex; + align-items: center; + + .avatar { + display: block; + margin: 0; + margin-right: 5px; + border-radius: 50%; + } + + &.suspended { + .avatar { + filter: grayscale(100%); + opacity: 0.8; + } + } +} + .speech-bubble { margin-bottom: 20px; border-left: 4px solid $ui-highlight-color; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index a982585c33..712b6f813f 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -425,7 +425,7 @@ .icon-button { flex: 0 1 auto; - color: $action-button-color; + color: $secondary-text-color; font-size: 14px; font-weight: 500; padding: 10px; @@ -434,7 +434,7 @@ &:hover, &:focus, &:active { - color: lighten($action-button-color, 7%); + color: lighten($secondary-text-color, 7%); } } @@ -1373,9 +1373,8 @@ a.account__display-name { } .notification__message { - margin-left: 68px; - padding: 8px 0; - padding-bottom: 0; + margin: 0 10px 0 68px; + padding: 8px 0 0; cursor: default; color: $darker-text-color; font-size: 15px; @@ -1984,6 +1983,7 @@ a.account__display-name { padding: 15px; margin: 0; z-index: 3; + outline: 0; &:hover { text-decoration: underline; @@ -3181,6 +3181,7 @@ a.status-card { &.active { background: $ui-highlight-color; color: $primary-text-color; + outline: 0; .privacy-dropdown__option__content { color: $primary-text-color; @@ -3283,6 +3284,15 @@ a.status-card { } .search__icon { + &::-moz-focus-inner { + border: 0; + } + + &::-moz-focus-inner, + &:focus { + outline: 0 !important; + } + .fa { position: absolute; top: 10px; @@ -3332,40 +3342,33 @@ a.status-card { .search-results__header { color: $dark-text-color; background: lighten($ui-base-color, 2%); - border-bottom: 1px solid darken($ui-base-color, 4%); - padding: 15px 10px; - font-size: 14px; + padding: 15px; font-weight: 500; + font-size: 16px; + cursor: default; + + .fa { + display: inline-block; + margin-right: 5px; + } } .search-results__section { - margin-bottom: 20px; + margin-bottom: 5px; h5 { - position: relative; + background: darken($ui-base-color, 4%); + border-bottom: 1px solid lighten($ui-base-color, 8%); + cursor: default; + display: flex; + padding: 15px; + font-weight: 500; + font-size: 16px; + color: $dark-text-color; - &::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 { + .fa { display: inline-block; - background: $ui-base-color; - color: $darker-text-color; - font-size: 14px; - font-weight: 500; - padding: 10px; - position: relative; - z-index: 1; - cursor: default; + margin-right: 5px; } } @@ -4033,7 +4036,7 @@ a.status-card { .report-modal__statuses { flex: 1 1 auto; min-height: 20vh; - max-height: 40vh; + max-height: 80vh; overflow-y: auto; overflow-x: hidden; @@ -4432,6 +4435,10 @@ a.status-card { max-width: 100%; border-radius: 4px; + &:focus { + outline: 0; + } + video { max-width: 100vw; max-height: 80vh; @@ -4732,6 +4739,8 @@ a.status-card { } } +.community-timeline__section-headline, +.public-timeline__section-headline, .account__section-headline { background: darken($ui-base-color, 4%); border-bottom: 1px solid lighten($ui-base-color, 8%); @@ -5159,39 +5168,124 @@ noscript { } } +.account__header .roles { + margin-top: 20px; + margin-bottom: 20px; + padding: 0 15px; +} + .account__header .account__header__fields { font-size: 14px; line-height: 20px; overflow: hidden; - border-collapse: collapse; margin: 20px -10px -20px; border-bottom: 0; - tr { + dl { border-top: 1px solid lighten($ui-base-color, 8%); - text-align: center; + display: flex; } - th, - td { + dt, + dd { + box-sizing: border-box; padding: 14px 20px; - vertical-align: middle; - max-height: 40px; + text-align: center; + max-height: 48px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } - th { + dt { color: $darker-text-color; background: darken($ui-base-color, 4%); - max-width: 120px; + width: 120px; + flex: 0 0 auto; font-weight: 500; } - td { - flex: auto; + dd { + flex: 1 1 auto; color: $primary-text-color; background: $ui-base-color; } } + +.trends { + &__header { + color: $dark-text-color; + background: lighten($ui-base-color, 2%); + border-bottom: 1px solid darken($ui-base-color, 4%); + font-weight: 500; + padding: 15px; + font-size: 16px; + cursor: default; + + .fa { + display: inline-block; + margin-right: 5px; + } + } + + &__item { + display: flex; + align-items: center; + padding: 15px; + border-bottom: 1px solid lighten($ui-base-color, 8%); + + &:last-child { + border-bottom: 0; + } + + &__name { + flex: 1 1 auto; + color: $dark-text-color; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + strong { + font-weight: 500; + } + + a { + color: $darker-text-color; + text-decoration: none; + font-size: 14px; + font-weight: 500; + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &:hover, + &:focus, + &:active { + span { + text-decoration: underline; + } + } + } + } + + &__current { + flex: 0 0 auto; + width: 100px; + font-size: 24px; + line-height: 36px; + font-weight: 500; + text-align: center; + color: $secondary-text-color; + } + + &__sparkline { + flex: 0 0 auto; + width: 50px; + + path { + stroke: lighten($highlight-text-color, 6%) !important; + } + } + } +} diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index 9d5ab66a48..ac648c8680 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -60,7 +60,7 @@ } } -.media-gallery-standalone__body { +.media-standalone__body { overflow: hidden; } diff --git a/app/javascript/styles/mastodon/footer.scss b/app/javascript/styles/mastodon/footer.scss index ba2a06954e..fe2d40c0cc 100644 --- a/app/javascript/styles/mastodon/footer.scss +++ b/app/javascript/styles/mastodon/footer.scss @@ -4,7 +4,7 @@ font-size: 12px; color: $darker-text-color; - .domain { + .footer__domain { font-weight: 500; a { @@ -26,5 +26,13 @@ text-decoration: none; } } + + img { + margin: 0 4px; + position: relative; + bottom: -1px; + height: 18px; + vertical-align: top; + } } } diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index f978901870..de16784a87 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -87,6 +87,10 @@ code { align-items: flex-start; } + &.file .label_input { + flex-wrap: nowrap; + } + &.select .label_input { align-items: initial; } diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index fa876e6031..982bfd9900 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -1,3 +1,9 @@ +@keyframes Swag { + 0% { background-position: 0% 0%; } + 50% { background-position: 100% 0%; } + 100% { background-position: 200% 0%; } +} + .table { width: 100%; max-width: 100%; @@ -187,6 +193,11 @@ a.table-action-link { strong { font-weight: 700; + background: linear-gradient(to right, orange , yellow, green, cyan, blue, violet,orange , yellow, green, cyan, blue, violet); + background-size: 200% 100%; + background-clip: text; + color: transparent; + animation: Swag 2s linear 0s infinite; } } } diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/variables.scss index cbefe35b4d..40aeb4afcf 100644 --- a/app/javascript/styles/mastodon/variables.scss +++ b/app/javascript/styles/mastodon/variables.scss @@ -1,10 +1,10 @@ // Commonly used web colors $black: #000000; // Black $white: #ffffff; // White -$success-green: #79bd9a; // Padua -$error-red: #df405a; // Cerise -$warning-red: #ff5050; // Sunset Orange -$gold-star: #ca8f04; // Dark Goldenrod +$success-green: #79bd9a !default; // Padua +$error-red: #df405a !default; // Cerise +$warning-red: #ff5050 !default; // Sunset Orange +$gold-star: #ca8f04 !default; // Dark Goldenrod // Values from the classic Mastodon UI $classic-base-color: #282c37; // Midnight Express diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 84d4b17520..03476920b2 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -118,4 +118,13 @@ class ActivityPub::Activity def delete_later!(uri) redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri) end + + def fetch_remote_original_status + if object_uri.start_with?('http') + return if ActivityPub::TagManager.instance.local_uri?(object_uri) + ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true, on_behalf_of: @account.followers.local.first) + elsif @object['url'].present? + ::FetchRemoteStatusService.new.call(@object['url']) + end + end end diff --git a/app/lib/activitypub/activity/add.rb b/app/lib/activitypub/activity/add.rb index ea94d2f983..688ab00b33 100644 --- a/app/lib/activitypub/activity/add.rb +++ b/app/lib/activitypub/activity/add.rb @@ -4,9 +4,10 @@ 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) + status = status_from_uri(object_uri) + status ||= fetch_remote_original_status - return unless status.account_id == @account.id && !@account.pinned?(status) + return unless !status.nil? && status.account_id == @account.id && !@account.pinned?(status) StatusPin.create!(account: @account, status: status) end diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb index 7e146ea8c4..1147a4481f 100644 --- a/app/lib/activitypub/activity/announce.rb +++ b/app/lib/activitypub/activity/announce.rb @@ -26,16 +26,6 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity private - def fetch_remote_original_status - if object_uri.start_with?('http') - return if ActivityPub::TagManager.instance.local_uri?(object_uri) - - ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true) - elsif @object['url'].present? - ::FetchRemoteStatusService.new.call(@object['url']) - end - end - def announceable?(status) status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility? end diff --git a/app/lib/activitypub/activity/block.rb b/app/lib/activitypub/activity/block.rb index f630d5db2a..26da8bdf5c 100644 --- a/app/lib/activitypub/activity/block.rb +++ b/app/lib/activitypub/activity/block.rb @@ -7,6 +7,6 @@ class ActivityPub::Activity::Block < ActivityPub::Activity return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id']) || @account.blocking?(target_account) UnfollowService.new.call(target_account, @account) if target_account.following?(@account) - @account.block!(target_account) + @account.block!(target_account, uri: @json['id']) end end diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 8d17a4ebe2..869749f1ec 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -11,6 +11,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity if lock.acquired? @status = find_existing_status process_status if @status.nil? + else + raise Mastodon::RaceConditionError end end @@ -76,9 +78,12 @@ class ActivityPub::Activity::Create < ActivityPub::Activity return if tag['name'].blank? hashtag = tag['name'].gsub(/\A#/, '').mb_chars.downcase - hashtag = Tag.where(name: hashtag).first_or_initialize(name: hashtag) + hashtag = Tag.where(name: hashtag).first_or_create(name: hashtag) + + return if status.tags.include?(hashtag) status.tags << hashtag + TrendingTags.record_use!(hashtag, status.account, status.created_at) rescue ActiveRecord::RecordInvalid nil end diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb index 8adbbb9c33..fbbf358a87 100644 --- a/app/lib/activitypub/activity/follow.rb +++ b/app/lib/activitypub/activity/follow.rb @@ -12,7 +12,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity return end - follow_request = FollowRequest.create!(account: @account, target_account: target_account) + follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id']) if target_account.locked? NotifyService.new.call(target_account, follow_request) diff --git a/app/lib/activitypub/activity/remove.rb b/app/lib/activitypub/activity/remove.rb index 62a1e3196e..f523ead9f6 100644 --- a/app/lib/activitypub/activity/remove.rb +++ b/app/lib/activitypub/activity/remove.rb @@ -6,7 +6,7 @@ class ActivityPub::Activity::Remove < ActivityPub::Activity status = status_from_uri(object_uri) - return unless status.account_id == @account.id + return unless !status.nil? && status.account_id == @account.id pin = StatusPin.find_by(account: @account, status: status) pin&.destroy! diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index fa2a8f7d31..95d1cf9f35 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -38,6 +38,10 @@ class ActivityPub::TagManager end end + def generate_uri_for(_target) + URI.join(root_url, 'payloads', SecureRandom.uuid) + end + def activity_uri_for(target) raise ArgumentError, 'target must be a local activity' unless %i(note comment activity).include?(target.object_type) && target.local? @@ -82,6 +86,8 @@ class ActivityPub::TagManager end def local_uri?(uri) + return false if uri.nil? + uri = Addressable::URI.parse(uri) host = uri.normalized_host host = "#{host}:#{uri.port}" if uri.port @@ -95,6 +101,8 @@ class ActivityPub::TagManager end def uri_to_resource(uri, klass) + return if uri.nil? + if local_uri?(uri) case klass.name when 'Account' diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 050c651ee9..e1ab05cc0b 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -67,9 +67,17 @@ class Formatter html.html_safe # rubocop:disable Rails/OutputSafety end - def format_field(account, str) + def format_display_name(account, **options) + html = encode(account.display_name.presence || account.username) + html = encode_custom_emojis(html, account.emojis) if options[:custom_emojify] + html.html_safe # rubocop:disable Rails/OutputSafety + end + + def format_field(account, str, **options) return reformat(str).html_safe unless account.local? # rubocop:disable Rails/OutputSafety - encode_and_link_urls(str, me: true).html_safe # rubocop:disable Rails/OutputSafety + html = encode_and_link_urls(str, me: true) + html = encode_custom_emojis(html, account.emojis) if options[:custom_emojify] + html.html_safe # rubocop:disable Rails/OutputSafety end def linkify(text) diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb index 1e7f470299..d3a303a0c4 100644 --- a/app/lib/ostatus/activity/creation.rb +++ b/app/lib/ostatus/activity/creation.rb @@ -15,6 +15,8 @@ class OStatus::Activity::Creation < OStatus::Activity::Base @status = find_status(id) return [@status, false] unless @status.nil? @status = process_status + else + raise Mastodon::RaceConditionError end end @@ -46,7 +48,8 @@ class OStatus::Activity::Creation < OStatus::Activity::Base visibility: visibility_scope, conversation: find_or_create_conversation, thread: thread? ? find_status(thread.first) || find_activitypub_status(thread.first, thread.second) : nil, - media_attachment_ids: media_attachments.map(&:id) + media_attachment_ids: media_attachments.map(&:id), + sensitive: sensitive? ) save_mentions(status) @@ -105,6 +108,11 @@ class OStatus::Activity::Creation < OStatus::Activity::Base private + def sensitive? + # OStatus-specific convention (not standard) + @xml.xpath('./xmlns:category', xmlns: OStatus::TagManager::XMLNS).any? { |category| category['term'] == 'nsfw' } + end + def find_or_create_conversation uri = @xml.at_xpath('./ostatus:conversation', ostatus: OStatus::TagManager::OS_XMLNS)&.attribute('ref')&.content return if uri.nil? diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb index 7c66f2066e..5c6ff4f9b9 100644 --- a/app/lib/ostatus/atom_serializer.rb +++ b/app/lib/ostatus/atom_serializer.rb @@ -354,7 +354,7 @@ class OStatus::AtomSerializer append_element(entry, 'summary', status.spoiler_text, 'xml:lang': status.language) if status.spoiler_text? append_element(entry, 'content', Formatter.instance.format(status).to_str, type: 'html', 'xml:lang': status.language) - status.mentions.order(:id).each do |mentioned| + status.mentions.sort_by(&:id).each do |mentioned| append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:person], href: OStatus::TagManager.instance.uri_for(mentioned.account)) end @@ -368,6 +368,7 @@ class OStatus::AtomSerializer append_element(entry, 'link', nil, rel: :enclosure, type: media.file_content_type, length: media.file_file_size, href: full_asset_url(media.file.url(:original, false))) end + append_element(entry, 'category', nil, term: 'nsfw') if status.sensitive? && status.media_attachments.any? append_element(entry, 'mastodon:scope', status.visibility) status.emojis.each do |emoji| diff --git a/app/lib/request.rb b/app/lib/request.rb index 00f94dacf5..397614fac9 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -57,10 +57,11 @@ class Request private def set_common_headers! - @headers[REQUEST_TARGET] = "#{@verb} #{@url.path}" - @headers['User-Agent'] = user_agent - @headers['Host'] = @url.host - @headers['Date'] = Time.now.utc.httpdate + @headers[REQUEST_TARGET] = "#{@verb} #{@url.path}" + @headers['User-Agent'] = Mastodon::Version.user_agent + @headers['Host'] = @url.host + @headers['Date'] = Time.now.utc.httpdate + @headers['Accept-Encoding'] = 'gzip' if @verb != :head end def set_digest! @@ -82,10 +83,6 @@ class Request @headers.keys.join(' ').downcase end - def user_agent - @user_agent ||= "#{HTTP::Request::USER_AGENT} (Mastodon/#{Mastodon::Version}; +#{root_url})" - end - def key_id case @key_id_format when :acct @@ -100,7 +97,7 @@ class Request end def http_client - @http_client ||= HTTP.timeout(:per_operation, timeout).follow(max_hops: 2) + @http_client ||= HTTP.use(:auto_inflate).timeout(:per_operation, timeout).follow(max_hops: 2) end def use_proxy? diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 78b3aa77cc..f8bacb036c 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -30,6 +30,7 @@ class UserSettingsDecorator 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['hide_network'] = hide_network_preference if change?('setting_hide_network') end def merged_notification_emails @@ -92,6 +93,10 @@ class UserSettingsDecorator settings['setting_skin'] end + def hide_network_preference + boolean_cast_setting 'setting_hide_network' + end + def boolean_cast_setting(key) ActiveModel::Type::Boolean.new.cast(settings[key]) end diff --git a/app/models/account.rb b/app/models/account.rb index c1ce1e99e8..48f2847858 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -45,6 +45,7 @@ # moved_to_account_id :bigint(8) # featured_collection_url :string # fields :jsonb +# actor_type :string # class Account < ApplicationRecord @@ -76,6 +77,7 @@ class Account < ApplicationRecord validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? } validates :display_name, length: { maximum: 30 }, if: -> { local? && will_save_change_to_display_name? } validate :note_length_does_not_exceed_length_limit, if: -> { local? && will_save_change_to_note? } + validates :fields, length: { maximum: 4 }, if: -> { local? && will_save_change_to_fields? } # Timelines has_many :stream_entries, inverse_of: :account, dependent: :destroy @@ -137,6 +139,7 @@ class Account < ApplicationRecord :moderator?, :staff?, :locale, + :hides_network?, to: :user, prefix: true, allow_nil: true @@ -151,6 +154,16 @@ class Account < ApplicationRecord moved_to_account_id.present? end + def bot? + %w(Application Service).include? actor_type + end + + alias bot bot? + + def bot=(val) + self.actor_type = ActiveModel::Type::Boolean.new.cast(val) ? 'Service' : 'Person' + end + def acct local? ? username : "#{username}@#{domain}" end @@ -201,9 +214,11 @@ class Account < ApplicationRecord def fields_attributes=(attributes) fields = [] - attributes.each_value do |attr| - next if attr[:name].blank? - fields << attr + if attributes.is_a?(Hash) + attributes.each_value do |attr| + next if attr[:name].blank? + fields << attr + end end self[:fields] = fields @@ -272,8 +287,8 @@ class Account < ApplicationRecord def initialize(account, attr) @account = account - @name = attr['name'] - @value = attr['value'] + @name = attr['name'].strip[0, 255] + @value = attr['value'].strip[0, 255] @errors = {} end @@ -398,7 +413,7 @@ class Account < ApplicationRecord end def emojis - @emojis ||= CustomEmoji.from_text(note, domain) + @emojis ||= CustomEmoji.from_text(emojifiable_text, domain) end before_create :generate_keys @@ -441,4 +456,8 @@ class Account < ApplicationRecord self.domain = TagManager.instance.normalize_domain(domain) end + + def emojifiable_text + [note, display_name, fields.map(&:value)].join(' ') + end end diff --git a/app/models/block.rb b/app/models/block.rb index df4a6bbacb..bf3e076003 100644 --- a/app/models/block.rb +++ b/app/models/block.rb @@ -8,6 +8,7 @@ # updated_at :datetime not null # account_id :bigint(8) not null # target_account_id :bigint(8) not null +# uri :string # class Block < ApplicationRecord @@ -19,7 +20,12 @@ class Block < ApplicationRecord validates :account_id, uniqueness: { scope: :target_account_id } + def local? + false # Force uri_for to use uri attribute + end + after_commit :remove_blocking_cache + before_validation :set_uri, only: :create private @@ -27,4 +33,8 @@ class Block < ApplicationRecord Rails.cache.delete("exclude_account_ids_for:#{account_id}") Rails.cache.delete("exclude_account_ids_for:#{target_account_id}") end + + def set_uri + self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? + end end diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 20fc74ba6a..a064248d91 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -82,16 +82,19 @@ module AccountInteractions has_many :domain_blocks, class_name: 'AccountDomainBlock', dependent: :destroy end - def follow!(other_account, reblogs: nil) + def follow!(other_account, reblogs: nil, uri: nil) reblogs = true if reblogs.nil? - rel = active_relationships.create_with(show_reblogs: reblogs).find_or_create_by!(target_account: other_account) - rel.update!(show_reblogs: reblogs) + rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri) + .find_or_create_by!(target_account: other_account) + + rel.update!(show_reblogs: reblogs) rel end - def block!(other_account) - block_relationships.find_or_create_by!(target_account: other_account) + def block!(other_account, uri: nil) + block_relationships.create_with(uri: uri) + .find_or_create_by!(target_account: other_account) end def mute!(other_account, notifications: nil) diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb index 7f1ef5191b..c17f047760 100644 --- a/app/models/concerns/remotable.rb +++ b/app/models/concerns/remotable.rb @@ -41,6 +41,9 @@ module Remotable rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e Rails.logger.debug "Error fetching remote #{attachment_name}: #{e}" nil + rescue Paperclip::Error, Mastodon::DimensionsValidationError => e + Rails.logger.debug "Error processing remote #{attachment_name}: #{e}" + nil end end diff --git a/app/models/concerns/status_threading_concern.rb b/app/models/concerns/status_threading_concern.rb index 8e817be00c..1ba8fc6939 100644 --- a/app/models/concerns/status_threading_concern.rb +++ b/app/models/concerns/status_threading_concern.rb @@ -74,16 +74,7 @@ module StatusThreadingConcern statuses = statuses_with_accounts(ids).to_a account_ids = statuses.map(&:account_id).uniq domains = statuses.map(&:account_domain).compact.uniq - - relations = if account.present? - { - blocking: Account.blocking_map(account_ids, account.id), - blocked_by: Account.blocked_by_map(account_ids, account.id), - muting: Account.muting_map(account_ids, account.id), - following: Account.following_map(account_ids, account.id), - domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id), - } - end + relations = relations_map_for_account(account, account_ids, domains) statuses.reject! { |status| filter_from_context?(status, account, relations) } @@ -91,6 +82,18 @@ module StatusThreadingConcern statuses.sort_by! { |status| ids.index(status.id) } end + def relations_map_for_account(account, account_ids, domains) + return {} if account.nil? + + { + blocking: Account.blocking_map(account_ids, account.id), + blocked_by: Account.blocked_by_map(account_ids, account.id), + muting: Account.muting_map(account_ids, account.id), + following: Account.following_map(account_ids, account.id), + domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id), + } + end + def statuses_with_accounts(ids) Status.where(id: ids).includes(:account) end diff --git a/app/models/follow.rb b/app/models/follow.rb index 2ca42ff70b..eaf8445f3b 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -9,6 +9,7 @@ # account_id :bigint(8) not null # target_account_id :bigint(8) not null # show_reblogs :boolean default(TRUE), not null +# uri :string # class Follow < ApplicationRecord @@ -26,4 +27,16 @@ class Follow < ApplicationRecord validates :account_id, uniqueness: { scope: :target_account_id } scope :recent, -> { reorder(id: :desc) } + + def local? + false # Force uri_for to use uri attribute + end + + before_validation :set_uri, only: :create + + private + + def set_uri + self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? + end end diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index d559a8f62f..9c4875564b 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -9,6 +9,7 @@ # account_id :bigint(8) not null # target_account_id :bigint(8) not null # show_reblogs :boolean default(TRUE), not null +# uri :string # class FollowRequest < ApplicationRecord @@ -23,11 +24,22 @@ class FollowRequest < ApplicationRecord validates :account_id, uniqueness: { scope: :target_account_id } def authorize! - account.follow!(target_account, reblogs: show_reblogs) + account.follow!(target_account, reblogs: show_reblogs, uri: uri) MergeWorker.perform_async(target_account.id, account.id) - destroy! end alias reject! destroy! + + def local? + false # Force uri_for to use uri attribute + end + + before_validation :set_uri, only: :create + + private + + def set_uri + self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? + end end diff --git a/app/models/status.rb b/app/models/status.rb index 0b3a7c0aaa..c6d6453df6 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -195,12 +195,45 @@ class Status < ApplicationRecord where(account: [account] + account.following).where(visibility: [:public, :unlisted, :private]) end - def as_direct_timeline(account) - query = joins("LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = #{account.id}") - .where("mentions.account_id = #{account.id} OR statuses.account_id = #{account.id}") - .where(visibility: [:direct]) + def as_direct_timeline(account, limit = 20, max_id = nil, since_id = nil, cache_ids = false) + # direct timeline is mix of direct message from_me and to_me. + # 2 querys are executed with pagination. + # constant expression using arel_table is required for partial index - apply_timeline_filters(query, account, false) + # _from_me part does not require any timeline filters + query_from_me = where(account_id: account.id) + .where(Status.arel_table[:visibility].eq(3)) + .limit(limit) + .order('statuses.id DESC') + + # _to_me part requires mute and block filter. + # FIXME: may we check mutes.hide_notifications? + query_to_me = Status + .joins(:mentions) + .merge(Mention.where(account_id: account.id)) + .where(Status.arel_table[:visibility].eq(3)) + .limit(limit) + .order('mentions.status_id DESC') + .not_excluded_by_account(account) + + if max_id.present? + query_from_me = query_from_me.where('statuses.id < ?', max_id) + query_to_me = query_to_me.where('mentions.status_id < ?', max_id) + end + + if since_id.present? + query_from_me = query_from_me.where('statuses.id > ?', since_id) + query_to_me = query_to_me.where('mentions.status_id > ?', since_id) + end + + if cache_ids + # returns array of cache_ids object that have id and updated_at + (query_from_me.cache_ids.to_a + query_to_me.cache_ids.to_a).uniq(&:id).sort_by(&:id).reverse.take(limit) + else + # returns ActiveRecord.Relation + items = (query_from_me.select(:id).to_a + query_to_me.select(:id).to_a).uniq(&:id).sort_by(&:id).reverse.take(limit) + Status.where(id: items.map(&:id)) + end end def as_public_timeline(account = nil, local_only = false) diff --git a/app/models/tag.rb b/app/models/tag.rb index 8b1b024120..4f31f796e6 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -21,6 +21,22 @@ class Tag < ApplicationRecord name end + def history + days = [] + + 7.times do |i| + day = i.days.ago.beginning_of_day.to_i + + days << { + day: day.to_s, + uses: Redis.current.get("activity:tags:#{id}:#{day}") || '0', + accounts: Redis.current.pfcount("activity:tags:#{id}:#{day}:accounts").to_s, + } + end + + days + end + class << self def search_for(term, limit = 5) pattern = sanitize_sql_like(term.strip) + '%' diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb new file mode 100644 index 0000000000..eedd92644c --- /dev/null +++ b/app/models/trending_tags.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +class TrendingTags + KEY = 'trending_tags' + HALF_LIFE = 1.day.to_i + MAX_ITEMS = 500 + EXPIRE_HISTORY_AFTER = 7.days.seconds + + class << self + def record_use!(tag, account, at_time = Time.now.utc) + return if disallowed_hashtags.include?(tag.name) || account.silenced? + + increment_vote!(tag.id, at_time) + increment_historical_use!(tag.id, at_time) + increment_unique_use!(tag.id, account.id, at_time) + end + + def get(limit) + tag_ids = redis.zrevrange(KEY, 0, limit).map(&:to_i) + tags = Tag.where(id: tag_ids).to_a.map { |tag| [tag.id, tag] }.to_h + tag_ids.map { |tag_id| tags[tag_id] }.compact + end + + private + + def increment_vote!(tag_id, at_time) + redis.zincrby(KEY, (2**((at_time.to_i - epoch) / HALF_LIFE)).to_f, tag_id.to_s) + redis.zremrangebyrank(KEY, 0, -MAX_ITEMS) if rand < (2.to_f / MAX_ITEMS) + end + + def increment_historical_use!(tag_id, at_time) + key = "activity:tags:#{tag_id}:#{at_time.beginning_of_day.to_i}" + redis.incrby(key, 1) + redis.expire(key, EXPIRE_HISTORY_AFTER) + end + + def increment_unique_use!(tag_id, account_id, at_time) + key = "activity:tags:#{tag_id}:#{at_time.beginning_of_day.to_i}:accounts" + redis.pfadd(key, account_id) + redis.expire(key, EXPIRE_HISTORY_AFTER) + end + + # The epoch needs to be 2.5 years in the future if the half-life is one day + # While dynamic, it will always be the same within one year + def epoch + @epoch ||= Date.new(Date.current.year + 2.5, 10, 1).to_datetime.to_i + end + + def disallowed_hashtags + return @disallowed_hashtags if defined?(@disallowed_hashtags) + + @disallowed_hashtags = Setting.disallowed_hashtags.nil? ? [] : Setting.disallowed_hashtags + @disallowed_hashtags = @disallowed_hashtags.split(' ') if @disallowed_hashtags.is_a? String + @disallowed_hashtags = @disallowed_hashtags.map(&:downcase) + end + + def redis + Redis.current + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 24beb77b21..ef48282fda 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -41,7 +41,7 @@ class User < ApplicationRecord include Settings::Extend include Omniauthable - ACTIVE_DURATION = 14.days + ACTIVE_DURATION = 7.days devise :two_factor_authenticatable, otp_secret_encryption_key: Rails.configuration.x.otp_secret @@ -65,6 +65,7 @@ class User < ApplicationRecord validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale? validates_with BlacklistedEmailValidator, if: :email_changed? + validates_with EmailMxValidator, if: :email_changed? scope :recent, -> { order(id: :desc) } scope :admins, -> { where(admin: true) } @@ -86,7 +87,7 @@ 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, :display_sensitive_media, + :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_sensitive_media, :hide_network, to: :settings, prefix: :setting, allow_nil: false attr_accessor :invite_code @@ -219,6 +220,10 @@ class User < ApplicationRecord settings.notification_emails['digest'] end + def hides_network? + @hides_network ||= settings.hide_network + end + def token_for_app(a) return nil if a.nil? || a.owner != self Doorkeeper::AccessToken @@ -245,7 +250,7 @@ class User < ApplicationRecord end def web_push_subscription(session) - session.web_push_subscription.nil? ? nil : session.web_push_subscription.as_payload + session.web_push_subscription.nil? ? nil : session.web_push_subscription end def invite_code=(code) diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb index 1736106f79..867bc95191 100644 --- a/app/models/web/push_subscription.rb +++ b/app/models/web/push_subscription.rb @@ -3,46 +3,65 @@ # # Table name: web_push_subscriptions # -# id :bigint(8) not null, primary key -# endpoint :string not null -# key_p256dh :string not null -# key_auth :string not null -# data :json -# created_at :datetime not null -# updated_at :datetime not null +# id :bigint(8) not null, primary key +# endpoint :string not null +# key_p256dh :string not null +# key_auth :string not null +# data :json +# created_at :datetime not null +# updated_at :datetime not null +# access_token_id :bigint(8) +# user_id :bigint(8) # -require 'webpush' - class Web::PushSubscription < ApplicationRecord + belongs_to :user, optional: true + belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', optional: true + has_one :session_activation def push(notification) - I18n.with_locale(session_activation.user.locale || I18n.default_locale) do - push_payload(message_from(notification), 48.hours.seconds) + I18n.with_locale(associated_user&.locale || I18n.default_locale) do + push_payload(payload_for_notification(notification), 48.hours.seconds) end end def pushable?(notification) - data&.key?('alerts') && data['alerts'][notification.type.to_s] + data&.key?('alerts') && ActiveModel::Type::Boolean.new.cast(data['alerts'][notification.type.to_s]) end - def as_payload - payload = { id: id, endpoint: endpoint } - payload[:alerts] = data['alerts'] if data&.key?('alerts') - payload + def associated_user + return @associated_user if defined?(@associated_user) + + @associated_user = if user_id.nil? + session_activation.user + else + user + end end - def access_token - find_or_create_access_token.token + def associated_access_token + return @associated_access_token if defined?(@associated_access_token) + + @associated_access_token = if access_token_id.nil? + find_or_create_access_token.token + else + access_token.token + end + end + + class << self + def unsubscribe_for(application_id, resource_owner) + access_token_ids = Doorkeeper::AccessToken.where(application_id: application_id, resource_owner_id: resource_owner.id, revoked_at: nil) + .pluck(:id) + + where(access_token_id: access_token_ids).delete_all + end end private def push_payload(message, ttl = 5.minutes.seconds) - # TODO: Make sure that the payload does not - # exceed 4KB - Webpush::PayloadTooLarge - Webpush.payload_send( message: Oj.dump(message), endpoint: endpoint, @@ -57,16 +76,20 @@ class Web::PushSubscription < ApplicationRecord ) end - def message_from(notification) - serializable_resource = ActiveModelSerializers::SerializableResource.new(notification, serializer: Web::NotificationSerializer, scope: self, scope_name: :current_push_subscription) - serializable_resource.as_json + def payload_for_notification(notification) + ActiveModelSerializers::SerializableResource.new( + notification, + serializer: Web::NotificationSerializer, + scope: self, + scope_name: :current_push_subscription + ).as_json end def find_or_create_access_token Doorkeeper::AccessToken.find_or_create_for( Doorkeeper::Application.find_by(superapp: true), session_activation.user_id, - Doorkeeper::OAuth::Scopes.from_string('read write follow'), + Doorkeeper::OAuth::Scopes.from_string('read write follow push'), Doorkeeper.configuration.access_token_expires_in, Doorkeeper.configuration.refresh_token_enabled? ) diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index fcf3bdf175..41c9aa44e8 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -37,7 +37,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer end def type - 'Person' + object.bot? ? 'Service' : 'Person' end def following diff --git a/app/serializers/activitypub/block_serializer.rb b/app/serializers/activitypub/block_serializer.rb index b3bd9f868e..624ce2fce8 100644 --- a/app/serializers/activitypub/block_serializer.rb +++ b/app/serializers/activitypub/block_serializer.rb @@ -5,7 +5,7 @@ class ActivityPub::BlockSerializer < ActiveModel::Serializer attribute :virtual_object, key: :object def id - [ActivityPub::TagManager.instance.uri_for(object.account), '#blocks/', object.id].join + ActivityPub::TagManager.instance.uri_for(object) || [ActivityPub::TagManager.instance.uri_for(object.account), '#blocks/', object.id].join end def type diff --git a/app/serializers/activitypub/follow_serializer.rb b/app/serializers/activitypub/follow_serializer.rb index 86c9992fe3..bb204ee8f3 100644 --- a/app/serializers/activitypub/follow_serializer.rb +++ b/app/serializers/activitypub/follow_serializer.rb @@ -5,7 +5,7 @@ class ActivityPub::FollowSerializer < ActiveModel::Serializer attribute :virtual_object, key: :object def id - [ActivityPub::TagManager.instance.uri_for(object.account), '#follows/', object.id].join + ActivityPub::TagManager.instance.uri_for(object) || [ActivityPub::TagManager.instance.uri_for(object.account), '#follows/', object.id].join end def type diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 1d17e2b0a3..4f2f4e38a1 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -2,19 +2,15 @@ class InitialStateSerializer < ActiveModel::Serializer attributes :meta, :compose, :accounts, - :media_attachments, :settings, :push_subscription, + :media_attachments, :settings, :max_toot_chars - has_many :custom_emojis, serializer: REST::CustomEmojiSerializer + has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer def max_toot_chars StatusLengthValidator::MAX_CHARS end - def custom_emojis - CustomEmoji.local.where(disabled: false) - end - def meta store = { streaming_api_base_url: Rails.configuration.x.streaming_api_base_url, diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 863238eb74..6adcd70390 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -3,11 +3,12 @@ class REST::AccountSerializer < ActiveModel::Serializer include RoutingHelper - attributes :id, :username, :acct, :display_name, :locked, :created_at, + attributes :id, :username, :acct, :display_name, :locked, :bot, :created_at, :note, :url, :avatar, :avatar_static, :header, :header_static, :followers_count, :following_count, :statuses_count has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested? + has_many :emojis, serializer: REST::CustomEmojiSerializer class FieldSerializer < ActiveModel::Serializer attributes :name, :value diff --git a/app/serializers/rest/tag_serializer.rb b/app/serializers/rest/tag_serializer.rb new file mode 100644 index 0000000000..74aa571a4c --- /dev/null +++ b/app/serializers/rest/tag_serializer.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class REST::TagSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :name, :url, :history + + def url + tag_url(object) + end +end diff --git a/app/serializers/rest/v2/search_serializer.rb b/app/serializers/rest/v2/search_serializer.rb new file mode 100644 index 0000000000..cdb6b3a530 --- /dev/null +++ b/app/serializers/rest/v2/search_serializer.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class REST::V2::SearchSerializer < ActiveModel::Serializer + has_many :accounts, serializer: REST::AccountSerializer + has_many :statuses, serializer: REST::StatusSerializer + has_many :hashtags, serializer: REST::TagSerializer +end diff --git a/app/serializers/rest/web_push_subscription_serializer.rb b/app/serializers/rest/web_push_subscription_serializer.rb new file mode 100644 index 0000000000..7fd952a567 --- /dev/null +++ b/app/serializers/rest/web_push_subscription_serializer.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class REST::WebPushSubscriptionSerializer < ActiveModel::Serializer + attributes :id, :endpoint, :alerts, :server_key + + def alerts + object.data&.dig('alerts') || {} + end + + def server_key + Rails.configuration.x.vapid_public_key + end +end diff --git a/app/serializers/web/notification_serializer.rb b/app/serializers/web/notification_serializer.rb index e5524fe7a7..43ba4d92a2 100644 --- a/app/serializers/web/notification_serializer.rb +++ b/app/serializers/web/notification_serializer.rb @@ -2,168 +2,38 @@ class Web::NotificationSerializer < ActiveModel::Serializer include RoutingHelper - include StreamEntriesHelper + include ActionView::Helpers::TextHelper + include ActionView::Helpers::SanitizeHelper - class DataSerializer < ActiveModel::Serializer - include RoutingHelper - include StreamEntriesHelper - include ActionView::Helpers::SanitizeHelper + attributes :access_token, :preferred_locale, :notification_id, + :notification_type, :icon, :title, :body - attributes :content, :nsfw, :url, :actions, - :access_token, :message, :dir - - def content - decoder.decode(strip_tags(body)) - end - - def dir - rtl?(body) ? 'rtl' : 'ltr' - end - - def nsfw - return if object.target_status.nil? - object.target_status.spoiler_text.presence - end - - def url - case object.type - when :mention - web_url("statuses/#{object.target_status.id}") - when :follow - web_url("accounts/#{object.from_account.id}") - when :favourite - web_url("statuses/#{object.target_status.id}") - when :reblog - web_url("statuses/#{object.target_status.id}") - end - end - - def actions - return @actions if defined?(@actions) - - @actions = [] - - if object.type == :mention - @actions << expand_action if collapsed? - @actions << favourite_action - @actions << reblog_action if rebloggable? - end - - @actions - end - - def access_token - return if actions.empty? - current_push_subscription.access_token - end - - def message - I18n.t('push_notifications.group.title') - end - - private - - def body - case object.type - when :mention - object.target_status.text - when :follow - object.from_account.note - when :favourite - object.target_status.text - when :reblog - object.target_status.text - end - end - - def decoder - @decoder ||= HTMLEntities.new - end - - def expand_action - { - title: I18n.t('push_notifications.mention.action_expand'), - icon: full_asset_url('web-push-icon_expand.png', skip_pipeline: true), - todo: 'expand', - action: 'expand', - } - end - - def favourite_action - { - title: I18n.t('push_notifications.mention.action_favourite'), - icon: full_asset_url('web-push-icon_favourite.png', skip_pipeline: true), - todo: 'request', - method: 'POST', - action: "/api/v1/statuses/#{object.target_status.id}/favourite", - } - end - - def reblog_action - { - title: I18n.t('push_notifications.mention.action_boost'), - icon: full_asset_url('web-push-icon_reblog.png', skip_pipeline: true), - todo: 'request', - method: 'POST', - action: "/api/v1/statuses/#{object.target_status.id}/reblog", - } - end - - def collapsed? - !object.target_status.nil? && (object.target_status.sensitive? || object.target_status.spoiler_text.present?) - end - - def rebloggable? - !object.target_status.nil? && !object.target_status.hidden? - end + def access_token + current_push_subscription.associated_access_token end - attributes :title, :image, :badge, :tag, - :timestamp, :icon - - has_one :data, serializer: DataSerializer - - def title - case object.type - when :mention - I18n.t('push_notifications.mention.title', name: name) - when :follow - I18n.t('push_notifications.follow.title', name: name) - when :favourite - I18n.t('push_notifications.favourite.title', name: name) - when :reblog - I18n.t('push_notifications.reblog.title', name: name) - end + def preferred_locale + current_push_subscription.associated_user&.locale || I18n.default_locale end - def image - return if object.target_status.nil? || object.target_status.media_attachments.empty? - full_asset_url(object.target_status.media_attachments.first.file.url(:small)) - end - - def badge - full_asset_url('badge.png', skip_pipeline: true) - end - - def tag + def notification_id object.id end - def timestamp - object.created_at + def notification_type + object.type end def icon - object.from_account.avatar_static_url + full_asset_url(object.from_account.avatar_static_url) end - def data - object + def title + I18n.t("notification_mailer.#{object.type}.subject", name: object.from_account.display_name.presence || object.from_account.username) end - private - - def name - display_name(object.from_account) + def body + str = truncate(strip_tags(object.target_status&.spoiler_text&.presence || object.target_status&.text || object.from_account.note), length: 140) + HTMLEntities.new.decode(str.to_str) # Do not encode entities, since this value will not be used in HTML end end diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index 930fbad1f1..2b447abb32 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -4,9 +4,9 @@ class ActivityPub::FetchRemoteStatusService < BaseService include JsonLdHelper # Should be called when uri has already been checked for locality - def call(uri, id: true, prefetched_body: nil) + def call(uri, id: true, prefetched_body: nil, on_behalf_of: nil) @json = if prefetched_body.nil? - fetch_resource(uri, id) + fetch_resource(uri, id, on_behalf_of) else body_to_json(prefetched_body) end @@ -34,6 +34,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService end def trustworthy_attribution?(uri, attributed_to) + return false if uri.nil? || attributed_to.nil? Addressable::URI.parse(uri).normalized_host.casecmp(Addressable::URI.parse(attributed_to).normalized_host).zero? end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index f67ebb443a..453253db40 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -23,6 +23,8 @@ class ActivityPub::ProcessAccountService < BaseService create_account if @account.nil? update_account process_tags + else + raise Mastodon::RaceConditionError end end @@ -44,7 +46,6 @@ class ActivityPub::ProcessAccountService < BaseService @account.protocol = :activitypub @account.username = @username @account.domain = @domain - @account.uri = @uri @account.suspended = true if auto_suspend? @account.silenced = true if auto_silence? @account.private_key = nil @@ -67,10 +68,12 @@ class ActivityPub::ProcessAccountService < BaseService @account.followers_url = @json['followers'] || '' @account.featured_collection_url = @json['featured'] || '' @account.url = url || @uri + @account.uri = @uri @account.display_name = @json['name'] || '' @account.note = @json['summary'] || '' @account.locked = @json['manuallyApprovesFollowers'] || false @account.fields = property_values || {} + @account.actor_type = actor_type end def set_fetchable_attributes! @@ -95,6 +98,14 @@ class ActivityPub::ProcessAccountService < BaseService ActivityPub::SynchronizeFeaturedCollectionWorker.perform_async(@account.id) end + def actor_type + if @json['type'].is_a?(Array) + @json['type'].find { |type| ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES.include?(type) } + else + @json['type'] + end + end + def image_url(key) value = first_of_value(@json[key]) diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb index eb93329e97..79cdca297b 100644 --- a/app/services/activitypub/process_collection_service.rb +++ b/app/services/activitypub/process_collection_service.rb @@ -45,5 +45,8 @@ class ActivityPub::ProcessCollectionService < BaseService def verify_account! @account = ActivityPub::LinkedDataSignature.new(@json).verify_account! + rescue JSON::LD::JsonLdError => e + Rails.logger.debug "Could not verify LD-Signature for #{value_or_id(@json['actor'])}: #{e.message}" + nil end end diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index cb65a22565..ace51a1fcc 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -81,6 +81,11 @@ class BatchedRemoveStatusService < BaseService redis.publish('timeline:public', payload) redis.publish('timeline:public:local', payload) if status.local? + if status.media_attachments.any? + redis.publish('timeline:public:media', payload) + redis.publish('timeline:public:local:media', payload) if status.local? + end + @tags[status.id].each do |hashtag| redis.publish("timeline:hashtag:#{hashtag}", payload) redis.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local? diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 510b80c823..8b36302299 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -25,6 +25,7 @@ class FanOutOnWriteService < BaseService return if status.reply? && status.in_reply_to_account_id != status.account_id deliver_to_public(status) + deliver_to_media(status) if status.media_attachments.any? end private @@ -37,7 +38,7 @@ class FanOutOnWriteService < BaseService def deliver_to_followers(status) Rails.logger.debug "Delivering status #{status.id} to followers" - status.account.followers.where(domain: nil).joins(:user).where('users.current_sign_in_at > ?', 14.days.ago).select(:id).reorder(nil).find_in_batches do |followers| + status.account.followers.where(domain: nil).joins(:user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).select(:id).reorder(nil).find_in_batches do |followers| FeedInsertWorker.push_bulk(followers) do |follower| [status.id, follower.id, :home] end @@ -47,7 +48,7 @@ class FanOutOnWriteService < BaseService def deliver_to_lists(status) Rails.logger.debug "Delivering status #{status.id} to lists" - status.account.lists.joins(account: :user).where('users.current_sign_in_at > ?', 14.days.ago).select(:id).reorder(nil).find_in_batches do |lists| + status.account.lists.joins(account: :user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).select(:id).reorder(nil).find_in_batches do |lists| FeedInsertWorker.push_bulk(lists) do |list| [status.id, list.id, :list] end @@ -85,6 +86,13 @@ class FanOutOnWriteService < BaseService Redis.current.publish('timeline:public:local', @payload) if status.local? end + def deliver_to_media(status) + Rails.logger.debug "Delivering status #{status.id} to media timeline" + + Redis.current.publish('timeline:public:media', @payload) + Redis.current.publish('timeline:public:local:media', @payload) if status.local? + end + def deliver_to_direct_timelines(status) Rails.logger.debug "Delivering status #{status.id} to direct timelines" diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 77d4aa5381..86d0f9971d 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -23,11 +23,13 @@ class FetchLinkCardService < BaseService if lock.acquired? @card = PreviewCard.find_by(url: @url) process_url if @card.nil? || @card.updated_at <= 2.weeks.ago + else + raise Mastodon::RaceConditionError end end attach_card if @card&.persisted? - rescue HTTP::Error, Addressable::URI::InvalidURIError => e + rescue HTTP::Error, Addressable::URI::InvalidURIError, Mastodon::LengthValidationError => e Rails.logger.debug "Error fetching link #{@url}: #{e}" nil end diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index ba086449cc..6490d2735b 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -9,6 +9,7 @@ class NotifyService < BaseService return if recipient.user.nil? || blocked? create_notification + push_notification if @notification.browserable? send_email if email_enabled? rescue ActiveRecord::RecordInvalid return @@ -101,25 +102,27 @@ class NotifyService < BaseService def create_notification @notification.save! - return unless @notification.browserable? + end + + def push_notification + return if @notification.activity.nil? + Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification))) send_push_notifications end def send_push_notifications - # HACK: Can be caused by quickly unfavouriting a status, since creating - # a favourite and creating a notification are not wrapped in a transaction. - return if @notification.activity.nil? + subscriptions_ids = ::Web::PushSubscription.where(user_id: @recipient.user.id) + .select { |subscription| subscription.pushable?(@notification) } + .map(&:id) - sessions_with_subscriptions = @recipient.user.session_activations.where.not(web_push_subscription: nil) - sessions_with_subscriptions_ids = sessions_with_subscriptions.select { |session| session.web_push_subscription.pushable? @notification }.map(&:id) - - WebPushNotificationWorker.push_bulk(sessions_with_subscriptions_ids) do |session_activation_id| - [session_activation_id, @notification.id] + ::Web::PushNotificationWorker.push_bulk(subscriptions_ids) do |subscription_id| + [subscription_id, @notification.id] end end def send_email + return if @notification.activity.nil? NotificationMailer.public_send(@notification.type, @recipient, @notification).deliver_later end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index fe03c044c3..b1d5bd3a72 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -22,7 +22,7 @@ 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? + text = '.' if text.blank? && media.present? ApplicationRecord.transaction do status = account.statuses.create!(text: text, @@ -31,12 +31,12 @@ class PostStatusService < BaseService sensitive: (options[:sensitive].nil? ? account.user&.setting_default_sensitive : options[:sensitive]), spoiler_text: options[:spoiler_text] || '', visibility: options[:visibility] || account.user&.setting_default_privacy, - language: LanguageDetector.instance.detect(text, account), + language: language_from_option(options[:language]) || LanguageDetector.instance.detect(text, account), application: options[:application]) end - process_mentions_service.call(status) process_hashtags_service.call(status) + process_mentions_service.call(status) LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text? DistributionWorker.perform_async(status.id) @@ -68,6 +68,10 @@ class PostStatusService < BaseService media end + def language_from_option(str) + ISO_639.find(str)&.alpha2 + end + def process_mentions_service ProcessMentionsService.new end diff --git a/app/services/process_hashtags_service.rb b/app/services/process_hashtags_service.rb index 5b45c865ff..0695922b86 100644 --- a/app/services/process_hashtags_service.rb +++ b/app/services/process_hashtags_service.rb @@ -4,8 +4,10 @@ class ProcessHashtagsService < BaseService def call(status, tags = []) tags = Extractor.extract_hashtags(status.text) if status.local? - tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |tag| - status.tags << Tag.where(name: tag).first_or_initialize(name: tag) + tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |name| + tag = Tag.where(name: name).first_or_create(name: name) + status.tags << tag + TrendingTags.record_use!(tag, status.account, status.created_at) end end end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index e164c03ab1..8c3e184442 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -20,6 +20,7 @@ class RemoveStatusService < BaseService remove_reblogs remove_from_hashtags remove_from_public + remove_from_media if status.media_attachments.any? remove_from_direct if status.direct_visibility? @status.destroy! @@ -131,6 +132,13 @@ class RemoveStatusService < BaseService Redis.current.publish('timeline:public:local', @payload) if @status.local? end + def remove_from_media + return unless @status.public_visibility? + + Redis.current.publish('timeline:public:media', @payload) + Redis.current.publish('timeline:public:local:media', @payload) if @status.local? + end + def remove_from_direct @mentions.each do |mention| Redis.current.publish("timeline:direct:#{mention.account.id}", @payload) if mention.account.local? diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb index de8d1151d3..4323e7f06d 100644 --- a/app/services/resolve_account_service.rb +++ b/app/services/resolve_account_service.rb @@ -49,6 +49,8 @@ class ResolveAccountService < BaseService else handle_ostatus end + else + raise Mastodon::RaceConditionError end end diff --git a/app/services/update_remote_profile_service.rb b/app/services/update_remote_profile_service.rb index aca1185de2..68d36addfc 100644 --- a/app/services/update_remote_profile_service.rb +++ b/app/services/update_remote_profile_service.rb @@ -41,24 +41,24 @@ class UpdateRemoteProfileService < BaseService account.header.destroy end - save_emojis(account) if remote_profile.emojis.present? + save_emojis if remote_profile.emojis.present? end end - def save_emojis(parent) - do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media? + def save_emojis + do_not_download = DomainBlock.find_by(domain: account.domain)&.reject_media? return if do_not_download - remote_account.emojis.each do |link| + remote_profile.emojis.each do |link| next unless link['href'] && link['name'] shortcode = link['name'].delete(':') - emoji = CustomEmoji.find_by(shortcode: shortcode, domain: parent.account.domain) + emoji = CustomEmoji.find_by(shortcode: shortcode, domain: account.domain) next unless emoji.nil? - emoji = CustomEmoji.new(shortcode: shortcode, domain: parent.account.domain) + emoji = CustomEmoji.new(shortcode: shortcode, domain: account.domain) emoji.image_remote_url = link['href'] emoji.save end diff --git a/app/validators/email_mx_validator.rb b/app/validators/email_mx_validator.rb new file mode 100644 index 0000000000..3cc5853c6e --- /dev/null +++ b/app/validators/email_mx_validator.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'resolv' + +class EmailMxValidator < ActiveModel::Validator + def validate(user) + return if Rails.env.test? || Rails.env.development? + user.errors.add(:email, I18n.t('users.invalid_email')) if invalid_mx?(user.email) + end + + private + + def invalid_mx?(value) + _, domain = value.split('@', 2) + + return true if domain.nil? + + records = Resolv::DNS.new.getresources(domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s } + records.empty? || on_blacklist?(records) + end + + def on_blacklist?(values) + EmailDomainBlock.where(domain: values).any? + end +end diff --git a/app/views/about/_administration.html.haml b/app/views/about/_administration.html.haml index ec5834f9cb..02286d68b7 100644 --- a/app/views/about/_administration.html.haml +++ b/app/views/about/_administration.html.haml @@ -6,7 +6,7 @@ .account__avatar{ style: "background-image: url(#{@instance_presenter.contact_account.avatar.url})" } %span.display-name %bdi - %strong.display-name__html.emojify= display_name(@instance_presenter.contact_account) + %strong.display-name__html.emojify= display_name(@instance_presenter.contact_account, custom_emojify: true) %span.display-name__account @#{@instance_presenter.contact_account.acct} - else .account__display-name diff --git a/app/views/about/_contact.html.haml b/app/views/about/_contact.html.haml index cf21ad5a3c..3215d50b53 100644 --- a/app/views/about/_contact.html.haml +++ b/app/views/about/_contact.html.haml @@ -12,7 +12,7 @@ .avatar= image_tag contact.contact_account.avatar.url .name = link_to TagManager.instance.url_for(contact.contact_account) do - %span.display_name.emojify= display_name(contact.contact_account) + %span.display_name.emojify= display_name(contact.contact_account, custom_emojify: true) %span.username @#{contact.contact_account.acct} - else .owner diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml index e264c8574d..e6d4cd10e2 100644 --- a/app/views/about/show.html.haml +++ b/app/views/about/show.html.haml @@ -141,3 +141,5 @@ %p = link_to t('about.source_code'), @instance_presenter.source_url = " (#{@instance_presenter.version_number})" + +#modal-container diff --git a/app/views/accounts/_follow_grid.html.haml b/app/views/accounts/_follow_grid.html.haml index a6d0ee8176..fdcef84be2 100644 --- a/app/views/accounts/_follow_grid.html.haml +++ b/app/views/accounts/_follow_grid.html.haml @@ -1,5 +1,6 @@ -.accounts-grid +.accounts-grid{ class: accounts.empty? ? 'empty' : '' } - if accounts.empty? + = image_tag asset_pack_path('elephant_ui_greeting.svg'), alt: '', role: 'presentational' = render partial: 'accounts/nothing_here' - else = render partial: 'accounts/grid_card', collection: accounts, as: :account, cached: !user_signed_in? diff --git a/app/views/accounts/_follow_grid_hidden.html.haml b/app/views/accounts/_follow_grid_hidden.html.haml new file mode 100644 index 0000000000..e970350e63 --- /dev/null +++ b/app/views/accounts/_follow_grid_hidden.html.haml @@ -0,0 +1,3 @@ +.accounts-grid.empty + = image_tag asset_pack_path('elephant_ui_greeting.svg'), alt: '', role: 'presentational' + %p.nothing-here= t('accounts.network_hidden') diff --git a/app/views/accounts/_grid_card.html.haml b/app/views/accounts/_grid_card.html.haml index 95acbd581e..a59ed128e6 100644 --- a/app/views/accounts/_grid_card.html.haml +++ b/app/views/accounts/_grid_card.html.haml @@ -5,7 +5,7 @@ .avatar= image_tag account.avatar.url(:original) .name = link_to TagManager.instance.url_for(account) do - %span.display_name.emojify= display_name(account) + %span.display_name.emojify= display_name(account, custom_emojify: true) %span.username @#{account.local? ? account.local_username_and_domain : account.acct} = fa_icon('lock') if account.locked? diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml index af79922c24..b5653f1614 100644 --- a/app/views/accounts/_header.html.haml +++ b/app/views/accounts/_header.html.haml @@ -6,11 +6,16 @@ .card__bio %h1.name - %span.p-name.emojify= display_name(account) + %span.p-name.emojify= display_name(account, custom_emojify: true) %small< %span>< @#{account.local_username_and_domain} = fa_icon('lock') if account.locked? - - if Setting.show_staff_badge + + - if account.bot? + .roles + .account-role.bot + = t 'accounts.roles.bot' + - elsif Setting.show_staff_badge - if account.user_admin? .roles .account-role.admin @@ -21,19 +26,19 @@ = t 'accounts.roles.moderator' .bio .account__header__content.p-note.emojify!=processed_bio[:text] + - if !account.fields.empty? - %table.account__header__fields - %tbody - - account.fields.each do |field| - %tr - %th.emojify= field.name - %td.emojify= Formatter.instance.format_field(account, field.value) + .account__header__fields + - account.fields.each do |field| + %dl + %dt.emojify{ title: field.name }= field.name + %dd.emojify{ title: field.value }= Formatter.instance.format_field(account, field.value, custom_emojify: true) - elsif processed_bio[:metadata].length > 0 - %table.account__header__fields< + .account__header__fields - processed_bio[:metadata].each do |i| - %tr - %th.emojify>!=i[0] - %td.emojify>!=i[1] + %dl + %dt.emojify{ title: i[0] }!= i[0] + %dd.emojify{ title: i[1] }!= i[1] .details-counters .counter{ class: active_nav_class(short_account_url(account)) } diff --git a/app/views/accounts/_moved_strip.html.haml b/app/views/accounts/_moved_strip.html.haml index 6a14a5dd30..ae18c6dc70 100644 --- a/app/views/accounts/_moved_strip.html.haml +++ b/app/views/accounts/_moved_strip.html.haml @@ -3,7 +3,7 @@ .moved-strip .moved-strip__message = fa_icon 'suitcase' - = t('accounts.moved_html', name: content_tag(:strong, display_name(account), class: :emojify), new_profile_link: link_to(content_tag(:strong, safe_join(['@', content_tag(:span, moved_to_account.acct)])), TagManager.instance.url_for(moved_to_account), class: 'mention')) + = t('accounts.moved_html', name: content_tag(:strong, display_name(account, custom_emojify: true), class: :emojify), new_profile_link: link_to(content_tag(:strong, safe_join(['@', content_tag(:span, moved_to_account.acct)])), TagManager.instance.url_for(moved_to_account), class: 'mention')) .moved-strip__card = link_to TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do @@ -13,5 +13,5 @@ .account__avatar-overlay-overlay{ style: "background-image: url('#{account.avatar.url(:original)}')" } %span.display-name - %strong.emojify= display_name(moved_to_account) + %strong.emojify= display_name(moved_to_account, custom_emojify: true) %span @#{moved_to_account.acct} diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index bbf2139a5e..cfdd3a9452 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -8,6 +8,7 @@ %meta{ name: 'robots', content: 'noindex' }/ %link{ rel: 'salmon', href: api_salmon_url(@account.id) }/ + %link{ rel: 'alternate', type: 'application/rss+xml', href: account_url(@account, format: 'rss') }/ %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) }/ diff --git a/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml b/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml index 6761a43192..432fb79a6e 100644 --- a/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml +++ b/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml @@ -1,10 +1,7 @@ -%tr - %td +.speech-bubble + .speech-bubble__bubble = simple_format(h(account_moderation_note.content)) - %td - = account_moderation_note.account.acct - %td - %time.formatted{ datetime: account_moderation_note.created_at.iso8601, title: l(account_moderation_note.created_at) } - = l account_moderation_note.created_at - %td - = link_to t('admin.account_moderation_notes.delete'), admin_account_moderation_note_path(account_moderation_note), method: :delete if can?(:destroy, account_moderation_note) + .speech-bubble__owner + = admin_account_link_to account_moderation_note.account + %time.formatted{ datetime: account_moderation_note.created_at.iso8601 }= l account_moderation_note.created_at + = table_link_to 'trash', t('admin.account_moderation_notes.delete'), admin_account_moderation_note_path(account_moderation_note), method: :delete if can?(:destroy, account_moderation_note) diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index 7312618ee2..ed8190af5a 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -2,7 +2,7 @@ = @account.acct .table-wrapper - %table.table + %table.table.inline-table %tbody %tr %th= t('admin.accounts.username') @@ -36,13 +36,19 @@ %th= t('admin.accounts.email') %td = @account.user_email - - if @account.user_confirmed? - = fa_icon('check') = table_link_to 'edit', t('admin.accounts.change_email.label'), admin_account_change_email_path(@account.id) if can?(:change_email, @account.user) - if @account.user_unconfirmed_email.present? %th= t('admin.accounts.unconfirmed_email') %td = @account.user_unconfirmed_email + %tr + %th= t('admin.accounts.email_status') + %td + - if @account.user&.confirmed? + = t('admin.accounts.confirmed') + - else + = t('admin.accounts.confirming') + = table_link_to 'refresh', t('admin.accounts.resend_confirmation.send'), resend_admin_account_confirmation_path(@account.id), method: :post if can?(:confirm, @account.user) %tr %th= t('admin.accounts.login_status') %td @@ -73,17 +79,17 @@ %tr %th= t('admin.accounts.follows') - %td= @account.following_count + %td= number_to_human @account.following_count %tr %th= t('admin.accounts.followers') - %td= @account.followers_count + %td= number_to_human @account.followers_count %tr %th= t('admin.accounts.statuses') - %td= link_to @account.statuses_count, admin_account_statuses_path(@account.id) + %td= link_to number_to_human(@account.statuses_count), admin_account_statuses_path(@account.id) %tr %th= t('admin.accounts.media_attachments') %td - = link_to @account.media_attachments.count, admin_account_statuses_path(@account.id, { media: true }) + = link_to number_to_human(@account.media_attachments.count), admin_account_statuses_path(@account.id, { media: true }) = surround '(', ')' do = number_to_human_size @account.media_attachments.sum('file_file_size') %tr @@ -120,11 +126,12 @@ = link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:suspend, @account) - if !@account.local? && @account.hub_url.present? - %hr + %hr.spacer/ + %h3 OStatus .table-wrapper - %table.table + %table.table.inline-table %tbody %tr %th= t('admin.accounts.feed_url') @@ -148,11 +155,12 @@ = link_to t('admin.accounts.unsubscribe'), unsubscribe_admin_account_path(@account.id), method: :post, class: 'button negative' if can?(:unsubscribe, @account) - if !@account.local? && @account.inbox_url.present? - %hr + %hr.spacer/ + %h3 ActivityPub .table-wrapper - %table.table + %table.table.inline-table %tbody %tr %th= t('admin.accounts.inbox_url') @@ -167,24 +175,15 @@ %th= t('admin.accounts.followers_url') %td= link_to @account.followers_url, @account.followers_url -%hr -%h3= t('admin.accounts.moderation_notes') +%hr.spacer/ + += render @moderation_notes = simple_form_for @account_moderation_note, url: admin_account_moderation_notes_path do |f| = render 'shared/error_messages', object: @account_moderation_note - = f.input :content + = f.input :content, placeholder: t('admin.reports.notes.placeholder'), rows: 6 = f.hidden_field :target_account_id .actions - = f.button :button, t('admin.account_moderation_notes.create'), type: :submit - -.table-wrapper - %table.table - %thead - %tr - %th - %th= t('admin.account_moderation_notes.account') - %th= t('admin.account_moderation_notes.created_at') - %tbody - = render @moderation_notes + = f.button :button, t('admin.account_moderation_notes.create'), type: :submit diff --git a/app/views/admin/reports/_account.html.haml b/app/views/admin/reports/_account.html.haml index 22b7a08618..9ac161c9c6 100644 --- a/app/views/admin/reports/_account.html.haml +++ b/app/views/admin/reports/_account.html.haml @@ -15,5 +15,5 @@ .account__avatar{ style: "background-image: url(#{account.avatar.url}); width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px" } %span.display-name %bdi - %strong.display-name__html.emojify= display_name(account) + %strong.display-name__html.emojify= display_name(account, custom_emojify: true) %span.display-name__account @#{account.acct} diff --git a/app/views/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml index 137609539b..5e174f312e 100644 --- a/app/views/admin/reports/_status.html.haml +++ b/app/views/admin/reports/_status.html.haml @@ -3,26 +3,30 @@ = f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id .batch-table__row__content .status__content>< - - unless status.spoiler_text.blank? + - unless status.proper.spoiler_text.blank? %p>< - %strong= Formatter.instance.format_spoiler(status) + %strong> Content warning: #{Formatter.instance.format_spoiler(status.proper)} - = Formatter.instance.format(status) + = Formatter.instance.format(status.proper, custom_emojify: true) - - unless status.media_attachments.empty? - - if status.media_attachments.first.video? - - video = status.media_attachments.first - = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, width: 610, height: 343, inline: true + - unless status.proper.media_attachments.empty? + - if status.proper.media_attachments.first.video? + - video = status.proper.media_attachments.first + = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.proper.sensitive? && !current_account&.user&.setting_display_sensitive_media, width: 610, height: 343, inline: true - else - = react_component :media_gallery, 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 } + = react_component :media_gallery, height: 343, sensitive: status.proper.sensitive? && !current_account&.user&.setting_display_sensitive_media, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } .detailed-status__meta = link_to TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener' do %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) · - = fa_visibility_icon(status) - = t("statuses.visibilities.#{status.visibility}") - - if status.sensitive? + - if status.reblog? + = fa_icon('retweet fw') + = t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(status.proper.account)) + - else + = fa_visibility_icon(status) + = t("statuses.visibilities.#{status.visibility}") + - if status.proper.sensitive? · = fa_icon('eye-slash fw') = t('stream_entries.sensitive_content') diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index 38e47e6ca3..ac2dec7ec6 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -72,7 +72,11 @@ .speech-bubble .speech-bubble__bubble= simple_format(@report.comment.presence || t('admin.reports.comment.none')) .speech-bubble__owner - = admin_account_link_to @report.account + - if @report.account.local? + = admin_account_link_to @report.account + - else + = @report.account.domain + %br/ %time.formatted{ datetime: @report.created_at.iso8601 } - unless @report.statuses.empty? diff --git a/app/views/admin/statuses/index.html.haml b/app/views/admin/statuses/index.html.haml index 9747a92cf0..704ce1dbbf 100644 --- a/app/views/admin/statuses/index.html.haml +++ b/app/views/admin/statuses/index.html.haml @@ -1,10 +1,7 @@ - content_for :page_title do = t('admin.statuses.title') - -.back-link - = link_to admin_account_path(@account.id) do - %i.fa.fa-chevron-left.fa-fw - = t('admin.statuses.back_to_account') + \- + = "@#{@account.acct}" .filters .filter-subset @@ -12,33 +9,26 @@ %ul %li= link_to t('admin.statuses.no_media'), admin_account_statuses_path(@account.id, current_params.merge(media: nil)), class: !params[:media] && 'selected' %li= link_to t('admin.statuses.with_media'), admin_account_statuses_path(@account.id, current_params.merge(media: true)), class: params[:media] && 'selected' + .back-link{ style: 'flex: 1 1 auto; text-align: right' } + = link_to admin_account_path(@account.id) do + %i.fa.fa-chevron-left.fa-fw + = t('admin.statuses.back_to_account') -- if @statuses.empty? - .accounts-grid - = render 'accounts/nothing_here' -- else - = form_for(@form, url: admin_account_statuses_path(@account.id)) do |f| - = hidden_field_tag :page, params[:page] - = hidden_field_tag :media, params[:media] - .batch-form-box - .batch-checkbox-all +%hr.spacer/ + += form_for(@form, url: admin_account_statuses_path(@account.id)) do |f| + = hidden_field_tag :page, params[:page] + = hidden_field_tag :media, params[:media] + + .batch-table + .batch-table__toolbar + %label.batch-table__toolbar__select.batch-checkbox-all = check_box_tag :batch_checkbox_all, nil, false - = f.select :action, Form::StatusBatch::ACTION_TYPE.map{|action| [t("admin.statuses.batch.#{action}"), action]} - = f.submit t('admin.statuses.execute'), data: { confirm: t('admin.reports.are_you_sure') }, class: 'button' - .media-spoiler-toggle-buttons - .media-spoiler-show-button.button= t('admin.statuses.media.show') - .media-spoiler-hide-button.button= t('admin.statuses.media.hide') - - @statuses.each do |status| - .account-status{ data: { id: status.id } } - .batch-checkbox - = f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id - .activity-stream.activity-stream-headless - .entry= render 'stream_entries/simple_status', status: status - .account-status__actions - - unless status.media_attachments.empty? - = link_to admin_account_status_path(@account.id, status, current_params.merge(status: { sensitive: !status.sensitive })), method: :patch, class: 'icon-button nsfw-button', title: t("admin.reports.nsfw.#{!status.sensitive}") do - = fa_icon status.sensitive? ? 'eye' : 'eye-slash' - = link_to admin_account_status_path(@account.id, status), method: :delete, class: 'icon-button trash-button', title: t('admin.reports.delete'), data: { confirm: t('admin.reports.are_you_sure') }, remote: true do - = fa_icon 'trash' + .batch-table__toolbar__actions + = f.button safe_join([fa_icon('eye-slash'), t('admin.statuses.batch.nsfw_on')]), name: :nsfw_on, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + = f.button safe_join([fa_icon('eye'), t('admin.statuses.batch.nsfw_off')]), name: :nsfw_off, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + = f.button safe_join([fa_icon('trash'), t('admin.statuses.batch.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + .batch-table__body + = render partial: 'admin/reports/status', collection: @statuses, locals: { f: f } = paginate @statuses diff --git a/app/views/auth/sessions/two_factor.html.haml b/app/views/auth/sessions/two_factor.html.haml index 2b07c923be..1af3193ae0 100644 --- a/app/views/auth/sessions/two_factor.html.haml +++ b/app/views/auth/sessions/two_factor.html.haml @@ -2,9 +2,12 @@ = t('auth.login') = simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| - = f.input :otp_attempt, type: :number, placeholder: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, required: true, autofocus: true, hint: t('simple_form.hints.sessions.otp') + %p.hint{ style: 'margin-bottom: 25px' }= t('simple_form.hints.sessions.otp') + + = f.input :otp_attempt, type: :number, placeholder: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, required: true, autofocus: true .actions = f.button :button, t('auth.login'), type: :submit -.form-footer= render 'auth/shared/links' + - if Setting.site_contact_email.present? + %p.hint.subtle-hint= t('users.otp_lost_help_html', email: mail_to(Setting.site_contact_email, nil)) diff --git a/app/views/authorize_follows/_card.html.haml b/app/views/authorize_follows/_card.html.haml index e81e292ba6..9abcfd37e1 100644 --- a/app/views/authorize_follows/_card.html.haml +++ b/app/views/authorize_follows/_card.html.haml @@ -6,7 +6,7 @@ %span.display-name - account_url = local_assigns[:admin] ? admin_account_path(account.id) : TagManager.instance.url_for(account) = link_to account_url, class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do - %strong.emojify= display_name(account) + %strong.emojify= display_name(account, custom_emojify: true) %span @#{account.acct} - if account.note? diff --git a/app/views/follower_accounts/index.html.haml b/app/views/follower_accounts/index.html.haml index a24e4ea20f..65af81a5b7 100644 --- a/app/views/follower_accounts/index.html.haml +++ b/app/views/follower_accounts/index.html.haml @@ -7,4 +7,7 @@ = render 'accounts/header', account: @account -= render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:account) +- if @account.user_hides_network? + = render 'accounts/follow_grid_hidden' +- else + = render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:account) diff --git a/app/views/following_accounts/index.html.haml b/app/views/following_accounts/index.html.haml index 67f6cfede4..8fd95a0b4d 100644 --- a/app/views/following_accounts/index.html.haml +++ b/app/views/following_accounts/index.html.haml @@ -7,4 +7,7 @@ = render 'accounts/header', account: @account -= render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:target_account) +- if @account.user_hides_network? + = render 'accounts/follow_grid_hidden' +- else + = render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:target_account) diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml index 07441a77d4..8bbd184bbc 100644 --- a/app/views/layouts/public.html.haml +++ b/app/views/layouts/public.html.haml @@ -5,10 +5,10 @@ %span.single-user-login = link_to t('auth.login'), new_user_session_path — - %span.domain= link_to site_hostname, about_path + %span.footer__domain= link_to site_hostname, about_path - else - %span.domain= link_to site_hostname, root_path + %span.footer__domain= link_to site_hostname, root_path %span.powered-by - != t('generic.powered_by', link: link_to('Mastodon', 'https://joinmastodon.org')) + != t('generic.powered_by', link: link_to('https://joinmastodon.org') { image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' }) = render template: 'layouts/application' diff --git a/app/views/remote_follow/new.html.haml b/app/views/remote_follow/new.html.haml index fa48e5e627..fc5c4da20b 100644 --- a/app/views/remote_follow/new.html.haml +++ b/app/views/remote_follow/new.html.haml @@ -7,7 +7,7 @@ = simple_form_for @remote_follow, as: :remote_follow, url: account_remote_follow_path(@account) do |f| = render 'shared/error_messages', object: @remote_follow - = f.input :acct, placeholder: t('remote_follow.acct') + = f.input :acct, placeholder: t('remote_follow.acct'), input_html: { autocapitalize: 'none', autocorrect: 'off' } .actions = f.button :button, t('remote_follow.proceed'), type: :submit diff --git a/app/views/remote_unfollows/_card.html.haml b/app/views/remote_unfollows/_card.html.haml index e81e292ba6..9abcfd37e1 100644 --- a/app/views/remote_unfollows/_card.html.haml +++ b/app/views/remote_unfollows/_card.html.haml @@ -6,7 +6,7 @@ %span.display-name - account_url = local_assigns[:admin] ? admin_account_path(account.id) : TagManager.instance.url_for(account) = link_to account_url, class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do - %strong.emojify= display_name(account) + %strong.emojify= display_name(account, custom_emojify: true) %span @#{account.acct} - if account.note? diff --git a/app/views/settings/exports/show.html.haml b/app/views/settings/exports/show.html.haml index 89d768d3fc..30cd269140 100644 --- a/app/views/settings/exports/show.html.haml +++ b/app/views/settings/exports/show.html.haml @@ -10,15 +10,15 @@ %td %tr %th= t('exports.follows') - %td= @export.total_follows + %td= number_to_human @export.total_follows %td= table_link_to 'download', t('exports.csv'), settings_exports_follows_path(format: :csv) %tr %th= t('exports.blocks') - %td= @export.total_blocks + %td= number_to_human @export.total_blocks %td= table_link_to 'download', t('exports.csv'), settings_exports_blocks_path(format: :csv) %tr %th= t('exports.mutes') - %td= @export.total_mutes + %td= number_to_human @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') diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index 102e4d2007..4632034d71 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -26,6 +26,9 @@ .fields-group = f.input :setting_noindex, as: :boolean, wrapper: :with_label + .fields-group + = f.input :setting_hide_network, as: :boolean, wrapper: :with_label + %h4= t 'preferences.web' .fields-group diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index 5f63466d9e..d65a7f36f7 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -19,6 +19,9 @@ .fields-group = f.input :locked, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.locked') + .fields-group + = f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot') + .fields-group .input.with_block_label %label= t('simple_form.labels.defaults.fields') diff --git a/app/views/shared/_landing_strip.html.haml b/app/views/shared/_landing_strip.html.haml index ae26fc1fff..78f5ed4bcc 100644 --- a/app/views/shared/_landing_strip.html.haml +++ b/app/views/shared/_landing_strip.html.haml @@ -2,7 +2,7 @@ = image_tag asset_pack_path('logo.svg'), class: 'logo' %div - = t('landing_strip_html', name: content_tag(:span, display_name(account), class: :emojify), link_to_root_path: link_to(content_tag(:strong, site_hostname), root_path)) + = t('landing_strip_html', name: content_tag(:span, display_name(account, custom_emojify: true), class: :emojify), link_to_root_path: link_to(content_tag(:strong, site_hostname), root_path)) - if open_registrations? = t('landing_strip_signup_html', sign_up_path: new_user_registration_path) diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index afc66d1487..c0f1e4f0f5 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -4,7 +4,7 @@ .avatar = image_tag status.account.avatar.url(:original), width: 48, height: 48, alt: '', class: 'u-photo' %span.display-name - %strong.p-name.emojify= display_name(status.account) + %strong.p-name.emojify= display_name(status.account, custom_emojify: true) %span= acct(status.account) - if embedded_view? diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index cc2b6abe8e..990e45094c 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -10,7 +10,7 @@ %div = image_tag status.account.avatar(:original), width: 48, height: 48, alt: '', class: 'u-photo' %span.display-name - %strong.p-name.emojify= display_name(status.account) + %strong.p-name.emojify= display_name(status.account, custom_emojify: true) %span= acct(status.account) .status__content.p-name.emojify< diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml index 9764bc74da..b87ca21771 100644 --- a/app/views/stream_entries/_status.html.haml +++ b/app/views/stream_entries/_status.html.haml @@ -28,7 +28,7 @@ = fa_icon('retweet fw') %span = link_to TagManager.instance.url_for(status.account), class: 'status__display-name muted' do - %strong.emojify= display_name(status.account) + %strong.emojify= display_name(status.account, custom_emojify: true) = t('stream_entries.reblogged') - elsif pinned .pre-header diff --git a/app/views/tags/_og.html.haml b/app/views/tags/_og.html.haml index 853a499aea..a7c289bcb0 100644 --- a/app/views/tags/_og.html.haml +++ b/app/views/tags/_og.html.haml @@ -2,5 +2,5 @@ = opengraph 'og:url', tag_url(@tag) = opengraph 'og:type', 'website' = opengraph 'og:title', "##{@tag.name}" -= opengraph 'og:description', t('about.about_hashtag_html', hashtag: @tag.name) += opengraph 'og:description', strip_tags(t('about.about_hashtag_html', hashtag: @tag.name)) = opengraph 'twitter:card', 'summary' diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml index 000aa0c4de..2b46e58c78 100644 --- a/app/views/tags/show.html.haml +++ b/app/views/tags/show.html.haml @@ -2,6 +2,8 @@ = "##{@tag.name}" - content_for :header_tags do + %link{ rel: 'alternate', type: 'application/rss+xml', href: tag_url(@tag, format: 'rss') }/ + %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) = render 'og' @@ -33,3 +35,5 @@ %p= t 'about.about_mastodon_html' = render 'features' + +#modal-container diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb index adffd1d3bb..323a9f85b9 100644 --- a/app/workers/activitypub/delivery_worker.rb +++ b/app/workers/activitypub/delivery_worker.rb @@ -3,6 +3,9 @@ class ActivityPub::DeliveryWorker include Sidekiq::Worker + STOPLIGHT_FAILURE_THRESHOLD = 10 + STOPLIGHT_COOLDOWN = 60 + sidekiq_options queue: 'push', retry: 16, dead: false HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze @@ -31,15 +34,21 @@ class ActivityPub::DeliveryWorker def perform_request light = Stoplight(@inbox_url) do build_request.perform do |response| - raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) + raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) end end - light.run + light.with_threshold(STOPLIGHT_FAILURE_THRESHOLD) + .with_cool_off_time(STOPLIGHT_COOLDOWN) + .run end def response_successful?(response) - response.code > 199 && response.code < 300 + (200...300).cover?(response.code) + end + + def response_error_unsalvageable?(response) + (400...500).cover?(response.code) && response.code != 429 end def failure_tracker diff --git a/app/workers/web/push_notification_worker.rb b/app/workers/web/push_notification_worker.rb new file mode 100644 index 0000000000..4a40e5c8bd --- /dev/null +++ b/app/workers/web/push_notification_worker.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Web::PushNotificationWorker + include Sidekiq::Worker + + sidekiq_options backtrace: true + + def perform(subscription_id, notification_id) + subscription = ::Web::PushSubscription.find(subscription_id) + notification = Notification.find(notification_id) + + subscription.push(notification) unless notification.activity.nil? + rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription + subscription.destroy! + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/app/workers/web_push_notification_worker.rb b/app/workers/web_push_notification_worker.rb deleted file mode 100644 index eacea04c34..0000000000 --- a/app/workers/web_push_notification_worker.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -class WebPushNotificationWorker - include Sidekiq::Worker - - sidekiq_options backtrace: true - - def perform(session_activation_id, notification_id) - session_activation = SessionActivation.find(session_activation_id) - notification = Notification.find(notification_id) - - return if session_activation.web_push_subscription.nil? || notification.activity.nil? - - session_activation.web_push_subscription.push(notification) - rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription - # Subscription expiration is not currently implemented in any browser - - session_activation.web_push_subscription.destroy! - session_activation.update!(web_push_subscription: nil) - - true - rescue ActiveRecord::RecordNotFound - true - end -end diff --git a/bin/retry b/bin/retry new file mode 100755 index 0000000000..419ece62a5 --- /dev/null +++ b/bin/retry @@ -0,0 +1,46 @@ +#!/bin/bash +# https://github.com/travis-ci/travis-build/blob/cbe49ea239ab37b9b38b5b44d287b7ec7a108c16/lib/travis/build/templates/header.sh#L243-L260 +# +# MIT LICENSE +# +# Copyright (c) 2016 Travis CI GmbH +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +retry() { + local result=0 + local count=1 + + while [ $count -le 3 ]; do + if [ $result -ne 0 ]; then + echo -e "\n${ANSI_RED}The command \"$@\" failed. Retrying, $count of 3.${ANSI_RESET}\n" >&2 + fi + + "$@" && { result=0 && break; } || result=$? + count=$(($count + 1)) + sleep 1 + done + + if [ $count -gt 3 ]; then + echo -e "\n${ANSI_RED}The command \"$@\" failed 3 times.${ANSI_RESET}\n" >&2 + fi + + return $result +} + +retry $@ diff --git a/config/application.rb b/config/application.rb index 77da5cc2ec..fca7391e86 100644 --- a/config/application.rb +++ b/config/application.rb @@ -41,6 +41,7 @@ module Mastodon :ar, :bg, :ca, + :co, :de, :el, :eo, @@ -67,6 +68,7 @@ module Mastodon :'pt-BR', :ru, :sk, + :sl, :sr, :'sr-Latn', :sv, diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 0236fb8ccd..e9564692f9 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -62,5 +62,5 @@ ignore_unused: - 'errors.429' - 'admin.accounts.roles.*' - 'admin.action_logs.actions.*' - - 'themes.default' + - 'themes.*' - 'statuses.attached.*' diff --git a/config/initializers/active_model_serializers.rb b/config/initializers/active_model_serializers.rb index b0230267d4..0e69e1d96c 100644 --- a/config/initializers/active_model_serializers.rb +++ b/config/initializers/active_model_serializers.rb @@ -1,3 +1,5 @@ ActiveModelSerializers.config.tap do |config| config.default_includes = '**' end + +ActiveSupport::Notifications.unsubscribe(ActiveModelSerializers::Logging::RENDER_EVENT) diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 074f8c410b..4695538030 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -55,7 +55,7 @@ Doorkeeper.configure do # For more information go to # https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes default_scopes :read - optional_scopes :write, :follow + optional_scopes :write, :follow, :push # Change the way client credentials are retrieved from the request object. # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then diff --git a/config/initializers/http_client_proxy.rb b/config/initializers/http_client_proxy.rb index f5026d59e2..52c595c5d9 100644 --- a/config/initializers/http_client_proxy.rb +++ b/config/initializers/http_client_proxy.rb @@ -18,7 +18,8 @@ module Goldfinger def self.finger(uri, opts = {}) to_hidden = /\.(onion|i2p)(:\d+)?$/.match(uri) raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if !Rails.configuration.x.access_to_hidden_service && to_hidden - opts = opts.merge(Rails.configuration.x.http_client_proxy).merge(ssl: !to_hidden) + opts = { ssl: !to_hidden, headers: {} }.merge(Rails.configuration.x.http_client_proxy).merge(opts) + opts[:headers]['User-Agent'] ||= Mastodon::Version.user_agent Goldfinger::Client.new(uri, opts).finger end end diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index 17a520aa20..c134bc5b8f 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -60,6 +60,7 @@ elsif ENV['SWIFT_ENABLED'] == 'true' fog_credentials: { provider: 'OpenStack', openstack_username: ENV['SWIFT_USERNAME'], + openstack_project_id: ENV['SWIFT_PROJECT_ID'], openstack_project_name: ENV['SWIFT_TENANT'], openstack_tenant: ENV['SWIFT_TENANT'], # Some OpenStack-v2 ignores project_name but needs tenant openstack_api_key: ENV['SWIFT_PASSWORD'], diff --git a/config/locales/activerecord.co.yml b/config/locales/activerecord.co.yml new file mode 100644 index 0000000000..af28d108fd --- /dev/null +++ b/config/locales/activerecord.co.yml @@ -0,0 +1,13 @@ +--- +co: + activerecord: + errors: + models: + account: + attributes: + username: + invalid: solu lettere, numeri è liniette basse + status: + attributes: + reblog: + taken: di u statutu esista digià diff --git a/config/locales/activerecord.nl.yml b/config/locales/activerecord.nl.yml index 7972096653..eeabab34aa 100644 --- a/config/locales/activerecord.nl.yml +++ b/config/locales/activerecord.nl.yml @@ -6,7 +6,7 @@ nl: account: attributes: username: - invalid: alleen letters, nummers en laag streepje + invalid: alleen letters, nummers en underscores status: attributes: reblog: diff --git a/config/locales/activerecord.sl.yml b/config/locales/activerecord.sl.yml new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/config/locales/activerecord.sl.yml @@ -0,0 +1 @@ +{} diff --git a/config/locales/ar.yml b/config/locales/ar.yml index e9ca3038e4..e2d057b96a 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -4,6 +4,7 @@ ar: about_hashtag_html: هذه هي الرسائل العامة مع الكلمات الدلالية 1#%{hashtag}. يمكنك التفاعل معهم إذا كان لديك حساب في أي مكان على الإنترنت المتحد. about_mastodon_html: ماستدون شبكة إجتماعية حرة و مفتوحة المصدر. هو بديل لامركزي لمنصات تجارية ، يمكنك من تجنب احتكار شركة واحدة للإتصالات الخاصة بك. يمكنك اختيار أي خادم تثق فيه. أيهما تختار، يمكنك التفاعل مع أي شخص آخر على الشبكة. يمكن لأي شخص تنصيب و تشغيل خادم ماستدون خاص به والمشاركة في الشبكات الاجتماعية بكل شفافية. about_this: عن مثيل الخادوم هذا + administered_by: 'يديره :' closed_registrations: التسجيلات في مثيل الخادوم هذا مُغلقة حاليًا. contact: للتواصل معنا contact_missing: غير محدد @@ -39,6 +40,7 @@ ar: following: يتابعون media: الوسائط moved_html: "%{name} إنتقلَ إلى %{new_profile_link} :" + network_hidden: إنّ المعطيات غير متوفرة nothing_here: لا يوجد أي شيء هنا ! people_followed_by: الأشخاص الذين يتبعهم %{name} people_who_follow: الأشخاص الذين يتبعون %{name} @@ -48,21 +50,29 @@ ar: reserved_username: إسم المستخدم محجوز roles: admin: المدير + bot: روبوت moderator: مُشرِف unfollow: إلغاء المتابعة admin: account_moderation_notes: - account: مُشرِف - create: إنشاء - created_at: التاريخ + create: إترك ملاحظة created_msg: تم إنشاء ملاحظة الإشراف بنجاح ! delete: حذف destroyed_msg: تم تدمير ملاحظة الإشراف بنجاح ! accounts: are_you_sure: متأكد ؟ + avatar: الصورة الرمزية by_domain: النطاق + change_email: + changed_msg: تم تعديل عنوان البريد الإلكتروني الخاص بالحساب بنجاح ! + current_email: عنوان البريد الإلكتروني الحالي + label: تعديل عنوان البريد الإلكتروني + new_email: عنوان البريد الإلكتروني الجديد + submit: تعديل عنوان البريد الإلكتروني + title: تعديل عنوان البريد الإلكتروني الخاص بـ %{username} confirm: تأكيد confirmed: مؤكَّد + confirming: التأكد demote: إنزال الرُتبة الوظيفية disable: تعطيل disable_two_factor_authentication: تعطيل 2FA @@ -71,6 +81,7 @@ ar: domain: النطاق edit: تعديل email: البريد الإلكتروني + email_status: حالة البريد الإلكتروني enable: تفعيل enabled: مفعَّل feed_url: عنوان رابط التغذية @@ -108,6 +119,11 @@ ar: public: عمومي push_subscription_expires: انتهاء الاشتراك ”PuSH“ redownload: تحديث الصورة الرمزية + remove_avatar: حذف الصورة الرمزية + resend_confirmation: + already_confirmed: هذا المستخدم مؤكد بالفعل + send: أعد إرسال رسالة البريد الالكتروني الخاصة بالتأكيد + success: تم إرسال رسالة التأكيد بنجاح! reset: إعادة التعيين reset_password: إعادة ضبط كلمة السر resubscribe: اشترك مرة أخرى @@ -128,6 +144,7 @@ ar: statuses: المنشورات subscribe: اشترك title: الحسابات + unconfirmed_email: البريد الإلكتروني غير المؤكد undo_silenced: رفع الصمت undo_suspension: إلغاء تعليق الحساب unsubscribe: إلغاء الاشتراك @@ -135,6 +152,8 @@ ar: web: الويب action_logs: actions: + assigned_to_self_report: قام {name} بتعيين التقرير٪ {target} لأنفسهم + change_email_user: غيّر٪ {name} عنوان البريد الإلكتروني للمستخدم٪ {target} confirm_user: "%{name} قد قام بتأكيد عنوان البريد الإلكتروني لـ %{target}" create_custom_emoji: "%{name} قام برفع إيموجي جديد %{target}" create_domain_block: "%{name} قام بحجب نطاق %{target}" @@ -150,10 +169,13 @@ ar: enable_user: "%{name} لقد قام بتنشيط تسجيل الدخول للمستخدِم %{target}" memorialize_account: لقد قام %{name} بتحويل حساب %{target} إلى صفحة تذكارية promote_user: "%{name} قام بترقية المستخدم %{target}" + remove_avatar_user: تمت إزالة٪ {name} الصورة الرمزية٪ {target} + reopen_report: تمت إعادة فتح التقرير {name}٪ {target} reset_password_user: "%{name} لقد قام بإعادة تعيين الكلمة السرية الخاصة بـ %{target}" resolve_report: قام %{name} بحل التقرير %{target} silence_account: لقد قام %{name} بكتم حساب %{target} suspend_account: لقد قام %{name} بتعليق حساب %{target} + unassigned_report: "٪ {name} تقرير غير معتمد٪ {target}" unsilence_account: لقد قام %{name} بإلغاء الكتم عن حساب %{target} unsuspend_account: لقد قام %{name} بإلغاء التعليق المفروض على حساب %{target} update_custom_emoji: "%{name} قام بتحديث الإيموجي %{target}" @@ -191,12 +213,15 @@ ar: domain: النطاق new: create: إنشاء حظر + hint: لن تمنع كتلة المجال إنشاء إدخالات حساب في قاعدة البيانات ، ولكنها ستطبق طرق الإشراف المحددة بأثر رجعي وتلقائي على هذه الحسابات. severity: + desc_html: "Silence سيجعل مشاركات الحساب غير مرئية لأي شخص لا يتبعها. Suspend سيزيل كل محتوى الحساب ووسائطه وبيانات ملفه الشخصي. Use None إذا كنت تريد فقط رفض ملفات الوسائط." noop: لا شيء silence: كتم suspend: تعليق title: حجب نطاق جديد reject_media: رفض ملفات الوسائط + reject_media_hint: يزيل ملفات الوسائط المخزنة محليًا ويرفض تنزيل أي ملفات في المستقبل. غير ذي صلة للتعليق severities: noop: لا شيء silence: إخفاء أو كتم @@ -236,39 +261,59 @@ ar: expired: المنتهي صلاحيتها title: التصفية title: الدعوات + report_notes: + created_msg: |- + 41/5000 + تم إنشاء ملاحظة التقرير بنجاح! + destroyed_msg: تم حذف ملاحظة التقرير بنجاح! reports: + account: + note: ملحوظة + report: تقرير action_taken_by: تم اتخاذ الإجراء مِن طرف are_you_sure: هل أنت متأكد ؟ + assign_to_self: عين لي + assigned: تعين رئيس comment: none: لا شيء - delete: حذف + created_at: ذكرت id: معرّف ID mark_as_resolved: إعتبار التقرير كمحلول - nsfw: - 'false': الكشف عن الصور - 'true': إخفاء الوسائط المرفقة + mark_as_unresolved: علام كغير محلولة + notes: + create: اضف ملاحظة + create_and_resolve: الحل مع ملاحظة + create_and_unresolve: إعادة فتح مع ملاحظة + delete: حذف + placeholder: قم بوصف الإجراءات التي تم اتخاذها أو أي تحديثات أخرى ذات علاقة … + reopen: إعادة فتح التقرير report: 'التقرير #%{id}' report_contents: المحتويات reported_account: حساب مُبلّغ عنه reported_by: أبلغ عنه من طرف resolved: معالجة + resolved_msg: تم حل تقرير بنجاح! silence_account: كتم و إخفاء الحساب status: الحالة suspend_account: فرض تعليق على الحساب target: الهدف title: التقارير + unassign: إلغاء تعيين unresolved: غير معالجة + updated_at: محدث view: عرض settings: activity_api_enabled: desc_html: عدد المنشورات المحلية و المستخدمين النشطين و التسجيلات الأسبوعية الجديدة title: نشر مُجمل الإحصائيات عن نشاط المستخدمين bootstrap_timeline_accounts: + desc_html: افصل بين أسماء المستخدمين المتعددة بواسطة الفاصلة. استعمل الحسابات المحلية والمفتوحة فقط. الافتراضي عندما تكون فارغة كل المسؤولين المحليين. title: الإشتراكات الإفتراضية للمستخدمين الجدد contact_information: email: البريد الإلكتروني المهني username: الإتصال بالمستخدِم hero: + desc_html: معروض على الصفحة الأولى. لا يقل عن 600 × 100 بكسل. عند عدم التعيين ، تعود الصورة إلى النسخة المصغرة على سبيل المثال title: الصورة الرأسية peers_api_enabled: desc_html: أسماء النطاقات التي إلتقى بها مثيل الخادوم على البيئة الموحَّدة فيديفرس @@ -287,15 +332,23 @@ ar: desc_html: السماح للجميع بإنشاء حساب title: فتح التسجيل show_known_fediverse_at_about_page: + desc_html: عند التثبت ، سوف تظهر toots من جميع fediverse المعروفة على عرض مسبق. وإلا فإنه سيعرض فقط toots المحلية. title: إظهار الفيديفرس الموحَّد في خيط المُعايَنة + show_staff_badge: + desc_html: عرض شارة الموظفين على صفحة المستخدم + title: إظهار شارة الموظفين site_description: + desc_html: فقرة تمهيدية على الصفحة الأولى وفي العلامات الوصفية. يمكنك استخدام علامات HTML ، ولا سيما <a> و <em>. title: وصف مثيل الخادوم site_description_extended: + desc_html: مكان جيد لمدونة قواعد السلوك والقواعد والإرشادات وغيرها من الأمور التي تحدد حالتك. يمكنك استخدام علامات HTML title: الوصف المُفصّل للموقع site_terms: + desc_html: يمكنك كتابة سياسة الخصوصية الخاصة بك ، شروط الخدمة أو غيرها من القوانين. يمكنك استخدام علامات HTML title: شروط الخدمة المخصصة site_title: إسم مثيل الخادم thumbnail: + desc_html: يستخدم للعروض السابقة عبر Open Graph و API. 1200x630px موصى به title: الصورة الرمزية المصغرة لمثيل الخادوم timeline_preview: desc_html: عرض الخيط العمومي على صفحة الإستقبال @@ -305,15 +358,16 @@ ar: back_to_account: العودة إلى صفحة الحساب batch: delete: حذف - execute: تفعيل + nsfw_off: ضع علامة انها غير حساسة + nsfw_on: ضع علامة انها حساسة failed_to_execute: خطأ في التفعيل media: - hide: إخفاء الوسائط - show: إظهار الوسائط title: الوسائط + no_media: لا يوجد وسائط title: منشورات الحساب with_media: بالوسائط subscriptions: + callback_url: عاود الاتصال بالعنوان confirmed: مؤكَّد expires_in: تنتهي مدة صلاحيتها في last_delivery: آخر إيداع @@ -323,6 +377,8 @@ ar: admin_mailer: new_report: body: قام %{reporter} بالإبلاغ عن %{target} + body_remote: أبلغ شخص ما من٪ {domain} عن٪ {target} + subject: تقرير جديد ل%{instance} (#%{id}) application_mailer: notification_preferences: تعديل خيارات البريد الإلكتروني salutation: "%{name}،" @@ -335,6 +391,7 @@ ar: destroyed: تم حذف التطبيق بنجاح invalid_url: إن الرابط المقدم غير صالح regenerate_token: إعادة توليد رمز النفاذ + token_regenerated: تم إعادة إنشاء الرمز الوصول بنجاح warning: كن حذرا مع هذه البيانات. لا تقم أبدا بمشاركتها مع الآخَرين ! your_token: رمز نفاذك auth: @@ -345,6 +402,7 @@ ar: delete_account_html: إن كنت ترغب في حذف حسابك يُمكنك المواصلة هنا. سوف يُطلَبُ منك التأكيد قبل الحذف. didnt_get_confirmation: لم تتلق تعليمات التأكيد ؟ forgot_password: نسيت كلمة المرور ؟ + invalid_reset_password_token: رمز إعادة تعيين كلمة المرور غير صالح أو منتهي الصلاحية. يرجى طلب واحد جديد. login: تسجيل الدخول logout: خروج migrate_account: الإنتقال إلى حساب آخر @@ -358,9 +416,10 @@ ar: register_elsewhere: التسجيل على خادوم آخَر resend_confirmation: إعادة إرسال تعليمات التأكيد reset_password: إعادة تعيين كلمة المرور - security: الهوية + security: الأمان set_new_password: إدخال كلمة مرور جديدة authorize_follow: + already_following: أنت تتابع بالفعل هذا الحساب error: يا للأسف، وقع هناك خطأ إثر عملية البحث عن الحساب عن بعد follow: إتبع follow_request: 'لقد قمت بإرسال طلب متابعة إلى :' @@ -403,7 +462,7 @@ ar: archive_takeout: date: التاريخ download: تنزيل نسخة لحسابك - hint_html: بإمكانك طلب نسخة كاملة لـ كافة تبويقاتك و الوسائط التي قمت بنشرها. البيانات المُصدَّرة ستكون محفوظة على شكل نسق ActivityPub و باستطاعتك قراءتها بأي برنامج يدعم هذا النسق. + hint_html: بإمكانك طلب نسخة كاملة لـ كافة تبويقاتك و الوسائط التي قمت بنشرها. البيانات المُصدَّرة ستكون محفوظة على شكل نسق ActivityPub و باستطاعتك قراءتها بأي برنامج يدعم هذا النسق. يُمكنك طلب نسخة كل 7 أيام. in_progress: عملية جمع نسخة لبيانات حسابك جارية … request: طلب نسخة لحسابك size: الحجم @@ -519,24 +578,14 @@ ar: other: إعدادات أخرى publishing: النشر web: الويب - push_notifications: - favourite: - title: أعجب %{name} بمنشورك - follow: - title: "%{name} من متتبعيك الآن" - group: - title: "%{count} إخطارات" - mention: - action_boost: ترقية - action_expand: عرض المزيد - title: أشار إليك %{name} - reblog: - title: قام %{name} بترقية منشورك remote_follow: acct: قم بإدخال عنوان حسابك username@domain الذي من خلاله تود المتابعة missing_resource: تعذر العثور على رابط التحويل المطلوب الخاص بحسابك proceed: أكمل المتابعة prompt: 'إنك بصدد متابعة :' + remote_unfollow: + error: '' + title: '' sessions: activity: آخر نشاط browser: المتصفح @@ -574,11 +623,13 @@ ar: windows: ويندوز windows_mobile: ويندوز موبايل windows_phone: ويندوز فون + revoke: '' revoke_success: تم إبطال الجلسة بنجاح title: الجلسات settings: authorized_apps: التطبيقات المرخص لها back: عودة إلى ماستدون + delete: '' development: التطوير edit_profile: تعديل الملف الشخصي export: تصدير البيانات @@ -588,12 +639,21 @@ ar: notifications: الإخطارات preferences: التفضيلات settings: الإعدادات - two_factor_authentication: إثبات الهويّة المزدوج + two_factor_authentication: المُصادقة بخُطوَتَيْن your_apps: تطبيقاتك statuses: + attached: + image: + one: '' + other: '' + video: + one: '' + other: '' + content_warning: '' open_in_web: إفتح في الويب over_character_limit: تم تجاوز حد الـ %{max} حرف المسموح بها pin_errors: + limit: '' ownership: لا يمكن تدبيس تبويق نشره شخص آخر private: لا يمكن تثبيت تبويق لم يُنشر للعامة reblog: لا يمكن تثبيت ترقية @@ -620,11 +680,11 @@ ar: default: "%b %d, %Y, %H:%M" two_factor_authentication: code_hint: قم بإدخال الرمز المُوَلّد عبر تطبيق المصادقة للتأكيد - description_html: في حال تفعيل المصادقة بخطوتين ، فتسجيل الدخول يتتطلب منك أن يكون بحوزتك هاتفك النقال قصد توليد الرمز الذي سيتم إدخاله. + description_html: في حال تفعيل المصادقة بخطوتين ، فتسجيل الدخول يتطلب منك أن يكون بحوزتك هاتفك النقال قصد توليد الرمز الذي سيتم إدخاله. disable: تعطيل enable: تفعيل enabled: نظام المصادقة بخطوتين مُفعَّل - enabled_success: تم تفعيل إثبات الهوية المزدوج بنجاح + enabled_success: تم تفعيل المصادقة بخطوتين بنجاح generate_recovery_codes: توليد رموز الإسترجاع instructions_html: "قم بمسح رمز الكيو آر عبر Google Authenticator أو أي تطبيق TOTP على جهازك. من الآن فصاعدا سوف يقوم ذاك التطبيق بتوليد رموز يجب عليك إدخالها عند تسجيل الدخول." manual_instructions: 'في حالة تعذّر مسح رمز الكيو آر أو طُلب منك إدخال يدوي، يُمْكِنك إدخال هذا النص السري على التطبيق :' @@ -634,9 +694,19 @@ ar: wrong_code: الرمز الذي أدخلته غير صالح ! تحقق من صحة الوقت على الخادم و الجهاز ؟ user_mailer: backup_ready: + explanation: '' subject: نسخة بيانات حسابك جاهزة للتنزيل title: المغادرة بأرشيف الحساب + welcome: + edit_profile_action: '' + explanation: '' + full_handle: '' + review_preferences_action: '' + subject: '' + tips: نصائح + title: أهلاً بك، %{name} ! users: invalid_email: عنوان البريد الإلكتروني غير صالح - invalid_otp_token: الرمز الثنائي غير صالح + invalid_otp_token: رمز المصادقة بخطوتين غير صالح seamless_external_login: لقد قمت بتسجيل الدخول عبر خدمة خارجية، إنّ إعدادات الكلمة السرية و البريد الإلكتروني غير متوفرة. + signed_in_as: 'تم تسجيل دخولك بصفة :' diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 063003218e..b05398d22e 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -40,6 +40,7 @@ ca: following: Seguint media: Mèdia moved_html: "%{name} s'ha mogut a %{new_profile_link}:" + network_hidden: Aquesta informació no està disponible nothing_here: No hi ha res aquí! people_followed_by: Usuaris seguits per %{name} people_who_follow: Usuaris que segueixen %{name} @@ -49,13 +50,12 @@ ca: reserved_username: El nom d'usuari està reservat roles: admin: Administrador + bot: Bot moderator: Moderador unfollow: Deixa de seguir admin: account_moderation_notes: - account: Moderador - create: Crea - created_at: Data + create: Crea nota created_msg: La nota de moderació s'ha creat correctament! delete: Suprimeix destroyed_msg: Nota de moderació destruïda amb èxit! @@ -72,6 +72,7 @@ ca: title: Canviar adreça de correu de %{username} confirm: Confirma confirmed: Confirmat + confirming: Confirmando demote: Degrada disable: Inhabilita disable_two_factor_authentication: Desactiva 2FA @@ -80,6 +81,7 @@ ca: domain: Domini edit: Edita email: Correu electrònic + email_status: Estado del correo electrónico enable: Habilita enabled: Habilitat feed_url: URL del canal @@ -118,6 +120,10 @@ ca: push_subscription_expires: La subscripció PuSH expira redownload: Actualitza l'avatar remove_avatar: Eliminar avatar + resend_confirmation: + already_confirmed: Este usuario ya está confirmado + send: Reenviar el correo electrónico de confirmación + success: "¡Correo electrónico de confirmación enviado con éxito!" reset: Reinicialitza reset_password: Restableix la contrasenya resubscribe: Torna a subscriure @@ -269,7 +275,6 @@ ca: comment: none: Cap created_at: Reportat - delete: Suprimeix id: ID mark_as_resolved: Marca com a resolt mark_as_unresolved: Marcar sense resoldre @@ -278,10 +283,7 @@ ca: create_and_resolve: Resoldre amb nota create_and_unresolve: Reobrir amb nota delete: Esborrar - placeholder: Descriu les accions que s'han pres o qualsevol altra actualització d'aquest informe… - nsfw: - 'false': Mostra els fitxers multimèdia adjunts - 'true': Amaga els fitxers multimèdia adjunts + placeholder: Descriu les accions que s'han pres o qualsevol altra actualització relacionada… reopen: Reobrir informe report: 'Informe #%{id}' report_contents: Contingut @@ -356,11 +358,8 @@ ca: delete: Suprimeix nsfw_off: Marcar com a no sensible nsfw_on: Marcar com a sensible - execute: Executa failed_to_execute: No s'ha pogut executar media: - hide: Amaga el contingut multimèdia - show: Mostra el contingut multimèdia title: Contingut multimèdia no_media: Sense contingut multimèdia title: Estats del compte @@ -376,6 +375,7 @@ ca: admin_mailer: new_report: body: "%{reporter} ha informat de %{target}" + body_remote: Algú des de el domini %{domain} ha informat sobre %{target} subject: Informe nou per a %{instance} (#%{id}) application_mailer: notification_preferences: Canvia les preferències de correu @@ -465,7 +465,7 @@ ca: archive_takeout: date: Data download: Descarrega l’arxiu - hint_html: Pots sol·licitar un arxiu dels teus toots i els fitxers multimèdia pujats. Les dades exportades tindran el format ActivityPub, llegible per qualsevol programari compatible. + hint_html: Pots sol·licitar un arxiu dels teus toots i els fitxers multimèdia pujats. Les dades exportades tindran el format ActivityPub, llegible per qualsevol programari compatible. Pots sol·licitar un arxiu cada 7 dies. in_progress: Compilant el teu arxiu... request: Sol·licita el teu arxiu size: Tamany @@ -595,20 +595,6 @@ ca: other: Altre publishing: Publicació web: Web - push_notifications: - favourite: - title: "%{name} ha marcat com a preferit el teu estat" - follow: - title: "%{name} ara et segueix" - group: - title: "%{count} notificacions" - mention: - action_boost: Retooteja - action_expand: Mostra'n més - action_favourite: Preferit - title: "%{name} t'ha mencionat" - reblog: - title: "%{name} t'ha retootejat" remote_follow: acct: Escriu el teu usuari@domini des del qual vols seguir missing_resource: No s'ha pogut trobar la URL de redirecció necessaria per al compte @@ -633,7 +619,7 @@ ca: micro_messenger: MicroMessenger nokia: Nokia S40 Ovi Browser opera: Opera - otter: Altre + otter: Otter phantom_js: PhantomJS qq: QQ Browser safari: Safari @@ -683,6 +669,7 @@ ca: video: one: "%{count} vídeo" other: "%{count} vídeos" + boosted_from_html: Impulsat des de %{acct_link} content_warning: 'Avís de contingut: %{warning}' disallowed_hashtags: one: 'conté una etiqueta no permesa: %{tags}' @@ -832,5 +819,6 @@ ca: users: invalid_email: L'adreça de correu no és correcta invalid_otp_token: El codi de dos factors no és correcte + otp_lost_help_html: Si has perdut l'accés a tots dos pots contactar per %{email} seamless_external_login: Has iniciat sessió via un servei extern per tant els ajustos de contrasenya i correu electrònic no estan disponibles. signed_in_as: 'Sessió iniciada com a:' diff --git a/config/locales/co.yml b/config/locales/co.yml new file mode 100644 index 0000000000..32661b2c5c --- /dev/null +++ b/config/locales/co.yml @@ -0,0 +1,824 @@ +--- +co: + about: + about_hashtag_html: Quessi sò statuti pubblichi taggati cù #%{hashtag}. Pudete interagisce cù elli sì voi avete un contu in qualche parte di u fediverse. + about_mastodon_html: Mastodon ghjè una rete suciale custruita incù prutucolli web aperti è lugiziali liberi. Hè decentralizatu cumu l’e-mail. + about_this: À prupositu + administered_by: 'Amministratu da:' + closed_registrations: Pè avà, l’arregistramenti sò chjosi nant’à st’istanza. Mà pudete truvà un’altr’istanza per fà un contu è avè accessu à listessa reta da quallà. + contact: Cuntattu + contact_missing: Mancante + contact_unavailable: Micca dispunibule + description_headline: Quale hè %{domain} ? + domain_count_after: altre istanze + domain_count_before: Cunnettati à + extended_description_html: | + Una bona piazza per e regule + A descrizzione stesa ùn hè micca stata riempiuta. + features: + humane_approach_body: Mastodon hà amparatu da i sbagli di l’altre rete suciale, è prova à fà scelte di cuncezzione più etiche per luttà contr’à l’abusu di i media suciali. + humane_approach_title: Una mentalità più umana + not_a_product_body: Mastodon ùn hè micca una rete cummerciale. Micca pubblicità, micca pruspizzione di dati, micca ambienti chjosi, è micca auturità centrale. + not_a_product_title: Site una parsona, micca un pruduttu + real_conversation_body: Cù 500 caratteri dispunibuli, diffusione persunalizata di u cuntinutu è avertimenti per media sensibili, pudete cumunicà cum’è voi vulete. + real_conversation_title: Fattu per una vera cunversazione + within_reach_body: Parechje app per iOS, Android è altre piattaforme, create cù un sistemu d’API accessibile à i prugrammatori, vi permettenu d’avè accessu à i vostri amichi senza prublemi. + within_reach_title: Sempre accessibile + generic_description: "%{domain} hè un servore di a rete" + hosted_on: Mastodon allughjatu nant’à %{domain} + learn_more: Amparà di più + other_instances: Lista di l’istanze + source_code: Codice di fonte + status_count_after: statuti + status_count_before: chì anu pubblicatu + user_count_after: parsone quì + user_count_before: Ci sò + what_is_mastodon: Quale hè Mastodon? + accounts: + follow: Siguità + followers: Abbunati + following: Abbunamenti + media: Media + moved_html: "%{name} hà cambiatu di contu, avà hè nant’à %{new_profile_link}:" + nothing_here: Ùn c’hè nunda quì! + people_followed_by: Seguitati da %{name} + people_who_follow: Seguitanu %{name} + posts: Statuti + posts_with_replies: Statuti è risposte + remote_follow: Siguità d’altrò + reserved_username: Stu cugnome hè riservatu + roles: + admin: Amministratore + moderator: Muderatore + unfollow: Ùn siguità più + admin: + account_moderation_notes: + create: Creà + created_msg: Nota di muderazione creata! + delete: Toglie + destroyed_msg: Nota di muderazione sguassata! + accounts: + are_you_sure: Site sicuru·a? + avatar: Ritrattu di prufile + by_domain: Duminiu + change_email: + changed_msg: Email di u contu cambiatu! + current_email: Email attuale + label: Mudificà l’Email + new_email: Novu Email + submit: Cambià Email + title: Mudificà l’Email di %{username} + confirm: Cunfirmà + confirmed: Cunfirmata + confirming: Cunfirmazione + demote: Ritrugradà + disable: Disattivà + disable_two_factor_authentication: Disattivà l’identificazione à 2 fattori + disabled: Disattivatu + display_name: Nome pubblicu + domain: Duminiu + edit: Mudificà + email: E-mail + email_status: Statu di l’e-mail + enable: Attivà + enabled: Attivatu + feed_url: URL di u flussu + followers: Abbunati + followers_url: URL di l’abbunati + follows: Abbunamenti + inbox_url: URL di l’inbox + ip: IP + location: + all: Tutti + local: Lucale + remote: D’altrò + title: Lucalizazione + login_status: Statutu di cunnessione + media_attachments: Media aghjunti + memorialize: Trasfurmà in mimuriale + moderation: + all: Tutti + silenced: Silenzati + suspended: Suspesi + title: Muderazione + moderation_notes: Note di muderazione + most_recent_activity: Attività più ricente + most_recent_ip: IP più ricente + not_subscribed: Micca abbunatu + order: + alphabetic: Alfabeticu + most_recent: Più ricente + title: Urdine + outbox_url: URL di l’outbox + perform_full_suspension: Fà una suspensione cumpleta + profile_url: URL di u prufile + promote: Prumove + protocol: Prutucollu + public: Pubblicu + push_subscription_expires: Spirata di l’abbunamentu PuSH + redownload: Mette à ghjornu i ritratti + remove_avatar: Toglie l’avatar + resend_confirmation: + already_confirmed: St’utilizatore hè digià cunfirmatu + send: Rimandà un’e-mail di cunfirmazione + success: L’e-mail di cunfirmazione hè statu mandatu! + reset: Reset + reset_password: Riinizializà a chjave d’accessu + resubscribe: Riabbunassi + role: Auturizazione + roles: + admin: Amministratore + moderator: Muderatore + staff: Squadra + user: Utilizatore + salmon_url: URL di Salmon + search: Cercà + shared_inbox_url: URL di l’inbox spartuta + show: + created_reports: Signalamenti creati da stu contu + report: Signalamentu + targeted_reports: Signalamenti creati contr’à stu contu + silence: Silenzà + statuses: Statuti + subscribe: Abbunassi + title: Conti + unconfirmed_email: E-mail micca cunfirmatu + undo_silenced: Ùn silenzà più + undo_suspension: Ùn suspende più + unsubscribe: Disabbunassi + username: Cugnome + web: Web + action_logs: + actions: + assigned_to_self_report: "%{name} s’hè assignatu u signalamentu %{target}" + change_email_user: "%{name} hà cambiatu l’indirizzu e-mail di %{target}" + confirm_user: "%{name} hà cunfirmatu l’indirizzu e-mail di %{target}" + create_custom_emoji: "%{name} hà caricatu una nov’emoji %{target}" + create_domain_block: "%{name} hà bluccatu u duminiu %{target}" + create_email_domain_block: "%{name} hà messu u duminiu e-mail %{target} nant’a lista nera" + demote_user: "%{name} hà ritrugradatu l’utilizatore %{target}" + destroy_domain_block: "%{name} hà sbluccatu u duminiu %{target}" + destroy_email_domain_block: "%{name} hà messu u duminiu e-mail %{target} nant’a lista bianca" + destroy_status: "%{name} hà toltu u statutu di %{target}" + disable_2fa_user: "%{name} hà disattivatu l’identificazione à dui fattori per %{target}" + disable_custom_emoji: "%{name} hà disattivatu l’emoji %{target}" + disable_user: "%{name} hà disattivatu a cunnessione per %{target}" + enable_custom_emoji: "%{name} hà attivatu l’emoji %{target}" + enable_user: "%{name} hà attivatu a cunnessione per %{target}" + memorialize_account: "%{name} hà trasfurmatu u contu di %{target} in una pagina mimuriale" + promote_user: "%{name} hà prumossu %{target}" + remove_avatar_user: "%{name} hà toltu u ritrattu di %{target}" + reopen_report: "%{name} hà riapertu u signalamentu %{target}" + reset_password_user: "%{name} hà riinizializatu a chjave d’accessu di %{target}" + resolve_report: "%{name} hà chjosu u signalamentu %{target}" + silence_account: "%{name} hà silenzatu u contu di %{target}" + suspend_account: "%{name} hà suspesu u contu di %{target}" + unassigned_report: "%{name} hà disassignatu u signalamentu %{target}" + unsilence_account: "%{name} hà fattu che u contu di %{target} ùn hè più silenzatu" + unsuspend_account: "%{name} hà fattu che u contu di %{target} ùn hè più suspesu" + update_custom_emoji: "%{name} hà messu à ghjornu l’emoji %{target}" + update_status: "%{name} hà cambiatu u statutu di %{target}" + title: Ghjurnale d’audit + custom_emojis: + by_domain: Duminiu + copied_msg: Copia lucale di l’emoji creata + copy: Cupià + copy_failed_msg: Ùn s’hè micca pussutu creà una copia di l’emoji + created_msg: L’emoji hè stata creata! + delete: Toglie + destroyed_msg: L’emoji hè stata tolta! + disable: Disattivà + disabled_msg: L’emoji hè stata disattivata + emoji: Emoji + enable: Attivà + enabled_msg: L’emoji hè stata attivata + image_hint: PNG di 50Ko o menu + listed: Listata + new: + title: Aghjustà una nov’emoji + overwrite: Soprascrive + shortcode: Accorta + shortcode_hint: 2 caratteri o più, solu lettere, numeri è liniette basse + title: Emoji parsunalizate + unlisted: Micca listata + update_failed_msg: Ùn s’hè micca pussutu mette à ghjornu l’emoji + updated_msg: L’emoji hè stata messa à ghjornu! + upload: Caricà + domain_blocks: + add_new: Aghjustà + created_msg: U blucchime di u duminiu hè attivu + destroyed_msg: U blucchime di u duminiu ùn hè più attivu + domain: Duminiu + new: + create: Creà un blucchime + hint: U blucchime di duminiu ùn impedirà micca a creazione di conti indè a database, mà metudi di muderazione specifiche saranu applicati. + severity: + desc_html: CùSilenzà, solu l’abbunati di u contu viderenu i so missaghji. Suspende sguassarà tutti i cuntenuti è dati di u contu. Utilizate Nisuna s’è voi vulete solu righjittà fugliali media. + noop: Nisuna + silence: Silenzà + suspend: Suspende + title: Novu blucchime di duminiu + reject_media: Righjittà i fugliali media + reject_media_hint: Sguassa tutti i media caricati è ricusa caricamenti futuri. Inutile per una suspensione + severities: + noop: Nisuna + silence: Silenzà + suspend: Suspende + severity: Severità + show: + affected_accounts: + one: Un contu tuccatu indè a database + other: "%{count} conti tuccati indè a database" + retroactive: + silence: Ùn silenzà più i conti nant’à stu duminiu + suspend: Ùn suspende più i conti nant’à stu duminiu + title: Ùn bluccà più u duminiu %{domain} + undo: Annullà + title: Blucchimi di duminiu + undo: Annullà + email_domain_blocks: + add_new: Aghjustà + created_msg: U blucchime di u duminiu d’e-mail hè attivu + delete: Toglie + destroyed_msg: U blucchime di u duminiu d’e-mail ùn hè più attivu + domain: Duminiu + new: + create: Creà un blucchime + title: Nova iscrizzione nant’a lista nera e-mail + title: Lista nera e-mail + instances: + account_count: Conti cunnisciuti + domain_name: Duminiu + reset: Riinizializà + search: Cercà + title: Istanze cunnisciute + invites: + filter: + all: Tuttu + available: Dispunibuli + expired: Spirati + title: Filtrà + title: Invitazione + report_notes: + created_msg: Nota di signalamentu creata! + destroyed_msg: Nota di signalamentu sguassata! + reports: + account: + note: nota + report: palisà + action_taken_by: Intervenzione di + are_you_sure: Site sicuru·a? + assign_to_self: Assignallu à mè + assigned: Muderatore assignatu + comment: + none: Nisunu + created_at: Palisatu + id: ID + mark_as_resolved: Indicà cum’è chjosu + mark_as_unresolved: Indicà cum’è sempre apertu + notes: + create: Aghjunghje una nota + create_and_resolve: Chjude cù una nota + create_and_unresolve: Riapre cù una nota + delete: Toglie + placeholder: Per parlà di l’azzione piglate, o altre messe à ghjornu nant’à u signalamentu… + reopen: Riapre u signalamentu + report: 'Signalamente #%{id}' + report_contents: Cuntenuti + reported_account: Contu palisatu + reported_by: Palisatu da + resolved: Scioltu è chjosu + resolved_msg: Signalamentu scioltu! + silence_account: Silenzà u contu + status: Statutu + suspend_account: Suspende u contu + target: Oggettu + title: Signalamenti + unassign: Disassignà + unresolved: Micca sciolti + updated_at: Messi à ghjornu + view: Vede + settings: + activity_api_enabled: + desc_html: Numeri di statuti creati quì, utilizatori attivi, è arregistramenti novi tutte e settimane + title: Pubblicà statistiche nant’à l’attività di l’utilizatori + bootstrap_timeline_accounts: + desc_html: Cugnomi separati cù virgule. Solu pussibule cù conti lucali è pubblichi. Quandu a lista hè viota, tutti l’amministratori lucali saranu selezziunati. + title: Abbunamenti predefiniti per l’utilizatori novi + contact_information: + email: E-mail prufissiunale + username: Identificatore di cuntattu + hero: + desc_html: Affissatu nant’a pagina d’accolta. Ricumandemu almenu 600x100px. S’ellu ùn hè micca definiti, a vignetta di l’istanza sarà usata + title: Ritrattu di cuprendula + peers_api_enabled: + desc_html: Indirizzi st’istanza hà vistu indè u fediverse + title: Pubblicà a lista d’istanza cunnisciute + registrations: + closed_message: + desc_html: Affissatu nant’a pagina d’accolta quandu l’arregistramenti sò chjosi. Pudete fà usu di u furmattu HTML + title: Missaghju per l’arregistramenti chjosi + deletion: + desc_html: Auturizà tuttu u mondu di sguassà u so propiu contu + title: Auturizà à sguassà i conti + min_invite_role: + disabled: Nisunu + title: Auturizà l’invitazione da + open: + desc_html: Auturizà tuttu u mondu à creà un contu quì + title: Apre l’arregistramenti + show_known_fediverse_at_about_page: + desc_html: Quandu ghjè selezziunatu, statuti di tuttu l’istanze cunnisciute saranu affissati indè a vista di e linee. Altrimente soli i statuti lucali saranu mustrati. + title: Vedde tuttu u fediverse cunnisciutu nant’a vista di e linee + show_staff_badge: + desc_html: Mustrerà un badge Squadra nant’à un prufile d’utilizatore + title: Mustrà un badge staff + site_description: + desc_html: Paragrafu di prisentazione nant’a pagina d’accolta è i marchi meta. Pudete fà usu di marchi HTML, in particulare <a> è <em>. + title: Discrizzione di l’istanza + site_description_extended: + desc_html: Una bona piazza per e regule, infurmazione è altre cose chì l’utilizatori duverìanu sapè. Pudete fà usu di marchi HTML + title: Discrizzione stesa di u situ + site_terms: + desc_html: Quì pudete scrive e vostre regule di cunfidenzialità, cundizione d’usu o altre menzione legale. Pudete fà usu di marchi HTML + title: Termini persunalizati + site_title: Nome di l’istanza + thumbnail: + desc_html: Utilizatu per viste cù OpenGraph è l’API. Ricumandemu 1200x630px + title: Vignetta di l’istanza + timeline_preview: + desc_html: Vede a linea pubblica nant’a pagina d’accolta + title: Vista di e linee + title: Parametri di u situ + statuses: + back_to_account: Ritornu à a pagina di u contu + batch: + delete: Toglie + nsfw_off: Indicà cum’è micca sensibile + nsfw_on: Indicà cum’è sensibile + failed_to_execute: Esecuzione impussibule + media: + title: Media + no_media: Nisun media + title: Statutu di u contu + with_media: Cù media + subscriptions: + callback_url: URL di richjama + confirmed: Cunfirmatu + expires_in: Spira in + last_delivery: Ultima arricata + title: WebSub + topic: Sughjettu + title: Amministrazione + admin_mailer: + new_report: + body: "%{reporter} hà palisatu %{target}" + body_remote: Qualch’unu da %{domain} hà palisatu %{target} + subject: Novu signalamentu nant’à %{instance} (#%{id}) + application_mailer: + notification_preferences: Cambià e priferenze e-mail + salutation: "%{name}," + settings: 'Cambià e priferenze e-mail: %{link}' + view: 'Vede:' + view_profile: Vede u prufile + view_status: Vede u statutu + applications: + created: Applicazione creata + destroyed: Applicazione sguassata + invalid_url: L’URL ch’è stata pruvista ùn hè valida + regenerate_token: Creà un’altra fiscia d’accessu + token_regenerated: A fiscia d’accessu hè stata rigenerata + warning: Abbadate à quessi dati. Ùn i date à nisunu! + your_token: Rigenerà a fiscia d’accessu + auth: + agreement_html: Arregistrassi vole dì chì site d’accunsentu per siguità e regule di l’istanza è e cundizione d’usu. + change_password: Chjave d’accessu + confirm_email: Cunfirmà l’e-mail + delete_account: Sguassà u contu + delete_account_html: S’è voi vulete toglie u vostru contu ghjè quì. Duverete cunfirmà a vostra scelta. + didnt_get_confirmation: Ùn avete micca ricevutu l’istruzione di cunfirmazione? + forgot_password: Chjave scurdata? + invalid_reset_password_token: U ligame di riinizializazione di a chjave d’accessu hè spiratu o ùn hè micca validu. Pudete dumandà un'altru ligame. + login: Cunnettassi + logout: Scunnettassi + migrate_account: Cambià di contu + migrate_account_html: S’è voi vulete riindirizà stu contu versu un’altru, ghjè pussibule quì. + or: o + or_log_in_with: O cunnettatevi cù + providers: + cas: CAS + saml: SAML + register: Arregistrassi + register_elsewhere: Arregistrassi altrò + resend_confirmation: Rimandà l’istruzzioni di cunfirmazione + reset_password: Cambià a chjave d’accessu + security: Sicurità + set_new_password: Creà una nova chjave d’accessu + authorize_follow: + already_following: Site digià abbunatu·a à stu contu + error: Peccatu, c’hè statu un prublemu ricercandu u contu + follow: Siguità + follow_request: 'Avete dumandatu di siguità:' + following: 'Eccu! Avà seguitate:' + post_follow: + close: O pudete ancu chjude sta finestra. + return: Rivultà à u prufile di l’utilizatore + web: Andà à l’interfaccia web + title: Siguità %{acct} + datetime: + distance_in_words: + about_x_hours: "%{count}h" + about_x_months: "%{count}mo" + about_x_years: "%{count}y" + almost_x_years: "%{count}y" + half_a_minute: Avà + less_than_x_minutes: "%{count}m" + less_than_x_seconds: Avà + over_x_years: "%{count}y" + x_days: "%{count}d" + x_minutes: "%{count}m" + x_months: "%{count}mo" + x_seconds: "%{count}s" + deletes: + bad_password_msg: È nò! Sta chjave ùn hè curretta + confirm_password: Entrate a vostra chjave d’accessu attuale per verificà a vostra identità + description_html: U contu sarà deattivatu è u cuntenutu sarà sguassatu di manera permanente è irreversibile. Ùn sarà micca pussibule piglià stu cugnome torna per evità l’impusture. + proceed: Sguassà u contu + success_msg: U vostru contu hè statu sguassatu + warning_html: Pudete esse sicuru·a solu chì u cuntenutu sarà sguassatu di st’istanza. S’ellu hè statu spartutu in altrò, sarà forse sempre quallà. + warning_title: Dispunibilità di i cuntenuti sparsi + errors: + '403': Ùn site micca auturizatu·a à vede sta pagina. + '404': Sta pagina ùn esiste micca. + '410': Sta pagina ùn esiste più. + '422': + content: C’hè statu un prublemu cù a verificazione di sicurità. Forse bluccate cookies? + title: Fiascu di verificazione + '429': Limitatu dop’à troppu richieste + '500': + content: Scusate, mà c’hè statu un prublemu cù u nostru servore. + title: Sta pagina ùn hè curretta + noscript_html: Mastodon nant’à u web hà bisognu di JavaScript per funziunà. Pudete ancu pruvà l’applicazione native per a vostra piattaforma. + exports: + archive_takeout: + date: Data + download: Scaricà l’archiviu + hint_html: Pudete dumandà un’archiviu di i vostri statuti è media caricati. I dati saranu in u furmattu ActivityPub è pudarenu esse letti da tutti i lugiziali chì u supportanu. + in_progress: Cumpilazione di l’archiviu... + request: Dumandà u vostr’archiviu + size: Pesu + blocks: Bluccate + csv: CSV + follows: Seguitate + mutes: Piattate + storage: I vostri media + followers: + domain: Duminiu + explanation_html: Per assicuravi di a cunfidenzialità di i vostri statuti, duvete avè primura di quale vi seguita. I vostri statuti privati sò mandati à tutte l’istanze induve avete abbunati. Pensate à u vostru livellu di cunfidenza in i so amministratori. + followers_count: Numeru d’abbunati + lock_link: Rendete u contu privatu + purge: Toglie di a lista d’abbunati + success: + one: Suppressione di l’abbunati d’un duminiu... + other: Suppressione di l’abbunati da %{count} duminii... + true_privacy_html: Ùn vi scurdate chì una vera cunfidenzialità pò solu esse ottenuta cù crittografia da un capu à l’altru. + unlocked_warning_html: Tuttu u mondu pò seguitavi è vede i vostri statuti privati. %{lock_link} per pudè cunfirmà o righjittà abbunamenti. + unlocked_warning_title: U vostru contu hè pubblicu + generic: + changes_saved_msg: Cambiamenti salvati! + powered_by: mossu da %{link} + save_changes: Salvà e mudificazione + validation_errors: + one: Qualcosa ùn và bè! Verificate u prublemu quì sottu + other: Qualcosa ùn và bè! Verificate %{count} prublemi quì sottu + imports: + preface: Pudete impurtà certi dati cumu e persone chì seguitate o bluccate nant’à u vostru contu nant’à st’istanza à partesi di fugliali creati nant’à un’altr’istanza. + success: I vostri dati sò stati impurtati è saranu trattati da quì à pocu + types: + blocking: Persone chì bluccate + following: Persone chì seguitate + muting: Persone chì piattate + upload: Impurtà + in_memoriam_html: In mimoria. + invites: + delete: Disattivà + expired: Spirata + expires_in: + '1800': 30 minuti + '21600': 6 ore + '3600': 1 ora + '43200': 12 ore + '604800': 1 settimana + '86400': 1 ghjornu + expires_in_prompt: Mai + generate: Creà + max_uses: + one: 1 usu + other: "%{count} usi" + max_uses_prompt: Micca limita + prompt: Create è spartete ligami cù altre parsone per dà accessu à l’istanza + table: + expires_at: Spira + uses: Utiliza + title: Invità ghjente + landing_strip_html: "%{name} hè nant’à %{link_to_root_path}. Pudete seguitallu·a o cumunicà cù ellu·a cù un contu in qualche parte di u fediverse." + landing_strip_signup_html: Pudete ancu arrigistravi quì. + lists: + errors: + limit: Ùn pudete più creà altre liste + media_attachments: + validations: + images_and_video: Ùn si pò micca aghjunghje un filmettu à un statutu chì hà digià ritratti + too_many: Ùn si pò micca aghjunghje più di 4 fugliali + migrations: + acct: cugnome@duminiu di u novu contu + currently_redirecting: 'U vostru prufile riindiriza tuttu versu à:' + proceed: Salvà + updated_msg: I paramettri di migrazione sò stati messi à ghjornu! + moderation: + title: Muderazione + notification_mailer: + digest: + action: Vede tutte e nutificazione + body: Eccu cio ch’avete mancatu dapoi à a vostr’ultima visita u %{since} + mention: "%{name} v’hà mintuvatu·a in:" + new_followers_summary: + one: Avete ancu un’abbunatu novu! + other: Avete ancu %{count} abbunati novi! + subject: + one: "Una nutificazione nova dapoi à a vostr’ultima visita \U0001F418" + other: "%{count} nutificazione nove dapoi à a vostr’ultima visita \U0001F418" + title: Dapoi l’ultima volta… + favourite: + body: "%{name} hà aghjuntu u vostru statutu à i so favuriti :" + subject: "%{name} hà messu u vostru post in i so favuriti" + title: Novu favuritu + follow: + body: "%{name} s’hè abbunatu à u vostru contu !" + subject: "%{name} vi seguita" + title: Abbunatu novu + follow_request: + action: Vede e dumande d’abbunamentu + body: "%{name} vole abbunassi à u vostru contu" + subject: 'Dumanda d’abbunamentu: %{name}' + title: Nova dumanda d’abbunamentu + mention: + action: Risposta + body: "%{name} v’hà mintuvatu·a indè :" + subject: "%{name} v’hà mintuvatu·a" + title: Nova menzione + reblog: + body: 'U vostru statutu hè statu spartutu da %{name}:' + subject: "%{name} hà spartutu u vostru statutu" + title: Nova spartera + number: + human: + decimal_units: + format: "%n%u" + units: + billion: G + million: M + quadrillion: P + thousand: K + trillion: T + unit: '' + pagination: + newer: Più ricente + next: Dopu + older: Più vechju + prev: Nanzu + truncate: "…" + preferences: + languages: Lingue + other: Altre + publishing: Pubblicazione + web: Web + remote_follow: + acct: Entrate u vostru cugnome@istanza da induve vulete siguità stu contu + missing_resource: Ùn avemu pussutu à truvà l’indirizzu di ridirezzione + proceed: Cuntinuà per siguità + prompt: 'Avete da siguità:' + remote_unfollow: + error: Errore + title: Titulu + unfollowed: Disabbunatu + sessions: + activity: Ultima attività + browser: Navigatore + browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + electron: Electron + firefox: Firefox + generic: Navigatore scunnisciutu + ie: Internet Explorer + micro_messenger: MicroMessenger + nokia: Nokia S40 Ovi Browser + opera: Opera + otter: Otter + phantom_js: PhantomJS + qq: QQ Browser + safari: Safari + uc_browser: UCBrowser + weibo: Weibo + current_session: Sessione attuale + description: "%{browser} nant’à %{platform}" + explanation: Quessi sò i navigatori cunnettati à u vostru contu Mastodon. + ip: IP + platforms: + adobe_air: Adobe Air + android: Android + blackberry: Blackberry + chrome_os: ChromeOS + firefox_os: Firefox OS + ios: iOS + linux: Linux + mac: Mac + other: piattaforma scunnisciuta + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone + revoke: Rivucà + revoke_success: Sessione rivucata + title: Sessioni + settings: + authorized_apps: Applicazione auturizate + back: Ritornu nant’à Mastodon + delete: Suppressione di u contu + development: Sviluppu + edit_profile: Mudificà u prufile + export: Spurtazione d’infurmazione + followers: Abbunati auturizati + import: Impurtazione + migrate: Migrazione di u contu + notifications: Nutificazione + preferences: Priferenze + settings: Parametri + two_factor_authentication: Identificazione à dui fattori + your_apps: E vostre applicazione + statuses: + attached: + description: 'Aghjuntu: %{attached}' + image: + one: "%{count} ritrattu" + other: "%{count} ritratti" + video: + one: "%{count} filmettu" + other: "%{count} filmetti" + content_warning: 'Avertimentu: %{warning}' + disallowed_hashtags: + one: 'cuntene l’hashtag disattivatu: %{tags}' + other: 'cuntene l’hashtag disattivati: %{tags}' + open_in_web: Apre nant’à u web + over_character_limit: Site sopr’à a limita di %{max} caratteri + pin_errors: + limit: Avete digià puntarulatu u numeru massimale di statuti + ownership: Pudete puntarulà solu unu di i vostri propii statuti + private: Ùn pudete micca puntarulà un statutu ch’ùn hè micca pubblicu + reblog: Ùn pudete micca puntarulà una spartera + show_more: Vede di più + title: '%{name}: "%{quote}"' + visibilities: + private: Solu per l’abbunati + private_long: Mustrà solu à l’abbunati + public: Pubblicu + public_long: Tuttu u mondu pò vede + unlisted: Micca listatu + unlisted_long: Tuttu u mondu pò vede, mà micca indè e linee pubbliche + stream_entries: + click_to_show: Cliccà per vede + pinned: Statutu puntarulatu + reblogged: spartutu + sensitive_content: Cuntenutu sensibile + terms: + body_html: | + Privacy Policy + What information do we collect? + + + Basic account information: If you register on this server, you may be asked to enter a username, an e-mail address and a password. You may also enter additional profile information such as a display name and biography, and upload a profile picture and header image. The username, display name, biography, profile picture and header image are always listed publicly. + Posts, following and other public information: The list of people you follow is listed publicly, the same is true for your followers. When you submit a message, the date and time is stored as well as the application you submitted the message from. Messages may contain media attachments, such as pictures and videos. Public and unlisted posts are available publicly. When you feature a post on your profile, that is also publicly available information. Your posts are delivered to your followers, in some cases it means they are delivered to different servers and copies are stored there. When you delete posts, this is likewise delivered to your followers. The action of reblogging or favouriting another post is always public. + Direct and followers-only posts: All posts are stored and processed on the server. Followers-only posts are delivered to your followers and users who are mentioned in them, and direct posts are delivered only to users mentioned in them. In some cases it means they are delivered to different servers and copies are stored there. We make a good faith effort to limit the access to those posts only to authorized persons, but other servers may fail to do so. Therefore it’s important to review servers your followers belong to. You may toggle an option to approve and reject new followers manually in the settings. Please keep in mind that the operators of the server and any receiving server may view such messages, and that recipients may screenshot, copy or otherwise re-share them. Do not share any dangerous information over Mastodon. + IPs and other metadata: When you log in, we record the IP address you log in from, as well as the name of your browser application. All the logged in sessions are available for your review and revocation in the settings. The latest IP address used is stored for up to 12 months. We also may retain server logs which include the IP address of every request to our server. + + + + + What do we use your information for? + + Any of the information we collect from you may be used in the following ways: + + + To provide the core functionality of Mastodon. You can only interact with other people’s content and post your own content when you are logged in. For example, you may follow other people to view their combined posts in your own personalized home timeline. + To aid moderation of the community, for example comparing your IP address with other known ones to determine ban evasion or other violations. + The email address you provide may be used to send you information, notifications about other people interacting with your content or sending you messages, and to respond to inquiries, and/or other requests or questions. + + + + + How do we protect your information? + + We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information. Among other things, your browser session, as well as the traffic between your applications and the API, are secured with SSL, and your password is hashed using a strong one-way algorithm. You may enable two-factor authentication to further secure access to your account. + + + + What is our data retention policy? + + We will make a good faith effort to: + + + Retain server logs containing the IP address of all requests to this server, in so far as such logs are kept, no more than 90 days. + Retain the IP addresses associated with registered users no more than 12 months. + + + You can request and download an archive of your content, including your posts, media attachments, profile picture, and header image. + + You may irreversibly delete your account at any time. + + + + Do we use cookies? + + Yes. Cookies are small files that a site or its service provider transfers to your computer’s hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account. + + We use cookies to understand and save your preferences for future visits. + + + + Do we disclose any information to outside parties? + + We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety. + + Your public content may be downloaded by other servers in the network. Your public and followers-only posts are delivered to the servers where your followers reside, and direct messages are delivered to the servers of the recipients, in so far as those followers or recipients reside on a different server than this. + + When you authorize an application to use your account, depending on the scope of permissions you approve, it may access your public profile information, your following list, your followers, your lists, all your posts, and your favourites. Applications can never access your e-mail address or password. + + + + Children’s Online Privacy Protection Act Compliance + + Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA (Children’s Online Privacy Protection Act) do not use this site. + + + + Changes to our Privacy Policy + + If we decide to change our privacy policy, we will post those changes on this page. + + This document is CC-BY-SA. It was last updated March 7, 2018. + + Originally adapted from the Discourse privacy policy. + title: Termini d’usu è di cunfidenzialità per %{instance} + themes: + contrast: Cuntrastu altu + default: Mastodon + time: + formats: + default: "%d %b %Y, %H:%M" + two_factor_authentication: + code_hint: Entrate u codice generatu da l’applicazione per cunfirmà + description_html: S’ella hè attivata l’identificazione à dui fattori, duvete avè u vostru telefuninu pè ottene un codice di cunnezzione. + disable: Disattivà + enable: Attivà + enabled: Identificazione à dui fattori attivata + enabled_success: L’identificazione à dui fattori hè stata attivata + generate_recovery_codes: Creà codici di ricuperazione + instructions_html: "Scanate stu QR code cù Google Authenticator, Authy o qualcosa cusì nant’à u vostru telefuninu. St’applicazione hà da creà codici da entrà ogni volta chì vi cunnettate." + lost_recovery_codes: I codici di ricuperazione à usu unicu vi permettenu di sempre avè accessu à u vostru contu s’è voi avete persu u vostru telefuninu. S’elli sò ancu persi, pudete creà codici novi quì. I vechji codici ùn marchjeranu più. + manual_instructions: 'S’ellu ùn hè micca pussibule scanà u QR code, pudete entre sta chjave sicreta:' + recovery_codes: Codici di ricuperazione + recovery_codes_regenerated: Codici di ricuperazione ricreati + recovery_instructions_html: Pudete fà usu di i codici quì sottu per sempre avè accessu à u vostru contu s’ellu hè statu persu u vostru telefuninu. Guardateli in una piazza sicura. Per esempiu, stampati è cunservati cù altri ducumenti impurtanti. + setup: Installà + wrong_code: U codice ùn hè micca currettu! Site sicuru che l’ora di u telefuninu è di u servore sò esatte? + user_mailer: + backup_ready: + explanation: Avete dumandatu un’archiviu cumpletu di u vostru contu Mastodon. Avà hè prontu per scaricà! + subject: U vostru archiviu hè prontu à scaricà + title: Archiviu prontu + welcome: + edit_profile_action: Cunfigurazione di u prufile + edit_profile_step: Pudete persunalizà u vostru prufile cù un ritrattu di prufile o di cuprendula, un nome pubblicu persunalizatu, etc. Pudete ancu rende u contu privatu per duvè cunfirmà ogni dumanda d’abbunamentu. + explanation: Eccu alcune idee per principià + final_action: Principià à pustà + final_step: 'Andemu! Ancu senza abbunati i vostri missaghji pubblichi puderanu esse visti da altre persone, per esempiu nant’a linea lucale è l’hashtag. Pudete ancu prisintavi nant’à u hashtag #introductions.' + full_handle: U vostru identificatore cumplettu + full_handle_hint: Quessu ghjè cio chì direte à i vostri amichi per circavi, abbunassi à u vostru contu da altrò, o mandà missaghji. + review_preferences_action: Mudificà e priferenze + review_preferences_step: Quì pudete adattà u cumpurtamentu di Mastodon à e vostre priferenze, cum’è l’email che vulete riceve, u nivellu di cunfidenzialità predefinitu di i vostri statuti, o u cumpurtamentu di i GIF animati. + subject: Benvenutu·a nant’à Mastodon + tip_bridge_html: S’è voi venite di Twitter, pudete truvà i vostri amichi da quallà chì sò nant’à Mastodon cù a bridge app. Mà ùn marchja chè s’elli l’anu ancu usata! + tip_federated_timeline: A linea pubblica glubale mostra i statuti da altre istanze nant’a rete Mastodon, mà ùn hè micca cumpleta perchè ci sò soli i conti à quelli sò abbunati membri di a vostr’istanza. + tip_following: Site digià abbunatu·a à l’amministratori di u vostru servore. Per truvà d’altre parsone da siguità, pudete pruvà e linee pubbliche. + tip_local_timeline: A linea pubblica lucale ghjè una vista crunulogica di i statuti di a ghjente nant’à %{instance}. Quessi sò i vostri cunvicini! + tip_mobile_webapp: Pudete aghjunghje Mastodon à a pagina d’accolta di u vostru navigatore di telefuninu per riceve nutificazione, cum’un applicazione! + tips: Cunsiglii + title: Benvenutu·a, %{name}! + users: + invalid_email: L’indirizzu e-mail ùn hè currettu + invalid_otp_token: U codice d’identificazione ùn hè currettu + otp_lost_help_html: S’è voi avete persu i dui, pudete cuntattà %{email} + seamless_external_login: Site cunnettatu·a dapoi un serviziu esternu, allora i parametri di chjave d’accessu è d’indirizzu e-mail ùn so micca dispunibili. + signed_in_as: 'Cunnettatu·a cum’è:' diff --git a/config/locales/de.yml b/config/locales/de.yml index 5fdcb1900f..3440439cda 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -40,6 +40,7 @@ de: following: Folgt media: Medien moved_html: "%{name} ist auf %{new_profile_link} umgezogen:" + network_hidden: Diese Informationen sind nicht verfügbar nothing_here: Hier gibt es nichts! people_followed_by: Profile, denen %{name} folgt people_who_follow: Profile, die %{name} folgen @@ -49,13 +50,12 @@ de: reserved_username: Dieser Profilname ist belegt roles: admin: Admin + bot: Bot moderator: Moderator unfollow: Entfolgen admin: account_moderation_notes: - account: Moderator*in - create: Erstellen - created_at: Datum + create: Notiz hinterlassen created_msg: Moderationsnotiz erfolgreich erstellt! delete: Löschen destroyed_msg: Moderationsnotiz erfolgreich gelöscht! @@ -72,6 +72,7 @@ de: title: E-Mail-Adresse für %{username} ändern confirm: Bestätigen confirmed: Bestätigt + confirming: Bestätigung demote: Degradieren disable: Ausschalten disable_two_factor_authentication: 2FA abschalten @@ -80,6 +81,7 @@ de: domain: Domain edit: Bearbeiten email: E-Mail + email_status: E-Mail-Status enable: Freischalten enabled: Freigegeben feed_url: Feed-URL @@ -118,6 +120,10 @@ de: push_subscription_expires: PuSH-Abonnement läuft aus redownload: Avatar neu laden remove_avatar: Profilbild entfernen + resend_confirmation: + already_confirmed: Dieser Benutzer wurde bereits bestätigt + send: Bestätigungsmail erneut senden + success: Bestätigungs-E-Mail erfolgreich gesendet! reset: Zurücksetzen reset_password: Passwort zurücksetzen resubscribe: Wieder abonnieren @@ -269,7 +275,6 @@ de: comment: none: Kein created_at: Gemeldet - delete: Löschen id: ID mark_as_resolved: Als gelöst markieren mark_as_unresolved: Als ungelöst markieren @@ -279,9 +284,6 @@ de: create_and_unresolve: Mit Kommentar wieder öffnen delete: Löschen placeholder: Beschreibe, welche Maßnahmen ergriffen wurden oder andere Neuigkeiten zu dieser Meldung… - nsfw: - 'false': Medienanhänge wieder anzeigen - 'true': Medienanhänge verbergen reopen: Meldung wieder öffnen report: 'Meldung #%{id}' report_contents: Inhalt @@ -356,11 +358,8 @@ de: delete: Löschen nsfw_off: Als nicht heikel markieren nsfw_on: Als heikel markieren - execute: Ausführen failed_to_execute: Ausführen fehlgeschlagen media: - hide: Medien verbergen - show: Medien anzeigen title: Medien no_media: Keine Medien title: Beiträge des Kontos @@ -376,6 +375,7 @@ de: admin_mailer: new_report: body: "%{reporter} hat %{target} gemeldet" + body_remote: Jemand von %{domain} hat %{target} gemeldet subject: Neue Meldung auf %{instance} (#%{id}) application_mailer: notification_preferences: Ändere E-Mail-Einstellungen @@ -465,7 +465,7 @@ de: archive_takeout: date: Datum download: Dein Archiv herunterladen - hint_html: Du kannst ein Archiv deiner Beiträge und hochgeladenen Medien anfragen. Die exportierten Daten werden im ActivityPub-Format gespeichert, welches mit jeder Software lesbar ist die das Format unterstützt. + hint_html: Du kannst ein Archiv deiner Beiträge und hochgeladenen Medien anfragen. Die exportierten Daten werden im ActivityPub-Format gespeichert, welches mit jeder Software lesbar ist, die das Format unterstützt. Du kannst alle 7 Tage ein neues Archiv anfordern. in_progress: Stelle dein Archiv zusammen... request: Dein Archiv anfragen size: Größe @@ -595,20 +595,6 @@ de: other: Weiteres publishing: Beiträge web: Web - push_notifications: - favourite: - title: "%{name} hat deinen Beitrag favorisiert" - follow: - title: "%{name} folgt dir nun" - group: - title: "%{count} Benachrichtigungen" - mention: - action_boost: Teilen - action_expand: Mehr anzeigen - action_favourite: Favorisieren - title: "%{name} hat dich erwähnt" - reblog: - title: "%{name} hat deinen Beitrag geteilt" remote_follow: acct: Profilname@Domain, von wo aus du dieser Person folgen möchtest missing_resource: Die erforderliche Weiterleitungs-URL für dein Konto konnte nicht gefunden werden @@ -757,5 +743,6 @@ de: users: invalid_email: Ungültige E-Mail-Adresse invalid_otp_token: Ungültiger Zwei-Faktor-Authentisierungs-Code + otp_lost_help_html: Wenn Sie zu beidem keinen Zugriff mehr haben, kontaktieren sie %{email} seamless_external_login: Du bist angemeldet über einen Drittanbieter-Dienst, weswegen Passwort- und E-Maileinstellungen nicht verfügbar sind. signed_in_as: 'Angemeldet als:' diff --git a/config/locales/devise.co.yml b/config/locales/devise.co.yml new file mode 100644 index 0000000000..2471f857be --- /dev/null +++ b/config/locales/devise.co.yml @@ -0,0 +1,82 @@ +--- +co: + devise: + confirmations: + confirmed: U vostru indirizzu email hè statu cunfirmatu. + send_instructions: Avete da riceve un’email cù l’istruzzione di cunfirmazione in qualchì minuta. Pensate à verificà u cartulare di spam s’ellu ùn c’hè nunda. + send_paranoid_instructions: S’ellu esiste u vostru indirizzu email in a database, avete da riceve l’istruzzione pè cunfirmà u vostru contu in qualchì minuta. Pensate à verificà u cartulare di spam s’ellu ùn c’hè nunda. + failure: + already_authenticated: Site digià cunnettatu·a. + inactive: U vostru contu ùn hè ancu attivatu. + invalid: L’ %{authentication_keys} o a chjave d’accessu ùn sò curretti. + last_attempt: Avete un’ultimu tintativu nanzu chì u vostru contu sia chjosu. + locked: U vostru contu hè chjosu. + not_found_in_database: L’ %{authentication_keys} o a chjave d’accessu ùn sò curretti. + timeout: A vostra sezzione hè spirata. Ricunnettatevi pè cuntinuà. + unauthenticated: Cunnettatevi o arregistratevi pè cuntinuà. + unconfirmed: Duvete cunfirmà u vostru contu pè cuntinuà. + mailer: + confirmation_instructions: + action: Verificà l’indirizzu email + explanation: Avete creatu un contu nant’à %{host} cù st’indirizzu email. Pudete attivallu cù un clic, o ignurà quessu missaghji s’ellu un era micca voi. + extra_html: Pensate à leghje e regule di l’istanza è i termini d’usu. + subject: 'Mastodon: Istruzzione di cunfirmazione per %{instance}' + title: Verificà l’indirizzu email + email_changed: + explanation: 'L’indirizzu email di u vostru contu hè stata cambiata per:' + extra: S’ellu un era micca voi ch’avete cambiatu u vostru email, qualch’un’altru hà accessu à u vostru contu. Duvete cambià a vostra chjave d’accessu o cuntattà l’amministratore di l’istanza s’ellu ùn hè più pussibule di cunnettavi. + subject: 'Mastodon: Email cambiatu' + title: Novu indirizzu email + password_change: + explanation: A chjave d’accessu per u vostru contu hè stata cambiata. + extra: S’ellu un era micca voi ch’avete cambiatu a vostra chjave d’accessu, qualch’un’altru hà accessu à u vostru contu. Duvete cambià a vostra chjave d’accessu o cuntattà l’amministratore di l’istanza s’ellu ùn hè più pussibule di cunnettavi. + subject: 'Mastodon: Chjave d’accessu cambiata' + title: Chjave cambiata + reconfirmation_instructions: + explanation: Cunfirmà u novu indirizzu per cambià l’email. + extra: S’ellu ùn era micca voi, ignurate stu missaghju. L’email ùn cambiarà micca s’è voi ùn cliccate micca u ligame. + subject: 'Mastodon: Cunfirmà l’email per %{instance}' + title: Verificà indirizzu email + reset_password_instructions: + action: Cambià a chjave d’accessu + explanation: Avete dumandatu una nova chjave d’accessu per u vostru contu. + extra: S’ellu ùn era micca voi, ignurate stu missaghju. A chjave d’accessu ùn cambiarà micca s’è voi ùn cliccate micca u ligame. + subject: 'Mastodon: Cambià a chjave d’accessu' + title: Cambià a chjave + unlock_instructions: + subject: 'Mastodon: Riapre u contu' + omniauth_callbacks: + failure: Ùn pudemu micca cunnettavi da %{kind} perchè "%{reason}". + success: Vi site cunnettatu·a da %{kind}. + passwords: + no_token: Ùn pudete micca vede sta pagina senza vene d’un e-mail di cambiamentu di chjave d’accessu. S’è voi venite quì dapoi st’e-mail, assicuratevi ch’avete utilizatu l’indirizzu URL cumpletu. + send_instructions: Avete da riceve l’istruzzione di cambiamentu di a chjave d’accessu in qualchì minuta. + send_paranoid_instructions: S’ellu esiste u vostr’e-mail in a database, avete da riceve un ligame di reinizialisazione. + updated: A vostra chjave d’accessu hè stata cambiata, è site cunnettatu·a. + updated_not_active: A vostra chjave d’accessu hè stata cambiata. + registrations: + destroyed: U vostru contu hè statu sguassatu. Avvedeci! + signed_up: Benvinutu! Site cunnettatu·a. + signed_up_but_inactive: Site arregistratu·a, mà ùn pudete micca cunnettavi perchè u vostru contu deve esse attivatu. + signed_up_but_locked: Site arregistratu·a, mà ùn pudete micca cunnettavi perchè u vostru contu hè chjosu. + signed_up_but_unconfirmed: Un missaghju cù un ligame di cunfirmazione hè statu mandatu à u vostru indirizzu e-mail. Aprite stu ligame pè attivà u vostru contu. Pensate à verificà u cartulare di spam s’ellu ùn c’hè nunda. + update_needs_confirmation: U vostru contu hè statu messu à ghjornu mà duvemu verificà u vostru novu e-mail. Un missaghju cù un ligame di cunfirmazione hè statu mandatu. Pensate à verificà u cartulare di spam s’ellu ùn c’hè nunda. + updated: U vostru contu hè statu messu à ghjornu. + sessions: + already_signed_out: Scunnettatu·a. + signed_in: Cunnettatu·a. + signed_out: Scunnettatu·a. + unlocks: + send_instructions: Avete da riceve un’e-mail cù l’istruzzione pè riapre u vostru contu in qualchì minuta. + send_paranoid_instructions: S’ellu esiste u vostru contu, avete da riceve un’e-mail dù l’istruzzione pè riapre u vostru contu. + unlocked: U vostru contu hè statu riapertu, pudete cunnettavi pè cuntinuà. + errors: + messages: + already_confirmed: hè digià cunfirmatu, pudete pruvà à cunnettà vi + confirmation_period_expired: deve esse cunfirmatu nanz’à %{period}, duvete fà un’altra dumanda + expired: hè spiratu, duvete fà un’altra dumanda + not_found: ùn hè micca statu trovu + not_locked: ùn era micca chjosu + not_saved: + one: 'Un prublemu hà impeditu a cunservazione di stu (sta) %{resource}:' + other: "%{count} prublemi anu impeditu a cunservazione di stu (sta) %{resource} :" diff --git a/config/locales/devise.fa.yml b/config/locales/devise.fa.yml index f78412f91d..e6e16b4b49 100644 --- a/config/locales/devise.fa.yml +++ b/config/locales/devise.fa.yml @@ -17,11 +17,32 @@ fa: unconfirmed: برای ادامه باید نشانی ایمیل خود را تأیید کنید. mailer: confirmation_instructions: + action: تأیید نشانی ایمیل + explanation: شما با این نشانی ایمیل حسابی در %{host} باز کردهاید. با یک کلیک میتوانید این حساب را فعال کنید. اگر شما چنین کاری نکردید، لطفاً این ایمیل را نادیده بگیرید. + extra_html: لطفاً همچنین قانونهای این سرور و شرایط کاربری آن را ببینید. subject: 'ماستدون: راهنمایی برای تأیید %{instance}' + title: تأیید نشانی ایمیل + email_changed: + explanation: 'نشانی ایمیل حساب شما تغییر میکند به:' + extra: اگر شما ایمیل خود را عوض نکردید، شاید کسی به حساب شما دسترسی پیدا کرده است. در این صورت لطفاً هر چه زودتر رمز حسابتان را عوض کنید. اگر رمزتان دیگر کار نمیکند، لطفاً با مدیر سرور تماس بگیرید. + subject: 'ماستدون: نشانی ایمیل عوض شد' + title: نشانی ایمیل تازه password_change: + explanation: رمز حساب شما تغییر کرد. + extra: اگر شما رمز حسابتان را تغییر ندادید، شاید کسی به حساب شما دسترسی پیدا کرده است. در این صورت لطفاً هر چه زودتر رمز حسابتان را عوض کنید. اگر رمزتان دیگر کار نمیکند، لطفاً با مدیر سرور تماس بگیرید. subject: 'ماستدون: رمزتان عوض شد' + title: رمزتان عوض شد + reconfirmation_instructions: + explanation: نشانی تازه را تأیید کنید تا ایمیلتان عوض شود. + extra: اگر شما باعث این تغییر نبودید، لطفاً این ایمیل را نادیده بگیرید. تا زمانی که شما پیوند بالا را باز نکنید، نشانی ایمیل مربوط به حساب شما عوض نخواهد شد. + subject: 'ماستدون: تأیید ایمیل برای %{instance}' + title: تأیید نشانی ایمیل reset_password_instructions: + action: تغییر رمز + explanation: شما رمز تازهای برای حسابتان درخواست کردید. + extra: اگر شما چنین درخواستی نکردید، لطفاً این ایمیل را نادیده بگیرید. تا زمانی که شما پیوند بالا را باز نکنید و رمز تازهای نسازید، رمز شما عوض نخواهد شد. subject: 'ماستدون: راهنمایی برای بازنشانی رمز' + title: بازنشانی رمز unlock_instructions: subject: 'ماستدون: راهنمایی برای بازکردن قفل' omniauth_callbacks: @@ -57,5 +78,5 @@ fa: not_found: پیدا نشد not_locked: قفل نبود not_saved: - one: خطایی نگذاشت که این %{resource} ذخیره شود - other: به خاطر %{count} خطا، این %{resource} ذخیره نشد + one: 'خطایی نگذاشت که این %{resource} ذخیره شود:' + other: 'به خاطر %{count} خطا، این %{resource} ذخیره نشد:' diff --git a/config/locales/devise.sk.yml b/config/locales/devise.sk.yml index e9c5dd4557..4bbc723e9b 100644 --- a/config/locales/devise.sk.yml +++ b/config/locales/devise.sk.yml @@ -78,5 +78,6 @@ sk: not_found: nenájdený not_locked: nebol uzamknutý not_saved: + few: "%{resource} nebol uložený kôli %{count} chybám:" one: "%{resource} nebol uložený kôli chybe:" other: "%{resource} nebol uložený kôli %{count} chybám:" diff --git a/config/locales/devise.sl.yml b/config/locales/devise.sl.yml new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/config/locales/devise.sl.yml @@ -0,0 +1 @@ +{} diff --git a/config/locales/doorkeeper.ar.yml b/config/locales/doorkeeper.ar.yml index 5586b8dc86..3b42029036 100644 --- a/config/locales/doorkeeper.ar.yml +++ b/config/locales/doorkeeper.ar.yml @@ -114,5 +114,6 @@ ar: title: طلب تصريح مفتوح OAuth scopes: follow: متابعة و حجب و فك الحجب و إلغاء متابعة حسابات المستخدمين + push: تلقى إشعارات حسابك read: قراءة بيانات حسابك write: النشر نيابةً عنك diff --git a/config/locales/doorkeeper.ca.yml b/config/locales/doorkeeper.ca.yml index c1748d05b1..56686e3e5b 100644 --- a/config/locales/doorkeeper.ca.yml +++ b/config/locales/doorkeeper.ca.yml @@ -115,5 +115,6 @@ ca: title: OAuth autorització requerida scopes: follow: seguir, blocar, desblocar i deixar de seguir comptes + push: rebre notificacions push del teu compte read: llegir les dades del teu compte write: publicar en el teu nom diff --git a/config/locales/doorkeeper.co.yml b/config/locales/doorkeeper.co.yml new file mode 100644 index 0000000000..31080d1533 --- /dev/null +++ b/config/locales/doorkeeper.co.yml @@ -0,0 +1,119 @@ +--- +co: + activerecord: + attributes: + doorkeeper/application: + name: Nome di l’applicazione + redirect_uri: URI di ridirezzione + scopes: Scopi + website: Situ di l’applicazione + errors: + models: + doorkeeper/application: + attributes: + redirect_uri: + fragment_present: ùn pò cuntene un pezzu. + invalid_uri: duve esse un’URI curretta. + relative_uri: duve esse un’URI assoluta. + secured_uri: duve esse un’URL HTTPS/SSL. + doorkeeper: + applications: + buttons: + authorize: Appruvà + cancel: Sguassà + destroy: Strughje + edit: Mudificà + submit: Mandà + confirmations: + destroy: Site sicuru·a? + edit: + title: Mudificà l’applicazione + form: + error: Uups! V’invitemu à verificà u vostru formulariu per vede s’elli ùn ci sò sbaglii + help: + native_redirect_uri: Utilizate %{native_redirect_uri} pè e prove lucale + redirect_uri: Utilizzate una linea per ogni URI + scopes: Separate i scopi cù spazii. Lasciate viotu per utilizzà i scopi predefiniti. + index: + application: Applicazione + callback_url: URL di richjama + delete: Toglie + name: Nome + new: Applicazione nova + scopes: Scopi + show: Vede + title: E vostre applicazione + new: + title: Applicazione nova + show: + actions: Azzioni + application_id: Chjave di u clientu + callback_urls: URL di richjama + scopes: Scopi + secret: Sicretu di u clientu + title: 'Applicazione : %{name}' + authorizations: + buttons: + authorize: Appruvà + deny: Ricusà + error: + title: C’hè statu un prublemu + new: + able_to: St’applicazione puderà + prompt: L’applicazione %{client_name} hà dumandatu d’avè accessu à u vostru contu + title: Permessu riquestu + show: + title: Codice d’auturizazione da cupià indè l’applicazione. + authorized_applications: + buttons: + revoke: Sguassà + confirmations: + revoke: Site sicuru·a? + index: + application: Applicazione + created_at: Auturizata u + date_format: "%d-%m-%Y %H:%M:%S" + scopes: Scopi + title: E vostre applicazione auturizate + errors: + messages: + access_denied: U pruprietariu di a risorsa o u servore d’autitinficazione hà ricusatu a dumanda. + credential_flow_not_configured: U flussu di l’identificazione di u pruprietariu di a risorsa hà fiascatu perchè Doorkeeper.configure.resource_owner_from_credentials ùn hè micca cunfiguratu. + invalid_client: L’autintificazione di u cliente hà fiascatu perchè u cliente ùn hè micca cunnisciutu, l’identificazione di u cliente ùn hè cumpresa, o u modu d’identificazione ùn marchja micca. + invalid_grant: L’accunsentu d’auturizazione furnitu ùn hè currettu, hè spiratu, sguassatu, ùn và micca cù l’indirizzu di ridirezzione usatu in a dumanda d’auturizazione, o hè statu emessu per un’altru cliente. + invalid_redirect_uri: L’URI di ridirezzione ùn hè curretta. + invalid_request: Ci manca un parametru riquestu indè a dumanda, cuntene un parametru ch’ùn esiste micca, o altru sbagliu di forma. + invalid_resource_owner: L’idintificanti di u pruprietariu di a risorsa ùn sò curretti, o u pruprietariu ùn pò micca esse trovu + invalid_scope: U scopu dumandatu ùn hè currettu, hè scunnisciutu, o altru sbagliu di forma. + invalid_token: + expired: A marca d’accessu hè spirata + revoked: A marca d’accessu hè stata rivucata + unknown: A marca d’accessu ùn hè curretta + resource_owner_authenticator_not_configured: Ùn c’hè micca pussutu ricercà u pruprietariu di a risorsa perchè Doorkeeper.configure.resource_owner_authenticator ùn hè micca cunfiguratu. + server_error: C’hè statu un prublemu cù u servore d’auturizazione. + temporarily_unavailable: U servore d’auturizazione ùn pò micca trattà a dumanda avà perchè hè sopraccaricatu o in mantenimentu. + unauthorized_client: U cliente ùn pò micca fà sta dumanda cusì. + unsupported_grant_type: Stu tippu d’accunsentu ùn marchja micca nant’à stu servore d’auturizazione. + unsupported_response_type: Sta risposta ùn marchja micca nant’à stu servore d’auturizazione. + flash: + applications: + create: + notice: Applicazione creata. + destroy: + notice: Applicazione sguassata. + update: + notice: Applicazione messa à ghjornu. + authorized_applications: + destroy: + notice: Applicazione sguassata. + layouts: + admin: + nav: + applications: Applicazione + oauth2_provider: Furnitore OAuth2 + application: + title: Auturizazione OAuth riquestata + scopes: + follow: bluccà, sbluccà, è reghje l’abbunamenti + read: leghje l’infurmazione di u vostru contu + write: mandà missaghji per voi diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index 33d544bed5..eca1fc675f 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -115,5 +115,6 @@ en: title: OAuth authorization required scopes: follow: follow, block, unblock and unfollow accounts + push: receive push notifications for your account read: read your account's data write: post on your behalf diff --git a/config/locales/doorkeeper.eo.yml b/config/locales/doorkeeper.eo.yml index 59df528526..9713c462c4 100644 --- a/config/locales/doorkeeper.eo.yml +++ b/config/locales/doorkeeper.eo.yml @@ -115,5 +115,6 @@ eo: title: OAuth-a rajtigo bezonata scopes: follow: sekvi, bloki, malbloki kaj malsekvi kontojn + push: ricevi puŝ-sciigojn por via konto read: legi la datumojn de via konto write: mesaĝi kiel vi diff --git a/config/locales/doorkeeper.fa.yml b/config/locales/doorkeeper.fa.yml index f3db862ca1..2293f1115e 100644 --- a/config/locales/doorkeeper.fa.yml +++ b/config/locales/doorkeeper.fa.yml @@ -19,56 +19,56 @@ fa: doorkeeper: applications: buttons: - authorize: Authorize - cancel: Cancel - destroy: Destroy - edit: Edit + authorize: اجازه دادن + cancel: لغو + destroy: پاک کردن + edit: ویرایش submit: Submit confirmations: - destroy: Are you sure? + destroy: آیا مطمئن هستید؟ edit: - title: Edit application + title: ویرایش برنامه form: - error: Whoops! Check your form for possible errors + error: اوخ! ببینید چیزی را اشتباهی در فرم وارد نکردهاید؟ help: - native_redirect_uri: Use %{native_redirect_uri} for local tests - redirect_uri: Use one line per URI - scopes: Separate scopes with spaces. Leave blank to use the default scopes. + native_redirect_uri: برای آزمایشهای محلی %{native_redirect_uri} را به کار ببرید + redirect_uri: هر URI را در یک سطر جدا بنویسید + scopes: دامنهها را با فاصلهٔ خالی از هم جدا کنید. برای بهکاربردن دامنهٔ پیشفرض خالی بگذارید. index: - application: Application - callback_url: Callback URL + application: برنامه + callback_url: نشانی Callback delete: Delete name: Name - new: New application - scopes: Scopes - show: Show - title: Your applications + new: برنامهٔ تازه + scopes: دامنهها + show: نمایش + title: برنامههای شما new: - title: New application + title: برنامهٔ تازه show: actions: Actions - application_id: Client key - callback_urls: Callback URLs - scopes: Scopes - secret: Client secret - title: 'Application: %{name}' + application_id: کلید کلاینت + callback_urls: نشانیهای Callabck + scopes: دامنهها + secret: کد سری کلاینت + title: 'برنامه: %{name}' authorizations: buttons: - authorize: Authorize - deny: Deny + authorize: اجازه دادن + deny: لغو اجازه error: - title: An error has occurred + title: خطایی رخ داد new: - able_to: It will be able to - prompt: Application %{client_name} requests access to your account - title: Authorization required + able_to: اجازه خواهد داشت + prompt: برنامهٔ %{client_name} میخواهد به حساب شما دسترسی داشته باشد + title: نیاز به اجازه دادن show: - title: Copy this authorization code and paste it to the application. + title: این کد مجوز را کپی کرده و در برنامه وارد کنید. authorized_applications: buttons: - revoke: Revoke + revoke: فسخ confirmations: - revoke: Are you sure? + revoke: آیا مطمئن هستید؟ index: application: برنامه created_at: مجازشده از @@ -77,7 +77,7 @@ fa: title: برنامههای مجاز errors: messages: - access_denied: The resource owner or authorization server denied the request. + access_denied: دارندهٔ منبع یا سرور اجازه دهنده درخواست را نپذیرفت. credential_flow_not_configured: Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured. invalid_client: Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method. invalid_grant: The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. @@ -86,34 +86,35 @@ fa: invalid_resource_owner: The provided resource owner credentials are not valid, or resource owner cannot be found invalid_scope: The requested scope is invalid, unknown, or malformed. invalid_token: - expired: The access token expired - revoked: The access token was revoked - unknown: The access token is invalid + expired: کد دسترسی منقضی شده است + revoked: کد دسترسی فسخ شده است + unknown: کد دسترسی معتبر نیست resource_owner_authenticator_not_configured: Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfiged. - server_error: The authorization server encountered an unexpected condition which prevented it from fulfilling the request. - temporarily_unavailable: The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server. + server_error: خطای پیشبینینشدهای برای سرور اجازهدهنده رخ داد که جلوی اجرای این درخواست را گرفت. + temporarily_unavailable: سرور اجازهدهنده به دلیل بار زیاد یا تعمیرات سرور هماینک نمیتواند درخواست شما را بررسی کند. unauthorized_client: The client is not authorized to perform this request using this method. unsupported_grant_type: The authorization grant type is not supported by the authorization server. unsupported_response_type: The authorization server does not support this response type. flash: applications: create: - notice: Application created. + notice: برنامه ساخته شد. destroy: - notice: Application deleted. + notice: برنامه حذف شد. update: - notice: Application updated. + notice: برنامه بهروز شد. authorized_applications: destroy: - notice: Application revoked. + notice: برنامه فسخ شد. layouts: admin: nav: - applications: Applications - oauth2_provider: OAuth2 Provider + applications: برنامهها + oauth2_provider: فراهمکنندهٔ ورود دومرحلهای application: - title: OAuth authorization required + title: درخواست اجازهٔ OAuth scopes: - follow: follow, block, unblock and unfollow accounts - read: read your account's data - write: post on your behalf + follow: پیگیری، مسدودسازی، لغو مسدودسازی، و لغو پیگیری حسابها + push: برای حساب خود اعلانهای لحظهای دریافت کنید + read: خواندن اطلاعات حساب شما + write: انتشار مطالب از طرف شما diff --git a/config/locales/doorkeeper.fr.yml b/config/locales/doorkeeper.fr.yml index a5d9b9e030..0c68dfa013 100644 --- a/config/locales/doorkeeper.fr.yml +++ b/config/locales/doorkeeper.fr.yml @@ -116,5 +116,6 @@ fr: title: Autorisation OAuth requise scopes: follow: s’abonner, se désabonner, bloquer et débloquer des comptes + push: recevoir des notifications pour votre compte read: lire les données de votre compte write: poster en votre nom diff --git a/config/locales/doorkeeper.gl.yml b/config/locales/doorkeeper.gl.yml index dc9a04f18f..6d0e16defb 100644 --- a/config/locales/doorkeeper.gl.yml +++ b/config/locales/doorkeeper.gl.yml @@ -115,5 +115,6 @@ gl: title: Precisa autorización OAuth scopes: follow: seguir, bloquear, desbloquear e deixar de seguir contas + push: recibir notificatións tipo push para a súa conta read: ler os datos da súa conta write: publicar no seu nome diff --git a/config/locales/doorkeeper.it.yml b/config/locales/doorkeeper.it.yml index 50b2c97801..ce6fa07d26 100644 --- a/config/locales/doorkeeper.it.yml +++ b/config/locales/doorkeeper.it.yml @@ -115,5 +115,6 @@ it: title: Autorizzazione OAuth richiesta scopes: follow: seguire, bloccare, sbloccare e smettere di seguire account + push: ricevi notifiche push per il tuo account read: leggere le informazioni del tuo account write: pubblicare post in tua vece diff --git a/config/locales/doorkeeper.ja.yml b/config/locales/doorkeeper.ja.yml index 96956c60f9..9c9098976f 100644 --- a/config/locales/doorkeeper.ja.yml +++ b/config/locales/doorkeeper.ja.yml @@ -115,5 +115,6 @@ ja: title: OAuth認証 scopes: follow: アカウントのフォロー, ブロック, ブロック解除, フォロー解除 + push: アカウントへのプッシュ通知の受信 read: アカウントからのデータの読み取り write: アカウントへのデータの書き込み diff --git a/config/locales/doorkeeper.nl.yml b/config/locales/doorkeeper.nl.yml index 7ad10f45ba..996cdce7f0 100644 --- a/config/locales/doorkeeper.nl.yml +++ b/config/locales/doorkeeper.nl.yml @@ -116,5 +116,6 @@ nl: title: OAuth-autorisatie vereist scopes: follow: accounts volgen, negeren en blokkeren + push: ontvang pushmeldingen voor jouw account read: berichten lezen write: berichten plaatsen diff --git a/config/locales/doorkeeper.oc.yml b/config/locales/doorkeeper.oc.yml index d83d07438d..46060e49f5 100644 --- a/config/locales/doorkeeper.oc.yml +++ b/config/locales/doorkeeper.oc.yml @@ -115,5 +115,6 @@ oc: title: Cal una autorizacion OAuth scopes: follow: sègre, blocar, quitar de blocar e quitar de sègre de comptes + push: recebre las notificacions push per vòstre compte read: legir las donadas de vòstre compte write: publicar per vos diff --git a/config/locales/doorkeeper.pl.yml b/config/locales/doorkeeper.pl.yml index 6c127b73be..5cad356fc9 100644 --- a/config/locales/doorkeeper.pl.yml +++ b/config/locales/doorkeeper.pl.yml @@ -115,5 +115,6 @@ pl: title: Uwierzytelnienie OAuth jest wymagane scopes: follow: możliwość śledzenia, blokowania, usuwania blokad, anulowania śledzenia kont + push: otrzymywanie powiadomień push dla Twojego konta read: dostęp do odczytu danych konta write: możliwość publikowania wpisów w Twoim imieniu diff --git a/config/locales/doorkeeper.pt-BR.yml b/config/locales/doorkeeper.pt-BR.yml index f3da6fcd1f..8d3ce8a296 100644 --- a/config/locales/doorkeeper.pt-BR.yml +++ b/config/locales/doorkeeper.pt-BR.yml @@ -115,5 +115,6 @@ pt-BR: title: Autorização OAuth obrigatória scopes: follow: seguir, bloquear, desbloquear e deixar de seguir outras contas + push: receber notificações push para a sua conta read: ler os dados da sua conta write: postar em seu nome diff --git a/config/locales/doorkeeper.ru.yml b/config/locales/doorkeeper.ru.yml index 28c0ff0bdf..0a88d628e5 100644 --- a/config/locales/doorkeeper.ru.yml +++ b/config/locales/doorkeeper.ru.yml @@ -115,5 +115,6 @@ ru: title: Требуется авторизация OAuth scopes: follow: подписываться, отписываться, блокировать и разблокировать аккаунты + push: принимать push-уведомления для Вашего аккаунта read: читать данные Вашего аккаунта write: отправлять за Вас посты diff --git a/config/locales/doorkeeper.sk.yml b/config/locales/doorkeeper.sk.yml index bda26429e8..d909271892 100644 --- a/config/locales/doorkeeper.sk.yml +++ b/config/locales/doorkeeper.sk.yml @@ -33,14 +33,14 @@ sk: help: native_redirect_uri: Použite %{native_redirect_uri} pre lokálne testy redirect_uri: Iba jedna URI na riadok - scopes: Rozsahy oddeľujte medzerami. Nechajte prázdne pre štandardné rozsahy. + scopes: Oprávnenia oddeľujte medzerami. Nechajte prázdne pre štandardné oprávnenia. index: application: Aplikácia callback_url: Návratová URL delete: Zmazať name: Názov new: Nová aplikácia - scopes: Rozsahy + scopes: Oprávnenia show: Ukázať title: Vaše aplikácie new: @@ -49,7 +49,7 @@ sk: actions: Akcie application_id: Kľúč klienta callback_urls: Návratové URL adresy - scopes: Rozsahy + scopes: Oprávnenia secret: Tajné slovo klienta title: 'Aplikácia: %{name}' authorizations: @@ -73,7 +73,7 @@ sk: application: Aplikácia created_at: Autorizované date_format: "%Y-%m-%d %H:%M:%S" - scopes: Rozsahy + scopes: Oprávnenia title: Vaše autorizované aplikácie errors: messages: @@ -115,5 +115,6 @@ sk: title: Požadovaná OAuth autorizácia scopes: follow: sledovať, blokovať, povoliť a zušiť sledovanie účtov + push: dostávaj oznámenia ohľadom tvojho účtu ako notifikácie na plochu read: prezrieť dáta na vašom účete write: poslať vo vašom mene diff --git a/config/locales/doorkeeper.sl.yml b/config/locales/doorkeeper.sl.yml new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/config/locales/doorkeeper.sl.yml @@ -0,0 +1 @@ +{} diff --git a/config/locales/doorkeeper.zh-CN.yml b/config/locales/doorkeeper.zh-CN.yml index 90c484369c..3c7dd99be4 100644 --- a/config/locales/doorkeeper.zh-CN.yml +++ b/config/locales/doorkeeper.zh-CN.yml @@ -115,5 +115,6 @@ zh-CN: title: 需要 OAuth 认证 scopes: follow: 关注或屏蔽用户 + push: 接收你的帐户的推送通知 read: 读取你的帐户数据 write: 为你发表嘟文 diff --git a/config/locales/doorkeeper.zh-HK.yml b/config/locales/doorkeeper.zh-HK.yml index 6eddcc27bf..19ed76d1a5 100644 --- a/config/locales/doorkeeper.zh-HK.yml +++ b/config/locales/doorkeeper.zh-HK.yml @@ -115,5 +115,6 @@ zh-HK: title: 需要 OAuth 授權 scopes: follow: 關注、封鎖、解除封鎖及取消關注用戶 + push: 接收你的帳號的推送通知 read: 閱讀你的用戶資料 write: 以你的名義發佈文章 diff --git a/config/locales/el.yml b/config/locales/el.yml index 8741635e1d..77b794dc86 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -1,12 +1,15 @@ --- el: about: + about_hashtag_html: Αυτά είναι δημόσια τουτ σημειωμένα με #%{hashtag}. Μπορείς να αλληλεπιδράσεις με αυτά αν έχεις λογαριασμό οπουδήποτε στο fediverse. about_mastodon_html: Το Mastodon είναι ένα κοινωνικό δίκτυο που βασίζεται σε ανοιχτά δικτυακά πρωτόκολλα και ελεύθερο λογισμικό ανοιχτού κώδικα. Είναι αποκεντρωμένο όπως το e-mail. about_this: Σχετικά administered_by: 'Διαχειρίζεται από:' - closed_registrations: Αυτή τη στιγμή οι εγγραφές σε αυτό τον διακομιστή είναι κλειστές. Αλλά! Μπορείς να βρεις έναν άλλο διακομιστή για να ανοίξεις λογαριασμό και να έχεις πρόσβαση από εκεί στο ίδιο ακριβώς δίκτυο. + closed_registrations: Αυτή τη στιγμή οι εγγραφές σε αυτό τον κόμβο είναι κλειστές. Αλλά! Μπορείς να βρεις έναν άλλο κόμβο για να ανοίξεις λογαριασμό και να έχεις πρόσβαση από εκεί στο ίδιο ακριβώς δίκτυο. contact: Επικοινωνία contact_missing: Δεν έχει οριστεί + contact_unavailable: Μ/Δ + description_headline: Τι είναι το %{domain}; domain_count_after: άλλοι διακομιστές domain_count_before: Συνδέεται με extended_description_html: | @@ -15,17 +18,21 @@ el: features: humane_approach_body: Μαθαίνοντας από τις αποτυχίες άλλων δικτύων, το Mastodon στοχεύει να κάνει σχεδιαστικά ηθικές επιλογές για να καταπολεμήσει την κακόβουλη χρήση των κοινωνικών δικτύων. humane_approach_title: Μια πιο ανθρώπινη προσέγγιση - not_a_product_title: Είσαι άτομο, όχι προϊόν + not_a_product_body: Το Mastodon δεν είναι εμπορικό δίκτυο. Δεν έχει διαφημίσεις, δεν έχει εξόρυξη δεδομένων, δεν έχει φραγμένες περιοχές. Δεν υπάρχει κεντρικό σημείο ελέγχου. + not_a_product_title: Είσαι άνθρωπος, όχι προϊόν + real_conversation_body: Με 500 χαρακτήρες στη διάθεσή σου και υποστήριξη για λεπτομερή έλεγχο και προειδοποιήσεις πολυμέσων, μπορείς να εκφραστείς με τον τρόπο που θέλεις. real_conversation_title: Φτιαγμένο για αληθινή συζήτηση within_reach_body: Οι πολλαπλές εφαρμογές για το iOS, το Android και τις υπόλοιπες πλατφόρμες, χάρη σε ένα φιλικό προς τους προγραμματιστές οικοσύστημα API, σου επιτρέπουν να κρατάς επαφή με τους φίλους και τις φίλες σου οπουδήποτε. + within_reach_title: Πάντα προσβάσιμο generic_description: "%{domain} είναι ένας εξυπηρετητής στο δίκτυο" hosted_on: Το Mastodon φιλοξενείται στο %{domain} learn_more: Μάθε περισσότερα - other_instances: Λίστα διακομιστών + other_instances: Λίστα κόμβων source_code: Πηγαίος κώδικας status_count_after: καταστάσεις status_count_before: Ποιός συνέγραψε user_count_after: χρήστες + user_count_before: Σπίτι what_is_mastodon: Τι είναι το Mastodon; accounts: follow: Ακολούθησε @@ -33,8 +40,294 @@ el: following: Ακολουθεί media: Πολυμέσα moved_html: 'Ο/Η %{name} μετακόμισε στο %{new_profile_link}:' + network_hidden: Αυτή η πληροφορία δεν είναι διαθέσιμη nothing_here: Δεν υπάρχει τίποτα εδώ! people_followed_by: Χρήστες που ακολουθεί ο/η %{name} people_who_follow: Χρήστες που ακολουθούν τον/την %{name} posts: Τουτ posts_with_replies: Τουτ και απαντήσεις + remote_follow: Απομακρυσμένη παρακολούθηση + reserved_username: Το όνομα χρήστη είναι κατειλημμένο + roles: + admin: Διαχειριστής + bot: Μποτ (αυτόματος λογαριασμός) + moderator: Μεσολαβητής + unfollow: Διακοπή παρακολούθησης + admin: + account_moderation_notes: + create: Άφησε σημείωση + created_msg: Επιτυχής δημιουργία σημειώματος μεσολάβησης! + delete: Διαγραφή + destroyed_msg: Επιτυχής καταστροφή σημειώματος μεσολάβησης! + accounts: + are_you_sure: Σίγουρα; + avatar: Αβατάρ + by_domain: Τομέας + change_email: + changed_msg: Επιτυχής αλλαγή email λογαριασμού! + current_email: Τρέχον email + label: Αλλαγή email + new_email: Νέο email + submit: Αλλαγή email + title: Αλλαγή email για %{username} + confirm: Επιβεβαίωση + confirmed: Επιβεβαιώθηκε + confirming: Προς επιβεβαίωση + demote: Υποβίβαση + disable: Απενεργοποίηση + disable_two_factor_authentication: Απενεργοποίηση 2FA + disabled: Απενεργοποιημένο + display_name: Όνομα εμφάνισης + domain: Τομέας + edit: Αλλαγή + email: Email + email_status: Κατάσταση email + enable: Ενεργοποίηση + enabled: Ενεργοποιημένο + feed_url: URL ροής + followers: Ακόλουθοι + followers_url: URL ακολούθων + follows: Ακολουθεί + inbox_url: URL εισερχομένων + ip: IP + location: + all: Όλα + local: Τοπικά + remote: Απομακρυσμένα + title: Τοποθεσία + login_status: Κατάσταση εισόδου + media_attachments: Συνημμένα πολυμέσα + memorialize: Μετατροπή σε νεκρολογία + moderation: + all: Όλα + silenced: Αποσιωπημένα + suspended: Σε αναστολή + title: Μεσολάβηση + moderation_notes: Σημειώσεις μεσολάβησης + most_recent_activity: Πιο πρόσφατη δραστηριότητα + most_recent_ip: Πιο πρόσφατη IP + not_subscribed: Άνευ εγγραφής + order: + alphabetic: Αλφαβητικά + most_recent: Πιο πρόσφατα + title: Ταξινόμηση + outbox_url: URL εξερχομένων + perform_full_suspension: Πλήρης αναστολή + profile_url: URL προφίλ + promote: Προβίβασε + protocol: Πρωτόκολλο + public: Δημόσιο + push_subscription_expires: Η εγγραφή PuSH λήγει + redownload: Ανανέωση αβατάρ + remove_avatar: Απομακρυσμένο αβατάρ + resend_confirmation: + already_confirmed: Ήδη επιβεβαιωμένος χρήστης + send: Επανάληψη αποστολής email επιβεβαίωσης + success: Το email επιβεβαίωσης στάλθηκε επιτυχώς! + reset: Επαναφορά + reset_password: Επαναφορά συνθηματικού + resubscribe: Επανεγγραφή + role: Δικαιώματα + roles: + admin: Διαχειριστής + moderator: Συντονιστής + staff: Προσωπικό + user: Χρήστης + salmon_url: URL Salmon + search: Αναζήτηση + shared_inbox_url: URL κοινόχρηστων εισερχομένων + show: + created_reports: Αναφορές από αυτόν το λογαριασμό + report: κατάγγειλε + targeted_reports: Αναφορές για αυτόν το λογαριασμό + silence: Αποσιώπησε + statuses: Καταστάσεις + subscribe: Εγγραφή + title: Λογαριασμοί + unconfirmed_email: Ανεπιβεβαίωτο email + undo_silenced: Αναίρεση αποσιώπησης + undo_suspension: Αναίρεση παύσης + unsubscribe: Κατάργηση εγγραφής + username: Όνομα χρήστη + web: Διαδίκτυο + action_logs: + actions: + assigned_to_self_report: Ο/Η %{name} ανάθεσε την καταγγελία %{target} στον εαυτό του/της + change_email_user: Ο/Η %{name} άλλαξε τη διεύθυνση email του χρήστη %{target} + confirm_user: Ο/Η %{name} επιβεβαίωσε τη διεύθυνση email του χρήστη %{target} + create_custom_emoji: Ο/Η %{name} ανέβασε νέο emoji %{target} + create_domain_block: Ο/Η %{name} μπλόκαρε τον τομέα %{target} + create_email_domain_block: Ο/Η %{name} έβαλε τον τομέα email %{target} σε μαύρη λίστα + demote_user: Ο/Η %{name} υποβίβασε το χρήστη %{target} + destroy_domain_block: Ο/Η %{name} ξεμπλόκαρε τον τομέα %{target} + destroy_email_domain_block: Ο/Η %{name} έβαλε τον τομέα email %{target} σε λευκή λίστα + destroy_status: Ο/Η %{name} αφαίρεσε την κατάσταση του/της %{target} + disable_2fa_user: Ο/Η %{name} απενεργοποίησε την απαίτηση δύο παραγόντων για το χρήστη %{target} + disable_custom_emoji: Ο/Η %{name} απενεργοποίησε το emoji %{target} + disable_user: Ο/Η %{name} απενεργοποίησε την είσοδο για το χρήστη %{target} + enable_custom_emoji: Ο/Η %{name} ενεργοποίησε το emoji %{target} + enable_user: Ο/Η %{name} ενεργοποίησε την είσοδο του χρήστη %{target} + memorialize_account: Ο/Η %{name} μετέτρεψε το λογαριασμό του/της %{target} σε σελίδα νεκρολογίας + promote_user: Ο/Η %{name} προβίβασε το χρήστη %{target} + remove_avatar_user: Ο/Η %{name} αφαίρεσε το αβατάρ του/της %{target} + reopen_report: Ο/Η %{name} ξανάνοιξε την καταγγελία %{target} + reset_password_user: Ο/Η %{name} επανέφερε το συνθηματικό του χρήστη %{target} + resolve_report: Ο/Η %{name} επέλυσε την καταγγελία %{target} + silence_account: Ο/Η %{name} αποσιώπησε το λογαριασμό του/της %{target} + suspend_account: Ο/Η %{name} έπαυσε το λογαριασμό του/της %{target} + unassigned_report: Ο/Η %{name} αποδέσμευσε την καταγγελία %{target} + unsilence_account: Ο/Η %{name} ήρε την αποσιώπηση του λογαριασμού του/της %{target} + unsuspend_account: Ο/Η %{name} ήρε την παύση του λογαριασμού του χρήστη %{target} + update_custom_emoji: Ο/Η %{name} ενημέρωσε το emoji %{target} + update_status: Ο/Η %{name} ενημέρωσε την κατάσταση του/της %{target} + title: Αρχείο ελέγχου + custom_emojis: + by_domain: Τομέας + copied_msg: Επιτυχής δημιουργία τοπικού αντίγραφου του emoji + copy: Αντιγραφή + copy_failed_msg: Αδυναμία δημιουργίας τοπικού αντίγραφου αυτού του emoji + created_msg: Επιτυχής δημιουργία του emoji! + delete: Διαγραφή + destroyed_msg: Επιτυχής καταστροφή του emojo! + disable: Απενεργοποίηση + disabled_msg: Επιτυχής απενεργοποίηση αυτού του emoji + emoji: Emoji + enable: Ενεργοποίηση + enabled_msg: Επιτυχής ενεργοποίηση αυτού του emoji + image_hint: PNG έως 50KB + listed: Αναφερθέντα + new: + title: Προσθήκη νέου προσαρμοσμένου emoji + overwrite: Αντικατάσταση + shortcode: Σύντομος κωδικός + shortcode_hint: Τουλάχιστον 2 χαρακτήρες, μόνο αλφαριθμητικοί και κάτω παύλες + title: Προσαρμοσμένα emoji + unlisted: Μη καταχωρημένα + update_failed_msg: Αδυναμία ενημέρωσης του emoji + updated_msg: Επιτυχής ενημέρωση του Emoji! + upload: Ανέβασμα + domain_blocks: + add_new: Προσθήκη νέου + created_msg: Ο αποκλεισμός τομέα είναι υπό επεξεργασία + destroyed_msg: Ο αποκλεισμός τομέα άρθηκε + domain: Τομέας + new: + create: Δημιουργία αποκλεισμού + hint: Ο αποκλεισμός τομέα δεν θα αποτρέψει νέες καταχωρίσεις λογαριασμών στην βάση δεδομένων, αλλά θα εφαρμόσει αναδρομικά και αυτόματα συγκεκριμένες πολιτικές μεσολάβησης σε αυτούς τους λογαριασμούς. + severity: + noop: Κανένα + silence: Σίγαση + suspend: Αναστολή + title: Αποκλεισμός νέου τομέα + reject_media: Απόρριψη πολυμέσων + severities: + noop: Κανένα + silence: Αποσιώπηση + suspend: Αναστολή + severity: Αυστηρότητα + show: + affected_accounts: + one: Επηρεάζεται ένας λογαριασμός στη βάση δεδομένων + other: Επηρεάζονται %{count} λογαριασμοί στη βάση δεδομένων + retroactive: + silence: Αναίρεση αποσιώπησης όλων των λογαριασμός του τομέα + suspend: Αναίρεση αναστολής όλων των λογαριασμών του τομέα + title: Αναίρεση αποκλεισμού για τον τομέα %{domain} + undo: Αναίρεση + title: Αποκλεισμένοι τομείς + undo: Αναίρεση + email_domain_blocks: + add_new: Πρόσθεση νέου + created_msg: Επιτυχής πρόσθεση email τομέα σε μαύρη λίστα + delete: Διαγραφή + destroyed_msg: Επιτυχής διαγραφή email τομέα από τη μαύρη λίστα + domain: Τομέας + new: + create: Πρόσθεση τομέα + title: Νέα εγγραφή email στη μαύρη λίστα + title: Μαύρη λίστα email + instances: + account_count: Γνωστοί λογαριασμοί + domain_name: Τομέας + reset: Επαναφορά + search: Αναζήτηση + title: Γνωστοί κόμβοι + invites: + filter: + all: Όλες + available: Διαθέσιμες + expired: Ληγμένες + title: Φίλτρο + title: Προσκλήσεις + report_notes: + created_msg: Επιτυχής δημιουργία σημείωσης καταγγελίας! + destroyed_msg: Επιτυχής διαγραφή σημείωσης καταγγελίας! + reports: + account: + note: σημείωση + report: καταγγελία + action_taken_by: Ενέργεια από τον/την + are_you_sure: Σίγουρα; + assign_to_self: Ανάθεση σε μένα + assigned: Αρμόδιος συντονιστής + comment: + none: Κανένα + created_at: Αναφέρθηκε + id: ID + mark_as_resolved: Σημειωμένο ως επιλυμένο + mark_as_unresolved: Σημειωμένο ως ανεπίλυτο + notes: + create: Πρόσθεσε σημείωση + create_and_resolve: Επίλυσε με σημείωση + create_and_unresolve: Ξανάνοιξε με σημείωση + delete: Διέγραψε + placeholder: Περιέγραψε τις ενέργειες που έγιναν, ή οποιαδήποτε άλλη ενημέρωση... + reopen: Ξανάνοιξε την καταγγελία + report: 'Καταγγελία #%{id}' + report_contents: Περιεχόμενα + reported_account: Αναφερόμενος λογαριασμός + reported_by: Αναφέρθηκε από + resolved: Επιλύθηκε + resolved_msg: Η καταγγελία επιλύθηκε επιτυχώς! + silence_account: Αποσιώπηση λογαριασμού + status: Κατάσταση + suspend_account: Ανέστειλε λογαριασμό + target: Στόχος + title: Αναφορές + settings: + hero: + desc_html: Εμφανίζεται στην μπροστινή σελίδα. Συνίσταται τουλάχιστον 600x100px. Όταν λείπει, χρησιμοποιείται η μικρογραφία του κόμβου + peers_api_enabled: + desc_html: Ονόματα τομέων που αυτός ο κόμβος έχει πετύχει στο fediverse + show_known_fediverse_at_about_page: + title: Εμφάνιση του γνωστού fediverse στην προεπισκόπηση ροής + site_description: + title: Περιγραφή κόμβου + site_description_extended: + desc_html: Ένα καλό μέρος για τον κώδικα δεοντολογίας, τους κανόνες, τις οδηγίες και ό,τι άλλο διαφοροποιεί τον κόμβο σου. Δέχεται και κώδικα HTML + site_title: Όνομα κόμβου + thumbnail: + title: Μικρογραφία κόμβου + timeline_preview: + desc_html: Εμφάνισε τη δημόσια ροή στην αρχική σελίδα + title: Προεπισκόπιση ροής + admin_mailer: + new_report: + subject: Νέα καταγγελία για %{instance} (#%{id}) + auth: + agreement_html: Με την εγγραφή σου, συμφωνείς να ακολουθείς τους κανόνες αυτού του κόμβου και τους όρους χρήσης του. + deletes: + warning_html: Μόνο η διαγραφή περιεχομένου από αυτό τον συγκεκριμένο κόμβο είναι εγγυημένη. Το περιεχόμενο που έχει διαμοιραστεί ευρέως είναι πιθανό να αφήσει ίχνη. Όσοι διακομιστές είναι εκτός σύνδεσης και όσοι έχουν διακόψει τη λήψη των ενημερώσεων του κόμβου σου, δε θα ενημερώσουν τις βάσεις δεδομένων τους. + imports: + preface: Μπορείς να εισάγεις τα δεδομένα που έχεις εξάγει από άλλο κόμβο, όπως τη λίστα των ανθρώπων που ακολουθείς ή μπλοκάρεις. + invites: + prompt: Φτιάξε και μοίρασε συνδέσμους με τρίτους για να δώσεις πρόσβαση σε αυτόν τον κόμβο + terms: + title: Όροι Χρήσης και Πολιτική Απορρήτου του κόμβου %{instance} + user_mailer: + welcome: + final_step: 'Ξεκίνα τις δημοσιεύσεις! Ακόμα και χωρίς ακόλουθους τα δημόσια μηνύματά σου μπορεί να τα δουν άλλοι, για παράδειγμα στην τοπική ροή και στις ετικέτες. Ίσως να θέλεις να κάνεις μια εισαγωγή του εαυτού σου με την ετικέτα #introductions.' + full_handle_hint: Αυτό θα εδώ θα πεις στους φίλους σου για να σου μιλήσουν ή να σε ακολουθήσουν από άλλο κόμβο. + tip_federated_timeline: Η ομοσπονδιακή ροή είναι μια όψη πραγματικού χρόνου στο δίκτυο του Mastodon. Παρόλα αυτά, περιλαμβάνει μόνο όσους ακολουθούν οι γείτονές σου, άρα δεν είναι πλήρης. + tip_following: Ακολουθείς το διαχειριστή του διακομιστή σου αυτόματα. Για να βρεις περισσότερους ενδιαφέροντες ανθρώπους, έλεγξε την τοπική και την ομοσπονδιακή ροή. + tip_local_timeline: Η τοπική ροή είναι η όψη πραγματικού χρόνου των ανθρώπων στον κόμβο %{instance}. Αυτοί είναι οι άμεσοι γείτονές σου! diff --git a/config/locales/en.yml b/config/locales/en.yml index 8e1b916c17..3c2a8c3db7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -40,6 +40,7 @@ en: following: Following media: Media moved_html: "%{name} has moved to %{new_profile_link}:" + network_hidden: This information is not available nothing_here: There is nothing here! people_followed_by: People whom %{name} follows people_who_follow: People who follow %{name} @@ -49,13 +50,12 @@ en: reserved_username: The username is reserved roles: admin: Admin + bot: Bot moderator: Mod unfollow: Unfollow admin: account_moderation_notes: - account: Moderator - create: Create - created_at: Date + create: Leave note created_msg: Moderation note successfully created! delete: Delete destroyed_msg: Moderation note successfully destroyed! @@ -72,6 +72,7 @@ en: title: Change Email for %{username} confirm: Confirm confirmed: Confirmed + confirming: Confirming demote: Demote disable: Disable disable_two_factor_authentication: Disable 2FA @@ -80,6 +81,7 @@ en: domain: Domain edit: Edit email: E-mail + email_status: E-mail Status enable: Enable enabled: Enabled feed_url: Feed URL @@ -118,6 +120,10 @@ en: push_subscription_expires: PuSH subscription expires redownload: Refresh avatar remove_avatar: Remove avatar + resend_confirmation: + already_confirmed: This user is already confirmed + send: Resend confirmation email + success: Confirmation email successfully sent! reset: Reset reset_password: Reset password resubscribe: Resubscribe @@ -269,7 +275,6 @@ en: comment: none: None created_at: Reported - delete: Delete id: ID mark_as_resolved: Mark as resolved mark_as_unresolved: Mark as unresolved @@ -278,10 +283,7 @@ en: create_and_resolve: Resolve with note create_and_unresolve: Reopen with note delete: Delete - placeholder: Describe what actions have been taken, or any other updates to this report… - nsfw: - 'false': Unhide media attachments - 'true': Hide media attachments + placeholder: Describe what actions have been taken, or any other related updates... reopen: Reopen report report: 'Report #%{id}' report_contents: Contents @@ -356,11 +358,8 @@ en: delete: Delete nsfw_off: Mark as not sensitive nsfw_on: Mark as sensitive - execute: Execute failed_to_execute: Failed to execute media: - hide: Hide media - show: Show media title: Media no_media: No media title: Account statuses @@ -466,7 +465,7 @@ en: archive_takeout: date: Date download: Download your archive - hint_html: You can request an archive of your toots and uploaded media. The exported data will be in ActivityPub format, readable by any compliant software. + hint_html: You can request an archive of your toots and uploaded media. The exported data will be in ActivityPub format, readable by any compliant software. You can request an archive every 7 days. in_progress: Compiling your archive... request: Request your archive size: Size @@ -560,7 +559,7 @@ en: subject: one: "1 new notification since your last visit \U0001F418" other: "%{count} new notifications since your last visit \U0001F418" - title: In your absence… + title: In your absence... favourite: body: 'Your status was favourited by %{name}:' subject: "%{name} favourited your status" @@ -605,20 +604,6 @@ en: other: Other publishing: Publishing web: Web - push_notifications: - favourite: - title: "%{name} favourited your status" - follow: - title: "%{name} is now following you" - group: - title: "%{count} notifications" - mention: - action_boost: Boost - action_expand: Show more - action_favourite: Favourite - title: "%{name} mentioned you" - reblog: - title: "%{name} boosted your status" remote_follow: acct: Enter your username@domain you want to follow from missing_resource: Could not find the required redirect URL for your account @@ -695,6 +680,7 @@ en: video: one: "%{count} video" other: "%{count} videos" + boosted_from_html: Boosted from %{acct_link} content_warning: 'Content warning: %{warning}' disallowed_hashtags: one: 'contained a disallowed hashtag: %{tags}' @@ -785,9 +771,13 @@ en: - Children's Online Privacy Protection Act Compliance + Site usage by children - Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site. + If this server is in the EU or the EEA: Our site, products and services are all directed to people who are at least 16 years old. If you are under the age of 16, per the requirements of the GDPR (General Data Protection Regulation) do not use this site. + + If this server is in the USA: Our site, products and services are all directed to people who are at least 13 years old. If you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site. + + Law requirements can be different if this server is in another jurisdiction. @@ -799,6 +789,10 @@ en: Originally adapted from the Discourse privacy policy. title: "%{instance} Terms of Service and Privacy Policy" + themes: + contrast: High contrast + default: Mastodon + mastodon-light: Mastodon (light) time: formats: default: "%b %d, %Y, %H:%M" @@ -844,5 +838,6 @@ en: users: invalid_email: The e-mail address is invalid invalid_otp_token: Invalid two-factor code + otp_lost_help_html: If you lost access to both, you may get in touch with %{email} seamless_external_login: You are logged in via an external service, so password and e-mail settings are not available. signed_in_as: 'Signed in as:' diff --git a/config/locales/eo.yml b/config/locales/eo.yml index c768d8a03d..f1a9ff79d4 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -4,6 +4,7 @@ eo: about_hashtag_html: Ĉi tiuj estas la publikaj mesaĝoj markitaj per #%{hashtag}. Vi povas interagi kun ili se vi havas konton ie ajn en la fediverse. about_mastodon_html: Mastodon estas socia reto bazita sur malfermitaj retaj protokoloj kaj sur libera malfermitkoda programo. Ĝi estas sencentra kiel retmesaĝoj. about_this: Pri + administered_by: 'Administrata de:' closed_registrations: Registriĝoj estas nuntempe fermitaj en ĉi tiu nodo. Tamen, vi povas trovi alian nodon por fari konton kaj aliri al la sama reto de tie. contact: Kontakti contact_missing: Ne elektita @@ -39,6 +40,7 @@ eo: following: Sekvatoj media: Aŭdovidaĵoj moved_html: "%{name} moviĝis al %{new_profile_link}:" + network_hidden: Tiu informo ne estas disponebla nothing_here: Estas nenio ĉi tie! people_followed_by: Sekvatoj de %{name} people_who_follow: Sekvantoj de %{name} @@ -48,21 +50,29 @@ eo: reserved_username: La uzantnomo estas rezervita roles: admin: Administranto + bot: Roboto moderator: Kontrolanto unfollow: Ne plu sekvi admin: account_moderation_notes: - account: Kontrolanto - create: Krei - created_at: Dato + create: Lasi noton created_msg: Kontrola noto sukcese kreita! delete: Forigi destroyed_msg: Kontrola noto sukcese detruita! accounts: are_you_sure: Ĉu vi certas? + avatar: Profilbildo by_domain: Domajno + change_email: + changed_msg: Konta retadreso sukcese ŝanĝita! + current_email: Nuna retadreso + label: Ŝanĝi retadreson + new_email: Nova retadreso + submit: Ŝanĝi retadreson + title: Ŝanĝi retadreson por %{username} confirm: Konfirmi confirmed: Konfirmita + confirming: Konfirmante demote: Degradi disable: Malebligi disable_two_factor_authentication: Malebligi 2FA @@ -71,6 +81,7 @@ eo: domain: Domajno edit: Redakti email: Retpoŝto + email_status: Retpoŝto Stato enable: Ebligi enabled: Ebligita feed_url: URL de la fluo @@ -108,6 +119,11 @@ eo: public: Publika push_subscription_expires: Eksvalidiĝo de la abono al PuSH redownload: Aktualigi profilbildon + remove_avatar: Forigi profilbildon + resend_confirmation: + already_confirmed: Ĉi tiu uzanto jam estas konfirmita + send: Esend konfirmi retpoŝton + success: Konfirma retmesaĝo sukcese sendita! reset: Restarigi reset_password: Restarigi pasvorton resubscribe: Reaboni @@ -128,6 +144,7 @@ eo: statuses: Mesaĝoj subscribe: Aboni title: Kontoj + unconfirmed_email: Nekonfirmita retadreso undo_silenced: Malfari kaŝon undo_suspension: Malfari haltigon unsubscribe: Malaboni @@ -135,6 +152,8 @@ eo: web: Reto action_logs: actions: + assigned_to_self_report: "%{name} asignis signalon %{target} al si mem" + change_email_user: "%{name} ŝanĝis retadreson de uzanto %{target}" confirm_user: "%{name} konfirmis retadreson de uzanto %{target}" create_custom_emoji: "%{name} alŝutis novan emoĝion %{target}" create_domain_block: "%{name} blokis domajnon %{target}" @@ -150,10 +169,13 @@ eo: enable_user: "%{name} ebligis ensaluton por uzanto %{target}" memorialize_account: "%{name} ŝanĝis la konton de %{target} al memora paĝo" promote_user: "%{name} plirangigis uzanton %{target}" + remove_avatar_user: "%{name} forigis profilbildon de %{target}" + reopen_report: "%{name} remalfermis signalon %{target}" reset_password_user: "%{name} restarigis pasvorton de uzanto %{target}" - resolve_report: "%{name} flankmetis signalon %{target}" + resolve_report: "%{name} solvis signalon %{target}" silence_account: "%{name} kaŝis la konton de %{target}" suspend_account: "%{name} haltigis la konton de %{target}" + unassigned_report: "%{name} malasignis signalon %{target}" unsilence_account: "%{name} malkaŝis la konton de %{target}" unsuspend_account: "%{name} malhaltigis la konton de %{target}" update_custom_emoji: "%{name} ĝisdatigis emoĝion %{target}" @@ -239,28 +261,44 @@ eo: expired: Eksvalida title: Filtri title: Invitoj + report_notes: + created_msg: Signala noto sukcese kreita! + destroyed_msg: Signala noto sukcese forigita! reports: + account: + note: noto + report: signalo action_taken_by: Ago farita de are_you_sure: Ĉu vi certas? + assign_to_self: Asigni al mi + assigned: Asignita kontrolanto comment: none: Nenio - delete: Forigi + created_at: Signalita id: ID - mark_as_resolved: Marki kiel solvita - nsfw: - 'false': Malkaŝi aŭdovidajn kunsendaĵojn - 'true': Kaŝi aŭdovidajn kunsendaĵojn + mark_as_resolved: Marki solvita + mark_as_unresolved: Marki nesolvita + notes: + create: Aldoni noton + create_and_resolve: Solvi per noto + create_and_unresolve: Remalfermi per noto + delete: Forigi + placeholder: Priskribu faritajn agojn, aŭ ajnan novan informon pri tiu signalo… + reopen: Remalfermi signalon report: 'Signalo #%{id}' report_contents: Enhavo reported_account: Signalita konto reported_by: Signalita de resolved: Solvita + resolved_msg: Signalo sukcese solvita! silence_account: Kaŝi konton status: Mesaĝoj suspend_account: Haltigi konton target: Celo title: Signaloj + unassign: Malasigni unresolved: Nesolvita + updated_at: Ĝisdatigita view: Vidi settings: activity_api_enabled: @@ -318,13 +356,10 @@ eo: back_to_account: Reveni al konta paĝo batch: delete: Forigi - nsfw_off: Malŝalti NSFW - nsfw_on: Ŝalti NSFW - execute: Ekigi + nsfw_off: Marki ne tikla + nsfw_on: Marki tikla failed_to_execute: Ekigo malsukcesa media: - hide: Kaŝi aŭdovidaĵojn - show: Montri aŭdovidaĵojn title: Aŭdovidaĵoj no_media: Neniu aŭdovidaĵo title: Mesaĝoj de la konto @@ -340,6 +375,7 @@ eo: admin_mailer: new_report: body: "%{reporter} signalis %{target}" + body_remote: Iu de %{domain} signalis %{target} subject: Nova signalo por %{instance} (#%{id}) application_mailer: notification_preferences: Ŝanĝi retmesaĝajn preferojn @@ -381,6 +417,7 @@ eo: security: Sekureco set_new_password: Elekti novan pasvorton authorize_follow: + already_following: Vi jam sekvas tiun konton error: Bedaŭrinde, estis eraro en la serĉado de la fora konto follow: Sekvi follow_request: 'Vi sendis peton de sekvado al:' @@ -430,7 +467,7 @@ eo: archive_takeout: date: Dato download: Elŝuti vian arkivon - hint_html: Vi povas peti arkivon de viaj mesaĝoj kaj alŝutitaj aŭdovidaĵoj. La eksportitaj datumoj estos en la formato ActivityPub, legebla de ajna konformema programo. + hint_html: Vi povas peti arkivon de viaj mesaĝoj kaj alŝutitaj aŭdovidaĵoj. La eksportitaj datumoj estos en la formato ActivityPub, legebla de ajna konformema programo. Vi povas peti arkivon ĉiuseptage. in_progress: Kunmetado de via arkivo… request: Peti vian arkivon size: Grandeco @@ -475,6 +512,7 @@ eo: '21600': 6 horoj '3600': 1 horo '43200': 12 horoj + '604800': 1 semajno '86400': 1 tago expires_in_prompt: Neniam generate: Krei @@ -559,25 +597,15 @@ eo: other: Aliaj aferoj publishing: Publikado web: Reto - push_notifications: - favourite: - title: "%{name} stelumis vian mesaĝon" - follow: - title: "%{name} eksekvis vin" - group: - title: "%{count} sciigoj" - mention: - action_boost: Diskonigi - action_expand: Montri pli - action_favourite: Stelumi - title: "%{name} menciis vin" - reblog: - title: "%{name} diskonigis vian mesaĝon" remote_follow: acct: Enmetu vian uzantnomo@domajno de kie vi volas sekvi missing_resource: La URL de plusendado ne estis trovita proceed: Daŭrigi por eksekvi prompt: 'Vi eksekvos:' + remote_unfollow: + error: Eraro + title: Titolo + unfollowed: Ne plu sekvita sessions: activity: Lasta ago browser: Retumilo @@ -643,7 +671,11 @@ eo: video: one: "%{count} video" other: "%{count} videoj" + boosted_from_html: Diskonigita de %{acct_link} content_warning: 'Enhava averto: %{warning}' + disallowed_hashtags: + one: 'enhavas malpermesitan kradvorton: %{tags}' + other: 'enhavis malpermesitan kradvorton: %{tags}' open_in_web: Malfermi retumile over_character_limit: limo de %{max} signoj transpasita pin_errors: @@ -668,7 +700,9 @@ eo: terms: title: Uzkondiĉoj kaj privateca politiko de %{instance} themes: + contrast: Forta kontrasto default: Mastodon + mastodon-light: Mastodon (hela) time: formats: default: "%Y-%m-%d %H:%M" @@ -714,5 +748,6 @@ eo: users: invalid_email: La retadreso estas nevalida invalid_otp_token: Nevalida kodo de dufaktora aŭtentigo + otp_lost_help_html: Se vi perdas aliron al ambaŭ, vi povas kontakti %{email} seamless_external_login: Vi estas ensalutinta per ekstera servo, do pasvortaj kaj retadresaj agordoj ne estas disponeblaj. signed_in_as: 'Ensalutinta kiel:' diff --git a/config/locales/es.yml b/config/locales/es.yml index bf449bf92c..9a7cb62fef 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -53,9 +53,7 @@ es: unfollow: Dejar de seguir admin: account_moderation_notes: - account: Moderador create: Crear - created_at: Fecha created_msg: "¡Nota de moderación creada con éxito!" delete: Borrar destroyed_msg: "¡Nota de moderación destruida con éxito!" @@ -72,6 +70,7 @@ es: title: Cambiar el correo electrónico de %{username} confirm: Confirmar confirmed: Confirmado + confirming: Confirmando demote: Degradar disable: Deshabilitar disable_two_factor_authentication: Desactivar autenticación de dos factores @@ -80,6 +79,7 @@ es: domain: Dominio edit: Editar email: E-mail + email_status: E-mail Status enable: Habilitar enabled: Habilitada feed_url: URL de notificaciones @@ -118,6 +118,10 @@ es: push_subscription_expires: Expiración de la suscripción PuSH redownload: Refrescar avatar remove_avatar: Eliminar el avatar + resend_confirmation: + already_confirmed: Este usuario ya está confirmado + send: Reenviar el correo electrónico de confirmación + success: "¡Correo electrónico de confirmación enviado con éxito" reset: Reiniciar reset_password: Reiniciar contraseña resubscribe: Re-suscribir @@ -269,7 +273,6 @@ es: comment: none: Ninguno created_at: Denunciado - delete: Eliminar id: ID mark_as_resolved: Marcar como resuelto mark_as_unresolved: Marcar como no resuelto @@ -279,9 +282,6 @@ es: create_and_unresolve: Reabrir con una nota delete: Eliminar placeholder: Especificar qué acciones se han tomado o cualquier otra novedad respecto a esta denuncia… - nsfw: - 'false': Mostrar multimedia - 'true': Ocultar multimedia reopen: Reabrir denuncia report: 'Reportar #%{id}' report_contents: Contenido @@ -356,11 +356,8 @@ es: delete: Eliminar nsfw_off: Marcar contenido como no sensible nsfw_on: Marcar contenido como sensible - execute: Ejecutar failed_to_execute: Falló al ejecutar media: - hide: Ocultar multimedia - show: Mostrar multimedia title: Multimedia no_media: No hay multimedia title: Estado de las cuentas @@ -376,6 +373,7 @@ es: admin_mailer: new_report: body: "%{reporter} ha reportado a %{target}" + body_remote: Alguien de %{domain} a reportado a %{target} subject: Nuevo reporte para la %{instance} (#%{id}) application_mailer: notification_preferences: Cambiar preferencias de correo electrónico @@ -595,20 +593,6 @@ es: other: Otros publishing: Publicación web: Web - push_notifications: - favourite: - title: "%A {name} le gustó tu estado" - follow: - title: "%{name} te ha empezado a seguir" - group: - title: "%{count} notificaciones" - mention: - action_boost: Retoot - action_expand: Mostrar más - action_favourite: Me Gusta - title: "%{name} te mencionó" - reblog: - title: "%{name} boosteó tu estado" remote_follow: acct: Ingesa tu usuario@dominio desde el que quieres seguir missing_resource: No se pudo encontrar la URL de redirección requerida para tu cuenta diff --git a/config/locales/eu.yml b/config/locales/eu.yml index 0967ef424b..fc8916ab9d 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -1 +1,11 @@ -{} +--- +eu: + about: + about_this: Honi buruz + administered_by: 'Administratzailea(k):' + contact: Kontaktua + contact_missing: Ezarri gabe + contact_unavailable: E/E + description_headline: Zer da %{domain}? + domain_count_after: beste instantziak + domain_count_before: 'Hona konektatuta:' diff --git a/config/locales/fa.yml b/config/locales/fa.yml index a3005547a5..e320092899 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -4,6 +4,7 @@ fa: about_hashtag_html: اینها نوشتههای عمومی هستند که برچسب (هشتگ) #%{hashtag} را دارند. اگر شما روی هر سروری حساب داشته باشید میتوانید به این نوشتهها واکنش نشان دهید. about_mastodon_html: ماستدون (Mastodon) یک شبکهٔ اجتماعی است که بر اساس پروتکلهای آزاد وب و نرمافزارهای آزاد و کدباز ساخته شده است. این شبکه مانند ایمیل غیرمتمرکز است. about_this: درباره + administered_by: 'با مدیریت:' closed_registrations: ثبتنام روی این سرور هماینک فعال نیست. اما شما میتوانید سرور دیگری بیابید و با حسابی که آنجا میسازید دقیقاً به همین شبکه دسترسی داشته باشید. contact: تماس contact_missing: تعیین نشده @@ -39,6 +40,7 @@ fa: following: پی میگیرد media: عکس و ویدیو moved_html: "%{name} حساب خود را به %{new_profile_link} منتقل کرده است:" + network_hidden: این اطلاعات در دسترس نیست nothing_here: اینجا چیزی نیست! people_followed_by: کسانی که %{name} پی میگیرد people_who_follow: کسانی که %{name} را پی میگیرند @@ -48,21 +50,29 @@ fa: reserved_username: این نام کاربری در دسترس نیست roles: admin: مدیر + bot: ربات moderator: ناظم unfollow: پایان پیگیری admin: account_moderation_notes: - account: مدیر - create: نوشتن - created_at: تاریخ + create: افزودن یادداشت created_msg: یادداشت مدیر با موفقیت ساخته شد! delete: پاک کردن destroyed_msg: یادداشت مدیر با موفقیت پاک شد! accounts: are_you_sure: آیا مطمئن هستید؟ + avatar: تصویر نمایه by_domain: دامین + change_email: + changed_msg: نشانی ایمیل این حساب با موفقیت تغییر کرد! + current_email: ایمیل کنونی + label: تغییر نشانی ایمیل + new_email: ایمیل تازه + submit: تغییر ایمیل + title: تغییر ایمیل برای %{username} confirm: تأیید confirmed: تأیید شد + confirming: تأیید demote: تنزلدادن disable: غیرفعال disable_two_factor_authentication: غیرفعالسازی ورود دومرحلهای @@ -71,6 +81,7 @@ fa: domain: دامین edit: ویرایش email: ایمیل + email_status: وضعیت ایمیل enable: فعال enabled: فعال feed_url: نشانی فید @@ -91,7 +102,7 @@ fa: all: همه silenced: بیصدا شده suspended: معلق شده - title: مدیریت + title: وضعیت moderation_notes: یادداشت مدیر most_recent_activity: آخرین فعالیتها most_recent_ip: آخرین IP ها @@ -108,6 +119,11 @@ fa: public: عمومی push_subscription_expires: عضویت از راه PuSH منقضی شد redownload: بهروزرسانی تصویر نمایه + remove_avatar: حذف تصویر نمایه + resend_confirmation: + already_confirmed: این کاربر قبلا تایید شده است + send: ایمیل تایید را دوباره بفرستید + success: ایمیل تایید با موفقیت ارسال شد! reset: بازنشانی reset_password: بازنشانی رمز resubscribe: اشتراک دوباره @@ -128,6 +144,7 @@ fa: statuses: نوشتهها subscribe: اشتراک title: حسابها + unconfirmed_email: ایمیل تأییدنشده undo_silenced: واگردانی بیصداکردن undo_suspension: واگردانی تعلیق unsubscribe: لغو اشتراک @@ -135,6 +152,8 @@ fa: web: وب action_logs: actions: + assigned_to_self_report: "%{name} رسیدگی به گزارش %{target} را به عهده گرفت" + change_email_user: "%{name} نشانی ایمیل کاربر %{target} را تغییر داد" confirm_user: "%{name} نشانی ایمیل کاربر %{target} را تأیید کرد" create_custom_emoji: "%{name} شکلک تازهٔ %{target} را بارگذاشت" create_domain_block: "%{name} دامین %{target} را مسدود کرد" @@ -150,10 +169,13 @@ fa: enable_user: "%{name} ورود را برای کاربر %{target} فعال کرد" memorialize_account: "%{name} حساب کاربر %{target} را تبدیل به صفحهٔ یادمان کرد" promote_user: "%{name} کاربر %{target} را ترفیع داد" + remove_avatar_user: "%{name} تصویر نمایهٔ کاربر %{target} را حذف کرد" + reopen_report: "%{name} گزارش %{target} را دوباره به جریان انداخت" reset_password_user: "%{name} رمز کاربر %{target} را بازنشاند" - resolve_report: "%{name} گزارش %{target} را نادیده گرفت" + resolve_report: "%{name} گزارش %{target} را رفع کرد" silence_account: "%{name} حساب کاربر %{target} را خاموش (بیصدا) کرد" suspend_account: "%{name} حساب کاربر %{target} را تعلیق کرد" + unassigned_report: "%{name} بررسی گزارش %{target} را متوقف کرد" unsilence_account: "%{name} حساب کاربر %{target} را روشن (باصدا) کرد" unsuspend_account: "%{name} حساب کاربر %{target} را از تعلیق خارج کرد" update_custom_emoji: "%{name} شکلک %{target} را بهروز کرد" @@ -239,36 +261,61 @@ fa: expired: منقضیشده title: فیلتر title: دعوتها + report_notes: + created_msg: یادداشت گزارش با موفقیت ساخته شد! + destroyed_msg: یادداشت گزارش با موفقیت حذف شد! reports: + account: + note: یادداشت + report: گزارش action_taken_by: انجامدهنده are_you_sure: آیا مطمئن هستید؟ + assign_to_self: به عهدهٔ من بگذار + assigned: مدیر عهدهدار comment: none: خالی - delete: پاککردن + created_at: گزارششده id: شناسه mark_as_resolved: علامتگذاری به عنوان حلشده - nsfw: - 'false': نمایش پیوستهای تصویری - 'true': نهفتن پیوستهای تصویری + mark_as_unresolved: علامتگذاری به عنوان حلنشده + notes: + create: افزودن یادداشت + create_and_resolve: حل کردن با یادداشت + create_and_unresolve: دوباره گشودن با یادداشت + delete: حذف + placeholder: کارهایی را که در این باره انجام شده، یا هر بهروزرسانی دیگری را بنویسید... + reopen: دوباره به جریان بیندازید report: 'گزارش #%{id}' report_contents: محتوا reported_account: حساب گزارششده reported_by: گزارش از طرف resolved: حلشده + resolved_msg: گزارش با موفقیت حل شد! silence_account: بیصدا کردن حساب status: نوشته suspend_account: معلقکردن حساب target: هدف title: گزارشها + unassign: پسگرفتن مسئولیت unresolved: حلنشده + updated_at: بهروز شد view: نمایش settings: + activity_api_enabled: + desc_html: تعداد بوقهای محلی، کاربران فعال، و کاربران تازه در هر هفته + title: انتشار آمار تجمیعی دربارهٔ فعالیت کاربران bootstrap_timeline_accounts: desc_html: نامهای کاربری را با ویرگول از هم جدا کنید. تنها حسابهای محلی و قفلنشده کار میکنند. اگر اینجا را خالی بگذارید، به طور پیشفرض همهٔ مدیرهای این سرور پیگرفته خواهند شد. title: پیگیریهای پیشفرض برای کاربران تازه contact_information: email: ایمیل کاری username: نام کاربری + hero: + desc_html: در صفحهٔ آغازین نمایش مییابد. دستکم ۶۰۰×۱۰۰ پیکسل توصیه میشود. اگر تعیین نشود، با تصویر بندانگشتی سرور جایگزین خواهد شد + title: تصویر سربرگ + peers_api_enabled: + desc_html: دامینهایی که این سرور به آنها برخورده است + title: انتشار فهرست سرورهای یافتهشده registrations: closed_message: desc_html: وقتی امکان ثبت نام روی سرور فعال نباشد در صفحهٔ اصلی نمایش مییابدمیتوانید HTML بنویسید @@ -282,6 +329,9 @@ fa: open: desc_html: همه بتوانند حساب باز کنند title: امکان ثبت نام + show_known_fediverse_at_about_page: + desc_html: اگر انتخاب شود، بوقهای همهٔ سرورهای دیگر نیز در پیشنمایش این سرور نمایش مییابد. وگرنه فقط بوقهای محلی نشان داده میشوند. + title: نمایش سرورهای دیگر در پیشنمایش این سرور show_staff_badge: desc_html: نمایش علامت همکار روی صفحهٔ کاربر title: نمایش علامت همکار @@ -306,17 +356,14 @@ fa: back_to_account: بازگشت به صفحهٔ حساب batch: delete: پاککردن - nsfw_off: NSFW خاموش - nsfw_on: NSFW روشن - execute: اجرا + nsfw_off: علامتزدن به عنوان غیرحساس + nsfw_on: علامتزدن به عنوان حساس failed_to_execute: اجرا نشد media: - hide: نهفتن رسانه - show: نمایش رسانه title: رسانه - no_media: بدون رسانه + no_media: بدون عکس یا ویدیو title: نوشتههای حساب - with_media: دارای رسانه + with_media: دارای عکس یا ویدیو subscriptions: callback_url: نشانی Callback confirmed: تأییدشده @@ -324,15 +371,19 @@ fa: last_delivery: آخرین ارسال title: WebSub topic: موضوع - title: مدیریت + title: مدیریت سرور admin_mailer: new_report: body: کاربر %{reporter} کاربر %{target} را گزارش داد + body_remote: کسی از %{domain} گزارش %{target} را فرستاده subject: گزارش تازهای برای %{instance} (#%{id}) application_mailer: + notification_preferences: تغییر ترجیحات ایمیل salutation: "%{name}،" settings: 'تغییر تنظیمات ایمیل: %{link}' view: 'نمایش:' + view_profile: دیدن نمایه + view_status: دیدن نوشتهها applications: created: برنامه با موفقیت ساخته شد destroyed: برنامه با موفقیت پاک شد @@ -343,6 +394,8 @@ fa: your_token: کد دسترسی شما auth: agreement_html: پیش از عضو شدن باید قوانین این سرور و شرایط استفادهٔ ما را بپذیرید. + change_password: رمز + confirm_email: تأیید ایمیل delete_account: پاککردن حساب delete_account_html: اگر میخواهید حساب خود را پاک کنید، از اینجا پیش بروید. از شما درخواست تأیید خواهد شد. didnt_get_confirmation: راهنمایی برای تأیید را دریافت نکردید؟ @@ -352,12 +405,19 @@ fa: logout: خروج migrate_account: نقل مکان به یک حساب دیگر migrate_account_html: اگر میخواهید این حساب را به حساب دیگری منتقل کنید، اینجا را کلیک کنید. + or: یا + or_log_in_with: یا ورود به وسیلهٔ + providers: + cas: CAS + saml: SAML register: عضو شوید + register_elsewhere: ثبت نام روی یک سرور دیگر resend_confirmation: راهنمایی برای تأیید را دوباره بفرست reset_password: بازنشانی رمز security: امنیت set_new_password: تعیین رمز تازه authorize_follow: + already_following: شما همین الان هم این حساب را پیمیگیرید error: متأسفانه حین یافتن آن حساب خطایی رخ داد follow: پی بگیرید follow_request: 'شما درخواست پیگیری فرستادهاید به:' @@ -402,6 +462,13 @@ fa: title: این صفحه درست نیست noscript_html: برای استفاده از نسخهٔ تحت وب ماستدون، لطفاً جاوااسکریپت را فعال کنید. یا به جایش میتوانید یک اپ ماستدون را بهکار ببرید. exports: + archive_takeout: + date: تاریخ + download: بایگانی خود را باربگیرید + hint_html: شما میتوانید بایگانی نوشتهها و پروندههای بارگذاریشدهٔ خود را درخواست کنید. دادههای برونبریشده در قالب ActivityPub خواهند بود و همهٔ نرمافزارهای سازگار خواهند توانست آن را بخوانند. شما هر ۷ روز میتوانید یک بار برای چنین بایگانیای درخواست دهید. + in_progress: در حال ساختن بایگانی شما... + request: درخواست بایگانی دادههایتان + size: اندازه blocks: حسابهای مسدودشده csv: CSV follows: حسابهای پیگرفته @@ -443,6 +510,7 @@ fa: '21600': ۶ ساعت '3600': ۱ ساعت '43200': ۱۲ ساعت + '604800': ۱ هفته '86400': ۱ روز expires_in_prompt: هیچ وقت generate: ساختن @@ -470,32 +538,41 @@ fa: proceed: ذخیره updated_msg: تنظیمات نقل مکان حساب شما با موفقیت بهروز شد! moderation: - title: مدیریت + title: مدیریت کاربران notification_mailer: digest: - body: 'خلاصهای از آنچه از زمان آخرین بازدید شما در %{since} روی %{instance} رخ داد :' + action: دیدن همهٔ اعلانها + body: خلاصهای از پیغامهایی که از زمان آخرین بازدید شما در %{since} فرستاده شد mention: "%{name} اینجا از شما نام برد:" new_followers_summary: - one: شما یک پیگیر تازه دارید! ای ول! - other: شما %{count} پیگیر تازه دارید! چه عالی! + one: در ضمن، وقتی که نبودید یک پیگیر تازه پیدا کردید! ای ول! + other: در ضمن، وقتی که نبودید %{count} پیگیر تازه پیدا کردید! چه عالی! subject: one: "یک اعلان تازه از زمان آخرین بازدید شما \U0001F418" other: "%{count} اعلان تازه از زمان آخرین بازدید شما \U0001F418" + title: در مدتی که نبودید... favourite: body: "%{name} این نوشتهٔ شما را پسندید:" subject: "%{name} نوشتهٔ شما را پسندید" + title: پسندیدهشدن تازه follow: body: "%{name} هماینک پیگیر شماست!" subject: "%{name} هماینک پیگیر شماست" + title: پیگیر تازه follow_request: + action: مدیریت درخواستهای پیگیری body: "%{name} میخواهد پیگیر نوشتههای شما باشد" subject: 'منتظر پیگیری: %{name}' + title: درخواست پیگیری تازه mention: + action: پاسخ body: "%{name} در اینجا از شما نام برد:" subject: "%{name} از شما نام برد" + title: نامبردهشدن تازه reblog: body: "%{name} نوشتهٔ شما را بازبوقید:" subject: "%{name} نوشتهٔ شما را بازبوقید" + title: بازبوق تازه number: human: decimal_units: @@ -508,33 +585,25 @@ fa: trillion: T unit: '' pagination: + newer: تازهتر next: بعدی + older: قدیمیتر prev: قبلی truncate: "…" preferences: - languages: زبانها - other: سایر - publishing: انتشار + languages: تنظیمات زبان + other: سایر تنظیمات + publishing: تنظیمات انتشار مطالب web: وب - push_notifications: - favourite: - title: "%{name} نوشتهٔ شما را پسندید" - follow: - title: "%{name} هماینک پیگیر شماست" - group: - title: "%{count} اعلان" - mention: - action_boost: بازبوق - action_expand: نمایش بیشتر - action_favourite: پسندیدن - title: "%{name} از شما نام برد" - reblog: - title: "%{name} نوشتهٔ شما را بازبوقید" remote_follow: acct: نشانی حساب username@domain خود را اینجا بنویسید missing_resource: نشانی اینترنتی برای رسیدن به حساب شما پیدا نشد proceed: درخواست پیگیری prompt: 'شما قرار است این حساب را پیگیری کنید:' + remote_unfollow: + error: خطا + title: عنوان + unfollowed: پایان پیگیری sessions: activity: آخرین کنش browser: مرورگر @@ -543,12 +612,14 @@ fa: blackberry: Blackberry chrome: Chrome edge: Microsoft Edge + electron: Electron firefox: Firefox generic: مرورگر ناشناخته ie: Internet Explorer micro_messenger: MicroMessenger nokia: Nokia S40 Ovi Browser opera: Opera + otter: Otter phantom_js: PhantomJS qq: QQ Browser safari: Safari @@ -578,7 +649,7 @@ fa: authorized_apps: برنامههای مجاز back: بازگشت به ماستدون delete: پاککردن حساب - development: Development + development: فرابری edit_profile: ویرایش نمایه export: برونسپاری دادهها followers: پیگیران مورد تأیید @@ -590,6 +661,19 @@ fa: two_factor_authentication: ورود دومرحلهای your_apps: برنامهٔ شما statuses: + attached: + description: 'پیوستشده: %{attached}' + image: + one: "%{count} تصویر" + other: "%{count} تصویر" + video: + one: "%{count} ویدیو" + other: "%{count} ویدیو" + boosted_from_html: بازبوقیده از طرف %{acct_link} + content_warning: 'هشدا محتوا: %{warning}' + disallowed_hashtags: + one: 'دارای هشتگ غیرمجاز: %{tags}' + other: 'دارای هشتگهای غیرمجاز: %{tags}' open_in_web: بازکردن در وب over_character_limit: از حد مجاز %{max} حرف فراتر رفتید pin_errors: @@ -601,7 +685,7 @@ fa: title: '%{name}: "%{quote}"' visibilities: private: خصوصی - private_long: نمایش تنها به پیگیران + private_long: تنها پیگیران شما میبینند public: عمومی public_long: همه میتوانند ببینند unlisted: فهرستنشده @@ -614,7 +698,9 @@ fa: terms: title: شرایط استفاده و سیاست رازداری %{instance} themes: + contrast: کنتراست بالا default: ماستدون + mastodon-light: ماستدون (روشن) time: formats: default: "%d %b %Y, %H:%M" @@ -631,10 +717,35 @@ fa: manual_instructions: 'اگر نمیتوانید کدها را اسکن کنید و باید آنها را دستی وارد کنید، متن کد امنیتی اینجاست:' recovery_codes: پشتیبانگیری از کدهای بازیابی recovery_codes_regenerated: کدهای بازیابی با موفقیت ساخته شدند - recovery_instructions_html: اگر تلفن خود را گم کردید، میتوانید با یکی از کدهای بازیابی زیر کنترل حساب خود را به دست بگیرید. این کدها را در جای امنی نگه دارید. مثلاً آنها را چاپ کنید و کنار سایر مدارک مهم خود قرار دهید + recovery_instructions_html: اگر تلفن خود را گم کردید، میتوانید با یکی از کدهای بازیابی زیر کنترل حساب خود را به دست بگیرید. این کدها را در جای امنی نگه دارید. مثلاً آنها را چاپ کنید و کنار سایر مدارک مهم خود قرار دهید. setup: راه اندازی wrong_code: کدی که وارد کردید نامعتبر بود! آیا ساعت سرور و ساعت دستگاه شما درست تنظیم شدهاند؟ + user_mailer: + backup_ready: + explanation: شما یک نسخهٔ پشتیبان کامل از حساب خود را درخواست کردید. این پشتیبان الان آمادهٔ بارگیری است! + subject: بایگانی شما آمادهٔ دریافت است + title: گرفتن بایگانی + welcome: + edit_profile_action: تنظیم نمایه + edit_profile_step: 'شما میتوانید نمایهٔ خود را به دلخواه خود تغییر دهید: میتوانید تصویر نمایه، تصویر پسزمینه، نام، و چیزهای دیگری را تعیین کنید. اگر بخواهید، میتوانید حساب خود را خصوصی کنید تا فقط کسانی که شما اجازه میدهید بتوانند پیگیر حساب شما شوند.' + explanation: نکتههایی که برای آغاز کار به شما کمک میکنند + final_action: چیزی منتشر کنید + final_step: 'چیزی بنویسید! حتی اگر الان کسی پیگیر شما نباشد، دیگران نوشتههای عمومی شما را میبینند، مثلاً در فهرست نوشتههای محلی و در هشتگها. شاید بخواهید با هشتگ #آشنایی خودتان را معرفی کنید.' + full_handle: نام کاربری کامل شما + full_handle_hint: این چیزی است که باید به دوستان خود بگویید تا بتوانند به شما پیغام بفرستند یا از سرورهای دیگر پیگیر شما شوند. + review_preferences_action: تغییر ترجیحات + review_preferences_step: با رفتن به صفحهٔ ترجیحات میتوانید چیزهای گوناگونی را تنظیم کنید. مثلاً این که چه ایمیلهای آگاهسازیای به شما فرستاده شود، یا حریم خصوصی پیشفرض نوشتههایتان چه باشد. اگر بیماری سفر (حالت تهوع بر اثر دیدن اجسام متحرک) ندارید، میتوانید پخش خودکار ویدیوها را فعال کنید. + subject: به ماستدون خوش آمدید + tip_bridge_html: اگر پیش از این کاربر توییتر بودید، میتوانید دوستان توییتری خود را که در ماستدون هستند به کمک bridge app پیدا کنید. البته این فقط وقتی کار میکند که آنها هم این اپ را به کار برده باشند! + tip_federated_timeline: "«فهرست نوشتههای همهجا» نمایی از کل شبکهٔ بزرگ ماستدون به شما میدهد. البته این فهرست فقط افردای را نشان میدهد که همسروریهای شما آنها را پیگیری میکنند، و بنابراین ممکن است کامل نباشد." + tip_following: شما به طور پیشفرض مدیر(های) سرور خود را پی میگیرید. برای یافتن افراد جالب دیگر، فهرست «نوشتههای محلی» و «نوشتههای همهجا» را ببینید. + tip_local_timeline: فهرست نوشتههای محلی نمایی کلی از کاربران روی %{instance} را ارائه میدهد. اینها همسایههای شما هستند! + tip_mobile_webapp: اگر مرورگر موبایل شما امکان گذاشتن ماستدون روی صفحهٔ اصلی موبایل را به شما میدهد، این یعنی میتوانید اعلانهای خودکار ماستدون را دریافت کنید. با این کار ماستدون خیلی شبیه یک اپ معمولی موبایل میشود! + tips: نکتهها + title: خوش آمدید، کاربر %{name}! users: invalid_email: نشانی ایمیل نامعتبر است invalid_otp_token: کد ورود دومرحلهای نامعتبر است + otp_lost_help_html: اگر شما دسترسی به هیچکدامشان ندارید، باید با ایمیل %{email} تماس بگیرید + seamless_external_login: شما با یک سرویس خارج از مجموعه وارد شدهاید، به همین دلیل تنظیمات ایمیل و رمز برای شما در دسترس نیست. signed_in_as: 'واردشده به نام:' diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 550ad1805e..1e02efbd24 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -4,6 +4,7 @@ fi: about_hashtag_html: Nämä ovat hashtagilla #%{hashtag} merkittyjä julkisia tuuttauksia. Voit vastata niihin, jos sinulla on tili jossain päin fediversumia. about_mastodon_html: Mastodon on sosiaalinen verkosto. Se on toteutettu avoimilla verkkoprotokollilla ja vapailla, avoimen lähdekoodin ohjelmistoilla, ja se toimii hajautetusti samaan tapaan kuin sähköposti. about_this: Tietoja tästä palvelimesta + administered_by: 'Ylläpitäjä:' closed_registrations: Tähän instanssiin ei voi tällä hetkellä rekisteröityä. Voit kuitenkin luoda tilin johonkin toiseen instanssiin ja käyttää samaa verkostoa sitä kautta. contact: Ota yhteyttä contact_missing: Ei asetettu @@ -52,32 +53,40 @@ fi: unfollow: Lopeta seuraaminen admin: account_moderation_notes: - account: Moderaattori create: Luo - created_at: Päiväys created_msg: Moderointimerkinnän luonti onnistui! delete: Poista destroyed_msg: Moderointimerkinnän poisto onnistui! accounts: are_you_sure: Oletko varma? - by_domain: Verkko-osoite + avatar: Profiilikuva + by_domain: Verkkotunnus + change_email: + changed_msg: Tilin sähköposti vaihdettu onnistuneesti! + current_email: Nykyinen sähköposti + label: Vaihda sähköposti + new_email: Uusi sähköposti + submit: Vaihda sähköposti + title: Vaihda sähköposti käyttäjälle %{username} confirm: Vahvista confirmed: Vahvistettu + confirming: Vahvistetaan demote: Alenna disable: Poista käytöstä disable_two_factor_authentication: Poista 2FA käytöstä disabled: Poistettu käytöstä - display_name: Näyttönimi - domain: Verkko-osoite + display_name: Nimimerkki + domain: Verkkotunnus edit: Muokkaa email: Sähköposti + email_status: Sähköpostin tila enable: Ota käyttöön enabled: Käytössä - feed_url: Syötteen URL + feed_url: Syötteen osoite followers: Seuraajat - followers_url: Seuraajien URL + followers_url: Seuraajien osoite follows: Seuraa - inbox_url: Saapuvan postilaatikon URL + inbox_url: Saapuvan postilaatikon osoite ip: IP location: all: Kaikki @@ -100,14 +109,19 @@ fi: alphabetic: Aakkosjärjestys most_recent: Uusin title: Järjestys - outbox_url: Lähtevän postilaatikon URL + outbox_url: Lähtevän postilaatikon osoite perform_full_suspension: Siirrä kokonaan jäähylle - profile_url: Profiilin URL + profile_url: Profiilin osoite promote: Ylennä protocol: Protokolla public: Julkinen push_subscription_expires: PuSH-tilaus vanhenee redownload: Päivitä profiilikuva + remove_avatar: Poista profiilikuva + resend_confirmation: + already_confirmed: Tämä käyttäjä on jo vahvistettu + send: Lähetä varmistusviesti uudelleen + success: Vahvistusviesti onnistuneesti lähetetty! reset: Palauta reset_password: Palauta salasana resubscribe: Tilaa uudelleen @@ -118,16 +132,17 @@ fi: staff: Henkilöstö user: Käyttäjä salmon_url: Salmon-URL - search: Haku - shared_inbox_url: Jaetun saapuvan postilaatikon URL + search: Hae + shared_inbox_url: Jaetun saapuvan postilaatikon osoite show: - created_reports: Tilin luomat raportit + created_reports: Tämän tilin luomat raportit report: raportti targeted_reports: Tästä tilistä tehdyt raportit silence: Hiljennä statuses: Tilat subscribe: Tilaa title: Tilit + unconfirmed_email: Sähköpostia ei vahvistettu undo_silenced: Peru hiljennys undo_suspension: Peru jäähy unsubscribe: Lopeta tilaus @@ -135,6 +150,8 @@ fi: web: Web action_logs: actions: + assigned_to_self_report: "%{name} otti raportin %{target} tehtäväkseen" + change_email_user: "%{name} vaihtoi käyttäjän %{target} sähköpostiosoitteen" confirm_user: "%{name} vahvisti käyttäjän %{target} sähköpostiosoitteen" create_custom_emoji: "%{name} lähetti uuden emojin %{target}" create_domain_block: "%{name} esti verkkotunnuksen %{target}" @@ -150,6 +167,8 @@ fi: enable_user: "%{name} salli sisäänkirjautumisen käyttäjälle %{target}" memorialize_account: "%{name} muutti käyttäjän %{target} tilin muistosivuksi" promote_user: "%{name} ylensi käyttäjän %{target}" + remove_avatar_user: "%{name} poisti käyttäjän %{target} profiilikuvan" + reopen_report: "%{name} avasi uudelleen raportin %{target}" reset_password_user: "%{name} palautti käyttäjän %{target} salasanan" resolve_report: "%{name} hylkäsi raportin %{target}" silence_account: "%{name} hiljensi käyttäjän %{target}" @@ -168,7 +187,7 @@ fi: delete: Poista destroyed_msg: Emojon poisto onnistui! disable: Poista käytöstä - disabled_msg: Emojin käytöstäpoisto onnistui + disabled_msg: Emojin poisto käytöstä onnistui emoji: Emoji enable: Ota käyttöön enabled_msg: Emojin käyttöönotto onnistui @@ -239,28 +258,42 @@ fi: expired: Vanhentunut title: Suodata title: Kutsut + report_notes: + created_msg: Muistiinpano onnistuneesti lisätty raporttiin! + destroyed_msg: Muistiinpano onnistuneesti poistettu raportista! reports: + account: + note: muistiinpano + report: raportti action_taken_by: Toimenpiteen tekijä are_you_sure: Oletko varma? + assign_to_self: Ota tehtäväksi comment: none: Ei mitään - delete: Poista + created_at: Raportoitu id: Tunniste mark_as_resolved: Merkitse ratkaistuksi - nsfw: - 'false': Peru medialiitteiden piilotus - 'true': Piilota medialiitteet + mark_as_unresolved: Merkitse ratkaisemattomaksi + notes: + create: Lisää muistiinpano + create_and_resolve: Ratkaise ja lisää muistiinpano + create_and_unresolve: Avaa uudelleen ja lisää muistiinpano + delete: Poista + placeholder: Kuvaile mitä toimia on tehty tai muita päivityksiä tähän raporttiin… + reopen: Avaa raportti uudestaan report: Raportti nro %{id} report_contents: Sisältö reported_account: Raportoitu tili reported_by: Raportoija resolved: Ratkaistut + resolved_msg: Raportti onnistuneesti ratkaistu! silence_account: Hiljennä tili status: Tila suspend_account: Siirrä tili jäähylle target: Kohde title: Raportit unresolved: Ratkaisemattomat + updated_at: Päivitetty view: Näytä settings: activity_api_enabled: @@ -320,11 +353,8 @@ fi: delete: Poista nsfw_off: NSFW POIS nsfw_on: NSFW PÄÄLLÄ - execute: Suorita failed_to_execute: Suoritus epäonnistui media: - hide: Piilota media - show: Näytä media title: Media no_media: Ei mediaa title: Tilin tilat @@ -340,6 +370,7 @@ fi: admin_mailer: new_report: body: "%{reporter} on raportoinut kohteen %{target}" + body_remote: Joku osoitteesta %{domain} on raportoinut kohteen %{target} subject: Uusi raportti instanssista %{instance} (nro %{id}) application_mailer: notification_preferences: Muuta sähköpostiasetuksia @@ -381,6 +412,7 @@ fi: security: Tunnukset set_new_password: Aseta uusi salasana authorize_follow: + already_following: Sinä seuraat jo tätä tiliä error: Valitettavasti etätilin haussa tapahtui virhe follow: Seuraa follow_request: 'Olet lähettänyt seuraamispyynnön käyttäjälle:' @@ -473,6 +505,7 @@ fi: '21600': 6 tuntia '3600': 1 tunti '43200': 12 tuntia + '604800': 1 viikko '86400': 1 vuorokausi expires_in_prompt: Ei koskaan generate: Luo @@ -557,25 +590,13 @@ fi: other: Muut publishing: Julkaiseminen web: Web - push_notifications: - favourite: - title: "%{name} tykkäsi tilastasi" - follow: - title: "%{name} seuraa nyt sinua" - group: - title: "%{count} ilmoitusta" - mention: - action_boost: Buustaa - action_expand: Näytä lisää - action_favourite: Tykkää - title: "%{nimi} mainitsi sinut" - reblog: - title: "%{name} buustasi tilaasi" remote_follow: acct: Syötä se käyttäjätunnus@verkkotunnus, josta haluat seurata missing_resource: Vaadittavaa uudelleenohjaus-URL:ää tiliisi ei löytynyt proceed: Siirry seuraamaan prompt: 'Olet aikeissa seurata:' + remote_unfollow: + error: Virhe sessions: activity: Viimeisin toiminta browser: Selain @@ -642,6 +663,9 @@ fi: one: "%{count} video" other: "%{count} videota" content_warning: 'Sisältövaroitus: %{warning}' + disallowed_hashtags: + one: 'sisälsi aihetunnisteen jota ei sallita: %{tags}' + other: 'sisälsi aihetunnisteet joita ei sallita: %{tags}' open_in_web: Avaa selaimessa over_character_limit: merkkimäärän rajoitus %{max} ylitetty pin_errors: @@ -712,5 +736,6 @@ fi: users: invalid_email: Virheellinen sähköpostiosoite invalid_otp_token: Virheellinen kaksivaiheisen todentamisen koodi + otp_lost_help_html: Jos sinulla ei ole pääsyä kumpaankaan, voit ottaa yhteyttä osoitteeseen %{email} seamless_external_login: Olet kirjautunut ulkoisen palvelun kautta, joten salasana- ja sähköpostiasetukset eivät ole käytettävissä. signed_in_as: 'Kirjautunut henkilönä:' diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 0579123dcb..b3914ea2b1 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -40,6 +40,7 @@ fr: following: Abonnements media: Médias moved_html: "%{name} a changé de compte pour %{new_profile_link} :" + network_hidden: Cette information n'est pas disponible nothing_here: Rien à voir ici ! people_followed_by: Personnes suivies par %{name} people_who_follow: Personnes qui suivent %{name} @@ -49,13 +50,12 @@ fr: reserved_username: Ce nom d’utilisateur⋅ice est réservé roles: admin: Admin + bot: Robot moderator: Modérateur·trice unfollow: Ne plus suivre admin: account_moderation_notes: - account: Modérateur·ice - create: Créer - created_at: Date + create: Créer une note created_msg: Note de modération créée avec succès ! delete: Supprimer destroyed_msg: Note de modération supprimée avec succès ! @@ -72,6 +72,7 @@ fr: title: Modifier le courriel pour %{username} confirm: Confirmer confirmed: Confirmé + confirming: Confirmation demote: Rétrograder disable: Désactiver disable_two_factor_authentication: Désactiver l’authentification à deux facteurs @@ -80,6 +81,7 @@ fr: domain: Domaine edit: Éditer email: Courriel + email_status: État de la messagerie enable: Activer enabled: Activé feed_url: URL du flux @@ -118,6 +120,10 @@ fr: push_subscription_expires: Expiration de l’abonnement PuSH redownload: Rafraîchir les avatars remove_avatar: Supprimer l'avatar + resend_confirmation: + already_confirmed: Cet utilisateur est déjà confirmé + send: Renvoyer un courriel de confirmation + success: Email de confirmation envoyé avec succès ! reset: Réinitialiser reset_password: Réinitialiser le mot de passe resubscribe: Se réabonner @@ -269,7 +275,6 @@ fr: comment: none: Aucun created_at: Signalé - delete: Supprimer id: ID mark_as_resolved: Marquer comme résolu mark_as_unresolved: Marquer comme non-résolu @@ -278,10 +283,7 @@ fr: create_and_resolve: Résoudre avec une note create_and_unresolve: Ré-ouvrir avec une note delete: Effacer - placeholder: Décrivez quelles actions ont été prises, ou toute autre mise à jour de ce signalement… - nsfw: - 'false': Ré-afficher les médias - 'true': Masquer les médias + placeholder: Décrivez quelles actions ont été prises, ou toute autre mise à jour… reopen: Ré-ouvrir le signalement report: 'Signalement #%{id}' report_contents: Contenu @@ -356,11 +358,8 @@ fr: delete: Supprimer nsfw_off: Marquer comme non-sensible nsfw_on: Marquer comme sensible - execute: Exécuter failed_to_execute: Erreur d’exécution media: - hide: Masquer les médias - show: Montrer les médias title: Médias no_media: Aucun média title: État du compte @@ -376,6 +375,7 @@ fr: admin_mailer: new_report: body: "%{reporter} a signalé %{target}" + body_remote: Quelqu'un de %{domain} a signalé %{target} subject: Nouveau signalement sur %{instance} (#%{id}) application_mailer: notification_preferences: Modifier les préférences de courriel @@ -465,7 +465,7 @@ fr: archive_takeout: date: Date download: Télécharger votre archive - hint_html: Vous pouvez demander une archive de vos pouets et médias téléversés. Les données exportées seront au format ActivityPub, lisible par tout logiciel compatible. + hint_html: Vous pouvez demander une archive de vos pouets et médias téléversés. Les données exportées seront au format ActivityPub, lisible par tout logiciel compatible. Vous pouvez demander une archive tous les 7 jours. in_progress: Élaboration de votre archive.... request: Demandez vos archives size: Taille @@ -550,7 +550,7 @@ fr: subject: one: "Une nouvelle notification depuis votre dernière visite \U0001F418" other: "%{count} nouvelles notifications depuis votre dernière visite \U0001F418" - title: Pendant votre absence… + title: Pendant votre absence... favourite: body: "%{name} a ajouté votre post à ses favoris :" subject: "%{name} a ajouté votre post à ses favoris" @@ -595,20 +595,6 @@ fr: other: Autre publishing: Publication web: Web - push_notifications: - favourite: - title: "%{name} a mis votre statut en favori" - follow: - title: "%{name} vous suit" - group: - title: "%{count} notifications" - mention: - action_boost: Partager - action_expand: Montrer plus - action_favourite: Ajouter aux favoris - title: "%{name} vous a mentionné·e" - reblog: - title: "%{name} a partagé votre statut" remote_follow: acct: Entrez votre pseudo@instance depuis lequel vous voulez suivre cet·te utilisateur⋅ice missing_resource: L’URL de redirection n’a pas pu être trouvée @@ -633,7 +619,7 @@ fr: micro_messenger: MicroMessenger nokia: Nokia S40 Ovi Browser opera: Opera - otter: Autre + otter: Otter phantom_js: PhantomJS qq: QQ Browser safari: Safari @@ -683,6 +669,7 @@ fr: video: one: "%{count} vidéo" other: "%{count} vidéos" + boosted_from_html: Repartagé depuis %{acct_link} content_warning: 'Attention au contenu : %{warning}' disallowed_hashtags: one: 'contient un hashtag désactivé : %{tags}' @@ -710,6 +697,10 @@ fr: sensitive_content: Contenu sensible terms: title: "%{instance} Conditions d’utilisations et politique de confidentialité" + themes: + contrast: Contraste élevé + default: Mastodon + mastodon-light: Mastodon (clair) time: formats: default: "%d %b %Y, %H:%M" @@ -749,11 +740,12 @@ fr: tip_federated_timeline: La chronologie fédérée est une vue en direct du réseau Mastodon. Mais elle n'inclut que les personnes auxquelles vos voisin·es sont abonné·es, donc elle n'est pas complète. tip_following: Vous suivez les administrateurs et administratrices de votre serveur par défaut. Pour trouver d'autres personnes intéressantes, consultez les chronologies locales et fédérées. tip_local_timeline: La chronologie locale est une vue des personnes sur %{instance}. Ce sont vos voisines et voisins immédiats ! - tip_mobile_webapp: Si votre navigateur mobile vous propose d'ajouter Mastodon à votre écran d'accueil, vous pouvez recevoir des notifications push. Il agit comme une application native de bien des façons ! + tip_mobile_webapp: Si votre navigateur mobile vous propose d'ajouter Mastodon à votre écran d'accueil, vous pouvez recevoir des notifications. Il agit comme une application native de bien des façons ! tips: Astuces title: Bienvenue à bord, %{name} ! users: invalid_email: L’adresse courriel est invalide invalid_otp_token: Le code d’authentification à deux facteurs est invalide + otp_lost_help_html: Si vous perdez accès aux deux, vous pouvez contacter %{email} seamless_external_login: Vous êtes connecté via un service externe, donc les paramètres concernant le mot de passe et le courriel ne sont pas disponibles. signed_in_as: 'Connecté·e en tant que :' diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 093fa70fe8..82636618aa 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -19,8 +19,8 @@ gl: humane_approach_body: Aprendendo dos erros de outras redes, Mastodon intenta tomar decisións éticas de deseño para loitar contra os usos incorrectos da rede. humane_approach_title: Unha aproximación máis humana not_a_product_body: Mastodon non é unha rede comercial. Sen anuncios, sen minería de datos, sen xardíns privados. Non hai autoridade centralizada. - not_a_product_title: Vostede é unha persona, non un producto - real_conversation_body: Con 500 caracteres a súa disposición e soporte para contido ao por menor e avisos sobre o contido, pode expresarse vostede do xeito que queira. + not_a_product_title: Vostede é unha persoa, non un producto + real_conversation_body: Con 500 caracteres a súa disposición, soporte para contido polo miúdo e avisos sobre o contido, pode expresarse vostede con libertade. real_conversation_title: Construído para conversacións reais within_reach_body: Existen múltiples aplicativos para iOS, Android e outras plataformas grazas a un entorno API amigable para o desenvolvedor que lle permite estar ao tanto cos seus amigos en calquer lugar. within_reach_title: Sempre en contacto @@ -30,9 +30,9 @@ gl: other_instances: Listado de instancias source_code: Código fonte status_count_after: estados - status_count_before: Quen escribeu + status_count_before: Que publicaron user_count_after: usuarias - user_count_before: Inicio de + user_count_before: Fogar de what_is_mastodon: Qué é Mastodon? accounts: follow: Seguir @@ -40,6 +40,7 @@ gl: following: Seguindo media: Medios moved_html: "%{name} mudouse a %{new_profile_link}:" + network_hidden: A información non está dispoñible nothing_here: Nada por aquí! people_followed_by: Personas que segue %{name} people_who_follow: Personas que seguen a %{name} @@ -49,13 +50,12 @@ gl: reserved_username: O nome de usuaria está reservado roles: admin: Admin + bot: Bot moderator: Mod unfollow: Deixar de seguir admin: account_moderation_notes: - account: Moderador - create: Crear - created_at: Data + create: Deixar nota created_msg: Nota a moderación creada con éxito! delete: Eliminar destroyed_msg: Nota a moderación destruída con éxito! @@ -72,6 +72,7 @@ gl: title: Cambiar o correo-e de %{username} confirm: Confirmar confirmed: Confirmado + confirming: Confirmar demote: Degradar disable: Deshabilitar disable_two_factor_authentication: Deshabilitar 2FA @@ -80,6 +81,7 @@ gl: domain: Dominio edit: Editar email: E-mail + email_status: Estado del correo electrónico enable: Habilitar enabled: Habilitado feed_url: URL fonte @@ -118,6 +120,10 @@ gl: push_subscription_expires: A suscrición PuSH caduca redownload: Actualizar avatar remove_avatar: Eliminar avatar + resend_confirmation: + already_confirmed: Este usuario ya está confirmado + send: Reenviar el correo electrónico de confirmación + success: "¡Correo electrónico de confirmación enviado con éxito!" reset: Restablecer reset_password: Restablecer contrasinal resubscribe: Voltar a suscribir @@ -269,7 +275,6 @@ gl: comment: none: Nada created_at: Reportado - delete: Eliminar id: ID mark_as_resolved: Marcar como resolto mark_as_unresolved: Marcar como non resolto @@ -278,10 +283,7 @@ gl: create_and_resolve: Resolver con nota create_and_unresolve: Voltar a abrir con nota delete: Eliminar - placeholder: Describir qué decisións foron tomadas, ou calquer actualización a este informe… - nsfw: - 'false': Non agochar anexos de medios - 'true': Agochar anexos de medios + placeholder: Describe qué medidas foron tomadas, ou calquer outra información relacionada... reopen: Voltar a abrir o informe report: 'Informe #%{id}' report_contents: Contidos @@ -356,11 +358,8 @@ gl: delete: Eliminar nsfw_off: Marcar como non sensible nsfw_on: Marcar como sensible - execute: Executar failed_to_execute: Fallou a execución media: - hide: Agochar medios - show: Mostar medios title: Medios no_media: Sen medios title: Estados da conta @@ -376,6 +375,7 @@ gl: admin_mailer: new_report: body: "%{reporter} informou sobre %{target}" + body_remote: Alguén desde %{domain} informou sobre %{target} subject: Novo informe sobre %{instance} (#%{id}) application_mailer: notification_preferences: Cambiar os axustes de correo-e @@ -465,7 +465,7 @@ gl: archive_takeout: date: Data download: Descargue o seu ficheiro - hint_html: Pode solicitar un ficheiro cos seus toots ficheiros de medios. Os datos estarán en formato ActivityPub e son compatibles con calquer software que o cumpla. + hint_html: Pode solicitar un ficheiro cos seus toots ficheiros de medios. Os datos estarán en formato ActivityPub e son compatibles con calquer software que o cumpla. Pode solicitar un ficheiro cada 7 días. in_progress: Xerando o seu ficheiro... request: Solicite o ficheiro size: Tamaño @@ -550,7 +550,7 @@ gl: subject: one: "1 nova notificación desde a súa última visita \U0001F418" other: "%{count} novas notificacións desde a súa última visita \U0001F418" - title: Na súa ausencia… + title: Na súa ausencia... favourite: body: 'O seu estado foi marcado favorito por %{name}:' subject: "%{name} marcou favorito o seu estado" @@ -595,20 +595,6 @@ gl: other: Outro publishing: Publicando web: Web - push_notifications: - favourite: - title: "%{name} marcou favorito o seu estado" - follow: - title: "%{name} agora está a seguila" - group: - title: "%{count} notificacións" - mention: - action_boost: Promover - action_expand: Mostar máis - action_favourite: Favorito - title: "%{name} mencionouna" - reblog: - title: "%{name} promoveu un dos seus estados" remote_follow: acct: Introduza o seu nomedeusuaria@dominio desde onde quere facer seguimento missing_resource: Non se puido atopar o URL de redirecionamento requerido para a súa conta @@ -683,6 +669,7 @@ gl: video: one: "%{count} vídeo" other: "%{count} vídeos" + boosted_from_html: Promovida desde %{acct_link} content_warning: 'Aviso sobre o contido: %{warning}' disallowed_hashtags: one: 'contiña unha etiqueta non permitida: %{tags}' @@ -789,6 +776,7 @@ gl: title: "%{instance} Termos do Servizo e Política de Intimidade" themes: default: Mastodon + mastodon-light: Mastodon (claro) time: formats: default: "%d %b, %Y, %H:%M" @@ -834,5 +822,6 @@ gl: users: invalid_email: O enderezo de correo non é válido invalid_otp_token: Código de doble-factor non válido + otp_lost_help_html: Si perde o acceso a ambos, pode contactar con %{email} seamless_external_login: Está conectado a través de un servizo externo, polo que os axustes de contrasinal e correo-e non están dispoñibles. signed_in_as: 'Rexistrada como:' diff --git a/config/locales/he.yml b/config/locales/he.yml index d641c6e1a5..c127db3852 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -52,9 +52,7 @@ he: unfollow: הפסקת מעקב admin: account_moderation_notes: - account: מנחה דיון create: ליצור - created_at: תאריך created_msg: הודעת מנחה נוצרה בהצלחה! delete: למחוק destroyed_msg: הודעת מנחה נמחקה בהצלחה! @@ -63,6 +61,7 @@ he: by_domain: שם מתחם confirm: אישור confirmed: אושר + confirming: המאשר demote: הורדה בדרגה disable: לחסום disable_two_factor_authentication: ביטול הזדהות דו-שלבית @@ -71,6 +70,7 @@ he: domain: תחום edit: עריכה email: דוא"ל + email_status: סטטוס דוא"ל enable: לאפשר enabled: מאופשר feed_url: כתובת פיד @@ -108,6 +108,10 @@ he: public: פומבי push_subscription_expires: הרשמה להודעות בדחיפה פגה redownload: לקריאה מחדש של האווטאר + resend_confirmation: + already_confirmed: משתמש זה כבר אושר + send: שלח מחדש דוא"ל אימות + success: הודעת האימייל נשלחה בהצלחה! reset: איפוס reset_password: אתחול סיסמא resubscribe: להרשם מחדש @@ -181,12 +185,8 @@ he: are_you_sure: 100% על בטוח? comment: none: ללא - delete: מחיקה id: ID mark_as_resolved: סימון כפתור - nsfw: - 'false': לכל המשפחה - 'true': תוכן רגיש report: 'דווח על #%{id}' report_contents: תוכן reported_account: חשבון מדווח diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 7fe431d377..41093aa434 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -52,9 +52,7 @@ hu: unfollow: Követés abbahagyása admin: account_moderation_notes: - account: Moderátor create: Új bejegyzés - created_at: Dátum created_msg: Moderációs bejegyzés létrehozva! delete: Törlés destroyed_msg: Moderációs bejegyzés törölve! @@ -63,6 +61,7 @@ hu: by_domain: Domain confirm: Megerősítés confirmed: Megerősítve + confirming: Megerősítve demote: Lefokozás disable: Kikapcsolás disable_two_factor_authentication: Kétlépcsős azonosítás kikapcsolása @@ -71,6 +70,7 @@ hu: domain: Domain edit: Szerkesztés email: E-mail + email_status: E-mail állapot enable: Engedélyezés enabled: Engedélyezve feed_url: Hírcsatorna URL @@ -108,6 +108,10 @@ hu: public: Nyilvános push_subscription_expires: A PuSH feliratkozás elévül redownload: Profilkép frissítése + resend_confirmation: + already_confirmed: Ezt a felhasználót már megerősítették + send: Küldd újra a megerősítő email-t + success: A megerősítő e-mail sikeresen elküldve! reset: Visszaállítás reset_password: Jelszó visszaállítása resubscribe: Feliratkozás ismét @@ -244,12 +248,8 @@ hu: are_you_sure: Biztos vagy benne? comment: none: Egyik sem - delete: Törlés id: ID mark_as_resolved: Megjelölés megoldottként - nsfw: - 'false': Média-csatolmányok rejtésének feloldása - 'true': Média-csatolmányok elrejtése report: "#%{id} számú jelentés" report_contents: Tartalom reported_account: Bejelentett fiók @@ -314,11 +314,8 @@ hu: delete: Törlés nsfw_off: Szenzitív tartalom kikapcsolva nsfw_on: Szenzitív tartalom bekapcsolva - execute: Végrehajt failed_to_execute: Végrehajtás sikertelen media: - hide: Média elrejtése - show: Média megjelenítése title: Média no_media: Nem található médiafájl title: Felhasználó tülkjei @@ -534,20 +531,6 @@ hu: other: Egyéb publishing: Közzététel web: Web - push_notifications: - favourite: - title: "%{name} a kedvenceihez adta a tülköd" - follow: - title: "%{name} mostantól követ téged" - group: - title: "%{count} értesítés" - mention: - action_boost: Reblog - action_expand: Mutass többet - action_favourite: Kedvencekhez adás - title: "%{name} megemlített téged" - reblog: - title: "%{name} reblogolta a tülköd" remote_follow: acct: Írd be a felhasználódat, amelyről követni szeretnéd felhasznalonev@domain formátumban missing_resource: A fiókodnál nem található a szükséges átirányítási URL diff --git a/config/locales/id.yml b/config/locales/id.yml index 5a63b8038a..4fb75f2b0e 100644 --- a/config/locales/id.yml +++ b/config/locales/id.yml @@ -29,10 +29,12 @@ id: are_you_sure: Anda yakin? confirm: Konfirmasi confirmed: Dikonfirmasi + confirming: Mengkonfirmasi display_name: Nama domain: Domain edit: Ubah email: E-mail + email_status: Status Email feed_url: URL Feed followers: Pengikut follows: Mengikut @@ -58,6 +60,10 @@ id: profile_url: URL profil public: Publik push_subscription_expires: Langganan PuSH telah kadaluarsa + resend_confirmation: + already_confirmed: Pengguna ini sudah dikonfirmasi + send: Kirim ulang email konfirmasi + success: Email konfirmasi berhasil dikirim! reset_password: Reset kata sandi salmon_url: URL Salmon show: @@ -107,7 +113,6 @@ id: reports: comment: none: Tidak ada - delete: Hapus id: ID mark_as_resolved: Tandai telah diseleseikan report: 'Laporkan #%{id}' diff --git a/config/locales/io.yml b/config/locales/io.yml index 7c25acc47a..bf15de4886 100644 --- a/config/locales/io.yml +++ b/config/locales/io.yml @@ -106,7 +106,6 @@ io: reports: comment: none: None - delete: Delete id: ID mark_as_resolved: Mark as resolved report: 'Report #%{id}' diff --git a/config/locales/it.yml b/config/locales/it.yml index 0518d20e61..5608c0574c 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -1,6 +1,7 @@ --- it: about: + about_hashtag_html: Questi sono i toot pubblici etichettati con #%{hashtag}. Puoi interagire con loro se hai un account nel fediverse. about_mastodon_html: Mastodon è un social network gratuito e open-source. Un'alternativa decentralizzata alle piattaforme commerciali che evita che una singola compagnia monopolizzi il tuo modo di comunicare. Scegli un server di cui ti fidi — qualunque sia la tua scelta, potrai interagire con chiunque altro. Chiunque può sviluppare un suo server Mastodon e partecipare alla vita del social network. about_this: A proposito di questo server administered_by: 'Amministrato da:' @@ -11,6 +12,9 @@ it: description_headline: Cos'è %{domain}? domain_count_after: altri server domain_count_before: Connesso a + extended_description_html: | + Un buon posto per le regole + La descrizione estesa non è ancora stata preparata. features: humane_approach_body: Imparando dai fallimenti degli altri networks, Mastodon mira a fare scelte di design etico per combattere l'abuso dei social media. humane_approach_title: Un approccio più umano @@ -33,9 +37,10 @@ it: accounts: follow: Segui followers: Seguaci - following: Seguiti + following: Segui media: Media moved_html: "%{name} è stato spostato su %{new_profile_link}:" + network_hidden: Questa informazione non e' disponibile nothing_here: Qui non c'è nulla! people_followed_by: Persone seguite da %{name} people_who_follow: Persone che seguono %{name} @@ -45,13 +50,12 @@ it: reserved_username: Il nome utente è riservato roles: admin: Amministratore - moderator: Mod + bot: Bot + moderator: Moderatore unfollow: Non seguire più admin: account_moderation_notes: - account: Moderatore - create: Crea - created_at: Data + create: Lascia nota created_msg: Nota di moderazione creata con successo! delete: Elimina destroyed_msg: Nota di moderazione distrutta con successo! @@ -68,6 +72,7 @@ it: title: Cambia email per %{username} confirm: Conferma confirmed: Confermato + confirming: Confermando demote: Declassa disable: Disabilita disable_two_factor_authentication: Disabilita 2FA @@ -76,12 +81,13 @@ it: domain: Dominio edit: Modifica email: Email + email_status: Stato email enable: Abilita enabled: Abilitato feed_url: URL Feed followers: Follower followers_url: URL follower - follows: Follows + follows: Segue inbox_url: URL inbox ip: IP location: @@ -111,8 +117,13 @@ it: promote: Promuovi protocol: Protocollo public: Pubblico + push_subscription_expires: Sottoscrizione PuSH scaduta redownload: Aggiorna avatar remove_avatar: Rimuovi avatar + resend_confirmation: + already_confirmed: Questo utente è già confermato + send: Reinvia email di conferma + success: Email di conferma inviata con successo! reset: Reimposta reset_password: Reimposta password resubscribe: Riscriversi @@ -120,9 +131,15 @@ it: roles: admin: Amministratore moderator: Moderatore - staff: Staff + staff: Personale user: Utente + salmon_url: URL Salmone search: Cerca + shared_inbox_url: URL Inbox Condiviso + show: + created_reports: Rapporti creati da questo account + report: segnala + targeted_reports: Rapporti che riguardano questo account silence: Silenzia statuses: Stati subscribe: Sottoscrivi @@ -135,10 +152,27 @@ it: web: Web action_logs: actions: - change_email_user: "%{name} ha cambiato l'indirizzo e-mail per l'utente %{target}" + assigned_to_self_report: "%{name} ha assegnato il rapporto %{target} a se stesso" + change_email_user: "%{name} ha cambiato l'indirizzo email per l'utente %{target}" confirm_user: "%{name} ha confermato l'indirizzo email per l'utente %{target}" create_custom_emoji: "%{name} ha caricato un nuovo emoji %{target}" create_domain_block: "%{name} ha bloccato il dominio %{target}" + destroy_domain_block: "%{name} ha sbloccato il dominio %{target}" + destroy_status: "%{name} ha eliminato lo status di %{target}" + disable_2fa_user: "%{name} ha disabilitato l'obbligo dei due fattori per l'utente %{target}" + disable_custom_emoji: "%{name} ha disabilitato l'emoji %{target}" + disable_user: "%{name} ha disabilitato il login per l'utente %{target}" + enable_custom_emoji: "%{name} ha abilitato l'emoji %{target}" + enable_user: "%{name} ha abilitato il login per l'utente %{target}" + remove_avatar_user: "%{name} ha eliminato l'avatar di %{target}" + reopen_report: "%{name} ha riaperto il rapporto %{target}" + reset_password_user: "%{name} ha reimpostato la password dell'utente %{target}" + resolve_report: "%{name} ha risolto il rapporto %{target}" + silence_account: "%{name} ha silenziato l'account di %{target}" + suspend_account: "%{name} ha sospeso l'account di %{target}" + unsilence_account: "%{name} ha de-silenziato l'account di %{target}" + unsuspend_account: "%{name} ha annullato la sospensione dell'account di %{target}" + update_custom_emoji: "%{name} ha aggiornato l'emoji %{target}" custom_emojis: by_domain: Dominio copied_msg: Creata con successo una copia locale dell'emoji @@ -152,12 +186,13 @@ it: emoji: Emoji enable: Abilita enabled_msg: Questa emoji è stata abilitata con successo - image_hint: PNG fino a 50KB + image_hint: PNG fino a 50 KB listed: Elencato new: title: Aggiungi nuovo emoji personalizzato overwrite: Sovrascrivi shortcode: Shortcode + shortcode_hint: Almeno due caratteri, solo caratteri alfanumerici e trattino basso title: Emoji personalizzate unlisted: Non elencato update_failed_msg: Impossibile aggiornare questa emojii @@ -181,14 +216,21 @@ it: suspend: Sospendi severity: Severità show: + affected_accounts: + one: Interessato un solo account nel database + other: Interessati %{count} account nel database + retroactive: + silence: De-silenzia tutti gli account esistenti da questo dominio + suspend: Annulla la sospensione di tutti gli account esistenti da questo dominio + title: Annulla il blocco del dominio per %{domain} undo: Annulla title: Blocchi dominio undo: Annulla email_domain_blocks: add_new: Aggiungi nuovo - created_msg: Dominio e-mail aggiunto con successo alla lista nera + created_msg: Dominio email aggiunto con successo alla lista nera delete: Elimina - destroyed_msg: Dominio e-mail cancellato con successo dalla lista nera + destroyed_msg: Dominio email cancellato con successo dalla lista nera domain: Dominio new: create: Aggiungi dominio @@ -214,7 +256,6 @@ it: assigned: Moderatore assegnato comment: none: Nessuno - delete: Elimina id: ID mark_as_resolved: Segna come risolto mark_as_unresolved: Segna come non risolto @@ -223,10 +264,10 @@ it: create_and_resolve: Risolvi con nota create_and_unresolve: Riapri con nota delete: Elimina - nsfw: - 'false': Mostra gli allegati multimediali - 'true': Nascondi allegati multimediali + reopen: Riapri rapporto + report: 'Rapporto #%{id}' report_contents: Contenuti + reported_by: Inviato da resolved: Risolto silence_account: Silenzia account status: Stato @@ -238,19 +279,31 @@ it: view: Mostra settings: activity_api_enabled: + desc_html: Conteggi degli status pubblicati localmente, degli utenti attivi e delle nuove registrazioni in gruppi settimanali title: Pubblica statistiche aggregate circa l'attività dell'utente + bootstrap_timeline_accounts: + title: Seguiti predefiniti per i nuovi utenti contact_information: username: Nome utente del contatto peers_api_enabled: + desc_html: Nomi di dominio che questa istanza ha incontrato nella fediverse title: Pubblica elenco di istanze scoperte registrations: + closed_message: + desc_html: Mostrato nella pagina iniziale quando le registrazioni sono chiuse. Puoi usare tag HTML + title: Messaggio per registrazioni chiuse deletion: desc_html: Consenti a chiunque di cancellare il proprio account title: Apri la cancellazione dell'account min_invite_role: disabled: Nessuno + title: Permetti inviti da open: desc_html: Consenti a chiunque di creare un account + title: Apri registrazioni + show_known_fediverse_at_about_page: + desc_html: Quando attivato, mostra nell'anteprima i toot da tutte le istanze conosciute. Altrimenti mostra solo i toot locali. + title: Mostra la fediverse conosciuta nell'anteprima della timeline show_staff_badge: title: Mostra badge staff site_description: @@ -262,17 +315,16 @@ it: title: Anteprima timeline title: Impostazioni sito statuses: + back_to_account: Torna alla pagina dell'account batch: delete: Elimina - nsfw_off: NSFW OFF - nsfw_on: NSFW ON - execute: Esegui + nsfw_off: Segna come non sensibile + nsfw_on: Segna come sensibile failed_to_execute: Impossibile eseguire media: - hide: Nascondi media - show: Mostra media title: Media no_media: Nessun media + title: Gli status dell'account with_media: con media subscriptions: callback_url: URL Callback @@ -283,7 +335,7 @@ it: application_mailer: notification_preferences: Cambia preferenze email salutation: "%{name}," - settings: 'Cambia le impostazioni per le e-mail: %{link}' + settings: 'Cambia le impostazioni per le email: %{link}' view: 'Guarda:' view_profile: Mostra profilo view_status: Mostra stati @@ -291,16 +343,23 @@ it: created: Applicazione creata con successo destroyed: Applicazione eliminata con successo invalid_url: L'URL fornito non è valido + regenerate_token: Rigenera il token di accesso + token_regenerated: Token di accesso rigenerato + warning: Fa' molta attenzione con questi dati. Non fornirli mai a nessun altro! auth: + agreement_html: Iscrivendoti, accetti di seguire le regole dell'istanza e le nostre condizioni di servizio. change_password: Password confirm_email: Conferma email delete_account: Elimina account + delete_account_html: Se desideri cancellare il tuo account, puoi farlo qui. Ti sarà chiesta conferma. didnt_get_confirmation: Non hai ricevuto le istruzioni di conferma? forgot_password: Hai dimenticato la tua password? login: Entra - logout: Logout + logout: Sloggati migrate_account: Sposta ad un account differente + migrate_account_html: Se vuoi che questo account sia reindirizzato a uno diverso, puoi configurarlo qui. or: o + or_log_in_with: Oppure accedi con register: Iscriviti register_elsewhere: Iscriviti su un altro server resend_confirmation: Invia di nuovo le istruzioni di conferma @@ -311,6 +370,11 @@ it: already_following: Stai già seguendo questo account error: Sfortunatamente c'è stato un errore nel consultare l'account remoto follow: Segui + follow_request: 'Hai mandato una richiesta di diventare seguace a:' + following: 'Accettato! Ora stai seguendo:' + post_follow: + close: Oppure puoi chiudere questa finestra. + return: Torna al profilo dell'utente title: Segui %{acct} datetime: distance_in_words: @@ -326,11 +390,41 @@ it: x_minutes: "%{count} minuti" x_months: "%{count} mesi" x_seconds: "%{count} secondi" + deletes: + bad_password_msg: Ci avete provato, hacker! Password errata + confirm_password: Inserisci la tua password attuale per verificare la tua identità + description_html: Questa azione eliminerà in modo permanente e irreversibile tutto il contenuto del tuo account e lo disattiverà. Il tuo nome utente resterà riservato per prevenire che qualcuno in futuro assuma la tua identità. + proceed: Cancella l'account + success_msg: Il tuo account è stato cancellato + warning_html: È garantita solo la cancellazione del contenuto solo da questa istanza. I contenuti che sono stati ampiamente condivisi probabilmente lasceranno delle tracce. I server offline e quelli che non ricevono più i tuoi aggiornamenti non aggiorneranno i loro database. + errors: + '403': Non sei autorizzato a visualizzare questa pagina. + '404': La pagina che stavi cercando non esiste. + '410': La pagina che stavi cercando non esiste più. + '422': + content: Verifica di sicurezza non riuscita. Stai bloccando i cookies? + title: Verifica di sicurezza non riuscita + noscript_html: Per usare l'interfaccia web di Mastodon dovi abilitare JavaScript. In alternativa puoi provare una delle app native per Mastodon per la tua piattaforma. exports: + archive_takeout: + date: Data + download: Scarica il tuo archivio + hint_html: Puoi richiedere un archivio dei tuoi toot e media caricati. I dati esportati sono in formato ActivityPub, leggibili da qualunque software che segue questo standard. Puoi richiedere un archivio ogni 7 giorni. + in_progress: Creazione archivio... + request: Richiedi il tuo archivio + size: Dimensioni blocks: Stai bloccando csv: CSV follows: Stai seguendo + mutes: Stai silenziando storage: Archiviazione media + followers: + domain: Dominio + explanation_html: Se vuoi garantire la privacy dei tuoi status, devi sapere chi ti sta seguendo. I tuoi status privati vengono inviati a tutte le istanze su cui hai dei seguaci. Puoi controllare chi sono i tuoi seguaci, ed eliminarli se non hai fiducia che la tua privacy venga rispettata dallo staff o dal software di quelle istanze. + followers_count: Numero di seguaci + purge: Elimina dai seguaci + true_privacy_html: Tieni presente che l'effettiva riservatezza si può ottenere solo con la crittografia end-to-end. + unlocked_warning_html: Chiunque può seguirti per vedere immediatamente i tuoi status privati. %{lock_link} per poter esaminare e respingere gli utenti che vogliono seguirti. generic: changes_saved_msg: Modifiche effettuate con successo! powered_by: offerto da %{link} @@ -344,38 +438,79 @@ it: types: blocking: Lista dei bloccati following: Lista dei seguaci + muting: Lista dei silenziati upload: Carica + in_memoriam_html: In Memoriam. + invites: + delete: Disattiva + expired: Scaduto + expires_in: + '1800': 30 minuti + '21600': 6 ore + '3600': 1 ora + '43200': 12 ore + '604800': 1 settimana + '86400': 1 giorno + expires_in_prompt: Mai + generate: Genera + max_uses: + other: "%{count} utilizzi" + max_uses_prompt: Nessun limite + prompt: Genera e condividi dei link ad altri per garantire l'accesso a questa istanza + table: + expires_at: Scade + uses: Utilizzi + title: Invita persone landing_strip_html: "%{name} è un utente su %{link_to_root_path}. Puoi seguirlo o interagire con lui se possiedi un account ovunque nel fediverse." landing_strip_signup_html: Se non possiedi un account, puoi iscriverti qui. + lists: + errors: + limit: Hai raggiunto il numero massimo di liste media_attachments: validations: images_and_video: Impossibile allegare video a un post che contiene già immagini too_many: Impossibile allegare più di 4 file + migrations: + acct: utente@dominio del nuovo account + currently_redirecting: 'Il tuo profilo sarà ridirezionato a:' + proceed: Salva + updated_msg: L'impostazione per la migrazione dell'account è sta aggiornata! + moderation: + title: Moderazione notification_mailer: digest: - body: 'Ecco un breve riassunto di quello che ti sei perso su %{instance} dalla tua ultima visita del %{since}:' + action: Vedi tutte le notifiche + body: Ecco un breve riassunto di quello che ti sei perso dalla tua ultima visita del %{since} mention: "%{name} ti ha menzionato:" new_followers_summary: - one: Hai ricevuto un nuovo seguace! Urrà! - other: Hai ricevuto %{count} nuovi seguaci! Incredibile! + one: E inoltre hai ricevuto un nuovo seguace mentre eri assente! Urrà! + other: Inoltre, hai acquisito %{count} nuovi seguaci mentre eri assente! Incredibile! subject: one: "1 nuova notifica dalla tua ultima visita \U0001F418" other: "%{count} nuove notifiche dalla tua ultima visita \U0001F418" + title: In tua assenza… favourite: body: 'Il tuo status è stato apprezzato da %{name}:' subject: "%{name} ha apprezzato il tuo status" + title: Nuovo preferito follow: body: "%{name} ti sta seguendo!" subject: "%{name} ti sta seguendo" + title: Nuovo seguace follow_request: + action: Gestisci richieste di essere seguito body: "%{name} ha chiesto di seguirti" subject: 'Seguace in sospeso: %{name}' + title: Nuova richiesta di essere seguito mention: + action: Rispondi body: 'Sei stato menzionato da %{name} su:' subject: Sei stato menzionato da %{name} + title: Nuova menzione reblog: body: 'Il tuo status è stato condiviso da %{name}:' subject: "%{name} ha condiviso il tuo status" + title: Nuova condivisione number: human: decimal_units: @@ -388,35 +523,80 @@ it: trillion: T unit: '' pagination: + newer: Più recente next: Avanti + older: Più vecchio prev: Indietro truncate: "…" + preferences: + languages: Lingue + other: Altro + publishing: Pubblicazione + web: Web remote_follow: acct: Inserisci il tuo username@dominio da cui vuoi seguire questo utente missing_resource: Impossibile trovare l'URL di reindirizzamento richiesto per il tuo account proceed: Conferma prompt: 'Stai per seguire:' + remote_unfollow: + error: Errore + title: Titolo + sessions: + activity: Ultima attività + browser: Browser + browsers: + blackberry: Blackberry + chrome: Chrome + generic: Browser sconosciuto + current_session: Sessione corrente + description: "%{browser} su %{platform}" + platforms: + other: piattaforma sconosciuta settings: authorized_apps: Applicazioni autorizzate back: Torna a Mastodon + delete: Cancellazione account + development: Sviluppo edit_profile: Modifica profilo export: Esporta impostazioni + followers: Seguaci autorizzati import: Importa + migrate: Migrazione dell'account + notifications: Notifiche preferences: Preferenze settings: Impostazioni - two_factor_authentication: Autenticazione a Due Fattori + two_factor_authentication: Autenticazione a due fattori + your_apps: Le tue applicazioni statuses: + attached: + video: + one: "%{count} video" + other: "%{count} video" open_in_web: Apri sul Web over_character_limit: Limite caratteri superato di %{max} + pin_errors: + limit: Hai già fissato in cima il massimo numero di toot + ownership: Non puoi fissare in cima un toot di qualcun altro + private: Un toot non pubblico non può essere fissato in cima + reblog: Un toot condiviso non può essere fissato in cima show_more: Mostra di più visibilities: private: Mostra solo ai tuoi seguaci + private_long: Mostra solo ai seguaci public: Pubblico + public_long: Tutti lo possono vedere unlisted: Pubblico, ma non visibile sulla timeline pubblica + unlisted_long: Tutti lo possono vedere, ma non compare nelle timeline pubbliche stream_entries: click_to_show: Clicca per mostrare + pinned: Toot fissato in cima reblogged: condiviso sensitive_content: Materiale sensibile + terms: + title: "%{instance} Termini di servizio e politica della privacy" + themes: + contrast: Contrasto elevato + default: Mastodon time: formats: default: "%b %d, %Y, %H:%M" @@ -425,15 +605,39 @@ it: description_html: Se abiliti l'autorizzazione a due fattori, entrare nel tuo account ti richiederà di avere vicino il tuo telefono, il quale ti genererà un codice per eseguire l'accesso. disable: Disabilita enable: Abilita + enabled: È abilitata l'autenticazione a due fattori enabled_success: Autenticazione a due fattori attivata con successo + generate_recovery_codes: Genera codici di recupero instructions_html: "Scannerizza questo QR code con Google Authenticator o un'app TOTP simile sul tuo telefono. Da ora in poi, quell'applicazione genererà codici da inserire necessariamente per eseguire l'accesso." + lost_recovery_codes: I codici di recupero ti permettono di accedere al tuo account se perdi il telefono. Se hai perso i tuoi codici di recupero, puoi rigenerarli qui. Quelli vecchi saranno annullati. manual_instructions: 'Se non puoi scannerizzare il QR code e hai bisogno di inserirlo manualmente, questo è il codice segreto in chiaro:' + recovery_codes_regenerated: I codici di recupero sono stati rigenerati + recovery_instructions_html: Se perdi il telefono, puoi usare uno dei codici di recupero qui sotto per riottenere l'accesso al tuo account. Conserva i codici di recupero in un posto sicuro. Ad esempio puoi stamparli e conservarli insieme ad altri documenti importanti. setup: Configura - wrong_code: Il codice inserito non è corretto! Assicurati che l'orario del server e l'orario del telefono siano corretti. + wrong_code: Il codice inserito non è corretto! Assicurati che l'orario del server e l'orario del dispotivo siano corretti. user_mailer: + backup_ready: + explanation: Hai richiesto un backup completo del tuo account Mastodon. È pronto per essere scaricato! + subject: Il tuo archivio è pronto per essere scaricato + title: Esportazione archivio welcome: + edit_profile_step: Puoi personalizzare il tuo profilo caricando un avatar, un'intestazione, modificando il tuo nome visualizzato e così via. Se vuoi controllare i tuoi nuovi seguaci prima di autorizzarli a seguirti, puoi bloccare il tuo account. + explanation: Ecco alcuni suggerimenti per iniziare + final_action: Inizia a postare + final_step: 'Inizia a postare! Anche se non hai seguaci, i tuoi messaggi pubblici possono essere visti da altri, ad esempio nelle timeline locali e negli hashtag. Se vuoi puoi presentarti con l''hashtag #introductions.' + full_handle: Il tuo nome utente completo + full_handle_hint: Questo è ciò che diresti ai tuoi amici in modo che possano seguirti o contattarti da un'altra istanza. + review_preferences_action: Cambia preferenze + review_preferences_step: Dovresti impostare le tue preferenze, ad esempio quali email vuoi ricevere oppure il livello predefinito di privacy per i tuoi post. Se le immagini in movimento non ti danno fastidio, puoi abilitare l'animazione automatica delle GIF. + subject: Benvenuto/a su Mastodon + tip_bridge_html: Se vieni da Twitter, puoi trovare i tuoi amici su Mastodon usando laapp bridge. Ma funziona solo se anche loro la usano! + tip_federated_timeline: La timeline federata visualizza uno dopo l'altro i messaggi pubblicati su Mastodon. Ma comprende solo gli utenti seguiti dai tuoi vicini, quindi non è completa. + tip_following: Per impostazione predefinita, segui l'amministratore/i del tuo server. Per trovare utenti più interessanti, dà un'occhiata alle timeline locale e federata. + tip_local_timeline: La timeline locale visualizza uno dopo l'altro i messaggi degli utenti di %{instance}. Questi sono i tuoi vicini! + tip_mobile_webapp: Se il tuo browser mobile ti dà la possibilità di aggiungere Mastodon allo schermo, puoi ricevere le notifiche. Funziona un po' come un'app natova! tips: Suggerimenti title: Benvenuto a bordo, %{name}! users: - invalid_email: L'indirizzo e-mail inserito non è valido + invalid_email: L'indirizzo email inserito non è valido invalid_otp_token: Codice d'accesso non valido + seamless_external_login: Ti sei collegato per mezzo di un servizio esterno, quindi le impostazioni di email e password non sono disponibili. diff --git a/config/locales/ja.yml b/config/locales/ja.yml index be9e2da2c2..19b4017c71 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -40,6 +40,7 @@ ja: following: フォロー中 media: メディア moved_html: "%{name} さんは引っ越しました %{new_profile_link}:" + network_hidden: この情報は利用できません nothing_here: 何もありません! people_followed_by: "%{name} さんがフォロー中のアカウント" people_who_follow: "%{name} さんをフォロー中のアカウント" @@ -49,13 +50,12 @@ ja: reserved_username: このユーザー名は予約されています roles: admin: Admin + bot: Bot moderator: Mod unfollow: フォロー解除 admin: account_moderation_notes: - account: モデレータ create: 書き込む - created_at: 日付 created_msg: モデレーションメモを書き込みました! delete: 削除 destroyed_msg: モデレーションメモを削除しました! @@ -72,6 +72,7 @@ ja: title: "%{username} さんのメールアドレスを変更" confirm: 確認 confirmed: 確認済み + confirming: 確認中 demote: 降格 disable: 無効化 disable_two_factor_authentication: 二段階認証を無効にする @@ -80,6 +81,7 @@ ja: domain: ドメイン edit: 編集 email: メールアドレス + email_status: メールアドレスの状態 enable: 有効化 enabled: 有効 feed_url: フィードURL @@ -118,6 +120,10 @@ ja: push_subscription_expires: PuSH購読期限 redownload: アバターの更新 remove_avatar: アイコンを削除 + resend_confirmation: + already_confirmed: メールアドレスは確認済みです + send: 確認メールを再送 + success: 確認メールを再送信しました! reset: リセット reset_password: パスワード再設定 resubscribe: 再講読 @@ -269,7 +275,6 @@ ja: comment: none: なし created_at: レポート日時 - delete: 削除 id: ID mark_as_resolved: 解決済みとしてマーク mark_as_unresolved: 未解決として再び開く @@ -278,10 +283,7 @@ ja: create_and_resolve: 書き込み、解決済みにする create_and_unresolve: 書き込み、未解決として開く delete: 削除 - placeholder: このレポートに取られた措置や、その他の更新を記述してください… - nsfw: - 'false': NSFW オフ - 'true': NSFW オン + placeholder: どのような措置が取られたか、または関連する更新を記述してください… reopen: 再び開く report: レポート#%{id} report_contents: 内容 @@ -354,13 +356,10 @@ ja: back_to_account: アカウントページに戻る batch: delete: 削除 - nsfw_off: 閲覧注意のマークを取り除く - nsfw_on: 閲覧注意としてマークする - execute: 実行 + nsfw_off: 閲覧注意をはずす + nsfw_on: 閲覧注意にする failed_to_execute: 実行に失敗しました media: - hide: メディアを隠す - show: メディアを表示 title: メディア no_media: メディアなし title: トゥート一覧 @@ -376,6 +375,7 @@ ja: admin_mailer: new_report: body: "%{reporter} が %{target} を通報しました" + body_remote: "%{domain} の誰かが %{target} を通報しました" subject: "%{instance} の新しい通報 (#%{id})" application_mailer: notification_preferences: メール設定の変更 @@ -393,7 +393,7 @@ ja: warning: このデータは気をつけて取り扱ってください。他の人と共有しないでください! your_token: アクセストークン auth: - agreement_html: 登録すると インスタンスのルール と 利用規約 に従うことに同意したことになります。 + agreement_html: 登録すると インスタンスのルール と プライバシーポリシー に従うことに同意したことになります。 change_password: パスワード confirm_email: メールアドレスの確認 delete_account: アカウントの削除 @@ -465,7 +465,7 @@ ja: archive_takeout: date: 日時 download: ダウンロード - hint_html: "トゥートとメディアのアーカイブをリクエストできます。 データはActivityPub形式で、対応しているソフトウェアで読み込むことができます。" + hint_html: "トゥートとメディアのアーカイブをリクエストできます。 データはActivityPub形式で、対応しているソフトウェアで読み込むことができます。7日毎にアーカイブをリクエストできます。" in_progress: 準備中... request: アーカイブをリクエスト size: 容量 @@ -478,14 +478,14 @@ ja: domain: ドメイン explanation_html: あなたの投稿のプライバシーを確保したい場合、誰があなたをフォローしているのかを把握している必要があります。 プライベート投稿は、あなたのフォロワーがいる全てのインスタンスに配信されます。 フォロワーのインスタンスの管理者やソフトウェアがあなたのプライバシーを尊重してくれるかどうか怪しい場合は、そのフォロワーを削除した方がよいかもしれません。 followers_count: フォロワー数 - lock_link: 非公開アカウントにする + lock_link: 承認制アカウントにする purge: フォロワーから削除する success: one: 1個のドメインからソフトブロックするフォロワーを処理中... other: "%{count} 個のドメインからソフトブロックするフォロワーを処理中..." true_privacy_html: "プライバシーの保護はエンドツーエンドの暗号化でのみ実現可能であることに留意ください。" - unlocked_warning_html: 誰でもあなたをフォローすることができ、あなたのプライベート投稿をすぐに見ることができます。フォローする人を限定したい場合は%{lock_link}に設定してください。 - unlocked_warning_title: このアカウントは非公開アカウントに設定されていません + unlocked_warning_html: 誰でもあなたをフォローすることができ、フォロワー限定の投稿をすぐに見ることができます。フォローする人を限定したい場合は%{lock_link}に設定してください。 + unlocked_warning_title: このアカウントは承認制アカウントに設定されていません generic: changes_saved_msg: 正常に変更されました! powered_by: powered by %{link} @@ -604,20 +604,6 @@ ja: other: その他 publishing: 投稿 web: ウェブ - push_notifications: - favourite: - title: あなたのトゥートが %{name} さんにお気に入り登録されました - follow: - title: "%{name} さんにフォローされました" - group: - title: "%{count} 件の通知" - mention: - action_boost: ブースト - action_expand: もっと見る - action_favourite: お気に入り - title: "%{name} さんから返信がありました" - reblog: - title: あなたのトゥートが %{name} さんにブーストされました remote_follow: acct: あなたの ユーザー名@ドメイン を入力してください missing_resource: リダイレクト先が見つかりませんでした @@ -694,6 +680,7 @@ ja: video: one: "%{count} 本の動画" other: "%{count} 本の動画" + boosted_from_html: "%{acct_link} からブースト" content_warning: '閲覧注意: %{warning}' disallowed_hashtags: one: '許可されていないハッシュタグが含まれています: %{tags}' @@ -708,7 +695,7 @@ ja: show_more: もっと見る title: '%{name}: "%{quote}"' visibilities: - private: 非公開 + private: フォロワー限定 private_long: フォロワーにのみ表示されます public: 公開 public_long: 誰でも見ることができ、かつ公開タイムラインに表示されます @@ -727,7 +714,7 @@ ja: 基本的なアカウント情報: 当サイトに登録すると、ユーザー名・メールアドレス・パスワードの入力を求められることがあります。また表示名や自己紹介・プロフィール画像・ヘッダー画像といった追加のプロフィールを登録できます。ユーザー名・表示名・自己紹介・プロフィール画像・ヘッダー画像は常に公開されます。 投稿・フォロー・その他公開情報: フォローしているユーザーの一覧は一般公開されます。フォロワーも同様です。メッセージを投稿する際、日時だけでなく投稿に使用したアプリケーション名も記録されます。メッセージには写真や動画といった添付メディアを含むことがあります。「公開」や「未収載」の投稿は一般公開されます。プロフィールに投稿を載せるとそれもまた公開情報となります。投稿はフォロワーに配信されます。場合によっては他のサーバーに配信され、そこにコピーが保存されることを意味します。投稿を削除した場合も同様にフォロワーに配信されます。他の投稿をリブログやお気に入り登録する行動は常に公開されます。 - 「ダイレクト」と「非公開」投稿: すべての投稿はサーバーに保存され、処理されます。「非公開」投稿はフォロワーと投稿に書かれたユーザーに配信されます。「ダイレクト」投稿は投稿に書かれたユーザーにのみ配信されます。場合によっては他のサーバーに配信され、そこにコピーが保存されることを意味します。私たちはこれらの閲覧を一部の許可された者に限定するよう誠意を持って努めます。しかし他のサーバーにおいても同様に扱われるとは限りません。したがって、相手の所属するサーバーを吟味することが重要です。設定で新しいフォロワーの承認または拒否を手動で行うよう切り替えることもできます。サーバー管理者は「ダイレクト」や「非公開」投稿も閲覧する可能性があることを忘れないでください。また受信者がスクリーンショットやコピー、もしくは共有する可能性があることを忘れないでください。いかなる危険な情報もMastodon上で共有しないでください。 + 「ダイレクト」と「フォロワー限定」投稿: すべての投稿はサーバーに保存され、処理されます。「フォロワー限定」投稿はフォロワーと投稿に書かれたユーザーに配信されます。「ダイレクト」投稿は投稿に書かれたユーザーにのみ配信されます。場合によっては他のサーバーに配信され、そこにコピーが保存されることを意味します。私たちはこれらの閲覧を一部の許可された者に限定するよう誠意を持って努めます。しかし他のサーバーにおいても同様に扱われるとは限りません。したがって、相手の所属するサーバーを吟味することが重要です。設定で新しいフォロワーの承認または拒否を手動で行うよう切り替えることもできます。サーバー管理者は「ダイレクト」や「フォロワー限定」投稿も閲覧する可能性があることを忘れないでください。また受信者がスクリーンショットやコピー、もしくは共有する可能性があることを忘れないでください。いかなる危険な情報もMastodon上で共有しないでください。 IPアドレスやその他メタデータ: ログインする際IPアドレスだけでなくブラウザーアプリケーション名を記録します。ログインしたセッションはすべてユーザー設定で見直し、取り消すことができます。使用されている最新のIPアドレスは最大12ヵ月間保存されます。またサーバーへのIPアドレスを含むすべてのリクエストのログを保持することがあります。 @@ -768,7 +755,7 @@ ja: クッキーを使用していますか? - はい。クッキーは (あなたが許可した場合に) WebサイトやサービスがWebブラウザーを介してコンピューターに保存する小さなファイルです。使用することで Web サイトがブラウザーを識別し、登録済みのアカウントがある場合関連付けます。 + はい。クッキーは (あなたが許可した場合に) WebサイトやサービスがWebブラウザーを介してコンピューターに保存する小さなファイルです。使用することでWebサイトがブラウザーを識別し、登録済みのアカウントがある場合関連付けます。 私たちはクッキーを将来の訪問のために設定を保存し呼び出す用途に使用します。 @@ -778,15 +765,19 @@ ja: 私たちは個人を特定できる情報を外部へ販売・取引・その他方法で渡すことはありません。これには当サイトの運営・業務遂行・サービス提供を行ううえで補助する信頼できる第三者をこの機密情報の保護に同意するかぎり含みません。法令の遵守やサイトポリシーの施行、権利・財産・安全の保護に適切と判断した場合、あなたの情報を公開することがあります。 - あなたの公開情報はネットワーク上の他のサーバーにダウンロードされることがあります。相手が異なるサーバーに所属する場合、「公開」と「非公開」投稿はフォロワーの所属するサーバーに配信され、「ダイレクト」投稿は受信者の所属するサーバーに配信されます。 + あなたの公開情報はネットワーク上の他のサーバーにダウンロードされることがあります。相手が異なるサーバーに所属する場合、「公開」と「フォロワー限定」投稿はフォロワーの所属するサーバーに配信され、「ダイレクト」投稿は受信者の所属するサーバーに配信されます。 あなたがアカウントの使用をアプリケーションに許可すると、承認した権限の範囲内で公開プロフィール情報・フォローリスト・フォロワー・リスト・すべての投稿・お気に入り登録にアクセスできます。アプリケーションはメールアドレスやパスワードに決してアクセスできません。 - 児童オンラインプライバシー保護法の遵守 + 児童によるサイト利用について - 当サイト・製品・サービスは13歳以上の人を対象としています。サーバーが米国にあり、あなたが13歳未満の場合、COPPA (Children's Online Privacy Protection Act - 児童オンラインプライバシー保護法) により当サイトを使用できません。 + サーバーがEUまたはEEA圏内にある場合: 当サイト・製品・サービスは16歳以上の人を対象としています。あなたが16歳未満の場合、GDPR (General Data Protection Regulation - EU一般データ保護規則) により当サイトを使用できません。 + + サーバーが米国にある場合: 当サイト・製品・サービスは13歳以上の人を対象としています。あなたが13歳未満の場合、COPPA (Children's Online Privacy Protection Act - 児童オンラインプライバシー保護法) により当サイトを使用できません。 + + サーバーが別の管轄区域にある場合、法的要件は異なることがあります。 @@ -826,7 +817,7 @@ ja: title: アーカイブの取り出し welcome: edit_profile_action: プロフィールを設定 - edit_profile_step: アバター画像やヘッダー画像をアップロードしたり、表示名やその他プロフィールを変更しカスタマイズすることができます。新しいフォロワーからのフォローを許可する前に検討したい場合、アカウントを非公開にすることができます。 + edit_profile_step: アバター画像やヘッダー画像をアップロードしたり、表示名やその他プロフィールを変更しカスタマイズすることができます。新しいフォロワーからのフォローを許可する前に検討したい場合、アカウントを承認制にすることができます。 explanation: 始めるにあたってのアドバイスです final_action: 始めましょう final_step: 'さあ始めましょう! たとえフォロワーがいなくても、あなたの公開した投稿はローカルタイムラインやハッシュタグなどで誰かの目に止まるかもしれません。自己紹介をしたい時は #introductions ハッシュタグを使うといいかもしれません。' @@ -845,5 +836,6 @@ ja: users: invalid_email: メールアドレスが無効です invalid_otp_token: 二段階認証コードが間違っています + otp_lost_help_html: どちらも使用できない場合、%{email} に連絡を取ると解決できるかもしれません seamless_external_login: あなたは外部サービスを介してログインしているため、パスワードとメールアドレスの設定は利用できません。 signed_in_as: '下記でログイン中:' diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 251c0c3d7b..38f411dd5f 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -19,12 +19,12 @@ ko: humane_approach_body: 다른 SNS의 실패를 교훈삼아, Mastodon은 소셜미디어가 잘못 사용되는 것을 막기 위하여 윤리적인 설계를 추구합니다. humane_approach_title: 보다 배려를 의식한 설계를 추구 not_a_product_body: Mastodon은 이익을 추구하는 SNS가 아닙니다. 그러므로 광고와 데이터의 수집 및 분석이 존재하지 않고, 유저를 구속하지도 않습니다. - not_a_product_title: 여러분은 사람이며, 상품이 아닙니다. + not_a_product_title: 여러분은 사람이며, 상품이 아닙니다 real_conversation_body: 자유롭게 사용할 수 있는 500문자의 메세지와 미디어 경고 내용을 바탕으로, 자기자신을 자유롭게 표현할 수 있습니다. real_conversation_title: 진정한 커뮤니케이션을 위하여 within_reach_body: 개발자 친화적인 API에 의해서 실현된 iOS나 Android, 그 외의 여러 Platform들 덕분에 어디서든 친구들과 자유롭게 메세지를 주고 받을 수 있습니다. within_reach_title: 언제나 유저의 곁에서 - generic_description: "%{domain} 은 Mastodon의 인스턴스 입니다." + generic_description: "%{domain} 은 네트워크에 있는 한 서버입니다" hosted_on: "%{domain}에서 호스팅 되는 마스토돈" learn_more: 자세히 other_instances: 다른 인스턴스 @@ -49,13 +49,12 @@ ko: reserved_username: 이 아이디는 예약되어 있습니다 roles: admin: 관리자 + bot: 봇 moderator: 모더레이터 unfollow: 팔로우 해제 admin: account_moderation_notes: - account: 모더레이터 - create: 작성하기 - created_at: 작성 날짜 + create: 모더레이션 노트 작성하기 created_msg: 모더레이션 기록이 성공적으로 작성되었습니다! delete: 삭제 destroyed_msg: 모더레이션 기록이 성공적으로 삭제되었습니다! @@ -72,6 +71,7 @@ ko: title: "%{username}의 이메일 주소 변경" confirm: 확인 confirmed: 확인됨 + confirming: 확인 중 demote: 모더레이터 강등 disable: 비활성화 disable_two_factor_authentication: 2단계 인증을 비활성화 @@ -80,6 +80,7 @@ ko: domain: 도메인 edit: 편집 email: E-mail + email_status: 이메일 상태 enable: 활성화 enabled: 활성화된 feed_url: 피드 URL @@ -118,6 +119,10 @@ ko: push_subscription_expires: PuSH 구독 기간 만료 redownload: 아바타 업데이트 remove_avatar: 아바타 지우기 + resend_confirmation: + already_confirmed: 이 사용자는 이미 확인되었습니다 + send: 다시 확인 이메일 + success: 확인 이메일이 전송되었습니다! reset: 초기화 reset_password: 비밀번호 초기화 resubscribe: 다시 구독 @@ -271,19 +276,15 @@ ko: comment: none: 없음 created_at: 리포트 시각 - delete: 삭제 id: ID mark_as_resolved: 해결 완료 처리 mark_as_unresolved: 미해결로 표시 notes: - create: 노트 추가 - create_and_resolve: 노트를 작성하고 해결됨으로 표시 - create_and_unresolve: 노트 작성과 함께 미해결로 표시 + create: 기록 추가 + create_and_resolve: 기록을 작성하고 해결됨으로 표시 + create_and_unresolve: 기록 작성과 함께 미해결로 표시 delete: 삭제 placeholder: 이 리포트에 대한 조치, 다른 업데이트 사항에 대해 설명합니다… - nsfw: - 'false': NSFW 꺼짐 - 'true': NSFW 켜짐 reopen: 리포트 다시 열기 report: '신고 #%{id}' report_contents: 내용 @@ -358,11 +359,8 @@ ko: delete: 삭제 nsfw_off: NSFW 끄기 nsfw_on: NSFW 켜기 - execute: 실행 failed_to_execute: 실행을 실패하였습니다 media: - hide: 미디어 숨기기 - show: 미디어 보여주기 title: 미디어 no_media: 미디어 없음 title: 계정 툿 @@ -378,6 +376,7 @@ ko: admin_mailer: new_report: body: "%{reporter} 가 %{target} 를 신고했습니다" + body_remote: "%{domain}의 누군가가 %{target}을 신고했습니다" subject: "%{instance} 에 새 신고 등록됨 (#%{id})" application_mailer: notification_preferences: 메일 설정 변경 @@ -467,7 +466,7 @@ ko: archive_takeout: date: 날짜 download: 아카이브 다운로드 - hint_html: 당신의 툿과 업로드 된 미디어의 아카이브를 요청할 수 있습니다. 내보내지는 데이터는 ActivityPub 포맷입니다. 호환 되는 모든 소프트웨어에서 읽을 수 있습니다. + hint_html: 당신의 툿과 업로드 된 미디어의 아카이브를 요청할 수 있습니다. 내보내지는 데이터는 ActivityPub 포맷입니다. 호환 되는 모든 소프트웨어에서 읽을 수 있습니다. 7일마다 새로운 아카이브를 요청할 수 있습니다. in_progress: 당신의 아카이브를 컴파일 중입니다… request: 아카이브 요청하기 size: 크기 @@ -597,20 +596,6 @@ ko: other: 기타 publishing: 퍼블리싱 web: 웹 - push_notifications: - favourite: - title: "%{name} 님이 당신의 툿를 즐겨찾기에 등록했습니다" - follow: - title: "%{name} 님이 나를 팔로우 하고 있습니다" - group: - title: "%{count} 건의 알림" - mention: - action_boost: 부스트 - action_expand: 더보기 - action_favourite: 즐겨찾기 - title: "%{name} 님이 답장을 보냈습니다" - reblog: - title: "%{name} 님이 당신의 툿을 부스트 했습니다" remote_follow: acct: 아이디@도메인을 입력해 주십시오 missing_resource: 리디렉션 대상을 찾을 수 없습니다 @@ -686,6 +671,9 @@ ko: one: "%{count} 영상" other: "%{count} 영상" content_warning: '열람 주의: %{warning}' + disallowed_hashtags: + one: '허용 되지 않은 해시태그를 포함하고 있습니다: %{tags}' + other: '허용되지 않은 해시태그를 포함하고 있습니다: %{tags}' open_in_web: Web으로 열기 over_character_limit: 최대 %{max}자까지 입력할 수 있습니다 pin_errors: @@ -709,6 +697,9 @@ ko: sensitive_content: 민감한 컨텐츠 terms: title: "%{instance} 이용약관과 개인정보 취급 방침" + themes: + contrast: 고대비 + default: 마스토돈 time: formats: default: "%Y년 %m월 %d일 %H:%M" @@ -727,10 +718,10 @@ ko: recovery_codes_regenerated: 복구 코드가 다시 생성되었습니다 recovery_instructions_html: 휴대전화를 분실한 경우, 아래 복구 코드 중 하나를 사용해 계정에 접근할 수 있습니다. 복구 코드는 안전하게 보관해 주십시오. 이 코드를 인쇄해 중요한 서류와 함께 보관하는 것도 좋습니다. setup: 초기 설정 - wrong_code: 코드가 올바르지 않습니다. 서버와 휴대전화 간의 시간이 일치하는지 확인해 주십시오. + wrong_code: 코드가 올바르지 않습니다. 서버와 휴대전화 간의 시각이 일치하나요? user_mailer: backup_ready: - explanation: 당신이 요청한 계정의 풀 백업이 이제 다운로드 가능합니다. + explanation: 당신이 요청한 계정의 풀 백업이 이제 다운로드 가능합니다! subject: 당신의 아카이브를 다운로드 가능합니다 title: 아카이브 테이크 아웃 welcome: @@ -754,5 +745,6 @@ ko: users: invalid_email: 메일 주소가 올바르지 않습니다 invalid_otp_token: 2단계 인증 코드가 올바르지 않습니다 + otp_lost_help_html: 만약 양쪽 모두를 잃어버렸다면 %{email}을 통해 복구할 수 있습니다 seamless_external_login: 외부 서비스를 이용해 로그인 했습니다, 패스워드와 이메일 설정을 할 수 없습니다. signed_in_as: '다음과 같이 로그인 중:' diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 1ccc01a8f1..1fe3b54720 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -5,10 +5,10 @@ nl: about_mastodon_html: Mastodon is een sociaal netwerk dat gebruikt maakt van open webprotocollen en vrije software. Het is net zoals e-mail gedecentraliseerd. about_this: Over deze server administered_by: 'Beheerd door:' - closed_registrations: Registreren op deze server is momenteel uitgeschakeld. + closed_registrations: Registreren op deze server is momenteel niet mogelijk. Je kunt echter een andere server vinden om zo toegang te krijgen tot het netwerk. contact: Contact contact_missing: Niet ingesteld - contact_unavailable: N/A + contact_unavailable: n.v.t description_headline: Wat is %{domain}? domain_count_after: andere servers domain_count_before: Verbonden met @@ -16,11 +16,11 @@ nl: Een goede plek voor richtlijnen De uitgebreide omschrijving is nog niet ingevuld. features: - humane_approach_body: Na van de fouten van andere netwerken te hebben geleerd, tracht Mastodon ethische ontwerpkeuzes te maken om misbruik van social media te voorkomen. + humane_approach_body: Mastodon heeft van de fouten van andere sociale netwerken geleerd en probeert aan de hand van ethische ontwerpkeuzes misbruik van sociale media te voorkomen. humane_approach_title: Een meer menselijke aanpak - not_a_product_body: Mastodon is geen commercieel netwerk. Dus geen advertenties, geen datamining en geen besloten systemen. Er is geen centrale organisatie die alles bepaald. + not_a_product_body: Mastodon is geen commercieel netwerk. Dus geen advertenties, geen datamining en geen besloten systemen. Er is geen centrale organisatie die alles bepaalt. not_a_product_title: Jij bent een persoon, geen product - real_conversation_body: Met 500 karakters tot jouw beschikking, en ondersteuning voor tekst- en media-waarschuwingen, kan je jezelf uiten zoals jij dat wil. + real_conversation_body: Met 500 tekens tot jouw beschikking en ondersteuning voor tekst- en media-waarschuwingen, kan je jezelf uiten zoals jij dat wil. real_conversation_title: Voor echte gesprekken gemaakt within_reach_body: Meerdere apps voor iOS, Android en andere platformen, met dank aan het ontwikkelaarsvriendelijke API-systeem, zorgen ervoor dat je overal op de hoogte blijft. within_reach_title: Altijd binnen bereik @@ -40,22 +40,22 @@ nl: following: Volgend media: Media moved_html: "%{name} is verhuisd naar %{new_profile_link}:" + network_hidden: Deze informatie is niet beschikbaar nothing_here: Hier is niets! - people_followed_by: Mensen die %{name} volgt + people_followed_by: Mensen die %{name} volgen people_who_follow: Mensen die %{name} volgen posts: Toots - posts_with_replies: Toots met reacties + posts_with_replies: Toots en reacties remote_follow: Extern volgen reserved_username: Deze gebruikersnaam is gereserveerd roles: admin: Beheerder - moderator: Mod + bot: Bot + moderator: Moderator unfollow: Ontvolgen admin: account_moderation_notes: - account: Moderator - create: Aanmaken - created_at: Datum + create: Laat een opmerking achter created_msg: Aanmaken van opmerking voor moderatoren geslaagd! delete: Verwijderen destroyed_msg: Verwijderen van opmerking voor moderatoren geslaagd! @@ -66,12 +66,13 @@ nl: change_email: changed_msg: E-mailadres van account succesvol veranderd! current_email: Huidig e-mailadres - label: E-mailadres veranderen + label: E-mailadres wijzigen new_email: Nieuw e-mailadres submit: E-mailadres veranderen - title: E-mailadres veranderen voor %{username} + title: E-mailadres wijzigen voor %{username} confirm: Bevestigen confirmed: Bevestigd + confirming: Bevestiging demote: Degraderen disable: Uitschakelen disable_two_factor_authentication: 2FA uitschakelen @@ -80,6 +81,7 @@ nl: domain: Domein edit: Bewerken email: E-mail + email_status: E-mail Status enable: Inschakelen enabled: Ingeschakeld feed_url: Feed-URL @@ -93,9 +95,9 @@ nl: local: Lokaal remote: Extern title: Locatie - login_status: Aanmeldstatus + login_status: Login status media_attachments: Mediabijlagen - memorialize: Verander naar in memoriam + memorialize: In gedenkpagina veranderen moderation: all: Alles silenced: Genegeerd @@ -112,16 +114,20 @@ nl: outbox_url: Outbox-URL perform_full_suspension: Volledig opschorten profile_url: Profiel-URL - promote: Promoten + promote: Promoveren protocol: Protocol public: Openbaar push_subscription_expires: PuSH-abonnement verloopt op redownload: Avatar vernieuwen remove_avatar: Avatar verwijderen + resend_confirmation: + already_confirmed: Deze gebruiker is al bevestigd + send: Verzend bevestigingsmail opnieuw + success: Bevestigingsmail succesvol verzonden! reset: Opnieuw reset_password: Wachtwoord opnieuw instellen resubscribe: Opnieuw abonneren - role: Permissies + role: Bevoegdheden roles: admin: Beheerder moderator: Moderator @@ -161,7 +167,7 @@ nl: disable_user: Aanmelden voor %{target} is door %{name} uitgeschakeld enable_custom_emoji: Emoji %{target} is door %{name} ingeschakeld enable_user: Inloggen voor %{target} is door %{name} ingeschakeld - memorialize_account: Account %{target} is door %{name} in een in-memoriampagina veranderd + memorialize_account: Account %{target} is door %{name} in een gedenkpagina veranderd promote_user: Gebruiker %{target} is door %{name} gepromoveerd remove_avatar_user: "%{name} verwijderde de avatar van %{target}" reopen_report: "%{name} heeft gerapporteerde toot %{target} heropend" @@ -220,7 +226,7 @@ nl: noop: Geen silence: Negeren suspend: Opschorten - severity: Strengheid + severity: Zwaarte show: affected_accounts: one: Eén account in de database aangepast @@ -269,7 +275,6 @@ nl: comment: none: Geen created_at: Gerapporteerd op - delete: Verwijderen id: ID mark_as_resolved: Markeer als opgelost mark_as_unresolved: Markeer als onopgelost @@ -278,10 +283,7 @@ nl: create_and_resolve: Oplossen met opmerking create_and_unresolve: Heropenen met opmerking delete: Verwijderen - placeholder: Beschrijf welke acties zijn ondernomen of andere opmerkingen over deze gerapporteerde toot… - nsfw: - 'false': Media tonen - 'true': Media verbergen + placeholder: Beschrijf welke acties zijn ondernomen of andere gerelateerde opmerkingen… reopen: Gerapporteerde toot heropenen report: 'Gerapporteerde toot #%{id}' report_contents: Inhoud @@ -356,11 +358,8 @@ nl: delete: Verwijderen nsfw_off: Als niet gevoelig markeren nsfw_on: Als gevoelig markeren - execute: Uitvoeren failed_to_execute: Uitvoeren mislukt media: - hide: Media verbergen - show: Media tonen title: Media no_media: Geen media title: Toots van account @@ -376,6 +375,7 @@ nl: admin_mailer: new_report: body: "%{reporter} heeft %{target} gerapporteerd" + body_remote: Iemand van %{domain} heeft %{target} gerapporteerd subject: Nieuwe toots gerapporteerd op %{instance} (#%{id}) application_mailer: notification_preferences: E-mailvoorkeuren wijzigen @@ -465,7 +465,7 @@ nl: archive_takeout: date: Datum download: Jouw archief downloaden - hint_html: Je kunt een archief opvragen van jouw toots en geüploade media. De geëxporteerde gegevens zijn in ActivityPub-formaat, dat door hiervoor geschikte software valt uit te lezen. + hint_html: Je kunt een archief opvragen van jouw toots en geüploade media. De geëxporteerde gegevens zijn in ActivityPub-formaat, dat door hiervoor geschikte software valt uit te lezen. Je kunt elke 7 dagen een kopie van je archief aanvragen. in_progress: Jouw archief wordt samengesteld... request: Jouw archief opvragen size: Omvang @@ -550,7 +550,7 @@ nl: subject: one: "1 nieuwe melding sinds jouw laatste bezoek \U0001F418" other: "%{count} nieuwe meldingen sinds jouw laatste bezoek \U0001F418" - title: Tijdens jouw afwezigheid… + title: Tijdens jouw afwezigheid... favourite: body: 'Jouw toot werd door %{name} als favoriet gemarkeerd:' subject: "%{name} markeerde jouw toot als favoriet" @@ -595,20 +595,6 @@ nl: other: Overig publishing: Publiceren web: Webapp - push_notifications: - favourite: - title: "%{name} markeerde jouw toot als favoriet" - follow: - title: "%{name} volgt jou nu" - group: - title: "%{count} meldingen" - mention: - action_boost: Boost - action_expand: Meer tonen - action_favourite: Favoriet - title: "%{name} vermeldde jou" - reblog: - title: "%{name} boostte jouw toot" remote_follow: acct: Geef jouw account@domein.tld op waarvandaan je wilt volgen missing_resource: Kon vereiste doorverwijzings-URL voor jouw account niet vinden @@ -683,6 +669,7 @@ nl: video: one: "%{count} video" other: "%{count} video's" + boosted_from_html: Geboost van %{acct_link} content_warning: 'Tekstwaarschuwing: %{warning}' disallowed_hashtags: one: 'bevatte een niet toegestane hashtag: %{tags}' @@ -773,9 +760,13 @@ nl: - Children's Online Privacy Protection Act Compliance + Site usage by children - Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site. + If this server is in the EU or the EEA: Our site, products and services are all directed to people who are at least 16 years old. If you are under the age of 16, per the requirements of the GDPR (General Data Protection Regulation) do not use this site. + + If this server is in the USA: Our site, products and services are all directed to people who are at least 13 years old. If you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site. + + Law requirements can be different if this server is in another jurisdiction. @@ -832,5 +823,6 @@ nl: users: invalid_email: E-mailadres is ongeldig invalid_otp_token: Ongeldige tweestaps-aanmeldcode + otp_lost_help_html: Als je toegang tot beiden kwijt bent geraakt, neem dan contact op via %{email} seamless_external_login: Je bent ingelogd via een externe dienst, daarom zijn wachtwoorden en e-mailinstellingen niet beschikbaar. signed_in_as: 'Ingelogd als:' diff --git a/config/locales/no.yml b/config/locales/no.yml index 8b84182af6..eb1d27a199 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -52,9 +52,7 @@ unfollow: Slutte følge admin: account_moderation_notes: - account: Moderator create: Lag - created_at: Dato created_msg: Moderasjonsnotat laget uten problem! delete: Slett destroyed_msg: Moderasjonsnotat slettet uten problem! @@ -63,6 +61,7 @@ by_domain: Domene confirm: Bekreft confirmed: Bekreftet + confirming: Bekrefte demote: Degrader disable: Deaktiver disable_two_factor_authentication: Skru av 2FA @@ -71,6 +70,7 @@ domain: Domene edit: Redigér email: E-post + email_status: E-poststatus enable: Aktiver enabled: Aktivert feed_url: Feed-URL @@ -108,6 +108,10 @@ public: Offentlig push_subscription_expires: PuSH-abonnent utløper redownload: Oppdater avatar + resend_confirmation: + already_confirmed: Denne brukeren er allerede bekreftet + send: Send bekreftelses-epost på nytt + success: Bekreftelses e-post er vellykket sendt! reset: Tilbakestill reset_password: Nullstill passord resubscribe: Abonner på nytt @@ -244,12 +248,8 @@ are_you_sure: Er du sikker? comment: none: Ingen - delete: Slett id: ID mark_as_resolved: Merk som løst - nsfw: - 'false': Vis mediavedlegg - 'true': Skjul mediavedlegg report: 'Rapportér #%{id}' report_contents: Innhold reported_account: Rapportert konto @@ -314,11 +314,8 @@ delete: Slett nsfw_off: NSFW AV nsfw_on: NSFW PÅ - execute: Utfør failed_to_execute: Utføring mislyktes media: - hide: Skjul media - show: Vis media title: Media no_media: Ingen media title: Kontostatuser @@ -534,20 +531,6 @@ other: Annet publishing: Publisering web: Web - push_notifications: - favourite: - title: "%{name} favoriserte din status" - follow: - title: "%{name} følger deg nå" - group: - title: "%{count} varslinger" - mention: - action_boost: Fremhev - action_expand: Vis mer - action_favourite: Favoritter - title: "%{name} nevnte deg" - reblog: - title: "%{name} fremhevde din status" remote_follow: acct: Tast inn brukernavn@domene som du vil følge fra missing_resource: Kunne ikke finne URLen for din konto diff --git a/config/locales/oc.yml b/config/locales/oc.yml index d5717c0b57..faf4f6d17c 100644 --- a/config/locales/oc.yml +++ b/config/locales/oc.yml @@ -40,22 +40,22 @@ oc: following: Abonaments media: Mèdias moved_html: "%{name} a mudat a %{new_profile_link} :" + network_hidden: Aquesta informacion es pas disponibla nothing_here: I a pas res aquí ! - people_followed_by: Lo mond que %{name} sèc - people_who_follow: Lo mond que sègon %{name} + people_followed_by: Lo monde que %{name} sèc + people_who_follow: Lo monde que sègon %{name} posts: Tuts posts_with_replies: Tuts e responsas remote_follow: Sègre a distància reserved_username: Aqueste nom d’utilizaire es reservat roles: admin: Admin - moderator: Mod + bot: Robòt + moderator: Moderador unfollow: Quitar de sègre admin: account_moderation_notes: - account: Moderator - create: Crear - created_at: Data + create: Crear una nòta created_msg: Nòta de moderacion ben creada ! delete: Suprimir destroyed_msg: Nòta de moderacion ben suprimida ! @@ -72,6 +72,7 @@ oc: title: Cambiar l’adreça a %{username} confirm: Confirmar confirmed: Confirmat + confirming: Confirmacion demote: Retrogradar disable: Desactivar disable_two_factor_authentication: Desactivar 2FA @@ -80,6 +81,7 @@ oc: domain: Domeni edit: Modificar email: Corrièl + email_status: Estat de l’adreça enable: Activar enabled: Activat feed_url: Flux URL @@ -118,13 +120,18 @@ oc: push_subscription_expires: Fin de l’abonament PuSH redownload: Actualizar los avatars remove_avatar: Supriir l’avatar + resend_confirmation: + already_confirmed: Aqueste utilizaire es ja confirmat + send: Tornar mandar lo corrièl de confirmacion + success: Corrièl de confirmacion corrèctament mandat ! reset: Reïnicializar reset_password: Reïnicializar lo senhal resubscribe: Se tornar abonar role: Permissions roles: admin: Administrator - moderator: Moderator + bot: Robòt + moderator: Moderador staff: Personnal user: Uitlizaire salmon_url: URL Salmon @@ -164,10 +171,12 @@ oc: memorialize_account: "%{name} transformèt en memorial la pagina de perfil a %{target}" promote_user: "%{name} promoguèt %{target}" remove_avatar_user: "%{name} suprimèt l’avatar a %{target}" + reopen_report: "%{name} tornèt dobrir lo rapòrt %{target}" reset_password_user: "%{name} reïnicializèt lo senhal a %{target}" - resolve_report: "%{name} anullèt lo rapòrt de %{target}" + resolve_report: "%{name} anullèt lo rapòrt %{target}" silence_account: "%{name} metèt en silenci lo compte a %{target}" suspend_account: "%{name} susprenguèt lo compte a %{target}" + unassigned_report: "%{name} daissèt de tractar lo rapòrt %{target}" unsilence_account: "%{name} levèt lo silenci del compte a %{target}" unsuspend_account: "%{name} restabliguèt lo compte a %{target}" update_custom_emoji: "%{name} metèt a jorn l’emoji %{target}" @@ -262,10 +271,11 @@ oc: report: rapòrt action_taken_by: Mesura menada per are_you_sure: Es segur ? + assign_to_self: Me l’assignar + assigned: Moderador assignat comment: none: Pas cap created_at: Creacion - delete: Suprimir id: ID mark_as_resolved: Marcar coma resolgut mark_as_unresolved: Marcar coma pas resolgut @@ -273,22 +283,23 @@ oc: create: Ajustar una nòta create_and_resolve: Resòlvre amb una nòta create_and_unresolve: Tornar dobrir amb una nòta - placeholder: Explicatz las accions que son estadas menadas o çò qu’es estat fach per aqueste rapòrt… - nsfw: - 'false': Sens contengut sensible - 'true': Contengut sensible activat + delete: Escafar + placeholder: Explicatz las accions que son estadas menadas o quicòm de ligat al senhalament… reopen: Tornar dobrir lo rapòrt report: 'senhalament #%{id}' - report_contents: Contenguts + report_contents: Contengut reported_account: Compte senhalat reported_by: Senhalat per resolved: Resolgut + resolved_msg: Rapòrt corrèctament resolgut ! silence_account: Metre lo compte en silenci status: Estatut suspend_account: Suspendre lo compte target: Cibla title: Senhalament + unassign: Levar unresolved: Pas resolguts + updated_at: Actualizat view: Veire settings: activity_api_enabled: @@ -311,13 +322,13 @@ oc: desc_html: Afichat sus las pagina d’acuèlh quand las inscripcions son tampadas.Podètz utilizar de balisas HTML title: Messatge de barradura de las inscripcions deletion: - desc_html: Autorizar lo mond a suprimir lor compte + desc_html: Autorizar lo monde a suprimir lor compte title: Possibilitat de suprimir lo compte min_invite_role: disabled: Degun title: Autorizat amb invitacions open: - desc_html: Autorizar lo mond a se marcar + desc_html: Autorizar lo monde a se marcar title: Inscripcions show_known_fediverse_at_about_page: desc_html: Un còp activat mostrarà los tuts de totes los fediverse dins l’apercebut. Autrament mostrarà pas que los tuts locals. @@ -346,15 +357,12 @@ oc: back_to_account: Tornar a la pagina Compte batch: delete: Suprimir - nsfw_off: NSFW OFF - nsfw_on: NSFW ON - execute: Lançar + nsfw_off: Marcar coma pas sensible + nsfw_on: Marcar coma sensible failed_to_execute: Fracàs media: - hide: Amagar mèdia - show: Mostrar mèdia title: Mèdia - no_media: Cap mèdia + no_media: Cap de mèdia title: Estatuts del compte with_media: Amb mèdia subscriptions: @@ -368,6 +376,7 @@ oc: admin_mailer: new_report: body: "%{reporter} a senhalat %{target}" + body_remote: Qualqu’un de %{domain} senhalèt %{target} subject: Novèl senhalament per %{instance} (#%{id}) application_mailer: notification_preferences: Cambiar las preferéncias de corrièl @@ -490,9 +499,7 @@ oc: less_than_x_minutes: one: Fa mens d’una minuta other: Fa mens de %{count} minutas - less_than_x_seconds: - one: Fa mens d’una segonda - other: Fa mens de %{count} segondas + less_than_x_seconds: Ara meteis over_x_years: one: Fa mai d’un an other: Fa mai de %{count} ans @@ -529,13 +536,13 @@ oc: '429': Lo servidor mòla (subrecargada) '500': content: Un quicomet a pas foncionat coma caliá. - title: Aquesta pagina es incorrècta + title: Aquesta pagina es pas corrècta noscript_html: Per utilizar l’aplicacion web de Mastodon, mercés d’activar JavaScript. O podètz utilizar una aplicacion per vòstra plataforma coma alernativa. exports: archive_takeout: date: Data download: Telecargar vòstre archiu - hint_html: Podètz demandar un archiu de vòstres tuts e mèdias enviats. Las donadas exportadas seràn al format ActivityPub, ligible pels logicials compatibles. + hint_html: Podètz demandar un archiu de vòstres tuts e mèdias enviats. Las donadas exportadas seràn al format ActivityPub, ligible pels logicials compatibles. Podètz demandar un archiu cada 7 jorns. in_progress: Complilacion de vòstre archiu... request: Demandar vòstre archiu size: Talha @@ -546,7 +553,7 @@ oc: storage: Mèdias gardats followers: domain: Domeni - explanation_html: Se volètz vos assegurar de la confidencialitat de vòstres estatuts, vos cal saber qual sèc vòstre compte. Vòstres estatuts privats son enviats a totas las instàncias qu’an de mond que vos sègon.. Benlèu que volètz repassar vòstra lista e tirar los seguidors s’avètz de dobtes tocant las politica de confidencialitat de lor instàncias. + explanation_html: Se volètz vos assegurar de la confidencialitat de vòstres estatuts, vos cal saber qual sèc vòstre compte. Vòstres estatuts privats son enviats a totas las instàncias qu’an de monde que vos sègon.. Benlèu que volètz repassar vòstra lista e tirar los seguidors s’avètz de dobtes tocant las politicas de confidencialitat dels gestionaris de lor instància o sul logicial qu’utilizan. followers_count: Nombre de seguidors lock_link: Clavar vòstre compte purge: Tirar dels seguidors @@ -554,22 +561,22 @@ oc: one: Soi a blocar los seguidors d’un domeni… other: Soi a blocar los seguidors de %{count} domenis… true_privacy_html: Mèfi que la vertadièra confidencialitat pòt solament èsser amb un chiframent del cap a la fin (end-to-end). - unlocked_warning_html: Tot lo mond pòt vos sègre e veire sulpic vòstres estatuts privats. %{lock_link} per poder repassar e regetar los seguidors. + unlocked_warning_html: Tot lo monde pòt vos sègre e veire sulpic vòstres estatuts privats. %{lock_link} per poder repassar e regetar los seguidors. unlocked_warning_title: Vòstre compte es pas clavat generic: changes_saved_msg: Cambiaments ben realizats ! powered_by: propulsat per %{link} - save_changes: Salvagardar los cambiaments + save_changes: Salvar los cambiaments validation_errors: one: I a quicòm que truca ! Mercés de corregir l’error çai-jos other: I a quicòm que truca ! Mercés de corregir las %{count} errors çai-jos imports: - preface: Podètz importar qualques donadas coma lo mond que seguètz o blocatz a-n aquesta instància d’un fichièr creat d’una autra instància. + preface: Podètz importar qualques donadas coma lo monde que seguètz o blocatz a-n aquesta instància d’un fichièr creat d’una autra instància. success: Vòstras donadas son ben estadas mandadas e seràn tractadas tre que possible types: blocking: Lista de blocatge - following: Lista de mond que seguètz - muting: Lista de mond que volètz pas legir + following: Lista de monde que seguètz + muting: Lista de monde que volètz pas legir upload: Importar in_memoriam_html: En Memòria. invites: @@ -655,9 +662,9 @@ oc: trillion: T unit: '' pagination: - newer: Mai recent + newer: Mai recents next: Seguent - older: Mai ancian + older: Mai ancians prev: Precedent truncate: "…" preferences: @@ -665,20 +672,6 @@ oc: other: Autre publishing: Publicar web: Interfàcia Web - push_notifications: - favourite: - title: "%{name} a mes vòstre estatut en favorit" - follow: - title: "%{name} vos sèc ara" - group: - title: "%{count} notificacions" - mention: - action_boost: Partejar - action_expand: Ne veire mai - action_favourite: Ajustar als favorits - title: "%{name} vos a mencionat" - reblog: - title: "%{name} a partejat vòstre estatut" remote_follow: acct: Picatz vòstre utilizaire@instància que cal utilizar per sègre aqueste utilizaire missing_resource: URL de redireccion pas trobada @@ -753,6 +746,8 @@ oc: video: one: "%{count} vidèo" other: "%{count} vidèos" + boosted_from_html: Partejat de %{acct_link} + content_warning: 'Avertiment de contengut : %{warning}' disallowed_hashtags: one: 'conten una etiqueta desactivada : %{tags}' other: 'conten las etiquetas desactivadas : %{tags}' @@ -769,9 +764,9 @@ oc: private: Seguidors solament private_long: Mostrar pas qu’als seguidors public: Public - public_long: Tot lo mond pòt veire + public_long: Tot lo monde pòt veire unlisted: Pas listat - unlisted_long: Tot lo mond pòt veire mai serà pas visible sul flux public + unlisted_long: Tot lo monde pòt veire mai serà pas visible sul flux public stream_entries: click_to_show: Clicatz per veire pinned: Tut penjat @@ -797,7 +792,7 @@ oc: recovery_codes_regenerated: Los còdis de recuperacion son ben estats tornats generar recovery_instructions_html: Se vos arriba de perdre vòstre mobil, podètz utilizar un dels còdis de recuperacion cai-jos per poder tornar accedir a vòstre compte. Gardatz los còdis en seguretat, per exemple, imprimissètz los e gardatz los amb vòstres documents importants. setup: Paramètres - wrong_code: Lo còdi picat es invalid ! L’ora es la bona sul servidor e lo mobil ? + wrong_code: Lo còdi picat es invalid ! L’ora es bona sul servidor e lo mobil ? user_mailer: backup_ready: explanation: Avètz demandat una salvagarda complèta de vòstre compte Mastodon. Es prèsta per telecargament ! @@ -815,14 +810,15 @@ oc: review_preferences_step: Pensatz de configurar vòstras preferéncias, tal coma los corrièls que volètz recebrer o lo nivèl de confidencialitat de vòstres tuts per defaut. O se l’animacion vos dòna pas enveja de rendre, podètz activar la lectura automatica dels GIF. subject: Benvengut a Mastodon tip_bridge_html: Se venètz de Twitter, podètz trobar vòstres amics sus Mastodon en utilizant l‘aplicacion de Pont. Aquò fonciona pas que s’utilizan lo Pont tanben ! - tip_federated_timeline: Lo flux d’actualitat federat es una vista generala del malhum Mastodon. Mas aquò inclutz solament lo mond que vòstres vesins sègon, doncas es pas complèt. - tip_following: Seguètz l’administrator del servidor per defaut. Per trobar de mond mai interessant, agachatz lo flux d’actualitat local e lo global. - tip_local_timeline: Lo flux d’actualitat local es una vista del mond de %{instance}. Son vòstres vesins dirèctes ! + tip_federated_timeline: Lo flux d’actualitat federat es una vista generala del malhum Mastodon. Mas aquò inclutz solament lo monde que vòstres vesins sègon, doncas es pas complèt. + tip_following: Seguètz l’administrator del servidor per defaut. Per trobar de monde mai interessant, agachatz lo flux d’actualitat local e lo global. + tip_local_timeline: Lo flux d’actualitat local es una vista del monde de %{instance}. Son vòstres vesins dirèctes ! tip_mobile_webapp: Se vòstre navigator mobil nos permet d’apondre Mastodon a l’ecran d‘acuèlh, podètz recebre de notificacions. Aquò se compòrta coma una aplicacion nativa ! tips: Astúcias title: Vos desirem la benvenguda a bòrd %{name} ! users: invalid_email: L’adreça de corrièl es invalida invalid_otp_token: Còdi d’autentificacion en dos temps invalid + otp_lost_help_html: Se perdatz l’accès al dos, podètz benlèu contactar %{email} seamless_external_login: Sètz connectat via un servici extèrn, los paramètres de senhal e de corrièl son doncas pas disponibles. signed_in_as: 'Session a :' diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 519207d38b..ac585368a0 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -36,10 +36,11 @@ pl: what_is_mastodon: Czym jest Mastodon? accounts: follow: Śledź - followers: Śledzących - following: Śledzi + followers: Śledzący + following: Śledzeni media: Zawartość multimedialna moved_html: "%{name} korzysta teraz z konta %{new_profile_link}:" + network_hidden: Ta informacja nie jest dostępna nothing_here: Niczego tu nie ma! people_followed_by: Konta śledzone przez %{name} people_who_follow: Osoby, które śledzą konto %{name} @@ -49,13 +50,12 @@ pl: reserved_username: Ta nazwa użytkownika jest zarezerwowana roles: admin: Administrator + bot: Bot moderator: Moderator unfollow: Przestań śledzić admin: account_moderation_notes: - account: Autor - create: Dodaj - created_at: Data + create: Pozostaw notatkę created_msg: Pomyślnie dodano notatkę moderacyjną! delete: Usuń destroyed_msg: Pomyślnie usunięto notatkę moderacyjną! @@ -72,6 +72,7 @@ pl: title: Zmień adres e-mail dla %{username} confirm: Potwierdź confirmed: Potwierdzono + confirming: Potwierdzanie demote: Degraduj disable: Dezaktywuj disable_two_factor_authentication: Wyłącz uwierzytelnianie dwuetapowe @@ -80,6 +81,7 @@ pl: domain: Domena edit: Edytuj email: Adres e-mail + email_status: Stan e-maila enable: Aktywuj enabled: Aktywowano feed_url: Adres kanału @@ -118,6 +120,10 @@ pl: push_subscription_expires: Subskrypcja PuSH wygasa redownload: Odśwież awatar remove_avatar: Usun awatar + resend_confirmation: + already_confirmed: To konto zostało już potwierdzone + send: Wyślij ponownie e-mail z potwierdzeniem + success: E-mail z potwierdzeniem został wysłany! reset: Resetuj reset_password: Resetuj hasło resubscribe: Ponów subskrypcję @@ -270,7 +276,6 @@ pl: comment: none: Brak created_at: Zgłoszono - delete: Usuń id: ID mark_as_resolved: Oznacz jako rozwiązane mark_as_unresolved: Oznacz jako nierozwiązane @@ -280,9 +285,6 @@ pl: create_and_unresolve: Cofnij rozwiązanie i pozostaw notatkę delete: Usuń placeholder: Opisz wykonane akcje i inne szczegóły dotyczące tego zgłoszenia… - nsfw: - 'false': Nie oznaczaj jako NSFW - 'true': Oznaczaj jako NSFW reopen: Otwórz ponownie report: 'Zgłoszenie #%{id}' report_contents: Zawartość @@ -357,11 +359,8 @@ pl: delete: Usuń nsfw_off: Cofnij NSFW nsfw_on: Oznacz jako NSFW - execute: Wykonaj failed_to_execute: Nie udało się wykonać media: - hide: Ukryj zawartość multimedialną - show: Pokaż zawartość multimedialną title: Media no_media: Bez zawartości multimedialnej title: Wpisy konta @@ -377,6 +376,7 @@ pl: admin_mailer: new_report: body: Użytkownik %{reporter} zgłosił %{target} + body_remote: Użytkownik instancji %{domain} zgłosił %{target} subject: Nowe zgłoszenie na %{instance} (#%{id}) application_mailer: notification_preferences: Zmień ustawienia e-maili @@ -466,7 +466,7 @@ pl: archive_takeout: date: Data download: Pobierz swoje archiwum - hint_html: Możesz uzyskać archiwum swoich wpisów i wysłanej zawartości multimedialnej. Wyeksportowane dane będą dostępne w formacie ActivityPub, który możesz otworzyć w obsługujących go programach. + hint_html: Możesz uzyskać archiwum swoich wpisów i wysłanej zawartości multimedialnej. Wyeksportowane dane będą dostępne w formacie ActivityPub, który możesz otworzyć w obsługujących go programach. Możesz wyeksportować je po 7 dniach od poprzedniego eksportu. in_progress: Tworzenie archiwum… request: Uzyskaj archiwum size: Rozmiar @@ -613,20 +613,6 @@ pl: other: Pozostałe publishing: Publikowanie web: Sieć - push_notifications: - favourite: - title: "%{name} dodał Twój wpis do ulubionych" - follow: - title: "%{name} zaczął Cię śledzić" - group: - title: "%{count} powiadomień" - mention: - action_boost: Podbij - action_expand: Pokaż więcej - action_favourite: Dodaj do ulubionych - title: "%{name} wspomniał o Tobie" - reblog: - title: "%{name} podbił Twój wpis" remote_follow: acct: Podaj swój adres (nazwa@domena), z którego chcesz śledzić missing_resource: Nie udało się znaleźć adresu przekierowania z Twojej domeny @@ -696,7 +682,7 @@ pl: your_apps: Twoje aplikacje statuses: attached: - description: 'Przytwierdzony: %{attached}' + description: 'Załączono: %{attached}' image: few: "%{count} obrazy" many: "%{count} obrazów" @@ -707,6 +693,7 @@ pl: many: "%{count} filmów" one: "%{count} film" other: "%{count} filmów" + boosted_from_html: Podbito przez %{acct_link} content_warning: 'Ostrzeżenie o zawartości: %{warning}' disallowed_hashtags: one: 'zawiera niedozwolony hashtag: %{tags}' @@ -856,5 +843,6 @@ pl: users: invalid_email: Adres e-mail jest niepoprawny invalid_otp_token: Kod uwierzytelniający jest niepoprawny + otp_lost_help_html: Jeżeli utracisz dostęp do obu, możesz skontaktować się z %{email} seamless_external_login: Zalogowano z użyciem zewnętrznej usługi, więc ustawienia hasła i adresu e-mail nie są dostępne. signed_in_as: 'Zalogowany jako:' diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index a575998a8d..89cc26cf36 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -40,6 +40,7 @@ pt-BR: following: Seguindo media: Mídia moved_html: "%{name} se mudou para %{new_profile_link}:" + network_hidden: Essa informação não está disponível nothing_here: Não há nada aqui! people_followed_by: Pessoas que %{name} segue people_who_follow: Pessoas que seguem %{name} @@ -49,13 +50,12 @@ pt-BR: reserved_username: Este usuário está reservado roles: admin: Administrador + bot: Robô moderator: Moderador unfollow: Deixar de seguir admin: account_moderation_notes: - account: Moderador - create: Criar - created_at: Data + create: Criar uma advertência created_msg: Nota de moderação criada com sucesso! delete: Excluir destroyed_msg: Nota de moderação excluída com sucesso! @@ -72,6 +72,7 @@ pt-BR: title: Mudar e-mail para %{username} confirm: Confirmar confirmed: Confirmado + confirming: Confirmando demote: Rebaixar disable: Desativar disable_two_factor_authentication: Desativar 2FA @@ -80,6 +81,7 @@ pt-BR: domain: Domínio edit: Editar email: E-mail + email_status: Estado del correo electrónico enable: Ativar enabled: Ativado feed_url: URL do feed @@ -118,6 +120,10 @@ pt-BR: push_subscription_expires: Inscrição PuSH expira redownload: Atualizar avatar remove_avatar: Remover avatar + resend_confirmation: + already_confirmed: Este usuario ya está confirmado + send: Reenviar el correo electrónico de confirmación + success: "¡Correo electrónico de confirmación enviado con éxito!" reset: Anular reset_password: Modificar senha resubscribe: Reinscrever-se @@ -269,7 +275,6 @@ pt-BR: comment: none: Nenhum created_at: Denunciado - delete: Excluir id: ID mark_as_resolved: Marcar como resolvido mark_as_unresolved: Marcar como não resolvido @@ -278,10 +283,7 @@ pt-BR: create_and_resolve: Resolver com nota create_and_unresolve: Reabrir com nota delete: Excluir - placeholder: Descreva que ações foram tomadas, ou quaisquer atualizações sobre esta denúncia… - nsfw: - 'false': Mostrar mídias anexadas - 'true': Esconder mídias anexadas + placeholder: Descreva que ações foram tomadas, ou quaisquer outras atualizações relacionadas… reopen: Reabrir denúncia report: 'Denúncia #%{id}' report_contents: Conteúdos @@ -356,11 +358,8 @@ pt-BR: delete: Deletar nsfw_off: Marcar como não-sensível nsfw_on: Marcar como sensível - execute: Executar failed_to_execute: Falha em executar media: - hide: Esconder mídia - show: Mostrar mídia title: Mídia no_media: Não há mídia title: Postagens da conta @@ -376,6 +375,7 @@ pt-BR: admin_mailer: new_report: body: "%{reporter} denunciou %{target}" + body_remote: Alguém da instância %{domain} reportou %{target} subject: Nova denúncia sobre %{instance} (#%{id}) application_mailer: notification_preferences: Mudar preferências de e-mail @@ -465,7 +465,7 @@ pt-BR: archive_takeout: date: Data download: Baixe o seu arquivo - hint_html: Você pode pedir um arquivo dos seus toots e mídia enviada. Os dados exportados estarão no formato ActivityPub, que podem ser lidos por qualquer software compatível. + hint_html: Você pode pedir um arquivo dos seus toots e mídia enviada. Os dados exportados estarão no formato ActivityPub, que podem ser lidos por qualquer software compatível. Você pode pedir um arquivo a cada 7 dias. in_progress: Preparando seu arquivo... request: Solicitar o seu arquivo size: Tamanho @@ -550,7 +550,7 @@ pt-BR: subject: one: "Uma nova notificação desde o seu último acesso \U0001F418" other: "%{count} novas notificações desde o seu último acesso \U0001F418" - title: Enquanto você estava ausente… + title: Enquanto você estava ausente... favourite: body: 'Sua postagem foi favoritada por %{name}:' subject: "%{name} favoritou a sua postagem" @@ -595,20 +595,6 @@ pt-BR: other: Outro publishing: Publicação web: Web - push_notifications: - favourite: - title: "%{name} favoritou a sua postagem" - follow: - title: "%{name} está te seguindo" - group: - title: "%{count} notificações" - mention: - action_boost: Compartilhar - action_expand: Mostrar mais - action_favourite: Favoritar - title: "%{name} mencionou você" - reblog: - title: "%{name} compartilhou a sua postagem" remote_follow: acct: Insira o seu usuário@domínio do qual você quer seguir missing_resource: Não foi possível encontrar a URL de direcionamento para a sua conta @@ -682,6 +668,7 @@ pt-BR: video: one: "%{count} vídeo" other: "%{count} vídeos" + boosted_from_html: Compartilhada à partir de %{acct_link} content_warning: 'Aviso de conteúdo: %{warning}' disallowed_hashtags: one: 'continha a hashtag não permitida: %{tags}' @@ -714,77 +701,77 @@ pt-BR: Informação básica de conta: Se você se registrar nesse servidor, podemos pedir que você utilize um nome de usuário, um e-mail e uma senha. Você também pode adicionar informações extras como um nome de exibição e biografia; enviar uma imagem de perfil e imagem de cabeçalho. O nome de usuário, nome de exibição, biografia, imagem de perfil e imagem de cabeçalho são sempre listadas publicamente. - Posts, informação de seguidores e outras informações públicas: A lista de pessoas que você segue é listada publicamente, o mesmo é verdade para quem te segue. Quando você envia uma mensagem, a data e o horário são armazenados, assim como a aplicação que você usou para enviar a mensagem. Mensagens podem conter mídias anexadas, como imagens e vídeos. Posts públicos e não-listados estão disponíveis publicamente. Quando você destaca um post no seu perfil, isso também é uma informação pública. Seus posts são entregues aos seus seguidores e em alguns casos isso significa que eles são enviados para servidores diferentes e cópias são armazenadas nesses servidores. Quando você remove posts, essa informação também é entregue aos seus seguidores. O ato de compartilhar ou favoritar um outro post é sempre público. + Posts, informação de seguidores e outras informações públicas: A lista de pessoas que você segue é listada publicamente, o mesmo é verdade para quem te segue. Quando você envia uma mensagem, a data e o horário são armazenados, assim como a aplicação que você usou para enviar a mensagem. Mensagens podem conter mídias anexadas, como imagens e vídeos. Posts públicos e não-listados estão disponíveis publicamente. Quando você destaca um post no seu perfil, isso também é uma informação pública. Seus posts são entregues aos seus seguidores e em alguns casos isso significa que eles são enviados para servidores diferentes e cópias são armazenadas nesses servidores. Quando você remove posts, essa informação também é entregue aos seus seguidores. O ato de compartilhar ou favoritar um outro post é sempre público. Mensagens diretas e posts somente para seguidores: Todos os posts são armazenados e processados no servidor. Posts somente para seguidores são entregues aos seus seguidores e usuários que são mencionados neles; mensagens diretas são entregues somente aos usuários mencionados nelas. Em alguns casos isso significa que as mensagens são entregues para servidores diferentes e cópias são armazenadas nesses servidores. Nós fazemos esforços substanciais para limitar o acesso dessas mensagens somente para as pessoas autorizadas, mas outros servidores podem não fazer o mesmo. É importante portanto revisar os servidores à qual seus seguidores pertencem. Você pode usar uma opção para aprovar ou rejeitar novos seguidores manualmente nas configurações. Por favor tenha em mente que os operadores do servidor e de qualquer servidores do destinatário podem ver tais mensagens, e que os destinatários podem fazer capturas de tela, copiar ou de outra maneira compartilhar as mensagens. Não compartilhe informação confidencial pelo Mastodon. - IPs and other metadata: When you log in, we record the IP address you log in from, as well as the name of your browser application. All the logged in sessions are available for your review and revocation in the settings. The latest IP address used is stored for up to 12 months. We also may retain server logs which include the IP address of every request to our server. + IPs e outros metadados: Quando você faz se autentica, nos guardamos o endereço de IP que você usou ao se autenticar e o nome do seu navegador da internet. Todas as sessões autenticadas são disponíveis para serem analisadas e revogadas nas configurações. O último endereço de IP usado é guardado por até 12 meses. Nós também podemos reter históricos do servidor que incluem o endereço de IP de todas as requisições ao nosso servidor. - What do we use your information for? + Para que usamos os seus dados? - Any of the information we collect from you may be used in the following ways: + Toda informação que coletamos de você pode ser usada das seguintes maneiras: - To provide the core functionality of Mastodon. You can only interact with other people's content and post your own content when you are logged in. For example, you may follow other people to view their combined posts in your own personalized home timeline. - To aid moderation of the community, for example comparing your IP address with other known ones to determine ban evasion or other violations. - The email address you provide may be used to send you information, notifications about other people interacting with your content or sending you messages, and to respond to inquiries, and/or other requests or questions. + Para prover a funcionalidade básica do Mastodon. Você só pode interagir com o conteúdo de outras pessoas e postar seu próprio conteúdo estando autenticado. Por exemplo, você pode seguir outras pessoas para ver seus posts combinados na sua linha do tempo personalizada. + Para auxiliar na moderação da comunidade, por exemplo ao comparar o seu endereço de IP com outros endereços de IP conhecidos para determinar evasão de banimento e outras violações. + O endereço de email que você prover pode ser usado para lhe enviar informação, notificação sobre outras pessoas interagindo com o seu conteúdo ou lhe enviando mensagens e para responder a questões ou outros pedidos. - How do we protect your information? + Como protegemos as suas informações? - We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information. Among other things, your browser session, as well as the traffic between your applications and the API, are secured with SSL, and your password is hashed using a strong one-way algorithm. You may enable two-factor authentication to further secure access to your account. + Nós implementamos diversas medidas de segurança para manter a segurança das suas informações pessoais quando você as acessa ou as envia. Entre outras coisas, sua sessão do navegador, bem como o tráfego entre as aplicações e a API são asseguradas usando SSL e a sua senha é guardada usando um algoritmo forte de encriptação de mão única. Você pode ativar autenticação em dois fatores como forma de aumentar a segurança no acesso à sua conta. - What is our data retention policy? + Qual é a nossa política de retenção de dados? - We will make a good faith effort to: + Nós fazemos esforços substanciais para: - Retain server logs containing the IP address of all requests to this server, in so far as such logs are kept, no more than 90 days. - Retain the IP addresses associated with registered users no more than 12 months. + Reter o histórico do servidor contendo os endereços de IP de todas as requisições feitas à esse servidor, e com respeito a quanto tempo esses logs são retidos, não mais que 90 dias. + Reter o endereço de IP associado com usuários registrados não mais que 12 meses. - You can request and download an archive of your content, including your posts, media attachments, profile picture, and header image. + Você pode pedir e fazer o download de um arquivo de todo o conteúdo da sua conta, incluindo as suas mensagens, suas mídias anexadas, imagem de perfil e imagem de topo. - You may irreversibly delete your account at any time. + Você pode remover irreversivelmente a sua conta a qualquer momento. - Do we use cookies? + Nós usamos cookies? - Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account. + Sim. Cookies são pequenos arquivos que um site ou serviço transfere ao seu disco rígido do seu computador através do seu navegador da web (se você permitir). Esses cookies permitem ao site conhecer seu navegador e, se você tiver uma conta registrada, associá-lo a sua conta. - We use cookies to understand and save your preferences for future visits. + Nós usamos cookies para compreender e salvar suas preferências para visitas futuras. - Do we disclose any information to outside parties? + Nós compartilhamos qualquer informação para terceiros? - We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety. + Nós não vendemos, trocamos ou transferimos de qualquer maneira informação que pode lhe identificar à terceiros. Isso não inclui terceiros que podemos nos auxiliam a operar o nosso site, realizar nossos negócios ou lhe prestar serviços, contanto que esses terceiros se comprometam a manter essa informação confidencial. Nós podemos também divulgar informação quando acreditamos que é apropriado para obedecer a lei, para fazer cumprir nossas políticas ou proteger nossos direitos, propriedade ou segurança ou o direito, propriedade e segurança de outrem. - Your public content may be downloaded by other servers in the network. Your public and followers-only posts are delivered to the servers where your followers reside, and direct messages are delivered to the servers of the recipients, in so far as those followers or recipients reside on a different server than this. + Seu conteúdo público pode ser descarregado por outros servidores na rede. Suas mensagens públicas e somente para seus seguidores são entregues aos servidores onde seus seguidores resides e as suas mensagens diretas são entregues ao servidor dos usuários mencionados nelas, contanto que esses seguidores ou usuários residam em um servidor diferente deste. - When you authorize an application to use your account, depending on the scope of permissions you approve, it may access your public profile information, your following list, your followers, your lists, all your posts, and your favourites. Applications can never access your e-mail address or password. + Quando você autoriza uma aplicação a usar sua conta, dependendo do escopo de permissões que você aprovar, a aplicação pode acessar sua informação pública, a lista de usuários que você segue, seus seguidores, suas listas, suas mensagens e suas mensagens favoritas. Aplicações nunca podem acessar o seu endereço de e-mail ou senha. - Children's Online Privacy Protection Act Compliance + Conformidade com a COPPA (Children's Online Privacy Protection Act) - Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site. + Nosso site, produto e serviços são direcionados à pessoas que tem ao menos 13 anos de idade. Se esse servidor está hospedado nos EUA e você tem menos de 13 anos, de acordo com os requerimentos da COPPA (Children's Online Privacy Protection Act) não use este site. - Changes to our Privacy Policy + Mudanças à nossa política de privacidade - If we decide to change our privacy policy, we will post those changes on this page. + Se decidirmos mudar nossa política de privacidade, nós iremos disponibilizar as mudanças nesta página. - This document is CC-BY-SA. It was last updated March 7, 2018. + Este documento é CC-BY-SA. Ele foi atualizado pela última vez em 7 de março de 2018. - Originally adapted from the Discourse privacy policy. + Adaptado originalmente a partir da política de privacidade Discourse. title: "%{instance} Termos de Serviço e Política de Privacidade" time: formats: @@ -831,5 +818,6 @@ pt-BR: users: invalid_email: O endereço de e-mail é inválido invalid_otp_token: Código de autenticação inválido + otp_lost_help_html: Se você perder o acesso à ambos, você pode entrar em contato com %{email} seamless_external_login: Você está logado usando um serviço externo, então configurações de e-mail e password não estão disponíveis. signed_in_as: 'Acesso como:' diff --git a/config/locales/pt.yml b/config/locales/pt.yml index fb2a6cad1c..a1370c91d5 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -52,9 +52,7 @@ pt: unfollow: Deixar de seguir admin: account_moderation_notes: - account: Moderador create: Criar - created_at: Data created_msg: Nota de moderação criada com sucesso! delete: Eliminar destroyed_msg: Nota de moderação excluída com sucesso! @@ -63,6 +61,7 @@ pt: by_domain: Domínio confirm: Confirme confirmed: Confirmado + confirming: Confirmer demote: Rebaixar disable: Desativar disable_two_factor_authentication: Desativar 2FA @@ -71,6 +70,7 @@ pt: domain: Domínio edit: Editar email: E-mail + email_status: État de la messagerie enable: Ativar enabled: Ativado feed_url: URL do Feed @@ -108,6 +108,10 @@ pt: public: Público push_subscription_expires: A Inscrição PuSH expira redownload: Atualizar avatar + resend_confirmation: + already_confirmed: Cet utilisateur est déjà confirmé + send: Renvoyer un courriel de confirmation + success: Email de confirmation envoyé avec succès! reset: Restaurar reset_password: Reset palavra-passe resubscribe: Reinscrever @@ -244,12 +248,8 @@ pt: are_you_sure: Tens a certeza? comment: none: Nenhum - delete: Eliminar id: ID mark_as_resolved: Marcar como resolvido - nsfw: - 'false': Mostrar imagens/vídeos - 'true': Esconder imagens/vídeos report: 'Denúncia #%{id}' report_contents: Conteúdos reported_account: Conta denunciada @@ -314,11 +314,8 @@ pt: delete: Eliminar nsfw_off: NSFW OFF nsfw_on: NSFW ON - execute: Executar failed_to_execute: Falhou ao executar media: - hide: Esconder média - show: Mostrar média title: Média no_media: Não há média title: Estado das contas @@ -537,20 +534,6 @@ pt: other: Outro publishing: Publicação web: Web - push_notifications: - favourite: - title: "%{name} adicionou o teu post aos favoritos" - follow: - title: "%{name} começou a seguir-te" - group: - title: "%{count} notificações" - mention: - action_boost: Partilhar - action_expand: Mostrar mais - action_favourite: Adicionar aos favoritos - title: "%{name} mencionou-te" - reblog: - title: "%{name} partilhou o teu post" remote_follow: acct: Entre seu usuário@domínio do qual quer seguir missing_resource: Não foi possível achar a URL de redirecionamento para sua conta diff --git a/config/locales/ru.yml b/config/locales/ru.yml index bf42257581..89aefc1cd9 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -4,6 +4,7 @@ ru: about_hashtag_html: Это публичные статусы, отмеченные хэштегом #%{hashtag}. Вы можете взаимодействовать с ними при наличии у Вас аккаунта в глобальной сети Mastodon. about_mastodon_html: Mastodon - это свободная социальная сеть с открытым исходным кодом. Как децентрализованная альтернатива коммерческим платформам, Mastodon предотвращает риск монополизации Вашего общения одной компанией. Выберите сервер, которому Вы доверяете — что бы Вы ни выбрали, Вы сможете общаться со всеми остальными. Любой может запустить свой собственный узел Mastodon и участвовать в социальной сети совершенно бесшовно. about_this: Об этом узле + administered_by: 'Администратор узла:' closed_registrations: В данный момент регистрация на этом узле закрыта. contact: Связаться contact_missing: Не установлено @@ -39,6 +40,7 @@ ru: following: Подписан(а) media: Медиаконтент moved_html: "%{name} переехал(а) на %{new_profile_link}:" + network_hidden: Эта информация недоступна nothing_here: Здесь ничего нет! people_followed_by: Люди, на которых подписан(а) %{name} people_who_follow: Подписчики %{name} @@ -48,21 +50,29 @@ ru: reserved_username: Имя пользователя зарезервировано roles: admin: Администратор + bot: Бот moderator: Модератор unfollow: Отписаться admin: account_moderation_notes: - account: Модератор create: Создать - created_at: Дата created_msg: Заметка модератора успешно создана! delete: Удалить destroyed_msg: Заметка модератора успешно удалена! accounts: are_you_sure: Вы уверены? + avatar: Аватар by_domain: Домен + change_email: + changed_msg: E-mail аккаунта успешно изменён! + current_email: Текущий e-mail + label: Сменить e-mail + new_email: Новый e-mail + submit: Сменить e-mail + title: Сменить e-mail для %{username} confirm: Подтвердить confirmed: Подтверждено + confirming: Подтверждение demote: Разжаловать disable: Отключить disable_two_factor_authentication: Отключить 2FA @@ -71,6 +81,7 @@ ru: domain: Домен edit: Изменить email: E-mail + email_status: Статус e-mail enable: Включить enabled: Включен feed_url: URL фида @@ -108,6 +119,11 @@ ru: public: Публичный push_subscription_expires: Подписка PuSH истекает redownload: Обновить аватар + remove_avatar: Удалить аватар + resend_confirmation: + already_confirmed: Этот пользователь уже подтвержден + send: Повторно отправить подтверждение по электронной почте + success: Письмо с подтверждением успешно отправлено! reset: Сбросить reset_password: Сбросить пароль resubscribe: Переподписаться @@ -128,13 +144,16 @@ ru: statuses: Статусы subscribe: Подписаться title: Аккаунты + unconfirmed_email: Неподтверждённый e-mail undo_silenced: Снять глушение undo_suspension: Снять блокировку unsubscribe: Отписаться username: Имя пользователя - web: WWW + web: Веб action_logs: actions: + assigned_to_self_report: "%{name} назначил(а) жалобу %{target} на себя" + change_email_user: "%{name} сменил(а) e-mail пользователя %{target}" confirm_user: "%{name} подтвердил(а) e-mail адрес пользователя %{target}" create_custom_emoji: "%{name} загрузил(а) новый эмодзи %{target}" create_domain_block: "%{name} заблокировал(а) домен %{target}" @@ -150,10 +169,13 @@ ru: enable_user: "%{name} включил(а) вход пользователя %{target}" memorialize_account: "%{name} перевел(а) аккаунт пользователя %{target} в режим памятника" promote_user: "%{name} повысил(а) пользователя %{target}" + remove_avatar_user: "%{name} удалил(а) аватар пользователя %{target}" + reopen_report: "%{name} переоткрыл(а) жалобу %{target}" reset_password_user: "%{name} сбросил(а) пароль пользователя %{target}" - resolve_report: "%{name} dismissed report %{target}" + resolve_report: "%{name} решил(а) жалобу %{target}" silence_account: "%{name} заглушил(а) аккаунт %{target}" suspend_account: "%{name} заморозил(а) аккаунт %{target}" + unassigned_report: "%{name} сняла назначение жалобы %{target}" unsilence_account: "%{name} снял(а) глушение аккаунта %{target}" unsuspend_account: "%{name} разморозил(а) аккаунт %{target}" update_custom_emoji: "%{name} обновил(а) эмодзи %{target}" @@ -241,28 +263,44 @@ ru: expired: Истёкшие title: Фильтр title: Приглашения + report_notes: + created_msg: Примечание жалобы создано! + destroyed_msg: Примечание жалобы удалено! reports: + account: + note: заметка + report: жалоба action_taken_by: 'Действие предпринято:' are_you_sure: Вы уверены? + assign_to_self: Назначить себе + assigned: Назначенный модератор comment: none: Нет - delete: Удалить + created_at: Создано id: ID mark_as_resolved: Отметить как разрешенную - nsfw: - 'false': Показать мультимедийные вложения - 'true': Скрыть мультимедийные вложения + mark_as_unresolved: Отметить как неразрешённую + notes: + create: Добавить заметку + create_and_resolve: Разрешить с заметкой + create_and_unresolve: Переоткрыть с заметкой + delete: Удалить + placeholder: Опишите, какие действия были приняты, или любые другие подробности… + reopen: Переоткрыть жалобу report: 'Жалоба #%{id}' report_contents: Содержимое reported_account: Аккаунт нарушителя reported_by: Отправитель жалобы resolved: Разрешено + resolved_msg: Жалоба успешно обработана! silence_account: Заглушить аккаунт status: Статус suspend_account: Блокировать аккаунт target: Цель title: Жалобы + unassign: Снять назначение unresolved: Неразрешенные + updated_at: Обновлена view: Просмотреть settings: activity_api_enabled: @@ -322,11 +360,8 @@ ru: delete: Удалить nsfw_off: Выключить NSFW nsfw_on: Включить NSFW - execute: Выполнить failed_to_execute: Не удалось выполнить media: - hide: Скрыть медиаконтент - show: Показать медиаконтент title: Медиаконтент no_media: Без медиаконтента title: Статусы аккаунта @@ -342,6 +377,7 @@ ru: admin_mailer: new_report: body: "%{reporter} подал(а) жалобу на %{target}" + body_remote: Кто-то с узла %{domain} пожаловался на %{target} subject: Новая жалоба, узел %{instance} (#%{id}) application_mailer: notification_preferences: Изменить настройки e-mail @@ -431,7 +467,7 @@ ru: archive_takeout: date: Дата download: Скачать ваш архив - hint_html: Вы можете запросить архив своих статусов и загруженных медиа-файлов. Экспортированные данные будут в формате ActivityPub, который можно прочесть любой соответствующей программой. + hint_html: Вы можете запросить архив своих статусов и загруженных медиа-файлов. Экспортированные данные будут в формате ActivityPub, который можно прочесть любой соответствующей программой. Запрашивать архив можно каждые 7 дней. in_progress: Собирается ваш архив... request: Запросить ваш архив size: Размер @@ -447,6 +483,8 @@ ru: lock_link: Закройте аккаунт purge: Удалить из подписчиков success: + few: В процессе мягкой блокировки подписчиков с %{count} доменов... + many: В процессе мягкой блокировки подписчиков с %{count} доменов... one: В процессе мягкой блокировки подписчиков с одного домена... other: В процессе мягкой блокировки подписчиков с %{count} доменов... true_privacy_html: Пожалуйста, заметьте, что настоящая конфиденциальность может быть достигнута только при помощи end-to-end шифрования. @@ -457,6 +495,8 @@ ru: powered_by: работает на %{link} save_changes: Сохранить изменения validation_errors: + few: Что-то здесь не так! Пожалуйста, прочитайте о %{count} ошибках ниже + many: Что-то здесь не так! Пожалуйста, прочитайте о %{count} ошибках ниже one: Что-то здесь не так! Пожалуйста, прочитайте об ошибке ниже other: Что-то здесь не так! Пожалуйста, прочитайте о %{count} ошибках ниже imports: @@ -552,7 +592,7 @@ ru: units: billion: млрд million: млн - quadrillion: Q + quadrillion: квадрлн thousand: тыс trillion: трлн unit: '' @@ -567,25 +607,15 @@ ru: other: Другое publishing: Публикация web: WWW - push_notifications: - favourite: - title: Ваш статус понравился %{name} - follow: - title: "%{name} теперь подписан(а) на Вас" - group: - title: "%{count} уведомлений" - mention: - action_boost: Продвинуть - action_expand: Развернуть - action_favourite: Нравится - title: Вас упомянул(а) %{name} - reblog: - title: "%{name} продвинул(а) Ваш статус" remote_follow: acct: Введите username@domain, откуда Вы хотите подписаться missing_resource: Поиск требуемого перенаправления URL для Вашего аккаунта завершился неудачей proceed: Продолжить подписку prompt: 'Вы хотите подписаться на:' + remote_unfollow: + error: Ошибка + title: Заголовок + unfollowed: Отписаны sessions: activity: Последняя активность browser: Браузер @@ -655,7 +685,13 @@ ru: many: "%{count} видео" one: "%{count} видео" other: "%{count} видео" + boosted_from_html: Продвижение польз. %{acct_link} content_warning: 'Спойлер: %{warning}' + disallowed_hashtags: + few: 'содержались запрещённые хэштеги: %{tags}' + many: 'содержались запрещённые хэштеги: %{tags}' + one: 'содержался запрещённый хэштег: %{tags}' + other: 'содержались запрещённые хэштеги: %{tags}' open_in_web: Открыть в WWW over_character_limit: превышен лимит символов (%{max}) pin_errors: @@ -678,9 +714,88 @@ ru: reblogged: продвинул(а) sensitive_content: Чувствительный контент terms: + body_html: | + Privacy Policy + What information do we collect? + + + Basic account information: If you register on this server, you may be asked to enter a username, an e-mail address and a password. You may also enter additional profile information such as a display name and biography, and upload a profile picture and header image. The username, display name, biography, profile picture and header image are always listed publicly. + Posts, following and other public information: The list of people you follow is listed publicly, the same is true for your followers. When you submit a message, the date and time is stored as well as the application you submitted the message from. Messages may contain media attachments, such as pictures and videos. Public and unlisted posts are available publicly. When you feature a post on your profile, that is also publicly available information. Your posts are delivered to your followers, in some cases it means they are delivered to different servers and copies are stored there. When you delete posts, this is likewise delivered to your followers. The action of reblogging or favouriting another post is always public. + Direct and followers-only posts: All posts are stored and processed on the server. Followers-only posts are delivered to your followers and users who are mentioned in them, and direct posts are delivered only to users mentioned in them. In some cases it means they are delivered to different servers and copies are stored there. We make a good faith effort to limit the access to those posts only to authorized persons, but other servers may fail to do so. Therefore it's important to review servers your followers belong to. You may toggle an option to approve and reject new followers manually in the settings. Please keep in mind that the operators of the server and any receiving server may view such messages, and that recipients may screenshot, copy or otherwise re-share them. Do not share any dangerous information over Mastodon. + IPs and other metadata: When you log in, we record the IP address you log in from, as well as the name of your browser application. All the logged in sessions are available for your review and revocation in the settings. The latest IP address used is stored for up to 12 months. We also may retain server logs which include the IP address of every request to our server. + + + + + What do we use your information for? + + Any of the information we collect from you may be used in the following ways: + + + To provide the core functionality of Mastodon. You can only interact with other people's content and post your own content when you are logged in. For example, you may follow other people to view their combined posts in your own personalized home timeline. + To aid moderation of the community, for example comparing your IP address with other known ones to determine ban evasion or other violations. + The email address you provide may be used to send you information, notifications about other people interacting with your content or sending you messages, and to respond to inquiries, and/or other requests or questions. + + + + + How do we protect your information? + + We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information. Among other things, your browser session, as well as the traffic between your applications and the API, are secured with SSL, and your password is hashed using a strong one-way algorithm. You may enable two-factor authentication to further secure access to your account. + + + + What is our data retention policy? + + We will make a good faith effort to: + + + Retain server logs containing the IP address of all requests to this server, in so far as such logs are kept, no more than 90 days. + Retain the IP addresses associated with registered users no more than 12 months. + + + You can request and download an archive of your content, including your posts, media attachments, profile picture, and header image. + + You may irreversibly delete your account at any time. + + + + Do we use cookies? + + Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account. + + We use cookies to understand and save your preferences for future visits. + + + + Do we disclose any information to outside parties? + + We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety. + + Your public content may be downloaded by other servers in the network. Your public and followers-only posts are delivered to the servers where your followers reside, and direct messages are delivered to the servers of the recipients, in so far as those followers or recipients reside on a different server than this. + + When you authorize an application to use your account, depending on the scope of permissions you approve, it may access your public profile information, your following list, your followers, your lists, all your posts, and your favourites. Applications can never access your e-mail address or password. + + + + Children's Online Privacy Protection Act Compliance + + Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site. + + + + Changes to our Privacy Policy + + If we decide to change our privacy policy, we will post those changes on this page. + + This document is CC-BY-SA. It was last updated March 7, 2018. + + Originally adapted from the Discourse privacy policy. title: Условия обслуживания и политика конфиденциальности %{instance} themes: + contrast: Высококонтрастная default: Mastodon + mastodon-light: Mastodon (светлая) time: formats: default: "%b %d, %Y, %H:%M" @@ -725,6 +840,7 @@ ru: title: Добро пожаловать на борт, %{name}! users: invalid_email: Введенный e-mail неверен - invalid_otp_token: Введен неверный код + invalid_otp_token: Введен неверный код двухфакторной аутентификации + otp_lost_help_html: Если Вы потеряли доступ к обоим, свяжитесь с %{email} seamless_external_login: Вы залогинены через сторонний сервис, поэтому настройки e-mail и пароля недоступны. signed_in_as: 'Выполнен вход под именем:' diff --git a/config/locales/simple_form.ar.yml b/config/locales/simple_form.ar.yml index 28cfa8ab74..3d555718c1 100644 --- a/config/locales/simple_form.ar.yml +++ b/config/locales/simple_form.ar.yml @@ -4,22 +4,18 @@ ar: hints: defaults: avatar: ملف PNG أو GIF أو JPG. حجمه على أقصى تصدير 2MB. سيتم تصغيره إلى 400x400px + bot: يُعلِم أنّ هذا الحساب لا يمثل شخصًا digest: تُرسَل إليك بعد مُضيّ مدة مِن خمول نشاطك و فقط إذا ما تلقيت رسائل شخصية مباشِرة أثناء فترة غيابك مِن الشبكة - display_name: - one: 1 حرف باقي - other: %{count} حروف متبقية fields: يُمكنك عرض 4 عناصر على شكل جدول في ملفك الشخصي header: ملف PNG أو GIF أو JPG. حجمه على أقصى تصدير 2MB. سيتم تصغيره إلى 700x335px locked: يتطلب منك الموافقة يدويا على طلبات المتابعة - note: - one: 1 حرف متبقي - other: %{count} حروف متبقية + setting_hide_network: الحسابات التي تُتابعها و التي تُتابِعك على حد سواء لن تُعرَض على صفحتك الشخصية setting_noindex: ذلك يؤثر على حالة ملفك الشخصي و صفحاتك setting_theme: ذلك يؤثر على الشكل الذي سيبدو عليه ماستدون عندما تقوم بالدخول مِن أي جهاز. imports: data: ملف CSV تم تصديره مِن مثيل خادوم ماستدون آخر sessions: - otp: قم بإدخال رمز المصادقة بخطوتين مِن هاتفك أو إستخدم أحد رموز النفاذ الإحتياطية. + otp: 'قم بإدخال رمز المصادقة بخطوتين الذي قام بتوليده تطبيق جهازك أو إستخدم أحد رموز النفاذ الإحتياطية :' user: filtered_languages: سوف يتم تصفية و إخفاء اللغات المختارة من خيوطك العمومية labels: @@ -29,6 +25,7 @@ ar: value: المحتوى defaults: avatar: الصورة الرمزية + bot: إنّ هذا الحساب روبوت آلي confirm_new_password: تأكيد كلمة السر الجديدة confirm_password: تأكيد كلمة السر current_password: كلمة السر الحالية @@ -52,6 +49,7 @@ ar: setting_default_sensitive: إعتبر الوسائط دائما كمحتوى حساس setting_delete_modal: إظهار مربع حوار للتأكيد قبل حذف أي تبويق setting_display_sensitive_media: دائمًا إظهار الوسائط الحساسة + setting_hide_network: إخفِ شبكتك setting_noindex: عدم السماح لمحركات البحث بفهرسة ملفك الشخصي setting_reduce_motion: تخفيض عدد الصور في الوسائط المتحركة setting_system_font_ui: إستخدم الخطوط الإفتراضية للنظام diff --git a/config/locales/simple_form.ca.yml b/config/locales/simple_form.ca.yml index 1b04da90ad..5df7bf77b2 100644 --- a/config/locales/simple_form.ca.yml +++ b/config/locales/simple_form.ca.yml @@ -4,6 +4,7 @@ ca: hints: defaults: avatar: PNG, GIF o JPG. Màxim 2MB. S'escalarà a 400x400px + bot: Aquest compte realitza principalment accions automatitzades i pot no estar controlat per cap persona digest: Només s'envia després d'un llarg període d'inactivitat amb un resum de les mencions que has rebut en la teva absència display_name: one: 1 càracter restant @@ -14,12 +15,13 @@ ca: note: one: 1 càracter restant other: %{count} caràcters restants + setting_hide_network: Qui tu segueixes i els que et segueixen a tu no es mostraran en el teu perfil setting_noindex: Afecta el teu perfil públic i les pàgines d'estat setting_theme: Afecta l'aspecte de Mastodon quan es visita des de qualsevol dispositiu. imports: data: Fitxer CSV exportat des de una altra instància de Mastodon sessions: - otp: Introdueix el codi de dos factors des del teu telèfon o utilitza un dels teus codis de recuperació. + otp: 'Introdueix el codi de dos factors generat per el teu telèfon o utilitza un dels teus codis de recuperació:' user: filtered_languages: Les llengües seleccionades s'eliminaran de les línies de temps públiques labels: @@ -29,6 +31,7 @@ ca: value: Contingut defaults: avatar: Avatar + bot: Aquest compte és un bot confirm_new_password: Confirma la contrasenya nova confirm_password: Confirma la contrasenya current_password: Contrasenya actual @@ -52,6 +55,7 @@ ca: setting_default_sensitive: Marca sempre els elements multimèdia com a sensibles setting_delete_modal: Mostra la finestra de confirmació abans de suprimir un toot setting_display_sensitive_media: Mostra sempre els elements multimèdia marcats com a sensibles + setting_hide_network: Amaga la teva xarxa setting_noindex: Desactivació de la indexació del motor de cerca setting_reduce_motion: Redueix el moviment en animacions setting_system_font_ui: Utilitza el tipus de lletra predeterminat del sistema diff --git a/config/locales/simple_form.co.yml b/config/locales/simple_form.co.yml new file mode 100644 index 0000000000..8d24704359 --- /dev/null +++ b/config/locales/simple_form.co.yml @@ -0,0 +1,81 @@ +--- +co: + simple_form: + hints: + defaults: + avatar: Furmatu PNG, GIF o JPG. 2Mo o menu. Sarà ridottu à 400x400px + bot: Avisa a ghjente chì stu contu ùn riprisenta micca una parsona + digest: Solu mandatu dopu à una longa perioda d’inattività, è solu s’elli ci sò novi missaghji diretti + display_name: + one: Ci ferma 1 caratteru + other: Ci fermanu %{count} caratteri + fields: Pudete avè fin’à 4 elementi mustrati cum’un tavulone nant’à u vostru prufile + header: Furmatu PNG, GIF o JPG. 2Mo o menu. Sarà ridottu à 700x335px + locked: Duvarete appruvà e dumande d’abbunamentu + note: + one: Ci ferma 1 caratteru + other: Ci fermanu %{count} caratteri + setting_noindex: Tocca à u vostru prufile pubblicu è i vostri statuti + setting_theme: Tocca à l’apparenza di Mastodon quandu site cunnettatu·a da qualch’apparechju. + imports: + data: Un fugliale CSV da un’altr’istanza di Mastodon + sessions: + otp: 'Entrate u codice d’identificazione à dui fattori nant’à u vostru telefuninu, o unu di i vostri codici di ricuperazione:' + user: + filtered_languages: Ùn viderete micca e lingue selezziunate nant’à e linee pubbliche + labels: + account: + fields: + name: Label + value: Cuntinutu + defaults: + avatar: Ritrattu di prufile + bot: Stu contu hè un bot + confirm_new_password: Cunfirmà a nova chjave d’accessu + confirm_password: Cunfirmà a chjave d’accessu + current_password: Chjave d’accessu attuale + data: Dati + display_name: Nome pubblicu + email: Indirizzu e-mail + expires_in: Spira dopu à + fields: Metadata di u prufile + filtered_languages: Lingue filtrate + header: Ritrattu di cuprendula + locale: Lingua + locked: Privatizà u contu + max_uses: Numeru massimale d’utilizazione + new_password: Nova chjave d’accessu + note: Descrizzione + otp_attempt: Codice d’identificazione à dui fattori + password: Chjave d’accessu + setting_auto_play_gif: Lettura autumatica di i GIF animati + setting_boost_modal: Mustrà una cunfirmazione per sparte un statutu + setting_default_privacy: Cunfidenzialità di i statuti + setting_default_sensitive: Sempre cunsiderà media cum’è sensibili + setting_delete_modal: Mustrà une cunfirmazione per toglie un statutu + setting_display_sensitive_media: Sempre mustrà media marcati cum’è sensibili + setting_noindex: Dumandà à i motori di ricerca internet d’un pudè micca esse truvatu·a cusì + setting_reduce_motion: Fà chì l’animazione vanu più pianu + setting_system_font_ui: Pulizza di caratteri di u sistemu + setting_theme: Tema di u situ + setting_unfollow_modal: Mustrà una cunfirmazione per siguità qualch’unu + severity: Severità + type: Tippu d’impurtazione + username: Cugnome + username_or_email: Cugnome o Email + interactions: + must_be_follower: Piattà e nutificazione di quelli·e ch’ùn vi seguitanu + must_be_following: Piattà e nutificazione di quelli·e ch’ùn seguitate + must_be_following_dm: Bluccà e missaghji diretti di quelli·e ch’ùn seguitate + notification_emails: + digest: Mandà e-mail di ricapitulazione + favourite: Mandà un’e-mail quandu qualch’unu aghjunghje i mo statuti à i so favuriti + follow: Mandà un’e-mail quandu qualch’unu mi seguita + follow_request: Mandà un’e-mail quandu qualch’unu vole seguitami + mention: Mandà un’e-mail quandu qualch’unu mi mintuva + reblog: Mandà un’e-mail quandu qualch’unu sparte i mo statuti + 'no': Nò + required: + mark: "*" + text: riquisiti + 'yes': Ié diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml index a9d650a268..1bf1cbf78c 100644 --- a/config/locales/simple_form.de.yml +++ b/config/locales/simple_form.de.yml @@ -4,6 +4,7 @@ de: hints: defaults: avatar: PNG, GIF oder JPG. Maximal 2 MB. Wird auf 400×400 px herunterskaliert + bot: Warnt Besucher das dieser Nutzer keine echte Person darstellt digest: Wenn du lange Zeit inaktiv bist, wird dir eine Zusammenfassung von Erwähnungen in deiner Abwesenheit zugeschickt display_name: one: 1 Zeichen verbleibt @@ -19,7 +20,7 @@ de: imports: data: CSV-Datei, die aus einer anderen Mastodon-Instanz exportiert wurde sessions: - otp: Gib den Zwei-Faktor-Authentisierungs-Code von deinem Telefon ein oder benutze einen deiner Wiederherstellungscodes. + otp: 'Gib den Zwei-Faktor-Authentisierungscode von deinem Telefon ein oder benutze einen deiner Wiederherstellungscodes:' user: filtered_languages: Ausgewählte Sprachen werden aus deinen öffentlichen Zeitleisten gefiltert labels: @@ -29,6 +30,7 @@ de: value: Inhalt defaults: avatar: Profilbild + bot: Dies ist ein bot Benutzer confirm_new_password: Neues Passwort bestätigen confirm_password: Passwort bestätigen current_password: Derzeitiges Passwort diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index f635bf441b..851b678e1a 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -4,6 +4,7 @@ en: hints: defaults: avatar: PNG, GIF or JPG. At most 2MB. Will be downscaled to 400x400px + bot: This account mainly performs automated actions and might not be monitored digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence display_name: one: 1 character left @@ -14,12 +15,13 @@ en: note: one: 1 character left other: %{count} characters left + setting_hide_network: Who you follow and who follows you will not be shown on your profile setting_noindex: Affects your public profile and status pages setting_skin: Reskins the selected Mastodon flavour imports: data: CSV file exported from another Mastodon instance sessions: - otp: Enter the Two-factor code from your phone or use one of your recovery codes. + otp: 'Enter the two-factor code generated by your phone app or use one of your recovery codes:' user: filtered_languages: Checked languages will be filtered from public timelines for you labels: @@ -29,6 +31,7 @@ en: value: Content defaults: avatar: Avatar + bot: This is a bot account confirm_new_password: Confirm new password confirm_password: Confirm password current_password: Current password @@ -53,6 +56,7 @@ en: setting_delete_modal: Show confirmation dialog before deleting a toot setting_display_sensitive_media: Always show media marked as sensitive setting_favourite_modal: Show confirmation dialog before favouriting + setting_hide_network: Hide your network setting_noindex: Opt-out of search engine indexing setting_reduce_motion: Reduce motion in animations setting_skin: Skin diff --git a/config/locales/simple_form.eo.yml b/config/locales/simple_form.eo.yml index 41a0c26aab..4027c1b603 100644 --- a/config/locales/simple_form.eo.yml +++ b/config/locales/simple_form.eo.yml @@ -4,26 +4,34 @@ eo: hints: defaults: avatar: Formato PNG, GIF aŭ JPG. Ĝis 2MB. Estos malgrandigita al 400x400px + bot: Tiu konto ĉefe faras aŭtomatajn agojn, kaj povas esti ne kontrolata digest: Sendita nur post longa tempo de neaktiveco, kaj nur se vi ricevis personan mesaĝon en via foresto display_name: one: 1 signo restas other: %{count} signoj restas + fields: Vi povas havi ĝis 4 tabelajn elementojn en via profilo header: Formato PNG, GIF aŭ JPG. Ĝis 2MB. Estos malgrandigita al 700x335px locked: Vi devos aprobi ĉiun peton de sekvado mane note: one: 1 signo restas other: %{count} signoj restas + setting_hide_network: Tiuj, kiujn vi sekvas, kaj tiuj, kiuj sekvas vin ne estos videblaj en via profilo setting_noindex: Influas vian publikan profilon kaj mesaĝajn paĝojn setting_theme: Influas kiel Mastodon aspektas post ensaluto de ajna aparato. imports: data: CSV-dosiero el alia nodo de Mastodon sessions: - otp: Enmetu la kodon de dufaktora aŭtentigo el via telefono aŭ uzu unu el la realiraj kodoj. + otp: 'Enmetu la kodon de dufaktora aŭtentigo el via telefono aŭ uzu unu el viaj realiraj kodoj:' user: filtered_languages: Markitaj lingvoj estos elfiltritaj de publikaj tempolinioj por vi labels: + account: + fields: + name: Etikedo + value: Enhavo defaults: avatar: Profilbildo + bot: Tio estas robota konto confirm_new_password: Konfirmi novan pasvorton confirm_password: Konfirmi pasvorton current_password: Nuna pasvorto @@ -31,6 +39,7 @@ eo: display_name: Publika nomo email: Retadreso expires_in: Eksvalidiĝas post + fields: Profilaj metadatumoj filtered_languages: Filtritaj lingvoj header: Fonbildo locale: Lingvo @@ -46,6 +55,7 @@ eo: setting_default_sensitive: Ĉiam marki aŭdovidaĵojn tiklaj setting_delete_modal: Montri fenestron por konfirmi antaŭ ol forigi mesaĝon setting_display_sensitive_media: Ĉiam montri aŭdovidaĵojn markitajn tiklaj + setting_hide_network: Kaŝi viajn sekvantojn kaj sekvatojn setting_noindex: Ellistiĝi de retserĉila indeksado setting_reduce_motion: Malrapidigi animaciojn setting_system_font_ui: Uzi la dekomencan tiparon de la sistemo diff --git a/config/locales/simple_form.eu.yml b/config/locales/simple_form.eu.yml index d856feac58..22b71bac1b 100644 --- a/config/locales/simple_form.eu.yml +++ b/config/locales/simple_form.eu.yml @@ -28,5 +28,14 @@ eu: filtered_languages: Iragazitako hizkuntzak locale: Hizkuntza new_password: Pasahitz berria - note: Bio + note: Biografia password: Pasahitza + setting_boost_modal: Erakutsi baieztapen elkarrizketa-koadroa bultzada eman aurretik + setting_default_privacy: Mezuaren pribatutasuna + notification_emails: + reblog: Bidali e-mail mezua norbaitek zure mezuari bultzada ematen badio + 'no': Ez + required: + mark: "*" + text: beharrezkoa + 'yes': Bai diff --git a/config/locales/simple_form.fa.yml b/config/locales/simple_form.fa.yml index ddb13ae43a..13ff7801eb 100644 --- a/config/locales/simple_form.fa.yml +++ b/config/locales/simple_form.fa.yml @@ -3,27 +3,35 @@ fa: simple_form: hints: defaults: - avatar: یکی از قالبهای PNG یا GIF یا JPG. بیشترین اندازه ۲ مگابایت. تصویر به اندازهٔ ۱۲۰×۱۲۰ پیکسل تبدیل خواهد شد - digest: پس از مدت طولانی عدم فعالیت فرستاده میشود، شامل خلاصهای از مواردی که در نبودتان از شما نام برده شده + avatar: یکی از قالبهای PNG یا GIF یا JPG. بیشترین اندازه ۲ مگابایت. تصویر به اندازهٔ ۴۰۰×۴۰۰ پیکسل تبدیل خواهد شد + bot: این حساب بیشتر به طور خودکار فعالیت میکند و نظارت پیوستهای روی آن وجود ندارد + digest: تنها وقتی فرستاده میشود که مدتی طولانی فعالیتی نداشته باشید و در این مدت برای شما پیغام خصوصیای نوشته شده باشد display_name: one: 1 حرف باقی مانده other: %{count} حرف باقی مانده + fields: شما میتوانید تا چهار مورد را در یک جدول در نمایهٔ خود نمایش دهید header: یکی از قالبهای PNG یا GIF یا JPG. بیشترین اندازه ۲ مگابایت. تصویر به اندازهٔ ۳۳۵×۷۰۰ پیکسل تبدیل خواهد شد - locked: باید پیگیران تازه را خودتان تأیید کنید. حریم خصوصی پیشفرض نوشتهها را روی پیگیران تنظیم میکند + locked: باید پیگیران تازه را خودتان تأیید کنید note: one: 1 حرف باقی مانده other: %{count} حرف باقی مانده + setting_hide_network: فهرست پیگیران شما و فهرست کسانی که شما پی میگیرید روی نمایهٔ شما دیده نخواهد شد setting_noindex: روی نمایهٔ عمومی و صفحهٔ نوشتههای شما تأثیر میگذارد setting_theme: ظاهر ماستدون را وقتی که از هر دستگاهی به آن وارد میشوید تعیین میکند. imports: data: پروندهٔ CSV که از سرور ماستدون دیگری برونسپاری شده sessions: - otp: کد تأیید دومرحلهای را از تلفن خود وارد کنید یا یکی از کدهای بازیابی را به کار ببرید. + otp: 'کد تأیید دومرحلهای که اپ روی تلفن شما ساخته را وارد کنید یا یکی از کدهای بازیابی را به کار ببرید:' user: filtered_languages: زبانهای انتخابشده از فهرست عمومی نوشتههایی که میبینید حذف میشوند labels: + account: + fields: + name: برچسب + value: محتوا defaults: avatar: تصویر نمایه + bot: این حساب یک ربات است confirm_new_password: تأیید رمز تازه confirm_password: تأیید رمز current_password: رمز فعلی @@ -31,6 +39,7 @@ fa: display_name: نمایش به نام email: نشانی ایمیل expires_in: تاریخ انقضا + fields: اطلاعات تکمیلی نمایه filtered_languages: زبانهای فیلترشده header: تصویر زمینه locale: زبان @@ -44,8 +53,10 @@ fa: setting_boost_modal: نمایش پیغام تأیید پیش از بازبوقیدن setting_default_privacy: حریم خصوصی نوشتهها setting_default_sensitive: همیشه تصاویر را به عنوان حساس علامت بزن - setting_delete_modal: پیش از پاک کردن یک نوشته پیغام تأیید نشان بده - setting_noindex: درخواست از موتورهای جستجو برای لغو فهرستسازی + setting_delete_modal: نمایش پیغام تأیید پیش از پاک کردن یک نوشته + setting_display_sensitive_media: همیشه تصویرهای علامتزدهشده به عنوان حساس را نمایش بده + setting_hide_network: نهفتن شبکهٔ ارتباطی + setting_noindex: درخواست از موتورهای جستجوگر برای ظاهر نشدن در نتایج جستجو setting_reduce_motion: کاستن از حرکت در پویانماییها setting_system_font_ui: بهکاربردن قلم پیشفرض سیستم setting_theme: تم سایت @@ -53,13 +64,14 @@ fa: severity: شدت type: نوع درونریزی username: نام کاربری (تنها حروف انگلیسی) + username_or_email: نام کاربری یا ایمیل interactions: must_be_follower: مسدودکردن اعلانهای همه به جز پیگیران must_be_following: مسدودکردن اعلانهای کسانی که شما پی نمیگیرید must_be_following_dm: مسدودکردن پیغامهای خصوصی کسانی که شما پی نمیگیرید notification_emails: digest: خلاصهکردن چند اعلان در یک ایمیل - favourite: وقتی کسی نوشتهٔ شما پسندید ایمیل بفرست + favourite: وقتی کسی نوشتهٔ شما را پسندید ایمیل بفرست follow: وقتی کسی پیگیر شما شد ایمیل بفرست follow_request: وقتی کسی درخواست پیگیری کرد ایمیل بفرست mention: وقتی کسی از شما نام برد ایمیل بفرست diff --git a/config/locales/simple_form.fi.yml b/config/locales/simple_form.fi.yml index f48e9ab23d..b7b97395a3 100644 --- a/config/locales/simple_form.fi.yml +++ b/config/locales/simple_form.fi.yml @@ -8,6 +8,7 @@ fi: display_name: one: 1 merkki jäljellä other: %{count} merkkiä jäljellä + fields: Sinulla voi olla korkeintaan 4 asiaa profiilissasi taulukossa header: PNG, GIF tai JPG. Enintään 2 Mt. Skaalataan kokoon 700 x 335 px locked: Sinun täytyy hyväksyä seuraajat manuaalisesti note: @@ -22,6 +23,9 @@ fi: user: filtered_languages: Valitut kielet suodatetaan pois julkisilta aikajanoilta labels: + account: + fields: + value: Sisältö defaults: avatar: Profiilikuva confirm_new_password: Vahvista uusi salasana @@ -31,6 +35,7 @@ fi: display_name: Nimimerkki email: Sähköpostiosoite expires_in: Vanhenee + fields: Profiilin metadata filtered_languages: Suodatetut kielet header: Otsakekuva locale: Kieli @@ -38,7 +43,7 @@ fi: max_uses: Käyttökertoja enintään new_password: Uusi salasana note: Kuvaus - otp_attempt: Kaksivaiheisen tunnistautumisen koodi + otp_attempt: Kaksivaiheisen tunnistuksen koodi password: Salasana setting_auto_play_gif: Toista GIF-animaatiot automaattisesti setting_boost_modal: Kysy vahvistusta ennen buustausta diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml index 88e1b88737..4e535cdf46 100644 --- a/config/locales/simple_form.fr.yml +++ b/config/locales/simple_form.fr.yml @@ -4,6 +4,7 @@ fr: hints: defaults: avatar: Au format PNG, GIF ou JPG. 2 Mo maximum. Sera réduit à 400x400px + bot: Ce compte exécute principalement des actions automatisées et pourrait ne pas être surveillé digest: Uniquement envoyé après une longue période d’inactivité et uniquement si vous avez reçu des messages personnels pendant votre absence display_name: one: 1 caractère restant @@ -14,12 +15,13 @@ fr: note: one: 1 caractère restant other: %{count} caractères restants + setting_hide_network: Ceux que vous suivez et ceux qui vous suivent ne seront pas affichés sur votre profil setting_noindex: Affecte votre profil public ainsi que vos statuts setting_theme: Affecte l’apparence de Mastodon quand vous êtes connecté·e depuis n’importe quel appareil. imports: data: Un fichier CSV généré par une autre instance de Mastodon sessions: - otp: Entrez le code d’authentification à deux facteurs depuis votre téléphone ou utilisez un de vos codes de récupération. + otp: 'Entrez le code d’authentification à deux facteurs généré par votre téléphone ou utilisez un de vos codes de récupération :' user: filtered_languages: Les langues sélectionnées seront filtrées hors de vos fils publics pour vous labels: @@ -29,6 +31,7 @@ fr: value: Contenu defaults: avatar: Image de profil + bot: Ceci est un robot confirm_new_password: Confirmation du nouveau mot de passe confirm_password: Confirmation du mot de passe current_password: Mot de passe actuel @@ -52,6 +55,7 @@ fr: setting_default_sensitive: Toujours marquer les médias comme sensibles setting_delete_modal: Afficher une fenêtre de confirmation avant de supprimer un pouet setting_display_sensitive_media: Toujours afficher les médias marqués comme sensibles + setting_hide_network: Cacher votre réseau setting_noindex: Demander aux moteurs de recherche de ne pas indexer vos informations personnelles setting_reduce_motion: Réduire la vitesse des animations setting_system_font_ui: Utiliser la police par défaut du système diff --git a/config/locales/simple_form.gl.yml b/config/locales/simple_form.gl.yml index 72633c7590..bae49a65dc 100644 --- a/config/locales/simple_form.gl.yml +++ b/config/locales/simple_form.gl.yml @@ -3,23 +3,25 @@ gl: simple_form: hints: defaults: - avatar: PNG, GIF ou JPG. Como moito 2MB. Será reducida ate 400x400px + avatar: PNG, GIF ou JPG. Máximo 2MB. Será reducida a 400x400px + bot: Esta conta realiza principalmente accións automatizadas e podería non estar monitorizada digest: Enviar só tras un longo período de inactividade e só si recibeu algunha mensaxe personal na súa ausencia display_name: one: 1 caracter restante other: %{count} caracteres restantes fields: Pode ter ate 4 elementos no seu perfil mostrados como unha táboa - header: PNG, GIF ou JPG. Como moito 2MB. Será reducida a 700x335px - locked: Require que vostede aprove as seguidoras de xeito manual + header: PNG, GIF ou JPG. Máximo 2MB. Será reducida a 700x335px + locked: Require que vostede acepte as seguidoras de xeito manual note: one: 1 caracter restante other: %{count} caracteres restantes + setting_hide_network: Non se mostrará no seu perfil quen a segue e quen a está a seguir setting_noindex: Afecta ao seu perfil público e páxinas de estado setting_theme: Afecta ao aspecto de Mastodon en calquer dispositivo cando está conectada. imports: data: Ficheiro CSV exportado desde outra instancia Mastodon sessions: - otp: Introduza o código de Doble-Factor desde o seu teléfono ou utilice un dos seus códigos de recuperación. + otp: Introduza o código de doble-factor xerado no aplicativo do seu móbil ou utilice un dos seus códigos de recuperación. user: filtered_languages: Os idiomas marcados filtraranse das liñas temporais públicas para vostede labels: @@ -29,10 +31,11 @@ gl: value: Contido defaults: avatar: Avatar + bot: Esta conta é de un bot confirm_new_password: Confirme o novo contrasinal confirm_password: Confirme o contrasinal current_password: Contrasinal actual - data: Data + data: Datos display_name: Nome mostrado email: enderezo correo electrónico expires_in: Caducidade despois de @@ -52,6 +55,7 @@ gl: setting_default_sensitive: Marcar sempre multimedia como sensible setting_delete_modal: Solicitar confirmación antes de eliminar unha mensaxe setting_display_sensitive_media: Mostrar sempre os medios marcados como sensibles + setting_hide_network: Agochar a súa rede setting_noindex: Pedir non aparecer nas buscas dos motores de busca setting_reduce_motion: Reducir o movemento nas animacións setting_system_font_ui: Utilizar a tipografía por defecto do sistema diff --git a/config/locales/simple_form.it.yml b/config/locales/simple_form.it.yml index 5d9ae18f5c..59a4cf525a 100644 --- a/config/locales/simple_form.it.yml +++ b/config/locales/simple_form.it.yml @@ -4,6 +4,7 @@ it: hints: defaults: avatar: PNG, GIF o JPG. Al massimo 2MB. Verranno scalate a 400x400px + bot: Avverte che l'account non rappresenta una persona digest: Inviata solo dopo un lungo periodo di intattività e solo se hai ricevuto qualsiasi messaggio personale in tua assenza display_name: one: 1 carattere rimanente @@ -14,12 +15,13 @@ it: note: one: 1 carattere rimanente other: %{count} caratteri rimanenti + setting_hide_network: Chi segui e chi segue te no saranno mostrati sul tuo profilo setting_noindex: Coinvolge il tuo profilo pubblico e le pagine di stato setting_theme: Coinvolge il modo in cui Mastodon verrà visualizzato quando sarai collegato da qualsiasi dispositivo. imports: data: File CSV esportato da un altra istanza di Mastodon sessions: - otp: Inserisci il codice due-fattori dal tuo telefono o usa uno dei codici di recupero. + otp: 'Inserisci il codice a due fattori generato dall''app del tuo telefono o usa uno dei codici di recupero:' user: filtered_languages: Le lingue selezionate verranno filtrate dalla timeline pubblica per te labels: @@ -29,6 +31,7 @@ it: value: Contenuto defaults: avatar: Avatar + bot: Questo account è un bot confirm_new_password: Conferma nuova password confirm_password: Conferma password current_password: Password corrente @@ -52,6 +55,7 @@ it: setting_default_sensitive: Segna sempre i media come sensibili setting_delete_modal: Mostra dialogo di conferma prima di eliminare un toot setting_display_sensitive_media: Mostra sempre i media segnati come sensibili + setting_hide_network: Nascondi la tua rete setting_noindex: Non indicizzare dai motori di ricerca setting_reduce_motion: Riduci movimento nelle animazioni setting_system_font_ui: Usa il carattere di default del sistema diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 9e4d404056..a3ae822fe4 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -3,19 +3,21 @@ ja: simple_form: hints: defaults: - avatar: 2MBまでのPNGやGIF、JPGが利用可能です。400x400pxまで縮小されます + avatar: 2MBまでのPNG、GIF、JPGが利用可能です。400x400pxまで縮小されます + bot: このアカウントは主に自動で動作し、人が見ていない可能性があります digest: 長期間使用していない場合と不在時に返信を受けた場合のみ送信されます display_name: あと%{count}文字入力できます。 fields: プロフィールに表として4つまでの項目を表示することができます - header: 2MBまでのPNGやGIF、JPGが利用可能です。 700x335pxまで縮小されます + header: 2MBまでのPNG、GIF、JPGが利用可能です。 700x335pxまで縮小されます locked: フォロワーを手動で承認する必要があります note: あと%{count}文字入力できます。 + setting_hide_network: フォローとフォロワーの情報がプロフィールページで見られないようにします setting_noindex: 公開プロフィールおよび各投稿ページに影響します setting_theme: ログインしている全てのデバイスで適用されるデザインです。 imports: data: 他の Mastodon インスタンスからエクスポートしたCSVファイルを選択して下さい sessions: - otp: 携帯電話に表示された2段階認証コードを入力するか、生成したリカバリーコードを使用してください。 + otp: '携帯電話のアプリで生成された二段階認証コードを入力するか、リカバリーコードを使用してください:' user: filtered_languages: 選択した言語があなたの公開タイムラインから取り除かれます labels: @@ -25,6 +27,7 @@ ja: value: 内容 defaults: avatar: アイコン + bot: これは BOT アカウントです confirm_new_password: 新しいパスワード(確認用) confirm_password: パスワード(確認用) current_password: 現在のパスワード @@ -36,7 +39,7 @@ ja: filtered_languages: 除外する言語 header: ヘッダー locale: 言語 - locked: 非公開アカウントにする + locked: 承認制アカウントにする max_uses: 使用できる回数 new_password: 新しいパスワード note: プロフィール @@ -49,6 +52,7 @@ ja: setting_delete_modal: トゥートを削除する前に確認ダイアログを表示する setting_display_sensitive_media: 閲覧注意としてマークされたメディアも常に表示する setting_favourite_modal: お気に入りをする前に確認ダイアログを表示する + setting_hide_network: 繋がりを隠す setting_noindex: 検索エンジンによるインデックスを拒否する setting_reduce_motion: アニメーションの動きを減らす setting_system_font_ui: システムのデフォルトフォントを使う diff --git a/config/locales/simple_form.ko.yml b/config/locales/simple_form.ko.yml index ccb05fd253..9120b58c71 100644 --- a/config/locales/simple_form.ko.yml +++ b/config/locales/simple_form.ko.yml @@ -4,6 +4,7 @@ ko: hints: defaults: avatar: PNG, GIF 혹은 JPG. 최대 2MB. 400x400px로 다운스케일 될 것임 + bot: 사람들에게 계정이 사람이 아님을 알립니다 digest: 오랫동안 활동하지 않았을 때 받은 멘션들에 대한 요약 받기 display_name: one: 1 글자 남음 @@ -19,7 +20,7 @@ ko: imports: data: 다른 마스토돈 인스턴스에서 추출된 CSV 파일 sessions: - otp: 2단계 인증 코드를 휴대전화를 보고 입력하거나, 복구 코드 중 하나를 사용하세요. + otp: '휴대전화에서 생성 된 2단계 인증 코드를 입력하거나, 복구 코드 중 하나를 사용하세요:' user: filtered_languages: 선택된 언어가 공개 타임라인에서 제외 될 것입니다 labels: @@ -29,6 +30,7 @@ ko: value: 내용 defaults: avatar: 아바타 + bot: 이것은 봇 계정입니다 confirm_new_password: 새로운 비밀번호 다시 입력 confirm_password: 현재 비밀번호 다시 입력 current_password: 현재 비밀번호 입력 diff --git a/config/locales/simple_form.nl.yml b/config/locales/simple_form.nl.yml index ec42adfd72..68aa0635d4 100644 --- a/config/locales/simple_form.nl.yml +++ b/config/locales/simple_form.nl.yml @@ -4,7 +4,8 @@ nl: hints: defaults: avatar: PNG, GIF of JPG. Maximaal 2MB. Wordt teruggeschaald naar 400x400px - digest: Wordt alleen na een lange periode van inactiviteit verzonden en alleen wanneer je tijdens jouw afwezigheid persoonlijke berichten ontvangt + bot: Dit is een geautomatiseerd account en wordt mogelijk niet gemonitord + digest: Wordt alleen na een lange periode van inactiviteit verzonden en alleen wanneer je tijdens jouw afwezigheid persoonlijke berichten hebt ontvangen display_name: one: 1 teken over other: %{count} tekens over @@ -14,14 +15,15 @@ nl: note: one: 1 teken over other: %{count} tekens over + setting_hide_network: Wie jij volgt en wie jou volgen wordt niet op jouw profiel getoond setting_noindex: Heeft invloed op jouw openbare profiel en toots setting_theme: Heeft invloed op hoe de webapp van Mastodon eruitziet (op elk apparaat waarmee je inlogt). imports: data: CSV-bestand dat op een andere Mastodonserver werd geëxporteerd sessions: - otp: Voer de tweestaps-aanmeldcode vanaf jouw mobiele telefoon in of gebruik een van jouw herstelcode's. + otp: Voer de tweestaps-aanmeldcode vanaf jouw mobiele telefoon in of gebruik een van jouw herstelcodes. user: - filtered_languages: De geselecteerde talen worden uit de lokale en globale tijdlijn verwijderd + filtered_languages: Geselecteerde talen worden uit de lokale en globale tijdlijn verwijderd labels: account: fields: @@ -29,6 +31,7 @@ nl: value: Inhoud defaults: avatar: Avatar + bot: Dit is een bot-account confirm_new_password: Nieuw wachtwoord bevestigen confirm_password: Wachtwoord bevestigen current_password: Huidig wachtwoord @@ -37,11 +40,11 @@ nl: email: E-mailadres expires_in: Vervalt na fields: Metadata profiel - filtered_languages: Talen filteren + filtered_languages: Gefilterde talen header: Omslagfoto locale: Taal locked: Maak account besloten - max_uses: Max aantal keer te gebruiken + max_uses: Max. aantal keer te gebruiken new_password: Nieuwe wachtwoord note: Bio otp_attempt: Tweestaps-aanmeldcode @@ -52,6 +55,7 @@ nl: setting_default_sensitive: Media altijd als gevoelig markeren setting_delete_modal: Vraag voor het verwijderen van een toot een bevestiging setting_display_sensitive_media: Als gevoelig gemarkeerde media altijd tonen + setting_hide_network: Jouw volgers en wie je volgt verbergen setting_noindex: Jouw toots niet door zoekmachines laten indexeren setting_reduce_motion: Langzamere animaties setting_system_font_ui: Standaardlettertype van jouw systeem gebruiken @@ -59,7 +63,7 @@ nl: setting_unfollow_modal: Vraag voor het ontvolgen van iemand een bevestiging severity: Zwaarte type: Importtype - username: gebruikersnaam + username: Gebruikersnaam username_or_email: Gebruikersnaam of e-mailadres interactions: must_be_follower: Meldingen van mensen die jou niet volgen blokkeren diff --git a/config/locales/simple_form.oc.yml b/config/locales/simple_form.oc.yml index 4ca58c1023..65b27e7166 100644 --- a/config/locales/simple_form.oc.yml +++ b/config/locales/simple_form.oc.yml @@ -4,6 +4,7 @@ oc: hints: defaults: avatar: PNG, GIF o JPG. Maximum 2 Mo. Serà retalhat en 400x400px + bot: Avisar lo monde qu’aqueste compte es pas d’una persona digest: Solament enviat aprèp un long moment d’inactivitat e solament s’avètz recebut de messatges personals pendent vòstra abséncia display_name: one: Demòra encara 1 caractèr @@ -14,12 +15,13 @@ oc: note: one: Demòra encara 1 caractèr other: Demòran encara %{count} caractèrs + setting_hide_network: Vòstre perfil mostrarà pas los que vos sègon e lo monde que seguètz setting_noindex: Aquò es destinat a vòstre perfil public e vòstra pagina d’estatuts setting_theme: Aquò càmbia lo tèma grafic de Mastodon quand sètz connectat qual que siasque lo periferic. imports: data: Fichièr CSV exportat d’una autra instància Mastodon sessions: - otp: Picatz lo còdi d’autentificacion en dos temps (Two factor code) de vòstre mobil o utilizatz un de vòstres còdis de recuperacion. + otp: 'Picatz lo còdi d’autentificacion en dos temps (Two factor code) de vòstra aplicacion mobil o utilizatz un de vòstres còdis de recuperacion :' user: filtered_languages: Las lengas seleccionadas seràn levadas de vòstre flux d’actualitat labels: @@ -29,6 +31,7 @@ oc: value: Contengut defaults: avatar: Avatar + bot: Aquò es lo compte a un robòt confirm_new_password: Confirmacion del nòu senhal confirm_password: Confirmatz lo nòu senhal current_password: Senhal actual @@ -41,7 +44,7 @@ oc: header: Bandièra locale: Lenga locked: Far venir lo compte privat - max_uses: Limit d’utilizacion + max_uses: Limit d’utilizacions new_password: Nòu senhal note: Bio otp_attempt: Còdi Two-factor @@ -52,6 +55,7 @@ oc: setting_default_sensitive: Totjorn marcar los mèdias coma sensibles setting_delete_modal: Afichar una fenèstra de confirmacion abans de suprimir un estatut setting_display_sensitive_media: Totjorn mostrar los mèdias coma sensibles + setting_hide_network: Amagar vòstre malhum setting_noindex: Èsser pas indexat pels motors de recèrca setting_reduce_motion: Reduire la velocitat de las animacions setting_system_font_ui: Utilizar la polissa del sisèma diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml index 8a6d47a015..67a2fc975f 100644 --- a/config/locales/simple_form.pl.yml +++ b/config/locales/simple_form.pl.yml @@ -4,6 +4,7 @@ pl: hints: defaults: avatar: PNG, GIF lub JPG. Maksymalnie 2MB. Zostanie zmniejszony do 400x400px + bot: Informuje użytkowników, że konto nie jest prowadzone przez człowieka digest: Wysyłane tylko po długiej nieaktywności, jeżeli w tym czasie otrzymaleś jakąś wiadomość bezpośrednią display_name: few: Pozostały %{count} znaki. @@ -18,12 +19,13 @@ pl: many: Pozostało %{count} znaków one: Pozostał 1 znak other: Pozostało %{count} znaków + setting_hide_network: Informacje o tym, kto Cię śledzi i kogo śledzisz nie będą widoczne setting_noindex: Wpływa na widoczność strony profilu i Twoich wpisów setting_skin: Zmienia wygląd używanej odmiany Mastodona imports: data: Plik CSV wyeksportowany z innej instancji Mastodona sessions: - otp: Wprowadź kod weryfikacji dwuetapowej z telefonu lub wykorzystaj jeden z kodów zapasowych. + otp: 'Wprowadź kod weryfikacji dwuetapowej z telefonu lub wykorzystaj jeden z kodów zapasowych:' user: filtered_languages: Wpisy w wybranych językach nie będą wyświetlać się na publicznych osiach czasu labels: @@ -33,6 +35,7 @@ pl: value: Zawartość defaults: avatar: Awatar + bot: To konto jest prowadzone przez bota confirm_new_password: Potwierdź nowe hasło confirm_password: Potwierdź hasło current_password: Obecne hasło @@ -57,6 +60,7 @@ pl: setting_delete_modal: Pytaj o potwierdzenie przed usunięciem wpisu setting_display_sensitive_media: Zawsze oznaczaj zawartość multimedialną jako wrażliwą setting_favourite_modal: Pytaj o potwierdzenie przed dodaniem do ulubionych + setting_hide_network: Ukryj swoją sieć setting_noindex: Nie indeksuj mojego profilu w wyszukiwarkach internetowych setting_reduce_motion: Ogranicz ruch w animacjach setting_skin: Motyw diff --git a/config/locales/simple_form.pt-BR.yml b/config/locales/simple_form.pt-BR.yml index cae1f671dd..50ed5eb1a8 100644 --- a/config/locales/simple_form.pt-BR.yml +++ b/config/locales/simple_form.pt-BR.yml @@ -4,6 +4,7 @@ pt-BR: hints: defaults: avatar: PNG, GIF or JPG. Arquivos de até 2MB. Eles serão diminuídos para 400x400px + bot: Essa conta executa principalmente ações automatizadas e pode não ser monitorada digest: Enviado após um longo período de inatividade com um resumo das menções que você recebeu em sua ausência display_name: one: 1 caracter restante @@ -14,12 +15,13 @@ pt-BR: note: one: 1 caracter restante other: %{count} caracteres restantes + setting_hide_network: Quem você segue e quem segue você não aparecerá no seu perfil setting_noindex: Afeta seu perfil público e as páginas de suas postagens setting_theme: Afeta a aparência do Mastodon quando em sua conta em qualquer aparelho. imports: data: Arquivo CSV exportado de outra instância do Mastodon sessions: - otp: Insira o código de autenticação do seu celular ou use um dos códigos de recuperação. + otp: 'Insira o código de autenticação gerado pelo app no seu celular ou use um dos códigos de recuperação:' user: filtered_languages: Selecione os idiomas que devem ser removidos de suas timelines públicas labels: @@ -29,6 +31,7 @@ pt-BR: value: Conteúdo defaults: avatar: Avatar + bot: Essa é a conta de um robô confirm_new_password: Confirmar nova senha confirm_password: Confirmar senha current_password: Senha atual @@ -52,6 +55,7 @@ pt-BR: setting_default_sensitive: Sempre marcar mídia como sensível setting_delete_modal: Mostrar diálogo de confirmação antes de deletar uma postagem setting_display_sensitive_media: Sempre mostrar mídia marcada como sensível + setting_hide_network: Esconder suas conexões setting_noindex: Não quero ser indexado por mecanismos de busca setting_reduce_motion: Reduz movimento em animações setting_system_font_ui: Usar a fonte padrão de seu sistema diff --git a/config/locales/simple_form.ru.yml b/config/locales/simple_form.ru.yml index b8ee5892d3..a6b50b9d2e 100644 --- a/config/locales/simple_form.ru.yml +++ b/config/locales/simple_form.ru.yml @@ -4,12 +4,14 @@ ru: hints: defaults: avatar: PNG, GIF или JPG. Максимально 2MB. Будет уменьшено до 400x400px - digest: Отсылается после долгого периода неактивности с общей информацией упоминаний, полученных в Ваше отсутствие + bot: Этот аккаунт обычно выполяет автоматизированные действия и может не просматриваться владельцем + digest: Отсылается лишь после длительной неактивности, если Вы в это время получали личные сообщения display_name: few: Осталось %{count} символа many: Осталось %{count} символов one: Остался 1 символ other: Осталось %{count} символов + fields: В профиле можно отобразить до 4 пунктов как таблицу header: PNG, GIF или JPG. Максимально 2MB. Будет уменьшено до 700x335px locked: Потребует от Вас ручного подтверждения подписчиков, изменит приватность постов по умолчанию на "только для подписчиков" note: @@ -17,17 +19,23 @@ ru: many: Осталось %{count} символов one: Остался 1 символ other: Осталось %{count} символов + setting_hide_network: Те, на кого Вы подписаны и кто подписан на Вас, не будут отображены в Вашем профиле setting_noindex: Относится к Вашему публичному профилю и страницам статусов setting_theme: Влияет на внешний вид Mastodon при выполненном входе в аккаунт. imports: data: Файл CSV, экспортированный с другого узла Mastodon sessions: - otp: Введите код двухфакторной аутентификации или используйте один из Ваших кодов восстановления. + otp: 'Введите код двухфакторной аутентификации, сгенерированный в мобильном приложении, или используйте один из Ваших кодов восстановления:' user: filtered_languages: Выбранные языки будут убраны из Ваших публичных лет. labels: + account: + fields: + name: Пункт + value: Значение defaults: avatar: Аватар + bot: Это аккаунт бота confirm_new_password: Повторите новый пароль confirm_password: Повторите пароль current_password: Текущий пароль @@ -35,6 +43,7 @@ ru: display_name: Показываемое имя email: Адрес e-mail expires_in: Срок действия + fields: Метаданные профиля filtered_languages: Фильтруемые языки header: Заголовок locale: Язык @@ -50,6 +59,7 @@ ru: setting_default_sensitive: Всегда отмечать медиаконтент как чувствительный setting_delete_modal: Показывать диалог подтверждения перед удалением setting_display_sensitive_media: Всегда показывать медиаконтент, отмеченный как чувствительный + setting_hide_network: Скрыть свои связи setting_noindex: Отказаться от индексации в поисковых машинах setting_reduce_motion: Уменьшить движение в анимации setting_system_font_ui: Использовать шрифт системы по умолчанию diff --git a/config/locales/simple_form.sk.yml b/config/locales/simple_form.sk.yml index 134e62ee37..c6887a3630 100644 --- a/config/locales/simple_form.sk.yml +++ b/config/locales/simple_form.sk.yml @@ -4,22 +4,26 @@ sk: hints: defaults: avatar: PNG, GIF alebo JPG. Maximálne 2MB. Bude zmenšený na 400x400px - digest: Odoslané iba v prípade dlhodobej neprítomnosti, a len ak ste obdŕžali nejaké osobné správy kým ste boli preč + bot: Tento účet vykonáva hlavne automatizované akcie, a je pravdepodobne nespravovaný + digest: Odoslané iba v prípade dlhodobej neprítomnosti, a len ak si obdŕžal/a nejaké osobné správy kým si bol/a preč display_name: + few: Ostávajú ti %{count} znaky one: Ostáva ti 1 znak other: Ostáva ti %{count} znakov fields: Môžeš mať 4 položky na svojom profile zobrazené vo forme tabuľky header: PNG, GIF alebo JPG. Maximálne 2MB. Bude zmenšený na 700x335px - locked: Musíte manuálne schváliť sledujúcich + locked: Vyžaduje manuálne schvalovať sledujúcich note: - one: Ostáva vám 1 znak + few: Ostávajú ti %{count} znaky + one: Ostáva ti 1 znak other: Ostáva ti %{count} znakov - setting_noindex: Ovplyvňuje profil a správy tak, že ich nebude možné nájsť vyhľadávaním - setting_theme: Toto ovplyvní ako bude Mastodon vyzerať pri prihlásení z hociktorého zariadenia. + setting_hide_network: Koho následuješ, a kto následuje teba nebude zobrazené na tvojom profile + setting_noindex: Ovplyvňuje verejný profil a statusy + setting_theme: Toto ovplyvňuje ako Mastodon vyzerá pri prihlásení z hociakého zariadenia. imports: data: CSV súbor vyexportovaný z inej Mastodon inštancie sessions: - otp: Napíš sem dvoj-faktorový kód z telefónu, alebo použite jeden z vašich obnovovacích kódov. + otp: 'Napíš sem dvoj-faktorový kód z telefónu, alebo použi jeden z tvojích obnovovacích kódov:' user: filtered_languages: Zaškrtnuté jazyky budú pre teba vynechané nebudú z verejnej časovej osi labels: @@ -29,6 +33,7 @@ sk: value: Obsah defaults: avatar: Avatar + bot: Toto je automatizovaný bot účet confirm_new_password: Znovu tvoje nové heslo, pre potvrdenie confirm_password: Potvrď heslo current_password: Súčasné heslo @@ -52,18 +57,19 @@ sk: setting_default_sensitive: Označ všetky mediálne súbory ako chúlostivé setting_delete_modal: Zobrazuj potvrdzovacie okno pred vymazaním toot-u setting_display_sensitive_media: Vždy zobraz médiá označené ako chúlostivé + setting_hide_network: Ukri svoju sieť kontaktov setting_noindex: Nezaraďuj príspevky do indexu pre vyhľadávče setting_reduce_motion: Redukovať pohyb v animáciách setting_system_font_ui: Použiť základné systémové písmo - setting_theme: Vzhľad - setting_unfollow_modal: Zobrazovať potvrdzovacie okno pred skončením sledovania iného používateľa + setting_theme: Vzhľad stránky + setting_unfollow_modal: Zobraziť potvrdzovacie okno pred skončením sledovania iného užívateľa severity: Závažnosť type: Typ importu - username: Užívateľské meno - username_or_email: Prezívka, alebo Email + username: Prezývka + username_or_email: Prezívka, alebo email interactions: - must_be_follower: Blokovať notifikácie pod používateľov, ktorí ťa nesledujú - must_be_following: Blokovať notifikácie od ľudí ktorí ťa nesledujú + must_be_follower: Blokovať oznámenia od užívateľov, ktorí ťa nesledujú + must_be_following: Blokovať oznámenia od ľudí ktorých nesleduješ must_be_following_dm: Blokovať súkromné správy od ľudí ktorých nesleduješ notification_emails: digest: Posielať súhrnné emaily diff --git a/config/locales/simple_form.sl.yml b/config/locales/simple_form.sl.yml new file mode 100644 index 0000000000..31d1e11701 --- /dev/null +++ b/config/locales/simple_form.sl.yml @@ -0,0 +1,15 @@ +--- +sl: + simple_form: + hints: + defaults: + avatar: PNG, GIF ali JPG. Največ 2MB. Zmanjšana bo na 400x400px + bot: Opozarja ljudi, da račun ne predstavlja osebe + digest: Pošlje se le po dolgem obdobju nedejavnosti in samo, če ste prejeli osebna sporočila v vaši odsotnosti + display_name: + one: 1 znak ostane + other: %{count} znakov ostane + fields: Na svojem profilu lahko imate do 4 predmete prikazane kot tabelo. + header: PNG, GIF ali JPG. Največ 2MB. Zmanjšana bo na 700x335px + imports: + data: Izvožena CSV datoteka iz drugega Mastodon vozlišča diff --git a/config/locales/simple_form.sv.yml b/config/locales/simple_form.sv.yml index 81ba61fb3b..f027d684b8 100644 --- a/config/locales/simple_form.sv.yml +++ b/config/locales/simple_form.sv.yml @@ -3,7 +3,8 @@ sv: simple_form: hints: defaults: - avatar: Högst 2 MB. Kommer nedskalas till 400x400px + avatar: Högst 2 MB. Kommer att skalas ner till 400x400px + bot: Detta konto utför huvudsakligen automatiserade åtgärder och kanske inte övervakas digest: Skickas endast efter en lång period av inaktivitet och endast om du har fått några personliga meddelanden i din frånvaro display_name: one: 1 tecken kvar @@ -14,12 +15,13 @@ sv: note: one: 1 tecken kvar other: %{count} tecken kvar + setting_hide_network: Vem du följer och vilka som följer dig kommer inte att visas på din profilsida setting_noindex: Påverkar din offentliga profil och statussidor setting_theme: Påverkar hur Mastodon ser ut oavsett från vilken enhet du är inloggad. imports: data: CSV-fil som exporteras från en annan Mastodon-instans sessions: - otp: Ange tvåfaktorkoden från din telefon eller använd någon av dina återställningskoder. + otp: 'Ange tvåfaktorkoden genererad från din telefonapp eller använd någon av dina återställningskoder:' user: filtered_languages: Kontrollerade språk filtreras från offentliga tidslinjer för dig labels: @@ -29,6 +31,7 @@ sv: value: Innehåll defaults: avatar: Avatar + bot: Detta är ett botkonto confirm_new_password: Bekräfta nytt lösenord confirm_password: Bekräfta lösenord current_password: Nuvarande lösenord @@ -52,6 +55,7 @@ sv: setting_default_sensitive: Markera alltid media som känsligt setting_delete_modal: Visa bekräftelsedialog innan du raderar en toot setting_display_sensitive_media: Visa alltid media märkt som känsligt + setting_hide_network: Göm ditt nätverk setting_noindex: Uteslutning av sökmotorindexering setting_reduce_motion: Minska rörelser i animationer setting_system_font_ui: Använd systemets standardfont diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml index 8252757566..5b3b25ab13 100644 --- a/config/locales/simple_form.zh-CN.yml +++ b/config/locales/simple_form.zh-CN.yml @@ -4,22 +4,30 @@ zh-CN: hints: defaults: avatar: 文件大小限制 2MB,只支持 PNG、GIF 或 JPG 格式。图片分辨率将会压缩至 400×400px + bot: 来自这个帐户的绝大多数操作都是自动进行的,并且可能无人监控 digest: 仅在你长时间未登录,且收到了私信时发送 display_name: 还能输入 %{count} 个字符 + fields: 这将会在个人资料页上以表格的形式展示,最多 4 个项目 header: 文件大小限制 2MB,只支持 PNG、GIF 或 JPG 格式。图片分辨率将会压缩至 700×335px locked: 你需要手动审核所有关注请求 note: 还能输入 %{count} 个字符 + setting_hide_network: 你关注的人和关注你的人将不会在你的个人资料页上展示 setting_noindex: 此设置会影响到你的公开个人资料以及嘟文页面 - setting_theme: 此设置会影响到你从任意设备登录时 Mastodon 的显示样式 + setting_theme: 此设置会影响到所有已登录设备上 Mastodon 的显示样式 imports: data: 请上传从其他 Mastodon 实例导出的 CSV 文件 sessions: - otp: 输入你手机上生成的双重认证码,或者任意一个恢复代码。 + otp: 输入你手机应用上生成的双重认证码,或者任意一个恢复代码: user: filtered_languages: 被勾选语言的嘟文将不会出现在你的公共时间轴上 labels: + account: + fields: + name: 标签 + value: 内容 defaults: avatar: 头像 + bot: 这是一个机器人帐户 confirm_new_password: 确认新密码 confirm_password: 确认密码 current_password: 当前密码 @@ -27,6 +35,7 @@ zh-CN: display_name: 昵称 email: 电子邮件地址 expires_in: 失效时间 + fields: 个人资料附加信息 filtered_languages: 语言过滤 header: 个人资料页横幅图片 locale: 语言 @@ -41,6 +50,8 @@ zh-CN: setting_default_privacy: 嘟文默认可见范围 setting_default_sensitive: 总是将我发送的媒体文件标记为敏感内容 setting_delete_modal: 在删除嘟文前询问我 + setting_display_sensitive_media: 总是显示标记为敏感的媒体文件 + setting_hide_network: 隐藏你的社交网络 setting_noindex: 禁止搜索引擎建立索引 setting_reduce_motion: 降低过渡动画效果 setting_system_font_ui: 使用系统默认字体 @@ -49,6 +60,7 @@ zh-CN: severity: 级别 type: 导入数据类型 username: 用户名 + username_or_email: 用户名或电子邮件地址 interactions: must_be_follower: 屏蔽来自未关注我的用户的通知 must_be_following: 屏蔽来自我未关注的用户的通知 diff --git a/config/locales/simple_form.zh-HK.yml b/config/locales/simple_form.zh-HK.yml index a21439a980..06d3f6f6c6 100644 --- a/config/locales/simple_form.zh-HK.yml +++ b/config/locales/simple_form.zh-HK.yml @@ -4,6 +4,7 @@ zh-HK: hints: defaults: avatar: 支援 PNG, GIF 或 JPG 圖片,檔案最大為 2MB,會縮裁成 400x400px + bot: 提醒用戶本帳號是機械人 digest: 僅在你長時間未登錄,且收到了私信時發送 display_name: one: 尚餘 1 個字 @@ -14,12 +15,13 @@ zh-HK: note: one: 尚餘 1 個字 other: 尚餘 %{count} 個字 + setting_hide_network: 你關注的人和關注你的人將不會在你的個人資料頁上顯示 setting_noindex: 此設定會影響到你的公開個人資料以及文章頁面 setting_theme: 此設置會影響到你從任意設備登入時 Mastodon 的顯示樣式。 imports: data: 自其他服務站匯出的 CSV 檔案 sessions: - otp: 輸入你手機上生成的雙重認證碼,或者任意一個恢復代碼。 + otp: 輸入你手機上生成的雙重認證碼,或者任意一個恢復代碼: user: filtered_languages: 下面被選擇的語言的文章將不會出現在你的公共時間軸上 labels: @@ -29,6 +31,7 @@ zh-HK: value: 內容 defaults: avatar: 個人頭像 + bot: 這帳號是機械人 confirm_new_password: 確認新密碼 confirm_password: 確認密碼 current_password: 目前密碼 @@ -52,6 +55,7 @@ zh-HK: setting_default_sensitive: 預設我的內容為敏感內容 setting_delete_modal: 刪推前詢問我 setting_display_sensitive_media: 預設我的媒體為敏感內容 + setting_hide_network: 隱藏你的社交網絡 setting_noindex: 阻止搜尋引擎檢索 setting_reduce_motion: 減低動畫效果 setting_system_font_ui: 使用系統預設字型 diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 8484ac52c0..d887d2de23 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -1,7 +1,7 @@ --- sk: about: - about_hashtag_html: Toto sú verejné toot príspevky otagované #%{tagom}. Ak máš účet niekde vo fediverse, môžeš ich používať. + about_hashtag_html: Toto sú verejné toot príspevky otagované #%{hashtag}. Ak máš účet niekde vo fediverse, môžeš ich používať. about_mastodon_html: Mastodon je sociálna sieť založená na otvorených webových protokoloch. Jej zrojový kód je otvorený a je decentralizovaná podobne ako email. about_this: O tejto instancii administered_by: 'Správca je:' @@ -10,7 +10,7 @@ sk: contact_missing: Nezadané contact_unavailable: Neuvedené description_headline: Čo je %{domain}? - domain_count_after: ďalším inštanciám + domain_count_after: ďalším instanciám domain_count_before: Pripojený k extended_description_html: | Pravidlá @@ -40,6 +40,7 @@ sk: following: Sleduje media: Médiá moved_html: "%{name} účet bol presunutý na %{new_profile_link}:" + network_hidden: Táto informácia nieje k dispozícii nothing_here: Nič tu nie je! people_followed_by: Ľudia, ktorých %{name} sleduje people_who_follow: Ľudia sledujúci %{name} @@ -53,9 +54,7 @@ sk: unfollow: Prestať sledovať admin: account_moderation_notes: - account: Moderátor - create: Vytvoriť - created_at: Dátum + create: Zanechaj poznámku created_msg: Poznámka moderátora bola úspešne vytvorená! delete: Zmazať destroyed_msg: Poznámka moderátora bola úspešne zmazaná! @@ -72,6 +71,7 @@ sk: title: Zmeň email pre %{username} confirm: Potvrdiť confirmed: Potvrdený + confirming: Potvrdzujúci demote: Degradovať disable: Zablokovať disable_two_factor_authentication: Zakázať 2FA @@ -80,6 +80,7 @@ sk: domain: Doména edit: Upraviť email: Email + email_status: Stav Email enable: Povoliť enabled: Povolený feed_url: URL časovej osi @@ -118,6 +119,10 @@ sk: push_subscription_expires: PuSH odoberanie expiruje redownload: Obnoviť avatar remove_avatar: Odstrániť avatár + resend_confirmation: + already_confirmed: Tento používateľ už bol potvrdený + send: Znova odoslať potvrdzovací e-mail + success: Potvrdený e-mail bol úspešne odoslaný! reset: Reset reset_password: Obnoviť heslo resubscribe: Znovu odoberať @@ -169,6 +174,7 @@ sk: resolve_report: "%{name} vyriešili nahlásenie užívateľa %{target}" silence_account: "%{name} utíšil/a účet %{target}" suspend_account: "%{name} zablokoval/a účet používateľa %{target}" + unassigned_report: "%{name} odobral/a report od %{target}" unsilence_account: "%{name} zrušil/a utíšenie účtu používateľa %{target}" unsuspend_account: "%{name} zrušil/a blokovanie účtu používateľa %{target}" update_custom_emoji: "%{name} aktualizoval/a emoji %{target}" @@ -222,6 +228,7 @@ sk: severity: Závažnosť show: affected_accounts: + few: "%{count} účty v databáze ovplyvnených" one: Jeden účet v databáze ovplyvnený other: "%{count} účtov v databáze ovplyvnených" retroactive: @@ -268,7 +275,6 @@ sk: comment: none: Žiadne created_at: Nahlásené - delete: Vymazať id: Identifikácia mark_as_resolved: Označiť ako vyriešené mark_as_unresolved: Označ ako nevyriešené @@ -277,10 +283,7 @@ sk: create_and_resolve: Vyrieš s poznámkou create_and_unresolve: Otvor znovu, s poznámkou delete: Vymaž - placeholder: Opíš aké opatrenia boli urobené, alebo akékoľvek iné aktualizácie k tomuto nahláseniu… - nsfw: - 'false': Odkryť mediálne prílohy - 'true': Skryť mediálne prílohy + placeholder: Opíš aké opatrenia boli urobené, alebo akékoľvek iné súvisiace aktualizácie… reopen: Znovu otvor report report: Nahlásiť report_contents: Obsah @@ -355,11 +358,8 @@ sk: delete: Vymazať nsfw_off: Obsah nieje chúlostivý nsfw_on: Označ obeah aka chúlostivý - execute: Vykonať failed_to_execute: Nepodarilo sa vykonať media: - hide: Skryť médiá - show: Zobraziť médiá title: Médiá no_media: Žiadné médiá title: Statusy na účte @@ -375,6 +375,7 @@ sk: admin_mailer: new_report: body: "%{reporter} nahlásil %{target}" + body_remote: Niekto z %{domain} nahlásil %{target} subject: Nový report pre %{instance} (#%{id}) application_mailer: notification_preferences: Zmeniť e-mailové voľby @@ -396,8 +397,8 @@ sk: change_password: Heslo confirm_email: Potvrdiť email delete_account: Vymazať účet - delete_account_html: Pokiaľ si želáte vymazať svoj účet, môžete tak 1 urobiť tu 2. Budete požiadaný/á o potvrdenie tohto kroku. - didnt_get_confirmation: Neobdŕžali ste kroky pre potvrdenie? + delete_account_html: Pokiaľ chceš vymazať svoj účet, môžeš tak urobiť tu. Budeš požiadaný/á o potvrdenie tohto kroku. + didnt_get_confirmation: Neobdŕžal/a si kroky pre potvrdenie? forgot_password: Zabudli ste heslo? invalid_reset_password_token: Token na obnovu hesla vypršal. Prosím vypítajte si nový. login: Prihlás sa @@ -463,7 +464,7 @@ sk: archive_takeout: date: Dátum download: Stiahni si svoj archív - hint_html: Môžeš si opýtať archív svojích príspevkov a nahratých médií. Exportované dáta budú v ActivityPub formáte, čítateľné hociakým kompatibilným softvérom. + hint_html: Môžeš si opýtať archív svojích príspevkov a nahratých médií. Exportované dáta budú v ActivityPub formáte, čítateľné hociakým kompatibilným softvérom. Archív si je možné vyžiadať každých sedem dní. in_progress: Balím tvoj archív... request: Vyžiadaj si tvoj archív size: Veľkosť @@ -479,9 +480,10 @@ sk: lock_link: Zamknite svoj účet purge: Odstrániť následovateľa success: + few: Počas utišovania sledovateľov z %{count} domén... one: Počas utišovania sledovateľov z jednej domény... other: Počas utišovania sledovateľov z %{count} domén... - true_privacy_html: Prosím majte na vedomí, 1 že ozajstné súkromie sa dá dosiahnúť iba za pomoci end-to-end enkrypcie 2. + true_privacy_html: Prosím ber na vedomie, že ozajstné súkromie sa dá dosiahnúť iba za pomoci end-to-end enkrypcie. unlocked_warning_html: Hocikto ťa môže následovať aby mohol/a ihneď vidieť tvoje súkromné príspevky. %{lock_link} aby si mohla skontrolovať a odmietať sledovateľov. unlocked_warning_title: Tvoj účet nieje zamknutý generic: @@ -489,8 +491,9 @@ sk: powered_by: poháňané vďaka %{link} save_changes: Uložiť zmeny validation_errors: + few: Niečo ešte stále nieje v poriadku! Prosím skontroluj všetky %{count} chyby one: Niečo nieje úplne v poriadku! Prosím skontroluj chybu - other: Niečo ešte stále nieje v poriadku! Prosím skontroluj všetky %{count} chyby + other: Niečo ešte stále nieje v poriadku! Prosím skontroluj všetkých %{count} chýb imports: preface: Môžeš importovať dáta ktoré si exportoval/a z iného Mastodon serveru, ako sú napríklad zoznamy ľudí ktorých sleduješ, alebo blokuješ. success: Tvoje dáta boli nahraté úspešne, a budú teraz spracované v danom čase @@ -513,6 +516,7 @@ sk: expires_in_prompt: Nikdy generate: Vygeneruj max_uses: + few: "%{count} použitia" one: jedno použitie other: "%{count} použití" max_uses_prompt: Bez limitov @@ -543,11 +547,13 @@ sk: body: Tu nájdete krátky súhrn správ ktoré ste zmeškali od svojej poslednj návštevi od %{since} mention: "%{name} ťa spomenul/a v:" new_followers_summary: + few: Taktiež, získal/a si %{count} nových následovníkov za tú dobu čo si bol/a preč. Yay! one: Taktiež, získal/a si jedného nového následovníka zatiaľ čo si bol/a preč. Yay! other: Taktiež, získal/a si %{count} nových následovníkov za tú dobu čo si bol/a preč. Yay! subject: + few: "%{count} nové notifikácie od tvojej poslednej návštevy \U0001F418" one: "1 nová notifikácia od tvojej poslednej návštevy \U0001F418" - other: "%{count} nové notifikácie od tvojej poslednej návštevy \U0001F418" + other: "%{count} nových notifikácií od tvojej poslednej návštevy \U0001F418" title: Zatiaľ čo si bol/a preč… favourite: body: 'Tvoj príspevok bol uložený medi obľúbené užívateľa %{name}:' @@ -592,20 +598,6 @@ sk: other: Ostatné publishing: Publikovanie web: Web - push_notifications: - favourite: - title: "%{name} si obľúbil/a tvoj príspevok" - follow: - title: "%{name} ťa teraz následuje" - group: - title: "%{count} notifikácie" - mention: - action_boost: Pozdvihni - action_expand: Ukáž viac - action_favourite: Obľúbené - title: "%{name} ťa spomenul/a" - reblog: - title: "%{name} vyzdvihli tvoj príspevok" remote_follow: acct: Napíš svoju prezývku@doménu z ktorej chceš následovať missing_resource: Nemôžeme nájsť potrebnú presmerovaciu adresu k tvojmu účtu @@ -638,10 +630,10 @@ sk: title: Sezóna settings: authorized_apps: Autorizované aplikácie - back: Späť do Mastodonu - delete: Zmazanie účtu + back: Späť na Mastodon + delete: Vymazanie účtu development: Vývoj - edit_profile: Upraviť profil + edit_profile: Uprav profil export: Exportovať dáta followers: Povolení sledovatelia import: Importovať @@ -655,9 +647,11 @@ sk: attached: description: 'Priložené: %{attached}' image: + few: "%{count} obrázky" one: "%{count} obrázok" other: "%{count} obrázkov" video: + few: "%{count} videá" one: "%{count} video" other: "%{count} videí" content_warning: 'Varovanie o obsahu: %{warning}' @@ -685,6 +679,7 @@ sk: title: Podmienky užívania, a pravidlá o súkromí pre %{instance} themes: default: Mastodon + mastodon-light: Mastodon (svetlý) time: formats: default: "%b %d, %R, %H:%M" @@ -729,5 +724,6 @@ sk: users: invalid_email: Emailová adresa je neplatná invalid_otp_token: Neplatný kód pre dvojfaktorovú autentikáciu + otp_lost_help_html: Pokiaľ si stratil/a prístup k obom, môžeš dať vedieť %{email} seamless_external_login: Si prihlásená/ý cez externú službu, takže nastavenia hesla a emailu ti niesú prístupné. signed_in_as: 'Prihlásený ako:' diff --git a/config/locales/sl.yml b/config/locales/sl.yml new file mode 100644 index 0000000000..00c4d8fb75 --- /dev/null +++ b/config/locales/sl.yml @@ -0,0 +1,104 @@ +--- +sl: + about: + about_hashtag_html: To so javni tuti, označeni z #%{hashtag}. Z njimi se lahko povežete, če imate račun kjerkoli v fediversu. + about_mastodon_html: Mastodon je socialno omrežje, ki temelji na odprtih spletnih protokolih in prosti ter odprtokodni programski opremi. Je decentraliziran, kot e-pošta. + about_this: O Mastodonu + administered_by: 'Upravlja:' + closed_registrations: Registracije so trenutno zaprte na tem vozlišču. Vendar! Tukaj lahko najdete druga vozlišča, na katerih se prijavite in dostopate do istega omrežja od tam. + contact: Kontakt + contact_missing: Ni nastavljeno + contact_unavailable: Ni na voljo + description_headline: Kaj je %{domain}? + domain_count_after: ostala vozlišča + domain_count_before: Povezan z + extended_description_html: | + Dober prostor za pravila + Razširjen opis še ni bil nastavljen. + features: + humane_approach_title: Bolj human pristop + not_a_product_title: Ti si oseba, ne izdelek + real_conversation_title: Zgrajen za pravi pogovor + within_reach_title: Vedno na dosegu roke + generic_description: "%{domain} je en strežnik v omrežju" + hosted_on: Mastodon gostuje na %{domain} + learn_more: Spoznaj več + other_instances: Seznam vozlišč + source_code: Izvorna koda + status_count_after: statusi + status_count_before: Kdo je avtor + user_count_after: uporabniki + user_count_before: Dom za + what_is_mastodon: Kaj je Mastodon? + accounts: + follow: Sledi + followers: Sledilci + following: Sledim + media: Medij + moved_html: "%{name} se je prestavil na %{new_profile_link}:" + nothing_here: Nič ni tukaj! + people_followed_by: Ljudje, ki jim sledi %{name} + people_who_follow: Ljudje, ki sledijo %{name} + posts: Tuti + posts_with_replies: Tuti in odgovori + remote_follow: Oddaljeno sledenje + reserved_username: Uporabniško ime je zasedeno + roles: + admin: Skrbnik + bot: Robot + moderator: Mod + unfollow: Prenehaj slediti + admin: + account_moderation_notes: + create: Pusti sporočilo + created_msg: Uspešno ustvarjena opomba moderiranja! + delete: Izbriši + accounts: + are_you_sure: Ali si prepričan? + avatar: Avatar + by_domain: Domena + change_email: + changed_msg: E-pošta računa je uspešno spremenjena! + current_email: Trenutna E-pošta + label: Spremeni E-pošto + new_email: Nova E-pošta + submit: Spremeni E-pošto + title: Spremeni E-pošto za %{username} + confirm: Potrdi + confirmed: Potrjeno + confirming: Potrjujem + disable: Onemogoči + disable_two_factor_authentication: Onemogoči 2FA + disabled: Onemogočeno + domain: Domena + edit: Uredi + email: E-pošta + email_status: Stanje E-pošte + enable: Omogoči + enabled: Omogočeno + feed_url: URL vir + followers: Sledilci + followers_url: URL sledilci + follows: Sledi + inbox_url: URl v mapi "Prejeto" + ip: IP + location: + all: Vse + local: Lokalno + remote: Oddaljeni + title: Lokacija + login_status: Stanje prijave + media_attachments: Medijske priloge + memorialize: Spremenite v spomin + moderation: + all: Vse + silenced: Utišan + suspended: Suspendiran + title: Moderiranje + moderation_notes: Opombe moderiranja + most_recent_activity: Zadnja aktivnost + most_recent_ip: Zadnji IP + order: + alphabetic: Po abecedi + most_recent: Najnovejše + title: Red diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml index 742c976d19..15c6b00acf 100644 --- a/config/locales/sr-Latn.yml +++ b/config/locales/sr-Latn.yml @@ -52,9 +52,7 @@ sr-Latn: unfollow: Otprati admin: account_moderation_notes: - account: Moderator create: Napravi - created_at: Datum created_msg: Moderatorska beleška uspešno napravljena! delete: Obriši destroyed_msg: Moderatorska beleška uspešno obrisana! @@ -63,6 +61,7 @@ sr-Latn: by_domain: Domen confirm: Potvrdi confirmed: Potvrđeno + confirming: Potvrđujući demote: Ražaluj disable: Isključi disable_two_factor_authentication: Isključi 2FA @@ -71,6 +70,7 @@ sr-Latn: domain: Domen edit: Izmeni email: E-pošta + email_status: Status e-pošte enable: Uključi enabled: Uključeno feed_url: Adresa dovoda @@ -108,6 +108,10 @@ sr-Latn: public: Javno push_subscription_expires: PuSH subscription expires redownload: Osveži avatar + resend_confirmation: + already_confirmed: Ovaj korisnik je već potvrđen + send: Ponovo pošaljite e-poruku za potvrdu + success: E-mail potvrde je uspešno poslat! reset: Resetuj reset_password: Resetuj lozinku resubscribe: Ponovo se pretplati @@ -246,12 +250,8 @@ sr-Latn: are_you_sure: Da li ste sigurni? comment: none: Ništa - delete: Obriši id: ID mark_as_resolved: Označi kao rešen - nsfw: - 'false': Otkrij medijske priloge - 'true': Sakrij medijske priloge report: 'Prijava #%{id}' report_contents: Sadržaj reported_account: Prijavljeni nalog @@ -310,11 +310,8 @@ sr-Latn: delete: Obriši nsfw_off: NSFW isključen nsfw_on: NSFW uključen - execute: Izvrši failed_to_execute: Neuspelo izvršavanje media: - hide: Sakrij multimediju - show: Prikaži multimediju title: Multimedija no_media: Bez multimedije title: Statusi naloga @@ -528,20 +525,6 @@ sr-Latn: other: Ostali publishing: Objavljivanje web: Veb - push_notifications: - favourite: - title: "%{name} je stavio Vaš status za omiljeni" - follow: - title: "%{name} Vas je zapratio" - group: - title: "%{count} obaveštenja" - mention: - action_boost: Podrži - action_expand: Prikaži još - action_favourite: Omiljeni - title: "%{name} Vas je pomenuo" - reblog: - title: "%{name} je podržao(la) Vaš status" remote_follow: acct: Unesite Vaš korisnik@domen sa koga želite da pratite missing_resource: Ne mogu da nađem zahtevanu adresu preusmeravanja za Vaš nalog diff --git a/config/locales/sr.yml b/config/locales/sr.yml index 0d55910a6c..d34a2ecbf2 100644 --- a/config/locales/sr.yml +++ b/config/locales/sr.yml @@ -52,9 +52,7 @@ sr: unfollow: Отпрати admin: account_moderation_notes: - account: Модератор create: Направи - created_at: Датум created_msg: Модераторска белешка успешно направљена! delete: Обриши destroyed_msg: Модераторска белешка успешно обрисана! @@ -63,6 +61,7 @@ sr: by_domain: Домен confirm: Потврди confirmed: Потврђено + confirming: Потврдување demote: Ражалуј disable: Искључи disable_two_factor_authentication: Искључи 2FA @@ -71,6 +70,7 @@ sr: domain: Домен edit: Измени email: Е-пошта + email_status: Е-пошта статус enable: Укључи enabled: Укључено feed_url: Адреса довода @@ -108,6 +108,10 @@ sr: public: Јавно push_subscription_expires: PuSH subscription expires redownload: Освежи аватар + resend_confirmation: + already_confirmed: Овој корисник е веќе потврден + send: Препрати го е-мајлот за потврда + success: Е-пошта за потврда успешно испратена! reset: Ресетуј reset_password: Ресетуј лозинку resubscribe: Поново се претплати @@ -246,12 +250,8 @@ sr: are_you_sure: Да ли сте сигурни? comment: none: Ништа - delete: Обриши id: ID mark_as_resolved: Означи као решен - nsfw: - 'false': Откриј медијске прилоге - 'true': Сакриј медијске прилоге report: 'Пријава #%{id}' report_contents: Садржај reported_account: Пријављени налог @@ -310,11 +310,8 @@ sr: delete: Обриши nsfw_off: NSFW искључен nsfw_on: NSFW укључен - execute: Изврши failed_to_execute: Неуспело извршавање media: - hide: Сакриј мултимедију - show: Прикажи мултимедију title: Мултимедија no_media: Без мултимедије title: Статуси налога @@ -528,20 +525,6 @@ sr: other: Остали publishing: Објављивање web: Веб - push_notifications: - favourite: - title: "%{name} је ставио Ваш статус за омиљени" - follow: - title: "%{name} Вас је запратио" - group: - title: "%{count} обавештења" - mention: - action_boost: Подржи - action_expand: Прикажи још - action_favourite: Омиљени - title: "%{name} Вас је поменуо" - reblog: - title: "%{name} је подржао(ла) Ваш статус" remote_follow: acct: Унесите Ваш корисник@домен са кога желите да пратите missing_resource: Не могу да нађем захтевану адресу преусмеравања за Ваш налог diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 845248652f..23ea7ddd1a 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -40,6 +40,7 @@ sv: following: Följer media: Media moved_html: "%{name} har flyttat till %{new_profile_link}:" + network_hidden: Denna information är inte tillgänglig nothing_here: Det finns inget här! people_followed_by: Personer som %{name} följer people_who_follow: Personer som följer %{name} @@ -49,13 +50,12 @@ sv: reserved_username: Användarnamnet är reserverat roles: admin: Admin + bot: Bot moderator: Moderator unfollow: Sluta följa admin: account_moderation_notes: - account: Moderator - create: Skapa - created_at: Datum + create: Lämna kommentar created_msg: Modereringsnotering skapad utan problem! delete: Ta bort destroyed_msg: Modereringsnotering borttagen utan problem! @@ -72,6 +72,7 @@ sv: title: Byt E-postadress för %{username} confirm: Bekräfta confirmed: Bekräftad + confirming: Bekräftande demote: Degradera disable: inaktivera disable_two_factor_authentication: Inaktivera 2FA @@ -80,6 +81,7 @@ sv: domain: Domän edit: Redigera email: E-post + email_status: E-poststatus enable: Aktivera enabled: Aktiverad feed_url: Flödes URL @@ -118,6 +120,10 @@ sv: push_subscription_expires: PuSH-prenumerationen löper ut redownload: Uppdatera avatar remove_avatar: Ta bort avatar + resend_confirmation: + already_confirmed: Den här användaren är redan bekräftad + send: Skicka om e-postbekräftelse + success: Bekräftelsemeddelande skickas framgångsrikt! reset: Återställ reset_password: Återställ lösenord resubscribe: Starta en ny prenumeration @@ -269,7 +275,6 @@ sv: comment: none: Ingen created_at: Anmäld - delete: Radera id: ID mark_as_resolved: Markera som löst mark_as_unresolved: Markera som olöst @@ -278,10 +283,7 @@ sv: create_and_resolve: Lös med anteckning create_and_unresolve: Återuppta med anteckning delete: Radera - placeholder: Beskriv vilka åtgärder som vidtagits eller andra uppdateringar till den här anmälan… - nsfw: - 'false': Visa bifogade mediafiler - 'true': Dölj bifogade mediafiler + placeholder: Beskriv vilka åtgärder som vidtagits eller andra uppdateringar till den här anmälan. reopen: Återuppta anmälan report: 'Anmäl #%{id}' report_contents: Innehåll @@ -356,11 +358,8 @@ sv: delete: Radera nsfw_off: Markera som ej känslig nsfw_on: Markera som känslig - execute: Utför failed_to_execute: Misslyckades att utföra media: - hide: Dölj media - show: Visa media title: Media no_media: Ingen media title: Kontostatus @@ -376,6 +375,7 @@ sv: admin_mailer: new_report: body: "%{reporter} har rapporterat %{target}" + body_remote: Någon från %{domain} har rapporterat %{target} subject: Ny rapport för %{instance} (#%{id}) application_mailer: notification_preferences: Ändra e-postinställningar @@ -465,7 +465,7 @@ sv: archive_takeout: date: Datum download: Ladda ner ditt arkiv - hint_html: Du kan begära ett arkiv av dina toots och uppladdad media. Den exporterade datan kommer att vara i ActivityPub-format och läsbar av kompatibel programvara. + hint_html: Du kan begära ett arkiv av dina toots och uppladdad media. Den exporterade datan kommer att vara i ActivityPub-format och läsbar av kompatibel programvara. Du kan begära ett arkiv var sjunde dag. in_progress: Kompilerar ditt arkiv... request: Efterfråga ditt arkiv size: Storlek @@ -595,20 +595,6 @@ sv: other: Annat publishing: Publicering web: Webb - push_notifications: - favourite: - title: "%{name} favoriserade din status" - follow: - title: "%{name} följer nu dig" - group: - title: "%{count} meddelanden" - mention: - action_boost: Knuffa - action_expand: Visa mer - action_favourite: Favoriter - title: "%{name} nämnde dig" - reblog: - title: "%{name} boostade din status" remote_follow: acct: Ange ditt användarnamn@domän du vill följa från missing_resource: Det gick inte att hitta den begärda omdirigeringsadressen för ditt konto @@ -683,6 +669,7 @@ sv: video: one: "%{count} video" other: "%{count} videor" + boosted_from_html: Boosted från %{acct_link} content_warning: 'Innehållsvarning: %{warning}' disallowed_hashtags: one: 'innehöll en otillåten hashtag: %{tags}' @@ -711,6 +698,7 @@ sv: terms: title: "%{instance} Användarvillkor och Sekretesspolicy" themes: + contrast: Hög kontrast default: Mastodon time: formats: @@ -757,5 +745,6 @@ sv: users: invalid_email: E-postadressen är ogiltig invalid_otp_token: Ogiltig tvåfaktorkod + otp_lost_help_html: Om du förlorat åtkomst till båda kan du komma i kontakt med %{email} seamless_external_login: Du är inloggad via en extern tjänst, så lösenord och e-postinställningar är inte tillgängliga. signed_in_as: 'Inloggad som:' diff --git a/config/locales/th.yml b/config/locales/th.yml index 350b93b521..6804dbd13d 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -29,11 +29,13 @@ th: are_you_sure: แน่ใจนะ? confirm: ยืนยัน confirmed: ยึนยันแล้ว + confirming: ยืนยัน disable_two_factor_authentication: Disable 2FA display_name: ชื่อสำหรับดีสเพล domain: โดแมน edit: แก้ไข email: อีเมล์ + email_status: สถานะอีเมล feed_url: Feed URL followers: ผู้ติดตาม follows: ติดตาม @@ -59,6 +61,10 @@ th: profile_url: Profile URL public: สาธารณะ push_subscription_expires: PuSH subscription expires + resend_confirmation: + already_confirmed: ผู้ใช้รายนี้ได้รับการยืนยันแล้ว + send: ส่งอีเมลยืนยันอีกครั้ง + success: ยืนยันอีเมลเรียบร้อยแล้ว! reset_password: รีเซ็ตรหัสผ่าน salmon_url: Salmon URL show: @@ -109,7 +115,6 @@ th: reports: comment: none: None - delete: ลบ id: ไอดี mark_as_resolved: ทำเครื่องหมายว่าจัดการแล้ว report: 'Report #%{id}' diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 6e7aeb77e9..8bafbface2 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -29,10 +29,12 @@ tr: are_you_sure: Emin misiniz? confirm: Onayla confirmed: Onaylandı + confirming: Onaylama display_name: Görünen adınız domain: Sunucu edit: Düzenle email: E-posta + email_status: Email Durumu feed_url: Besleme linki followers: Takipçiler follows: Takip edilen @@ -58,6 +60,10 @@ tr: profile_url: Profil linki public: Herkese açık push_subscription_expires: PuSH aboneliği dolumu + resend_confirmation: + already_confirmed: Bu kullanıcı zaten onaylandı + send: Doğrulama epostasını yeniden gönder + success: Onay e-postası başarıyla gönderildi! reset_password: Parolayı değiştir salmon_url: Salmon Linki show: @@ -108,7 +114,6 @@ tr: reports: comment: none: Yok - delete: Sil id: ID mark_as_resolved: Giderildi olarak işaretle report: 'Şikayet #%{id}' diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 44f64b5c9e..6fe46b4d98 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -100,7 +100,6 @@ uk: reports: comment: none: Немає - delete: Видалити id: ID mark_as_resolved: Відмітити як вирішену report: 'Скарга #%{id}' diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 78c72bd302..357575a3b3 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -4,6 +4,7 @@ zh-CN: about_hashtag_html: 这里展示的是带有话题标签 #%{hashtag} 的公开嘟文。如果你想与他们互动,你需要在任意一个 Mastodon 实例或与其兼容的网站上拥有一个帐户。 about_mastodon_html: Mastodon(长毛象)是一个建立在开放式网络协议和自由、开源软件之上的社交网络,有着类似于电子邮件的分布式设计。 about_this: 关于本实例 + administered_by: 本实例的管理员: closed_registrations: 这个实例目前没有开放注册。不过,你可以前往其他实例注册一个帐户,同样可以加入到这个网络中哦! contact: 联系方式 contact_missing: 未设定 @@ -25,9 +26,9 @@ zh-CN: within_reach_title: 始终触手可及 generic_description: "%{domain} 是这个庞大网络中的一台服务器" hosted_on: 一个在 %{domain} 上运行的 Mastodon 实例 - learn_more: 详细了解 + learn_more: 了解详情 other_instances: 其他实例 - source_code: 源码 + source_code: 源代码 status_count_after: 条嘟文 status_count_before: 他们共嘟出了 user_count_after: 位用户 @@ -39,6 +40,7 @@ zh-CN: following: 正在关注 media: 媒体 moved_html: "%{name} 已经迁移到 %{new_profile_link}:" + network_hidden: 此信息不可用。 nothing_here: 这里神马都没有! people_followed_by: "%{name} 关注的人" people_who_follow: 关注 %{name} 的人 @@ -48,21 +50,29 @@ zh-CN: reserved_username: 此用户名已被保留 roles: admin: 管理员 + bot: 机器人 moderator: 监察员 unfollow: 取消关注 admin: account_moderation_notes: - account: 管理员 create: 新建 - created_at: 日期 created_msg: 管理备忘建立成功! delete: 删除 destroyed_msg: 管理备忘删除成功! accounts: are_you_sure: 你确定吗? + avatar: 头像 by_domain: 域名 + change_email: + changed_msg: 帐户电子邮件地址更改成功! + current_email: 当前的电子邮件地址 + label: 更改电子邮件地址 + new_email: 新的电子邮件地址 + submit: 更改电子邮件地址 + title: 为 %{username} 更改电子邮件地址 confirm: 确认 confirmed: 已确认 + confirming: 确认 demote: 降任 disable: 停用 disable_two_factor_authentication: 停用双重认证 @@ -71,6 +81,7 @@ zh-CN: domain: 域名 edit: 编辑 email: 电子邮件地址 + email_status: 电子邮件地址状态 enable: 启用 enabled: 已启用 feed_url: 订阅 URL @@ -108,6 +119,11 @@ zh-CN: public: 公开页面 push_subscription_expires: PuSH 订阅过期时间 redownload: 刷新头像 + remove_avatar: 删除头像 + resend_confirmation: + already_confirmed: 该用户已被确认 + send: 重发确认邮件 + success: 确认邮件发送成功! reset: 重置 reset_password: 重置密码 resubscribe: 重新订阅 @@ -128,6 +144,7 @@ zh-CN: statuses: 嘟文 subscribe: 订阅 title: 用户 + unconfirmed_email: 待验证的电子邮件地址 undo_silenced: 解除隐藏 undo_suspension: 解除封禁 unsubscribe: 取消订阅 @@ -135,6 +152,8 @@ zh-CN: web: 站内页面 action_logs: actions: + assigned_to_self_report: "%{name} 接管了举报 %{target}" + change_email_user: "%{name} 更改了用户 %{target} 的电子邮件地址" confirm_user: "%{name} 确认了用户 %{target} 的电子邮件地址" create_custom_emoji: "%{name} 添加了新的自定义表情 %{target}" create_domain_block: "%{name} 屏蔽了域名 %{target}" @@ -150,10 +169,13 @@ zh-CN: enable_user: "%{name} 将用户 %{target} 设置为允许登录" memorialize_account: "%{name} 将 %{target} 设置为追悼帐户" promote_user: "%{name} 对用户 %{target} 进行了升任操作" + remove_avatar_user: "%{name} 删除了 %{target} 的头像" + reopen_report: "%{name} 重开了举报 %{target}" reset_password_user: "%{name} 重置了用户 %{target} 的密码" resolve_report: "%{name} 处理了举报 %{target}" silence_account: "%{name} 隐藏了用户 %{target}" suspend_account: "%{name} 封禁了用户 %{target}" + unassigned_report: "%{name} 放弃了举报 %{target} 的接管" unsilence_account: "%{name} 解除了用户 %{target} 的隐藏状态" unsuspend_account: "%{name} 解除了用户 %{target} 的封禁状态" update_custom_emoji: "%{name} 更新了自定义表情 %{target}" @@ -237,28 +259,44 @@ zh-CN: expired: 已失效 title: 筛选 title: 邀请用户 + report_notes: + created_msg: 举报记录建立成功! + destroyed_msg: 举报记录删除成功! reports: + account: + note: 条记录 + report: 条举报 action_taken_by: 操作执行者 are_you_sure: 你确定吗? + assign_to_self: 接管 + assigned: 已接管的监察员 comment: none: 没有 - delete: 删除 + created_at: 举报时间 id: ID mark_as_resolved: 标记为“已处理” - nsfw: - 'false': 取消 NSFW 标记 - 'true': 添加 NSFW 标记 + mark_as_unresolved: 标记为“未处理” + notes: + create: 添加记录 + create_and_resolve: 添加记录并标记为“已处理” + create_and_unresolve: 添加记录并重开 + delete: 删除 + placeholder: 描述已经执行的操作,或其他任何相关的跟进情况 + reopen: 重开举报 report: '举报 #%{id}' report_contents: 内容 reported_account: 举报用户 reported_by: 举报人 resolved: 已处理 + resolved_msg: 举报处理成功! silence_account: 隐藏用户 status: 状态 suspend_account: 封禁用户 target: 被举报人 title: 举报 + unassign: 取消接管 unresolved: 未处理 + updated_at: 更新时间 view: 查看 settings: activity_api_enabled: @@ -270,6 +308,9 @@ zh-CN: contact_information: email: 用于联系的公开电子邮件地址 username: 用于联系的公开用户名 + hero: + desc_html: 用于在首页展示。推荐分辨率 600×100px 以上。未指定的情况下将默认使用本站缩略图 + title: 主题图片 peers_api_enabled: desc_html: 截至目前本实例在网络中已发现的域名 title: 公开已知实例的列表 @@ -286,11 +327,14 @@ zh-CN: open: desc_html: 允许所有人建立帐户 title: 开放注册 + show_known_fediverse_at_about_page: + desc_html: 启用此选项将会在预览中显示来自已知实例的嘟文,否则只会显示本站时间轴的内容 + title: 在时间轴预览中显示已知实例 show_staff_badge: desc_html: 在个人资料页上显示管理人员标志 title: 显示管理人员标志 site_description: - desc_html: 用于首页展示以及 meta 标签中的网站简介。可以使用 HTML 标签,包括 <a> 和 <em>。 + desc_html: 用于首页展示以及 meta 标签中的网站简介。可以使用 HTML 标签,包括 <a> 和 <em> title: 本站简介 site_description_extended: desc_html: 可以填写行为守则、规定、指南或其他本站特有的内容。可以使用 HTML 标签 @@ -310,13 +354,10 @@ zh-CN: back_to_account: 返回帐户信息页 batch: delete: 删除 - nsfw_off: 取消 NSFW 标记 - nsfw_on: 添加 NSFW 标记 - execute: 执行 + nsfw_off: 标记为非敏感内容 + nsfw_on: 标记为敏感内容 failed_to_execute: 执行失败 media: - hide: 隐藏媒体文件 - show: 显示媒体文件 title: 媒体文件 no_media: 不含媒体文件 title: 帐户嘟文 @@ -331,7 +372,8 @@ zh-CN: title: 管理 admin_mailer: new_report: - body: "%{reporter} 举报了用户 %{target}。" + body: "%{reporter} 举报了用户 %{target}" + body_remote: 来自 %{domain} 的用户举报了用户 %{target} subject: 来自 %{instance} 的用户举报(#%{id}) application_mailer: notification_preferences: 更改电子邮件首选项 @@ -350,6 +392,8 @@ zh-CN: your_token: 你的访问令牌 auth: agreement_html: 注册即表示你同意遵守本实例的相关规定和我们的使用条款。 + change_password: 密码 + confirm_email: 确认电子邮件地址 delete_account: 删除帐户 delete_account_html: 如果你想删除你的帐户,请点击这里继续。你需要确认你的操作。 didnt_get_confirmation: 没有收到确认邮件? @@ -359,12 +403,19 @@ zh-CN: logout: 登出 migrate_account: 迁移到另一个帐户 migrate_account_html: 如果你希望引导他人关注另一个帐户,请点击这里进行设置。 + or: 或者 + or_log_in_with: 或通过其他方式登录 + providers: + cas: CAS + saml: SAML register: 注册 + register_elsewhere: 前往其他实例注册 resend_confirmation: 重新发送确认邮件 reset_password: 重置密码 security: 帐户安全 set_new_password: 设置新密码 authorize_follow: + already_following: 你已经在关注此用户了 error: 对不起,寻找这个跨站用户时出错 follow: 关注 follow_request: 关注请求已发送给: @@ -409,6 +460,13 @@ zh-CN: title: 这个页面有问题 noscript_html: 使用 Mastodon 网页版应用需要启用 JavaScript。你也可以选择适用于你的平台的 Mastodon 应用。 exports: + archive_takeout: + date: 日期 + download: 下载你的存档 + hint_html: 你可以请求一份帐户数据存档,其中包含你的嘟文和已上传的媒体文件。导出的数据为 ActivityPub 格式,因而可以被兼容的软件读取。每次允许请求存档的间隔至少为 7 天。 + in_progress: 正在准备你的存档…… + request: 请求你的存档 + size: 大小 blocks: 屏蔽的用户 csv: CSV follows: 关注的用户 @@ -448,6 +506,7 @@ zh-CN: '21600': 6 小时后 '3600': 1 小时后 '43200': 12 小时后 + '604800': 1 周后 '86400': 1 天后 expires_in_prompt: 永不过期 generate: 生成邀请链接 @@ -518,7 +577,9 @@ zh-CN: trillion: T unit: '' pagination: + newer: 更新 next: 下一页 + older: 更早 prev: 上一页 truncate: "…" preferences: @@ -526,25 +587,15 @@ zh-CN: other: 其他 publishing: 发布 web: 站内 - push_notifications: - favourite: - title: "%{name} 收藏了你的嘟文" - follow: - title: "%{name} 关注了你" - group: - title: "%{count} 条新通知" - mention: - action_boost: 转嘟 - action_expand: 显示更多 - action_favourite: 收藏 - title: "%{name} 提到了你" - reblog: - title: "%{name} 转嘟了你的嘟文" remote_follow: acct: 请输入你的“用户名@实例域名” missing_resource: 无法确定你的帐户的跳转 URL proceed: 确认关注 prompt: 你正准备关注: + remote_unfollow: + error: 错误 + title: 标题 + unfollowed: 已取消关注 sessions: activity: 最后一次活跃的时间 browser: 浏览器 @@ -553,12 +604,14 @@ zh-CN: blackberry: Blackberry chrome: Chrome edge: Microsoft Edge + electron: Electron firefox: Firefox generic: 未知浏览器 ie: Internet Explorer micro_messenger: 微信 nokia: Nokia S40 Ovi 浏览器 opera: Opera + otter: Otter phantom_js: PhantomJS qq: QQ浏览器 safari: Safari @@ -586,7 +639,7 @@ zh-CN: title: 会话 settings: authorized_apps: 已授权的应用 - back: 回到 Mastodon + back: 返回 Mastodon delete: 删除帐户 development: 开发 edit_profile: 更改个人资料 @@ -600,6 +653,15 @@ zh-CN: two_factor_authentication: 双重认证 your_apps: 你的应用 statuses: + attached: + description: 附加媒体:%{attached} + image: "%{count} 张图片" + video: "%{count} 个视频" + boosted_from_html: 转嘟自 %{acct_link} + content_warning: 内容警告:%{warning} + disallowed_hashtags: + one: 包含了一个禁止的话题标签:%{tags} + other: 包含了这些禁止的话题标签:%{tags} open_in_web: 在站内打开 over_character_limit: 超过了 %{max} 字的限制 pin_errors: @@ -623,6 +685,10 @@ zh-CN: sensitive_content: 敏感内容 terms: title: "%{instance} 使用条款和隐私权政策" + themes: + contrast: 高对比度 + default: Mastodon + mastodon-light: Mastodon(亮色主题) time: formats: default: "%Y年%-m月%d日 %H:%M" @@ -643,6 +709,10 @@ zh-CN: setup: 设置 wrong_code: 输入的认证码无效!请核对一下你的设备显示的时间,如果正确,你可能需要联系一下实例的管理员,让他们校准服务器的时间。 user_mailer: + backup_ready: + explanation: 你请求了一份 Mastodon 帐户的完整备份。现在你可以下载了! + subject: 你的存档已经准备完毕 + title: 存档导出 welcome: edit_profile_action: 设置个人资料 edit_profile_step: 你可以自定义你的个人资料,包括上传头像、横幅图片、更改昵称等等。如果你想在新的关注者关注你之前对他们进行审核,你也可以选择为你的帐户开启保护。 @@ -664,4 +734,6 @@ zh-CN: users: invalid_email: 输入的电子邮件地址无效 invalid_otp_token: 输入的双重认证代码无效 + otp_lost_help_html: 如果你不慎丢失了所有的代码,请联系 %{email} 寻求帮助 + seamless_external_login: 因为你是通过外部服务登录的,所以密码和电子邮件地址设置都不可用。 signed_in_as: 当前登录的帐户: diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml index a27b0c04c8..c489d8bd49 100644 --- a/config/locales/zh-HK.yml +++ b/config/locales/zh-HK.yml @@ -40,6 +40,7 @@ zh-HK: following: 正在關注 media: 媒體 moved_html: "%{name} 已經轉移到 %{new_profile_link}:" + network_hidden: 此信息不可用 nothing_here: 暫時未有內容可以顯示 people_followed_by: "%{name} 關注的人" people_who_follow: 關注 %{name} 的人 @@ -49,13 +50,12 @@ zh-HK: reserved_username: 此用戶名已被保留 roles: admin: 管理員 - moderator: 監察员 + bot: 機械人 + moderator: 監察員 unfollow: 取消關注 admin: account_moderation_notes: - account: 管理員 - create: 新增 - created_at: 日期 + create: 記錄 created_msg: 管理記錄已新增 delete: 刪除 destroyed_msg: 管理記錄已被刪除 @@ -72,6 +72,7 @@ zh-HK: title: 改變 %{username} 的電郵 confirm: 確定 confirmed: 已確定 + confirming: 確定 demote: 降任 disable: 停用 disable_two_factor_authentication: 停用雙重認證 @@ -80,9 +81,10 @@ zh-HK: domain: 域名 edit: 編輯 email: 電郵地址 + email_status: 电子邮件状态 enable: 啟用 enabled: 已啟用 - feed_url: Feed URL + feed_url: 訂閱 URL followers: 關注者 followers_url: 關注者(Followers)URL follows: 正在關注 @@ -118,13 +120,17 @@ zh-HK: push_subscription_expires: PuSH 訂閱過期 redownload: 更新頭像 remove_avatar: 取消頭像 + resend_confirmation: + already_confirmed: 该用户已被确认 + send: 重发确认邮件 + success: 确认电子邮件成功发送! reset: 重設 reset_password: 重設密碼 resubscribe: 重新訂閱 role: 身份 roles: admin: 管理員 - moderator: 監察员 + moderator: 監察員 staff: 管理人員 user: 普通用戶 salmon_url: Salmon 反饋 URL @@ -223,6 +229,7 @@ zh-HK: severity: 阻隔分級 show: affected_accounts: + one: 資料庫中有 %{count} 個用戶受影響 other: 資料庫中有%{count}個用戶受影響 retroactive: silence: 對此域名的所有用戶取消靜音 @@ -268,7 +275,6 @@ zh-HK: comment: none: 沒有 created_at: 日期 - delete: 刪除 id: ID mark_as_resolved: 標示為「已處理」 mark_as_unresolved: 標示為「未處理」 @@ -277,10 +283,7 @@ zh-HK: create_and_resolve: 建立筆記並標示為「已處理」 create_and_unresolve: 建立筆記並標示為「未處理」 delete: 刪除 - placeholder: 記錄已執行的動作,或其他更新 - nsfw: - 'false': 取消 NSFW 標記 - 'true': 添加 NSFW 標記 + placeholder: 記錄已執行的動作,或其他相關的更新…… reopen: 重開舉報 report: '舉報 #%{id}' report_contents: 舉報內容 @@ -355,11 +358,8 @@ zh-HK: delete: 刪除 nsfw_off: 取消 NSFW 標記 nsfw_on: 添加 NSFW 標記 - execute: 執行 failed_to_execute: 執行失敗 media: - hide: 隱藏媒體檔案 - show: 顯示媒體檔案 title: 媒體檔案 no_media: 不含媒體檔案 title: 帳戶文章 @@ -375,6 +375,7 @@ zh-HK: admin_mailer: new_report: body: "%{reporter} 舉報了用戶 %{target}。" + body_remote: 來自 %{domain} 的用戶舉報了用戶 %{target} subject: 來自 %{instance} 的用戶舉報(#%{id}) application_mailer: notification_preferences: 更改電郵首選項 @@ -464,7 +465,7 @@ zh-HK: archive_takeout: date: 日期 download: 下載檔案 - hint_html: 你可以下載包含你的文章和媒體的檔案。資料以 ActivityPub 格式儲存,可用於相容的軟體。 + hint_html: 你可以下載包含你的文章和媒體的檔案。資料以 ActivityPub 格式儲存,可用於相容的軟體。你可以每七天下載一次。 in_progress: 檔案製作中... request: 下載檔案 size: 檔案大小 @@ -513,7 +514,9 @@ zh-HK: '86400': 1 天後 expires_in_prompt: 永不過期 generate: 生成邀請連結 - max_uses: "%{count} 次" + max_uses: + one: 1 次 + other: "%{count} 次" max_uses_prompt: 無限制 prompt: 生成分享連結,邀請他人在本服務站註冊 table: @@ -580,7 +583,6 @@ zh-HK: quadrillion: Q thousand: K trillion: T - unit: '' pagination: newer: 較新 next: 下一頁 @@ -592,20 +594,6 @@ zh-HK: other: 其他 publishing: 發佈 web: 站内 - push_notifications: - favourite: - title: "%{name} 收藏了你的文章" - follow: - title: "%{name} 關注了你" - group: - title: "%{count} 條新通知" - mention: - action_boost: 轉推 - action_expand: 顯示更多 - action_favourite: 收藏 - title: "%{name} 提到了你" - reblog: - title: "%{name} 轉推了你的文章" remote_follow: acct: 請輸入你的︰用戶名稱@服務點域名 missing_resource: 無法找到你用戶的轉接網址 @@ -672,9 +660,17 @@ zh-HK: statuses: attached: description: 附件: %{attached} - image: "%{count} 張圖片" - video: "%{count} 段影片" - content_warning: 'Content warning: %{warning}' + image: + one: "%{count} 幅圖片" + other: "%{count} 幅圖片" + video: + one: "%{count} 段影片" + other: "%{count} 段影片" + boosted_from_html: 轉推自 %{acct_link} + content_warning: 內容警告: %{warning} + disallowed_hashtags: + one: 包含不允許的標籤: %{tags} + other: 包含不允許的標籤: %{tags} open_in_web: 開啟網頁 over_character_limit: 超過了 %{max} 字的限制 pin_errors: @@ -698,6 +694,10 @@ zh-HK: sensitive_content: 敏感內容 terms: title: "%{instance} 使用條款和隱私權政策" + themes: + contrast: 高對比 + default: 萬象 + mastodon-light: 萬象(亮色主題) time: formats: default: "%Y年%-m月%d日 %H:%M" @@ -743,5 +743,6 @@ zh-HK: users: invalid_email: 電郵地址格式不正確 invalid_otp_token: 雙重認證確認碼不正確 + otp_lost_help_html: 如果你無法訪問這兩者,可以通過 %{email} 與我們聯繫。 seamless_external_login: 由於你是從外部系統登入,所以不能設定密碼和電郵。 signed_in_as: 目前登入的帳戶: diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index f69d22d79a..d8c0f89a96 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -80,7 +80,6 @@ zh-TW: reports: comment: none: 無 - delete: 刪除 id: ID mark_as_resolved: 標記為已解決 report: '檢舉 #%{id}' diff --git a/config/routes.rb b/config/routes.rb index 9d4aa00edb..6dd9975b4e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -14,7 +14,9 @@ Rails.application.routes.draw do end use_doorkeeper do - controllers authorizations: 'oauth/authorizations', authorized_applications: 'oauth/authorized_applications' + controllers authorizations: 'oauth/authorizations', + authorized_applications: 'oauth/authorized_applications', + tokens: 'oauth/tokens' end get '.well-known/host-meta', to: 'well_known/host_meta#show', as: :host_meta, defaults: { format: 'xml' } @@ -144,7 +146,7 @@ Rails.application.routes.draw do end resources :reports, only: [:index, :show, :update] do - resources :reported_statuses, only: [:create, :update, :destroy] + resources :reported_statuses, only: [:create] end resources :report_notes, only: [:create, :destroy] @@ -164,9 +166,14 @@ Rails.application.routes.draw do resource :reset, only: [:create] resource :silence, only: [:create, :destroy] resource :suspension, only: [:create, :destroy] - resource :confirmation, only: [:create] resources :statuses, only: [:index, :create, :update, :destroy] + resource :confirmation, only: [:create] do + collection do + post :resend + end + end + resource :role do member do post :promote @@ -264,6 +271,7 @@ Rails.application.routes.draw do resources :favourites, only: [:index] resources :bookmarks, only: [:index] resources :reports, only: [:index, :create] + resources :trends, only: [:index] namespace :apps do get :verify_credentials, to: 'credentials#show' @@ -319,6 +327,14 @@ Rails.application.routes.draw do resources :lists, only: [:index, :create, :show, :update, :destroy] do resource :accounts, only: [:show, :create, :destroy], controller: 'lists/accounts' end + + namespace :push do + resource :subscription, only: [:create, :show, :update, :destroy] + end + end + + namespace :v2 do + get '/search', to: 'search#index', as: :search end namespace :web do diff --git a/config/settings.yml b/config/settings.yml index a92a0bfd0e..4a3720c2de 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -20,6 +20,7 @@ defaults: &defaults min_invite_role: 'admin' show_staff_badge: true default_sensitive: false + hide_network: false unfollow_modal: false boost_modal: false favourite_modal: false diff --git a/config/webpack/configuration.js b/config/webpack/configuration.js index 47883b68d2..4d0d28582a 100644 --- a/config/webpack/configuration.js +++ b/config/webpack/configuration.js @@ -80,7 +80,10 @@ module.exports = { settings, core, flavours, - env, + env: { + CDN_HOST: env.CDN_HOST, + NODE_ENV: env.NODE_ENV, + }, loadersDir, output, }; diff --git a/config/webpack/production.js b/config/webpack/production.js index e1c681232e..1469a948f2 100644 --- a/config/webpack/production.js +++ b/config/webpack/production.js @@ -8,6 +8,7 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPl const OfflinePlugin = require('offline-plugin'); const { publicPath } = require('./configuration.js'); const path = require('path'); +const { URL } = require('whatwg-url'); let compressionAlgorithm; try { @@ -19,6 +20,21 @@ try { compressionAlgorithm = 'gzip'; } +let attachmentHost; + +if (process.env.S3_ENABLED === 'true') { + if (process.env.S3_CLOUDFRONT_HOST) { + attachmentHost = process.env.S3_CLOUDFRONT_HOST; + } else { + attachmentHost = process.env.S3_HOSTNAME || `s3-${process.env.S3_REGION || 'us-east-1'}.amazonaws.com`; + } +} else if (process.env.SWIFT_ENABLED === 'true') { + const { host } = new URL(process.env.SWIFT_OBJECT_URL); + attachmentHost = host; +} else { + attachmentHost = null; +} + module.exports = merge(sharedConfig, { output: { filename: '[name]-[chunkhash].js', @@ -90,7 +106,7 @@ module.exports = merge(sharedConfig, { '**/*.woff', ], ServiceWorker: { - entry: `imports-loader?process.env=>${encodeURIComponent(JSON.stringify(process.env))}!${encodeURI(path.join(__dirname, '../../app/javascript/mastodon/service_worker/entry.js'))}`, + entry: `imports-loader?ATTACHMENT_HOST=>${encodeURIComponent(JSON.stringify(attachmentHost))}!${encodeURI(path.join(__dirname, '../../app/javascript/mastodon/service_worker/entry.js'))}`, cacheName: 'mastodon', output: '../assets/sw.js', publicPath: '/sw.js', diff --git a/db/migrate/20180416210259_add_uri_to_relationships.rb b/db/migrate/20180416210259_add_uri_to_relationships.rb new file mode 100644 index 0000000000..d8eaca450b --- /dev/null +++ b/db/migrate/20180416210259_add_uri_to_relationships.rb @@ -0,0 +1,7 @@ +class AddUriToRelationships < ActiveRecord::Migration[5.2] + def change + add_column :follows, :uri, :string + add_column :follow_requests, :uri, :string + add_column :blocks, :uri, :string + end +end diff --git a/db/migrate/20180506221944_add_actor_type_to_accounts.rb b/db/migrate/20180506221944_add_actor_type_to_accounts.rb new file mode 100644 index 0000000000..7cfed640f7 --- /dev/null +++ b/db/migrate/20180506221944_add_actor_type_to_accounts.rb @@ -0,0 +1,5 @@ +class AddActorTypeToAccounts < ActiveRecord::Migration[5.2] + def change + add_column :accounts, :actor_type, :string + end +end diff --git a/db/migrate/20180510214435_add_access_token_id_to_web_push_subscriptions.rb b/db/migrate/20180510214435_add_access_token_id_to_web_push_subscriptions.rb new file mode 100644 index 0000000000..94ef8e0f59 --- /dev/null +++ b/db/migrate/20180510214435_add_access_token_id_to_web_push_subscriptions.rb @@ -0,0 +1,6 @@ +class AddAccessTokenIdToWebPushSubscriptions < ActiveRecord::Migration[5.2] + def change + add_reference :web_push_subscriptions, :access_token, null: true, default: nil, foreign_key: { on_delete: :cascade, to_table: :oauth_access_tokens }, index: false + add_reference :web_push_subscriptions, :user, null: true, default: nil, foreign_key: { on_delete: :cascade }, index: false + end +end diff --git a/db/migrate/20180510230049_migrate_web_push_subscriptions.rb b/db/migrate/20180510230049_migrate_web_push_subscriptions.rb new file mode 100644 index 0000000000..6de1bed795 --- /dev/null +++ b/db/migrate/20180510230049_migrate_web_push_subscriptions.rb @@ -0,0 +1,13 @@ +class MigrateWebPushSubscriptions < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def up + add_index :web_push_subscriptions, :user_id, algorithm: :concurrently + add_index :web_push_subscriptions, :access_token_id, algorithm: :concurrently + end + + def down + remove_index :web_push_subscriptions, :user_id + remove_index :web_push_subscriptions, :access_token_id + end +end diff --git a/db/migrate/20180514130000_improve_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb b/db/migrate/20180514130000_improve_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb new file mode 100644 index 0000000000..2573bdf949 --- /dev/null +++ b/db/migrate/20180514130000_improve_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class ImproveIndexOnStatusesForApiV1AccountsAccountIdStatuses < ActiveRecord::Migration[5.1] + disable_ddl_transaction! + + def change + add_index :statuses, [:account_id, :id, :visibility], where: 'visibility IN (0, 1, 2)', algorithm: :concurrently + add_index :statuses, [:account_id, :id], where: 'visibility = 3', algorithm: :concurrently + remove_index :statuses, column: [:account_id, :id, :visibility, :updated_at], order: { id: :desc }, algorithm: :concurrently, name: :index_statuses_20180106 + end +end diff --git a/db/migrate/20180514140000_revert_index_change_on_statuses_for_api_v1_accounts_account_id_statuses.rb b/db/migrate/20180514140000_revert_index_change_on_statuses_for_api_v1_accounts_account_id_statuses.rb new file mode 100644 index 0000000000..b6b9f96ae8 --- /dev/null +++ b/db/migrate/20180514140000_revert_index_change_on_statuses_for_api_v1_accounts_account_id_statuses.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class RevertIndexChangeOnStatusesForApiV1AccountsAccountIdStatuses < ActiveRecord::Migration[5.1] + disable_ddl_transaction! + + def change + safety_assured do + add_index :statuses, [:account_id, :id, :visibility, :updated_at], order: { id: :desc }, algorithm: :concurrently, name: :index_statuses_20180106 + end + + remove_index :statuses, column: [:account_id, :id, :visibility], where: 'visibility IN (0, 1, 2)', algorithm: :concurrently + remove_index :statuses, column: [:account_id, :id], where: 'visibility = 3', algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index 6413db3f08..def0505a6f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2018_04_10_220657) do +ActiveRecord::Schema.define(version: 2018_05_14_140000) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -75,6 +75,7 @@ ActiveRecord::Schema.define(version: 2018_04_10_220657) do t.bigint "moved_to_account_id" t.string "featured_collection_url" t.jsonb "fields" + t.string "actor_type" t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower" t.index ["uri"], name: "index_accounts_on_uri" @@ -111,6 +112,7 @@ ActiveRecord::Schema.define(version: 2018_04_10_220657) do t.datetime "updated_at", null: false t.bigint "account_id", null: false t.bigint "target_account_id", null: false + t.string "uri" t.index ["account_id", "target_account_id"], name: "index_blocks_on_account_id_and_target_account_id", unique: true end @@ -185,6 +187,7 @@ ActiveRecord::Schema.define(version: 2018_04_10_220657) do t.bigint "account_id", null: false t.bigint "target_account_id", null: false t.boolean "show_reblogs", default: true, null: false + t.string "uri" t.index ["account_id", "target_account_id"], name: "index_follow_requests_on_account_id_and_target_account_id", unique: true end @@ -194,6 +197,7 @@ ActiveRecord::Schema.define(version: 2018_04_10_220657) do t.bigint "account_id", null: false t.bigint "target_account_id", null: false t.boolean "show_reblogs", default: true, null: false + t.string "uri" t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true end @@ -556,6 +560,10 @@ ActiveRecord::Schema.define(version: 2018_04_10_220657) do t.json "data" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "access_token_id" + t.bigint "user_id" + t.index ["access_token_id"], name: "index_web_push_subscriptions_on_access_token_id" + t.index ["user_id"], name: "index_web_push_subscriptions_on_user_id" end create_table "web_settings", force: :cascade do |t| @@ -625,5 +633,7 @@ ActiveRecord::Schema.define(version: 2018_04_10_220657) do add_foreign_key "subscriptions", "accounts", name: "fk_9847d1cbb5", on_delete: :cascade add_foreign_key "users", "accounts", name: "fk_50500f500d", on_delete: :cascade add_foreign_key "users", "invites", on_delete: :nullify + add_foreign_key "web_push_subscriptions", "oauth_access_tokens", column: "access_token_id", on_delete: :cascade + add_foreign_key "web_push_subscriptions", "users", on_delete: :cascade add_foreign_key "web_settings", "users", name: "fk_11910667b2", on_delete: :cascade end diff --git a/docker-compose.yml b/docker-compose.yml index 8058326dc6..496fb25487 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,7 +35,7 @@ services: image: tootsuite/mastodon restart: always env_file: .env.production - command: bundle exec rails s -p 3000 -b '0.0.0.0' + command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000 -b '0.0.0.0'" networks: - external_network - internal_network diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 1d63048645..9a7d49674c 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -9,11 +9,11 @@ module Mastodon end def minor - 3 + 4 end def patch - 3 + 0 end def pre @@ -48,5 +48,9 @@ module Mastodon source_base_url end end + + def user_agent + @user_agent ||= "#{HTTP::Request::USER_AGENT} (Mastodon/#{Version}; +http#{Rails.configuration.x.use_https ? 's' : ''}://#{Rails.configuration.x.web_domain}/)" + end end end diff --git a/package.json b/package.json index d861f1723f..a5be28b843 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "emoji-mart": "Gargron/emoji-mart#build", "es6-symbol": "^3.1.1", "escape-html": "^1.0.3", + "exif-js": "^2.3.0", "express": "^4.16.2", "extract-text-webpack-plugin": "^3.0.2", "file-loader": "^0.11.2", @@ -97,6 +98,7 @@ "react-redux-loading-bar": "^2.9.3", "react-router-dom": "^4.1.1", "react-router-scroll-4": "^1.0.0-beta.1", + "react-sparklines": "^1.7.0", "react-swipeable-views": "^0.12.3", "react-textarea-autosize": "^5.2.1", "react-toggle": "^4.0.1", @@ -120,7 +122,8 @@ "webpack-bundle-analyzer": "^2.9.1", "webpack-manifest-plugin": "^1.2.1", "webpack-merge": "^4.1.1", - "websocket.js": "^0.1.12" + "websocket.js": "^0.1.12", + "whatwg-url": "^6.4.1" }, "devDependencies": { "babel-eslint": "^8.2.1", diff --git a/spec/controllers/admin/account_moderation_notes_controller_spec.rb b/spec/controllers/admin/account_moderation_notes_controller_spec.rb index ca4e55c4d4..410ce6543d 100644 --- a/spec/controllers/admin/account_moderation_notes_controller_spec.rb +++ b/spec/controllers/admin/account_moderation_notes_controller_spec.rb @@ -1,4 +1,46 @@ require 'rails_helper' RSpec.describe Admin::AccountModerationNotesController, type: :controller do + render_views + + let(:user) { Fabricate(:user, admin: true) } + let(:target_account) { Fabricate(:account) } + + before do + sign_in user, scope: :user + end + + describe 'POST #create' do + subject { post :create, params: params } + + context 'when parameters are valid' do + let(:params) { { account_moderation_note: { target_account_id: target_account.id, content: 'test content' } } } + + it 'successfully creates a note' do + expect { subject }.to change { AccountModerationNote.count }.by(1) + expect(subject).to redirect_to admin_account_path(target_account.id) + end + end + + context 'when parameters are invalid' do + let(:params) { { account_moderation_note: { target_account_id: target_account.id, content: '' } } } + + it 'falls to create a note' do + expect { subject }.not_to change { AccountModerationNote.count } + expect(subject).to render_template 'admin/accounts/show' + end + end + end + + describe 'DELETE #destroy' do + subject { delete :destroy, params: { id: note.id } } + + let!(:note) { Fabricate(:account_moderation_note, account: account, target_account: target_account) } + let(:account) { Fabricate(:account) } + + it 'destroys note' do + expect { subject }.to change { AccountModerationNote.count }.by(-1) + expect(subject).to redirect_to admin_account_path(target_account.id) + end + end end diff --git a/spec/controllers/admin/confirmations_controller_spec.rb b/spec/controllers/admin/confirmations_controller_spec.rb index 7c80349646..eec2b2f5c4 100644 --- a/spec/controllers/admin/confirmations_controller_spec.rb +++ b/spec/controllers/admin/confirmations_controller_spec.rb @@ -30,4 +30,35 @@ RSpec.describe Admin::ConfirmationsController, type: :controller do expect(response).to have_http_status(404) end end + + describe 'POST #resernd' do + subject { post :resend, params: { account_id: account.id } } + + let(:account) { Fabricate(:account) } + let!(:user) { Fabricate(:user, confirmed_at: confirmed_at, account: account) } + + before do + allow(UserMailer).to receive(:confirmation_instructions) { double(:email, deliver_later: nil) } + end + + context 'when email is not confirmed' do + let(:confirmed_at) { nil } + + it 'resends confirmation mail' do + expect(subject).to redirect_to admin_accounts_path + expect(flash[:notice]).to eq I18n.t('admin.accounts.resend_confirmation.success') + expect(UserMailer).to have_received(:confirmation_instructions).once + end + end + + context 'when email is confirmed' do + let(:confirmed_at) { Time.zone.now } + + it 'does not resend confirmation mail' do + expect(subject).to redirect_to admin_accounts_path + expect(flash[:error]).to eq I18n.t('admin.accounts.resend_confirmation.already_confirmed') + expect(UserMailer).not_to have_received(:confirmation_instructions) + end + end + end end diff --git a/spec/controllers/admin/custom_emojis_controller_spec.rb b/spec/controllers/admin/custom_emojis_controller_spec.rb new file mode 100644 index 0000000000..b7e2894e90 --- /dev/null +++ b/spec/controllers/admin/custom_emojis_controller_spec.rb @@ -0,0 +1,115 @@ +require 'rails_helper' + +describe Admin::CustomEmojisController do + render_views + + let(:user) { Fabricate(:user, admin: true) } + + before do + sign_in user, scope: :user + end + + describe 'GET #index' do + subject { get :index } + + before do + Fabricate(:custom_emoji) + end + + it 'renders index page' do + expect(subject).to have_http_status 200 + expect(subject).to render_template :index + end + end + + describe 'GET #new' do + subject { get :new } + + it 'renders new page' do + expect(subject).to have_http_status 200 + expect(subject).to render_template :new + end + end + + describe 'POST #create' do + subject { post :create, params: { custom_emoji: params } } + + let(:image) { fixture_file_upload(Rails.root.join('spec', 'fixtures', 'files', 'emojo.png'), 'image/png') } + + context 'when parameter is valid' do + let(:params) { { shortcode: 'test', image: image } } + + it 'creates custom emoji' do + expect { subject }.to change { CustomEmoji.count }.by(1) + end + end + + context 'when parameter is invalid' do + let(:params) { { shortcode: 't', image: image } } + + it 'renders new' do + expect(subject).to render_template :new + end + end + end + + describe 'PUT #update' do + let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test') } + let(:image) { fixture_file_upload(Rails.root.join('spec', 'fixtures', 'files', 'emojo.png'), 'image/png') } + + before do + put :update, params: { id: custom_emoji.id, custom_emoji: params } + end + + context 'when parameter is valid' do + let(:params) { { shortcode: 'updated', image: image } } + + it 'succeeds in updating custom emoji' do + expect(flash[:notice]).to eq I18n.t('admin.custom_emojis.updated_msg') + expect(custom_emoji.reload).to have_attributes(shortcode: 'updated') + end + end + + context 'when parameter is invalid' do + let(:params) { { shortcode: 'u', image: image } } + + it 'fails to update custom emoji' do + expect(flash[:alert]).to eq I18n.t('admin.custom_emojis.update_failed_msg') + expect(custom_emoji.reload).to have_attributes(shortcode: 'test') + end + end + end + + describe 'POST #copy' do + subject { post :copy, params: { id: custom_emoji.id } } + + let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test') } + + it 'copies custom emoji' do + expect { subject }.to change { CustomEmoji.where(shortcode: 'test').count }.by(1) + expect(flash[:notice]).to eq I18n.t('admin.custom_emojis.copied_msg') + end + end + + describe 'POST #enable' do + let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test', disabled: true) } + + before { post :enable, params: { id: custom_emoji.id } } + + it 'enables custom emoji' do + expect(response).to redirect_to admin_custom_emojis_path + expect(custom_emoji.reload).to have_attributes(disabled: false) + end + end + + describe 'POST #disable' do + let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test', disabled: false) } + + before { post :disable, params: { id: custom_emoji.id } } + + it 'enables custom emoji' do + expect(response).to redirect_to admin_custom_emojis_path + expect(custom_emoji.reload).to have_attributes(disabled: true) + end + end +end diff --git a/spec/controllers/admin/invites_controller_spec.rb b/spec/controllers/admin/invites_controller_spec.rb new file mode 100644 index 0000000000..e7d9954118 --- /dev/null +++ b/spec/controllers/admin/invites_controller_spec.rb @@ -0,0 +1,43 @@ +require 'rails_helper' + +describe Admin::InvitesController do + render_views + + let(:user) { Fabricate(:user, admin: true) } + + before do + sign_in user, scope: :user + end + + describe 'GET #index' do + subject { get :index, params: { available: true } } + + let!(:invite) { Fabricate(:invite) } + + it 'renders index page' do + expect(subject).to render_template :index + expect(assigns(:invites)).to include invite + end + end + + describe 'POST #create' do + subject { post :create, params: { invite: { max_uses: '10', expires_in: 1800 } } } + + it 'succeeds to create a invite' do + expect{ subject }.to change { Invite.count }.by(1) + expect(subject).to redirect_to admin_invites_path + expect(Invite.last).to have_attributes(user_id: user.id, max_uses: 10) + end + end + + describe 'DELETE #destroy' do + let!(:invite) { Fabricate(:invite, expires_at: nil) } + + subject { delete :destroy, params: { id: invite.id } } + + it 'expires invite' do + expect(subject).to redirect_to admin_invites_path + expect(invite.reload).to be_expired + end + end +end diff --git a/spec/controllers/admin/report_notes_controller_spec.rb b/spec/controllers/admin/report_notes_controller_spec.rb new file mode 100644 index 0000000000..2c32303fb3 --- /dev/null +++ b/spec/controllers/admin/report_notes_controller_spec.rb @@ -0,0 +1,91 @@ +require 'rails_helper' + +describe Admin::ReportNotesController do + render_views + + let(:user) { Fabricate(:user, admin: true) } + + before do + sign_in user, scope: :user + end + + describe 'POST #create' do + subject { post :create, params: params } + + let(:report) { Fabricate(:report, action_taken: action_taken, action_taken_by_account_id: account_id) } + + context 'when parameter is valid' do + + context 'when report is unsolved' do + let(:action_taken) { false } + let(:account_id) { nil } + + context 'when create_and_resolve flag is on' do + let(:params) { { report_note: { content: 'test content', report_id: report.id }, create_and_resolve: nil } } + + it 'creates a report note and resolves report' do + expect{ subject }.to change{ ReportNote.count }.by(1) + expect(report.reload).to be_action_taken + expect(subject).to redirect_to admin_reports_path + end + end + + context 'when create_and_resolve flag is false' do + let(:params) { { report_note: { content: 'test content', report_id: report.id } } } + + it 'creates a report note and does not resolve report' do + expect{ subject }.to change{ ReportNote.count }.by(1) + expect(report.reload).not_to be_action_taken + expect(subject).to redirect_to admin_report_path(report) + end + end + end + + context 'when report is resolved' do + let(:action_taken) { true } + let(:account_id) { user.account.id } + + context 'when create_and_unresolve flag is on' do + let(:params) { { report_note: { content: 'test content', report_id: report.id }, create_and_unresolve: nil } } + + it 'creates a report note and unresolves report' do + expect{ subject }.to change{ ReportNote.count }.by(1) + expect(report.reload).not_to be_action_taken + expect(subject).to redirect_to admin_report_path(report) + end + end + + context 'when create_and_unresolve flag is false' do + let(:params) { { report_note: { content: 'test content', report_id: report.id } } } + + it 'creates a report note and does not unresolve report' do + expect{ subject }.to change{ ReportNote.count }.by(1) + expect(report.reload).to be_action_taken + expect(subject).to redirect_to admin_report_path(report) + end + end + end + end + + context 'when parameter is invalid' do + let(:params) { { report_note: { content: '', report_id: report.id } } } + let(:action_taken) { false } + let(:account_id) { nil } + + it 'renders admin/reports/show' do + expect(subject).to render_template 'admin/reports/show' + end + end + end + + describe 'DELETE #destroy' do + subject { delete :destroy, params: { id: report_note.id } } + + let!(:report_note) { Fabricate(:report_note) } + + it 'deletes note' do + expect{ subject }.to change{ ReportNote.count }.by(-1) + expect(subject).to redirect_to admin_report_path(report_note.report) + end + end +end diff --git a/spec/controllers/admin/reported_statuses_controller_spec.rb b/spec/controllers/admin/reported_statuses_controller_spec.rb index 29957ed37e..7adbf36b9c 100644 --- a/spec/controllers/admin/reported_statuses_controller_spec.rb +++ b/spec/controllers/admin/reported_statuses_controller_spec.rb @@ -22,7 +22,7 @@ describe Admin::ReportedStatusesController do let(:sensitive) { true } let!(:media_attachment) { Fabricate(:media_attachment, status: status) } - context 'updates sensitive column to true' do + context 'when action is nsfw_on' do it 'updates sensitive column' do is_expected.to change { status.reload.sensitive @@ -30,7 +30,7 @@ describe Admin::ReportedStatusesController do end end - context 'updates sensitive column to false' do + context 'when action is nsfw_off' do let(:action) { 'nsfw_off' } let(:sensitive) { false } @@ -41,35 +41,13 @@ describe Admin::ReportedStatusesController do end end - it 'redirects to report page' do - subject.call - expect(response).to redirect_to(admin_report_path(report)) - end - end + context 'when action is delete' do + let(:action) { 'delete' } - describe 'PATCH #update' do - subject do - -> { patch :update, params: { report_id: report, id: status, status: { sensitive: sensitive } } } - end - - let(:status) { Fabricate(:status, sensitive: !sensitive) } - let(:sensitive) { true } - - context 'updates sensitive column to true' do - it 'updates sensitive column' do - is_expected.to change { - status.reload.sensitive - }.from(false).to(true) - end - end - - context 'updates sensitive column to false' do - let(:sensitive) { false } - - it 'updates sensitive column' do - is_expected.to change { - status.reload.sensitive - }.from(true).to(false) + it 'removes a status' do + allow(RemovalWorker).to receive(:perform_async) + subject.call + expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first) end end @@ -78,15 +56,4 @@ describe Admin::ReportedStatusesController do expect(response).to redirect_to(admin_report_path(report)) end end - - describe 'DELETE #destroy' do - it 'removes a status' do - allow(RemovalWorker).to receive(:perform_async) - - delete :destroy, params: { report_id: report, id: status } - expect(response).to have_http_status(200) - expect(RemovalWorker). - to have_received(:perform_async).with(status.id) - end - end end diff --git a/spec/controllers/admin/roles_controller_spec.rb b/spec/controllers/admin/roles_controller_spec.rb new file mode 100644 index 0000000000..8e0de73cbd --- /dev/null +++ b/spec/controllers/admin/roles_controller_spec.rb @@ -0,0 +1,33 @@ +require 'rails_helper' + +describe Admin::RolesController do + render_views + + let(:admin) { Fabricate(:user, admin: true) } + + before do + sign_in admin, scope: :user + end + + describe 'POST #promote' do + subject { post :promote, params: { account_id: user.account_id } } + + let(:user) { Fabricate(:user, moderator: false, admin: false) } + + it 'promotes user' do + expect(subject).to redirect_to admin_account_path(user.account_id) + expect(user.reload).to be_moderator + end + end + + describe 'POST #demote' do + subject { post :demote, params: { account_id: user.account_id } } + + let(:user) { Fabricate(:user, moderator: true, admin: false) } + + it 'demotes user' do + expect(subject).to redirect_to admin_account_path(user.account_id) + expect(user.reload).not_to be_moderator + end + end +end diff --git a/spec/controllers/admin/statuses_controller_spec.rb b/spec/controllers/admin/statuses_controller_spec.rb index cbaf397865..6afcc14425 100644 --- a/spec/controllers/admin/statuses_controller_spec.rb +++ b/spec/controllers/admin/statuses_controller_spec.rb @@ -34,13 +34,13 @@ describe Admin::StatusesController do describe 'POST #create' do subject do - -> { post :create, params: { account_id: account.id, form_status_batch: { action: action, status_ids: status_ids } } } + -> { post :create, params: { :account_id => account.id, action => '', :form_status_batch => { status_ids: status_ids } } } end let(:action) { 'nsfw_on' } let(:status_ids) { [media_attached_status.id] } - context 'updates sensitive column to true' do + context 'when action is nsfw_on' do it 'updates sensitive column' do is_expected.to change { media_attached_status.reload.sensitive @@ -48,7 +48,7 @@ describe Admin::StatusesController do end end - context 'updates sensitive column to false' do + context 'when action is nsfw_off' do let(:action) { 'nsfw_off' } let(:sensitive) { false } @@ -59,32 +59,13 @@ describe Admin::StatusesController do end end - it 'redirects to account statuses page' do - subject.call - expect(response).to redirect_to(admin_account_statuses_path(account.id)) - end - end + context 'when action is delete' do + let(:action) { 'delete' } - describe 'PATCH #update' do - subject do - -> { patch :update, params: { account_id: account.id, id: media_attached_status, status: { sensitive: sensitive } } } - end - - context 'updates sensitive column to true' do - it 'updates sensitive column' do - is_expected.to change { - media_attached_status.reload.sensitive - }.from(false).to(true) - end - end - - context 'updates sensitive column to false' do - let(:sensitive) { false } - - it 'updates sensitive column' do - is_expected.to change { - media_attached_status.reload.sensitive - }.from(true).to(false) + it 'removes a status' do + allow(RemovalWorker).to receive(:perform_async) + subject.call + expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first) end end @@ -93,15 +74,4 @@ describe Admin::StatusesController do expect(response).to redirect_to(admin_account_statuses_path(account.id)) end end - - describe 'DELETE #destroy' do - it 'removes a status' do - allow(RemovalWorker).to receive(:perform_async) - - delete :destroy, params: { account_id: account.id, id: status } - expect(response).to have_http_status(200) - expect(RemovalWorker). - to have_received(:perform_async).with(status.id) - end - end end diff --git a/spec/controllers/api/v1/push/subscriptions_controller_spec.rb b/spec/controllers/api/v1/push/subscriptions_controller_spec.rb new file mode 100644 index 0000000000..01146294f8 --- /dev/null +++ b/spec/controllers/api/v1/push/subscriptions_controller_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Push::SubscriptionsController do + render_views + + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'push') } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + let(:create_payload) do + { + subscription: { + endpoint: 'https://fcm.googleapis.com/fcm/send/fiuH06a27qE:APA91bHnSiGcLwdaxdyqVXNDR9w1NlztsHb6lyt5WDKOC_Z_Q8BlFxQoR8tWFSXUIDdkyw0EdvxTu63iqamSaqVSevW5LfoFwojws8XYDXv_NRRLH6vo2CdgiN4jgHv5VLt2A8ah6lUX', + keys: { + p256dh: 'BEm_a0bdPDhf0SOsrnB2-ategf1hHoCnpXgQsFj5JCkcoMrMt2WHoPfEYOYPzOIs9mZE8ZUaD7VA5vouy0kEkr8=', + auth: 'eH_C8rq2raXqlcBVDa1gLg==', + }, + } + }.with_indifferent_access + end + + let(:alerts_payload) do + { + data: { + alerts: { + follow: true, + favourite: false, + reblog: true, + mention: false, + } + } + }.with_indifferent_access + end + + describe 'POST #create' do + it 'saves push subscriptions' do + post :create, params: create_payload + + push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint]) + + expect(push_subscription.endpoint).to eq(create_payload[:subscription][:endpoint]) + expect(push_subscription.key_p256dh).to eq(create_payload[:subscription][:keys][:p256dh]) + expect(push_subscription.key_auth).to eq(create_payload[:subscription][:keys][:auth]) + expect(push_subscription.user_id).to eq user.id + expect(push_subscription.access_token_id).to eq token.id + end + + it 'replaces old subscription on repeat calls' do + post :create, params: create_payload + post :create, params: create_payload + + expect(Web::PushSubscription.where(endpoint: create_payload[:subscription][:endpoint]).count).to eq 1 + end + end + + describe 'PUT #update' do + it 'changes alert settings' do + post :create, params: create_payload + put :update, params: alerts_payload + + push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint]) + + expect(push_subscription.data.dig('alerts', 'follow')).to eq(alerts_payload[:data][:alerts][:follow].to_s) + expect(push_subscription.data.dig('alerts', 'favourite')).to eq(alerts_payload[:data][:alerts][:favourite].to_s) + expect(push_subscription.data.dig('alerts', 'reblog')).to eq(alerts_payload[:data][:alerts][:reblog].to_s) + expect(push_subscription.data.dig('alerts', 'mention')).to eq(alerts_payload[:data][:alerts][:mention].to_s) + end + end + + describe 'DELETE #destroy' do + it 'removes the subscription' do + post :create, params: create_payload + delete :destroy + + expect(Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])).to be_nil + end + end +end diff --git a/spec/controllers/api/web/push_subscriptions_controller_spec.rb b/spec/controllers/api/web/push_subscriptions_controller_spec.rb index bbf94c5c66..381cdeab94 100644 --- a/spec/controllers/api/web/push_subscriptions_controller_spec.rb +++ b/spec/controllers/api/web/push_subscriptions_controller_spec.rb @@ -59,10 +59,10 @@ describe Api::Web::PushSubscriptionsController do push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint]) - expect(push_subscription.data['follow']).to eq(alerts_payload[:data][:follow]) - expect(push_subscription.data['favourite']).to eq(alerts_payload[:data][:favourite]) - expect(push_subscription.data['reblog']).to eq(alerts_payload[:data][:reblog]) - expect(push_subscription.data['mention']).to eq(alerts_payload[:data][:mention]) + expect(push_subscription.data['alerts']['follow']).to eq(alerts_payload[:data][:alerts][:follow].to_s) + expect(push_subscription.data['alerts']['favourite']).to eq(alerts_payload[:data][:alerts][:favourite].to_s) + expect(push_subscription.data['alerts']['reblog']).to eq(alerts_payload[:data][:alerts][:reblog].to_s) + expect(push_subscription.data['alerts']['mention']).to eq(alerts_payload[:data][:alerts][:mention].to_s) end end end @@ -81,10 +81,10 @@ describe Api::Web::PushSubscriptionsController do push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint]) - expect(push_subscription.data['follow']).to eq(alerts_payload[:data][:follow]) - expect(push_subscription.data['favourite']).to eq(alerts_payload[:data][:favourite]) - expect(push_subscription.data['reblog']).to eq(alerts_payload[:data][:reblog]) - expect(push_subscription.data['mention']).to eq(alerts_payload[:data][:mention]) + expect(push_subscription.data['alerts']['follow']).to eq(alerts_payload[:data][:alerts][:follow].to_s) + expect(push_subscription.data['alerts']['favourite']).to eq(alerts_payload[:data][:alerts][:favourite].to_s) + expect(push_subscription.data['alerts']['reblog']).to eq(alerts_payload[:data][:alerts][:reblog].to_s) + expect(push_subscription.data['alerts']['mention']).to eq(alerts_payload[:data][:alerts][:mention].to_s) end end end diff --git a/spec/controllers/emojis_controller_spec.rb b/spec/controllers/emojis_controller_spec.rb new file mode 100644 index 0000000000..68bae256d7 --- /dev/null +++ b/spec/controllers/emojis_controller_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +describe EmojisController do + render_views + + let(:emoji) { Fabricate(:custom_emoji) } + + describe 'GET #show' do + subject(:responce) { get :show, params: { id: emoji.id, format: :json } } + subject(:body) { JSON.parse(response.body, symbolize_names: true) } + + it 'returns the right response' do + expect(responce).to have_http_status 200 + expect(body[:name]).to eq ':coolcat:' + end + end +end diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb new file mode 100644 index 0000000000..9f5ab67c30 --- /dev/null +++ b/spec/controllers/invites_controller_spec.rb @@ -0,0 +1,72 @@ +require 'rails_helper' + +describe InvitesController do + render_views + + before do + sign_in user + end + + around do |example| + min_invite_role = Setting.min_invite_role + example.run + Setting.min_invite_role = min_invite_role + end + + describe 'GET #index' do + subject { get :index } + + let(:user) { Fabricate(:user, moderator: false, admin: false) } + let!(:invite) { Fabricate(:invite, user: user) } + + context 'when user is a staff' do + it 'renders index page' do + Setting.min_invite_role = 'user' + expect(subject).to render_template :index + expect(assigns(:invites)).to include invite + expect(assigns(:invites).count).to eq 1 + end + end + + context 'when user is not a staff' do + it 'returns 403' do + Setting.min_invite_role = 'modelator' + expect(subject).to have_http_status 403 + end + end + end + + describe 'POST #create' do + subject { post :create, params: { invite: { max_uses: '10', expires_in: 1800 } } } + + context 'when user is an admin' do + let(:user) { Fabricate(:user, moderator: false, admin: true) } + + it 'succeeds to create a invite' do + expect{ subject }.to change { Invite.count }.by(1) + expect(subject).to redirect_to invites_path + expect(Invite.last).to have_attributes(user_id: user.id, max_uses: 10) + end + end + + context 'when user is not an admin' do + let(:user) { Fabricate(:user, moderator: true, admin: false) } + + it 'returns 403' do + expect(subject).to have_http_status 403 + end + end + end + + describe 'DELETE #create' do + subject { delete :destroy, params: { id: invite.id } } + + let!(:invite) { Fabricate(:invite, user: user, expires_at: nil) } + let(:user) { Fabricate(:user, moderator: false, admin: true) } + + it 'expires invite' do + expect(subject).to redirect_to invites_path + expect(invite.reload).to be_expired + end + end +end diff --git a/spec/controllers/oauth/authorized_applications_controller_spec.rb b/spec/controllers/oauth/authorized_applications_controller_spec.rb index f967b507f0..901e538e95 100644 --- a/spec/controllers/oauth/authorized_applications_controller_spec.rb +++ b/spec/controllers/oauth/authorized_applications_controller_spec.rb @@ -39,4 +39,24 @@ describe Oauth::AuthorizedApplicationsController do include_examples 'stores location for user' end end + + describe 'DELETE #destroy' do + let!(:user) { Fabricate(:user) } + let!(:application) { Fabricate(:application) } + let!(:access_token) { Fabricate(:accessible_access_token, application: application, resource_owner_id: user.id) } + let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) } + + before do + sign_in user, scope: :user + post :destroy, params: { id: application.id } + end + + it 'revokes access tokens for the application' do + expect(Doorkeeper::AccessToken.where(application: application).first.revoked_at).to_not be_nil + end + + it 'removes subscriptions for the application\'s access tokens' do + expect(Web::PushSubscription.where(user: user).count).to eq 0 + end + end end diff --git a/spec/controllers/oauth/tokens_controller_spec.rb b/spec/controllers/oauth/tokens_controller_spec.rb new file mode 100644 index 0000000000..ba8e367a68 --- /dev/null +++ b/spec/controllers/oauth/tokens_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Oauth::TokensController, type: :controller do + describe 'POST #revoke' do + let!(:user) { Fabricate(:user) } + let!(:access_token) { Fabricate(:accessible_access_token, resource_owner_id: user.id) } + let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) } + + before do + post :revoke, params: { token: access_token.token } + end + + it 'revokes the token' do + expect(access_token.reload.revoked_at).to_not be_nil + end + + it 'removes web push subscription for token' do + expect(Web::PushSubscription.where(access_token: access_token).count).to eq 0 + end + end +end diff --git a/spec/fabricators/report_note_fabricator.rb b/spec/fabricators/report_note_fabricator.rb new file mode 100644 index 0000000000..e139efffbc --- /dev/null +++ b/spec/fabricators/report_note_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator(:report_note) do + report + account { Fabricate(:account) } + content "Test Content" +end diff --git a/spec/fabricators/web_push_subscription_fabricator.rb b/spec/fabricators/web_push_subscription_fabricator.rb index 72d11b77cc..97f90675de 100644 --- a/spec/fabricators/web_push_subscription_fabricator.rb +++ b/spec/fabricators/web_push_subscription_fabricator.rb @@ -1,4 +1,4 @@ -Fabricator(:web_push_subscription) do +Fabricator(:web_push_subscription, from: Web::PushSubscription) do endpoint Faker::Internet.url key_p256dh Faker::Internet.password key_auth Faker::Internet.password diff --git a/spec/fabricators/web_setting_fabricator.rb b/spec/fabricators/web_setting_fabricator.rb index e5136829b9..369b86bc18 100644 --- a/spec/fabricators/web_setting_fabricator.rb +++ b/spec/fabricators/web_setting_fabricator.rb @@ -1,3 +1,2 @@ -Fabricator('Web::Setting') do - +Fabricator(:web_setting, from: Web::Setting) do end diff --git a/spec/lib/activitypub/activity/add_spec.rb b/spec/lib/activitypub/activity/add_spec.rb index 3ebab4e373..16db71c880 100644 --- a/spec/lib/activitypub/activity/add_spec.rb +++ b/spec/lib/activitypub/activity/add_spec.rb @@ -18,12 +18,31 @@ RSpec.describe ActivityPub::Activity::Add do describe '#perform' do subject { described_class.new(json, sender) } - before do + it 'creates a pin' do subject.perform + expect(sender.pinned?(status)).to be true end - it 'creates a pin' do - expect(sender.pinned?(status)).to be true + context 'when status was not known before' do + let(:json) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Add', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: 'https://example.com/unknown', + target: sender.featured_collection_url, + }.with_indifferent_access + end + + before do + stub_request(:get, 'https://example.com/unknown').to_return(status: 410) + end + + it 'fetches the status' do + subject.perform + expect(a_request(:get, 'https://example.com/unknown')).to have_been_made.at_least_once + end end end end diff --git a/spec/models/follow_request_spec.rb b/spec/models/follow_request_spec.rb index 7bc93a2aae..4b824c0db6 100644 --- a/spec/models/follow_request_spec.rb +++ b/spec/models/follow_request_spec.rb @@ -7,7 +7,7 @@ RSpec.describe FollowRequest, type: :model do let(:target_account) { Fabricate(:account) } it 'calls Account#follow!, MergeWorker.perform_async, and #destroy!' do - expect(account).to receive(:follow!).with(target_account, reblogs: true) + expect(account).to receive(:follow!).with(target_account, reblogs: true, uri: follow_request.uri) expect(MergeWorker).to receive(:perform_async).with(target_account.id, account.id) expect(follow_request).to receive(:destroy!) follow_request.authorize! diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index 1f5a03877f..03d1a94de1 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -154,7 +154,7 @@ RSpec.describe Status, type: :model do describe '#target' do it 'returns nil if the status is self-contained' do - expect(subject.target).to be_nil + expect(subject.target).to be_nil end it 'returns nil if the status is a reply' do @@ -370,24 +370,25 @@ RSpec.describe Status, type: :model do expect(@results).to_not include(@followed_public_status) end - it 'includes direct statuses mentioning recipient from followed' do - Fabricate(:mention, account: account, status: @followed_direct_status) - expect(@results).to include(@followed_direct_status) - end - it 'does not include direct statuses not mentioning recipient from followed' do expect(@results).to_not include(@followed_direct_status) end - it 'includes direct statuses mentioning recipient from non-followed' do - Fabricate(:mention, account: account, status: @not_followed_direct_status) - expect(@results).to include(@not_followed_direct_status) - end - it 'does not include direct statuses not mentioning recipient from non-followed' do expect(@results).to_not include(@not_followed_direct_status) end + it 'includes direct statuses mentioning recipient from followed' do + Fabricate(:mention, account: account, status: @followed_direct_status) + results2 = Status.as_direct_timeline(account) + expect(results2).to include(@followed_direct_status) + end + + it 'includes direct statuses mentioning recipient from non-followed' do + Fabricate(:mention, account: account, status: @not_followed_direct_status) + results2 = Status.as_direct_timeline(account) + expect(results2).to include(@not_followed_direct_status) + end end describe '.as_public_timeline' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 760214dede..cc8d88cc85 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -75,7 +75,7 @@ RSpec.describe User, type: :model do describe 'inactive' do it 'returns a relation of inactive users' do specified = Fabricate(:user, current_sign_in_at: 15.days.ago) - Fabricate(:user, current_sign_in_at: 13.days.ago) + Fabricate(:user, current_sign_in_at: 6.days.ago) expect(User.inactive).to match_array([specified]) end diff --git a/spec/models/web/push_subscription_spec.rb b/spec/models/web/push_subscription_spec.rb index 574da55ac2..c6665611c6 100644 --- a/spec/models/web/push_subscription_spec.rb +++ b/spec/models/web/push_subscription_spec.rb @@ -2,20 +2,8 @@ require 'rails_helper' RSpec.describe Web::PushSubscription, type: :model do let(:alerts) { { mention: true, reblog: false, follow: true, follow_request: false, favourite: true } } - let(:payload_no_alerts) { Web::PushSubscription.new(id: 1, endpoint: 'a', key_p256dh: 'c', key_auth: 'd').as_payload } - let(:payload_alerts) { Web::PushSubscription.new(id: 1, endpoint: 'a', key_p256dh: 'c', key_auth: 'd', data: { alerts: alerts }).as_payload } let(:push_subscription) { Web::PushSubscription.new(data: { alerts: alerts }) } - describe '#as_payload' do - it 'only returns id and endpoint' do - expect(payload_no_alerts.keys).to eq [:id, :endpoint] - end - - it 'returns alerts if set' do - expect(payload_alerts.keys).to eq [:id, :endpoint, :alerts] - end - end - describe '#pushable?' do it 'obeys alert settings' do expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Mention'))).to eq true diff --git a/spec/services/resolve_account_service_spec.rb b/spec/services/resolve_account_service_spec.rb index f4c810f758..dd7561587d 100644 --- a/spec/services/resolve_account_service_spec.rb +++ b/spec/services/resolve_account_service_spec.rb @@ -116,6 +116,7 @@ RSpec.describe ResolveAccountService, type: :service do expect(account.activitypub?).to eq true expect(account.domain).to eq 'ap.example.com' expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox' + expect(account.actor_type).to eq 'Person' end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9030329373..0cd1f91d02 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,20 +1,17 @@ -#require 'rspec/retry' -require 'simplecov' - GC.disable -SimpleCov.start 'rails' do - add_group 'Services', 'app/services' - add_group 'Presenters', 'app/presenters' - add_group 'Validators', 'app/validators' +if ENV['DISABLE_SIMPLECOV'] != 'true' + require 'simplecov' + SimpleCov.start 'rails' do + add_group 'Services', 'app/services' + add_group 'Presenters', 'app/presenters' + add_group 'Validators', 'app/validators' + end end gc_counter = -1 RSpec.configure do |config| - #config.verbose_retry = true - #config.display_try_failure_messages = true - config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true end @@ -29,10 +26,6 @@ RSpec.configure do |config| end end - #config.around :each do |ex| - # ex.run_with_retry retry: 3 - #end - config.before :suite do Chewy.strategy(:bypass) end diff --git a/streaming/index.js b/streaming/index.js index 48bab8078e..4eaf668658 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -241,7 +241,9 @@ const startWorker = (workerId) => { const PUBLIC_STREAMS = [ 'public', + 'public:media', 'public:local', + 'public:local:media', 'hashtag', 'hashtag:local', ]; @@ -459,11 +461,17 @@ const startWorker = (workerId) => { }); app.get('/api/v1/streaming/public', (req, res) => { - streamFrom('timeline:public', req, streamToHttp(req, res), streamHttpEnd(req), true); + const onlyMedia = req.query.only_media === '1' || req.query.only_media === 'true'; + const channel = onlyMedia ? 'timeline:public:media' : 'timeline:public'; + + streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req), true); }); app.get('/api/v1/streaming/public/local', (req, res) => { - streamFrom('timeline:public:local', req, streamToHttp(req, res), streamHttpEnd(req), true); + const onlyMedia = req.query.only_media === '1' || req.query.only_media === 'true'; + const channel = onlyMedia ? 'timeline:public:local:media' : 'timeline:public:local'; + + streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req), true); }); app.get('/api/v1/streaming/direct', (req, res) => { @@ -521,6 +529,12 @@ const startWorker = (workerId) => { case 'public:local': streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; + case 'public:media': + streamFrom('timeline:public:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); + break; + case 'public:local:media': + streamFrom('timeline:public:local:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); + break; case 'direct': streamFrom(`timeline:direct:${req.accountId}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; diff --git a/yarn.lock b/yarn.lock index 0513845eb8..4a9d182e7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2605,6 +2605,10 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +exif-js@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/exif-js/-/exif-js-2.3.0.tgz#9d10819bf571f873813e7640241255ab9ce1a814" + expand-brackets@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" @@ -4387,6 +4391,10 @@ lodash.restparam@^3.0.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + lodash.tail@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" @@ -6142,6 +6150,12 @@ react-router@^4.2.0: prop-types "^15.5.4" warning "^3.0.0" +react-sparklines@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/react-sparklines/-/react-sparklines-1.7.0.tgz#9b1d97e8c8610095eeb2ad658d2e1fcf91f91a60" + dependencies: + prop-types "^15.5.10" + react-swipeable-views-core@^0.12.11: version "0.12.11" resolved "https://registry.yarnpkg.com/react-swipeable-views-core/-/react-swipeable-views-core-0.12.11.tgz#3cf2b4daffbb36f9d69bd19bf5b2d5370b6b2c1b" @@ -7269,6 +7283,12 @@ tough-cookie@^2.3.2, tough-cookie@~2.3.0, tough-cookie@~2.3.3: dependencies: punycode "^1.4.1" +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + dependencies: + punycode "^2.1.0" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -7506,7 +7526,7 @@ webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" -webidl-conversions@^4.0.0: +webidl-conversions@^4.0.0, webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" @@ -7649,6 +7669,14 @@ whatwg-url@^4.3.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" +whatwg-url@^6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.4.1.tgz#fdb94b440fd4ad836202c16e9737d511f012fd67" + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + whet.extend@~0.9.9: version "0.9.9" resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1"
{' '} @@ -208,8 +206,6 @@ export default class StatusContent extends React.PureComponent { ref={this.setRef} style={directionStyle} tabIndex={!hidden ? 0 : null} - onMouseDown={this.handleMouseDown} - onMouseUp={this.handleMouseUp} dangerouslySetInnerHTML={content} /> {media} @@ -222,12 +218,12 @@ export default class StatusContent extends React.PureComponent {
/g, '\n\n'); - - const emojiMap = normalStatus.emojis.reduce((obj, emoji) => { - obj[`:${emoji.shortcode}:`] = emoji; - return obj; - }, {}); + const emojiMap = makeEmojiMap(normalStatus); normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 7aa070f569..3f95f6667f 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -8,6 +8,7 @@ import { importFetchedStatuses, } from './importer'; import { defineMessages } from 'react-intl'; +import { unescapeHTML } from '../utils/html'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; @@ -21,6 +22,7 @@ export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, + group: { id: 'notifications.group', defaultMessage: '{count} notifications' }, }); const fetchRelatedRelationships = (dispatch, notifications) => { @@ -31,13 +33,6 @@ const fetchRelatedRelationships = (dispatch, notifications) => { } }; -const unescapeHTML = (html) => { - const wrapper = document.createElement('div'); - html = html.replace(/||\n/g, ' '); - wrapper.innerHTML = html; - return wrapper.textContent; -}; - export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true); @@ -82,9 +77,14 @@ export function updateNotifications(notification, intlMessages, intlLocale) { const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); -export function expandNotifications({ maxId } = {}) { +const noOp = () => {}; + +export function expandNotifications({ maxId } = {}, done = noOp) { return (dispatch, getState) => { - if (getState().getIn(['notifications', 'isLoading'])) { + const notifications = getState().get('notifications'); + + if (notifications.get('isLoading')) { + done(); return; } @@ -93,6 +93,10 @@ export function expandNotifications({ maxId } = {}) { exclude_types: excludeTypesFromSettings(getState()), }; + if (!maxId && notifications.get('items').size > 0) { + params.since_id = notifications.getIn(['items', 0]); + } + dispatch(expandNotificationsRequest()); api(getState).get('/api/v1/notifications', { params }).then(response => { @@ -103,8 +107,10 @@ export function expandNotifications({ maxId } = {}) { dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null)); fetchRelatedRelationships(dispatch, response.data); + done(); }).catch(error => { dispatch(expandNotificationsFail(error)); + done(); }); }; }; diff --git a/app/javascript/mastodon/actions/push_notifications/registerer.js b/app/javascript/mastodon/actions/push_notifications/registerer.js index 82fe4519a2..b0f42b6a20 100644 --- a/app/javascript/mastodon/actions/push_notifications/registerer.js +++ b/app/javascript/mastodon/actions/push_notifications/registerer.js @@ -51,13 +51,6 @@ export function register () { return (dispatch, getState) => { dispatch(setBrowserSupport(supportsPushNotifications)); - if (me && !pushNotificationsSetting.get(me)) { - const alerts = getState().getIn(['push_notifications', 'alerts']); - if (alerts) { - pushNotificationsSetting.set(me, { alerts: alerts }); - } - } - if (supportsPushNotifications) { if (!getApplicationServerKey()) { console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.'); diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js index 882c1709e9..b670d25c3c 100644 --- a/app/javascript/mastodon/actions/search.js +++ b/app/javascript/mastodon/actions/search.js @@ -33,7 +33,7 @@ export function submitSearch() { dispatch(fetchSearchRequest()); - api(getState).get('/api/v1/search', { + api(getState).get('/api/v2/search', { params: { q: value, resolve: true, diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 14215ab6d5..f56853bffb 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -36,15 +36,13 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null) }); } -function refreshHomeTimelineAndNotification (dispatch) { - dispatch(expandHomeTimeline()); - dispatch(expandNotifications()); -} +const refreshHomeTimelineAndNotification = (dispatch, done) => { + dispatch(expandHomeTimeline({}, () => dispatch(expandNotifications({}, done)))); +}; -export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); -export const connectCommunityStream = () => connectTimelineStream('community', 'public:local'); -export const connectMediaStream = () => connectTimelineStream('community', 'public:local'); -export const connectPublicStream = () => connectTimelineStream('public', 'public'); -export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`); -export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); -export const connectListStream = (id) => connectTimelineStream(`list:${id}`, `list&list=${id}`); +export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); +export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`); +export const connectPublicStream = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`); +export const connectHashtagStream = tag => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`); +export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); +export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`); diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index eca847ee73..11a199db6a 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -1,6 +1,6 @@ import { importFetchedStatus, importFetchedStatuses } from './importer'; import api, { getLinks } from '../api'; -import { Map as ImmutableMap } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE'; @@ -13,21 +13,9 @@ export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; -export const TIMELINE_CONTEXT_UPDATE = 'CONTEXT_UPDATE'; - export function updateTimeline(timeline, status) { return (dispatch, getState) => { const references = status.reblog ? getState().get('statuses').filter((item, itemId) => (itemId === status.reblog.id || item.get('reblog') === status.reblog.id)).map((_, itemId) => itemId) : []; - const parents = []; - - if (status.in_reply_to_id) { - let parent = getState().getIn(['statuses', status.in_reply_to_id]); - - while (parent && parent.get('in_reply_to_id')) { - parents.push(parent.get('id')); - parent = getState().getIn(['statuses', parent.get('in_reply_to_id')]); - } - } dispatch(importFetchedStatus(status)); @@ -37,14 +25,6 @@ export function updateTimeline(timeline, status) { status, references, }); - - if (parents.length > 0) { - dispatch({ - type: TIMELINE_CONTEXT_UPDATE, - status, - references: parents, - }); - } }; }; @@ -64,35 +44,44 @@ export function deleteFromTimelines(id) { }; }; -export function expandTimeline(timelineId, path, params = {}) { +const noOp = () => {}; + +export function expandTimeline(timelineId, path, params = {}, done = noOp) { return (dispatch, getState) => { const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); if (timeline.get('isLoading')) { + done(); return; } + if (!params.max_id && timeline.get('items', ImmutableList()).size > 0) { + params.since_id = timeline.getIn(['items', 0]); + } + dispatch(expandTimelineRequest(timelineId)); api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206)); + done(); }).catch(error => { dispatch(expandTimelineFail(timelineId, error)); + done(); }); }; }; -export const expandHomeTimeline = ({ maxId } = {}) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }); -export const expandPublicTimeline = ({ maxId } = {}) => expandTimeline('public', '/api/v1/timelines/public', { max_id: maxId }); -export const expandCommunityTimeline = ({ maxId } = {}) => expandTimeline('community', '/api/v1/timelines/public', { local: true, max_id: maxId }); -export const expandDirectTimeline = ({ maxId } = {}) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }); -export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); +export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); +export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done); +export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); +export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); +export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); -export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true }); -export const expandHashtagTimeline = (hashtag, { maxId } = {}) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId }); -export const expandListTimeline = (id, { maxId } = {}) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }); +export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true }); +export const expandHashtagTimeline = (hashtag, { maxId } = {}, done = noOp) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId }, done); +export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); export function expandTimelineRequest(timeline) { return { diff --git a/app/javascript/mastodon/actions/trends.js b/app/javascript/mastodon/actions/trends.js new file mode 100644 index 0000000000..853e4f60ae --- /dev/null +++ b/app/javascript/mastodon/actions/trends.js @@ -0,0 +1,32 @@ +import api from '../api'; + +export const TRENDS_FETCH_REQUEST = 'TRENDS_FETCH_REQUEST'; +export const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS'; +export const TRENDS_FETCH_FAIL = 'TRENDS_FETCH_FAIL'; + +export const fetchTrends = () => (dispatch, getState) => { + dispatch(fetchTrendsRequest()); + + api(getState) + .get('/api/v1/trends') + .then(({ data }) => dispatch(fetchTrendsSuccess(data))) + .catch(err => dispatch(fetchTrendsFail(err))); +}; + +export const fetchTrendsRequest = () => ({ + type: TRENDS_FETCH_REQUEST, + skipLoading: true, +}); + +export const fetchTrendsSuccess = trends => ({ + type: TRENDS_FETCH_SUCCESS, + trends, + skipLoading: true, +}); + +export const fetchTrendsFail = error => ({ + type: TRENDS_FETCH_FAIL, + error, + skipLoading: true, + skipAlert: true, +}); diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index 982d34718e..0a6e7c6272 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -43,6 +43,7 @@ class DropdownMenu extends React.PureComponent { componentDidMount () { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + if (this.focusedItem) this.focusedItem.focus(); this.setState({ mounted: true }); } @@ -55,6 +56,46 @@ class DropdownMenu extends React.PureComponent { this.node = c; } + setFocusRef = c => { + this.focusedItem = c; + } + + handleKeyDown = e => { + const items = Array.from(this.node.getElementsByTagName('a')); + const index = items.indexOf(e.currentTarget); + let element; + + switch(e.key) { + case 'Enter': + this.handleClick(e); + break; + case 'ArrowDown': + element = items[index+1]; + if (element) { + element.focus(); + } + break; + case 'ArrowUp': + element = items[index-1]; + if (element) { + element.focus(); + } + break; + case 'Home': + element = items[0]; + if (element) { + element.focus(); + } + break; + case 'End': + element = items[items.length-1]; + if (element) { + element.focus(); + } + break; + } + } + handleClick = e => { const i = Number(e.currentTarget.getAttribute('data-index')); const { action, to } = this.props.items[i]; @@ -79,7 +120,7 @@ class DropdownMenu extends React.PureComponent { return (
/g, '\n\n').replace(/<[^>]*>/g, '')); - return self.registration.showNotification(nextNotification.title, nextNotification); +const handlePush = (event) => { + const { access_token, notification_id, preferred_locale, title, body, icon } = event.data.json(); + + // Placeholder until more information can be loaded + event.waitUntil( + notify({ + title, + body, + icon, + tag: notification_id, + timestamp: new Date(), + badge: '/badge.png', + data: { access_token, preferred_locale, url: '/web/notifications' }, + }).then(() => fetchFromApi(`/api/v1/notifications/${notification_id}`, 'get', access_token)).then(notification => { + const options = {}; + + options.title = formatMessage(`notification.${notification.type}`, preferred_locale, { name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username }); + options.body = notification.status && htmlToPlainText(notification.status.content); + options.icon = notification.account.avatar_static; + options.timestamp = notification.created_at && new Date(notification.created_at); + options.tag = notification.id; + options.badge = '/badge.png'; + options.image = notification.status && notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url || undefined; + options.data = { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/web/statuses/${notification.status.id}` : `/web/accounts/${notification.account.id}` }; + + if (notification.status && notification.status.sensitive) { + options.data.hiddenBody = htmlToPlainText(notification.status.content); + options.data.hiddenImage = notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url; + + options.body = notification.status.spoiler_text; + options.image = undefined; + options.actions = [actionExpand(preferred_locale)]; + } else if (notification.type === 'mention') { + options.actions = [actionReblog(preferred_locale), actionFavourite(preferred_locale)]; + } + + return notify(options); + }) + ); }; -const makeRequest = (notification, action) => - fetch(action.action, { - headers: { - 'Authorization': `Bearer ${notification.data.access_token}`, - 'Content-Type': 'application/json', - }, - method: action.method, - credentials: 'include', - }); +const actionExpand = preferred_locale => ({ + action: 'expand', + icon: '/web-push-icon_expand.png', + title: formatMessage('status.show_more', preferred_locale), +}); + +const actionReblog = preferred_locale => ({ + action: 'reblog', + icon: '/web-push-icon_reblog.png', + title: formatMessage('status.reblog', preferred_locale), +}); + +const actionFavourite = preferred_locale => ({ + action: 'favourite', + icon: '/web-push-icon_favourite.png', + title: formatMessage('status.favourite', preferred_locale), +}); const findBestClient = clients => { const focusedClient = clients.find(client => client.focused); @@ -99,6 +141,24 @@ const findBestClient = clients => { return focusedClient || visibleClient || clients[0]; }; +const expandNotification = notification => { + const newNotification = cloneNotification(notification); + + newNotification.body = newNotification.data.hiddenBody; + newNotification.image = newNotification.data.hiddenImage; + newNotification.actions = [actionReblog(notification.data.preferred_locale), actionFavourite(notification.data.preferred_locale)]; + + return self.registration.showNotification(newNotification.title, newNotification); +}; + +const removeActionFromNotification = (notification, action) => { + const newNotification = cloneNotification(notification); + + newNotification.actions = newNotification.actions.filter(item => item.action !== action); + + return self.registration.showNotification(newNotification.title, newNotification); +}; + const openUrl = url => self.clients.matchAll({ type: 'window' }).then(clientList => { if (clientList.length !== 0) { @@ -124,27 +184,19 @@ const openUrl = url => return self.clients.openWindow(url); }); -const removeActionFromNotification = (notification, action) => { - const actions = notification.actions.filter(act => act.action !== action.action); - const nextNotification = cloneNotification(notification); - - nextNotification.actions = actions; - - return self.registration.showNotification(nextNotification.title, nextNotification); -}; - const handleNotificationClick = (event) => { const reactToNotificationClick = new Promise((resolve, reject) => { if (event.action) { - const action = event.notification.data.actions.find(({ action }) => action === event.action); - - if (action.todo === 'expand') { + if (event.action === 'expand') { resolve(expandNotification(event.notification)); - } else if (action.todo === 'request') { - resolve(makeRequest(event.notification, action) - .then(() => removeActionFromNotification(event.notification, action))); + } else if (event.action === 'reblog') { + const { data } = event.notification; + resolve(fetchFromApi(`/api/v1/statuses/${data.id}/reblog`, 'post', data.access_token).then(() => removeActionFromNotification(event.notification, 'reblog'))); + } else if (event.action === 'favourite') { + const { data } = event.notification; + resolve(fetchFromApi(`/api/v1/statuses/${data.id}/favourite`, 'post', data.access_token).then(() => removeActionFromNotification(event.notification, 'favourite'))); } else { - reject(`Unknown action: ${action.todo}`); + reject(`Unknown action: ${event.action}`); } } else { event.notification.close(); diff --git a/app/javascript/mastodon/storage/modifier.js b/app/javascript/mastodon/storage/modifier.js index c2ed6f807e..9fadabef44 100644 --- a/app/javascript/mastodon/storage/modifier.js +++ b/app/javascript/mastodon/storage/modifier.js @@ -4,6 +4,11 @@ const accountAssetKeys = ['avatar', 'avatar_static', 'header', 'header_static']; const storageMargin = 8388608; const storeLimit = 1024; +// navigator.storage is not present on: +// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.100 Safari/537.36 Edge/16.16299 +// estimate method is not present on Chrome 57.0.2987.98 on Linux. +export const storageFreeable = 'storage' in navigator && 'estimate' in navigator.storage; + function openCache() { // ServiceWorker and Cache API is not available on iOS 11 // https://webkit.org/status/#specification-service-workers @@ -182,7 +187,7 @@ export function putStatuses(records) { } export function freeStorage() { - return navigator.storage.estimate().then(({ quota, usage }) => { + return storageFreeable && navigator.storage.estimate().then(({ quota, usage }) => { if (usage + storageMargin < quota) { return null; } diff --git a/app/javascript/mastodon/stream.js b/app/javascript/mastodon/stream.js index 6c67ba2755..9928d0dd76 100644 --- a/app/javascript/mastodon/stream.js +++ b/app/javascript/mastodon/stream.js @@ -1,21 +1,24 @@ import WebSocketClient from 'websocket.js'; +const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max)); + export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onDisconnect() {}, onReceive() {} })) { return (dispatch, getState) => { const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']); const accessToken = getState().getIn(['meta', 'access_token']); const { onDisconnect, onReceive } = callbacks(dispatch, getState); + let polling = null; const setupPolling = () => { - polling = setInterval(() => { - pollingRefresh(dispatch); - }, 20000); + pollingRefresh(dispatch, () => { + polling = setTimeout(() => setupPolling(), 20000 + randomIntUpTo(20000)); + }); }; const clearPolling = () => { if (polling) { - clearInterval(polling); + clearTimeout(polling); polling = null; } }; @@ -29,8 +32,9 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({ disconnected () { if (pollingRefresh) { - setupPolling(); + polling = setTimeout(() => setupPolling(), randomIntUpTo(40000)); } + onDisconnect(); }, @@ -51,6 +55,7 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({ if (subscription) { subscription.close(); } + clearPolling(); }; diff --git a/app/javascript/mastodon/utils/html.js b/app/javascript/mastodon/utils/html.js new file mode 100644 index 0000000000..0b646ce58f --- /dev/null +++ b/app/javascript/mastodon/utils/html.js @@ -0,0 +1,6 @@ +export const unescapeHTML = (html) => { + const wrapper = document.createElement('div'); + html = html.replace(/||\n/g, ' '); + wrapper.innerHTML = html; + return wrapper.textContent; +}; diff --git a/app/javascript/mastodon/utils/resize_image.js b/app/javascript/mastodon/utils/resize_image.js index 6442eda38c..279a858cad 100644 --- a/app/javascript/mastodon/utils/resize_image.js +++ b/app/javascript/mastodon/utils/resize_image.js @@ -1,3 +1,5 @@ +import EXIF from 'exif-js'; + const MAX_IMAGE_DIMENSION = 1280; const getImageUrl = inputFile => new Promise((resolve, reject) => { @@ -28,6 +30,73 @@ const loadImage = inputFile => new Promise((resolve, reject) => { }).catch(reject); }); +const getOrientation = (img, type = 'image/png') => new Promise(resolve => { + if (type !== 'image/jpeg') { + resolve(1); + return; + } + + EXIF.getData(img, () => { + const orientation = EXIF.getTag(img, 'Orientation'); + resolve(orientation); + }); +}); + +const processImage = (img, { width, height, orientation, type = 'image/png' }) => new Promise(resolve => { + const canvas = document.createElement('canvas'); + + if (4 < orientation && orientation < 9) { + canvas.width = height; + canvas.height = width; + } else { + canvas.width = width; + canvas.height = height; + } + + const context = canvas.getContext('2d'); + + switch (orientation) { + case 2: context.transform(-1, 0, 0, 1, width, 0); break; + case 3: context.transform(-1, 0, 0, -1, width, height); break; + case 4: context.transform(1, 0, 0, -1, 0, height); break; + case 5: context.transform(0, 1, 1, 0, 0, 0); break; + case 6: context.transform(0, 1, -1, 0, height, 0); break; + case 7: context.transform(0, -1, -1, 0, height, width); break; + case 8: context.transform(0, -1, 1, 0, 0, width); break; + } + + context.drawImage(img, 0, 0, width, height); + + canvas.toBlob(resolve, type); +}); + +const resizeImage = (img, type = 'image/png') => new Promise((resolve, reject) => { + const { width, height } = img; + + let newWidth, newHeight; + + if (width > height) { + newHeight = height * MAX_IMAGE_DIMENSION / width; + newWidth = MAX_IMAGE_DIMENSION; + } else if (height > width) { + newWidth = width * MAX_IMAGE_DIMENSION / height; + newHeight = MAX_IMAGE_DIMENSION; + } else { + newWidth = MAX_IMAGE_DIMENSION; + newHeight = MAX_IMAGE_DIMENSION; + } + + getOrientation(img, type) + .then(orientation => processImage(img, { + width: newWidth, + height: newHeight, + orientation, + type, + })) + .then(resolve) + .catch(reject); +}); + export default inputFile => new Promise((resolve, reject) => { if (!inputFile.type.match(/image.*/) || inputFile.type === 'image/gif') { resolve(inputFile); @@ -35,32 +104,13 @@ export default inputFile => new Promise((resolve, reject) => { } loadImage(inputFile).then(img => { - const canvas = document.createElement('canvas'); - const { width, height } = img; - - let newWidth, newHeight; - - if (width < MAX_IMAGE_DIMENSION && height < MAX_IMAGE_DIMENSION) { + if (img.width < MAX_IMAGE_DIMENSION && img.height < MAX_IMAGE_DIMENSION) { resolve(inputFile); return; } - if (width > height) { - newHeight = height * MAX_IMAGE_DIMENSION / width; - newWidth = MAX_IMAGE_DIMENSION; - } else if (height > width) { - newWidth = width * MAX_IMAGE_DIMENSION / height; - newHeight = MAX_IMAGE_DIMENSION; - } else { - newWidth = MAX_IMAGE_DIMENSION; - newHeight = MAX_IMAGE_DIMENSION; - } - - canvas.width = newWidth; - canvas.height = newHeight; - - canvas.getContext('2d').drawImage(img, 0, 0, newWidth, newHeight); - - canvas.toBlob(resolve, inputFile.type); + resizeImage(img, inputFile.type) + .then(resolve) + .catch(() => resolve(inputFile)); }).catch(reject); }); diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index 3377c23291..1e6ee62af2 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -6,8 +6,6 @@ function main() { const emojify = require('../mastodon/features/emoji/emoji').default; const { getLocale } = require('../mastodon/locales'); const { localeData } = getLocale(); - const VideoContainer = require('../mastodon/containers/video_container').default; - const CardContainer = require('../mastodon/containers/card_container').default; const React = require('react'); const ReactDOM = require('react-dom'); @@ -52,24 +50,16 @@ function main() { }); }); - [].forEach.call(document.querySelectorAll('[data-component="Video"]'), (content) => { - const props = JSON.parse(content.getAttribute('data-props')); - ReactDOM.render(, content); - }); + const reactComponents = document.querySelectorAll('[data-component]'); + if (reactComponents.length > 0) { + import(/* webpackChunkName: "containers/media_container" */ '../mastodon/containers/media_container') + .then(({ default: MediaContainer }) => { + const content = document.createElement('div'); - [].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => { - const props = JSON.parse(content.getAttribute('data-props')); - ReactDOM.render(, content); - }); - - const mediaGalleries = document.querySelectorAll('[data-component="MediaGallery"]'); - - if (mediaGalleries.length > 0) { - const MediaGalleriesContainer = require('../mastodon/containers/media_galleries_container').default; - const content = document.createElement('div'); - - ReactDOM.render(, content); - document.body.appendChild(content); + ReactDOM.render(, content); + document.body.appendChild(content); + }) + .catch(error => console.error(error)); } }); } diff --git a/app/javascript/skins/glitch/mastodon-light/common.scss b/app/javascript/skins/glitch/mastodon-light/common.scss new file mode 100644 index 0000000000..c37f407b3e --- /dev/null +++ b/app/javascript/skins/glitch/mastodon-light/common.scss @@ -0,0 +1 @@ +@import 'flavours/glitch/styles/mastodon-light'; diff --git a/app/javascript/skins/glitch/mastodon-light/names.yml b/app/javascript/skins/glitch/mastodon-light/names.yml new file mode 100644 index 0000000000..f15424f2bb --- /dev/null +++ b/app/javascript/skins/glitch/mastodon-light/names.yml @@ -0,0 +1,5 @@ +en: + skins: + glitch: + mastodon-light: Mastodon (light) + diff --git a/app/javascript/styles/mastodon-light.scss b/app/javascript/styles/mastodon-light.scss index 6a22a78226..756a12d868 100644 --- a/app/javascript/styles/mastodon-light.scss +++ b/app/javascript/styles/mastodon-light.scss @@ -1,228 +1,3 @@ -// Set variables -$ui-base-color: #d9e1e8; -$ui-base-lighter-color: darken($ui-base-color, 57%); -$ui-highlight-color: #2b90d9; -$ui-primary-color: darken($ui-highlight-color, 28%); -$ui-secondary-color: #282c37; - -$primary-text-color: black; -$base-overlay-background: $ui-base-color; - -$login-button-color: white; -$account-background-color: white; - -// Import defaults +@import 'mastodon-light/variables'; @import 'application'; - -// Change the color of the log in button -.button { - &.button-alternative-2 { - color: $login-button-color; - } -} - -// Change columns' default background colors -.column { - > .scrollable { - background: lighten($ui-base-color, 13%); - } -} - -.drawer__inner { - background: $ui-base-color; -} - -.drawer__inner__mastodon { - background: $ui-base-color url('data:image/svg+xml;utf8,') no-repeat bottom / 100% auto; -} - -// Change the default appearance of the content warning button -.status__content, -.reply-indicator__content { - - .status__content__spoiler-link { - - background: darken($ui-base-color, 30%); - - &:hover { - background: darken($ui-base-color, 35%); - text-decoration: none; - } - - } - -} - -// Change the default appearance of the action buttons -.icon-button { - - &:hover, - &:active, - &:focus { - color: darken($ui-base-color, 40%); - transition: color 200ms ease-out; - } - - &.disabled { - color: darken($ui-base-color, 30%); - } - -} - -.status { - &.status-direct { - .icon-button.disabled { - color: darken($ui-base-color, 30%); - } - } -} - -button.icon-button i.fa-retweet { - &:hover { - background-image: url("data:image/svg+xml;utf8,"); - } -} - -button.icon-button.disabled i.fa-retweet { - background-image: url("data:image/svg+xml;utf8,"); -} - -// Change the colors used in the dropdown menu -.dropdown-menu { - background: $ui-base-color; -} - -.dropdown-menu__arrow { - - &.left { - border-left-color: $ui-base-color; - } - - &.top { - border-top-color: $ui-base-color; - } - - &.bottom { - border-bottom-color: $ui-base-color; - } - - &.right { - border-right-color: $ui-base-color; - } - -} - -.dropdown-menu__item { - a { - background: $ui-base-color; - color: $ui-secondary-color; - } -} - -// Change the default color of several parts of the compose form -.compose-form { - - .compose-form__warning { - color: lighten($ui-secondary-color, 65%); - } - - strong { - color: lighten($ui-secondary-color, 65%); - } - - .autosuggest-textarea__textarea, - .spoiler-input__input { - - color: darken($ui-base-color, 80%); - - &::placeholder { - color: darken($ui-base-color, 70%); - } - - } - - .compose-form__buttons-wrapper { - background: darken($ui-base-color, 10%); - } - - .privacy-dropdown__option { - color: $ui-primary-color; - } - - .privacy-dropdown__option__content { - - strong { - color: $ui-primary-color; - } - - } - -} - -// Change the default color used for the text in an empty column or on the error column -.empty-column-indicator, -.error-column { - color: darken($ui-base-color, 60%); -} - -// Change the default colors used on some parts of the profile pages -.activity-stream-tabs { - - background: $account-background-color; - - a { - &.active { - color: $ui-primary-color; - } - } - -} - -.activity-stream { - - .entry { - background: $account-background-color; - } - - .status.light { - - .status__content { - color: $primary-text-color; - } - - .display-name { - strong { - color: $primary-text-color; - } - } - - } - -} - -.accounts-grid { - .account-grid-card { - - .controls { - .icon-button { - color: $ui-secondary-color; - } - } - - .name { - a { - color: $primary-text-color; - } - } - - .username { - color: $ui-secondary-color; - } - - .account__header__content { - color: $primary-text-color; - } - - } -} - +@import 'mastodon-light/diff'; diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss new file mode 100644 index 0000000000..fe304317d9 --- /dev/null +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -0,0 +1,240 @@ +// Notes! +// Sass color functions, "darken" and "lighten" are automatically replaced. + +// Change the colors of button texts +.button { + color: $white; + + &.button-alternative-2 { + color: $white; + } +} + +// Change default background colors of columns +.column { + > .scrollable { + background: $white; + } +} + +.drawer__inner { + background: $ui-base-color; +} + +.drawer__inner__mastodon { + background: $ui-base-color url('data:image/svg+xml;utf8,') no-repeat bottom / 100% auto; +} + +.compose-form .compose-form__modifiers .compose-form__upload__actions .icon-button { + color: $ui-base-color; + + &:active, + &:focus, + &:hover { + color: darken($ui-base-color, 7%); + } +} + +.compose-form .compose-form__modifiers .compose-form__upload-description input { + color: $ui-base-color; + + &::placeholder { + color: $ui-base-color; + } +} + +.compose-form .compose-form__buttons-wrapper { + background: darken($ui-base-color, 6%); +} + +.focusable:focus { + background: $ui-base-color; +} + +.status.status-direct { + background: lighten($ui-base-color, 4%); +} + +.focusable:focus .status.status-direct { + background: lighten($ui-base-color, 8%); +} + +.detailed-status, +.detailed-status__action-bar { + background: darken($ui-base-color, 6%); +} + +// Change the background color of status__content__spoiler-link +.reply-indicator__content .status__content__spoiler-link, +.status__content .status__content__spoiler-link { + background: $ui-base-lighter-color; + + &:hover { + background: lighten($ui-base-lighter-color, 6%); + } +} + +// Change the colors used in the dropdown menu +.dropdown-menu { + background: $ui-base-color; +} + +.dropdown-menu__arrow { + &.left { + border-left-color: $ui-base-color; + } + + &.top { + border-top-color: $ui-base-color; + } + + &.bottom { + border-bottom-color: $ui-base-color; + } + + &.right { + border-right-color: $ui-base-color; + } +} + +.dropdown-menu__item { + a { + background: $ui-base-color; + color: $ui-secondary-color; + } +} + +// Change the text colors on inverted background +.privacy-dropdown__option.active .privacy-dropdown__option__content, +.privacy-dropdown__option.active .privacy-dropdown__option__content strong, +.privacy-dropdown__option:hover .privacy-dropdown__option__content, +.privacy-dropdown__option:hover .privacy-dropdown__option__content strong, +.dropdown-menu__item a:active, +.dropdown-menu__item a:focus, +.dropdown-menu__item a:hover, +.actions-modal ul li:not(:empty) a.active, +.actions-modal ul li:not(:empty) a.active button, +.actions-modal ul li:not(:empty) a:active, +.actions-modal ul li:not(:empty) a:active button, +.actions-modal ul li:not(:empty) a:focus, +.actions-modal ul li:not(:empty) a:focus button, +.actions-modal ul li:not(:empty) a:hover, +.actions-modal ul li:not(:empty) a:hover button, +.admin-wrapper .sidebar ul ul a.selected, +.simple_form .block-button, +.simple_form .button, +.simple_form button { + color: $white; +} + +// Change the background colors of modals +.actions-modal, +.boost-modal, +.confirmation-modal, +.mute-modal, +.report-modal, +.embed-modal, +.error-modal, +.onboarding-modal { + background: $ui-base-color; +} + +.boost-modal__action-bar, +.confirmation-modal__action-bar, +.mute-modal__action-bar, +.onboarding-modal__paginator, +.error-modal__footer { + background: darken($ui-base-color, 6%); + + .onboarding-modal__nav, + .error-modal__nav { + &:hover, + &:focus, + &:active { + background-color: darken($ui-base-color, 12%); + } + } +} + +.display-case__case { + background: $white; +} + +.embed-modal .embed-modal__container .embed-modal__html { + background: $white; + + &:focus { + background: darken($ui-base-color, 6%); + } +} + +.react-toggle-track { + background: $ui-secondary-color; +} + +.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track { + background: darken($ui-secondary-color, 10%); +} + +.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track { + background: lighten($ui-highlight-color, 10%); +} + +// Change the default color used for the text in an empty column or on the error column +.empty-column-indicator, +.error-column { + color: $primary-text-color; +} + +// Change the default colors used on some parts of the profile pages +.activity-stream-tabs { + background: $account-background-color; + + a { + &.active { + color: $ui-primary-color; + } + } +} + +.activity-stream { + .entry { + background: $account-background-color; + } + + .status.light { + .status__content { + color: $primary-text-color; + } + + .display-name { + strong { + color: $primary-text-color; + } + } + } +} + +.accounts-grid { + .account-grid-card { + .controls { + .icon-button { + color: $ui-secondary-color; + } + } + + .name { + a { + color: $primary-text-color; + } + } + + .username { + color: $ui-secondary-color; + } + + .account__header__content { + color: $primary-text-color; + } + } +} diff --git a/app/javascript/styles/mastodon-light/variables.scss b/app/javascript/styles/mastodon-light/variables.scss new file mode 100644 index 0000000000..9f6d470b13 --- /dev/null +++ b/app/javascript/styles/mastodon-light/variables.scss @@ -0,0 +1,41 @@ +// Dependent colors +$black: #000000; +$white: #ffffff; + +$classic-base-color: #282c37; +$classic-primary-color: #9baec8; +$classic-secondary-color: #d9e1e8; +$classic-highlight-color: #2b90d9; + +// Differences +$success-green: #3c754d; + +$base-overlay-background: $white !default; +$valid-value-color: $success-green !default; + +$ui-base-color: $classic-secondary-color !default; +$ui-base-lighter-color: #b0c0cf; +$ui-primary-color: #9bcbed; +$ui-secondary-color: $classic-base-color !default; +$ui-highlight-color: #2b5fd9; + +$primary-text-color: $black !default; +$darker-text-color: $classic-base-color !default; +$dark-text-color: #444b5d; +$action-button-color: #606984; + +$inverted-text-color: $black !default; +$lighter-text-color: $classic-base-color !default; +$light-text-color: #444b5d; + +//Newly added colors +$account-background-color: $white !default; + +//Invert darkened and lightened colors +@function darken($color, $amount) { + @return hsl(hue($color), saturation($color), lightness($color) + $amount); +} + +@function lighten($color, $amount) { + @return hsl(hue($color), saturation($color), lightness($color) - $amount); +} diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss index c9c0e3081a..77728995d1 100644 --- a/app/javascript/styles/mastodon/about.scss +++ b/app/javascript/styles/mastodon/about.scss @@ -396,7 +396,7 @@ $small-breakpoint: 960px; display: flex; justify-content: center; align-items: center; - color: $ui-primary-color; + color: $darker-text-color; text-decoration: none; padding: 12px 16px; line-height: 32px; diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index c2d0de4b99..3ccce383b0 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -1,5 +1,5 @@ .card { - background-color: lighten($ui-base-color, 4%); + background-color: $base-shadow-color; background-size: cover; background-position: center; border-radius: 4px 4px 0 0; @@ -79,6 +79,10 @@ font-weight: 400; overflow: hidden; text-overflow: ellipsis; + + .fa { + margin-left: 3px; + } } } @@ -322,6 +326,15 @@ z-index: 2; position: relative; + &.empty img { + position: absolute; + opacity: 0.2; + height: 200px; + left: 0; + bottom: 0; + pointer-events: none; + } + @media screen and (max-width: 740px) { border-radius: 0; box-shadow: none; @@ -438,8 +451,8 @@ font-size: 14px; font-weight: 500; text-align: center; - padding: 60px 0; - padding-top: 55px; + padding: 130px 0; + padding-top: 125px; margin: 0 auto; cursor: default; } @@ -565,36 +578,41 @@ } .account__header__fields { - border-collapse: collapse; padding: 0; margin: 15px -15px -15px; border: 0 none; border-top: 1px solid lighten($ui-base-color, 4%); border-bottom: 1px solid lighten($ui-base-color, 4%); + font-size: 14px; + line-height: 20px; - th, - td { - padding: 15px; - padding-left: 15px; - border: 0 none; + dl { + display: flex; border-bottom: 1px solid lighten($ui-base-color, 4%); - vertical-align: middle; } - th { - padding-left: 15px; - font-weight: 500; + dt, + dd { + box-sizing: border-box; + padding: 14px; text-align: center; - width: 94px; + max-height: 48px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + dt { + font-weight: 500; + width: 120px; + flex: 0 0 auto; color: $secondary-text-color; background: rgba(darken($ui-base-color, 8%), 0.5); } - td { + dd { + flex: 1 1 auto; color: $darker-text-color; - text-align: center; - width: 100%; - padding-left: 0; } a { @@ -608,12 +626,7 @@ } } - tr { - &:last-child { - th, - td { - border-bottom: 0; - } - } + dl:last-child { + border-bottom: 0; } } diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index a6cc8b62ba..560b11ddf4 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -336,7 +336,8 @@ } } -.simple_form.new_report_note { +.simple_form.new_report_note, +.simple_form.new_account_moderation_note { max-width: 100%; } @@ -483,19 +484,12 @@ } a.name-tag, -.name-tag { - display: flex; - align-items: center; +.name-tag, +a.inline-name-tag, +.inline-name-tag { text-decoration: none; color: $secondary-text-color; - .avatar { - display: block; - margin: 0; - margin-right: 5px; - border-radius: 50%; - } - .username { font-weight: 500; } @@ -513,6 +507,26 @@ a.name-tag, } } +a.name-tag, +.name-tag { + display: flex; + align-items: center; + + .avatar { + display: block; + margin: 0; + margin-right: 5px; + border-radius: 50%; + } + + &.suspended { + .avatar { + filter: grayscale(100%); + opacity: 0.8; + } + } +} + .speech-bubble { margin-bottom: 20px; border-left: 4px solid $ui-highlight-color; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index a982585c33..712b6f813f 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -425,7 +425,7 @@ .icon-button { flex: 0 1 auto; - color: $action-button-color; + color: $secondary-text-color; font-size: 14px; font-weight: 500; padding: 10px; @@ -434,7 +434,7 @@ &:hover, &:focus, &:active { - color: lighten($action-button-color, 7%); + color: lighten($secondary-text-color, 7%); } } @@ -1373,9 +1373,8 @@ a.account__display-name { } .notification__message { - margin-left: 68px; - padding: 8px 0; - padding-bottom: 0; + margin: 0 10px 0 68px; + padding: 8px 0 0; cursor: default; color: $darker-text-color; font-size: 15px; @@ -1984,6 +1983,7 @@ a.account__display-name { padding: 15px; margin: 0; z-index: 3; + outline: 0; &:hover { text-decoration: underline; @@ -3181,6 +3181,7 @@ a.status-card { &.active { background: $ui-highlight-color; color: $primary-text-color; + outline: 0; .privacy-dropdown__option__content { color: $primary-text-color; @@ -3283,6 +3284,15 @@ a.status-card { } .search__icon { + &::-moz-focus-inner { + border: 0; + } + + &::-moz-focus-inner, + &:focus { + outline: 0 !important; + } + .fa { position: absolute; top: 10px; @@ -3332,40 +3342,33 @@ a.status-card { .search-results__header { color: $dark-text-color; background: lighten($ui-base-color, 2%); - border-bottom: 1px solid darken($ui-base-color, 4%); - padding: 15px 10px; - font-size: 14px; + padding: 15px; font-weight: 500; + font-size: 16px; + cursor: default; + + .fa { + display: inline-block; + margin-right: 5px; + } } .search-results__section { - margin-bottom: 20px; + margin-bottom: 5px; h5 { - position: relative; + background: darken($ui-base-color, 4%); + border-bottom: 1px solid lighten($ui-base-color, 8%); + cursor: default; + display: flex; + padding: 15px; + font-weight: 500; + font-size: 16px; + color: $dark-text-color; - &::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 { + .fa { display: inline-block; - background: $ui-base-color; - color: $darker-text-color; - font-size: 14px; - font-weight: 500; - padding: 10px; - position: relative; - z-index: 1; - cursor: default; + margin-right: 5px; } } @@ -4033,7 +4036,7 @@ a.status-card { .report-modal__statuses { flex: 1 1 auto; min-height: 20vh; - max-height: 40vh; + max-height: 80vh; overflow-y: auto; overflow-x: hidden; @@ -4432,6 +4435,10 @@ a.status-card { max-width: 100%; border-radius: 4px; + &:focus { + outline: 0; + } + video { max-width: 100vw; max-height: 80vh; @@ -4732,6 +4739,8 @@ a.status-card { } } +.community-timeline__section-headline, +.public-timeline__section-headline, .account__section-headline { background: darken($ui-base-color, 4%); border-bottom: 1px solid lighten($ui-base-color, 8%); @@ -5159,39 +5168,124 @@ noscript { } } +.account__header .roles { + margin-top: 20px; + margin-bottom: 20px; + padding: 0 15px; +} + .account__header .account__header__fields { font-size: 14px; line-height: 20px; overflow: hidden; - border-collapse: collapse; margin: 20px -10px -20px; border-bottom: 0; - tr { + dl { border-top: 1px solid lighten($ui-base-color, 8%); - text-align: center; + display: flex; } - th, - td { + dt, + dd { + box-sizing: border-box; padding: 14px 20px; - vertical-align: middle; - max-height: 40px; + text-align: center; + max-height: 48px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } - th { + dt { color: $darker-text-color; background: darken($ui-base-color, 4%); - max-width: 120px; + width: 120px; + flex: 0 0 auto; font-weight: 500; } - td { - flex: auto; + dd { + flex: 1 1 auto; color: $primary-text-color; background: $ui-base-color; } } + +.trends { + &__header { + color: $dark-text-color; + background: lighten($ui-base-color, 2%); + border-bottom: 1px solid darken($ui-base-color, 4%); + font-weight: 500; + padding: 15px; + font-size: 16px; + cursor: default; + + .fa { + display: inline-block; + margin-right: 5px; + } + } + + &__item { + display: flex; + align-items: center; + padding: 15px; + border-bottom: 1px solid lighten($ui-base-color, 8%); + + &:last-child { + border-bottom: 0; + } + + &__name { + flex: 1 1 auto; + color: $dark-text-color; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + strong { + font-weight: 500; + } + + a { + color: $darker-text-color; + text-decoration: none; + font-size: 14px; + font-weight: 500; + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &:hover, + &:focus, + &:active { + span { + text-decoration: underline; + } + } + } + } + + &__current { + flex: 0 0 auto; + width: 100px; + font-size: 24px; + line-height: 36px; + font-weight: 500; + text-align: center; + color: $secondary-text-color; + } + + &__sparkline { + flex: 0 0 auto; + width: 50px; + + path { + stroke: lighten($highlight-text-color, 6%) !important; + } + } + } +} diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index 9d5ab66a48..ac648c8680 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -60,7 +60,7 @@ } } -.media-gallery-standalone__body { +.media-standalone__body { overflow: hidden; } diff --git a/app/javascript/styles/mastodon/footer.scss b/app/javascript/styles/mastodon/footer.scss index ba2a06954e..fe2d40c0cc 100644 --- a/app/javascript/styles/mastodon/footer.scss +++ b/app/javascript/styles/mastodon/footer.scss @@ -4,7 +4,7 @@ font-size: 12px; color: $darker-text-color; - .domain { + .footer__domain { font-weight: 500; a { @@ -26,5 +26,13 @@ text-decoration: none; } } + + img { + margin: 0 4px; + position: relative; + bottom: -1px; + height: 18px; + vertical-align: top; + } } } diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index f978901870..de16784a87 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -87,6 +87,10 @@ code { align-items: flex-start; } + &.file .label_input { + flex-wrap: nowrap; + } + &.select .label_input { align-items: initial; } diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index fa876e6031..982bfd9900 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -1,3 +1,9 @@ +@keyframes Swag { + 0% { background-position: 0% 0%; } + 50% { background-position: 100% 0%; } + 100% { background-position: 200% 0%; } +} + .table { width: 100%; max-width: 100%; @@ -187,6 +193,11 @@ a.table-action-link { strong { font-weight: 700; + background: linear-gradient(to right, orange , yellow, green, cyan, blue, violet,orange , yellow, green, cyan, blue, violet); + background-size: 200% 100%; + background-clip: text; + color: transparent; + animation: Swag 2s linear 0s infinite; } } } diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/variables.scss index cbefe35b4d..40aeb4afcf 100644 --- a/app/javascript/styles/mastodon/variables.scss +++ b/app/javascript/styles/mastodon/variables.scss @@ -1,10 +1,10 @@ // Commonly used web colors $black: #000000; // Black $white: #ffffff; // White -$success-green: #79bd9a; // Padua -$error-red: #df405a; // Cerise -$warning-red: #ff5050; // Sunset Orange -$gold-star: #ca8f04; // Dark Goldenrod +$success-green: #79bd9a !default; // Padua +$error-red: #df405a !default; // Cerise +$warning-red: #ff5050 !default; // Sunset Orange +$gold-star: #ca8f04 !default; // Dark Goldenrod // Values from the classic Mastodon UI $classic-base-color: #282c37; // Midnight Express diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 84d4b17520..03476920b2 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -118,4 +118,13 @@ class ActivityPub::Activity def delete_later!(uri) redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri) end + + def fetch_remote_original_status + if object_uri.start_with?('http') + return if ActivityPub::TagManager.instance.local_uri?(object_uri) + ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true, on_behalf_of: @account.followers.local.first) + elsif @object['url'].present? + ::FetchRemoteStatusService.new.call(@object['url']) + end + end end diff --git a/app/lib/activitypub/activity/add.rb b/app/lib/activitypub/activity/add.rb index ea94d2f983..688ab00b33 100644 --- a/app/lib/activitypub/activity/add.rb +++ b/app/lib/activitypub/activity/add.rb @@ -4,9 +4,10 @@ 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) + status = status_from_uri(object_uri) + status ||= fetch_remote_original_status - return unless status.account_id == @account.id && !@account.pinned?(status) + return unless !status.nil? && status.account_id == @account.id && !@account.pinned?(status) StatusPin.create!(account: @account, status: status) end diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb index 7e146ea8c4..1147a4481f 100644 --- a/app/lib/activitypub/activity/announce.rb +++ b/app/lib/activitypub/activity/announce.rb @@ -26,16 +26,6 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity private - def fetch_remote_original_status - if object_uri.start_with?('http') - return if ActivityPub::TagManager.instance.local_uri?(object_uri) - - ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true) - elsif @object['url'].present? - ::FetchRemoteStatusService.new.call(@object['url']) - end - end - def announceable?(status) status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility? end diff --git a/app/lib/activitypub/activity/block.rb b/app/lib/activitypub/activity/block.rb index f630d5db2a..26da8bdf5c 100644 --- a/app/lib/activitypub/activity/block.rb +++ b/app/lib/activitypub/activity/block.rb @@ -7,6 +7,6 @@ class ActivityPub::Activity::Block < ActivityPub::Activity return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id']) || @account.blocking?(target_account) UnfollowService.new.call(target_account, @account) if target_account.following?(@account) - @account.block!(target_account) + @account.block!(target_account, uri: @json['id']) end end diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 8d17a4ebe2..869749f1ec 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -11,6 +11,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity if lock.acquired? @status = find_existing_status process_status if @status.nil? + else + raise Mastodon::RaceConditionError end end @@ -76,9 +78,12 @@ class ActivityPub::Activity::Create < ActivityPub::Activity return if tag['name'].blank? hashtag = tag['name'].gsub(/\A#/, '').mb_chars.downcase - hashtag = Tag.where(name: hashtag).first_or_initialize(name: hashtag) + hashtag = Tag.where(name: hashtag).first_or_create(name: hashtag) + + return if status.tags.include?(hashtag) status.tags << hashtag + TrendingTags.record_use!(hashtag, status.account, status.created_at) rescue ActiveRecord::RecordInvalid nil end diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb index 8adbbb9c33..fbbf358a87 100644 --- a/app/lib/activitypub/activity/follow.rb +++ b/app/lib/activitypub/activity/follow.rb @@ -12,7 +12,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity return end - follow_request = FollowRequest.create!(account: @account, target_account: target_account) + follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id']) if target_account.locked? NotifyService.new.call(target_account, follow_request) diff --git a/app/lib/activitypub/activity/remove.rb b/app/lib/activitypub/activity/remove.rb index 62a1e3196e..f523ead9f6 100644 --- a/app/lib/activitypub/activity/remove.rb +++ b/app/lib/activitypub/activity/remove.rb @@ -6,7 +6,7 @@ class ActivityPub::Activity::Remove < ActivityPub::Activity status = status_from_uri(object_uri) - return unless status.account_id == @account.id + return unless !status.nil? && status.account_id == @account.id pin = StatusPin.find_by(account: @account, status: status) pin&.destroy! diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index fa2a8f7d31..95d1cf9f35 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -38,6 +38,10 @@ class ActivityPub::TagManager end end + def generate_uri_for(_target) + URI.join(root_url, 'payloads', SecureRandom.uuid) + end + def activity_uri_for(target) raise ArgumentError, 'target must be a local activity' unless %i(note comment activity).include?(target.object_type) && target.local? @@ -82,6 +86,8 @@ class ActivityPub::TagManager end def local_uri?(uri) + return false if uri.nil? + uri = Addressable::URI.parse(uri) host = uri.normalized_host host = "#{host}:#{uri.port}" if uri.port @@ -95,6 +101,8 @@ class ActivityPub::TagManager end def uri_to_resource(uri, klass) + return if uri.nil? + if local_uri?(uri) case klass.name when 'Account' diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 050c651ee9..e1ab05cc0b 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -67,9 +67,17 @@ class Formatter html.html_safe # rubocop:disable Rails/OutputSafety end - def format_field(account, str) + def format_display_name(account, **options) + html = encode(account.display_name.presence || account.username) + html = encode_custom_emojis(html, account.emojis) if options[:custom_emojify] + html.html_safe # rubocop:disable Rails/OutputSafety + end + + def format_field(account, str, **options) return reformat(str).html_safe unless account.local? # rubocop:disable Rails/OutputSafety - encode_and_link_urls(str, me: true).html_safe # rubocop:disable Rails/OutputSafety + html = encode_and_link_urls(str, me: true) + html = encode_custom_emojis(html, account.emojis) if options[:custom_emojify] + html.html_safe # rubocop:disable Rails/OutputSafety end def linkify(text) diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb index 1e7f470299..d3a303a0c4 100644 --- a/app/lib/ostatus/activity/creation.rb +++ b/app/lib/ostatus/activity/creation.rb @@ -15,6 +15,8 @@ class OStatus::Activity::Creation < OStatus::Activity::Base @status = find_status(id) return [@status, false] unless @status.nil? @status = process_status + else + raise Mastodon::RaceConditionError end end @@ -46,7 +48,8 @@ class OStatus::Activity::Creation < OStatus::Activity::Base visibility: visibility_scope, conversation: find_or_create_conversation, thread: thread? ? find_status(thread.first) || find_activitypub_status(thread.first, thread.second) : nil, - media_attachment_ids: media_attachments.map(&:id) + media_attachment_ids: media_attachments.map(&:id), + sensitive: sensitive? ) save_mentions(status) @@ -105,6 +108,11 @@ class OStatus::Activity::Creation < OStatus::Activity::Base private + def sensitive? + # OStatus-specific convention (not standard) + @xml.xpath('./xmlns:category', xmlns: OStatus::TagManager::XMLNS).any? { |category| category['term'] == 'nsfw' } + end + def find_or_create_conversation uri = @xml.at_xpath('./ostatus:conversation', ostatus: OStatus::TagManager::OS_XMLNS)&.attribute('ref')&.content return if uri.nil? diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb index 7c66f2066e..5c6ff4f9b9 100644 --- a/app/lib/ostatus/atom_serializer.rb +++ b/app/lib/ostatus/atom_serializer.rb @@ -354,7 +354,7 @@ class OStatus::AtomSerializer append_element(entry, 'summary', status.spoiler_text, 'xml:lang': status.language) if status.spoiler_text? append_element(entry, 'content', Formatter.instance.format(status).to_str, type: 'html', 'xml:lang': status.language) - status.mentions.order(:id).each do |mentioned| + status.mentions.sort_by(&:id).each do |mentioned| append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:person], href: OStatus::TagManager.instance.uri_for(mentioned.account)) end @@ -368,6 +368,7 @@ class OStatus::AtomSerializer append_element(entry, 'link', nil, rel: :enclosure, type: media.file_content_type, length: media.file_file_size, href: full_asset_url(media.file.url(:original, false))) end + append_element(entry, 'category', nil, term: 'nsfw') if status.sensitive? && status.media_attachments.any? append_element(entry, 'mastodon:scope', status.visibility) status.emojis.each do |emoji| diff --git a/app/lib/request.rb b/app/lib/request.rb index 00f94dacf5..397614fac9 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -57,10 +57,11 @@ class Request private def set_common_headers! - @headers[REQUEST_TARGET] = "#{@verb} #{@url.path}" - @headers['User-Agent'] = user_agent - @headers['Host'] = @url.host - @headers['Date'] = Time.now.utc.httpdate + @headers[REQUEST_TARGET] = "#{@verb} #{@url.path}" + @headers['User-Agent'] = Mastodon::Version.user_agent + @headers['Host'] = @url.host + @headers['Date'] = Time.now.utc.httpdate + @headers['Accept-Encoding'] = 'gzip' if @verb != :head end def set_digest! @@ -82,10 +83,6 @@ class Request @headers.keys.join(' ').downcase end - def user_agent - @user_agent ||= "#{HTTP::Request::USER_AGENT} (Mastodon/#{Mastodon::Version}; +#{root_url})" - end - def key_id case @key_id_format when :acct @@ -100,7 +97,7 @@ class Request end def http_client - @http_client ||= HTTP.timeout(:per_operation, timeout).follow(max_hops: 2) + @http_client ||= HTTP.use(:auto_inflate).timeout(:per_operation, timeout).follow(max_hops: 2) end def use_proxy? diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 78b3aa77cc..f8bacb036c 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -30,6 +30,7 @@ class UserSettingsDecorator 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['hide_network'] = hide_network_preference if change?('setting_hide_network') end def merged_notification_emails @@ -92,6 +93,10 @@ class UserSettingsDecorator settings['setting_skin'] end + def hide_network_preference + boolean_cast_setting 'setting_hide_network' + end + def boolean_cast_setting(key) ActiveModel::Type::Boolean.new.cast(settings[key]) end diff --git a/app/models/account.rb b/app/models/account.rb index c1ce1e99e8..48f2847858 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -45,6 +45,7 @@ # moved_to_account_id :bigint(8) # featured_collection_url :string # fields :jsonb +# actor_type :string # class Account < ApplicationRecord @@ -76,6 +77,7 @@ class Account < ApplicationRecord validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? } validates :display_name, length: { maximum: 30 }, if: -> { local? && will_save_change_to_display_name? } validate :note_length_does_not_exceed_length_limit, if: -> { local? && will_save_change_to_note? } + validates :fields, length: { maximum: 4 }, if: -> { local? && will_save_change_to_fields? } # Timelines has_many :stream_entries, inverse_of: :account, dependent: :destroy @@ -137,6 +139,7 @@ class Account < ApplicationRecord :moderator?, :staff?, :locale, + :hides_network?, to: :user, prefix: true, allow_nil: true @@ -151,6 +154,16 @@ class Account < ApplicationRecord moved_to_account_id.present? end + def bot? + %w(Application Service).include? actor_type + end + + alias bot bot? + + def bot=(val) + self.actor_type = ActiveModel::Type::Boolean.new.cast(val) ? 'Service' : 'Person' + end + def acct local? ? username : "#{username}@#{domain}" end @@ -201,9 +214,11 @@ class Account < ApplicationRecord def fields_attributes=(attributes) fields = [] - attributes.each_value do |attr| - next if attr[:name].blank? - fields << attr + if attributes.is_a?(Hash) + attributes.each_value do |attr| + next if attr[:name].blank? + fields << attr + end end self[:fields] = fields @@ -272,8 +287,8 @@ class Account < ApplicationRecord def initialize(account, attr) @account = account - @name = attr['name'] - @value = attr['value'] + @name = attr['name'].strip[0, 255] + @value = attr['value'].strip[0, 255] @errors = {} end @@ -398,7 +413,7 @@ class Account < ApplicationRecord end def emojis - @emojis ||= CustomEmoji.from_text(note, domain) + @emojis ||= CustomEmoji.from_text(emojifiable_text, domain) end before_create :generate_keys @@ -441,4 +456,8 @@ class Account < ApplicationRecord self.domain = TagManager.instance.normalize_domain(domain) end + + def emojifiable_text + [note, display_name, fields.map(&:value)].join(' ') + end end diff --git a/app/models/block.rb b/app/models/block.rb index df4a6bbacb..bf3e076003 100644 --- a/app/models/block.rb +++ b/app/models/block.rb @@ -8,6 +8,7 @@ # updated_at :datetime not null # account_id :bigint(8) not null # target_account_id :bigint(8) not null +# uri :string # class Block < ApplicationRecord @@ -19,7 +20,12 @@ class Block < ApplicationRecord validates :account_id, uniqueness: { scope: :target_account_id } + def local? + false # Force uri_for to use uri attribute + end + after_commit :remove_blocking_cache + before_validation :set_uri, only: :create private @@ -27,4 +33,8 @@ class Block < ApplicationRecord Rails.cache.delete("exclude_account_ids_for:#{account_id}") Rails.cache.delete("exclude_account_ids_for:#{target_account_id}") end + + def set_uri + self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? + end end diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 20fc74ba6a..a064248d91 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -82,16 +82,19 @@ module AccountInteractions has_many :domain_blocks, class_name: 'AccountDomainBlock', dependent: :destroy end - def follow!(other_account, reblogs: nil) + def follow!(other_account, reblogs: nil, uri: nil) reblogs = true if reblogs.nil? - rel = active_relationships.create_with(show_reblogs: reblogs).find_or_create_by!(target_account: other_account) - rel.update!(show_reblogs: reblogs) + rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri) + .find_or_create_by!(target_account: other_account) + + rel.update!(show_reblogs: reblogs) rel end - def block!(other_account) - block_relationships.find_or_create_by!(target_account: other_account) + def block!(other_account, uri: nil) + block_relationships.create_with(uri: uri) + .find_or_create_by!(target_account: other_account) end def mute!(other_account, notifications: nil) diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb index 7f1ef5191b..c17f047760 100644 --- a/app/models/concerns/remotable.rb +++ b/app/models/concerns/remotable.rb @@ -41,6 +41,9 @@ module Remotable rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e Rails.logger.debug "Error fetching remote #{attachment_name}: #{e}" nil + rescue Paperclip::Error, Mastodon::DimensionsValidationError => e + Rails.logger.debug "Error processing remote #{attachment_name}: #{e}" + nil end end diff --git a/app/models/concerns/status_threading_concern.rb b/app/models/concerns/status_threading_concern.rb index 8e817be00c..1ba8fc6939 100644 --- a/app/models/concerns/status_threading_concern.rb +++ b/app/models/concerns/status_threading_concern.rb @@ -74,16 +74,7 @@ module StatusThreadingConcern statuses = statuses_with_accounts(ids).to_a account_ids = statuses.map(&:account_id).uniq domains = statuses.map(&:account_domain).compact.uniq - - relations = if account.present? - { - blocking: Account.blocking_map(account_ids, account.id), - blocked_by: Account.blocked_by_map(account_ids, account.id), - muting: Account.muting_map(account_ids, account.id), - following: Account.following_map(account_ids, account.id), - domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id), - } - end + relations = relations_map_for_account(account, account_ids, domains) statuses.reject! { |status| filter_from_context?(status, account, relations) } @@ -91,6 +82,18 @@ module StatusThreadingConcern statuses.sort_by! { |status| ids.index(status.id) } end + def relations_map_for_account(account, account_ids, domains) + return {} if account.nil? + + { + blocking: Account.blocking_map(account_ids, account.id), + blocked_by: Account.blocked_by_map(account_ids, account.id), + muting: Account.muting_map(account_ids, account.id), + following: Account.following_map(account_ids, account.id), + domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id), + } + end + def statuses_with_accounts(ids) Status.where(id: ids).includes(:account) end diff --git a/app/models/follow.rb b/app/models/follow.rb index 2ca42ff70b..eaf8445f3b 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -9,6 +9,7 @@ # account_id :bigint(8) not null # target_account_id :bigint(8) not null # show_reblogs :boolean default(TRUE), not null +# uri :string # class Follow < ApplicationRecord @@ -26,4 +27,16 @@ class Follow < ApplicationRecord validates :account_id, uniqueness: { scope: :target_account_id } scope :recent, -> { reorder(id: :desc) } + + def local? + false # Force uri_for to use uri attribute + end + + before_validation :set_uri, only: :create + + private + + def set_uri + self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? + end end diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index d559a8f62f..9c4875564b 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -9,6 +9,7 @@ # account_id :bigint(8) not null # target_account_id :bigint(8) not null # show_reblogs :boolean default(TRUE), not null +# uri :string # class FollowRequest < ApplicationRecord @@ -23,11 +24,22 @@ class FollowRequest < ApplicationRecord validates :account_id, uniqueness: { scope: :target_account_id } def authorize! - account.follow!(target_account, reblogs: show_reblogs) + account.follow!(target_account, reblogs: show_reblogs, uri: uri) MergeWorker.perform_async(target_account.id, account.id) - destroy! end alias reject! destroy! + + def local? + false # Force uri_for to use uri attribute + end + + before_validation :set_uri, only: :create + + private + + def set_uri + self.uri = ActivityPub::TagManager.instance.generate_uri_for(self) if uri.nil? + end end diff --git a/app/models/status.rb b/app/models/status.rb index 0b3a7c0aaa..c6d6453df6 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -195,12 +195,45 @@ class Status < ApplicationRecord where(account: [account] + account.following).where(visibility: [:public, :unlisted, :private]) end - def as_direct_timeline(account) - query = joins("LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = #{account.id}") - .where("mentions.account_id = #{account.id} OR statuses.account_id = #{account.id}") - .where(visibility: [:direct]) + def as_direct_timeline(account, limit = 20, max_id = nil, since_id = nil, cache_ids = false) + # direct timeline is mix of direct message from_me and to_me. + # 2 querys are executed with pagination. + # constant expression using arel_table is required for partial index - apply_timeline_filters(query, account, false) + # _from_me part does not require any timeline filters + query_from_me = where(account_id: account.id) + .where(Status.arel_table[:visibility].eq(3)) + .limit(limit) + .order('statuses.id DESC') + + # _to_me part requires mute and block filter. + # FIXME: may we check mutes.hide_notifications? + query_to_me = Status + .joins(:mentions) + .merge(Mention.where(account_id: account.id)) + .where(Status.arel_table[:visibility].eq(3)) + .limit(limit) + .order('mentions.status_id DESC') + .not_excluded_by_account(account) + + if max_id.present? + query_from_me = query_from_me.where('statuses.id < ?', max_id) + query_to_me = query_to_me.where('mentions.status_id < ?', max_id) + end + + if since_id.present? + query_from_me = query_from_me.where('statuses.id > ?', since_id) + query_to_me = query_to_me.where('mentions.status_id > ?', since_id) + end + + if cache_ids + # returns array of cache_ids object that have id and updated_at + (query_from_me.cache_ids.to_a + query_to_me.cache_ids.to_a).uniq(&:id).sort_by(&:id).reverse.take(limit) + else + # returns ActiveRecord.Relation + items = (query_from_me.select(:id).to_a + query_to_me.select(:id).to_a).uniq(&:id).sort_by(&:id).reverse.take(limit) + Status.where(id: items.map(&:id)) + end end def as_public_timeline(account = nil, local_only = false) diff --git a/app/models/tag.rb b/app/models/tag.rb index 8b1b024120..4f31f796e6 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -21,6 +21,22 @@ class Tag < ApplicationRecord name end + def history + days = [] + + 7.times do |i| + day = i.days.ago.beginning_of_day.to_i + + days << { + day: day.to_s, + uses: Redis.current.get("activity:tags:#{id}:#{day}") || '0', + accounts: Redis.current.pfcount("activity:tags:#{id}:#{day}:accounts").to_s, + } + end + + days + end + class << self def search_for(term, limit = 5) pattern = sanitize_sql_like(term.strip) + '%' diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb new file mode 100644 index 0000000000..eedd92644c --- /dev/null +++ b/app/models/trending_tags.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +class TrendingTags + KEY = 'trending_tags' + HALF_LIFE = 1.day.to_i + MAX_ITEMS = 500 + EXPIRE_HISTORY_AFTER = 7.days.seconds + + class << self + def record_use!(tag, account, at_time = Time.now.utc) + return if disallowed_hashtags.include?(tag.name) || account.silenced? + + increment_vote!(tag.id, at_time) + increment_historical_use!(tag.id, at_time) + increment_unique_use!(tag.id, account.id, at_time) + end + + def get(limit) + tag_ids = redis.zrevrange(KEY, 0, limit).map(&:to_i) + tags = Tag.where(id: tag_ids).to_a.map { |tag| [tag.id, tag] }.to_h + tag_ids.map { |tag_id| tags[tag_id] }.compact + end + + private + + def increment_vote!(tag_id, at_time) + redis.zincrby(KEY, (2**((at_time.to_i - epoch) / HALF_LIFE)).to_f, tag_id.to_s) + redis.zremrangebyrank(KEY, 0, -MAX_ITEMS) if rand < (2.to_f / MAX_ITEMS) + end + + def increment_historical_use!(tag_id, at_time) + key = "activity:tags:#{tag_id}:#{at_time.beginning_of_day.to_i}" + redis.incrby(key, 1) + redis.expire(key, EXPIRE_HISTORY_AFTER) + end + + def increment_unique_use!(tag_id, account_id, at_time) + key = "activity:tags:#{tag_id}:#{at_time.beginning_of_day.to_i}:accounts" + redis.pfadd(key, account_id) + redis.expire(key, EXPIRE_HISTORY_AFTER) + end + + # The epoch needs to be 2.5 years in the future if the half-life is one day + # While dynamic, it will always be the same within one year + def epoch + @epoch ||= Date.new(Date.current.year + 2.5, 10, 1).to_datetime.to_i + end + + def disallowed_hashtags + return @disallowed_hashtags if defined?(@disallowed_hashtags) + + @disallowed_hashtags = Setting.disallowed_hashtags.nil? ? [] : Setting.disallowed_hashtags + @disallowed_hashtags = @disallowed_hashtags.split(' ') if @disallowed_hashtags.is_a? String + @disallowed_hashtags = @disallowed_hashtags.map(&:downcase) + end + + def redis + Redis.current + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 24beb77b21..ef48282fda 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -41,7 +41,7 @@ class User < ApplicationRecord include Settings::Extend include Omniauthable - ACTIVE_DURATION = 14.days + ACTIVE_DURATION = 7.days devise :two_factor_authenticatable, otp_secret_encryption_key: Rails.configuration.x.otp_secret @@ -65,6 +65,7 @@ class User < ApplicationRecord validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale? validates_with BlacklistedEmailValidator, if: :email_changed? + validates_with EmailMxValidator, if: :email_changed? scope :recent, -> { order(id: :desc) } scope :admins, -> { where(admin: true) } @@ -86,7 +87,7 @@ 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, :display_sensitive_media, + :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_sensitive_media, :hide_network, to: :settings, prefix: :setting, allow_nil: false attr_accessor :invite_code @@ -219,6 +220,10 @@ class User < ApplicationRecord settings.notification_emails['digest'] end + def hides_network? + @hides_network ||= settings.hide_network + end + def token_for_app(a) return nil if a.nil? || a.owner != self Doorkeeper::AccessToken @@ -245,7 +250,7 @@ class User < ApplicationRecord end def web_push_subscription(session) - session.web_push_subscription.nil? ? nil : session.web_push_subscription.as_payload + session.web_push_subscription.nil? ? nil : session.web_push_subscription end def invite_code=(code) diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb index 1736106f79..867bc95191 100644 --- a/app/models/web/push_subscription.rb +++ b/app/models/web/push_subscription.rb @@ -3,46 +3,65 @@ # # Table name: web_push_subscriptions # -# id :bigint(8) not null, primary key -# endpoint :string not null -# key_p256dh :string not null -# key_auth :string not null -# data :json -# created_at :datetime not null -# updated_at :datetime not null +# id :bigint(8) not null, primary key +# endpoint :string not null +# key_p256dh :string not null +# key_auth :string not null +# data :json +# created_at :datetime not null +# updated_at :datetime not null +# access_token_id :bigint(8) +# user_id :bigint(8) # -require 'webpush' - class Web::PushSubscription < ApplicationRecord + belongs_to :user, optional: true + belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', optional: true + has_one :session_activation def push(notification) - I18n.with_locale(session_activation.user.locale || I18n.default_locale) do - push_payload(message_from(notification), 48.hours.seconds) + I18n.with_locale(associated_user&.locale || I18n.default_locale) do + push_payload(payload_for_notification(notification), 48.hours.seconds) end end def pushable?(notification) - data&.key?('alerts') && data['alerts'][notification.type.to_s] + data&.key?('alerts') && ActiveModel::Type::Boolean.new.cast(data['alerts'][notification.type.to_s]) end - def as_payload - payload = { id: id, endpoint: endpoint } - payload[:alerts] = data['alerts'] if data&.key?('alerts') - payload + def associated_user + return @associated_user if defined?(@associated_user) + + @associated_user = if user_id.nil? + session_activation.user + else + user + end end - def access_token - find_or_create_access_token.token + def associated_access_token + return @associated_access_token if defined?(@associated_access_token) + + @associated_access_token = if access_token_id.nil? + find_or_create_access_token.token + else + access_token.token + end + end + + class << self + def unsubscribe_for(application_id, resource_owner) + access_token_ids = Doorkeeper::AccessToken.where(application_id: application_id, resource_owner_id: resource_owner.id, revoked_at: nil) + .pluck(:id) + + where(access_token_id: access_token_ids).delete_all + end end private def push_payload(message, ttl = 5.minutes.seconds) - # TODO: Make sure that the payload does not - # exceed 4KB - Webpush::PayloadTooLarge - Webpush.payload_send( message: Oj.dump(message), endpoint: endpoint, @@ -57,16 +76,20 @@ class Web::PushSubscription < ApplicationRecord ) end - def message_from(notification) - serializable_resource = ActiveModelSerializers::SerializableResource.new(notification, serializer: Web::NotificationSerializer, scope: self, scope_name: :current_push_subscription) - serializable_resource.as_json + def payload_for_notification(notification) + ActiveModelSerializers::SerializableResource.new( + notification, + serializer: Web::NotificationSerializer, + scope: self, + scope_name: :current_push_subscription + ).as_json end def find_or_create_access_token Doorkeeper::AccessToken.find_or_create_for( Doorkeeper::Application.find_by(superapp: true), session_activation.user_id, - Doorkeeper::OAuth::Scopes.from_string('read write follow'), + Doorkeeper::OAuth::Scopes.from_string('read write follow push'), Doorkeeper.configuration.access_token_expires_in, Doorkeeper.configuration.refresh_token_enabled? ) diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index fcf3bdf175..41c9aa44e8 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -37,7 +37,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer end def type - 'Person' + object.bot? ? 'Service' : 'Person' end def following diff --git a/app/serializers/activitypub/block_serializer.rb b/app/serializers/activitypub/block_serializer.rb index b3bd9f868e..624ce2fce8 100644 --- a/app/serializers/activitypub/block_serializer.rb +++ b/app/serializers/activitypub/block_serializer.rb @@ -5,7 +5,7 @@ class ActivityPub::BlockSerializer < ActiveModel::Serializer attribute :virtual_object, key: :object def id - [ActivityPub::TagManager.instance.uri_for(object.account), '#blocks/', object.id].join + ActivityPub::TagManager.instance.uri_for(object) || [ActivityPub::TagManager.instance.uri_for(object.account), '#blocks/', object.id].join end def type diff --git a/app/serializers/activitypub/follow_serializer.rb b/app/serializers/activitypub/follow_serializer.rb index 86c9992fe3..bb204ee8f3 100644 --- a/app/serializers/activitypub/follow_serializer.rb +++ b/app/serializers/activitypub/follow_serializer.rb @@ -5,7 +5,7 @@ class ActivityPub::FollowSerializer < ActiveModel::Serializer attribute :virtual_object, key: :object def id - [ActivityPub::TagManager.instance.uri_for(object.account), '#follows/', object.id].join + ActivityPub::TagManager.instance.uri_for(object) || [ActivityPub::TagManager.instance.uri_for(object.account), '#follows/', object.id].join end def type diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 1d17e2b0a3..4f2f4e38a1 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -2,19 +2,15 @@ class InitialStateSerializer < ActiveModel::Serializer attributes :meta, :compose, :accounts, - :media_attachments, :settings, :push_subscription, + :media_attachments, :settings, :max_toot_chars - has_many :custom_emojis, serializer: REST::CustomEmojiSerializer + has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer def max_toot_chars StatusLengthValidator::MAX_CHARS end - def custom_emojis - CustomEmoji.local.where(disabled: false) - end - def meta store = { streaming_api_base_url: Rails.configuration.x.streaming_api_base_url, diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 863238eb74..6adcd70390 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -3,11 +3,12 @@ class REST::AccountSerializer < ActiveModel::Serializer include RoutingHelper - attributes :id, :username, :acct, :display_name, :locked, :created_at, + attributes :id, :username, :acct, :display_name, :locked, :bot, :created_at, :note, :url, :avatar, :avatar_static, :header, :header_static, :followers_count, :following_count, :statuses_count has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested? + has_many :emojis, serializer: REST::CustomEmojiSerializer class FieldSerializer < ActiveModel::Serializer attributes :name, :value diff --git a/app/serializers/rest/tag_serializer.rb b/app/serializers/rest/tag_serializer.rb new file mode 100644 index 0000000000..74aa571a4c --- /dev/null +++ b/app/serializers/rest/tag_serializer.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class REST::TagSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :name, :url, :history + + def url + tag_url(object) + end +end diff --git a/app/serializers/rest/v2/search_serializer.rb b/app/serializers/rest/v2/search_serializer.rb new file mode 100644 index 0000000000..cdb6b3a530 --- /dev/null +++ b/app/serializers/rest/v2/search_serializer.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class REST::V2::SearchSerializer < ActiveModel::Serializer + has_many :accounts, serializer: REST::AccountSerializer + has_many :statuses, serializer: REST::StatusSerializer + has_many :hashtags, serializer: REST::TagSerializer +end diff --git a/app/serializers/rest/web_push_subscription_serializer.rb b/app/serializers/rest/web_push_subscription_serializer.rb new file mode 100644 index 0000000000..7fd952a567 --- /dev/null +++ b/app/serializers/rest/web_push_subscription_serializer.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class REST::WebPushSubscriptionSerializer < ActiveModel::Serializer + attributes :id, :endpoint, :alerts, :server_key + + def alerts + object.data&.dig('alerts') || {} + end + + def server_key + Rails.configuration.x.vapid_public_key + end +end diff --git a/app/serializers/web/notification_serializer.rb b/app/serializers/web/notification_serializer.rb index e5524fe7a7..43ba4d92a2 100644 --- a/app/serializers/web/notification_serializer.rb +++ b/app/serializers/web/notification_serializer.rb @@ -2,168 +2,38 @@ class Web::NotificationSerializer < ActiveModel::Serializer include RoutingHelper - include StreamEntriesHelper + include ActionView::Helpers::TextHelper + include ActionView::Helpers::SanitizeHelper - class DataSerializer < ActiveModel::Serializer - include RoutingHelper - include StreamEntriesHelper - include ActionView::Helpers::SanitizeHelper + attributes :access_token, :preferred_locale, :notification_id, + :notification_type, :icon, :title, :body - attributes :content, :nsfw, :url, :actions, - :access_token, :message, :dir - - def content - decoder.decode(strip_tags(body)) - end - - def dir - rtl?(body) ? 'rtl' : 'ltr' - end - - def nsfw - return if object.target_status.nil? - object.target_status.spoiler_text.presence - end - - def url - case object.type - when :mention - web_url("statuses/#{object.target_status.id}") - when :follow - web_url("accounts/#{object.from_account.id}") - when :favourite - web_url("statuses/#{object.target_status.id}") - when :reblog - web_url("statuses/#{object.target_status.id}") - end - end - - def actions - return @actions if defined?(@actions) - - @actions = [] - - if object.type == :mention - @actions << expand_action if collapsed? - @actions << favourite_action - @actions << reblog_action if rebloggable? - end - - @actions - end - - def access_token - return if actions.empty? - current_push_subscription.access_token - end - - def message - I18n.t('push_notifications.group.title') - end - - private - - def body - case object.type - when :mention - object.target_status.text - when :follow - object.from_account.note - when :favourite - object.target_status.text - when :reblog - object.target_status.text - end - end - - def decoder - @decoder ||= HTMLEntities.new - end - - def expand_action - { - title: I18n.t('push_notifications.mention.action_expand'), - icon: full_asset_url('web-push-icon_expand.png', skip_pipeline: true), - todo: 'expand', - action: 'expand', - } - end - - def favourite_action - { - title: I18n.t('push_notifications.mention.action_favourite'), - icon: full_asset_url('web-push-icon_favourite.png', skip_pipeline: true), - todo: 'request', - method: 'POST', - action: "/api/v1/statuses/#{object.target_status.id}/favourite", - } - end - - def reblog_action - { - title: I18n.t('push_notifications.mention.action_boost'), - icon: full_asset_url('web-push-icon_reblog.png', skip_pipeline: true), - todo: 'request', - method: 'POST', - action: "/api/v1/statuses/#{object.target_status.id}/reblog", - } - end - - def collapsed? - !object.target_status.nil? && (object.target_status.sensitive? || object.target_status.spoiler_text.present?) - end - - def rebloggable? - !object.target_status.nil? && !object.target_status.hidden? - end + def access_token + current_push_subscription.associated_access_token end - attributes :title, :image, :badge, :tag, - :timestamp, :icon - - has_one :data, serializer: DataSerializer - - def title - case object.type - when :mention - I18n.t('push_notifications.mention.title', name: name) - when :follow - I18n.t('push_notifications.follow.title', name: name) - when :favourite - I18n.t('push_notifications.favourite.title', name: name) - when :reblog - I18n.t('push_notifications.reblog.title', name: name) - end + def preferred_locale + current_push_subscription.associated_user&.locale || I18n.default_locale end - def image - return if object.target_status.nil? || object.target_status.media_attachments.empty? - full_asset_url(object.target_status.media_attachments.first.file.url(:small)) - end - - def badge - full_asset_url('badge.png', skip_pipeline: true) - end - - def tag + def notification_id object.id end - def timestamp - object.created_at + def notification_type + object.type end def icon - object.from_account.avatar_static_url + full_asset_url(object.from_account.avatar_static_url) end - def data - object + def title + I18n.t("notification_mailer.#{object.type}.subject", name: object.from_account.display_name.presence || object.from_account.username) end - private - - def name - display_name(object.from_account) + def body + str = truncate(strip_tags(object.target_status&.spoiler_text&.presence || object.target_status&.text || object.from_account.note), length: 140) + HTMLEntities.new.decode(str.to_str) # Do not encode entities, since this value will not be used in HTML end end diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index 930fbad1f1..2b447abb32 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -4,9 +4,9 @@ class ActivityPub::FetchRemoteStatusService < BaseService include JsonLdHelper # Should be called when uri has already been checked for locality - def call(uri, id: true, prefetched_body: nil) + def call(uri, id: true, prefetched_body: nil, on_behalf_of: nil) @json = if prefetched_body.nil? - fetch_resource(uri, id) + fetch_resource(uri, id, on_behalf_of) else body_to_json(prefetched_body) end @@ -34,6 +34,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService end def trustworthy_attribution?(uri, attributed_to) + return false if uri.nil? || attributed_to.nil? Addressable::URI.parse(uri).normalized_host.casecmp(Addressable::URI.parse(attributed_to).normalized_host).zero? end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index f67ebb443a..453253db40 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -23,6 +23,8 @@ class ActivityPub::ProcessAccountService < BaseService create_account if @account.nil? update_account process_tags + else + raise Mastodon::RaceConditionError end end @@ -44,7 +46,6 @@ class ActivityPub::ProcessAccountService < BaseService @account.protocol = :activitypub @account.username = @username @account.domain = @domain - @account.uri = @uri @account.suspended = true if auto_suspend? @account.silenced = true if auto_silence? @account.private_key = nil @@ -67,10 +68,12 @@ class ActivityPub::ProcessAccountService < BaseService @account.followers_url = @json['followers'] || '' @account.featured_collection_url = @json['featured'] || '' @account.url = url || @uri + @account.uri = @uri @account.display_name = @json['name'] || '' @account.note = @json['summary'] || '' @account.locked = @json['manuallyApprovesFollowers'] || false @account.fields = property_values || {} + @account.actor_type = actor_type end def set_fetchable_attributes! @@ -95,6 +98,14 @@ class ActivityPub::ProcessAccountService < BaseService ActivityPub::SynchronizeFeaturedCollectionWorker.perform_async(@account.id) end + def actor_type + if @json['type'].is_a?(Array) + @json['type'].find { |type| ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES.include?(type) } + else + @json['type'] + end + end + def image_url(key) value = first_of_value(@json[key]) diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb index eb93329e97..79cdca297b 100644 --- a/app/services/activitypub/process_collection_service.rb +++ b/app/services/activitypub/process_collection_service.rb @@ -45,5 +45,8 @@ class ActivityPub::ProcessCollectionService < BaseService def verify_account! @account = ActivityPub::LinkedDataSignature.new(@json).verify_account! + rescue JSON::LD::JsonLdError => e + Rails.logger.debug "Could not verify LD-Signature for #{value_or_id(@json['actor'])}: #{e.message}" + nil end end diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index cb65a22565..ace51a1fcc 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -81,6 +81,11 @@ class BatchedRemoveStatusService < BaseService redis.publish('timeline:public', payload) redis.publish('timeline:public:local', payload) if status.local? + if status.media_attachments.any? + redis.publish('timeline:public:media', payload) + redis.publish('timeline:public:local:media', payload) if status.local? + end + @tags[status.id].each do |hashtag| redis.publish("timeline:hashtag:#{hashtag}", payload) redis.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local? diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 510b80c823..8b36302299 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -25,6 +25,7 @@ class FanOutOnWriteService < BaseService return if status.reply? && status.in_reply_to_account_id != status.account_id deliver_to_public(status) + deliver_to_media(status) if status.media_attachments.any? end private @@ -37,7 +38,7 @@ class FanOutOnWriteService < BaseService def deliver_to_followers(status) Rails.logger.debug "Delivering status #{status.id} to followers" - status.account.followers.where(domain: nil).joins(:user).where('users.current_sign_in_at > ?', 14.days.ago).select(:id).reorder(nil).find_in_batches do |followers| + status.account.followers.where(domain: nil).joins(:user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).select(:id).reorder(nil).find_in_batches do |followers| FeedInsertWorker.push_bulk(followers) do |follower| [status.id, follower.id, :home] end @@ -47,7 +48,7 @@ class FanOutOnWriteService < BaseService def deliver_to_lists(status) Rails.logger.debug "Delivering status #{status.id} to lists" - status.account.lists.joins(account: :user).where('users.current_sign_in_at > ?', 14.days.ago).select(:id).reorder(nil).find_in_batches do |lists| + status.account.lists.joins(account: :user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).select(:id).reorder(nil).find_in_batches do |lists| FeedInsertWorker.push_bulk(lists) do |list| [status.id, list.id, :list] end @@ -85,6 +86,13 @@ class FanOutOnWriteService < BaseService Redis.current.publish('timeline:public:local', @payload) if status.local? end + def deliver_to_media(status) + Rails.logger.debug "Delivering status #{status.id} to media timeline" + + Redis.current.publish('timeline:public:media', @payload) + Redis.current.publish('timeline:public:local:media', @payload) if status.local? + end + def deliver_to_direct_timelines(status) Rails.logger.debug "Delivering status #{status.id} to direct timelines" diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 77d4aa5381..86d0f9971d 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -23,11 +23,13 @@ class FetchLinkCardService < BaseService if lock.acquired? @card = PreviewCard.find_by(url: @url) process_url if @card.nil? || @card.updated_at <= 2.weeks.ago + else + raise Mastodon::RaceConditionError end end attach_card if @card&.persisted? - rescue HTTP::Error, Addressable::URI::InvalidURIError => e + rescue HTTP::Error, Addressable::URI::InvalidURIError, Mastodon::LengthValidationError => e Rails.logger.debug "Error fetching link #{@url}: #{e}" nil end diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index ba086449cc..6490d2735b 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -9,6 +9,7 @@ class NotifyService < BaseService return if recipient.user.nil? || blocked? create_notification + push_notification if @notification.browserable? send_email if email_enabled? rescue ActiveRecord::RecordInvalid return @@ -101,25 +102,27 @@ class NotifyService < BaseService def create_notification @notification.save! - return unless @notification.browserable? + end + + def push_notification + return if @notification.activity.nil? + Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification))) send_push_notifications end def send_push_notifications - # HACK: Can be caused by quickly unfavouriting a status, since creating - # a favourite and creating a notification are not wrapped in a transaction. - return if @notification.activity.nil? + subscriptions_ids = ::Web::PushSubscription.where(user_id: @recipient.user.id) + .select { |subscription| subscription.pushable?(@notification) } + .map(&:id) - sessions_with_subscriptions = @recipient.user.session_activations.where.not(web_push_subscription: nil) - sessions_with_subscriptions_ids = sessions_with_subscriptions.select { |session| session.web_push_subscription.pushable? @notification }.map(&:id) - - WebPushNotificationWorker.push_bulk(sessions_with_subscriptions_ids) do |session_activation_id| - [session_activation_id, @notification.id] + ::Web::PushNotificationWorker.push_bulk(subscriptions_ids) do |subscription_id| + [subscription_id, @notification.id] end end def send_email + return if @notification.activity.nil? NotificationMailer.public_send(@notification.type, @recipient, @notification).deliver_later end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index fe03c044c3..b1d5bd3a72 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -22,7 +22,7 @@ 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? + text = '.' if text.blank? && media.present? ApplicationRecord.transaction do status = account.statuses.create!(text: text, @@ -31,12 +31,12 @@ class PostStatusService < BaseService sensitive: (options[:sensitive].nil? ? account.user&.setting_default_sensitive : options[:sensitive]), spoiler_text: options[:spoiler_text] || '', visibility: options[:visibility] || account.user&.setting_default_privacy, - language: LanguageDetector.instance.detect(text, account), + language: language_from_option(options[:language]) || LanguageDetector.instance.detect(text, account), application: options[:application]) end - process_mentions_service.call(status) process_hashtags_service.call(status) + process_mentions_service.call(status) LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text? DistributionWorker.perform_async(status.id) @@ -68,6 +68,10 @@ class PostStatusService < BaseService media end + def language_from_option(str) + ISO_639.find(str)&.alpha2 + end + def process_mentions_service ProcessMentionsService.new end diff --git a/app/services/process_hashtags_service.rb b/app/services/process_hashtags_service.rb index 5b45c865ff..0695922b86 100644 --- a/app/services/process_hashtags_service.rb +++ b/app/services/process_hashtags_service.rb @@ -4,8 +4,10 @@ class ProcessHashtagsService < BaseService def call(status, tags = []) tags = Extractor.extract_hashtags(status.text) if status.local? - tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |tag| - status.tags << Tag.where(name: tag).first_or_initialize(name: tag) + tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |name| + tag = Tag.where(name: name).first_or_create(name: name) + status.tags << tag + TrendingTags.record_use!(tag, status.account, status.created_at) end end end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index e164c03ab1..8c3e184442 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -20,6 +20,7 @@ class RemoveStatusService < BaseService remove_reblogs remove_from_hashtags remove_from_public + remove_from_media if status.media_attachments.any? remove_from_direct if status.direct_visibility? @status.destroy! @@ -131,6 +132,13 @@ class RemoveStatusService < BaseService Redis.current.publish('timeline:public:local', @payload) if @status.local? end + def remove_from_media + return unless @status.public_visibility? + + Redis.current.publish('timeline:public:media', @payload) + Redis.current.publish('timeline:public:local:media', @payload) if @status.local? + end + def remove_from_direct @mentions.each do |mention| Redis.current.publish("timeline:direct:#{mention.account.id}", @payload) if mention.account.local? diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb index de8d1151d3..4323e7f06d 100644 --- a/app/services/resolve_account_service.rb +++ b/app/services/resolve_account_service.rb @@ -49,6 +49,8 @@ class ResolveAccountService < BaseService else handle_ostatus end + else + raise Mastodon::RaceConditionError end end diff --git a/app/services/update_remote_profile_service.rb b/app/services/update_remote_profile_service.rb index aca1185de2..68d36addfc 100644 --- a/app/services/update_remote_profile_service.rb +++ b/app/services/update_remote_profile_service.rb @@ -41,24 +41,24 @@ class UpdateRemoteProfileService < BaseService account.header.destroy end - save_emojis(account) if remote_profile.emojis.present? + save_emojis if remote_profile.emojis.present? end end - def save_emojis(parent) - do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media? + def save_emojis + do_not_download = DomainBlock.find_by(domain: account.domain)&.reject_media? return if do_not_download - remote_account.emojis.each do |link| + remote_profile.emojis.each do |link| next unless link['href'] && link['name'] shortcode = link['name'].delete(':') - emoji = CustomEmoji.find_by(shortcode: shortcode, domain: parent.account.domain) + emoji = CustomEmoji.find_by(shortcode: shortcode, domain: account.domain) next unless emoji.nil? - emoji = CustomEmoji.new(shortcode: shortcode, domain: parent.account.domain) + emoji = CustomEmoji.new(shortcode: shortcode, domain: account.domain) emoji.image_remote_url = link['href'] emoji.save end diff --git a/app/validators/email_mx_validator.rb b/app/validators/email_mx_validator.rb new file mode 100644 index 0000000000..3cc5853c6e --- /dev/null +++ b/app/validators/email_mx_validator.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'resolv' + +class EmailMxValidator < ActiveModel::Validator + def validate(user) + return if Rails.env.test? || Rails.env.development? + user.errors.add(:email, I18n.t('users.invalid_email')) if invalid_mx?(user.email) + end + + private + + def invalid_mx?(value) + _, domain = value.split('@', 2) + + return true if domain.nil? + + records = Resolv::DNS.new.getresources(domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s } + records.empty? || on_blacklist?(records) + end + + def on_blacklist?(values) + EmailDomainBlock.where(domain: values).any? + end +end diff --git a/app/views/about/_administration.html.haml b/app/views/about/_administration.html.haml index ec5834f9cb..02286d68b7 100644 --- a/app/views/about/_administration.html.haml +++ b/app/views/about/_administration.html.haml @@ -6,7 +6,7 @@ .account__avatar{ style: "background-image: url(#{@instance_presenter.contact_account.avatar.url})" } %span.display-name %bdi - %strong.display-name__html.emojify= display_name(@instance_presenter.contact_account) + %strong.display-name__html.emojify= display_name(@instance_presenter.contact_account, custom_emojify: true) %span.display-name__account @#{@instance_presenter.contact_account.acct} - else .account__display-name diff --git a/app/views/about/_contact.html.haml b/app/views/about/_contact.html.haml index cf21ad5a3c..3215d50b53 100644 --- a/app/views/about/_contact.html.haml +++ b/app/views/about/_contact.html.haml @@ -12,7 +12,7 @@ .avatar= image_tag contact.contact_account.avatar.url .name = link_to TagManager.instance.url_for(contact.contact_account) do - %span.display_name.emojify= display_name(contact.contact_account) + %span.display_name.emojify= display_name(contact.contact_account, custom_emojify: true) %span.username @#{contact.contact_account.acct} - else .owner diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml index e264c8574d..e6d4cd10e2 100644 --- a/app/views/about/show.html.haml +++ b/app/views/about/show.html.haml @@ -141,3 +141,5 @@ %p = link_to t('about.source_code'), @instance_presenter.source_url = " (#{@instance_presenter.version_number})" + +#modal-container diff --git a/app/views/accounts/_follow_grid.html.haml b/app/views/accounts/_follow_grid.html.haml index a6d0ee8176..fdcef84be2 100644 --- a/app/views/accounts/_follow_grid.html.haml +++ b/app/views/accounts/_follow_grid.html.haml @@ -1,5 +1,6 @@ -.accounts-grid +.accounts-grid{ class: accounts.empty? ? 'empty' : '' } - if accounts.empty? + = image_tag asset_pack_path('elephant_ui_greeting.svg'), alt: '', role: 'presentational' = render partial: 'accounts/nothing_here' - else = render partial: 'accounts/grid_card', collection: accounts, as: :account, cached: !user_signed_in? diff --git a/app/views/accounts/_follow_grid_hidden.html.haml b/app/views/accounts/_follow_grid_hidden.html.haml new file mode 100644 index 0000000000..e970350e63 --- /dev/null +++ b/app/views/accounts/_follow_grid_hidden.html.haml @@ -0,0 +1,3 @@ +.accounts-grid.empty + = image_tag asset_pack_path('elephant_ui_greeting.svg'), alt: '', role: 'presentational' + %p.nothing-here= t('accounts.network_hidden') diff --git a/app/views/accounts/_grid_card.html.haml b/app/views/accounts/_grid_card.html.haml index 95acbd581e..a59ed128e6 100644 --- a/app/views/accounts/_grid_card.html.haml +++ b/app/views/accounts/_grid_card.html.haml @@ -5,7 +5,7 @@ .avatar= image_tag account.avatar.url(:original) .name = link_to TagManager.instance.url_for(account) do - %span.display_name.emojify= display_name(account) + %span.display_name.emojify= display_name(account, custom_emojify: true) %span.username @#{account.local? ? account.local_username_and_domain : account.acct} = fa_icon('lock') if account.locked? diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml index af79922c24..b5653f1614 100644 --- a/app/views/accounts/_header.html.haml +++ b/app/views/accounts/_header.html.haml @@ -6,11 +6,16 @@ .card__bio %h1.name - %span.p-name.emojify= display_name(account) + %span.p-name.emojify= display_name(account, custom_emojify: true) %small< %span>< @#{account.local_username_and_domain} = fa_icon('lock') if account.locked? - - if Setting.show_staff_badge + + - if account.bot? + .roles + .account-role.bot + = t 'accounts.roles.bot' + - elsif Setting.show_staff_badge - if account.user_admin? .roles .account-role.admin @@ -21,19 +26,19 @@ = t 'accounts.roles.moderator' .bio .account__header__content.p-note.emojify!=processed_bio[:text] + - if !account.fields.empty? - %table.account__header__fields - %tbody - - account.fields.each do |field| - %tr - %th.emojify= field.name - %td.emojify= Formatter.instance.format_field(account, field.value) + .account__header__fields + - account.fields.each do |field| + %dl + %dt.emojify{ title: field.name }= field.name + %dd.emojify{ title: field.value }= Formatter.instance.format_field(account, field.value, custom_emojify: true) - elsif processed_bio[:metadata].length > 0 - %table.account__header__fields< + .account__header__fields - processed_bio[:metadata].each do |i| - %tr - %th.emojify>!=i[0] - %td.emojify>!=i[1] + %dl + %dt.emojify{ title: i[0] }!= i[0] + %dd.emojify{ title: i[1] }!= i[1] .details-counters .counter{ class: active_nav_class(short_account_url(account)) } diff --git a/app/views/accounts/_moved_strip.html.haml b/app/views/accounts/_moved_strip.html.haml index 6a14a5dd30..ae18c6dc70 100644 --- a/app/views/accounts/_moved_strip.html.haml +++ b/app/views/accounts/_moved_strip.html.haml @@ -3,7 +3,7 @@ .moved-strip .moved-strip__message = fa_icon 'suitcase' - = t('accounts.moved_html', name: content_tag(:strong, display_name(account), class: :emojify), new_profile_link: link_to(content_tag(:strong, safe_join(['@', content_tag(:span, moved_to_account.acct)])), TagManager.instance.url_for(moved_to_account), class: 'mention')) + = t('accounts.moved_html', name: content_tag(:strong, display_name(account, custom_emojify: true), class: :emojify), new_profile_link: link_to(content_tag(:strong, safe_join(['@', content_tag(:span, moved_to_account.acct)])), TagManager.instance.url_for(moved_to_account), class: 'mention')) .moved-strip__card = link_to TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do @@ -13,5 +13,5 @@ .account__avatar-overlay-overlay{ style: "background-image: url('#{account.avatar.url(:original)}')" } %span.display-name - %strong.emojify= display_name(moved_to_account) + %strong.emojify= display_name(moved_to_account, custom_emojify: true) %span @#{moved_to_account.acct} diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index bbf2139a5e..cfdd3a9452 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -8,6 +8,7 @@ %meta{ name: 'robots', content: 'noindex' }/ %link{ rel: 'salmon', href: api_salmon_url(@account.id) }/ + %link{ rel: 'alternate', type: 'application/rss+xml', href: account_url(@account, format: 'rss') }/ %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) }/ diff --git a/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml b/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml index 6761a43192..432fb79a6e 100644 --- a/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml +++ b/app/views/admin/account_moderation_notes/_account_moderation_note.html.haml @@ -1,10 +1,7 @@ -%tr - %td +.speech-bubble + .speech-bubble__bubble = simple_format(h(account_moderation_note.content)) - %td - = account_moderation_note.account.acct - %td - %time.formatted{ datetime: account_moderation_note.created_at.iso8601, title: l(account_moderation_note.created_at) } - = l account_moderation_note.created_at - %td - = link_to t('admin.account_moderation_notes.delete'), admin_account_moderation_note_path(account_moderation_note), method: :delete if can?(:destroy, account_moderation_note) + .speech-bubble__owner + = admin_account_link_to account_moderation_note.account + %time.formatted{ datetime: account_moderation_note.created_at.iso8601 }= l account_moderation_note.created_at + = table_link_to 'trash', t('admin.account_moderation_notes.delete'), admin_account_moderation_note_path(account_moderation_note), method: :delete if can?(:destroy, account_moderation_note) diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index 7312618ee2..ed8190af5a 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -2,7 +2,7 @@ = @account.acct .table-wrapper - %table.table + %table.table.inline-table %tbody %tr %th= t('admin.accounts.username') @@ -36,13 +36,19 @@ %th= t('admin.accounts.email') %td = @account.user_email - - if @account.user_confirmed? - = fa_icon('check') = table_link_to 'edit', t('admin.accounts.change_email.label'), admin_account_change_email_path(@account.id) if can?(:change_email, @account.user) - if @account.user_unconfirmed_email.present? %th= t('admin.accounts.unconfirmed_email') %td = @account.user_unconfirmed_email + %tr + %th= t('admin.accounts.email_status') + %td + - if @account.user&.confirmed? + = t('admin.accounts.confirmed') + - else + = t('admin.accounts.confirming') + = table_link_to 'refresh', t('admin.accounts.resend_confirmation.send'), resend_admin_account_confirmation_path(@account.id), method: :post if can?(:confirm, @account.user) %tr %th= t('admin.accounts.login_status') %td @@ -73,17 +79,17 @@ %tr %th= t('admin.accounts.follows') - %td= @account.following_count + %td= number_to_human @account.following_count %tr %th= t('admin.accounts.followers') - %td= @account.followers_count + %td= number_to_human @account.followers_count %tr %th= t('admin.accounts.statuses') - %td= link_to @account.statuses_count, admin_account_statuses_path(@account.id) + %td= link_to number_to_human(@account.statuses_count), admin_account_statuses_path(@account.id) %tr %th= t('admin.accounts.media_attachments') %td - = link_to @account.media_attachments.count, admin_account_statuses_path(@account.id, { media: true }) + = link_to number_to_human(@account.media_attachments.count), admin_account_statuses_path(@account.id, { media: true }) = surround '(', ')' do = number_to_human_size @account.media_attachments.sum('file_file_size') %tr @@ -120,11 +126,12 @@ = link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:suspend, @account) - if !@account.local? && @account.hub_url.present? - %hr + %hr.spacer/ + %h3 OStatus .table-wrapper - %table.table + %table.table.inline-table %tbody %tr %th= t('admin.accounts.feed_url') @@ -148,11 +155,12 @@ = link_to t('admin.accounts.unsubscribe'), unsubscribe_admin_account_path(@account.id), method: :post, class: 'button negative' if can?(:unsubscribe, @account) - if !@account.local? && @account.inbox_url.present? - %hr + %hr.spacer/ + %h3 ActivityPub .table-wrapper - %table.table + %table.table.inline-table %tbody %tr %th= t('admin.accounts.inbox_url') @@ -167,24 +175,15 @@ %th= t('admin.accounts.followers_url') %td= link_to @account.followers_url, @account.followers_url -%hr -%h3= t('admin.accounts.moderation_notes') +%hr.spacer/ + += render @moderation_notes = simple_form_for @account_moderation_note, url: admin_account_moderation_notes_path do |f| = render 'shared/error_messages', object: @account_moderation_note - = f.input :content + = f.input :content, placeholder: t('admin.reports.notes.placeholder'), rows: 6 = f.hidden_field :target_account_id .actions - = f.button :button, t('admin.account_moderation_notes.create'), type: :submit - -.table-wrapper - %table.table - %thead - %tr - %th - %th= t('admin.account_moderation_notes.account') - %th= t('admin.account_moderation_notes.created_at') - %tbody - = render @moderation_notes + = f.button :button, t('admin.account_moderation_notes.create'), type: :submit diff --git a/app/views/admin/reports/_account.html.haml b/app/views/admin/reports/_account.html.haml index 22b7a08618..9ac161c9c6 100644 --- a/app/views/admin/reports/_account.html.haml +++ b/app/views/admin/reports/_account.html.haml @@ -15,5 +15,5 @@ .account__avatar{ style: "background-image: url(#{account.avatar.url}); width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px" } %span.display-name %bdi - %strong.display-name__html.emojify= display_name(account) + %strong.display-name__html.emojify= display_name(account, custom_emojify: true) %span.display-name__account @#{account.acct} diff --git a/app/views/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml index 137609539b..5e174f312e 100644 --- a/app/views/admin/reports/_status.html.haml +++ b/app/views/admin/reports/_status.html.haml @@ -3,26 +3,30 @@ = f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id .batch-table__row__content .status__content>< - - unless status.spoiler_text.blank? + - unless status.proper.spoiler_text.blank? %p>< - %strong= Formatter.instance.format_spoiler(status) + %strong> Content warning: #{Formatter.instance.format_spoiler(status.proper)} - = Formatter.instance.format(status) + = Formatter.instance.format(status.proper, custom_emojify: true) - - unless status.media_attachments.empty? - - if status.media_attachments.first.video? - - video = status.media_attachments.first - = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive? && !current_account&.user&.setting_display_sensitive_media, width: 610, height: 343, inline: true + - unless status.proper.media_attachments.empty? + - if status.proper.media_attachments.first.video? + - video = status.proper.media_attachments.first + = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.proper.sensitive? && !current_account&.user&.setting_display_sensitive_media, width: 610, height: 343, inline: true - else - = react_component :media_gallery, 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 } + = react_component :media_gallery, height: 343, sensitive: status.proper.sensitive? && !current_account&.user&.setting_display_sensitive_media, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } .detailed-status__meta = link_to TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener' do %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) · - = fa_visibility_icon(status) - = t("statuses.visibilities.#{status.visibility}") - - if status.sensitive? + - if status.reblog? + = fa_icon('retweet fw') + = t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(status.proper.account)) + - else + = fa_visibility_icon(status) + = t("statuses.visibilities.#{status.visibility}") + - if status.proper.sensitive? · = fa_icon('eye-slash fw') = t('stream_entries.sensitive_content') diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index 38e47e6ca3..ac2dec7ec6 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -72,7 +72,11 @@ .speech-bubble .speech-bubble__bubble= simple_format(@report.comment.presence || t('admin.reports.comment.none')) .speech-bubble__owner - = admin_account_link_to @report.account + - if @report.account.local? + = admin_account_link_to @report.account + - else + = @report.account.domain + %br/ %time.formatted{ datetime: @report.created_at.iso8601 } - unless @report.statuses.empty? diff --git a/app/views/admin/statuses/index.html.haml b/app/views/admin/statuses/index.html.haml index 9747a92cf0..704ce1dbbf 100644 --- a/app/views/admin/statuses/index.html.haml +++ b/app/views/admin/statuses/index.html.haml @@ -1,10 +1,7 @@ - content_for :page_title do = t('admin.statuses.title') - -.back-link - = link_to admin_account_path(@account.id) do - %i.fa.fa-chevron-left.fa-fw - = t('admin.statuses.back_to_account') + \- + = "@#{@account.acct}" .filters .filter-subset @@ -12,33 +9,26 @@ %ul %li= link_to t('admin.statuses.no_media'), admin_account_statuses_path(@account.id, current_params.merge(media: nil)), class: !params[:media] && 'selected' %li= link_to t('admin.statuses.with_media'), admin_account_statuses_path(@account.id, current_params.merge(media: true)), class: params[:media] && 'selected' + .back-link{ style: 'flex: 1 1 auto; text-align: right' } + = link_to admin_account_path(@account.id) do + %i.fa.fa-chevron-left.fa-fw + = t('admin.statuses.back_to_account') -- if @statuses.empty? - .accounts-grid - = render 'accounts/nothing_here' -- else - = form_for(@form, url: admin_account_statuses_path(@account.id)) do |f| - = hidden_field_tag :page, params[:page] - = hidden_field_tag :media, params[:media] - .batch-form-box - .batch-checkbox-all +%hr.spacer/ + += form_for(@form, url: admin_account_statuses_path(@account.id)) do |f| + = hidden_field_tag :page, params[:page] + = hidden_field_tag :media, params[:media] + + .batch-table + .batch-table__toolbar + %label.batch-table__toolbar__select.batch-checkbox-all = check_box_tag :batch_checkbox_all, nil, false - = f.select :action, Form::StatusBatch::ACTION_TYPE.map{|action| [t("admin.statuses.batch.#{action}"), action]} - = f.submit t('admin.statuses.execute'), data: { confirm: t('admin.reports.are_you_sure') }, class: 'button' - .media-spoiler-toggle-buttons - .media-spoiler-show-button.button= t('admin.statuses.media.show') - .media-spoiler-hide-button.button= t('admin.statuses.media.hide') - - @statuses.each do |status| - .account-status{ data: { id: status.id } } - .batch-checkbox - = f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id - .activity-stream.activity-stream-headless - .entry= render 'stream_entries/simple_status', status: status - .account-status__actions - - unless status.media_attachments.empty? - = link_to admin_account_status_path(@account.id, status, current_params.merge(status: { sensitive: !status.sensitive })), method: :patch, class: 'icon-button nsfw-button', title: t("admin.reports.nsfw.#{!status.sensitive}") do - = fa_icon status.sensitive? ? 'eye' : 'eye-slash' - = link_to admin_account_status_path(@account.id, status), method: :delete, class: 'icon-button trash-button', title: t('admin.reports.delete'), data: { confirm: t('admin.reports.are_you_sure') }, remote: true do - = fa_icon 'trash' + .batch-table__toolbar__actions + = f.button safe_join([fa_icon('eye-slash'), t('admin.statuses.batch.nsfw_on')]), name: :nsfw_on, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + = f.button safe_join([fa_icon('eye'), t('admin.statuses.batch.nsfw_off')]), name: :nsfw_off, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + = f.button safe_join([fa_icon('trash'), t('admin.statuses.batch.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + .batch-table__body + = render partial: 'admin/reports/status', collection: @statuses, locals: { f: f } = paginate @statuses diff --git a/app/views/auth/sessions/two_factor.html.haml b/app/views/auth/sessions/two_factor.html.haml index 2b07c923be..1af3193ae0 100644 --- a/app/views/auth/sessions/two_factor.html.haml +++ b/app/views/auth/sessions/two_factor.html.haml @@ -2,9 +2,12 @@ = t('auth.login') = simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| - = f.input :otp_attempt, type: :number, placeholder: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, required: true, autofocus: true, hint: t('simple_form.hints.sessions.otp') + %p.hint{ style: 'margin-bottom: 25px' }= t('simple_form.hints.sessions.otp') + + = f.input :otp_attempt, type: :number, placeholder: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, required: true, autofocus: true .actions = f.button :button, t('auth.login'), type: :submit -.form-footer= render 'auth/shared/links' + - if Setting.site_contact_email.present? + %p.hint.subtle-hint= t('users.otp_lost_help_html', email: mail_to(Setting.site_contact_email, nil)) diff --git a/app/views/authorize_follows/_card.html.haml b/app/views/authorize_follows/_card.html.haml index e81e292ba6..9abcfd37e1 100644 --- a/app/views/authorize_follows/_card.html.haml +++ b/app/views/authorize_follows/_card.html.haml @@ -6,7 +6,7 @@ %span.display-name - account_url = local_assigns[:admin] ? admin_account_path(account.id) : TagManager.instance.url_for(account) = link_to account_url, class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do - %strong.emojify= display_name(account) + %strong.emojify= display_name(account, custom_emojify: true) %span @#{account.acct} - if account.note? diff --git a/app/views/follower_accounts/index.html.haml b/app/views/follower_accounts/index.html.haml index a24e4ea20f..65af81a5b7 100644 --- a/app/views/follower_accounts/index.html.haml +++ b/app/views/follower_accounts/index.html.haml @@ -7,4 +7,7 @@ = render 'accounts/header', account: @account -= render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:account) +- if @account.user_hides_network? + = render 'accounts/follow_grid_hidden' +- else + = render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:account) diff --git a/app/views/following_accounts/index.html.haml b/app/views/following_accounts/index.html.haml index 67f6cfede4..8fd95a0b4d 100644 --- a/app/views/following_accounts/index.html.haml +++ b/app/views/following_accounts/index.html.haml @@ -7,4 +7,7 @@ = render 'accounts/header', account: @account -= render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:target_account) +- if @account.user_hides_network? + = render 'accounts/follow_grid_hidden' +- else + = render 'accounts/follow_grid', follows: @follows, accounts: @follows.map(&:target_account) diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml index 07441a77d4..8bbd184bbc 100644 --- a/app/views/layouts/public.html.haml +++ b/app/views/layouts/public.html.haml @@ -5,10 +5,10 @@ %span.single-user-login = link_to t('auth.login'), new_user_session_path — - %span.domain= link_to site_hostname, about_path + %span.footer__domain= link_to site_hostname, about_path - else - %span.domain= link_to site_hostname, root_path + %span.footer__domain= link_to site_hostname, root_path %span.powered-by - != t('generic.powered_by', link: link_to('Mastodon', 'https://joinmastodon.org')) + != t('generic.powered_by', link: link_to('https://joinmastodon.org') { image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' }) = render template: 'layouts/application' diff --git a/app/views/remote_follow/new.html.haml b/app/views/remote_follow/new.html.haml index fa48e5e627..fc5c4da20b 100644 --- a/app/views/remote_follow/new.html.haml +++ b/app/views/remote_follow/new.html.haml @@ -7,7 +7,7 @@ = simple_form_for @remote_follow, as: :remote_follow, url: account_remote_follow_path(@account) do |f| = render 'shared/error_messages', object: @remote_follow - = f.input :acct, placeholder: t('remote_follow.acct') + = f.input :acct, placeholder: t('remote_follow.acct'), input_html: { autocapitalize: 'none', autocorrect: 'off' } .actions = f.button :button, t('remote_follow.proceed'), type: :submit diff --git a/app/views/remote_unfollows/_card.html.haml b/app/views/remote_unfollows/_card.html.haml index e81e292ba6..9abcfd37e1 100644 --- a/app/views/remote_unfollows/_card.html.haml +++ b/app/views/remote_unfollows/_card.html.haml @@ -6,7 +6,7 @@ %span.display-name - account_url = local_assigns[:admin] ? admin_account_path(account.id) : TagManager.instance.url_for(account) = link_to account_url, class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do - %strong.emojify= display_name(account) + %strong.emojify= display_name(account, custom_emojify: true) %span @#{account.acct} - if account.note? diff --git a/app/views/settings/exports/show.html.haml b/app/views/settings/exports/show.html.haml index 89d768d3fc..30cd269140 100644 --- a/app/views/settings/exports/show.html.haml +++ b/app/views/settings/exports/show.html.haml @@ -10,15 +10,15 @@ %td %tr %th= t('exports.follows') - %td= @export.total_follows + %td= number_to_human @export.total_follows %td= table_link_to 'download', t('exports.csv'), settings_exports_follows_path(format: :csv) %tr %th= t('exports.blocks') - %td= @export.total_blocks + %td= number_to_human @export.total_blocks %td= table_link_to 'download', t('exports.csv'), settings_exports_blocks_path(format: :csv) %tr %th= t('exports.mutes') - %td= @export.total_mutes + %td= number_to_human @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') diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index 102e4d2007..4632034d71 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -26,6 +26,9 @@ .fields-group = f.input :setting_noindex, as: :boolean, wrapper: :with_label + .fields-group + = f.input :setting_hide_network, as: :boolean, wrapper: :with_label + %h4= t 'preferences.web' .fields-group diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index 5f63466d9e..d65a7f36f7 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -19,6 +19,9 @@ .fields-group = f.input :locked, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.locked') + .fields-group + = f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot') + .fields-group .input.with_block_label %label= t('simple_form.labels.defaults.fields') diff --git a/app/views/shared/_landing_strip.html.haml b/app/views/shared/_landing_strip.html.haml index ae26fc1fff..78f5ed4bcc 100644 --- a/app/views/shared/_landing_strip.html.haml +++ b/app/views/shared/_landing_strip.html.haml @@ -2,7 +2,7 @@ = image_tag asset_pack_path('logo.svg'), class: 'logo' %div - = t('landing_strip_html', name: content_tag(:span, display_name(account), class: :emojify), link_to_root_path: link_to(content_tag(:strong, site_hostname), root_path)) + = t('landing_strip_html', name: content_tag(:span, display_name(account, custom_emojify: true), class: :emojify), link_to_root_path: link_to(content_tag(:strong, site_hostname), root_path)) - if open_registrations? = t('landing_strip_signup_html', sign_up_path: new_user_registration_path) diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index afc66d1487..c0f1e4f0f5 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -4,7 +4,7 @@ .avatar = image_tag status.account.avatar.url(:original), width: 48, height: 48, alt: '', class: 'u-photo' %span.display-name - %strong.p-name.emojify= display_name(status.account) + %strong.p-name.emojify= display_name(status.account, custom_emojify: true) %span= acct(status.account) - if embedded_view? diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index cc2b6abe8e..990e45094c 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -10,7 +10,7 @@ %div = image_tag status.account.avatar(:original), width: 48, height: 48, alt: '', class: 'u-photo' %span.display-name - %strong.p-name.emojify= display_name(status.account) + %strong.p-name.emojify= display_name(status.account, custom_emojify: true) %span= acct(status.account) .status__content.p-name.emojify< diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml index 9764bc74da..b87ca21771 100644 --- a/app/views/stream_entries/_status.html.haml +++ b/app/views/stream_entries/_status.html.haml @@ -28,7 +28,7 @@ = fa_icon('retweet fw') %span = link_to TagManager.instance.url_for(status.account), class: 'status__display-name muted' do - %strong.emojify= display_name(status.account) + %strong.emojify= display_name(status.account, custom_emojify: true) = t('stream_entries.reblogged') - elsif pinned .pre-header diff --git a/app/views/tags/_og.html.haml b/app/views/tags/_og.html.haml index 853a499aea..a7c289bcb0 100644 --- a/app/views/tags/_og.html.haml +++ b/app/views/tags/_og.html.haml @@ -2,5 +2,5 @@ = opengraph 'og:url', tag_url(@tag) = opengraph 'og:type', 'website' = opengraph 'og:title', "##{@tag.name}" -= opengraph 'og:description', t('about.about_hashtag_html', hashtag: @tag.name) += opengraph 'og:description', strip_tags(t('about.about_hashtag_html', hashtag: @tag.name)) = opengraph 'twitter:card', 'summary' diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml index 000aa0c4de..2b46e58c78 100644 --- a/app/views/tags/show.html.haml +++ b/app/views/tags/show.html.haml @@ -2,6 +2,8 @@ = "##{@tag.name}" - content_for :header_tags do + %link{ rel: 'alternate', type: 'application/rss+xml', href: tag_url(@tag, format: 'rss') }/ + %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) = render 'og' @@ -33,3 +35,5 @@ %p= t 'about.about_mastodon_html' = render 'features' + +#modal-container diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb index adffd1d3bb..323a9f85b9 100644 --- a/app/workers/activitypub/delivery_worker.rb +++ b/app/workers/activitypub/delivery_worker.rb @@ -3,6 +3,9 @@ class ActivityPub::DeliveryWorker include Sidekiq::Worker + STOPLIGHT_FAILURE_THRESHOLD = 10 + STOPLIGHT_COOLDOWN = 60 + sidekiq_options queue: 'push', retry: 16, dead: false HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze @@ -31,15 +34,21 @@ class ActivityPub::DeliveryWorker def perform_request light = Stoplight(@inbox_url) do build_request.perform do |response| - raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) + raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) end end - light.run + light.with_threshold(STOPLIGHT_FAILURE_THRESHOLD) + .with_cool_off_time(STOPLIGHT_COOLDOWN) + .run end def response_successful?(response) - response.code > 199 && response.code < 300 + (200...300).cover?(response.code) + end + + def response_error_unsalvageable?(response) + (400...500).cover?(response.code) && response.code != 429 end def failure_tracker diff --git a/app/workers/web/push_notification_worker.rb b/app/workers/web/push_notification_worker.rb new file mode 100644 index 0000000000..4a40e5c8bd --- /dev/null +++ b/app/workers/web/push_notification_worker.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Web::PushNotificationWorker + include Sidekiq::Worker + + sidekiq_options backtrace: true + + def perform(subscription_id, notification_id) + subscription = ::Web::PushSubscription.find(subscription_id) + notification = Notification.find(notification_id) + + subscription.push(notification) unless notification.activity.nil? + rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription + subscription.destroy! + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/app/workers/web_push_notification_worker.rb b/app/workers/web_push_notification_worker.rb deleted file mode 100644 index eacea04c34..0000000000 --- a/app/workers/web_push_notification_worker.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -class WebPushNotificationWorker - include Sidekiq::Worker - - sidekiq_options backtrace: true - - def perform(session_activation_id, notification_id) - session_activation = SessionActivation.find(session_activation_id) - notification = Notification.find(notification_id) - - return if session_activation.web_push_subscription.nil? || notification.activity.nil? - - session_activation.web_push_subscription.push(notification) - rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription - # Subscription expiration is not currently implemented in any browser - - session_activation.web_push_subscription.destroy! - session_activation.update!(web_push_subscription: nil) - - true - rescue ActiveRecord::RecordNotFound - true - end -end diff --git a/bin/retry b/bin/retry new file mode 100755 index 0000000000..419ece62a5 --- /dev/null +++ b/bin/retry @@ -0,0 +1,46 @@ +#!/bin/bash +# https://github.com/travis-ci/travis-build/blob/cbe49ea239ab37b9b38b5b44d287b7ec7a108c16/lib/travis/build/templates/header.sh#L243-L260 +# +# MIT LICENSE +# +# Copyright (c) 2016 Travis CI GmbH +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +retry() { + local result=0 + local count=1 + + while [ $count -le 3 ]; do + if [ $result -ne 0 ]; then + echo -e "\n${ANSI_RED}The command \"$@\" failed. Retrying, $count of 3.${ANSI_RESET}\n" >&2 + fi + + "$@" && { result=0 && break; } || result=$? + count=$(($count + 1)) + sleep 1 + done + + if [ $count -gt 3 ]; then + echo -e "\n${ANSI_RED}The command \"$@\" failed 3 times.${ANSI_RESET}\n" >&2 + fi + + return $result +} + +retry $@ diff --git a/config/application.rb b/config/application.rb index 77da5cc2ec..fca7391e86 100644 --- a/config/application.rb +++ b/config/application.rb @@ -41,6 +41,7 @@ module Mastodon :ar, :bg, :ca, + :co, :de, :el, :eo, @@ -67,6 +68,7 @@ module Mastodon :'pt-BR', :ru, :sk, + :sl, :sr, :'sr-Latn', :sv, diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 0236fb8ccd..e9564692f9 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -62,5 +62,5 @@ ignore_unused: - 'errors.429' - 'admin.accounts.roles.*' - 'admin.action_logs.actions.*' - - 'themes.default' + - 'themes.*' - 'statuses.attached.*' diff --git a/config/initializers/active_model_serializers.rb b/config/initializers/active_model_serializers.rb index b0230267d4..0e69e1d96c 100644 --- a/config/initializers/active_model_serializers.rb +++ b/config/initializers/active_model_serializers.rb @@ -1,3 +1,5 @@ ActiveModelSerializers.config.tap do |config| config.default_includes = '**' end + +ActiveSupport::Notifications.unsubscribe(ActiveModelSerializers::Logging::RENDER_EVENT) diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 074f8c410b..4695538030 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -55,7 +55,7 @@ Doorkeeper.configure do # For more information go to # https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes default_scopes :read - optional_scopes :write, :follow + optional_scopes :write, :follow, :push # Change the way client credentials are retrieved from the request object. # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then diff --git a/config/initializers/http_client_proxy.rb b/config/initializers/http_client_proxy.rb index f5026d59e2..52c595c5d9 100644 --- a/config/initializers/http_client_proxy.rb +++ b/config/initializers/http_client_proxy.rb @@ -18,7 +18,8 @@ module Goldfinger def self.finger(uri, opts = {}) to_hidden = /\.(onion|i2p)(:\d+)?$/.match(uri) raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if !Rails.configuration.x.access_to_hidden_service && to_hidden - opts = opts.merge(Rails.configuration.x.http_client_proxy).merge(ssl: !to_hidden) + opts = { ssl: !to_hidden, headers: {} }.merge(Rails.configuration.x.http_client_proxy).merge(opts) + opts[:headers]['User-Agent'] ||= Mastodon::Version.user_agent Goldfinger::Client.new(uri, opts).finger end end diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index 17a520aa20..c134bc5b8f 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -60,6 +60,7 @@ elsif ENV['SWIFT_ENABLED'] == 'true' fog_credentials: { provider: 'OpenStack', openstack_username: ENV['SWIFT_USERNAME'], + openstack_project_id: ENV['SWIFT_PROJECT_ID'], openstack_project_name: ENV['SWIFT_TENANT'], openstack_tenant: ENV['SWIFT_TENANT'], # Some OpenStack-v2 ignores project_name but needs tenant openstack_api_key: ENV['SWIFT_PASSWORD'], diff --git a/config/locales/activerecord.co.yml b/config/locales/activerecord.co.yml new file mode 100644 index 0000000000..af28d108fd --- /dev/null +++ b/config/locales/activerecord.co.yml @@ -0,0 +1,13 @@ +--- +co: + activerecord: + errors: + models: + account: + attributes: + username: + invalid: solu lettere, numeri è liniette basse + status: + attributes: + reblog: + taken: di u statutu esista digià diff --git a/config/locales/activerecord.nl.yml b/config/locales/activerecord.nl.yml index 7972096653..eeabab34aa 100644 --- a/config/locales/activerecord.nl.yml +++ b/config/locales/activerecord.nl.yml @@ -6,7 +6,7 @@ nl: account: attributes: username: - invalid: alleen letters, nummers en laag streepje + invalid: alleen letters, nummers en underscores status: attributes: reblog: diff --git a/config/locales/activerecord.sl.yml b/config/locales/activerecord.sl.yml new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/config/locales/activerecord.sl.yml @@ -0,0 +1 @@ +{} diff --git a/config/locales/ar.yml b/config/locales/ar.yml index e9ca3038e4..e2d057b96a 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -4,6 +4,7 @@ ar: about_hashtag_html: هذه هي الرسائل العامة مع الكلمات الدلالية 1#%{hashtag}. يمكنك التفاعل معهم إذا كان لديك حساب في أي مكان على الإنترنت المتحد. about_mastodon_html: ماستدون شبكة إجتماعية حرة و مفتوحة المصدر. هو بديل لامركزي لمنصات تجارية ، يمكنك من تجنب احتكار شركة واحدة للإتصالات الخاصة بك. يمكنك اختيار أي خادم تثق فيه. أيهما تختار، يمكنك التفاعل مع أي شخص آخر على الشبكة. يمكن لأي شخص تنصيب و تشغيل خادم ماستدون خاص به والمشاركة في الشبكات الاجتماعية بكل شفافية. about_this: عن مثيل الخادوم هذا + administered_by: 'يديره :' closed_registrations: التسجيلات في مثيل الخادوم هذا مُغلقة حاليًا. contact: للتواصل معنا contact_missing: غير محدد @@ -39,6 +40,7 @@ ar: following: يتابعون media: الوسائط moved_html: "%{name} إنتقلَ إلى %{new_profile_link} :" + network_hidden: إنّ المعطيات غير متوفرة nothing_here: لا يوجد أي شيء هنا ! people_followed_by: الأشخاص الذين يتبعهم %{name} people_who_follow: الأشخاص الذين يتبعون %{name} @@ -48,21 +50,29 @@ ar: reserved_username: إسم المستخدم محجوز roles: admin: المدير + bot: روبوت moderator: مُشرِف unfollow: إلغاء المتابعة admin: account_moderation_notes: - account: مُشرِف - create: إنشاء - created_at: التاريخ + create: إترك ملاحظة created_msg: تم إنشاء ملاحظة الإشراف بنجاح ! delete: حذف destroyed_msg: تم تدمير ملاحظة الإشراف بنجاح ! accounts: are_you_sure: متأكد ؟ + avatar: الصورة الرمزية by_domain: النطاق + change_email: + changed_msg: تم تعديل عنوان البريد الإلكتروني الخاص بالحساب بنجاح ! + current_email: عنوان البريد الإلكتروني الحالي + label: تعديل عنوان البريد الإلكتروني + new_email: عنوان البريد الإلكتروني الجديد + submit: تعديل عنوان البريد الإلكتروني + title: تعديل عنوان البريد الإلكتروني الخاص بـ %{username} confirm: تأكيد confirmed: مؤكَّد + confirming: التأكد demote: إنزال الرُتبة الوظيفية disable: تعطيل disable_two_factor_authentication: تعطيل 2FA @@ -71,6 +81,7 @@ ar: domain: النطاق edit: تعديل email: البريد الإلكتروني + email_status: حالة البريد الإلكتروني enable: تفعيل enabled: مفعَّل feed_url: عنوان رابط التغذية @@ -108,6 +119,11 @@ ar: public: عمومي push_subscription_expires: انتهاء الاشتراك ”PuSH“ redownload: تحديث الصورة الرمزية + remove_avatar: حذف الصورة الرمزية + resend_confirmation: + already_confirmed: هذا المستخدم مؤكد بالفعل + send: أعد إرسال رسالة البريد الالكتروني الخاصة بالتأكيد + success: تم إرسال رسالة التأكيد بنجاح! reset: إعادة التعيين reset_password: إعادة ضبط كلمة السر resubscribe: اشترك مرة أخرى @@ -128,6 +144,7 @@ ar: statuses: المنشورات subscribe: اشترك title: الحسابات + unconfirmed_email: البريد الإلكتروني غير المؤكد undo_silenced: رفع الصمت undo_suspension: إلغاء تعليق الحساب unsubscribe: إلغاء الاشتراك @@ -135,6 +152,8 @@ ar: web: الويب action_logs: actions: + assigned_to_self_report: قام {name} بتعيين التقرير٪ {target} لأنفسهم + change_email_user: غيّر٪ {name} عنوان البريد الإلكتروني للمستخدم٪ {target} confirm_user: "%{name} قد قام بتأكيد عنوان البريد الإلكتروني لـ %{target}" create_custom_emoji: "%{name} قام برفع إيموجي جديد %{target}" create_domain_block: "%{name} قام بحجب نطاق %{target}" @@ -150,10 +169,13 @@ ar: enable_user: "%{name} لقد قام بتنشيط تسجيل الدخول للمستخدِم %{target}" memorialize_account: لقد قام %{name} بتحويل حساب %{target} إلى صفحة تذكارية promote_user: "%{name} قام بترقية المستخدم %{target}" + remove_avatar_user: تمت إزالة٪ {name} الصورة الرمزية٪ {target} + reopen_report: تمت إعادة فتح التقرير {name}٪ {target} reset_password_user: "%{name} لقد قام بإعادة تعيين الكلمة السرية الخاصة بـ %{target}" resolve_report: قام %{name} بحل التقرير %{target} silence_account: لقد قام %{name} بكتم حساب %{target} suspend_account: لقد قام %{name} بتعليق حساب %{target} + unassigned_report: "٪ {name} تقرير غير معتمد٪ {target}" unsilence_account: لقد قام %{name} بإلغاء الكتم عن حساب %{target} unsuspend_account: لقد قام %{name} بإلغاء التعليق المفروض على حساب %{target} update_custom_emoji: "%{name} قام بتحديث الإيموجي %{target}" @@ -191,12 +213,15 @@ ar: domain: النطاق new: create: إنشاء حظر + hint: لن تمنع كتلة المجال إنشاء إدخالات حساب في قاعدة البيانات ، ولكنها ستطبق طرق الإشراف المحددة بأثر رجعي وتلقائي على هذه الحسابات. severity: + desc_html: "Silence سيجعل مشاركات الحساب غير مرئية لأي شخص لا يتبعها. Suspend سيزيل كل محتوى الحساب ووسائطه وبيانات ملفه الشخصي. Use None إذا كنت تريد فقط رفض ملفات الوسائط." noop: لا شيء silence: كتم suspend: تعليق title: حجب نطاق جديد reject_media: رفض ملفات الوسائط + reject_media_hint: يزيل ملفات الوسائط المخزنة محليًا ويرفض تنزيل أي ملفات في المستقبل. غير ذي صلة للتعليق severities: noop: لا شيء silence: إخفاء أو كتم @@ -236,39 +261,59 @@ ar: expired: المنتهي صلاحيتها title: التصفية title: الدعوات + report_notes: + created_msg: |- + 41/5000 + تم إنشاء ملاحظة التقرير بنجاح! + destroyed_msg: تم حذف ملاحظة التقرير بنجاح! reports: + account: + note: ملحوظة + report: تقرير action_taken_by: تم اتخاذ الإجراء مِن طرف are_you_sure: هل أنت متأكد ؟ + assign_to_self: عين لي + assigned: تعين رئيس comment: none: لا شيء - delete: حذف + created_at: ذكرت id: معرّف ID mark_as_resolved: إعتبار التقرير كمحلول - nsfw: - 'false': الكشف عن الصور - 'true': إخفاء الوسائط المرفقة + mark_as_unresolved: علام كغير محلولة + notes: + create: اضف ملاحظة + create_and_resolve: الحل مع ملاحظة + create_and_unresolve: إعادة فتح مع ملاحظة + delete: حذف + placeholder: قم بوصف الإجراءات التي تم اتخاذها أو أي تحديثات أخرى ذات علاقة … + reopen: إعادة فتح التقرير report: 'التقرير #%{id}' report_contents: المحتويات reported_account: حساب مُبلّغ عنه reported_by: أبلغ عنه من طرف resolved: معالجة + resolved_msg: تم حل تقرير بنجاح! silence_account: كتم و إخفاء الحساب status: الحالة suspend_account: فرض تعليق على الحساب target: الهدف title: التقارير + unassign: إلغاء تعيين unresolved: غير معالجة + updated_at: محدث view: عرض settings: activity_api_enabled: desc_html: عدد المنشورات المحلية و المستخدمين النشطين و التسجيلات الأسبوعية الجديدة title: نشر مُجمل الإحصائيات عن نشاط المستخدمين bootstrap_timeline_accounts: + desc_html: افصل بين أسماء المستخدمين المتعددة بواسطة الفاصلة. استعمل الحسابات المحلية والمفتوحة فقط. الافتراضي عندما تكون فارغة كل المسؤولين المحليين. title: الإشتراكات الإفتراضية للمستخدمين الجدد contact_information: email: البريد الإلكتروني المهني username: الإتصال بالمستخدِم hero: + desc_html: معروض على الصفحة الأولى. لا يقل عن 600 × 100 بكسل. عند عدم التعيين ، تعود الصورة إلى النسخة المصغرة على سبيل المثال title: الصورة الرأسية peers_api_enabled: desc_html: أسماء النطاقات التي إلتقى بها مثيل الخادوم على البيئة الموحَّدة فيديفرس @@ -287,15 +332,23 @@ ar: desc_html: السماح للجميع بإنشاء حساب title: فتح التسجيل show_known_fediverse_at_about_page: + desc_html: عند التثبت ، سوف تظهر toots من جميع fediverse المعروفة على عرض مسبق. وإلا فإنه سيعرض فقط toots المحلية. title: إظهار الفيديفرس الموحَّد في خيط المُعايَنة + show_staff_badge: + desc_html: عرض شارة الموظفين على صفحة المستخدم + title: إظهار شارة الموظفين site_description: + desc_html: فقرة تمهيدية على الصفحة الأولى وفي العلامات الوصفية. يمكنك استخدام علامات HTML ، ولا سيما <a> و <em>. title: وصف مثيل الخادوم site_description_extended: + desc_html: مكان جيد لمدونة قواعد السلوك والقواعد والإرشادات وغيرها من الأمور التي تحدد حالتك. يمكنك استخدام علامات HTML title: الوصف المُفصّل للموقع site_terms: + desc_html: يمكنك كتابة سياسة الخصوصية الخاصة بك ، شروط الخدمة أو غيرها من القوانين. يمكنك استخدام علامات HTML title: شروط الخدمة المخصصة site_title: إسم مثيل الخادم thumbnail: + desc_html: يستخدم للعروض السابقة عبر Open Graph و API. 1200x630px موصى به title: الصورة الرمزية المصغرة لمثيل الخادوم timeline_preview: desc_html: عرض الخيط العمومي على صفحة الإستقبال @@ -305,15 +358,16 @@ ar: back_to_account: العودة إلى صفحة الحساب batch: delete: حذف - execute: تفعيل + nsfw_off: ضع علامة انها غير حساسة + nsfw_on: ضع علامة انها حساسة failed_to_execute: خطأ في التفعيل media: - hide: إخفاء الوسائط - show: إظهار الوسائط title: الوسائط + no_media: لا يوجد وسائط title: منشورات الحساب with_media: بالوسائط subscriptions: + callback_url: عاود الاتصال بالعنوان confirmed: مؤكَّد expires_in: تنتهي مدة صلاحيتها في last_delivery: آخر إيداع @@ -323,6 +377,8 @@ ar: admin_mailer: new_report: body: قام %{reporter} بالإبلاغ عن %{target} + body_remote: أبلغ شخص ما من٪ {domain} عن٪ {target} + subject: تقرير جديد ل%{instance} (#%{id}) application_mailer: notification_preferences: تعديل خيارات البريد الإلكتروني salutation: "%{name}،" @@ -335,6 +391,7 @@ ar: destroyed: تم حذف التطبيق بنجاح invalid_url: إن الرابط المقدم غير صالح regenerate_token: إعادة توليد رمز النفاذ + token_regenerated: تم إعادة إنشاء الرمز الوصول بنجاح warning: كن حذرا مع هذه البيانات. لا تقم أبدا بمشاركتها مع الآخَرين ! your_token: رمز نفاذك auth: @@ -345,6 +402,7 @@ ar: delete_account_html: إن كنت ترغب في حذف حسابك يُمكنك المواصلة هنا. سوف يُطلَبُ منك التأكيد قبل الحذف. didnt_get_confirmation: لم تتلق تعليمات التأكيد ؟ forgot_password: نسيت كلمة المرور ؟ + invalid_reset_password_token: رمز إعادة تعيين كلمة المرور غير صالح أو منتهي الصلاحية. يرجى طلب واحد جديد. login: تسجيل الدخول logout: خروج migrate_account: الإنتقال إلى حساب آخر @@ -358,9 +416,10 @@ ar: register_elsewhere: التسجيل على خادوم آخَر resend_confirmation: إعادة إرسال تعليمات التأكيد reset_password: إعادة تعيين كلمة المرور - security: الهوية + security: الأمان set_new_password: إدخال كلمة مرور جديدة authorize_follow: + already_following: أنت تتابع بالفعل هذا الحساب error: يا للأسف، وقع هناك خطأ إثر عملية البحث عن الحساب عن بعد follow: إتبع follow_request: 'لقد قمت بإرسال طلب متابعة إلى :' @@ -403,7 +462,7 @@ ar: archive_takeout: date: التاريخ download: تنزيل نسخة لحسابك - hint_html: بإمكانك طلب نسخة كاملة لـ كافة تبويقاتك و الوسائط التي قمت بنشرها. البيانات المُصدَّرة ستكون محفوظة على شكل نسق ActivityPub و باستطاعتك قراءتها بأي برنامج يدعم هذا النسق. + hint_html: بإمكانك طلب نسخة كاملة لـ كافة تبويقاتك و الوسائط التي قمت بنشرها. البيانات المُصدَّرة ستكون محفوظة على شكل نسق ActivityPub و باستطاعتك قراءتها بأي برنامج يدعم هذا النسق. يُمكنك طلب نسخة كل 7 أيام. in_progress: عملية جمع نسخة لبيانات حسابك جارية … request: طلب نسخة لحسابك size: الحجم @@ -519,24 +578,14 @@ ar: other: إعدادات أخرى publishing: النشر web: الويب - push_notifications: - favourite: - title: أعجب %{name} بمنشورك - follow: - title: "%{name} من متتبعيك الآن" - group: - title: "%{count} إخطارات" - mention: - action_boost: ترقية - action_expand: عرض المزيد - title: أشار إليك %{name} - reblog: - title: قام %{name} بترقية منشورك remote_follow: acct: قم بإدخال عنوان حسابك username@domain الذي من خلاله تود المتابعة missing_resource: تعذر العثور على رابط التحويل المطلوب الخاص بحسابك proceed: أكمل المتابعة prompt: 'إنك بصدد متابعة :' + remote_unfollow: + error: '' + title: '' sessions: activity: آخر نشاط browser: المتصفح @@ -574,11 +623,13 @@ ar: windows: ويندوز windows_mobile: ويندوز موبايل windows_phone: ويندوز فون + revoke: '' revoke_success: تم إبطال الجلسة بنجاح title: الجلسات settings: authorized_apps: التطبيقات المرخص لها back: عودة إلى ماستدون + delete: '' development: التطوير edit_profile: تعديل الملف الشخصي export: تصدير البيانات @@ -588,12 +639,21 @@ ar: notifications: الإخطارات preferences: التفضيلات settings: الإعدادات - two_factor_authentication: إثبات الهويّة المزدوج + two_factor_authentication: المُصادقة بخُطوَتَيْن your_apps: تطبيقاتك statuses: + attached: + image: + one: '' + other: '' + video: + one: '' + other: '' + content_warning: '' open_in_web: إفتح في الويب over_character_limit: تم تجاوز حد الـ %{max} حرف المسموح بها pin_errors: + limit: '' ownership: لا يمكن تدبيس تبويق نشره شخص آخر private: لا يمكن تثبيت تبويق لم يُنشر للعامة reblog: لا يمكن تثبيت ترقية @@ -620,11 +680,11 @@ ar: default: "%b %d, %Y, %H:%M" two_factor_authentication: code_hint: قم بإدخال الرمز المُوَلّد عبر تطبيق المصادقة للتأكيد - description_html: في حال تفعيل المصادقة بخطوتين ، فتسجيل الدخول يتتطلب منك أن يكون بحوزتك هاتفك النقال قصد توليد الرمز الذي سيتم إدخاله. + description_html: في حال تفعيل المصادقة بخطوتين ، فتسجيل الدخول يتطلب منك أن يكون بحوزتك هاتفك النقال قصد توليد الرمز الذي سيتم إدخاله. disable: تعطيل enable: تفعيل enabled: نظام المصادقة بخطوتين مُفعَّل - enabled_success: تم تفعيل إثبات الهوية المزدوج بنجاح + enabled_success: تم تفعيل المصادقة بخطوتين بنجاح generate_recovery_codes: توليد رموز الإسترجاع instructions_html: "قم بمسح رمز الكيو آر عبر Google Authenticator أو أي تطبيق TOTP على جهازك. من الآن فصاعدا سوف يقوم ذاك التطبيق بتوليد رموز يجب عليك إدخالها عند تسجيل الدخول." manual_instructions: 'في حالة تعذّر مسح رمز الكيو آر أو طُلب منك إدخال يدوي، يُمْكِنك إدخال هذا النص السري على التطبيق :' @@ -634,9 +694,19 @@ ar: wrong_code: الرمز الذي أدخلته غير صالح ! تحقق من صحة الوقت على الخادم و الجهاز ؟ user_mailer: backup_ready: + explanation: '' subject: نسخة بيانات حسابك جاهزة للتنزيل title: المغادرة بأرشيف الحساب + welcome: + edit_profile_action: '' + explanation: '' + full_handle: '' + review_preferences_action: '' + subject: '' + tips: نصائح + title: أهلاً بك، %{name} ! users: invalid_email: عنوان البريد الإلكتروني غير صالح - invalid_otp_token: الرمز الثنائي غير صالح + invalid_otp_token: رمز المصادقة بخطوتين غير صالح seamless_external_login: لقد قمت بتسجيل الدخول عبر خدمة خارجية، إنّ إعدادات الكلمة السرية و البريد الإلكتروني غير متوفرة. + signed_in_as: 'تم تسجيل دخولك بصفة :' diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 063003218e..b05398d22e 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -40,6 +40,7 @@ ca: following: Seguint media: Mèdia moved_html: "%{name} s'ha mogut a %{new_profile_link}:" + network_hidden: Aquesta informació no està disponible nothing_here: No hi ha res aquí! people_followed_by: Usuaris seguits per %{name} people_who_follow: Usuaris que segueixen %{name} @@ -49,13 +50,12 @@ ca: reserved_username: El nom d'usuari està reservat roles: admin: Administrador + bot: Bot moderator: Moderador unfollow: Deixa de seguir admin: account_moderation_notes: - account: Moderador - create: Crea - created_at: Data + create: Crea nota created_msg: La nota de moderació s'ha creat correctament! delete: Suprimeix destroyed_msg: Nota de moderació destruïda amb èxit! @@ -72,6 +72,7 @@ ca: title: Canviar adreça de correu de %{username} confirm: Confirma confirmed: Confirmat + confirming: Confirmando demote: Degrada disable: Inhabilita disable_two_factor_authentication: Desactiva 2FA @@ -80,6 +81,7 @@ ca: domain: Domini edit: Edita email: Correu electrònic + email_status: Estado del correo electrónico enable: Habilita enabled: Habilitat feed_url: URL del canal @@ -118,6 +120,10 @@ ca: push_subscription_expires: La subscripció PuSH expira redownload: Actualitza l'avatar remove_avatar: Eliminar avatar + resend_confirmation: + already_confirmed: Este usuario ya está confirmado + send: Reenviar el correo electrónico de confirmación + success: "¡Correo electrónico de confirmación enviado con éxito!" reset: Reinicialitza reset_password: Restableix la contrasenya resubscribe: Torna a subscriure @@ -269,7 +275,6 @@ ca: comment: none: Cap created_at: Reportat - delete: Suprimeix id: ID mark_as_resolved: Marca com a resolt mark_as_unresolved: Marcar sense resoldre @@ -278,10 +283,7 @@ ca: create_and_resolve: Resoldre amb nota create_and_unresolve: Reobrir amb nota delete: Esborrar - placeholder: Descriu les accions que s'han pres o qualsevol altra actualització d'aquest informe… - nsfw: - 'false': Mostra els fitxers multimèdia adjunts - 'true': Amaga els fitxers multimèdia adjunts + placeholder: Descriu les accions que s'han pres o qualsevol altra actualització relacionada… reopen: Reobrir informe report: 'Informe #%{id}' report_contents: Contingut @@ -356,11 +358,8 @@ ca: delete: Suprimeix nsfw_off: Marcar com a no sensible nsfw_on: Marcar com a sensible - execute: Executa failed_to_execute: No s'ha pogut executar media: - hide: Amaga el contingut multimèdia - show: Mostra el contingut multimèdia title: Contingut multimèdia no_media: Sense contingut multimèdia title: Estats del compte @@ -376,6 +375,7 @@ ca: admin_mailer: new_report: body: "%{reporter} ha informat de %{target}" + body_remote: Algú des de el domini %{domain} ha informat sobre %{target} subject: Informe nou per a %{instance} (#%{id}) application_mailer: notification_preferences: Canvia les preferències de correu @@ -465,7 +465,7 @@ ca: archive_takeout: date: Data download: Descarrega l’arxiu - hint_html: Pots sol·licitar un arxiu dels teus toots i els fitxers multimèdia pujats. Les dades exportades tindran el format ActivityPub, llegible per qualsevol programari compatible. + hint_html: Pots sol·licitar un arxiu dels teus toots i els fitxers multimèdia pujats. Les dades exportades tindran el format ActivityPub, llegible per qualsevol programari compatible. Pots sol·licitar un arxiu cada 7 dies. in_progress: Compilant el teu arxiu... request: Sol·licita el teu arxiu size: Tamany @@ -595,20 +595,6 @@ ca: other: Altre publishing: Publicació web: Web - push_notifications: - favourite: - title: "%{name} ha marcat com a preferit el teu estat" - follow: - title: "%{name} ara et segueix" - group: - title: "%{count} notificacions" - mention: - action_boost: Retooteja - action_expand: Mostra'n més - action_favourite: Preferit - title: "%{name} t'ha mencionat" - reblog: - title: "%{name} t'ha retootejat" remote_follow: acct: Escriu el teu usuari@domini des del qual vols seguir missing_resource: No s'ha pogut trobar la URL de redirecció necessaria per al compte @@ -633,7 +619,7 @@ ca: micro_messenger: MicroMessenger nokia: Nokia S40 Ovi Browser opera: Opera - otter: Altre + otter: Otter phantom_js: PhantomJS qq: QQ Browser safari: Safari @@ -683,6 +669,7 @@ ca: video: one: "%{count} vídeo" other: "%{count} vídeos" + boosted_from_html: Impulsat des de %{acct_link} content_warning: 'Avís de contingut: %{warning}' disallowed_hashtags: one: 'conté una etiqueta no permesa: %{tags}' @@ -832,5 +819,6 @@ ca: users: invalid_email: L'adreça de correu no és correcta invalid_otp_token: El codi de dos factors no és correcte + otp_lost_help_html: Si has perdut l'accés a tots dos pots contactar per %{email} seamless_external_login: Has iniciat sessió via un servei extern per tant els ajustos de contrasenya i correu electrònic no estan disponibles. signed_in_as: 'Sessió iniciada com a:' diff --git a/config/locales/co.yml b/config/locales/co.yml new file mode 100644 index 0000000000..32661b2c5c --- /dev/null +++ b/config/locales/co.yml @@ -0,0 +1,824 @@ +--- +co: + about: + about_hashtag_html: Quessi sò statuti pubblichi taggati cù #%{hashtag}. Pudete interagisce cù elli sì voi avete un contu in qualche parte di u fediverse. + about_mastodon_html: Mastodon ghjè una rete suciale custruita incù prutucolli web aperti è lugiziali liberi. Hè decentralizatu cumu l’e-mail. + about_this: À prupositu + administered_by: 'Amministratu da:' + closed_registrations: Pè avà, l’arregistramenti sò chjosi nant’à st’istanza. Mà pudete truvà un’altr’istanza per fà un contu è avè accessu à listessa reta da quallà. + contact: Cuntattu + contact_missing: Mancante + contact_unavailable: Micca dispunibule + description_headline: Quale hè %{domain} ? + domain_count_after: altre istanze + domain_count_before: Cunnettati à + extended_description_html: | + Una bona piazza per e regule + A descrizzione stesa ùn hè micca stata riempiuta. + features: + humane_approach_body: Mastodon hà amparatu da i sbagli di l’altre rete suciale, è prova à fà scelte di cuncezzione più etiche per luttà contr’à l’abusu di i media suciali. + humane_approach_title: Una mentalità più umana + not_a_product_body: Mastodon ùn hè micca una rete cummerciale. Micca pubblicità, micca pruspizzione di dati, micca ambienti chjosi, è micca auturità centrale. + not_a_product_title: Site una parsona, micca un pruduttu + real_conversation_body: Cù 500 caratteri dispunibuli, diffusione persunalizata di u cuntinutu è avertimenti per media sensibili, pudete cumunicà cum’è voi vulete. + real_conversation_title: Fattu per una vera cunversazione + within_reach_body: Parechje app per iOS, Android è altre piattaforme, create cù un sistemu d’API accessibile à i prugrammatori, vi permettenu d’avè accessu à i vostri amichi senza prublemi. + within_reach_title: Sempre accessibile + generic_description: "%{domain} hè un servore di a rete" + hosted_on: Mastodon allughjatu nant’à %{domain} + learn_more: Amparà di più + other_instances: Lista di l’istanze + source_code: Codice di fonte + status_count_after: statuti + status_count_before: chì anu pubblicatu + user_count_after: parsone quì + user_count_before: Ci sò + what_is_mastodon: Quale hè Mastodon? + accounts: + follow: Siguità + followers: Abbunati + following: Abbunamenti + media: Media + moved_html: "%{name} hà cambiatu di contu, avà hè nant’à %{new_profile_link}:" + nothing_here: Ùn c’hè nunda quì! + people_followed_by: Seguitati da %{name} + people_who_follow: Seguitanu %{name} + posts: Statuti + posts_with_replies: Statuti è risposte + remote_follow: Siguità d’altrò + reserved_username: Stu cugnome hè riservatu + roles: + admin: Amministratore + moderator: Muderatore + unfollow: Ùn siguità più + admin: + account_moderation_notes: + create: Creà + created_msg: Nota di muderazione creata! + delete: Toglie + destroyed_msg: Nota di muderazione sguassata! + accounts: + are_you_sure: Site sicuru·a? + avatar: Ritrattu di prufile + by_domain: Duminiu + change_email: + changed_msg: Email di u contu cambiatu! + current_email: Email attuale + label: Mudificà l’Email + new_email: Novu Email + submit: Cambià Email + title: Mudificà l’Email di %{username} + confirm: Cunfirmà + confirmed: Cunfirmata + confirming: Cunfirmazione + demote: Ritrugradà + disable: Disattivà + disable_two_factor_authentication: Disattivà l’identificazione à 2 fattori + disabled: Disattivatu + display_name: Nome pubblicu + domain: Duminiu + edit: Mudificà + email: E-mail + email_status: Statu di l’e-mail + enable: Attivà + enabled: Attivatu + feed_url: URL di u flussu + followers: Abbunati + followers_url: URL di l’abbunati + follows: Abbunamenti + inbox_url: URL di l’inbox + ip: IP + location: + all: Tutti + local: Lucale + remote: D’altrò + title: Lucalizazione + login_status: Statutu di cunnessione + media_attachments: Media aghjunti + memorialize: Trasfurmà in mimuriale + moderation: + all: Tutti + silenced: Silenzati + suspended: Suspesi + title: Muderazione + moderation_notes: Note di muderazione + most_recent_activity: Attività più ricente + most_recent_ip: IP più ricente + not_subscribed: Micca abbunatu + order: + alphabetic: Alfabeticu + most_recent: Più ricente + title: Urdine + outbox_url: URL di l’outbox + perform_full_suspension: Fà una suspensione cumpleta + profile_url: URL di u prufile + promote: Prumove + protocol: Prutucollu + public: Pubblicu + push_subscription_expires: Spirata di l’abbunamentu PuSH + redownload: Mette à ghjornu i ritratti + remove_avatar: Toglie l’avatar + resend_confirmation: + already_confirmed: St’utilizatore hè digià cunfirmatu + send: Rimandà un’e-mail di cunfirmazione + success: L’e-mail di cunfirmazione hè statu mandatu! + reset: Reset + reset_password: Riinizializà a chjave d’accessu + resubscribe: Riabbunassi + role: Auturizazione + roles: + admin: Amministratore + moderator: Muderatore + staff: Squadra + user: Utilizatore + salmon_url: URL di Salmon + search: Cercà + shared_inbox_url: URL di l’inbox spartuta + show: + created_reports: Signalamenti creati da stu contu + report: Signalamentu + targeted_reports: Signalamenti creati contr’à stu contu + silence: Silenzà + statuses: Statuti + subscribe: Abbunassi + title: Conti + unconfirmed_email: E-mail micca cunfirmatu + undo_silenced: Ùn silenzà più + undo_suspension: Ùn suspende più + unsubscribe: Disabbunassi + username: Cugnome + web: Web + action_logs: + actions: + assigned_to_self_report: "%{name} s’hè assignatu u signalamentu %{target}" + change_email_user: "%{name} hà cambiatu l’indirizzu e-mail di %{target}" + confirm_user: "%{name} hà cunfirmatu l’indirizzu e-mail di %{target}" + create_custom_emoji: "%{name} hà caricatu una nov’emoji %{target}" + create_domain_block: "%{name} hà bluccatu u duminiu %{target}" + create_email_domain_block: "%{name} hà messu u duminiu e-mail %{target} nant’a lista nera" + demote_user: "%{name} hà ritrugradatu l’utilizatore %{target}" + destroy_domain_block: "%{name} hà sbluccatu u duminiu %{target}" + destroy_email_domain_block: "%{name} hà messu u duminiu e-mail %{target} nant’a lista bianca" + destroy_status: "%{name} hà toltu u statutu di %{target}" + disable_2fa_user: "%{name} hà disattivatu l’identificazione à dui fattori per %{target}" + disable_custom_emoji: "%{name} hà disattivatu l’emoji %{target}" + disable_user: "%{name} hà disattivatu a cunnessione per %{target}" + enable_custom_emoji: "%{name} hà attivatu l’emoji %{target}" + enable_user: "%{name} hà attivatu a cunnessione per %{target}" + memorialize_account: "%{name} hà trasfurmatu u contu di %{target} in una pagina mimuriale" + promote_user: "%{name} hà prumossu %{target}" + remove_avatar_user: "%{name} hà toltu u ritrattu di %{target}" + reopen_report: "%{name} hà riapertu u signalamentu %{target}" + reset_password_user: "%{name} hà riinizializatu a chjave d’accessu di %{target}" + resolve_report: "%{name} hà chjosu u signalamentu %{target}" + silence_account: "%{name} hà silenzatu u contu di %{target}" + suspend_account: "%{name} hà suspesu u contu di %{target}" + unassigned_report: "%{name} hà disassignatu u signalamentu %{target}" + unsilence_account: "%{name} hà fattu che u contu di %{target} ùn hè più silenzatu" + unsuspend_account: "%{name} hà fattu che u contu di %{target} ùn hè più suspesu" + update_custom_emoji: "%{name} hà messu à ghjornu l’emoji %{target}" + update_status: "%{name} hà cambiatu u statutu di %{target}" + title: Ghjurnale d’audit + custom_emojis: + by_domain: Duminiu + copied_msg: Copia lucale di l’emoji creata + copy: Cupià + copy_failed_msg: Ùn s’hè micca pussutu creà una copia di l’emoji + created_msg: L’emoji hè stata creata! + delete: Toglie + destroyed_msg: L’emoji hè stata tolta! + disable: Disattivà + disabled_msg: L’emoji hè stata disattivata + emoji: Emoji + enable: Attivà + enabled_msg: L’emoji hè stata attivata + image_hint: PNG di 50Ko o menu + listed: Listata + new: + title: Aghjustà una nov’emoji + overwrite: Soprascrive + shortcode: Accorta + shortcode_hint: 2 caratteri o più, solu lettere, numeri è liniette basse + title: Emoji parsunalizate + unlisted: Micca listata + update_failed_msg: Ùn s’hè micca pussutu mette à ghjornu l’emoji + updated_msg: L’emoji hè stata messa à ghjornu! + upload: Caricà + domain_blocks: + add_new: Aghjustà + created_msg: U blucchime di u duminiu hè attivu + destroyed_msg: U blucchime di u duminiu ùn hè più attivu + domain: Duminiu + new: + create: Creà un blucchime + hint: U blucchime di duminiu ùn impedirà micca a creazione di conti indè a database, mà metudi di muderazione specifiche saranu applicati. + severity: + desc_html: CùSilenzà, solu l’abbunati di u contu viderenu i so missaghji. Suspende sguassarà tutti i cuntenuti è dati di u contu. Utilizate Nisuna s’è voi vulete solu righjittà fugliali media. + noop: Nisuna + silence: Silenzà + suspend: Suspende + title: Novu blucchime di duminiu + reject_media: Righjittà i fugliali media + reject_media_hint: Sguassa tutti i media caricati è ricusa caricamenti futuri. Inutile per una suspensione + severities: + noop: Nisuna + silence: Silenzà + suspend: Suspende + severity: Severità + show: + affected_accounts: + one: Un contu tuccatu indè a database + other: "%{count} conti tuccati indè a database" + retroactive: + silence: Ùn silenzà più i conti nant’à stu duminiu + suspend: Ùn suspende più i conti nant’à stu duminiu + title: Ùn bluccà più u duminiu %{domain} + undo: Annullà + title: Blucchimi di duminiu + undo: Annullà + email_domain_blocks: + add_new: Aghjustà + created_msg: U blucchime di u duminiu d’e-mail hè attivu + delete: Toglie + destroyed_msg: U blucchime di u duminiu d’e-mail ùn hè più attivu + domain: Duminiu + new: + create: Creà un blucchime + title: Nova iscrizzione nant’a lista nera e-mail + title: Lista nera e-mail + instances: + account_count: Conti cunnisciuti + domain_name: Duminiu + reset: Riinizializà + search: Cercà + title: Istanze cunnisciute + invites: + filter: + all: Tuttu + available: Dispunibuli + expired: Spirati + title: Filtrà + title: Invitazione + report_notes: + created_msg: Nota di signalamentu creata! + destroyed_msg: Nota di signalamentu sguassata! + reports: + account: + note: nota + report: palisà + action_taken_by: Intervenzione di + are_you_sure: Site sicuru·a? + assign_to_self: Assignallu à mè + assigned: Muderatore assignatu + comment: + none: Nisunu + created_at: Palisatu + id: ID + mark_as_resolved: Indicà cum’è chjosu + mark_as_unresolved: Indicà cum’è sempre apertu + notes: + create: Aghjunghje una nota + create_and_resolve: Chjude cù una nota + create_and_unresolve: Riapre cù una nota + delete: Toglie + placeholder: Per parlà di l’azzione piglate, o altre messe à ghjornu nant’à u signalamentu… + reopen: Riapre u signalamentu + report: 'Signalamente #%{id}' + report_contents: Cuntenuti + reported_account: Contu palisatu + reported_by: Palisatu da + resolved: Scioltu è chjosu + resolved_msg: Signalamentu scioltu! + silence_account: Silenzà u contu + status: Statutu + suspend_account: Suspende u contu + target: Oggettu + title: Signalamenti + unassign: Disassignà + unresolved: Micca sciolti + updated_at: Messi à ghjornu + view: Vede + settings: + activity_api_enabled: + desc_html: Numeri di statuti creati quì, utilizatori attivi, è arregistramenti novi tutte e settimane + title: Pubblicà statistiche nant’à l’attività di l’utilizatori + bootstrap_timeline_accounts: + desc_html: Cugnomi separati cù virgule. Solu pussibule cù conti lucali è pubblichi. Quandu a lista hè viota, tutti l’amministratori lucali saranu selezziunati. + title: Abbunamenti predefiniti per l’utilizatori novi + contact_information: + email: E-mail prufissiunale + username: Identificatore di cuntattu + hero: + desc_html: Affissatu nant’a pagina d’accolta. Ricumandemu almenu 600x100px. S’ellu ùn hè micca definiti, a vignetta di l’istanza sarà usata + title: Ritrattu di cuprendula + peers_api_enabled: + desc_html: Indirizzi st’istanza hà vistu indè u fediverse + title: Pubblicà a lista d’istanza cunnisciute + registrations: + closed_message: + desc_html: Affissatu nant’a pagina d’accolta quandu l’arregistramenti sò chjosi. Pudete fà usu di u furmattu HTML + title: Missaghju per l’arregistramenti chjosi + deletion: + desc_html: Auturizà tuttu u mondu di sguassà u so propiu contu + title: Auturizà à sguassà i conti + min_invite_role: + disabled: Nisunu + title: Auturizà l’invitazione da + open: + desc_html: Auturizà tuttu u mondu à creà un contu quì + title: Apre l’arregistramenti + show_known_fediverse_at_about_page: + desc_html: Quandu ghjè selezziunatu, statuti di tuttu l’istanze cunnisciute saranu affissati indè a vista di e linee. Altrimente soli i statuti lucali saranu mustrati. + title: Vedde tuttu u fediverse cunnisciutu nant’a vista di e linee + show_staff_badge: + desc_html: Mustrerà un badge Squadra nant’à un prufile d’utilizatore + title: Mustrà un badge staff + site_description: + desc_html: Paragrafu di prisentazione nant’a pagina d’accolta è i marchi meta. Pudete fà usu di marchi HTML, in particulare <a> è <em>. + title: Discrizzione di l’istanza + site_description_extended: + desc_html: Una bona piazza per e regule, infurmazione è altre cose chì l’utilizatori duverìanu sapè. Pudete fà usu di marchi HTML + title: Discrizzione stesa di u situ + site_terms: + desc_html: Quì pudete scrive e vostre regule di cunfidenzialità, cundizione d’usu o altre menzione legale. Pudete fà usu di marchi HTML + title: Termini persunalizati + site_title: Nome di l’istanza + thumbnail: + desc_html: Utilizatu per viste cù OpenGraph è l’API. Ricumandemu 1200x630px + title: Vignetta di l’istanza + timeline_preview: + desc_html: Vede a linea pubblica nant’a pagina d’accolta + title: Vista di e linee + title: Parametri di u situ + statuses: + back_to_account: Ritornu à a pagina di u contu + batch: + delete: Toglie + nsfw_off: Indicà cum’è micca sensibile + nsfw_on: Indicà cum’è sensibile + failed_to_execute: Esecuzione impussibule + media: + title: Media + no_media: Nisun media + title: Statutu di u contu + with_media: Cù media + subscriptions: + callback_url: URL di richjama + confirmed: Cunfirmatu + expires_in: Spira in + last_delivery: Ultima arricata + title: WebSub + topic: Sughjettu + title: Amministrazione + admin_mailer: + new_report: + body: "%{reporter} hà palisatu %{target}" + body_remote: Qualch’unu da %{domain} hà palisatu %{target} + subject: Novu signalamentu nant’à %{instance} (#%{id}) + application_mailer: + notification_preferences: Cambià e priferenze e-mail + salutation: "%{name}," + settings: 'Cambià e priferenze e-mail: %{link}' + view: 'Vede:' + view_profile: Vede u prufile + view_status: Vede u statutu + applications: + created: Applicazione creata + destroyed: Applicazione sguassata + invalid_url: L’URL ch’è stata pruvista ùn hè valida + regenerate_token: Creà un’altra fiscia d’accessu + token_regenerated: A fiscia d’accessu hè stata rigenerata + warning: Abbadate à quessi dati. Ùn i date à nisunu! + your_token: Rigenerà a fiscia d’accessu + auth: + agreement_html: Arregistrassi vole dì chì site d’accunsentu per siguità e regule di l’istanza è e cundizione d’usu. + change_password: Chjave d’accessu + confirm_email: Cunfirmà l’e-mail + delete_account: Sguassà u contu + delete_account_html: S’è voi vulete toglie u vostru contu ghjè quì. Duverete cunfirmà a vostra scelta. + didnt_get_confirmation: Ùn avete micca ricevutu l’istruzione di cunfirmazione? + forgot_password: Chjave scurdata? + invalid_reset_password_token: U ligame di riinizializazione di a chjave d’accessu hè spiratu o ùn hè micca validu. Pudete dumandà un'altru ligame. + login: Cunnettassi + logout: Scunnettassi + migrate_account: Cambià di contu + migrate_account_html: S’è voi vulete riindirizà stu contu versu un’altru, ghjè pussibule quì. + or: o + or_log_in_with: O cunnettatevi cù + providers: + cas: CAS + saml: SAML + register: Arregistrassi + register_elsewhere: Arregistrassi altrò + resend_confirmation: Rimandà l’istruzzioni di cunfirmazione + reset_password: Cambià a chjave d’accessu + security: Sicurità + set_new_password: Creà una nova chjave d’accessu + authorize_follow: + already_following: Site digià abbunatu·a à stu contu + error: Peccatu, c’hè statu un prublemu ricercandu u contu + follow: Siguità + follow_request: 'Avete dumandatu di siguità:' + following: 'Eccu! Avà seguitate:' + post_follow: + close: O pudete ancu chjude sta finestra. + return: Rivultà à u prufile di l’utilizatore + web: Andà à l’interfaccia web + title: Siguità %{acct} + datetime: + distance_in_words: + about_x_hours: "%{count}h" + about_x_months: "%{count}mo" + about_x_years: "%{count}y" + almost_x_years: "%{count}y" + half_a_minute: Avà + less_than_x_minutes: "%{count}m" + less_than_x_seconds: Avà + over_x_years: "%{count}y" + x_days: "%{count}d" + x_minutes: "%{count}m" + x_months: "%{count}mo" + x_seconds: "%{count}s" + deletes: + bad_password_msg: È nò! Sta chjave ùn hè curretta + confirm_password: Entrate a vostra chjave d’accessu attuale per verificà a vostra identità + description_html: U contu sarà deattivatu è u cuntenutu sarà sguassatu di manera permanente è irreversibile. Ùn sarà micca pussibule piglià stu cugnome torna per evità l’impusture. + proceed: Sguassà u contu + success_msg: U vostru contu hè statu sguassatu + warning_html: Pudete esse sicuru·a solu chì u cuntenutu sarà sguassatu di st’istanza. S’ellu hè statu spartutu in altrò, sarà forse sempre quallà. + warning_title: Dispunibilità di i cuntenuti sparsi + errors: + '403': Ùn site micca auturizatu·a à vede sta pagina. + '404': Sta pagina ùn esiste micca. + '410': Sta pagina ùn esiste più. + '422': + content: C’hè statu un prublemu cù a verificazione di sicurità. Forse bluccate cookies? + title: Fiascu di verificazione + '429': Limitatu dop’à troppu richieste + '500': + content: Scusate, mà c’hè statu un prublemu cù u nostru servore. + title: Sta pagina ùn hè curretta + noscript_html: Mastodon nant’à u web hà bisognu di JavaScript per funziunà. Pudete ancu pruvà l’applicazione native per a vostra piattaforma. + exports: + archive_takeout: + date: Data + download: Scaricà l’archiviu + hint_html: Pudete dumandà un’archiviu di i vostri statuti è media caricati. I dati saranu in u furmattu ActivityPub è pudarenu esse letti da tutti i lugiziali chì u supportanu. + in_progress: Cumpilazione di l’archiviu... + request: Dumandà u vostr’archiviu + size: Pesu + blocks: Bluccate + csv: CSV + follows: Seguitate + mutes: Piattate + storage: I vostri media + followers: + domain: Duminiu + explanation_html: Per assicuravi di a cunfidenzialità di i vostri statuti, duvete avè primura di quale vi seguita. I vostri statuti privati sò mandati à tutte l’istanze induve avete abbunati. Pensate à u vostru livellu di cunfidenza in i so amministratori. + followers_count: Numeru d’abbunati + lock_link: Rendete u contu privatu + purge: Toglie di a lista d’abbunati + success: + one: Suppressione di l’abbunati d’un duminiu... + other: Suppressione di l’abbunati da %{count} duminii... + true_privacy_html: Ùn vi scurdate chì una vera cunfidenzialità pò solu esse ottenuta cù crittografia da un capu à l’altru. + unlocked_warning_html: Tuttu u mondu pò seguitavi è vede i vostri statuti privati. %{lock_link} per pudè cunfirmà o righjittà abbunamenti. + unlocked_warning_title: U vostru contu hè pubblicu + generic: + changes_saved_msg: Cambiamenti salvati! + powered_by: mossu da %{link} + save_changes: Salvà e mudificazione + validation_errors: + one: Qualcosa ùn và bè! Verificate u prublemu quì sottu + other: Qualcosa ùn và bè! Verificate %{count} prublemi quì sottu + imports: + preface: Pudete impurtà certi dati cumu e persone chì seguitate o bluccate nant’à u vostru contu nant’à st’istanza à partesi di fugliali creati nant’à un’altr’istanza. + success: I vostri dati sò stati impurtati è saranu trattati da quì à pocu + types: + blocking: Persone chì bluccate + following: Persone chì seguitate + muting: Persone chì piattate + upload: Impurtà + in_memoriam_html: In mimoria. + invites: + delete: Disattivà + expired: Spirata + expires_in: + '1800': 30 minuti + '21600': 6 ore + '3600': 1 ora + '43200': 12 ore + '604800': 1 settimana + '86400': 1 ghjornu + expires_in_prompt: Mai + generate: Creà + max_uses: + one: 1 usu + other: "%{count} usi" + max_uses_prompt: Micca limita + prompt: Create è spartete ligami cù altre parsone per dà accessu à l’istanza + table: + expires_at: Spira + uses: Utiliza + title: Invità ghjente + landing_strip_html: "%{name} hè nant’à %{link_to_root_path}. Pudete seguitallu·a o cumunicà cù ellu·a cù un contu in qualche parte di u fediverse." + landing_strip_signup_html: Pudete ancu arrigistravi quì. + lists: + errors: + limit: Ùn pudete più creà altre liste + media_attachments: + validations: + images_and_video: Ùn si pò micca aghjunghje un filmettu à un statutu chì hà digià ritratti + too_many: Ùn si pò micca aghjunghje più di 4 fugliali + migrations: + acct: cugnome@duminiu di u novu contu + currently_redirecting: 'U vostru prufile riindiriza tuttu versu à:' + proceed: Salvà + updated_msg: I paramettri di migrazione sò stati messi à ghjornu! + moderation: + title: Muderazione + notification_mailer: + digest: + action: Vede tutte e nutificazione + body: Eccu cio ch’avete mancatu dapoi à a vostr’ultima visita u %{since} + mention: "%{name} v’hà mintuvatu·a in:" + new_followers_summary: + one: Avete ancu un’abbunatu novu! + other: Avete ancu %{count} abbunati novi! + subject: + one: "Una nutificazione nova dapoi à a vostr’ultima visita \U0001F418" + other: "%{count} nutificazione nove dapoi à a vostr’ultima visita \U0001F418" + title: Dapoi l’ultima volta… + favourite: + body: "%{name} hà aghjuntu u vostru statutu à i so favuriti :" + subject: "%{name} hà messu u vostru post in i so favuriti" + title: Novu favuritu + follow: + body: "%{name} s’hè abbunatu à u vostru contu !" + subject: "%{name} vi seguita" + title: Abbunatu novu + follow_request: + action: Vede e dumande d’abbunamentu + body: "%{name} vole abbunassi à u vostru contu" + subject: 'Dumanda d’abbunamentu: %{name}' + title: Nova dumanda d’abbunamentu + mention: + action: Risposta + body: "%{name} v’hà mintuvatu·a indè :" + subject: "%{name} v’hà mintuvatu·a" + title: Nova menzione + reblog: + body: 'U vostru statutu hè statu spartutu da %{name}:' + subject: "%{name} hà spartutu u vostru statutu" + title: Nova spartera + number: + human: + decimal_units: + format: "%n%u" + units: + billion: G + million: M + quadrillion: P + thousand: K + trillion: T + unit: '' + pagination: + newer: Più ricente + next: Dopu + older: Più vechju + prev: Nanzu + truncate: "…" + preferences: + languages: Lingue + other: Altre + publishing: Pubblicazione + web: Web + remote_follow: + acct: Entrate u vostru cugnome@istanza da induve vulete siguità stu contu + missing_resource: Ùn avemu pussutu à truvà l’indirizzu di ridirezzione + proceed: Cuntinuà per siguità + prompt: 'Avete da siguità:' + remote_unfollow: + error: Errore + title: Titulu + unfollowed: Disabbunatu + sessions: + activity: Ultima attività + browser: Navigatore + browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + electron: Electron + firefox: Firefox + generic: Navigatore scunnisciutu + ie: Internet Explorer + micro_messenger: MicroMessenger + nokia: Nokia S40 Ovi Browser + opera: Opera + otter: Otter + phantom_js: PhantomJS + qq: QQ Browser + safari: Safari + uc_browser: UCBrowser + weibo: Weibo + current_session: Sessione attuale + description: "%{browser} nant’à %{platform}" + explanation: Quessi sò i navigatori cunnettati à u vostru contu Mastodon. + ip: IP + platforms: + adobe_air: Adobe Air + android: Android + blackberry: Blackberry + chrome_os: ChromeOS + firefox_os: Firefox OS + ios: iOS + linux: Linux + mac: Mac + other: piattaforma scunnisciuta + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone + revoke: Rivucà + revoke_success: Sessione rivucata + title: Sessioni + settings: + authorized_apps: Applicazione auturizate + back: Ritornu nant’à Mastodon + delete: Suppressione di u contu + development: Sviluppu + edit_profile: Mudificà u prufile + export: Spurtazione d’infurmazione + followers: Abbunati auturizati + import: Impurtazione + migrate: Migrazione di u contu + notifications: Nutificazione + preferences: Priferenze + settings: Parametri + two_factor_authentication: Identificazione à dui fattori + your_apps: E vostre applicazione + statuses: + attached: + description: 'Aghjuntu: %{attached}' + image: + one: "%{count} ritrattu" + other: "%{count} ritratti" + video: + one: "%{count} filmettu" + other: "%{count} filmetti" + content_warning: 'Avertimentu: %{warning}' + disallowed_hashtags: + one: 'cuntene l’hashtag disattivatu: %{tags}' + other: 'cuntene l’hashtag disattivati: %{tags}' + open_in_web: Apre nant’à u web + over_character_limit: Site sopr’à a limita di %{max} caratteri + pin_errors: + limit: Avete digià puntarulatu u numeru massimale di statuti + ownership: Pudete puntarulà solu unu di i vostri propii statuti + private: Ùn pudete micca puntarulà un statutu ch’ùn hè micca pubblicu + reblog: Ùn pudete micca puntarulà una spartera + show_more: Vede di più + title: '%{name}: "%{quote}"' + visibilities: + private: Solu per l’abbunati + private_long: Mustrà solu à l’abbunati + public: Pubblicu + public_long: Tuttu u mondu pò vede + unlisted: Micca listatu + unlisted_long: Tuttu u mondu pò vede, mà micca indè e linee pubbliche + stream_entries: + click_to_show: Cliccà per vede + pinned: Statutu puntarulatu + reblogged: spartutu + sensitive_content: Cuntenutu sensibile + terms: + body_html: | + Privacy Policy + What information do we collect? + + + Basic account information: If you register on this server, you may be asked to enter a username, an e-mail address and a password. You may also enter additional profile information such as a display name and biography, and upload a profile picture and header image. The username, display name, biography, profile picture and header image are always listed publicly. + Posts, following and other public information: The list of people you follow is listed publicly, the same is true for your followers. When you submit a message, the date and time is stored as well as the application you submitted the message from. Messages may contain media attachments, such as pictures and videos. Public and unlisted posts are available publicly. When you feature a post on your profile, that is also publicly available information. Your posts are delivered to your followers, in some cases it means they are delivered to different servers and copies are stored there. When you delete posts, this is likewise delivered to your followers. The action of reblogging or favouriting another post is always public. + Direct and followers-only posts: All posts are stored and processed on the server. Followers-only posts are delivered to your followers and users who are mentioned in them, and direct posts are delivered only to users mentioned in them. In some cases it means they are delivered to different servers and copies are stored there. We make a good faith effort to limit the access to those posts only to authorized persons, but other servers may fail to do so. Therefore it’s important to review servers your followers belong to. You may toggle an option to approve and reject new followers manually in the settings. Please keep in mind that the operators of the server and any receiving server may view such messages, and that recipients may screenshot, copy or otherwise re-share them. Do not share any dangerous information over Mastodon. + IPs and other metadata: When you log in, we record the IP address you log in from, as well as the name of your browser application. All the logged in sessions are available for your review and revocation in the settings. The latest IP address used is stored for up to 12 months. We also may retain server logs which include the IP address of every request to our server. + + + + + What do we use your information for? + + Any of the information we collect from you may be used in the following ways: + + + To provide the core functionality of Mastodon. You can only interact with other people’s content and post your own content when you are logged in. For example, you may follow other people to view their combined posts in your own personalized home timeline. + To aid moderation of the community, for example comparing your IP address with other known ones to determine ban evasion or other violations. + The email address you provide may be used to send you information, notifications about other people interacting with your content or sending you messages, and to respond to inquiries, and/or other requests or questions. + + + + + How do we protect your information? + + We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information. Among other things, your browser session, as well as the traffic between your applications and the API, are secured with SSL, and your password is hashed using a strong one-way algorithm. You may enable two-factor authentication to further secure access to your account. + + + + What is our data retention policy? + + We will make a good faith effort to: + + + Retain server logs containing the IP address of all requests to this server, in so far as such logs are kept, no more than 90 days. + Retain the IP addresses associated with registered users no more than 12 months. + + + You can request and download an archive of your content, including your posts, media attachments, profile picture, and header image. + + You may irreversibly delete your account at any time. + + + + Do we use cookies? + + Yes. Cookies are small files that a site or its service provider transfers to your computer’s hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account. + + We use cookies to understand and save your preferences for future visits. + + + + Do we disclose any information to outside parties? + + We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety. + + Your public content may be downloaded by other servers in the network. Your public and followers-only posts are delivered to the servers where your followers reside, and direct messages are delivered to the servers of the recipients, in so far as those followers or recipients reside on a different server than this. + + When you authorize an application to use your account, depending on the scope of permissions you approve, it may access your public profile information, your following list, your followers, your lists, all your posts, and your favourites. Applications can never access your e-mail address or password. + + + + Children’s Online Privacy Protection Act Compliance + + Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA (Children’s Online Privacy Protection Act) do not use this site. + + + + Changes to our Privacy Policy + + If we decide to change our privacy policy, we will post those changes on this page. + + This document is CC-BY-SA. It was last updated March 7, 2018. + + Originally adapted from the Discourse privacy policy. + title: Termini d’usu è di cunfidenzialità per %{instance} + themes: + contrast: Cuntrastu altu + default: Mastodon + time: + formats: + default: "%d %b %Y, %H:%M" + two_factor_authentication: + code_hint: Entrate u codice generatu da l’applicazione per cunfirmà + description_html: S’ella hè attivata l’identificazione à dui fattori, duvete avè u vostru telefuninu pè ottene un codice di cunnezzione. + disable: Disattivà + enable: Attivà + enabled: Identificazione à dui fattori attivata + enabled_success: L’identificazione à dui fattori hè stata attivata + generate_recovery_codes: Creà codici di ricuperazione + instructions_html: "Scanate stu QR code cù Google Authenticator, Authy o qualcosa cusì nant’à u vostru telefuninu. St’applicazione hà da creà codici da entrà ogni volta chì vi cunnettate." + lost_recovery_codes: I codici di ricuperazione à usu unicu vi permettenu di sempre avè accessu à u vostru contu s’è voi avete persu u vostru telefuninu. S’elli sò ancu persi, pudete creà codici novi quì. I vechji codici ùn marchjeranu più. + manual_instructions: 'S’ellu ùn hè micca pussibule scanà u QR code, pudete entre sta chjave sicreta:' + recovery_codes: Codici di ricuperazione + recovery_codes_regenerated: Codici di ricuperazione ricreati + recovery_instructions_html: Pudete fà usu di i codici quì sottu per sempre avè accessu à u vostru contu s’ellu hè statu persu u vostru telefuninu. Guardateli in una piazza sicura. Per esempiu, stampati è cunservati cù altri ducumenti impurtanti. + setup: Installà + wrong_code: U codice ùn hè micca currettu! Site sicuru che l’ora di u telefuninu è di u servore sò esatte? + user_mailer: + backup_ready: + explanation: Avete dumandatu un’archiviu cumpletu di u vostru contu Mastodon. Avà hè prontu per scaricà! + subject: U vostru archiviu hè prontu à scaricà + title: Archiviu prontu + welcome: + edit_profile_action: Cunfigurazione di u prufile + edit_profile_step: Pudete persunalizà u vostru prufile cù un ritrattu di prufile o di cuprendula, un nome pubblicu persunalizatu, etc. Pudete ancu rende u contu privatu per duvè cunfirmà ogni dumanda d’abbunamentu. + explanation: Eccu alcune idee per principià + final_action: Principià à pustà + final_step: 'Andemu! Ancu senza abbunati i vostri missaghji pubblichi puderanu esse visti da altre persone, per esempiu nant’a linea lucale è l’hashtag. Pudete ancu prisintavi nant’à u hashtag #introductions.' + full_handle: U vostru identificatore cumplettu + full_handle_hint: Quessu ghjè cio chì direte à i vostri amichi per circavi, abbunassi à u vostru contu da altrò, o mandà missaghji. + review_preferences_action: Mudificà e priferenze + review_preferences_step: Quì pudete adattà u cumpurtamentu di Mastodon à e vostre priferenze, cum’è l’email che vulete riceve, u nivellu di cunfidenzialità predefinitu di i vostri statuti, o u cumpurtamentu di i GIF animati. + subject: Benvenutu·a nant’à Mastodon + tip_bridge_html: S’è voi venite di Twitter, pudete truvà i vostri amichi da quallà chì sò nant’à Mastodon cù a bridge app. Mà ùn marchja chè s’elli l’anu ancu usata! + tip_federated_timeline: A linea pubblica glubale mostra i statuti da altre istanze nant’a rete Mastodon, mà ùn hè micca cumpleta perchè ci sò soli i conti à quelli sò abbunati membri di a vostr’istanza. + tip_following: Site digià abbunatu·a à l’amministratori di u vostru servore. Per truvà d’altre parsone da siguità, pudete pruvà e linee pubbliche. + tip_local_timeline: A linea pubblica lucale ghjè una vista crunulogica di i statuti di a ghjente nant’à %{instance}. Quessi sò i vostri cunvicini! + tip_mobile_webapp: Pudete aghjunghje Mastodon à a pagina d’accolta di u vostru navigatore di telefuninu per riceve nutificazione, cum’un applicazione! + tips: Cunsiglii + title: Benvenutu·a, %{name}! + users: + invalid_email: L’indirizzu e-mail ùn hè currettu + invalid_otp_token: U codice d’identificazione ùn hè currettu + otp_lost_help_html: S’è voi avete persu i dui, pudete cuntattà %{email} + seamless_external_login: Site cunnettatu·a dapoi un serviziu esternu, allora i parametri di chjave d’accessu è d’indirizzu e-mail ùn so micca dispunibili. + signed_in_as: 'Cunnettatu·a cum’è:' diff --git a/config/locales/de.yml b/config/locales/de.yml index 5fdcb1900f..3440439cda 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -40,6 +40,7 @@ de: following: Folgt media: Medien moved_html: "%{name} ist auf %{new_profile_link} umgezogen:" + network_hidden: Diese Informationen sind nicht verfügbar nothing_here: Hier gibt es nichts! people_followed_by: Profile, denen %{name} folgt people_who_follow: Profile, die %{name} folgen @@ -49,13 +50,12 @@ de: reserved_username: Dieser Profilname ist belegt roles: admin: Admin + bot: Bot moderator: Moderator unfollow: Entfolgen admin: account_moderation_notes: - account: Moderator*in - create: Erstellen - created_at: Datum + create: Notiz hinterlassen created_msg: Moderationsnotiz erfolgreich erstellt! delete: Löschen destroyed_msg: Moderationsnotiz erfolgreich gelöscht! @@ -72,6 +72,7 @@ de: title: E-Mail-Adresse für %{username} ändern confirm: Bestätigen confirmed: Bestätigt + confirming: Bestätigung demote: Degradieren disable: Ausschalten disable_two_factor_authentication: 2FA abschalten @@ -80,6 +81,7 @@ de: domain: Domain edit: Bearbeiten email: E-Mail + email_status: E-Mail-Status enable: Freischalten enabled: Freigegeben feed_url: Feed-URL @@ -118,6 +120,10 @@ de: push_subscription_expires: PuSH-Abonnement läuft aus redownload: Avatar neu laden remove_avatar: Profilbild entfernen + resend_confirmation: + already_confirmed: Dieser Benutzer wurde bereits bestätigt + send: Bestätigungsmail erneut senden + success: Bestätigungs-E-Mail erfolgreich gesendet! reset: Zurücksetzen reset_password: Passwort zurücksetzen resubscribe: Wieder abonnieren @@ -269,7 +275,6 @@ de: comment: none: Kein created_at: Gemeldet - delete: Löschen id: ID mark_as_resolved: Als gelöst markieren mark_as_unresolved: Als ungelöst markieren @@ -279,9 +284,6 @@ de: create_and_unresolve: Mit Kommentar wieder öffnen delete: Löschen placeholder: Beschreibe, welche Maßnahmen ergriffen wurden oder andere Neuigkeiten zu dieser Meldung… - nsfw: - 'false': Medienanhänge wieder anzeigen - 'true': Medienanhänge verbergen reopen: Meldung wieder öffnen report: 'Meldung #%{id}' report_contents: Inhalt @@ -356,11 +358,8 @@ de: delete: Löschen nsfw_off: Als nicht heikel markieren nsfw_on: Als heikel markieren - execute: Ausführen failed_to_execute: Ausführen fehlgeschlagen media: - hide: Medien verbergen - show: Medien anzeigen title: Medien no_media: Keine Medien title: Beiträge des Kontos @@ -376,6 +375,7 @@ de: admin_mailer: new_report: body: "%{reporter} hat %{target} gemeldet" + body_remote: Jemand von %{domain} hat %{target} gemeldet subject: Neue Meldung auf %{instance} (#%{id}) application_mailer: notification_preferences: Ändere E-Mail-Einstellungen @@ -465,7 +465,7 @@ de: archive_takeout: date: Datum download: Dein Archiv herunterladen - hint_html: Du kannst ein Archiv deiner Beiträge und hochgeladenen Medien anfragen. Die exportierten Daten werden im ActivityPub-Format gespeichert, welches mit jeder Software lesbar ist die das Format unterstützt. + hint_html: Du kannst ein Archiv deiner Beiträge und hochgeladenen Medien anfragen. Die exportierten Daten werden im ActivityPub-Format gespeichert, welches mit jeder Software lesbar ist, die das Format unterstützt. Du kannst alle 7 Tage ein neues Archiv anfordern. in_progress: Stelle dein Archiv zusammen... request: Dein Archiv anfragen size: Größe @@ -595,20 +595,6 @@ de: other: Weiteres publishing: Beiträge web: Web - push_notifications: - favourite: - title: "%{name} hat deinen Beitrag favorisiert" - follow: - title: "%{name} folgt dir nun" - group: - title: "%{count} Benachrichtigungen" - mention: - action_boost: Teilen - action_expand: Mehr anzeigen - action_favourite: Favorisieren - title: "%{name} hat dich erwähnt" - reblog: - title: "%{name} hat deinen Beitrag geteilt" remote_follow: acct: Profilname@Domain, von wo aus du dieser Person folgen möchtest missing_resource: Die erforderliche Weiterleitungs-URL für dein Konto konnte nicht gefunden werden @@ -757,5 +743,6 @@ de: users: invalid_email: Ungültige E-Mail-Adresse invalid_otp_token: Ungültiger Zwei-Faktor-Authentisierungs-Code + otp_lost_help_html: Wenn Sie zu beidem keinen Zugriff mehr haben, kontaktieren sie %{email} seamless_external_login: Du bist angemeldet über einen Drittanbieter-Dienst, weswegen Passwort- und E-Maileinstellungen nicht verfügbar sind. signed_in_as: 'Angemeldet als:' diff --git a/config/locales/devise.co.yml b/config/locales/devise.co.yml new file mode 100644 index 0000000000..2471f857be --- /dev/null +++ b/config/locales/devise.co.yml @@ -0,0 +1,82 @@ +--- +co: + devise: + confirmations: + confirmed: U vostru indirizzu email hè statu cunfirmatu. + send_instructions: Avete da riceve un’email cù l’istruzzione di cunfirmazione in qualchì minuta. Pensate à verificà u cartulare di spam s’ellu ùn c’hè nunda. + send_paranoid_instructions: S’ellu esiste u vostru indirizzu email in a database, avete da riceve l’istruzzione pè cunfirmà u vostru contu in qualchì minuta. Pensate à verificà u cartulare di spam s’ellu ùn c’hè nunda. + failure: + already_authenticated: Site digià cunnettatu·a. + inactive: U vostru contu ùn hè ancu attivatu. + invalid: L’ %{authentication_keys} o a chjave d’accessu ùn sò curretti. + last_attempt: Avete un’ultimu tintativu nanzu chì u vostru contu sia chjosu. + locked: U vostru contu hè chjosu. + not_found_in_database: L’ %{authentication_keys} o a chjave d’accessu ùn sò curretti. + timeout: A vostra sezzione hè spirata. Ricunnettatevi pè cuntinuà. + unauthenticated: Cunnettatevi o arregistratevi pè cuntinuà. + unconfirmed: Duvete cunfirmà u vostru contu pè cuntinuà. + mailer: + confirmation_instructions: + action: Verificà l’indirizzu email + explanation: Avete creatu un contu nant’à %{host} cù st’indirizzu email. Pudete attivallu cù un clic, o ignurà quessu missaghji s’ellu un era micca voi. + extra_html: Pensate à leghje e regule di l’istanza è i termini d’usu. + subject: 'Mastodon: Istruzzione di cunfirmazione per %{instance}' + title: Verificà l’indirizzu email + email_changed: + explanation: 'L’indirizzu email di u vostru contu hè stata cambiata per:' + extra: S’ellu un era micca voi ch’avete cambiatu u vostru email, qualch’un’altru hà accessu à u vostru contu. Duvete cambià a vostra chjave d’accessu o cuntattà l’amministratore di l’istanza s’ellu ùn hè più pussibule di cunnettavi. + subject: 'Mastodon: Email cambiatu' + title: Novu indirizzu email + password_change: + explanation: A chjave d’accessu per u vostru contu hè stata cambiata. + extra: S’ellu un era micca voi ch’avete cambiatu a vostra chjave d’accessu, qualch’un’altru hà accessu à u vostru contu. Duvete cambià a vostra chjave d’accessu o cuntattà l’amministratore di l’istanza s’ellu ùn hè più pussibule di cunnettavi. + subject: 'Mastodon: Chjave d’accessu cambiata' + title: Chjave cambiata + reconfirmation_instructions: + explanation: Cunfirmà u novu indirizzu per cambià l’email. + extra: S’ellu ùn era micca voi, ignurate stu missaghju. L’email ùn cambiarà micca s’è voi ùn cliccate micca u ligame. + subject: 'Mastodon: Cunfirmà l’email per %{instance}' + title: Verificà indirizzu email + reset_password_instructions: + action: Cambià a chjave d’accessu + explanation: Avete dumandatu una nova chjave d’accessu per u vostru contu. + extra: S’ellu ùn era micca voi, ignurate stu missaghju. A chjave d’accessu ùn cambiarà micca s’è voi ùn cliccate micca u ligame. + subject: 'Mastodon: Cambià a chjave d’accessu' + title: Cambià a chjave + unlock_instructions: + subject: 'Mastodon: Riapre u contu' + omniauth_callbacks: + failure: Ùn pudemu micca cunnettavi da %{kind} perchè "%{reason}". + success: Vi site cunnettatu·a da %{kind}. + passwords: + no_token: Ùn pudete micca vede sta pagina senza vene d’un e-mail di cambiamentu di chjave d’accessu. S’è voi venite quì dapoi st’e-mail, assicuratevi ch’avete utilizatu l’indirizzu URL cumpletu. + send_instructions: Avete da riceve l’istruzzione di cambiamentu di a chjave d’accessu in qualchì minuta. + send_paranoid_instructions: S’ellu esiste u vostr’e-mail in a database, avete da riceve un ligame di reinizialisazione. + updated: A vostra chjave d’accessu hè stata cambiata, è site cunnettatu·a. + updated_not_active: A vostra chjave d’accessu hè stata cambiata. + registrations: + destroyed: U vostru contu hè statu sguassatu. Avvedeci! + signed_up: Benvinutu! Site cunnettatu·a. + signed_up_but_inactive: Site arregistratu·a, mà ùn pudete micca cunnettavi perchè u vostru contu deve esse attivatu. + signed_up_but_locked: Site arregistratu·a, mà ùn pudete micca cunnettavi perchè u vostru contu hè chjosu. + signed_up_but_unconfirmed: Un missaghju cù un ligame di cunfirmazione hè statu mandatu à u vostru indirizzu e-mail. Aprite stu ligame pè attivà u vostru contu. Pensate à verificà u cartulare di spam s’ellu ùn c’hè nunda. + update_needs_confirmation: U vostru contu hè statu messu à ghjornu mà duvemu verificà u vostru novu e-mail. Un missaghju cù un ligame di cunfirmazione hè statu mandatu. Pensate à verificà u cartulare di spam s’ellu ùn c’hè nunda. + updated: U vostru contu hè statu messu à ghjornu. + sessions: + already_signed_out: Scunnettatu·a. + signed_in: Cunnettatu·a. + signed_out: Scunnettatu·a. + unlocks: + send_instructions: Avete da riceve un’e-mail cù l’istruzzione pè riapre u vostru contu in qualchì minuta. + send_paranoid_instructions: S’ellu esiste u vostru contu, avete da riceve un’e-mail dù l’istruzzione pè riapre u vostru contu. + unlocked: U vostru contu hè statu riapertu, pudete cunnettavi pè cuntinuà. + errors: + messages: + already_confirmed: hè digià cunfirmatu, pudete pruvà à cunnettà vi + confirmation_period_expired: deve esse cunfirmatu nanz’à %{period}, duvete fà un’altra dumanda + expired: hè spiratu, duvete fà un’altra dumanda + not_found: ùn hè micca statu trovu + not_locked: ùn era micca chjosu + not_saved: + one: 'Un prublemu hà impeditu a cunservazione di stu (sta) %{resource}:' + other: "%{count} prublemi anu impeditu a cunservazione di stu (sta) %{resource} :" diff --git a/config/locales/devise.fa.yml b/config/locales/devise.fa.yml index f78412f91d..e6e16b4b49 100644 --- a/config/locales/devise.fa.yml +++ b/config/locales/devise.fa.yml @@ -17,11 +17,32 @@ fa: unconfirmed: برای ادامه باید نشانی ایمیل خود را تأیید کنید. mailer: confirmation_instructions: + action: تأیید نشانی ایمیل + explanation: شما با این نشانی ایمیل حسابی در %{host} باز کردهاید. با یک کلیک میتوانید این حساب را فعال کنید. اگر شما چنین کاری نکردید، لطفاً این ایمیل را نادیده بگیرید. + extra_html: لطفاً همچنین قانونهای این سرور و شرایط کاربری آن را ببینید. subject: 'ماستدون: راهنمایی برای تأیید %{instance}' + title: تأیید نشانی ایمیل + email_changed: + explanation: 'نشانی ایمیل حساب شما تغییر میکند به:' + extra: اگر شما ایمیل خود را عوض نکردید، شاید کسی به حساب شما دسترسی پیدا کرده است. در این صورت لطفاً هر چه زودتر رمز حسابتان را عوض کنید. اگر رمزتان دیگر کار نمیکند، لطفاً با مدیر سرور تماس بگیرید. + subject: 'ماستدون: نشانی ایمیل عوض شد' + title: نشانی ایمیل تازه password_change: + explanation: رمز حساب شما تغییر کرد. + extra: اگر شما رمز حسابتان را تغییر ندادید، شاید کسی به حساب شما دسترسی پیدا کرده است. در این صورت لطفاً هر چه زودتر رمز حسابتان را عوض کنید. اگر رمزتان دیگر کار نمیکند، لطفاً با مدیر سرور تماس بگیرید. subject: 'ماستدون: رمزتان عوض شد' + title: رمزتان عوض شد + reconfirmation_instructions: + explanation: نشانی تازه را تأیید کنید تا ایمیلتان عوض شود. + extra: اگر شما باعث این تغییر نبودید، لطفاً این ایمیل را نادیده بگیرید. تا زمانی که شما پیوند بالا را باز نکنید، نشانی ایمیل مربوط به حساب شما عوض نخواهد شد. + subject: 'ماستدون: تأیید ایمیل برای %{instance}' + title: تأیید نشانی ایمیل reset_password_instructions: + action: تغییر رمز + explanation: شما رمز تازهای برای حسابتان درخواست کردید. + extra: اگر شما چنین درخواستی نکردید، لطفاً این ایمیل را نادیده بگیرید. تا زمانی که شما پیوند بالا را باز نکنید و رمز تازهای نسازید، رمز شما عوض نخواهد شد. subject: 'ماستدون: راهنمایی برای بازنشانی رمز' + title: بازنشانی رمز unlock_instructions: subject: 'ماستدون: راهنمایی برای بازکردن قفل' omniauth_callbacks: @@ -57,5 +78,5 @@ fa: not_found: پیدا نشد not_locked: قفل نبود not_saved: - one: خطایی نگذاشت که این %{resource} ذخیره شود - other: به خاطر %{count} خطا، این %{resource} ذخیره نشد + one: 'خطایی نگذاشت که این %{resource} ذخیره شود:' + other: 'به خاطر %{count} خطا، این %{resource} ذخیره نشد:' diff --git a/config/locales/devise.sk.yml b/config/locales/devise.sk.yml index e9c5dd4557..4bbc723e9b 100644 --- a/config/locales/devise.sk.yml +++ b/config/locales/devise.sk.yml @@ -78,5 +78,6 @@ sk: not_found: nenájdený not_locked: nebol uzamknutý not_saved: + few: "%{resource} nebol uložený kôli %{count} chybám:" one: "%{resource} nebol uložený kôli chybe:" other: "%{resource} nebol uložený kôli %{count} chybám:" diff --git a/config/locales/devise.sl.yml b/config/locales/devise.sl.yml new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/config/locales/devise.sl.yml @@ -0,0 +1 @@ +{} diff --git a/config/locales/doorkeeper.ar.yml b/config/locales/doorkeeper.ar.yml index 5586b8dc86..3b42029036 100644 --- a/config/locales/doorkeeper.ar.yml +++ b/config/locales/doorkeeper.ar.yml @@ -114,5 +114,6 @@ ar: title: طلب تصريح مفتوح OAuth scopes: follow: متابعة و حجب و فك الحجب و إلغاء متابعة حسابات المستخدمين + push: تلقى إشعارات حسابك read: قراءة بيانات حسابك write: النشر نيابةً عنك diff --git a/config/locales/doorkeeper.ca.yml b/config/locales/doorkeeper.ca.yml index c1748d05b1..56686e3e5b 100644 --- a/config/locales/doorkeeper.ca.yml +++ b/config/locales/doorkeeper.ca.yml @@ -115,5 +115,6 @@ ca: title: OAuth autorització requerida scopes: follow: seguir, blocar, desblocar i deixar de seguir comptes + push: rebre notificacions push del teu compte read: llegir les dades del teu compte write: publicar en el teu nom diff --git a/config/locales/doorkeeper.co.yml b/config/locales/doorkeeper.co.yml new file mode 100644 index 0000000000..31080d1533 --- /dev/null +++ b/config/locales/doorkeeper.co.yml @@ -0,0 +1,119 @@ +--- +co: + activerecord: + attributes: + doorkeeper/application: + name: Nome di l’applicazione + redirect_uri: URI di ridirezzione + scopes: Scopi + website: Situ di l’applicazione + errors: + models: + doorkeeper/application: + attributes: + redirect_uri: + fragment_present: ùn pò cuntene un pezzu. + invalid_uri: duve esse un’URI curretta. + relative_uri: duve esse un’URI assoluta. + secured_uri: duve esse un’URL HTTPS/SSL. + doorkeeper: + applications: + buttons: + authorize: Appruvà + cancel: Sguassà + destroy: Strughje + edit: Mudificà + submit: Mandà + confirmations: + destroy: Site sicuru·a? + edit: + title: Mudificà l’applicazione + form: + error: Uups! V’invitemu à verificà u vostru formulariu per vede s’elli ùn ci sò sbaglii + help: + native_redirect_uri: Utilizate %{native_redirect_uri} pè e prove lucale + redirect_uri: Utilizzate una linea per ogni URI + scopes: Separate i scopi cù spazii. Lasciate viotu per utilizzà i scopi predefiniti. + index: + application: Applicazione + callback_url: URL di richjama + delete: Toglie + name: Nome + new: Applicazione nova + scopes: Scopi + show: Vede + title: E vostre applicazione + new: + title: Applicazione nova + show: + actions: Azzioni + application_id: Chjave di u clientu + callback_urls: URL di richjama + scopes: Scopi + secret: Sicretu di u clientu + title: 'Applicazione : %{name}' + authorizations: + buttons: + authorize: Appruvà + deny: Ricusà + error: + title: C’hè statu un prublemu + new: + able_to: St’applicazione puderà + prompt: L’applicazione %{client_name} hà dumandatu d’avè accessu à u vostru contu + title: Permessu riquestu + show: + title: Codice d’auturizazione da cupià indè l’applicazione. + authorized_applications: + buttons: + revoke: Sguassà + confirmations: + revoke: Site sicuru·a? + index: + application: Applicazione + created_at: Auturizata u + date_format: "%d-%m-%Y %H:%M:%S" + scopes: Scopi + title: E vostre applicazione auturizate + errors: + messages: + access_denied: U pruprietariu di a risorsa o u servore d’autitinficazione hà ricusatu a dumanda. + credential_flow_not_configured: U flussu di l’identificazione di u pruprietariu di a risorsa hà fiascatu perchè Doorkeeper.configure.resource_owner_from_credentials ùn hè micca cunfiguratu. + invalid_client: L’autintificazione di u cliente hà fiascatu perchè u cliente ùn hè micca cunnisciutu, l’identificazione di u cliente ùn hè cumpresa, o u modu d’identificazione ùn marchja micca. + invalid_grant: L’accunsentu d’auturizazione furnitu ùn hè currettu, hè spiratu, sguassatu, ùn và micca cù l’indirizzu di ridirezzione usatu in a dumanda d’auturizazione, o hè statu emessu per un’altru cliente. + invalid_redirect_uri: L’URI di ridirezzione ùn hè curretta. + invalid_request: Ci manca un parametru riquestu indè a dumanda, cuntene un parametru ch’ùn esiste micca, o altru sbagliu di forma. + invalid_resource_owner: L’idintificanti di u pruprietariu di a risorsa ùn sò curretti, o u pruprietariu ùn pò micca esse trovu + invalid_scope: U scopu dumandatu ùn hè currettu, hè scunnisciutu, o altru sbagliu di forma. + invalid_token: + expired: A marca d’accessu hè spirata + revoked: A marca d’accessu hè stata rivucata + unknown: A marca d’accessu ùn hè curretta + resource_owner_authenticator_not_configured: Ùn c’hè micca pussutu ricercà u pruprietariu di a risorsa perchè Doorkeeper.configure.resource_owner_authenticator ùn hè micca cunfiguratu. + server_error: C’hè statu un prublemu cù u servore d’auturizazione. + temporarily_unavailable: U servore d’auturizazione ùn pò micca trattà a dumanda avà perchè hè sopraccaricatu o in mantenimentu. + unauthorized_client: U cliente ùn pò micca fà sta dumanda cusì. + unsupported_grant_type: Stu tippu d’accunsentu ùn marchja micca nant’à stu servore d’auturizazione. + unsupported_response_type: Sta risposta ùn marchja micca nant’à stu servore d’auturizazione. + flash: + applications: + create: + notice: Applicazione creata. + destroy: + notice: Applicazione sguassata. + update: + notice: Applicazione messa à ghjornu. + authorized_applications: + destroy: + notice: Applicazione sguassata. + layouts: + admin: + nav: + applications: Applicazione + oauth2_provider: Furnitore OAuth2 + application: + title: Auturizazione OAuth riquestata + scopes: + follow: bluccà, sbluccà, è reghje l’abbunamenti + read: leghje l’infurmazione di u vostru contu + write: mandà missaghji per voi diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index 33d544bed5..eca1fc675f 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -115,5 +115,6 @@ en: title: OAuth authorization required scopes: follow: follow, block, unblock and unfollow accounts + push: receive push notifications for your account read: read your account's data write: post on your behalf diff --git a/config/locales/doorkeeper.eo.yml b/config/locales/doorkeeper.eo.yml index 59df528526..9713c462c4 100644 --- a/config/locales/doorkeeper.eo.yml +++ b/config/locales/doorkeeper.eo.yml @@ -115,5 +115,6 @@ eo: title: OAuth-a rajtigo bezonata scopes: follow: sekvi, bloki, malbloki kaj malsekvi kontojn + push: ricevi puŝ-sciigojn por via konto read: legi la datumojn de via konto write: mesaĝi kiel vi diff --git a/config/locales/doorkeeper.fa.yml b/config/locales/doorkeeper.fa.yml index f3db862ca1..2293f1115e 100644 --- a/config/locales/doorkeeper.fa.yml +++ b/config/locales/doorkeeper.fa.yml @@ -19,56 +19,56 @@ fa: doorkeeper: applications: buttons: - authorize: Authorize - cancel: Cancel - destroy: Destroy - edit: Edit + authorize: اجازه دادن + cancel: لغو + destroy: پاک کردن + edit: ویرایش submit: Submit confirmations: - destroy: Are you sure? + destroy: آیا مطمئن هستید؟ edit: - title: Edit application + title: ویرایش برنامه form: - error: Whoops! Check your form for possible errors + error: اوخ! ببینید چیزی را اشتباهی در فرم وارد نکردهاید؟ help: - native_redirect_uri: Use %{native_redirect_uri} for local tests - redirect_uri: Use one line per URI - scopes: Separate scopes with spaces. Leave blank to use the default scopes. + native_redirect_uri: برای آزمایشهای محلی %{native_redirect_uri} را به کار ببرید + redirect_uri: هر URI را در یک سطر جدا بنویسید + scopes: دامنهها را با فاصلهٔ خالی از هم جدا کنید. برای بهکاربردن دامنهٔ پیشفرض خالی بگذارید. index: - application: Application - callback_url: Callback URL + application: برنامه + callback_url: نشانی Callback delete: Delete name: Name - new: New application - scopes: Scopes - show: Show - title: Your applications + new: برنامهٔ تازه + scopes: دامنهها + show: نمایش + title: برنامههای شما new: - title: New application + title: برنامهٔ تازه show: actions: Actions - application_id: Client key - callback_urls: Callback URLs - scopes: Scopes - secret: Client secret - title: 'Application: %{name}' + application_id: کلید کلاینت + callback_urls: نشانیهای Callabck + scopes: دامنهها + secret: کد سری کلاینت + title: 'برنامه: %{name}' authorizations: buttons: - authorize: Authorize - deny: Deny + authorize: اجازه دادن + deny: لغو اجازه error: - title: An error has occurred + title: خطایی رخ داد new: - able_to: It will be able to - prompt: Application %{client_name} requests access to your account - title: Authorization required + able_to: اجازه خواهد داشت + prompt: برنامهٔ %{client_name} میخواهد به حساب شما دسترسی داشته باشد + title: نیاز به اجازه دادن show: - title: Copy this authorization code and paste it to the application. + title: این کد مجوز را کپی کرده و در برنامه وارد کنید. authorized_applications: buttons: - revoke: Revoke + revoke: فسخ confirmations: - revoke: Are you sure? + revoke: آیا مطمئن هستید؟ index: application: برنامه created_at: مجازشده از @@ -77,7 +77,7 @@ fa: title: برنامههای مجاز errors: messages: - access_denied: The resource owner or authorization server denied the request. + access_denied: دارندهٔ منبع یا سرور اجازه دهنده درخواست را نپذیرفت. credential_flow_not_configured: Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured. invalid_client: Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method. invalid_grant: The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. @@ -86,34 +86,35 @@ fa: invalid_resource_owner: The provided resource owner credentials are not valid, or resource owner cannot be found invalid_scope: The requested scope is invalid, unknown, or malformed. invalid_token: - expired: The access token expired - revoked: The access token was revoked - unknown: The access token is invalid + expired: کد دسترسی منقضی شده است + revoked: کد دسترسی فسخ شده است + unknown: کد دسترسی معتبر نیست resource_owner_authenticator_not_configured: Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfiged. - server_error: The authorization server encountered an unexpected condition which prevented it from fulfilling the request. - temporarily_unavailable: The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server. + server_error: خطای پیشبینینشدهای برای سرور اجازهدهنده رخ داد که جلوی اجرای این درخواست را گرفت. + temporarily_unavailable: سرور اجازهدهنده به دلیل بار زیاد یا تعمیرات سرور هماینک نمیتواند درخواست شما را بررسی کند. unauthorized_client: The client is not authorized to perform this request using this method. unsupported_grant_type: The authorization grant type is not supported by the authorization server. unsupported_response_type: The authorization server does not support this response type. flash: applications: create: - notice: Application created. + notice: برنامه ساخته شد. destroy: - notice: Application deleted. + notice: برنامه حذف شد. update: - notice: Application updated. + notice: برنامه بهروز شد. authorized_applications: destroy: - notice: Application revoked. + notice: برنامه فسخ شد. layouts: admin: nav: - applications: Applications - oauth2_provider: OAuth2 Provider + applications: برنامهها + oauth2_provider: فراهمکنندهٔ ورود دومرحلهای application: - title: OAuth authorization required + title: درخواست اجازهٔ OAuth scopes: - follow: follow, block, unblock and unfollow accounts - read: read your account's data - write: post on your behalf + follow: پیگیری، مسدودسازی، لغو مسدودسازی، و لغو پیگیری حسابها + push: برای حساب خود اعلانهای لحظهای دریافت کنید + read: خواندن اطلاعات حساب شما + write: انتشار مطالب از طرف شما diff --git a/config/locales/doorkeeper.fr.yml b/config/locales/doorkeeper.fr.yml index a5d9b9e030..0c68dfa013 100644 --- a/config/locales/doorkeeper.fr.yml +++ b/config/locales/doorkeeper.fr.yml @@ -116,5 +116,6 @@ fr: title: Autorisation OAuth requise scopes: follow: s’abonner, se désabonner, bloquer et débloquer des comptes + push: recevoir des notifications pour votre compte read: lire les données de votre compte write: poster en votre nom diff --git a/config/locales/doorkeeper.gl.yml b/config/locales/doorkeeper.gl.yml index dc9a04f18f..6d0e16defb 100644 --- a/config/locales/doorkeeper.gl.yml +++ b/config/locales/doorkeeper.gl.yml @@ -115,5 +115,6 @@ gl: title: Precisa autorización OAuth scopes: follow: seguir, bloquear, desbloquear e deixar de seguir contas + push: recibir notificatións tipo push para a súa conta read: ler os datos da súa conta write: publicar no seu nome diff --git a/config/locales/doorkeeper.it.yml b/config/locales/doorkeeper.it.yml index 50b2c97801..ce6fa07d26 100644 --- a/config/locales/doorkeeper.it.yml +++ b/config/locales/doorkeeper.it.yml @@ -115,5 +115,6 @@ it: title: Autorizzazione OAuth richiesta scopes: follow: seguire, bloccare, sbloccare e smettere di seguire account + push: ricevi notifiche push per il tuo account read: leggere le informazioni del tuo account write: pubblicare post in tua vece diff --git a/config/locales/doorkeeper.ja.yml b/config/locales/doorkeeper.ja.yml index 96956c60f9..9c9098976f 100644 --- a/config/locales/doorkeeper.ja.yml +++ b/config/locales/doorkeeper.ja.yml @@ -115,5 +115,6 @@ ja: title: OAuth認証 scopes: follow: アカウントのフォロー, ブロック, ブロック解除, フォロー解除 + push: アカウントへのプッシュ通知の受信 read: アカウントからのデータの読み取り write: アカウントへのデータの書き込み diff --git a/config/locales/doorkeeper.nl.yml b/config/locales/doorkeeper.nl.yml index 7ad10f45ba..996cdce7f0 100644 --- a/config/locales/doorkeeper.nl.yml +++ b/config/locales/doorkeeper.nl.yml @@ -116,5 +116,6 @@ nl: title: OAuth-autorisatie vereist scopes: follow: accounts volgen, negeren en blokkeren + push: ontvang pushmeldingen voor jouw account read: berichten lezen write: berichten plaatsen diff --git a/config/locales/doorkeeper.oc.yml b/config/locales/doorkeeper.oc.yml index d83d07438d..46060e49f5 100644 --- a/config/locales/doorkeeper.oc.yml +++ b/config/locales/doorkeeper.oc.yml @@ -115,5 +115,6 @@ oc: title: Cal una autorizacion OAuth scopes: follow: sègre, blocar, quitar de blocar e quitar de sègre de comptes + push: recebre las notificacions push per vòstre compte read: legir las donadas de vòstre compte write: publicar per vos diff --git a/config/locales/doorkeeper.pl.yml b/config/locales/doorkeeper.pl.yml index 6c127b73be..5cad356fc9 100644 --- a/config/locales/doorkeeper.pl.yml +++ b/config/locales/doorkeeper.pl.yml @@ -115,5 +115,6 @@ pl: title: Uwierzytelnienie OAuth jest wymagane scopes: follow: możliwość śledzenia, blokowania, usuwania blokad, anulowania śledzenia kont + push: otrzymywanie powiadomień push dla Twojego konta read: dostęp do odczytu danych konta write: możliwość publikowania wpisów w Twoim imieniu diff --git a/config/locales/doorkeeper.pt-BR.yml b/config/locales/doorkeeper.pt-BR.yml index f3da6fcd1f..8d3ce8a296 100644 --- a/config/locales/doorkeeper.pt-BR.yml +++ b/config/locales/doorkeeper.pt-BR.yml @@ -115,5 +115,6 @@ pt-BR: title: Autorização OAuth obrigatória scopes: follow: seguir, bloquear, desbloquear e deixar de seguir outras contas + push: receber notificações push para a sua conta read: ler os dados da sua conta write: postar em seu nome diff --git a/config/locales/doorkeeper.ru.yml b/config/locales/doorkeeper.ru.yml index 28c0ff0bdf..0a88d628e5 100644 --- a/config/locales/doorkeeper.ru.yml +++ b/config/locales/doorkeeper.ru.yml @@ -115,5 +115,6 @@ ru: title: Требуется авторизация OAuth scopes: follow: подписываться, отписываться, блокировать и разблокировать аккаунты + push: принимать push-уведомления для Вашего аккаунта read: читать данные Вашего аккаунта write: отправлять за Вас посты diff --git a/config/locales/doorkeeper.sk.yml b/config/locales/doorkeeper.sk.yml index bda26429e8..d909271892 100644 --- a/config/locales/doorkeeper.sk.yml +++ b/config/locales/doorkeeper.sk.yml @@ -33,14 +33,14 @@ sk: help: native_redirect_uri: Použite %{native_redirect_uri} pre lokálne testy redirect_uri: Iba jedna URI na riadok - scopes: Rozsahy oddeľujte medzerami. Nechajte prázdne pre štandardné rozsahy. + scopes: Oprávnenia oddeľujte medzerami. Nechajte prázdne pre štandardné oprávnenia. index: application: Aplikácia callback_url: Návratová URL delete: Zmazať name: Názov new: Nová aplikácia - scopes: Rozsahy + scopes: Oprávnenia show: Ukázať title: Vaše aplikácie new: @@ -49,7 +49,7 @@ sk: actions: Akcie application_id: Kľúč klienta callback_urls: Návratové URL adresy - scopes: Rozsahy + scopes: Oprávnenia secret: Tajné slovo klienta title: 'Aplikácia: %{name}' authorizations: @@ -73,7 +73,7 @@ sk: application: Aplikácia created_at: Autorizované date_format: "%Y-%m-%d %H:%M:%S" - scopes: Rozsahy + scopes: Oprávnenia title: Vaše autorizované aplikácie errors: messages: @@ -115,5 +115,6 @@ sk: title: Požadovaná OAuth autorizácia scopes: follow: sledovať, blokovať, povoliť a zušiť sledovanie účtov + push: dostávaj oznámenia ohľadom tvojho účtu ako notifikácie na plochu read: prezrieť dáta na vašom účete write: poslať vo vašom mene diff --git a/config/locales/doorkeeper.sl.yml b/config/locales/doorkeeper.sl.yml new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/config/locales/doorkeeper.sl.yml @@ -0,0 +1 @@ +{} diff --git a/config/locales/doorkeeper.zh-CN.yml b/config/locales/doorkeeper.zh-CN.yml index 90c484369c..3c7dd99be4 100644 --- a/config/locales/doorkeeper.zh-CN.yml +++ b/config/locales/doorkeeper.zh-CN.yml @@ -115,5 +115,6 @@ zh-CN: title: 需要 OAuth 认证 scopes: follow: 关注或屏蔽用户 + push: 接收你的帐户的推送通知 read: 读取你的帐户数据 write: 为你发表嘟文 diff --git a/config/locales/doorkeeper.zh-HK.yml b/config/locales/doorkeeper.zh-HK.yml index 6eddcc27bf..19ed76d1a5 100644 --- a/config/locales/doorkeeper.zh-HK.yml +++ b/config/locales/doorkeeper.zh-HK.yml @@ -115,5 +115,6 @@ zh-HK: title: 需要 OAuth 授權 scopes: follow: 關注、封鎖、解除封鎖及取消關注用戶 + push: 接收你的帳號的推送通知 read: 閱讀你的用戶資料 write: 以你的名義發佈文章 diff --git a/config/locales/el.yml b/config/locales/el.yml index 8741635e1d..77b794dc86 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -1,12 +1,15 @@ --- el: about: + about_hashtag_html: Αυτά είναι δημόσια τουτ σημειωμένα με #%{hashtag}. Μπορείς να αλληλεπιδράσεις με αυτά αν έχεις λογαριασμό οπουδήποτε στο fediverse. about_mastodon_html: Το Mastodon είναι ένα κοινωνικό δίκτυο που βασίζεται σε ανοιχτά δικτυακά πρωτόκολλα και ελεύθερο λογισμικό ανοιχτού κώδικα. Είναι αποκεντρωμένο όπως το e-mail. about_this: Σχετικά administered_by: 'Διαχειρίζεται από:' - closed_registrations: Αυτή τη στιγμή οι εγγραφές σε αυτό τον διακομιστή είναι κλειστές. Αλλά! Μπορείς να βρεις έναν άλλο διακομιστή για να ανοίξεις λογαριασμό και να έχεις πρόσβαση από εκεί στο ίδιο ακριβώς δίκτυο. + closed_registrations: Αυτή τη στιγμή οι εγγραφές σε αυτό τον κόμβο είναι κλειστές. Αλλά! Μπορείς να βρεις έναν άλλο κόμβο για να ανοίξεις λογαριασμό και να έχεις πρόσβαση από εκεί στο ίδιο ακριβώς δίκτυο. contact: Επικοινωνία contact_missing: Δεν έχει οριστεί + contact_unavailable: Μ/Δ + description_headline: Τι είναι το %{domain}; domain_count_after: άλλοι διακομιστές domain_count_before: Συνδέεται με extended_description_html: | @@ -15,17 +18,21 @@ el: features: humane_approach_body: Μαθαίνοντας από τις αποτυχίες άλλων δικτύων, το Mastodon στοχεύει να κάνει σχεδιαστικά ηθικές επιλογές για να καταπολεμήσει την κακόβουλη χρήση των κοινωνικών δικτύων. humane_approach_title: Μια πιο ανθρώπινη προσέγγιση - not_a_product_title: Είσαι άτομο, όχι προϊόν + not_a_product_body: Το Mastodon δεν είναι εμπορικό δίκτυο. Δεν έχει διαφημίσεις, δεν έχει εξόρυξη δεδομένων, δεν έχει φραγμένες περιοχές. Δεν υπάρχει κεντρικό σημείο ελέγχου. + not_a_product_title: Είσαι άνθρωπος, όχι προϊόν + real_conversation_body: Με 500 χαρακτήρες στη διάθεσή σου και υποστήριξη για λεπτομερή έλεγχο και προειδοποιήσεις πολυμέσων, μπορείς να εκφραστείς με τον τρόπο που θέλεις. real_conversation_title: Φτιαγμένο για αληθινή συζήτηση within_reach_body: Οι πολλαπλές εφαρμογές για το iOS, το Android και τις υπόλοιπες πλατφόρμες, χάρη σε ένα φιλικό προς τους προγραμματιστές οικοσύστημα API, σου επιτρέπουν να κρατάς επαφή με τους φίλους και τις φίλες σου οπουδήποτε. + within_reach_title: Πάντα προσβάσιμο generic_description: "%{domain} είναι ένας εξυπηρετητής στο δίκτυο" hosted_on: Το Mastodon φιλοξενείται στο %{domain} learn_more: Μάθε περισσότερα - other_instances: Λίστα διακομιστών + other_instances: Λίστα κόμβων source_code: Πηγαίος κώδικας status_count_after: καταστάσεις status_count_before: Ποιός συνέγραψε user_count_after: χρήστες + user_count_before: Σπίτι what_is_mastodon: Τι είναι το Mastodon; accounts: follow: Ακολούθησε @@ -33,8 +40,294 @@ el: following: Ακολουθεί media: Πολυμέσα moved_html: 'Ο/Η %{name} μετακόμισε στο %{new_profile_link}:' + network_hidden: Αυτή η πληροφορία δεν είναι διαθέσιμη nothing_here: Δεν υπάρχει τίποτα εδώ! people_followed_by: Χρήστες που ακολουθεί ο/η %{name} people_who_follow: Χρήστες που ακολουθούν τον/την %{name} posts: Τουτ posts_with_replies: Τουτ και απαντήσεις + remote_follow: Απομακρυσμένη παρακολούθηση + reserved_username: Το όνομα χρήστη είναι κατειλημμένο + roles: + admin: Διαχειριστής + bot: Μποτ (αυτόματος λογαριασμός) + moderator: Μεσολαβητής + unfollow: Διακοπή παρακολούθησης + admin: + account_moderation_notes: + create: Άφησε σημείωση + created_msg: Επιτυχής δημιουργία σημειώματος μεσολάβησης! + delete: Διαγραφή + destroyed_msg: Επιτυχής καταστροφή σημειώματος μεσολάβησης! + accounts: + are_you_sure: Σίγουρα; + avatar: Αβατάρ + by_domain: Τομέας + change_email: + changed_msg: Επιτυχής αλλαγή email λογαριασμού! + current_email: Τρέχον email + label: Αλλαγή email + new_email: Νέο email + submit: Αλλαγή email + title: Αλλαγή email για %{username} + confirm: Επιβεβαίωση + confirmed: Επιβεβαιώθηκε + confirming: Προς επιβεβαίωση + demote: Υποβίβαση + disable: Απενεργοποίηση + disable_two_factor_authentication: Απενεργοποίηση 2FA + disabled: Απενεργοποιημένο + display_name: Όνομα εμφάνισης + domain: Τομέας + edit: Αλλαγή + email: Email + email_status: Κατάσταση email + enable: Ενεργοποίηση + enabled: Ενεργοποιημένο + feed_url: URL ροής + followers: Ακόλουθοι + followers_url: URL ακολούθων + follows: Ακολουθεί + inbox_url: URL εισερχομένων + ip: IP + location: + all: Όλα + local: Τοπικά + remote: Απομακρυσμένα + title: Τοποθεσία + login_status: Κατάσταση εισόδου + media_attachments: Συνημμένα πολυμέσα + memorialize: Μετατροπή σε νεκρολογία + moderation: + all: Όλα + silenced: Αποσιωπημένα + suspended: Σε αναστολή + title: Μεσολάβηση + moderation_notes: Σημειώσεις μεσολάβησης + most_recent_activity: Πιο πρόσφατη δραστηριότητα + most_recent_ip: Πιο πρόσφατη IP + not_subscribed: Άνευ εγγραφής + order: + alphabetic: Αλφαβητικά + most_recent: Πιο πρόσφατα + title: Ταξινόμηση + outbox_url: URL εξερχομένων + perform_full_suspension: Πλήρης αναστολή + profile_url: URL προφίλ + promote: Προβίβασε + protocol: Πρωτόκολλο + public: Δημόσιο + push_subscription_expires: Η εγγραφή PuSH λήγει + redownload: Ανανέωση αβατάρ + remove_avatar: Απομακρυσμένο αβατάρ + resend_confirmation: + already_confirmed: Ήδη επιβεβαιωμένος χρήστης + send: Επανάληψη αποστολής email επιβεβαίωσης + success: Το email επιβεβαίωσης στάλθηκε επιτυχώς! + reset: Επαναφορά + reset_password: Επαναφορά συνθηματικού + resubscribe: Επανεγγραφή + role: Δικαιώματα + roles: + admin: Διαχειριστής + moderator: Συντονιστής + staff: Προσωπικό + user: Χρήστης + salmon_url: URL Salmon + search: Αναζήτηση + shared_inbox_url: URL κοινόχρηστων εισερχομένων + show: + created_reports: Αναφορές από αυτόν το λογαριασμό + report: κατάγγειλε + targeted_reports: Αναφορές για αυτόν το λογαριασμό + silence: Αποσιώπησε + statuses: Καταστάσεις + subscribe: Εγγραφή + title: Λογαριασμοί + unconfirmed_email: Ανεπιβεβαίωτο email + undo_silenced: Αναίρεση αποσιώπησης + undo_suspension: Αναίρεση παύσης + unsubscribe: Κατάργηση εγγραφής + username: Όνομα χρήστη + web: Διαδίκτυο + action_logs: + actions: + assigned_to_self_report: Ο/Η %{name} ανάθεσε την καταγγελία %{target} στον εαυτό του/της + change_email_user: Ο/Η %{name} άλλαξε τη διεύθυνση email του χρήστη %{target} + confirm_user: Ο/Η %{name} επιβεβαίωσε τη διεύθυνση email του χρήστη %{target} + create_custom_emoji: Ο/Η %{name} ανέβασε νέο emoji %{target} + create_domain_block: Ο/Η %{name} μπλόκαρε τον τομέα %{target} + create_email_domain_block: Ο/Η %{name} έβαλε τον τομέα email %{target} σε μαύρη λίστα + demote_user: Ο/Η %{name} υποβίβασε το χρήστη %{target} + destroy_domain_block: Ο/Η %{name} ξεμπλόκαρε τον τομέα %{target} + destroy_email_domain_block: Ο/Η %{name} έβαλε τον τομέα email %{target} σε λευκή λίστα + destroy_status: Ο/Η %{name} αφαίρεσε την κατάσταση του/της %{target} + disable_2fa_user: Ο/Η %{name} απενεργοποίησε την απαίτηση δύο παραγόντων για το χρήστη %{target} + disable_custom_emoji: Ο/Η %{name} απενεργοποίησε το emoji %{target} + disable_user: Ο/Η %{name} απενεργοποίησε την είσοδο για το χρήστη %{target} + enable_custom_emoji: Ο/Η %{name} ενεργοποίησε το emoji %{target} + enable_user: Ο/Η %{name} ενεργοποίησε την είσοδο του χρήστη %{target} + memorialize_account: Ο/Η %{name} μετέτρεψε το λογαριασμό του/της %{target} σε σελίδα νεκρολογίας + promote_user: Ο/Η %{name} προβίβασε το χρήστη %{target} + remove_avatar_user: Ο/Η %{name} αφαίρεσε το αβατάρ του/της %{target} + reopen_report: Ο/Η %{name} ξανάνοιξε την καταγγελία %{target} + reset_password_user: Ο/Η %{name} επανέφερε το συνθηματικό του χρήστη %{target} + resolve_report: Ο/Η %{name} επέλυσε την καταγγελία %{target} + silence_account: Ο/Η %{name} αποσιώπησε το λογαριασμό του/της %{target} + suspend_account: Ο/Η %{name} έπαυσε το λογαριασμό του/της %{target} + unassigned_report: Ο/Η %{name} αποδέσμευσε την καταγγελία %{target} + unsilence_account: Ο/Η %{name} ήρε την αποσιώπηση του λογαριασμού του/της %{target} + unsuspend_account: Ο/Η %{name} ήρε την παύση του λογαριασμού του χρήστη %{target} + update_custom_emoji: Ο/Η %{name} ενημέρωσε το emoji %{target} + update_status: Ο/Η %{name} ενημέρωσε την κατάσταση του/της %{target} + title: Αρχείο ελέγχου + custom_emojis: + by_domain: Τομέας + copied_msg: Επιτυχής δημιουργία τοπικού αντίγραφου του emoji + copy: Αντιγραφή + copy_failed_msg: Αδυναμία δημιουργίας τοπικού αντίγραφου αυτού του emoji + created_msg: Επιτυχής δημιουργία του emoji! + delete: Διαγραφή + destroyed_msg: Επιτυχής καταστροφή του emojo! + disable: Απενεργοποίηση + disabled_msg: Επιτυχής απενεργοποίηση αυτού του emoji + emoji: Emoji + enable: Ενεργοποίηση + enabled_msg: Επιτυχής ενεργοποίηση αυτού του emoji + image_hint: PNG έως 50KB + listed: Αναφερθέντα + new: + title: Προσθήκη νέου προσαρμοσμένου emoji + overwrite: Αντικατάσταση + shortcode: Σύντομος κωδικός + shortcode_hint: Τουλάχιστον 2 χαρακτήρες, μόνο αλφαριθμητικοί και κάτω παύλες + title: Προσαρμοσμένα emoji + unlisted: Μη καταχωρημένα + update_failed_msg: Αδυναμία ενημέρωσης του emoji + updated_msg: Επιτυχής ενημέρωση του Emoji! + upload: Ανέβασμα + domain_blocks: + add_new: Προσθήκη νέου + created_msg: Ο αποκλεισμός τομέα είναι υπό επεξεργασία + destroyed_msg: Ο αποκλεισμός τομέα άρθηκε + domain: Τομέας + new: + create: Δημιουργία αποκλεισμού + hint: Ο αποκλεισμός τομέα δεν θα αποτρέψει νέες καταχωρίσεις λογαριασμών στην βάση δεδομένων, αλλά θα εφαρμόσει αναδρομικά και αυτόματα συγκεκριμένες πολιτικές μεσολάβησης σε αυτούς τους λογαριασμούς. + severity: + noop: Κανένα + silence: Σίγαση + suspend: Αναστολή + title: Αποκλεισμός νέου τομέα + reject_media: Απόρριψη πολυμέσων + severities: + noop: Κανένα + silence: Αποσιώπηση + suspend: Αναστολή + severity: Αυστηρότητα + show: + affected_accounts: + one: Επηρεάζεται ένας λογαριασμός στη βάση δεδομένων + other: Επηρεάζονται %{count} λογαριασμοί στη βάση δεδομένων + retroactive: + silence: Αναίρεση αποσιώπησης όλων των λογαριασμός του τομέα + suspend: Αναίρεση αναστολής όλων των λογαριασμών του τομέα + title: Αναίρεση αποκλεισμού για τον τομέα %{domain} + undo: Αναίρεση + title: Αποκλεισμένοι τομείς + undo: Αναίρεση + email_domain_blocks: + add_new: Πρόσθεση νέου + created_msg: Επιτυχής πρόσθεση email τομέα σε μαύρη λίστα + delete: Διαγραφή + destroyed_msg: Επιτυχής διαγραφή email τομέα από τη μαύρη λίστα + domain: Τομέας + new: + create: Πρόσθεση τομέα + title: Νέα εγγραφή email στη μαύρη λίστα + title: Μαύρη λίστα email + instances: + account_count: Γνωστοί λογαριασμοί + domain_name: Τομέας + reset: Επαναφορά + search: Αναζήτηση + title: Γνωστοί κόμβοι + invites: + filter: + all: Όλες + available: Διαθέσιμες + expired: Ληγμένες + title: Φίλτρο + title: Προσκλήσεις + report_notes: + created_msg: Επιτυχής δημιουργία σημείωσης καταγγελίας! + destroyed_msg: Επιτυχής διαγραφή σημείωσης καταγγελίας! + reports: + account: + note: σημείωση + report: καταγγελία + action_taken_by: Ενέργεια από τον/την + are_you_sure: Σίγουρα; + assign_to_self: Ανάθεση σε μένα + assigned: Αρμόδιος συντονιστής + comment: + none: Κανένα + created_at: Αναφέρθηκε + id: ID + mark_as_resolved: Σημειωμένο ως επιλυμένο + mark_as_unresolved: Σημειωμένο ως ανεπίλυτο + notes: + create: Πρόσθεσε σημείωση + create_and_resolve: Επίλυσε με σημείωση + create_and_unresolve: Ξανάνοιξε με σημείωση + delete: Διέγραψε + placeholder: Περιέγραψε τις ενέργειες που έγιναν, ή οποιαδήποτε άλλη ενημέρωση... + reopen: Ξανάνοιξε την καταγγελία + report: 'Καταγγελία #%{id}' + report_contents: Περιεχόμενα + reported_account: Αναφερόμενος λογαριασμός + reported_by: Αναφέρθηκε από + resolved: Επιλύθηκε + resolved_msg: Η καταγγελία επιλύθηκε επιτυχώς! + silence_account: Αποσιώπηση λογαριασμού + status: Κατάσταση + suspend_account: Ανέστειλε λογαριασμό + target: Στόχος + title: Αναφορές + settings: + hero: + desc_html: Εμφανίζεται στην μπροστινή σελίδα. Συνίσταται τουλάχιστον 600x100px. Όταν λείπει, χρησιμοποιείται η μικρογραφία του κόμβου + peers_api_enabled: + desc_html: Ονόματα τομέων που αυτός ο κόμβος έχει πετύχει στο fediverse + show_known_fediverse_at_about_page: + title: Εμφάνιση του γνωστού fediverse στην προεπισκόπηση ροής + site_description: + title: Περιγραφή κόμβου + site_description_extended: + desc_html: Ένα καλό μέρος για τον κώδικα δεοντολογίας, τους κανόνες, τις οδηγίες και ό,τι άλλο διαφοροποιεί τον κόμβο σου. Δέχεται και κώδικα HTML + site_title: Όνομα κόμβου + thumbnail: + title: Μικρογραφία κόμβου + timeline_preview: + desc_html: Εμφάνισε τη δημόσια ροή στην αρχική σελίδα + title: Προεπισκόπιση ροής + admin_mailer: + new_report: + subject: Νέα καταγγελία για %{instance} (#%{id}) + auth: + agreement_html: Με την εγγραφή σου, συμφωνείς να ακολουθείς τους κανόνες αυτού του κόμβου και τους όρους χρήσης του. + deletes: + warning_html: Μόνο η διαγραφή περιεχομένου από αυτό τον συγκεκριμένο κόμβο είναι εγγυημένη. Το περιεχόμενο που έχει διαμοιραστεί ευρέως είναι πιθανό να αφήσει ίχνη. Όσοι διακομιστές είναι εκτός σύνδεσης και όσοι έχουν διακόψει τη λήψη των ενημερώσεων του κόμβου σου, δε θα ενημερώσουν τις βάσεις δεδομένων τους. + imports: + preface: Μπορείς να εισάγεις τα δεδομένα που έχεις εξάγει από άλλο κόμβο, όπως τη λίστα των ανθρώπων που ακολουθείς ή μπλοκάρεις. + invites: + prompt: Φτιάξε και μοίρασε συνδέσμους με τρίτους για να δώσεις πρόσβαση σε αυτόν τον κόμβο + terms: + title: Όροι Χρήσης και Πολιτική Απορρήτου του κόμβου %{instance} + user_mailer: + welcome: + final_step: 'Ξεκίνα τις δημοσιεύσεις! Ακόμα και χωρίς ακόλουθους τα δημόσια μηνύματά σου μπορεί να τα δουν άλλοι, για παράδειγμα στην τοπική ροή και στις ετικέτες. Ίσως να θέλεις να κάνεις μια εισαγωγή του εαυτού σου με την ετικέτα #introductions.' + full_handle_hint: Αυτό θα εδώ θα πεις στους φίλους σου για να σου μιλήσουν ή να σε ακολουθήσουν από άλλο κόμβο. + tip_federated_timeline: Η ομοσπονδιακή ροή είναι μια όψη πραγματικού χρόνου στο δίκτυο του Mastodon. Παρόλα αυτά, περιλαμβάνει μόνο όσους ακολουθούν οι γείτονές σου, άρα δεν είναι πλήρης. + tip_following: Ακολουθείς το διαχειριστή του διακομιστή σου αυτόματα. Για να βρεις περισσότερους ενδιαφέροντες ανθρώπους, έλεγξε την τοπική και την ομοσπονδιακή ροή. + tip_local_timeline: Η τοπική ροή είναι η όψη πραγματικού χρόνου των ανθρώπων στον κόμβο %{instance}. Αυτοί είναι οι άμεσοι γείτονές σου! diff --git a/config/locales/en.yml b/config/locales/en.yml index 8e1b916c17..3c2a8c3db7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -40,6 +40,7 @@ en: following: Following media: Media moved_html: "%{name} has moved to %{new_profile_link}:" + network_hidden: This information is not available nothing_here: There is nothing here! people_followed_by: People whom %{name} follows people_who_follow: People who follow %{name} @@ -49,13 +50,12 @@ en: reserved_username: The username is reserved roles: admin: Admin + bot: Bot moderator: Mod unfollow: Unfollow admin: account_moderation_notes: - account: Moderator - create: Create - created_at: Date + create: Leave note created_msg: Moderation note successfully created! delete: Delete destroyed_msg: Moderation note successfully destroyed! @@ -72,6 +72,7 @@ en: title: Change Email for %{username} confirm: Confirm confirmed: Confirmed + confirming: Confirming demote: Demote disable: Disable disable_two_factor_authentication: Disable 2FA @@ -80,6 +81,7 @@ en: domain: Domain edit: Edit email: E-mail + email_status: E-mail Status enable: Enable enabled: Enabled feed_url: Feed URL @@ -118,6 +120,10 @@ en: push_subscription_expires: PuSH subscription expires redownload: Refresh avatar remove_avatar: Remove avatar + resend_confirmation: + already_confirmed: This user is already confirmed + send: Resend confirmation email + success: Confirmation email successfully sent! reset: Reset reset_password: Reset password resubscribe: Resubscribe @@ -269,7 +275,6 @@ en: comment: none: None created_at: Reported - delete: Delete id: ID mark_as_resolved: Mark as resolved mark_as_unresolved: Mark as unresolved @@ -278,10 +283,7 @@ en: create_and_resolve: Resolve with note create_and_unresolve: Reopen with note delete: Delete - placeholder: Describe what actions have been taken, or any other updates to this report… - nsfw: - 'false': Unhide media attachments - 'true': Hide media attachments + placeholder: Describe what actions have been taken, or any other related updates... reopen: Reopen report report: 'Report #%{id}' report_contents: Contents @@ -356,11 +358,8 @@ en: delete: Delete nsfw_off: Mark as not sensitive nsfw_on: Mark as sensitive - execute: Execute failed_to_execute: Failed to execute media: - hide: Hide media - show: Show media title: Media no_media: No media title: Account statuses @@ -466,7 +465,7 @@ en: archive_takeout: date: Date download: Download your archive - hint_html: You can request an archive of your toots and uploaded media. The exported data will be in ActivityPub format, readable by any compliant software. + hint_html: You can request an archive of your toots and uploaded media. The exported data will be in ActivityPub format, readable by any compliant software. You can request an archive every 7 days. in_progress: Compiling your archive... request: Request your archive size: Size @@ -560,7 +559,7 @@ en: subject: one: "1 new notification since your last visit \U0001F418" other: "%{count} new notifications since your last visit \U0001F418" - title: In your absence… + title: In your absence... favourite: body: 'Your status was favourited by %{name}:' subject: "%{name} favourited your status" @@ -605,20 +604,6 @@ en: other: Other publishing: Publishing web: Web - push_notifications: - favourite: - title: "%{name} favourited your status" - follow: - title: "%{name} is now following you" - group: - title: "%{count} notifications" - mention: - action_boost: Boost - action_expand: Show more - action_favourite: Favourite - title: "%{name} mentioned you" - reblog: - title: "%{name} boosted your status" remote_follow: acct: Enter your username@domain you want to follow from missing_resource: Could not find the required redirect URL for your account @@ -695,6 +680,7 @@ en: video: one: "%{count} video" other: "%{count} videos" + boosted_from_html: Boosted from %{acct_link} content_warning: 'Content warning: %{warning}' disallowed_hashtags: one: 'contained a disallowed hashtag: %{tags}' @@ -785,9 +771,13 @@ en: - Children's Online Privacy Protection Act Compliance + Site usage by children - Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site. + If this server is in the EU or the EEA: Our site, products and services are all directed to people who are at least 16 years old. If you are under the age of 16, per the requirements of the GDPR (General Data Protection Regulation) do not use this site. + + If this server is in the USA: Our site, products and services are all directed to people who are at least 13 years old. If you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site. + + Law requirements can be different if this server is in another jurisdiction. @@ -799,6 +789,10 @@ en: Originally adapted from the Discourse privacy policy. title: "%{instance} Terms of Service and Privacy Policy" + themes: + contrast: High contrast + default: Mastodon + mastodon-light: Mastodon (light) time: formats: default: "%b %d, %Y, %H:%M" @@ -844,5 +838,6 @@ en: users: invalid_email: The e-mail address is invalid invalid_otp_token: Invalid two-factor code + otp_lost_help_html: If you lost access to both, you may get in touch with %{email} seamless_external_login: You are logged in via an external service, so password and e-mail settings are not available. signed_in_as: 'Signed in as:' diff --git a/config/locales/eo.yml b/config/locales/eo.yml index c768d8a03d..f1a9ff79d4 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -4,6 +4,7 @@ eo: about_hashtag_html: Ĉi tiuj estas la publikaj mesaĝoj markitaj per #%{hashtag}. Vi povas interagi kun ili se vi havas konton ie ajn en la fediverse. about_mastodon_html: Mastodon estas socia reto bazita sur malfermitaj retaj protokoloj kaj sur libera malfermitkoda programo. Ĝi estas sencentra kiel retmesaĝoj. about_this: Pri + administered_by: 'Administrata de:' closed_registrations: Registriĝoj estas nuntempe fermitaj en ĉi tiu nodo. Tamen, vi povas trovi alian nodon por fari konton kaj aliri al la sama reto de tie. contact: Kontakti contact_missing: Ne elektita @@ -39,6 +40,7 @@ eo: following: Sekvatoj media: Aŭdovidaĵoj moved_html: "%{name} moviĝis al %{new_profile_link}:" + network_hidden: Tiu informo ne estas disponebla nothing_here: Estas nenio ĉi tie! people_followed_by: Sekvatoj de %{name} people_who_follow: Sekvantoj de %{name} @@ -48,21 +50,29 @@ eo: reserved_username: La uzantnomo estas rezervita roles: admin: Administranto + bot: Roboto moderator: Kontrolanto unfollow: Ne plu sekvi admin: account_moderation_notes: - account: Kontrolanto - create: Krei - created_at: Dato + create: Lasi noton created_msg: Kontrola noto sukcese kreita! delete: Forigi destroyed_msg: Kontrola noto sukcese detruita! accounts: are_you_sure: Ĉu vi certas? + avatar: Profilbildo by_domain: Domajno + change_email: + changed_msg: Konta retadreso sukcese ŝanĝita! + current_email: Nuna retadreso + label: Ŝanĝi retadreson + new_email: Nova retadreso + submit: Ŝanĝi retadreson + title: Ŝanĝi retadreson por %{username} confirm: Konfirmi confirmed: Konfirmita + confirming: Konfirmante demote: Degradi disable: Malebligi disable_two_factor_authentication: Malebligi 2FA @@ -71,6 +81,7 @@ eo: domain: Domajno edit: Redakti email: Retpoŝto + email_status: Retpoŝto Stato enable: Ebligi enabled: Ebligita feed_url: URL de la fluo @@ -108,6 +119,11 @@ eo: public: Publika push_subscription_expires: Eksvalidiĝo de la abono al PuSH redownload: Aktualigi profilbildon + remove_avatar: Forigi profilbildon + resend_confirmation: + already_confirmed: Ĉi tiu uzanto jam estas konfirmita + send: Esend konfirmi retpoŝton + success: Konfirma retmesaĝo sukcese sendita! reset: Restarigi reset_password: Restarigi pasvorton resubscribe: Reaboni @@ -128,6 +144,7 @@ eo: statuses: Mesaĝoj subscribe: Aboni title: Kontoj + unconfirmed_email: Nekonfirmita retadreso undo_silenced: Malfari kaŝon undo_suspension: Malfari haltigon unsubscribe: Malaboni @@ -135,6 +152,8 @@ eo: web: Reto action_logs: actions: + assigned_to_self_report: "%{name} asignis signalon %{target} al si mem" + change_email_user: "%{name} ŝanĝis retadreson de uzanto %{target}" confirm_user: "%{name} konfirmis retadreson de uzanto %{target}" create_custom_emoji: "%{name} alŝutis novan emoĝion %{target}" create_domain_block: "%{name} blokis domajnon %{target}" @@ -150,10 +169,13 @@ eo: enable_user: "%{name} ebligis ensaluton por uzanto %{target}" memorialize_account: "%{name} ŝanĝis la konton de %{target} al memora paĝo" promote_user: "%{name} plirangigis uzanton %{target}" + remove_avatar_user: "%{name} forigis profilbildon de %{target}" + reopen_report: "%{name} remalfermis signalon %{target}" reset_password_user: "%{name} restarigis pasvorton de uzanto %{target}" - resolve_report: "%{name} flankmetis signalon %{target}" + resolve_report: "%{name} solvis signalon %{target}" silence_account: "%{name} kaŝis la konton de %{target}" suspend_account: "%{name} haltigis la konton de %{target}" + unassigned_report: "%{name} malasignis signalon %{target}" unsilence_account: "%{name} malkaŝis la konton de %{target}" unsuspend_account: "%{name} malhaltigis la konton de %{target}" update_custom_emoji: "%{name} ĝisdatigis emoĝion %{target}" @@ -239,28 +261,44 @@ eo: expired: Eksvalida title: Filtri title: Invitoj + report_notes: + created_msg: Signala noto sukcese kreita! + destroyed_msg: Signala noto sukcese forigita! reports: + account: + note: noto + report: signalo action_taken_by: Ago farita de are_you_sure: Ĉu vi certas? + assign_to_self: Asigni al mi + assigned: Asignita kontrolanto comment: none: Nenio - delete: Forigi + created_at: Signalita id: ID - mark_as_resolved: Marki kiel solvita - nsfw: - 'false': Malkaŝi aŭdovidajn kunsendaĵojn - 'true': Kaŝi aŭdovidajn kunsendaĵojn + mark_as_resolved: Marki solvita + mark_as_unresolved: Marki nesolvita + notes: + create: Aldoni noton + create_and_resolve: Solvi per noto + create_and_unresolve: Remalfermi per noto + delete: Forigi + placeholder: Priskribu faritajn agojn, aŭ ajnan novan informon pri tiu signalo… + reopen: Remalfermi signalon report: 'Signalo #%{id}' report_contents: Enhavo reported_account: Signalita konto reported_by: Signalita de resolved: Solvita + resolved_msg: Signalo sukcese solvita! silence_account: Kaŝi konton status: Mesaĝoj suspend_account: Haltigi konton target: Celo title: Signaloj + unassign: Malasigni unresolved: Nesolvita + updated_at: Ĝisdatigita view: Vidi settings: activity_api_enabled: @@ -318,13 +356,10 @@ eo: back_to_account: Reveni al konta paĝo batch: delete: Forigi - nsfw_off: Malŝalti NSFW - nsfw_on: Ŝalti NSFW - execute: Ekigi + nsfw_off: Marki ne tikla + nsfw_on: Marki tikla failed_to_execute: Ekigo malsukcesa media: - hide: Kaŝi aŭdovidaĵojn - show: Montri aŭdovidaĵojn title: Aŭdovidaĵoj no_media: Neniu aŭdovidaĵo title: Mesaĝoj de la konto @@ -340,6 +375,7 @@ eo: admin_mailer: new_report: body: "%{reporter} signalis %{target}" + body_remote: Iu de %{domain} signalis %{target} subject: Nova signalo por %{instance} (#%{id}) application_mailer: notification_preferences: Ŝanĝi retmesaĝajn preferojn @@ -381,6 +417,7 @@ eo: security: Sekureco set_new_password: Elekti novan pasvorton authorize_follow: + already_following: Vi jam sekvas tiun konton error: Bedaŭrinde, estis eraro en la serĉado de la fora konto follow: Sekvi follow_request: 'Vi sendis peton de sekvado al:' @@ -430,7 +467,7 @@ eo: archive_takeout: date: Dato download: Elŝuti vian arkivon - hint_html: Vi povas peti arkivon de viaj mesaĝoj kaj alŝutitaj aŭdovidaĵoj. La eksportitaj datumoj estos en la formato ActivityPub, legebla de ajna konformema programo. + hint_html: Vi povas peti arkivon de viaj mesaĝoj kaj alŝutitaj aŭdovidaĵoj. La eksportitaj datumoj estos en la formato ActivityPub, legebla de ajna konformema programo. Vi povas peti arkivon ĉiuseptage. in_progress: Kunmetado de via arkivo… request: Peti vian arkivon size: Grandeco @@ -475,6 +512,7 @@ eo: '21600': 6 horoj '3600': 1 horo '43200': 12 horoj + '604800': 1 semajno '86400': 1 tago expires_in_prompt: Neniam generate: Krei @@ -559,25 +597,15 @@ eo: other: Aliaj aferoj publishing: Publikado web: Reto - push_notifications: - favourite: - title: "%{name} stelumis vian mesaĝon" - follow: - title: "%{name} eksekvis vin" - group: - title: "%{count} sciigoj" - mention: - action_boost: Diskonigi - action_expand: Montri pli - action_favourite: Stelumi - title: "%{name} menciis vin" - reblog: - title: "%{name} diskonigis vian mesaĝon" remote_follow: acct: Enmetu vian uzantnomo@domajno de kie vi volas sekvi missing_resource: La URL de plusendado ne estis trovita proceed: Daŭrigi por eksekvi prompt: 'Vi eksekvos:' + remote_unfollow: + error: Eraro + title: Titolo + unfollowed: Ne plu sekvita sessions: activity: Lasta ago browser: Retumilo @@ -643,7 +671,11 @@ eo: video: one: "%{count} video" other: "%{count} videoj" + boosted_from_html: Diskonigita de %{acct_link} content_warning: 'Enhava averto: %{warning}' + disallowed_hashtags: + one: 'enhavas malpermesitan kradvorton: %{tags}' + other: 'enhavis malpermesitan kradvorton: %{tags}' open_in_web: Malfermi retumile over_character_limit: limo de %{max} signoj transpasita pin_errors: @@ -668,7 +700,9 @@ eo: terms: title: Uzkondiĉoj kaj privateca politiko de %{instance} themes: + contrast: Forta kontrasto default: Mastodon + mastodon-light: Mastodon (hela) time: formats: default: "%Y-%m-%d %H:%M" @@ -714,5 +748,6 @@ eo: users: invalid_email: La retadreso estas nevalida invalid_otp_token: Nevalida kodo de dufaktora aŭtentigo + otp_lost_help_html: Se vi perdas aliron al ambaŭ, vi povas kontakti %{email} seamless_external_login: Vi estas ensalutinta per ekstera servo, do pasvortaj kaj retadresaj agordoj ne estas disponeblaj. signed_in_as: 'Ensalutinta kiel:' diff --git a/config/locales/es.yml b/config/locales/es.yml index bf449bf92c..9a7cb62fef 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -53,9 +53,7 @@ es: unfollow: Dejar de seguir admin: account_moderation_notes: - account: Moderador create: Crear - created_at: Fecha created_msg: "¡Nota de moderación creada con éxito!" delete: Borrar destroyed_msg: "¡Nota de moderación destruida con éxito!" @@ -72,6 +70,7 @@ es: title: Cambiar el correo electrónico de %{username} confirm: Confirmar confirmed: Confirmado + confirming: Confirmando demote: Degradar disable: Deshabilitar disable_two_factor_authentication: Desactivar autenticación de dos factores @@ -80,6 +79,7 @@ es: domain: Dominio edit: Editar email: E-mail + email_status: E-mail Status enable: Habilitar enabled: Habilitada feed_url: URL de notificaciones @@ -118,6 +118,10 @@ es: push_subscription_expires: Expiración de la suscripción PuSH redownload: Refrescar avatar remove_avatar: Eliminar el avatar + resend_confirmation: + already_confirmed: Este usuario ya está confirmado + send: Reenviar el correo electrónico de confirmación + success: "¡Correo electrónico de confirmación enviado con éxito" reset: Reiniciar reset_password: Reiniciar contraseña resubscribe: Re-suscribir @@ -269,7 +273,6 @@ es: comment: none: Ninguno created_at: Denunciado - delete: Eliminar id: ID mark_as_resolved: Marcar como resuelto mark_as_unresolved: Marcar como no resuelto @@ -279,9 +282,6 @@ es: create_and_unresolve: Reabrir con una nota delete: Eliminar placeholder: Especificar qué acciones se han tomado o cualquier otra novedad respecto a esta denuncia… - nsfw: - 'false': Mostrar multimedia - 'true': Ocultar multimedia reopen: Reabrir denuncia report: 'Reportar #%{id}' report_contents: Contenido @@ -356,11 +356,8 @@ es: delete: Eliminar nsfw_off: Marcar contenido como no sensible nsfw_on: Marcar contenido como sensible - execute: Ejecutar failed_to_execute: Falló al ejecutar media: - hide: Ocultar multimedia - show: Mostrar multimedia title: Multimedia no_media: No hay multimedia title: Estado de las cuentas @@ -376,6 +373,7 @@ es: admin_mailer: new_report: body: "%{reporter} ha reportado a %{target}" + body_remote: Alguien de %{domain} a reportado a %{target} subject: Nuevo reporte para la %{instance} (#%{id}) application_mailer: notification_preferences: Cambiar preferencias de correo electrónico @@ -595,20 +593,6 @@ es: other: Otros publishing: Publicación web: Web - push_notifications: - favourite: - title: "%A {name} le gustó tu estado" - follow: - title: "%{name} te ha empezado a seguir" - group: - title: "%{count} notificaciones" - mention: - action_boost: Retoot - action_expand: Mostrar más - action_favourite: Me Gusta - title: "%{name} te mencionó" - reblog: - title: "%{name} boosteó tu estado" remote_follow: acct: Ingesa tu usuario@dominio desde el que quieres seguir missing_resource: No se pudo encontrar la URL de redirección requerida para tu cuenta diff --git a/config/locales/eu.yml b/config/locales/eu.yml index 0967ef424b..fc8916ab9d 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -1 +1,11 @@ -{} +--- +eu: + about: + about_this: Honi buruz + administered_by: 'Administratzailea(k):' + contact: Kontaktua + contact_missing: Ezarri gabe + contact_unavailable: E/E + description_headline: Zer da %{domain}? + domain_count_after: beste instantziak + domain_count_before: 'Hona konektatuta:' diff --git a/config/locales/fa.yml b/config/locales/fa.yml index a3005547a5..e320092899 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -4,6 +4,7 @@ fa: about_hashtag_html: اینها نوشتههای عمومی هستند که برچسب (هشتگ) #%{hashtag} را دارند. اگر شما روی هر سروری حساب داشته باشید میتوانید به این نوشتهها واکنش نشان دهید. about_mastodon_html: ماستدون (Mastodon) یک شبکهٔ اجتماعی است که بر اساس پروتکلهای آزاد وب و نرمافزارهای آزاد و کدباز ساخته شده است. این شبکه مانند ایمیل غیرمتمرکز است. about_this: درباره + administered_by: 'با مدیریت:' closed_registrations: ثبتنام روی این سرور هماینک فعال نیست. اما شما میتوانید سرور دیگری بیابید و با حسابی که آنجا میسازید دقیقاً به همین شبکه دسترسی داشته باشید. contact: تماس contact_missing: تعیین نشده @@ -39,6 +40,7 @@ fa: following: پی میگیرد media: عکس و ویدیو moved_html: "%{name} حساب خود را به %{new_profile_link} منتقل کرده است:" + network_hidden: این اطلاعات در دسترس نیست nothing_here: اینجا چیزی نیست! people_followed_by: کسانی که %{name} پی میگیرد people_who_follow: کسانی که %{name} را پی میگیرند @@ -48,21 +50,29 @@ fa: reserved_username: این نام کاربری در دسترس نیست roles: admin: مدیر + bot: ربات moderator: ناظم unfollow: پایان پیگیری admin: account_moderation_notes: - account: مدیر - create: نوشتن - created_at: تاریخ + create: افزودن یادداشت created_msg: یادداشت مدیر با موفقیت ساخته شد! delete: پاک کردن destroyed_msg: یادداشت مدیر با موفقیت پاک شد! accounts: are_you_sure: آیا مطمئن هستید؟ + avatar: تصویر نمایه by_domain: دامین + change_email: + changed_msg: نشانی ایمیل این حساب با موفقیت تغییر کرد! + current_email: ایمیل کنونی + label: تغییر نشانی ایمیل + new_email: ایمیل تازه + submit: تغییر ایمیل + title: تغییر ایمیل برای %{username} confirm: تأیید confirmed: تأیید شد + confirming: تأیید demote: تنزلدادن disable: غیرفعال disable_two_factor_authentication: غیرفعالسازی ورود دومرحلهای @@ -71,6 +81,7 @@ fa: domain: دامین edit: ویرایش email: ایمیل + email_status: وضعیت ایمیل enable: فعال enabled: فعال feed_url: نشانی فید @@ -91,7 +102,7 @@ fa: all: همه silenced: بیصدا شده suspended: معلق شده - title: مدیریت + title: وضعیت moderation_notes: یادداشت مدیر most_recent_activity: آخرین فعالیتها most_recent_ip: آخرین IP ها @@ -108,6 +119,11 @@ fa: public: عمومی push_subscription_expires: عضویت از راه PuSH منقضی شد redownload: بهروزرسانی تصویر نمایه + remove_avatar: حذف تصویر نمایه + resend_confirmation: + already_confirmed: این کاربر قبلا تایید شده است + send: ایمیل تایید را دوباره بفرستید + success: ایمیل تایید با موفقیت ارسال شد! reset: بازنشانی reset_password: بازنشانی رمز resubscribe: اشتراک دوباره @@ -128,6 +144,7 @@ fa: statuses: نوشتهها subscribe: اشتراک title: حسابها + unconfirmed_email: ایمیل تأییدنشده undo_silenced: واگردانی بیصداکردن undo_suspension: واگردانی تعلیق unsubscribe: لغو اشتراک @@ -135,6 +152,8 @@ fa: web: وب action_logs: actions: + assigned_to_self_report: "%{name} رسیدگی به گزارش %{target} را به عهده گرفت" + change_email_user: "%{name} نشانی ایمیل کاربر %{target} را تغییر داد" confirm_user: "%{name} نشانی ایمیل کاربر %{target} را تأیید کرد" create_custom_emoji: "%{name} شکلک تازهٔ %{target} را بارگذاشت" create_domain_block: "%{name} دامین %{target} را مسدود کرد" @@ -150,10 +169,13 @@ fa: enable_user: "%{name} ورود را برای کاربر %{target} فعال کرد" memorialize_account: "%{name} حساب کاربر %{target} را تبدیل به صفحهٔ یادمان کرد" promote_user: "%{name} کاربر %{target} را ترفیع داد" + remove_avatar_user: "%{name} تصویر نمایهٔ کاربر %{target} را حذف کرد" + reopen_report: "%{name} گزارش %{target} را دوباره به جریان انداخت" reset_password_user: "%{name} رمز کاربر %{target} را بازنشاند" - resolve_report: "%{name} گزارش %{target} را نادیده گرفت" + resolve_report: "%{name} گزارش %{target} را رفع کرد" silence_account: "%{name} حساب کاربر %{target} را خاموش (بیصدا) کرد" suspend_account: "%{name} حساب کاربر %{target} را تعلیق کرد" + unassigned_report: "%{name} بررسی گزارش %{target} را متوقف کرد" unsilence_account: "%{name} حساب کاربر %{target} را روشن (باصدا) کرد" unsuspend_account: "%{name} حساب کاربر %{target} را از تعلیق خارج کرد" update_custom_emoji: "%{name} شکلک %{target} را بهروز کرد" @@ -239,36 +261,61 @@ fa: expired: منقضیشده title: فیلتر title: دعوتها + report_notes: + created_msg: یادداشت گزارش با موفقیت ساخته شد! + destroyed_msg: یادداشت گزارش با موفقیت حذف شد! reports: + account: + note: یادداشت + report: گزارش action_taken_by: انجامدهنده are_you_sure: آیا مطمئن هستید؟ + assign_to_self: به عهدهٔ من بگذار + assigned: مدیر عهدهدار comment: none: خالی - delete: پاککردن + created_at: گزارششده id: شناسه mark_as_resolved: علامتگذاری به عنوان حلشده - nsfw: - 'false': نمایش پیوستهای تصویری - 'true': نهفتن پیوستهای تصویری + mark_as_unresolved: علامتگذاری به عنوان حلنشده + notes: + create: افزودن یادداشت + create_and_resolve: حل کردن با یادداشت + create_and_unresolve: دوباره گشودن با یادداشت + delete: حذف + placeholder: کارهایی را که در این باره انجام شده، یا هر بهروزرسانی دیگری را بنویسید... + reopen: دوباره به جریان بیندازید report: 'گزارش #%{id}' report_contents: محتوا reported_account: حساب گزارششده reported_by: گزارش از طرف resolved: حلشده + resolved_msg: گزارش با موفقیت حل شد! silence_account: بیصدا کردن حساب status: نوشته suspend_account: معلقکردن حساب target: هدف title: گزارشها + unassign: پسگرفتن مسئولیت unresolved: حلنشده + updated_at: بهروز شد view: نمایش settings: + activity_api_enabled: + desc_html: تعداد بوقهای محلی، کاربران فعال، و کاربران تازه در هر هفته + title: انتشار آمار تجمیعی دربارهٔ فعالیت کاربران bootstrap_timeline_accounts: desc_html: نامهای کاربری را با ویرگول از هم جدا کنید. تنها حسابهای محلی و قفلنشده کار میکنند. اگر اینجا را خالی بگذارید، به طور پیشفرض همهٔ مدیرهای این سرور پیگرفته خواهند شد. title: پیگیریهای پیشفرض برای کاربران تازه contact_information: email: ایمیل کاری username: نام کاربری + hero: + desc_html: در صفحهٔ آغازین نمایش مییابد. دستکم ۶۰۰×۱۰۰ پیکسل توصیه میشود. اگر تعیین نشود، با تصویر بندانگشتی سرور جایگزین خواهد شد + title: تصویر سربرگ + peers_api_enabled: + desc_html: دامینهایی که این سرور به آنها برخورده است + title: انتشار فهرست سرورهای یافتهشده registrations: closed_message: desc_html: وقتی امکان ثبت نام روی سرور فعال نباشد در صفحهٔ اصلی نمایش مییابدمیتوانید HTML بنویسید @@ -282,6 +329,9 @@ fa: open: desc_html: همه بتوانند حساب باز کنند title: امکان ثبت نام + show_known_fediverse_at_about_page: + desc_html: اگر انتخاب شود، بوقهای همهٔ سرورهای دیگر نیز در پیشنمایش این سرور نمایش مییابد. وگرنه فقط بوقهای محلی نشان داده میشوند. + title: نمایش سرورهای دیگر در پیشنمایش این سرور show_staff_badge: desc_html: نمایش علامت همکار روی صفحهٔ کاربر title: نمایش علامت همکار @@ -306,17 +356,14 @@ fa: back_to_account: بازگشت به صفحهٔ حساب batch: delete: پاککردن - nsfw_off: NSFW خاموش - nsfw_on: NSFW روشن - execute: اجرا + nsfw_off: علامتزدن به عنوان غیرحساس + nsfw_on: علامتزدن به عنوان حساس failed_to_execute: اجرا نشد media: - hide: نهفتن رسانه - show: نمایش رسانه title: رسانه - no_media: بدون رسانه + no_media: بدون عکس یا ویدیو title: نوشتههای حساب - with_media: دارای رسانه + with_media: دارای عکس یا ویدیو subscriptions: callback_url: نشانی Callback confirmed: تأییدشده @@ -324,15 +371,19 @@ fa: last_delivery: آخرین ارسال title: WebSub topic: موضوع - title: مدیریت + title: مدیریت سرور admin_mailer: new_report: body: کاربر %{reporter} کاربر %{target} را گزارش داد + body_remote: کسی از %{domain} گزارش %{target} را فرستاده subject: گزارش تازهای برای %{instance} (#%{id}) application_mailer: + notification_preferences: تغییر ترجیحات ایمیل salutation: "%{name}،" settings: 'تغییر تنظیمات ایمیل: %{link}' view: 'نمایش:' + view_profile: دیدن نمایه + view_status: دیدن نوشتهها applications: created: برنامه با موفقیت ساخته شد destroyed: برنامه با موفقیت پاک شد @@ -343,6 +394,8 @@ fa: your_token: کد دسترسی شما auth: agreement_html: پیش از عضو شدن باید قوانین این سرور و شرایط استفادهٔ ما را بپذیرید. + change_password: رمز + confirm_email: تأیید ایمیل delete_account: پاککردن حساب delete_account_html: اگر میخواهید حساب خود را پاک کنید، از اینجا پیش بروید. از شما درخواست تأیید خواهد شد. didnt_get_confirmation: راهنمایی برای تأیید را دریافت نکردید؟ @@ -352,12 +405,19 @@ fa: logout: خروج migrate_account: نقل مکان به یک حساب دیگر migrate_account_html: اگر میخواهید این حساب را به حساب دیگری منتقل کنید، اینجا را کلیک کنید. + or: یا + or_log_in_with: یا ورود به وسیلهٔ + providers: + cas: CAS + saml: SAML register: عضو شوید + register_elsewhere: ثبت نام روی یک سرور دیگر resend_confirmation: راهنمایی برای تأیید را دوباره بفرست reset_password: بازنشانی رمز security: امنیت set_new_password: تعیین رمز تازه authorize_follow: + already_following: شما همین الان هم این حساب را پیمیگیرید error: متأسفانه حین یافتن آن حساب خطایی رخ داد follow: پی بگیرید follow_request: 'شما درخواست پیگیری فرستادهاید به:' @@ -402,6 +462,13 @@ fa: title: این صفحه درست نیست noscript_html: برای استفاده از نسخهٔ تحت وب ماستدون، لطفاً جاوااسکریپت را فعال کنید. یا به جایش میتوانید یک اپ ماستدون را بهکار ببرید. exports: + archive_takeout: + date: تاریخ + download: بایگانی خود را باربگیرید + hint_html: شما میتوانید بایگانی نوشتهها و پروندههای بارگذاریشدهٔ خود را درخواست کنید. دادههای برونبریشده در قالب ActivityPub خواهند بود و همهٔ نرمافزارهای سازگار خواهند توانست آن را بخوانند. شما هر ۷ روز میتوانید یک بار برای چنین بایگانیای درخواست دهید. + in_progress: در حال ساختن بایگانی شما... + request: درخواست بایگانی دادههایتان + size: اندازه blocks: حسابهای مسدودشده csv: CSV follows: حسابهای پیگرفته @@ -443,6 +510,7 @@ fa: '21600': ۶ ساعت '3600': ۱ ساعت '43200': ۱۲ ساعت + '604800': ۱ هفته '86400': ۱ روز expires_in_prompt: هیچ وقت generate: ساختن @@ -470,32 +538,41 @@ fa: proceed: ذخیره updated_msg: تنظیمات نقل مکان حساب شما با موفقیت بهروز شد! moderation: - title: مدیریت + title: مدیریت کاربران notification_mailer: digest: - body: 'خلاصهای از آنچه از زمان آخرین بازدید شما در %{since} روی %{instance} رخ داد :' + action: دیدن همهٔ اعلانها + body: خلاصهای از پیغامهایی که از زمان آخرین بازدید شما در %{since} فرستاده شد mention: "%{name} اینجا از شما نام برد:" new_followers_summary: - one: شما یک پیگیر تازه دارید! ای ول! - other: شما %{count} پیگیر تازه دارید! چه عالی! + one: در ضمن، وقتی که نبودید یک پیگیر تازه پیدا کردید! ای ول! + other: در ضمن، وقتی که نبودید %{count} پیگیر تازه پیدا کردید! چه عالی! subject: one: "یک اعلان تازه از زمان آخرین بازدید شما \U0001F418" other: "%{count} اعلان تازه از زمان آخرین بازدید شما \U0001F418" + title: در مدتی که نبودید... favourite: body: "%{name} این نوشتهٔ شما را پسندید:" subject: "%{name} نوشتهٔ شما را پسندید" + title: پسندیدهشدن تازه follow: body: "%{name} هماینک پیگیر شماست!" subject: "%{name} هماینک پیگیر شماست" + title: پیگیر تازه follow_request: + action: مدیریت درخواستهای پیگیری body: "%{name} میخواهد پیگیر نوشتههای شما باشد" subject: 'منتظر پیگیری: %{name}' + title: درخواست پیگیری تازه mention: + action: پاسخ body: "%{name} در اینجا از شما نام برد:" subject: "%{name} از شما نام برد" + title: نامبردهشدن تازه reblog: body: "%{name} نوشتهٔ شما را بازبوقید:" subject: "%{name} نوشتهٔ شما را بازبوقید" + title: بازبوق تازه number: human: decimal_units: @@ -508,33 +585,25 @@ fa: trillion: T unit: '' pagination: + newer: تازهتر next: بعدی + older: قدیمیتر prev: قبلی truncate: "…" preferences: - languages: زبانها - other: سایر - publishing: انتشار + languages: تنظیمات زبان + other: سایر تنظیمات + publishing: تنظیمات انتشار مطالب web: وب - push_notifications: - favourite: - title: "%{name} نوشتهٔ شما را پسندید" - follow: - title: "%{name} هماینک پیگیر شماست" - group: - title: "%{count} اعلان" - mention: - action_boost: بازبوق - action_expand: نمایش بیشتر - action_favourite: پسندیدن - title: "%{name} از شما نام برد" - reblog: - title: "%{name} نوشتهٔ شما را بازبوقید" remote_follow: acct: نشانی حساب username@domain خود را اینجا بنویسید missing_resource: نشانی اینترنتی برای رسیدن به حساب شما پیدا نشد proceed: درخواست پیگیری prompt: 'شما قرار است این حساب را پیگیری کنید:' + remote_unfollow: + error: خطا + title: عنوان + unfollowed: پایان پیگیری sessions: activity: آخرین کنش browser: مرورگر @@ -543,12 +612,14 @@ fa: blackberry: Blackberry chrome: Chrome edge: Microsoft Edge + electron: Electron firefox: Firefox generic: مرورگر ناشناخته ie: Internet Explorer micro_messenger: MicroMessenger nokia: Nokia S40 Ovi Browser opera: Opera + otter: Otter phantom_js: PhantomJS qq: QQ Browser safari: Safari @@ -578,7 +649,7 @@ fa: authorized_apps: برنامههای مجاز back: بازگشت به ماستدون delete: پاککردن حساب - development: Development + development: فرابری edit_profile: ویرایش نمایه export: برونسپاری دادهها followers: پیگیران مورد تأیید @@ -590,6 +661,19 @@ fa: two_factor_authentication: ورود دومرحلهای your_apps: برنامهٔ شما statuses: + attached: + description: 'پیوستشده: %{attached}' + image: + one: "%{count} تصویر" + other: "%{count} تصویر" + video: + one: "%{count} ویدیو" + other: "%{count} ویدیو" + boosted_from_html: بازبوقیده از طرف %{acct_link} + content_warning: 'هشدا محتوا: %{warning}' + disallowed_hashtags: + one: 'دارای هشتگ غیرمجاز: %{tags}' + other: 'دارای هشتگهای غیرمجاز: %{tags}' open_in_web: بازکردن در وب over_character_limit: از حد مجاز %{max} حرف فراتر رفتید pin_errors: @@ -601,7 +685,7 @@ fa: title: '%{name}: "%{quote}"' visibilities: private: خصوصی - private_long: نمایش تنها به پیگیران + private_long: تنها پیگیران شما میبینند public: عمومی public_long: همه میتوانند ببینند unlisted: فهرستنشده @@ -614,7 +698,9 @@ fa: terms: title: شرایط استفاده و سیاست رازداری %{instance} themes: + contrast: کنتراست بالا default: ماستدون + mastodon-light: ماستدون (روشن) time: formats: default: "%d %b %Y, %H:%M" @@ -631,10 +717,35 @@ fa: manual_instructions: 'اگر نمیتوانید کدها را اسکن کنید و باید آنها را دستی وارد کنید، متن کد امنیتی اینجاست:' recovery_codes: پشتیبانگیری از کدهای بازیابی recovery_codes_regenerated: کدهای بازیابی با موفقیت ساخته شدند - recovery_instructions_html: اگر تلفن خود را گم کردید، میتوانید با یکی از کدهای بازیابی زیر کنترل حساب خود را به دست بگیرید. این کدها را در جای امنی نگه دارید. مثلاً آنها را چاپ کنید و کنار سایر مدارک مهم خود قرار دهید + recovery_instructions_html: اگر تلفن خود را گم کردید، میتوانید با یکی از کدهای بازیابی زیر کنترل حساب خود را به دست بگیرید. این کدها را در جای امنی نگه دارید. مثلاً آنها را چاپ کنید و کنار سایر مدارک مهم خود قرار دهید. setup: راه اندازی wrong_code: کدی که وارد کردید نامعتبر بود! آیا ساعت سرور و ساعت دستگاه شما درست تنظیم شدهاند؟ + user_mailer: + backup_ready: + explanation: شما یک نسخهٔ پشتیبان کامل از حساب خود را درخواست کردید. این پشتیبان الان آمادهٔ بارگیری است! + subject: بایگانی شما آمادهٔ دریافت است + title: گرفتن بایگانی + welcome: + edit_profile_action: تنظیم نمایه + edit_profile_step: 'شما میتوانید نمایهٔ خود را به دلخواه خود تغییر دهید: میتوانید تصویر نمایه، تصویر پسزمینه، نام، و چیزهای دیگری را تعیین کنید. اگر بخواهید، میتوانید حساب خود را خصوصی کنید تا فقط کسانی که شما اجازه میدهید بتوانند پیگیر حساب شما شوند.' + explanation: نکتههایی که برای آغاز کار به شما کمک میکنند + final_action: چیزی منتشر کنید + final_step: 'چیزی بنویسید! حتی اگر الان کسی پیگیر شما نباشد، دیگران نوشتههای عمومی شما را میبینند، مثلاً در فهرست نوشتههای محلی و در هشتگها. شاید بخواهید با هشتگ #آشنایی خودتان را معرفی کنید.' + full_handle: نام کاربری کامل شما + full_handle_hint: این چیزی است که باید به دوستان خود بگویید تا بتوانند به شما پیغام بفرستند یا از سرورهای دیگر پیگیر شما شوند. + review_preferences_action: تغییر ترجیحات + review_preferences_step: با رفتن به صفحهٔ ترجیحات میتوانید چیزهای گوناگونی را تنظیم کنید. مثلاً این که چه ایمیلهای آگاهسازیای به شما فرستاده شود، یا حریم خصوصی پیشفرض نوشتههایتان چه باشد. اگر بیماری سفر (حالت تهوع بر اثر دیدن اجسام متحرک) ندارید، میتوانید پخش خودکار ویدیوها را فعال کنید. + subject: به ماستدون خوش آمدید + tip_bridge_html: اگر پیش از این کاربر توییتر بودید، میتوانید دوستان توییتری خود را که در ماستدون هستند به کمک bridge app پیدا کنید. البته این فقط وقتی کار میکند که آنها هم این اپ را به کار برده باشند! + tip_federated_timeline: "«فهرست نوشتههای همهجا» نمایی از کل شبکهٔ بزرگ ماستدون به شما میدهد. البته این فهرست فقط افردای را نشان میدهد که همسروریهای شما آنها را پیگیری میکنند، و بنابراین ممکن است کامل نباشد." + tip_following: شما به طور پیشفرض مدیر(های) سرور خود را پی میگیرید. برای یافتن افراد جالب دیگر، فهرست «نوشتههای محلی» و «نوشتههای همهجا» را ببینید. + tip_local_timeline: فهرست نوشتههای محلی نمایی کلی از کاربران روی %{instance} را ارائه میدهد. اینها همسایههای شما هستند! + tip_mobile_webapp: اگر مرورگر موبایل شما امکان گذاشتن ماستدون روی صفحهٔ اصلی موبایل را به شما میدهد، این یعنی میتوانید اعلانهای خودکار ماستدون را دریافت کنید. با این کار ماستدون خیلی شبیه یک اپ معمولی موبایل میشود! + tips: نکتهها + title: خوش آمدید، کاربر %{name}! users: invalid_email: نشانی ایمیل نامعتبر است invalid_otp_token: کد ورود دومرحلهای نامعتبر است + otp_lost_help_html: اگر شما دسترسی به هیچکدامشان ندارید، باید با ایمیل %{email} تماس بگیرید + seamless_external_login: شما با یک سرویس خارج از مجموعه وارد شدهاید، به همین دلیل تنظیمات ایمیل و رمز برای شما در دسترس نیست. signed_in_as: 'واردشده به نام:' diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 550ad1805e..1e02efbd24 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -4,6 +4,7 @@ fi: about_hashtag_html: Nämä ovat hashtagilla #%{hashtag} merkittyjä julkisia tuuttauksia. Voit vastata niihin, jos sinulla on tili jossain päin fediversumia. about_mastodon_html: Mastodon on sosiaalinen verkosto. Se on toteutettu avoimilla verkkoprotokollilla ja vapailla, avoimen lähdekoodin ohjelmistoilla, ja se toimii hajautetusti samaan tapaan kuin sähköposti. about_this: Tietoja tästä palvelimesta + administered_by: 'Ylläpitäjä:' closed_registrations: Tähän instanssiin ei voi tällä hetkellä rekisteröityä. Voit kuitenkin luoda tilin johonkin toiseen instanssiin ja käyttää samaa verkostoa sitä kautta. contact: Ota yhteyttä contact_missing: Ei asetettu @@ -52,32 +53,40 @@ fi: unfollow: Lopeta seuraaminen admin: account_moderation_notes: - account: Moderaattori create: Luo - created_at: Päiväys created_msg: Moderointimerkinnän luonti onnistui! delete: Poista destroyed_msg: Moderointimerkinnän poisto onnistui! accounts: are_you_sure: Oletko varma? - by_domain: Verkko-osoite + avatar: Profiilikuva + by_domain: Verkkotunnus + change_email: + changed_msg: Tilin sähköposti vaihdettu onnistuneesti! + current_email: Nykyinen sähköposti + label: Vaihda sähköposti + new_email: Uusi sähköposti + submit: Vaihda sähköposti + title: Vaihda sähköposti käyttäjälle %{username} confirm: Vahvista confirmed: Vahvistettu + confirming: Vahvistetaan demote: Alenna disable: Poista käytöstä disable_two_factor_authentication: Poista 2FA käytöstä disabled: Poistettu käytöstä - display_name: Näyttönimi - domain: Verkko-osoite + display_name: Nimimerkki + domain: Verkkotunnus edit: Muokkaa email: Sähköposti + email_status: Sähköpostin tila enable: Ota käyttöön enabled: Käytössä - feed_url: Syötteen URL + feed_url: Syötteen osoite followers: Seuraajat - followers_url: Seuraajien URL + followers_url: Seuraajien osoite follows: Seuraa - inbox_url: Saapuvan postilaatikon URL + inbox_url: Saapuvan postilaatikon osoite ip: IP location: all: Kaikki @@ -100,14 +109,19 @@ fi: alphabetic: Aakkosjärjestys most_recent: Uusin title: Järjestys - outbox_url: Lähtevän postilaatikon URL + outbox_url: Lähtevän postilaatikon osoite perform_full_suspension: Siirrä kokonaan jäähylle - profile_url: Profiilin URL + profile_url: Profiilin osoite promote: Ylennä protocol: Protokolla public: Julkinen push_subscription_expires: PuSH-tilaus vanhenee redownload: Päivitä profiilikuva + remove_avatar: Poista profiilikuva + resend_confirmation: + already_confirmed: Tämä käyttäjä on jo vahvistettu + send: Lähetä varmistusviesti uudelleen + success: Vahvistusviesti onnistuneesti lähetetty! reset: Palauta reset_password: Palauta salasana resubscribe: Tilaa uudelleen @@ -118,16 +132,17 @@ fi: staff: Henkilöstö user: Käyttäjä salmon_url: Salmon-URL - search: Haku - shared_inbox_url: Jaetun saapuvan postilaatikon URL + search: Hae + shared_inbox_url: Jaetun saapuvan postilaatikon osoite show: - created_reports: Tilin luomat raportit + created_reports: Tämän tilin luomat raportit report: raportti targeted_reports: Tästä tilistä tehdyt raportit silence: Hiljennä statuses: Tilat subscribe: Tilaa title: Tilit + unconfirmed_email: Sähköpostia ei vahvistettu undo_silenced: Peru hiljennys undo_suspension: Peru jäähy unsubscribe: Lopeta tilaus @@ -135,6 +150,8 @@ fi: web: Web action_logs: actions: + assigned_to_self_report: "%{name} otti raportin %{target} tehtäväkseen" + change_email_user: "%{name} vaihtoi käyttäjän %{target} sähköpostiosoitteen" confirm_user: "%{name} vahvisti käyttäjän %{target} sähköpostiosoitteen" create_custom_emoji: "%{name} lähetti uuden emojin %{target}" create_domain_block: "%{name} esti verkkotunnuksen %{target}" @@ -150,6 +167,8 @@ fi: enable_user: "%{name} salli sisäänkirjautumisen käyttäjälle %{target}" memorialize_account: "%{name} muutti käyttäjän %{target} tilin muistosivuksi" promote_user: "%{name} ylensi käyttäjän %{target}" + remove_avatar_user: "%{name} poisti käyttäjän %{target} profiilikuvan" + reopen_report: "%{name} avasi uudelleen raportin %{target}" reset_password_user: "%{name} palautti käyttäjän %{target} salasanan" resolve_report: "%{name} hylkäsi raportin %{target}" silence_account: "%{name} hiljensi käyttäjän %{target}" @@ -168,7 +187,7 @@ fi: delete: Poista destroyed_msg: Emojon poisto onnistui! disable: Poista käytöstä - disabled_msg: Emojin käytöstäpoisto onnistui + disabled_msg: Emojin poisto käytöstä onnistui emoji: Emoji enable: Ota käyttöön enabled_msg: Emojin käyttöönotto onnistui @@ -239,28 +258,42 @@ fi: expired: Vanhentunut title: Suodata title: Kutsut + report_notes: + created_msg: Muistiinpano onnistuneesti lisätty raporttiin! + destroyed_msg: Muistiinpano onnistuneesti poistettu raportista! reports: + account: + note: muistiinpano + report: raportti action_taken_by: Toimenpiteen tekijä are_you_sure: Oletko varma? + assign_to_self: Ota tehtäväksi comment: none: Ei mitään - delete: Poista + created_at: Raportoitu id: Tunniste mark_as_resolved: Merkitse ratkaistuksi - nsfw: - 'false': Peru medialiitteiden piilotus - 'true': Piilota medialiitteet + mark_as_unresolved: Merkitse ratkaisemattomaksi + notes: + create: Lisää muistiinpano + create_and_resolve: Ratkaise ja lisää muistiinpano + create_and_unresolve: Avaa uudelleen ja lisää muistiinpano + delete: Poista + placeholder: Kuvaile mitä toimia on tehty tai muita päivityksiä tähän raporttiin… + reopen: Avaa raportti uudestaan report: Raportti nro %{id} report_contents: Sisältö reported_account: Raportoitu tili reported_by: Raportoija resolved: Ratkaistut + resolved_msg: Raportti onnistuneesti ratkaistu! silence_account: Hiljennä tili status: Tila suspend_account: Siirrä tili jäähylle target: Kohde title: Raportit unresolved: Ratkaisemattomat + updated_at: Päivitetty view: Näytä settings: activity_api_enabled: @@ -320,11 +353,8 @@ fi: delete: Poista nsfw_off: NSFW POIS nsfw_on: NSFW PÄÄLLÄ - execute: Suorita failed_to_execute: Suoritus epäonnistui media: - hide: Piilota media - show: Näytä media title: Media no_media: Ei mediaa title: Tilin tilat @@ -340,6 +370,7 @@ fi: admin_mailer: new_report: body: "%{reporter} on raportoinut kohteen %{target}" + body_remote: Joku osoitteesta %{domain} on raportoinut kohteen %{target} subject: Uusi raportti instanssista %{instance} (nro %{id}) application_mailer: notification_preferences: Muuta sähköpostiasetuksia @@ -381,6 +412,7 @@ fi: security: Tunnukset set_new_password: Aseta uusi salasana authorize_follow: + already_following: Sinä seuraat jo tätä tiliä error: Valitettavasti etätilin haussa tapahtui virhe follow: Seuraa follow_request: 'Olet lähettänyt seuraamispyynnön käyttäjälle:' @@ -473,6 +505,7 @@ fi: '21600': 6 tuntia '3600': 1 tunti '43200': 12 tuntia + '604800': 1 viikko '86400': 1 vuorokausi expires_in_prompt: Ei koskaan generate: Luo @@ -557,25 +590,13 @@ fi: other: Muut publishing: Julkaiseminen web: Web - push_notifications: - favourite: - title: "%{name} tykkäsi tilastasi" - follow: - title: "%{name} seuraa nyt sinua" - group: - title: "%{count} ilmoitusta" - mention: - action_boost: Buustaa - action_expand: Näytä lisää - action_favourite: Tykkää - title: "%{nimi} mainitsi sinut" - reblog: - title: "%{name} buustasi tilaasi" remote_follow: acct: Syötä se käyttäjätunnus@verkkotunnus, josta haluat seurata missing_resource: Vaadittavaa uudelleenohjaus-URL:ää tiliisi ei löytynyt proceed: Siirry seuraamaan prompt: 'Olet aikeissa seurata:' + remote_unfollow: + error: Virhe sessions: activity: Viimeisin toiminta browser: Selain @@ -642,6 +663,9 @@ fi: one: "%{count} video" other: "%{count} videota" content_warning: 'Sisältövaroitus: %{warning}' + disallowed_hashtags: + one: 'sisälsi aihetunnisteen jota ei sallita: %{tags}' + other: 'sisälsi aihetunnisteet joita ei sallita: %{tags}' open_in_web: Avaa selaimessa over_character_limit: merkkimäärän rajoitus %{max} ylitetty pin_errors: @@ -712,5 +736,6 @@ fi: users: invalid_email: Virheellinen sähköpostiosoite invalid_otp_token: Virheellinen kaksivaiheisen todentamisen koodi + otp_lost_help_html: Jos sinulla ei ole pääsyä kumpaankaan, voit ottaa yhteyttä osoitteeseen %{email} seamless_external_login: Olet kirjautunut ulkoisen palvelun kautta, joten salasana- ja sähköpostiasetukset eivät ole käytettävissä. signed_in_as: 'Kirjautunut henkilönä:' diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 0579123dcb..b3914ea2b1 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -40,6 +40,7 @@ fr: following: Abonnements media: Médias moved_html: "%{name} a changé de compte pour %{new_profile_link} :" + network_hidden: Cette information n'est pas disponible nothing_here: Rien à voir ici ! people_followed_by: Personnes suivies par %{name} people_who_follow: Personnes qui suivent %{name} @@ -49,13 +50,12 @@ fr: reserved_username: Ce nom d’utilisateur⋅ice est réservé roles: admin: Admin + bot: Robot moderator: Modérateur·trice unfollow: Ne plus suivre admin: account_moderation_notes: - account: Modérateur·ice - create: Créer - created_at: Date + create: Créer une note created_msg: Note de modération créée avec succès ! delete: Supprimer destroyed_msg: Note de modération supprimée avec succès ! @@ -72,6 +72,7 @@ fr: title: Modifier le courriel pour %{username} confirm: Confirmer confirmed: Confirmé + confirming: Confirmation demote: Rétrograder disable: Désactiver disable_two_factor_authentication: Désactiver l’authentification à deux facteurs @@ -80,6 +81,7 @@ fr: domain: Domaine edit: Éditer email: Courriel + email_status: État de la messagerie enable: Activer enabled: Activé feed_url: URL du flux @@ -118,6 +120,10 @@ fr: push_subscription_expires: Expiration de l’abonnement PuSH redownload: Rafraîchir les avatars remove_avatar: Supprimer l'avatar + resend_confirmation: + already_confirmed: Cet utilisateur est déjà confirmé + send: Renvoyer un courriel de confirmation + success: Email de confirmation envoyé avec succès ! reset: Réinitialiser reset_password: Réinitialiser le mot de passe resubscribe: Se réabonner @@ -269,7 +275,6 @@ fr: comment: none: Aucun created_at: Signalé - delete: Supprimer id: ID mark_as_resolved: Marquer comme résolu mark_as_unresolved: Marquer comme non-résolu @@ -278,10 +283,7 @@ fr: create_and_resolve: Résoudre avec une note create_and_unresolve: Ré-ouvrir avec une note delete: Effacer - placeholder: Décrivez quelles actions ont été prises, ou toute autre mise à jour de ce signalement… - nsfw: - 'false': Ré-afficher les médias - 'true': Masquer les médias + placeholder: Décrivez quelles actions ont été prises, ou toute autre mise à jour… reopen: Ré-ouvrir le signalement report: 'Signalement #%{id}' report_contents: Contenu @@ -356,11 +358,8 @@ fr: delete: Supprimer nsfw_off: Marquer comme non-sensible nsfw_on: Marquer comme sensible - execute: Exécuter failed_to_execute: Erreur d’exécution media: - hide: Masquer les médias - show: Montrer les médias title: Médias no_media: Aucun média title: État du compte @@ -376,6 +375,7 @@ fr: admin_mailer: new_report: body: "%{reporter} a signalé %{target}" + body_remote: Quelqu'un de %{domain} a signalé %{target} subject: Nouveau signalement sur %{instance} (#%{id}) application_mailer: notification_preferences: Modifier les préférences de courriel @@ -465,7 +465,7 @@ fr: archive_takeout: date: Date download: Télécharger votre archive - hint_html: Vous pouvez demander une archive de vos pouets et médias téléversés. Les données exportées seront au format ActivityPub, lisible par tout logiciel compatible. + hint_html: Vous pouvez demander une archive de vos pouets et médias téléversés. Les données exportées seront au format ActivityPub, lisible par tout logiciel compatible. Vous pouvez demander une archive tous les 7 jours. in_progress: Élaboration de votre archive.... request: Demandez vos archives size: Taille @@ -550,7 +550,7 @@ fr: subject: one: "Une nouvelle notification depuis votre dernière visite \U0001F418" other: "%{count} nouvelles notifications depuis votre dernière visite \U0001F418" - title: Pendant votre absence… + title: Pendant votre absence... favourite: body: "%{name} a ajouté votre post à ses favoris :" subject: "%{name} a ajouté votre post à ses favoris" @@ -595,20 +595,6 @@ fr: other: Autre publishing: Publication web: Web - push_notifications: - favourite: - title: "%{name} a mis votre statut en favori" - follow: - title: "%{name} vous suit" - group: - title: "%{count} notifications" - mention: - action_boost: Partager - action_expand: Montrer plus - action_favourite: Ajouter aux favoris - title: "%{name} vous a mentionné·e" - reblog: - title: "%{name} a partagé votre statut" remote_follow: acct: Entrez votre pseudo@instance depuis lequel vous voulez suivre cet·te utilisateur⋅ice missing_resource: L’URL de redirection n’a pas pu être trouvée @@ -633,7 +619,7 @@ fr: micro_messenger: MicroMessenger nokia: Nokia S40 Ovi Browser opera: Opera - otter: Autre + otter: Otter phantom_js: PhantomJS qq: QQ Browser safari: Safari @@ -683,6 +669,7 @@ fr: video: one: "%{count} vidéo" other: "%{count} vidéos" + boosted_from_html: Repartagé depuis %{acct_link} content_warning: 'Attention au contenu : %{warning}' disallowed_hashtags: one: 'contient un hashtag désactivé : %{tags}' @@ -710,6 +697,10 @@ fr: sensitive_content: Contenu sensible terms: title: "%{instance} Conditions d’utilisations et politique de confidentialité" + themes: + contrast: Contraste élevé + default: Mastodon + mastodon-light: Mastodon (clair) time: formats: default: "%d %b %Y, %H:%M" @@ -749,11 +740,12 @@ fr: tip_federated_timeline: La chronologie fédérée est une vue en direct du réseau Mastodon. Mais elle n'inclut que les personnes auxquelles vos voisin·es sont abonné·es, donc elle n'est pas complète. tip_following: Vous suivez les administrateurs et administratrices de votre serveur par défaut. Pour trouver d'autres personnes intéressantes, consultez les chronologies locales et fédérées. tip_local_timeline: La chronologie locale est une vue des personnes sur %{instance}. Ce sont vos voisines et voisins immédiats ! - tip_mobile_webapp: Si votre navigateur mobile vous propose d'ajouter Mastodon à votre écran d'accueil, vous pouvez recevoir des notifications push. Il agit comme une application native de bien des façons ! + tip_mobile_webapp: Si votre navigateur mobile vous propose d'ajouter Mastodon à votre écran d'accueil, vous pouvez recevoir des notifications. Il agit comme une application native de bien des façons ! tips: Astuces title: Bienvenue à bord, %{name} ! users: invalid_email: L’adresse courriel est invalide invalid_otp_token: Le code d’authentification à deux facteurs est invalide + otp_lost_help_html: Si vous perdez accès aux deux, vous pouvez contacter %{email} seamless_external_login: Vous êtes connecté via un service externe, donc les paramètres concernant le mot de passe et le courriel ne sont pas disponibles. signed_in_as: 'Connecté·e en tant que :' diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 093fa70fe8..82636618aa 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -19,8 +19,8 @@ gl: humane_approach_body: Aprendendo dos erros de outras redes, Mastodon intenta tomar decisións éticas de deseño para loitar contra os usos incorrectos da rede. humane_approach_title: Unha aproximación máis humana not_a_product_body: Mastodon non é unha rede comercial. Sen anuncios, sen minería de datos, sen xardíns privados. Non hai autoridade centralizada. - not_a_product_title: Vostede é unha persona, non un producto - real_conversation_body: Con 500 caracteres a súa disposición e soporte para contido ao por menor e avisos sobre o contido, pode expresarse vostede do xeito que queira. + not_a_product_title: Vostede é unha persoa, non un producto + real_conversation_body: Con 500 caracteres a súa disposición, soporte para contido polo miúdo e avisos sobre o contido, pode expresarse vostede con libertade. real_conversation_title: Construído para conversacións reais within_reach_body: Existen múltiples aplicativos para iOS, Android e outras plataformas grazas a un entorno API amigable para o desenvolvedor que lle permite estar ao tanto cos seus amigos en calquer lugar. within_reach_title: Sempre en contacto @@ -30,9 +30,9 @@ gl: other_instances: Listado de instancias source_code: Código fonte status_count_after: estados - status_count_before: Quen escribeu + status_count_before: Que publicaron user_count_after: usuarias - user_count_before: Inicio de + user_count_before: Fogar de what_is_mastodon: Qué é Mastodon? accounts: follow: Seguir @@ -40,6 +40,7 @@ gl: following: Seguindo media: Medios moved_html: "%{name} mudouse a %{new_profile_link}:" + network_hidden: A información non está dispoñible nothing_here: Nada por aquí! people_followed_by: Personas que segue %{name} people_who_follow: Personas que seguen a %{name} @@ -49,13 +50,12 @@ gl: reserved_username: O nome de usuaria está reservado roles: admin: Admin + bot: Bot moderator: Mod unfollow: Deixar de seguir admin: account_moderation_notes: - account: Moderador - create: Crear - created_at: Data + create: Deixar nota created_msg: Nota a moderación creada con éxito! delete: Eliminar destroyed_msg: Nota a moderación destruída con éxito! @@ -72,6 +72,7 @@ gl: title: Cambiar o correo-e de %{username} confirm: Confirmar confirmed: Confirmado + confirming: Confirmar demote: Degradar disable: Deshabilitar disable_two_factor_authentication: Deshabilitar 2FA @@ -80,6 +81,7 @@ gl: domain: Dominio edit: Editar email: E-mail + email_status: Estado del correo electrónico enable: Habilitar enabled: Habilitado feed_url: URL fonte @@ -118,6 +120,10 @@ gl: push_subscription_expires: A suscrición PuSH caduca redownload: Actualizar avatar remove_avatar: Eliminar avatar + resend_confirmation: + already_confirmed: Este usuario ya está confirmado + send: Reenviar el correo electrónico de confirmación + success: "¡Correo electrónico de confirmación enviado con éxito!" reset: Restablecer reset_password: Restablecer contrasinal resubscribe: Voltar a suscribir @@ -269,7 +275,6 @@ gl: comment: none: Nada created_at: Reportado - delete: Eliminar id: ID mark_as_resolved: Marcar como resolto mark_as_unresolved: Marcar como non resolto @@ -278,10 +283,7 @@ gl: create_and_resolve: Resolver con nota create_and_unresolve: Voltar a abrir con nota delete: Eliminar - placeholder: Describir qué decisións foron tomadas, ou calquer actualización a este informe… - nsfw: - 'false': Non agochar anexos de medios - 'true': Agochar anexos de medios + placeholder: Describe qué medidas foron tomadas, ou calquer outra información relacionada... reopen: Voltar a abrir o informe report: 'Informe #%{id}' report_contents: Contidos @@ -356,11 +358,8 @@ gl: delete: Eliminar nsfw_off: Marcar como non sensible nsfw_on: Marcar como sensible - execute: Executar failed_to_execute: Fallou a execución media: - hide: Agochar medios - show: Mostar medios title: Medios no_media: Sen medios title: Estados da conta @@ -376,6 +375,7 @@ gl: admin_mailer: new_report: body: "%{reporter} informou sobre %{target}" + body_remote: Alguén desde %{domain} informou sobre %{target} subject: Novo informe sobre %{instance} (#%{id}) application_mailer: notification_preferences: Cambiar os axustes de correo-e @@ -465,7 +465,7 @@ gl: archive_takeout: date: Data download: Descargue o seu ficheiro - hint_html: Pode solicitar un ficheiro cos seus toots ficheiros de medios. Os datos estarán en formato ActivityPub e son compatibles con calquer software que o cumpla. + hint_html: Pode solicitar un ficheiro cos seus toots ficheiros de medios. Os datos estarán en formato ActivityPub e son compatibles con calquer software que o cumpla. Pode solicitar un ficheiro cada 7 días. in_progress: Xerando o seu ficheiro... request: Solicite o ficheiro size: Tamaño @@ -550,7 +550,7 @@ gl: subject: one: "1 nova notificación desde a súa última visita \U0001F418" other: "%{count} novas notificacións desde a súa última visita \U0001F418" - title: Na súa ausencia… + title: Na súa ausencia... favourite: body: 'O seu estado foi marcado favorito por %{name}:' subject: "%{name} marcou favorito o seu estado" @@ -595,20 +595,6 @@ gl: other: Outro publishing: Publicando web: Web - push_notifications: - favourite: - title: "%{name} marcou favorito o seu estado" - follow: - title: "%{name} agora está a seguila" - group: - title: "%{count} notificacións" - mention: - action_boost: Promover - action_expand: Mostar máis - action_favourite: Favorito - title: "%{name} mencionouna" - reblog: - title: "%{name} promoveu un dos seus estados" remote_follow: acct: Introduza o seu nomedeusuaria@dominio desde onde quere facer seguimento missing_resource: Non se puido atopar o URL de redirecionamento requerido para a súa conta @@ -683,6 +669,7 @@ gl: video: one: "%{count} vídeo" other: "%{count} vídeos" + boosted_from_html: Promovida desde %{acct_link} content_warning: 'Aviso sobre o contido: %{warning}' disallowed_hashtags: one: 'contiña unha etiqueta non permitida: %{tags}' @@ -789,6 +776,7 @@ gl: title: "%{instance} Termos do Servizo e Política de Intimidade" themes: default: Mastodon + mastodon-light: Mastodon (claro) time: formats: default: "%d %b, %Y, %H:%M" @@ -834,5 +822,6 @@ gl: users: invalid_email: O enderezo de correo non é válido invalid_otp_token: Código de doble-factor non válido + otp_lost_help_html: Si perde o acceso a ambos, pode contactar con %{email} seamless_external_login: Está conectado a través de un servizo externo, polo que os axustes de contrasinal e correo-e non están dispoñibles. signed_in_as: 'Rexistrada como:' diff --git a/config/locales/he.yml b/config/locales/he.yml index d641c6e1a5..c127db3852 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -52,9 +52,7 @@ he: unfollow: הפסקת מעקב admin: account_moderation_notes: - account: מנחה דיון create: ליצור - created_at: תאריך created_msg: הודעת מנחה נוצרה בהצלחה! delete: למחוק destroyed_msg: הודעת מנחה נמחקה בהצלחה! @@ -63,6 +61,7 @@ he: by_domain: שם מתחם confirm: אישור confirmed: אושר + confirming: המאשר demote: הורדה בדרגה disable: לחסום disable_two_factor_authentication: ביטול הזדהות דו-שלבית @@ -71,6 +70,7 @@ he: domain: תחום edit: עריכה email: דוא"ל + email_status: סטטוס דוא"ל enable: לאפשר enabled: מאופשר feed_url: כתובת פיד @@ -108,6 +108,10 @@ he: public: פומבי push_subscription_expires: הרשמה להודעות בדחיפה פגה redownload: לקריאה מחדש של האווטאר + resend_confirmation: + already_confirmed: משתמש זה כבר אושר + send: שלח מחדש דוא"ל אימות + success: הודעת האימייל נשלחה בהצלחה! reset: איפוס reset_password: אתחול סיסמא resubscribe: להרשם מחדש @@ -181,12 +185,8 @@ he: are_you_sure: 100% על בטוח? comment: none: ללא - delete: מחיקה id: ID mark_as_resolved: סימון כפתור - nsfw: - 'false': לכל המשפחה - 'true': תוכן רגיש report: 'דווח על #%{id}' report_contents: תוכן reported_account: חשבון מדווח diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 7fe431d377..41093aa434 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -52,9 +52,7 @@ hu: unfollow: Követés abbahagyása admin: account_moderation_notes: - account: Moderátor create: Új bejegyzés - created_at: Dátum created_msg: Moderációs bejegyzés létrehozva! delete: Törlés destroyed_msg: Moderációs bejegyzés törölve! @@ -63,6 +61,7 @@ hu: by_domain: Domain confirm: Megerősítés confirmed: Megerősítve + confirming: Megerősítve demote: Lefokozás disable: Kikapcsolás disable_two_factor_authentication: Kétlépcsős azonosítás kikapcsolása @@ -71,6 +70,7 @@ hu: domain: Domain edit: Szerkesztés email: E-mail + email_status: E-mail állapot enable: Engedélyezés enabled: Engedélyezve feed_url: Hírcsatorna URL @@ -108,6 +108,10 @@ hu: public: Nyilvános push_subscription_expires: A PuSH feliratkozás elévül redownload: Profilkép frissítése + resend_confirmation: + already_confirmed: Ezt a felhasználót már megerősítették + send: Küldd újra a megerősítő email-t + success: A megerősítő e-mail sikeresen elküldve! reset: Visszaállítás reset_password: Jelszó visszaállítása resubscribe: Feliratkozás ismét @@ -244,12 +248,8 @@ hu: are_you_sure: Biztos vagy benne? comment: none: Egyik sem - delete: Törlés id: ID mark_as_resolved: Megjelölés megoldottként - nsfw: - 'false': Média-csatolmányok rejtésének feloldása - 'true': Média-csatolmányok elrejtése report: "#%{id} számú jelentés" report_contents: Tartalom reported_account: Bejelentett fiók @@ -314,11 +314,8 @@ hu: delete: Törlés nsfw_off: Szenzitív tartalom kikapcsolva nsfw_on: Szenzitív tartalom bekapcsolva - execute: Végrehajt failed_to_execute: Végrehajtás sikertelen media: - hide: Média elrejtése - show: Média megjelenítése title: Média no_media: Nem található médiafájl title: Felhasználó tülkjei @@ -534,20 +531,6 @@ hu: other: Egyéb publishing: Közzététel web: Web - push_notifications: - favourite: - title: "%{name} a kedvenceihez adta a tülköd" - follow: - title: "%{name} mostantól követ téged" - group: - title: "%{count} értesítés" - mention: - action_boost: Reblog - action_expand: Mutass többet - action_favourite: Kedvencekhez adás - title: "%{name} megemlített téged" - reblog: - title: "%{name} reblogolta a tülköd" remote_follow: acct: Írd be a felhasználódat, amelyről követni szeretnéd felhasznalonev@domain formátumban missing_resource: A fiókodnál nem található a szükséges átirányítási URL diff --git a/config/locales/id.yml b/config/locales/id.yml index 5a63b8038a..4fb75f2b0e 100644 --- a/config/locales/id.yml +++ b/config/locales/id.yml @@ -29,10 +29,12 @@ id: are_you_sure: Anda yakin? confirm: Konfirmasi confirmed: Dikonfirmasi + confirming: Mengkonfirmasi display_name: Nama domain: Domain edit: Ubah email: E-mail + email_status: Status Email feed_url: URL Feed followers: Pengikut follows: Mengikut @@ -58,6 +60,10 @@ id: profile_url: URL profil public: Publik push_subscription_expires: Langganan PuSH telah kadaluarsa + resend_confirmation: + already_confirmed: Pengguna ini sudah dikonfirmasi + send: Kirim ulang email konfirmasi + success: Email konfirmasi berhasil dikirim! reset_password: Reset kata sandi salmon_url: URL Salmon show: @@ -107,7 +113,6 @@ id: reports: comment: none: Tidak ada - delete: Hapus id: ID mark_as_resolved: Tandai telah diseleseikan report: 'Laporkan #%{id}' diff --git a/config/locales/io.yml b/config/locales/io.yml index 7c25acc47a..bf15de4886 100644 --- a/config/locales/io.yml +++ b/config/locales/io.yml @@ -106,7 +106,6 @@ io: reports: comment: none: None - delete: Delete id: ID mark_as_resolved: Mark as resolved report: 'Report #%{id}' diff --git a/config/locales/it.yml b/config/locales/it.yml index 0518d20e61..5608c0574c 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -1,6 +1,7 @@ --- it: about: + about_hashtag_html: Questi sono i toot pubblici etichettati con #%{hashtag}. Puoi interagire con loro se hai un account nel fediverse. about_mastodon_html: Mastodon è un social network gratuito e open-source. Un'alternativa decentralizzata alle piattaforme commerciali che evita che una singola compagnia monopolizzi il tuo modo di comunicare. Scegli un server di cui ti fidi — qualunque sia la tua scelta, potrai interagire con chiunque altro. Chiunque può sviluppare un suo server Mastodon e partecipare alla vita del social network. about_this: A proposito di questo server administered_by: 'Amministrato da:' @@ -11,6 +12,9 @@ it: description_headline: Cos'è %{domain}? domain_count_after: altri server domain_count_before: Connesso a + extended_description_html: | + Un buon posto per le regole + La descrizione estesa non è ancora stata preparata. features: humane_approach_body: Imparando dai fallimenti degli altri networks, Mastodon mira a fare scelte di design etico per combattere l'abuso dei social media. humane_approach_title: Un approccio più umano @@ -33,9 +37,10 @@ it: accounts: follow: Segui followers: Seguaci - following: Seguiti + following: Segui media: Media moved_html: "%{name} è stato spostato su %{new_profile_link}:" + network_hidden: Questa informazione non e' disponibile nothing_here: Qui non c'è nulla! people_followed_by: Persone seguite da %{name} people_who_follow: Persone che seguono %{name} @@ -45,13 +50,12 @@ it: reserved_username: Il nome utente è riservato roles: admin: Amministratore - moderator: Mod + bot: Bot + moderator: Moderatore unfollow: Non seguire più admin: account_moderation_notes: - account: Moderatore - create: Crea - created_at: Data + create: Lascia nota created_msg: Nota di moderazione creata con successo! delete: Elimina destroyed_msg: Nota di moderazione distrutta con successo! @@ -68,6 +72,7 @@ it: title: Cambia email per %{username} confirm: Conferma confirmed: Confermato + confirming: Confermando demote: Declassa disable: Disabilita disable_two_factor_authentication: Disabilita 2FA @@ -76,12 +81,13 @@ it: domain: Dominio edit: Modifica email: Email + email_status: Stato email enable: Abilita enabled: Abilitato feed_url: URL Feed followers: Follower followers_url: URL follower - follows: Follows + follows: Segue inbox_url: URL inbox ip: IP location: @@ -111,8 +117,13 @@ it: promote: Promuovi protocol: Protocollo public: Pubblico + push_subscription_expires: Sottoscrizione PuSH scaduta redownload: Aggiorna avatar remove_avatar: Rimuovi avatar + resend_confirmation: + already_confirmed: Questo utente è già confermato + send: Reinvia email di conferma + success: Email di conferma inviata con successo! reset: Reimposta reset_password: Reimposta password resubscribe: Riscriversi @@ -120,9 +131,15 @@ it: roles: admin: Amministratore moderator: Moderatore - staff: Staff + staff: Personale user: Utente + salmon_url: URL Salmone search: Cerca + shared_inbox_url: URL Inbox Condiviso + show: + created_reports: Rapporti creati da questo account + report: segnala + targeted_reports: Rapporti che riguardano questo account silence: Silenzia statuses: Stati subscribe: Sottoscrivi @@ -135,10 +152,27 @@ it: web: Web action_logs: actions: - change_email_user: "%{name} ha cambiato l'indirizzo e-mail per l'utente %{target}" + assigned_to_self_report: "%{name} ha assegnato il rapporto %{target} a se stesso" + change_email_user: "%{name} ha cambiato l'indirizzo email per l'utente %{target}" confirm_user: "%{name} ha confermato l'indirizzo email per l'utente %{target}" create_custom_emoji: "%{name} ha caricato un nuovo emoji %{target}" create_domain_block: "%{name} ha bloccato il dominio %{target}" + destroy_domain_block: "%{name} ha sbloccato il dominio %{target}" + destroy_status: "%{name} ha eliminato lo status di %{target}" + disable_2fa_user: "%{name} ha disabilitato l'obbligo dei due fattori per l'utente %{target}" + disable_custom_emoji: "%{name} ha disabilitato l'emoji %{target}" + disable_user: "%{name} ha disabilitato il login per l'utente %{target}" + enable_custom_emoji: "%{name} ha abilitato l'emoji %{target}" + enable_user: "%{name} ha abilitato il login per l'utente %{target}" + remove_avatar_user: "%{name} ha eliminato l'avatar di %{target}" + reopen_report: "%{name} ha riaperto il rapporto %{target}" + reset_password_user: "%{name} ha reimpostato la password dell'utente %{target}" + resolve_report: "%{name} ha risolto il rapporto %{target}" + silence_account: "%{name} ha silenziato l'account di %{target}" + suspend_account: "%{name} ha sospeso l'account di %{target}" + unsilence_account: "%{name} ha de-silenziato l'account di %{target}" + unsuspend_account: "%{name} ha annullato la sospensione dell'account di %{target}" + update_custom_emoji: "%{name} ha aggiornato l'emoji %{target}" custom_emojis: by_domain: Dominio copied_msg: Creata con successo una copia locale dell'emoji @@ -152,12 +186,13 @@ it: emoji: Emoji enable: Abilita enabled_msg: Questa emoji è stata abilitata con successo - image_hint: PNG fino a 50KB + image_hint: PNG fino a 50 KB listed: Elencato new: title: Aggiungi nuovo emoji personalizzato overwrite: Sovrascrivi shortcode: Shortcode + shortcode_hint: Almeno due caratteri, solo caratteri alfanumerici e trattino basso title: Emoji personalizzate unlisted: Non elencato update_failed_msg: Impossibile aggiornare questa emojii @@ -181,14 +216,21 @@ it: suspend: Sospendi severity: Severità show: + affected_accounts: + one: Interessato un solo account nel database + other: Interessati %{count} account nel database + retroactive: + silence: De-silenzia tutti gli account esistenti da questo dominio + suspend: Annulla la sospensione di tutti gli account esistenti da questo dominio + title: Annulla il blocco del dominio per %{domain} undo: Annulla title: Blocchi dominio undo: Annulla email_domain_blocks: add_new: Aggiungi nuovo - created_msg: Dominio e-mail aggiunto con successo alla lista nera + created_msg: Dominio email aggiunto con successo alla lista nera delete: Elimina - destroyed_msg: Dominio e-mail cancellato con successo dalla lista nera + destroyed_msg: Dominio email cancellato con successo dalla lista nera domain: Dominio new: create: Aggiungi dominio @@ -214,7 +256,6 @@ it: assigned: Moderatore assegnato comment: none: Nessuno - delete: Elimina id: ID mark_as_resolved: Segna come risolto mark_as_unresolved: Segna come non risolto @@ -223,10 +264,10 @@ it: create_and_resolve: Risolvi con nota create_and_unresolve: Riapri con nota delete: Elimina - nsfw: - 'false': Mostra gli allegati multimediali - 'true': Nascondi allegati multimediali + reopen: Riapri rapporto + report: 'Rapporto #%{id}' report_contents: Contenuti + reported_by: Inviato da resolved: Risolto silence_account: Silenzia account status: Stato @@ -238,19 +279,31 @@ it: view: Mostra settings: activity_api_enabled: + desc_html: Conteggi degli status pubblicati localmente, degli utenti attivi e delle nuove registrazioni in gruppi settimanali title: Pubblica statistiche aggregate circa l'attività dell'utente + bootstrap_timeline_accounts: + title: Seguiti predefiniti per i nuovi utenti contact_information: username: Nome utente del contatto peers_api_enabled: + desc_html: Nomi di dominio che questa istanza ha incontrato nella fediverse title: Pubblica elenco di istanze scoperte registrations: + closed_message: + desc_html: Mostrato nella pagina iniziale quando le registrazioni sono chiuse. Puoi usare tag HTML + title: Messaggio per registrazioni chiuse deletion: desc_html: Consenti a chiunque di cancellare il proprio account title: Apri la cancellazione dell'account min_invite_role: disabled: Nessuno + title: Permetti inviti da open: desc_html: Consenti a chiunque di creare un account + title: Apri registrazioni + show_known_fediverse_at_about_page: + desc_html: Quando attivato, mostra nell'anteprima i toot da tutte le istanze conosciute. Altrimenti mostra solo i toot locali. + title: Mostra la fediverse conosciuta nell'anteprima della timeline show_staff_badge: title: Mostra badge staff site_description: @@ -262,17 +315,16 @@ it: title: Anteprima timeline title: Impostazioni sito statuses: + back_to_account: Torna alla pagina dell'account batch: delete: Elimina - nsfw_off: NSFW OFF - nsfw_on: NSFW ON - execute: Esegui + nsfw_off: Segna come non sensibile + nsfw_on: Segna come sensibile failed_to_execute: Impossibile eseguire media: - hide: Nascondi media - show: Mostra media title: Media no_media: Nessun media + title: Gli status dell'account with_media: con media subscriptions: callback_url: URL Callback @@ -283,7 +335,7 @@ it: application_mailer: notification_preferences: Cambia preferenze email salutation: "%{name}," - settings: 'Cambia le impostazioni per le e-mail: %{link}' + settings: 'Cambia le impostazioni per le email: %{link}' view: 'Guarda:' view_profile: Mostra profilo view_status: Mostra stati @@ -291,16 +343,23 @@ it: created: Applicazione creata con successo destroyed: Applicazione eliminata con successo invalid_url: L'URL fornito non è valido + regenerate_token: Rigenera il token di accesso + token_regenerated: Token di accesso rigenerato + warning: Fa' molta attenzione con questi dati. Non fornirli mai a nessun altro! auth: + agreement_html: Iscrivendoti, accetti di seguire le regole dell'istanza e le nostre condizioni di servizio. change_password: Password confirm_email: Conferma email delete_account: Elimina account + delete_account_html: Se desideri cancellare il tuo account, puoi farlo qui. Ti sarà chiesta conferma. didnt_get_confirmation: Non hai ricevuto le istruzioni di conferma? forgot_password: Hai dimenticato la tua password? login: Entra - logout: Logout + logout: Sloggati migrate_account: Sposta ad un account differente + migrate_account_html: Se vuoi che questo account sia reindirizzato a uno diverso, puoi configurarlo qui. or: o + or_log_in_with: Oppure accedi con register: Iscriviti register_elsewhere: Iscriviti su un altro server resend_confirmation: Invia di nuovo le istruzioni di conferma @@ -311,6 +370,11 @@ it: already_following: Stai già seguendo questo account error: Sfortunatamente c'è stato un errore nel consultare l'account remoto follow: Segui + follow_request: 'Hai mandato una richiesta di diventare seguace a:' + following: 'Accettato! Ora stai seguendo:' + post_follow: + close: Oppure puoi chiudere questa finestra. + return: Torna al profilo dell'utente title: Segui %{acct} datetime: distance_in_words: @@ -326,11 +390,41 @@ it: x_minutes: "%{count} minuti" x_months: "%{count} mesi" x_seconds: "%{count} secondi" + deletes: + bad_password_msg: Ci avete provato, hacker! Password errata + confirm_password: Inserisci la tua password attuale per verificare la tua identità + description_html: Questa azione eliminerà in modo permanente e irreversibile tutto il contenuto del tuo account e lo disattiverà. Il tuo nome utente resterà riservato per prevenire che qualcuno in futuro assuma la tua identità. + proceed: Cancella l'account + success_msg: Il tuo account è stato cancellato + warning_html: È garantita solo la cancellazione del contenuto solo da questa istanza. I contenuti che sono stati ampiamente condivisi probabilmente lasceranno delle tracce. I server offline e quelli che non ricevono più i tuoi aggiornamenti non aggiorneranno i loro database. + errors: + '403': Non sei autorizzato a visualizzare questa pagina. + '404': La pagina che stavi cercando non esiste. + '410': La pagina che stavi cercando non esiste più. + '422': + content: Verifica di sicurezza non riuscita. Stai bloccando i cookies? + title: Verifica di sicurezza non riuscita + noscript_html: Per usare l'interfaccia web di Mastodon dovi abilitare JavaScript. In alternativa puoi provare una delle app native per Mastodon per la tua piattaforma. exports: + archive_takeout: + date: Data + download: Scarica il tuo archivio + hint_html: Puoi richiedere un archivio dei tuoi toot e media caricati. I dati esportati sono in formato ActivityPub, leggibili da qualunque software che segue questo standard. Puoi richiedere un archivio ogni 7 giorni. + in_progress: Creazione archivio... + request: Richiedi il tuo archivio + size: Dimensioni blocks: Stai bloccando csv: CSV follows: Stai seguendo + mutes: Stai silenziando storage: Archiviazione media + followers: + domain: Dominio + explanation_html: Se vuoi garantire la privacy dei tuoi status, devi sapere chi ti sta seguendo. I tuoi status privati vengono inviati a tutte le istanze su cui hai dei seguaci. Puoi controllare chi sono i tuoi seguaci, ed eliminarli se non hai fiducia che la tua privacy venga rispettata dallo staff o dal software di quelle istanze. + followers_count: Numero di seguaci + purge: Elimina dai seguaci + true_privacy_html: Tieni presente che l'effettiva riservatezza si può ottenere solo con la crittografia end-to-end. + unlocked_warning_html: Chiunque può seguirti per vedere immediatamente i tuoi status privati. %{lock_link} per poter esaminare e respingere gli utenti che vogliono seguirti. generic: changes_saved_msg: Modifiche effettuate con successo! powered_by: offerto da %{link} @@ -344,38 +438,79 @@ it: types: blocking: Lista dei bloccati following: Lista dei seguaci + muting: Lista dei silenziati upload: Carica + in_memoriam_html: In Memoriam. + invites: + delete: Disattiva + expired: Scaduto + expires_in: + '1800': 30 minuti + '21600': 6 ore + '3600': 1 ora + '43200': 12 ore + '604800': 1 settimana + '86400': 1 giorno + expires_in_prompt: Mai + generate: Genera + max_uses: + other: "%{count} utilizzi" + max_uses_prompt: Nessun limite + prompt: Genera e condividi dei link ad altri per garantire l'accesso a questa istanza + table: + expires_at: Scade + uses: Utilizzi + title: Invita persone landing_strip_html: "%{name} è un utente su %{link_to_root_path}. Puoi seguirlo o interagire con lui se possiedi un account ovunque nel fediverse." landing_strip_signup_html: Se non possiedi un account, puoi iscriverti qui. + lists: + errors: + limit: Hai raggiunto il numero massimo di liste media_attachments: validations: images_and_video: Impossibile allegare video a un post che contiene già immagini too_many: Impossibile allegare più di 4 file + migrations: + acct: utente@dominio del nuovo account + currently_redirecting: 'Il tuo profilo sarà ridirezionato a:' + proceed: Salva + updated_msg: L'impostazione per la migrazione dell'account è sta aggiornata! + moderation: + title: Moderazione notification_mailer: digest: - body: 'Ecco un breve riassunto di quello che ti sei perso su %{instance} dalla tua ultima visita del %{since}:' + action: Vedi tutte le notifiche + body: Ecco un breve riassunto di quello che ti sei perso dalla tua ultima visita del %{since} mention: "%{name} ti ha menzionato:" new_followers_summary: - one: Hai ricevuto un nuovo seguace! Urrà! - other: Hai ricevuto %{count} nuovi seguaci! Incredibile! + one: E inoltre hai ricevuto un nuovo seguace mentre eri assente! Urrà! + other: Inoltre, hai acquisito %{count} nuovi seguaci mentre eri assente! Incredibile! subject: one: "1 nuova notifica dalla tua ultima visita \U0001F418" other: "%{count} nuove notifiche dalla tua ultima visita \U0001F418" + title: In tua assenza… favourite: body: 'Il tuo status è stato apprezzato da %{name}:' subject: "%{name} ha apprezzato il tuo status" + title: Nuovo preferito follow: body: "%{name} ti sta seguendo!" subject: "%{name} ti sta seguendo" + title: Nuovo seguace follow_request: + action: Gestisci richieste di essere seguito body: "%{name} ha chiesto di seguirti" subject: 'Seguace in sospeso: %{name}' + title: Nuova richiesta di essere seguito mention: + action: Rispondi body: 'Sei stato menzionato da %{name} su:' subject: Sei stato menzionato da %{name} + title: Nuova menzione reblog: body: 'Il tuo status è stato condiviso da %{name}:' subject: "%{name} ha condiviso il tuo status" + title: Nuova condivisione number: human: decimal_units: @@ -388,35 +523,80 @@ it: trillion: T unit: '' pagination: + newer: Più recente next: Avanti + older: Più vecchio prev: Indietro truncate: "…" + preferences: + languages: Lingue + other: Altro + publishing: Pubblicazione + web: Web remote_follow: acct: Inserisci il tuo username@dominio da cui vuoi seguire questo utente missing_resource: Impossibile trovare l'URL di reindirizzamento richiesto per il tuo account proceed: Conferma prompt: 'Stai per seguire:' + remote_unfollow: + error: Errore + title: Titolo + sessions: + activity: Ultima attività + browser: Browser + browsers: + blackberry: Blackberry + chrome: Chrome + generic: Browser sconosciuto + current_session: Sessione corrente + description: "%{browser} su %{platform}" + platforms: + other: piattaforma sconosciuta settings: authorized_apps: Applicazioni autorizzate back: Torna a Mastodon + delete: Cancellazione account + development: Sviluppo edit_profile: Modifica profilo export: Esporta impostazioni + followers: Seguaci autorizzati import: Importa + migrate: Migrazione dell'account + notifications: Notifiche preferences: Preferenze settings: Impostazioni - two_factor_authentication: Autenticazione a Due Fattori + two_factor_authentication: Autenticazione a due fattori + your_apps: Le tue applicazioni statuses: + attached: + video: + one: "%{count} video" + other: "%{count} video" open_in_web: Apri sul Web over_character_limit: Limite caratteri superato di %{max} + pin_errors: + limit: Hai già fissato in cima il massimo numero di toot + ownership: Non puoi fissare in cima un toot di qualcun altro + private: Un toot non pubblico non può essere fissato in cima + reblog: Un toot condiviso non può essere fissato in cima show_more: Mostra di più visibilities: private: Mostra solo ai tuoi seguaci + private_long: Mostra solo ai seguaci public: Pubblico + public_long: Tutti lo possono vedere unlisted: Pubblico, ma non visibile sulla timeline pubblica + unlisted_long: Tutti lo possono vedere, ma non compare nelle timeline pubbliche stream_entries: click_to_show: Clicca per mostrare + pinned: Toot fissato in cima reblogged: condiviso sensitive_content: Materiale sensibile + terms: + title: "%{instance} Termini di servizio e politica della privacy" + themes: + contrast: Contrasto elevato + default: Mastodon time: formats: default: "%b %d, %Y, %H:%M" @@ -425,15 +605,39 @@ it: description_html: Se abiliti l'autorizzazione a due fattori, entrare nel tuo account ti richiederà di avere vicino il tuo telefono, il quale ti genererà un codice per eseguire l'accesso. disable: Disabilita enable: Abilita + enabled: È abilitata l'autenticazione a due fattori enabled_success: Autenticazione a due fattori attivata con successo + generate_recovery_codes: Genera codici di recupero instructions_html: "Scannerizza questo QR code con Google Authenticator o un'app TOTP simile sul tuo telefono. Da ora in poi, quell'applicazione genererà codici da inserire necessariamente per eseguire l'accesso." + lost_recovery_codes: I codici di recupero ti permettono di accedere al tuo account se perdi il telefono. Se hai perso i tuoi codici di recupero, puoi rigenerarli qui. Quelli vecchi saranno annullati. manual_instructions: 'Se non puoi scannerizzare il QR code e hai bisogno di inserirlo manualmente, questo è il codice segreto in chiaro:' + recovery_codes_regenerated: I codici di recupero sono stati rigenerati + recovery_instructions_html: Se perdi il telefono, puoi usare uno dei codici di recupero qui sotto per riottenere l'accesso al tuo account. Conserva i codici di recupero in un posto sicuro. Ad esempio puoi stamparli e conservarli insieme ad altri documenti importanti. setup: Configura - wrong_code: Il codice inserito non è corretto! Assicurati che l'orario del server e l'orario del telefono siano corretti. + wrong_code: Il codice inserito non è corretto! Assicurati che l'orario del server e l'orario del dispotivo siano corretti. user_mailer: + backup_ready: + explanation: Hai richiesto un backup completo del tuo account Mastodon. È pronto per essere scaricato! + subject: Il tuo archivio è pronto per essere scaricato + title: Esportazione archivio welcome: + edit_profile_step: Puoi personalizzare il tuo profilo caricando un avatar, un'intestazione, modificando il tuo nome visualizzato e così via. Se vuoi controllare i tuoi nuovi seguaci prima di autorizzarli a seguirti, puoi bloccare il tuo account. + explanation: Ecco alcuni suggerimenti per iniziare + final_action: Inizia a postare + final_step: 'Inizia a postare! Anche se non hai seguaci, i tuoi messaggi pubblici possono essere visti da altri, ad esempio nelle timeline locali e negli hashtag. Se vuoi puoi presentarti con l''hashtag #introductions.' + full_handle: Il tuo nome utente completo + full_handle_hint: Questo è ciò che diresti ai tuoi amici in modo che possano seguirti o contattarti da un'altra istanza. + review_preferences_action: Cambia preferenze + review_preferences_step: Dovresti impostare le tue preferenze, ad esempio quali email vuoi ricevere oppure il livello predefinito di privacy per i tuoi post. Se le immagini in movimento non ti danno fastidio, puoi abilitare l'animazione automatica delle GIF. + subject: Benvenuto/a su Mastodon + tip_bridge_html: Se vieni da Twitter, puoi trovare i tuoi amici su Mastodon usando laapp bridge. Ma funziona solo se anche loro la usano! + tip_federated_timeline: La timeline federata visualizza uno dopo l'altro i messaggi pubblicati su Mastodon. Ma comprende solo gli utenti seguiti dai tuoi vicini, quindi non è completa. + tip_following: Per impostazione predefinita, segui l'amministratore/i del tuo server. Per trovare utenti più interessanti, dà un'occhiata alle timeline locale e federata. + tip_local_timeline: La timeline locale visualizza uno dopo l'altro i messaggi degli utenti di %{instance}. Questi sono i tuoi vicini! + tip_mobile_webapp: Se il tuo browser mobile ti dà la possibilità di aggiungere Mastodon allo schermo, puoi ricevere le notifiche. Funziona un po' come un'app natova! tips: Suggerimenti title: Benvenuto a bordo, %{name}! users: - invalid_email: L'indirizzo e-mail inserito non è valido + invalid_email: L'indirizzo email inserito non è valido invalid_otp_token: Codice d'accesso non valido + seamless_external_login: Ti sei collegato per mezzo di un servizio esterno, quindi le impostazioni di email e password non sono disponibili. diff --git a/config/locales/ja.yml b/config/locales/ja.yml index be9e2da2c2..19b4017c71 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -40,6 +40,7 @@ ja: following: フォロー中 media: メディア moved_html: "%{name} さんは引っ越しました %{new_profile_link}:" + network_hidden: この情報は利用できません nothing_here: 何もありません! people_followed_by: "%{name} さんがフォロー中のアカウント" people_who_follow: "%{name} さんをフォロー中のアカウント" @@ -49,13 +50,12 @@ ja: reserved_username: このユーザー名は予約されています roles: admin: Admin + bot: Bot moderator: Mod unfollow: フォロー解除 admin: account_moderation_notes: - account: モデレータ create: 書き込む - created_at: 日付 created_msg: モデレーションメモを書き込みました! delete: 削除 destroyed_msg: モデレーションメモを削除しました! @@ -72,6 +72,7 @@ ja: title: "%{username} さんのメールアドレスを変更" confirm: 確認 confirmed: 確認済み + confirming: 確認中 demote: 降格 disable: 無効化 disable_two_factor_authentication: 二段階認証を無効にする @@ -80,6 +81,7 @@ ja: domain: ドメイン edit: 編集 email: メールアドレス + email_status: メールアドレスの状態 enable: 有効化 enabled: 有効 feed_url: フィードURL @@ -118,6 +120,10 @@ ja: push_subscription_expires: PuSH購読期限 redownload: アバターの更新 remove_avatar: アイコンを削除 + resend_confirmation: + already_confirmed: メールアドレスは確認済みです + send: 確認メールを再送 + success: 確認メールを再送信しました! reset: リセット reset_password: パスワード再設定 resubscribe: 再講読 @@ -269,7 +275,6 @@ ja: comment: none: なし created_at: レポート日時 - delete: 削除 id: ID mark_as_resolved: 解決済みとしてマーク mark_as_unresolved: 未解決として再び開く @@ -278,10 +283,7 @@ ja: create_and_resolve: 書き込み、解決済みにする create_and_unresolve: 書き込み、未解決として開く delete: 削除 - placeholder: このレポートに取られた措置や、その他の更新を記述してください… - nsfw: - 'false': NSFW オフ - 'true': NSFW オン + placeholder: どのような措置が取られたか、または関連する更新を記述してください… reopen: 再び開く report: レポート#%{id} report_contents: 内容 @@ -354,13 +356,10 @@ ja: back_to_account: アカウントページに戻る batch: delete: 削除 - nsfw_off: 閲覧注意のマークを取り除く - nsfw_on: 閲覧注意としてマークする - execute: 実行 + nsfw_off: 閲覧注意をはずす + nsfw_on: 閲覧注意にする failed_to_execute: 実行に失敗しました media: - hide: メディアを隠す - show: メディアを表示 title: メディア no_media: メディアなし title: トゥート一覧 @@ -376,6 +375,7 @@ ja: admin_mailer: new_report: body: "%{reporter} が %{target} を通報しました" + body_remote: "%{domain} の誰かが %{target} を通報しました" subject: "%{instance} の新しい通報 (#%{id})" application_mailer: notification_preferences: メール設定の変更 @@ -393,7 +393,7 @@ ja: warning: このデータは気をつけて取り扱ってください。他の人と共有しないでください! your_token: アクセストークン auth: - agreement_html: 登録すると インスタンスのルール と 利用規約 に従うことに同意したことになります。 + agreement_html: 登録すると インスタンスのルール と プライバシーポリシー に従うことに同意したことになります。 change_password: パスワード confirm_email: メールアドレスの確認 delete_account: アカウントの削除 @@ -465,7 +465,7 @@ ja: archive_takeout: date: 日時 download: ダウンロード - hint_html: "トゥートとメディアのアーカイブをリクエストできます。 データはActivityPub形式で、対応しているソフトウェアで読み込むことができます。" + hint_html: "トゥートとメディアのアーカイブをリクエストできます。 データはActivityPub形式で、対応しているソフトウェアで読み込むことができます。7日毎にアーカイブをリクエストできます。" in_progress: 準備中... request: アーカイブをリクエスト size: 容量 @@ -478,14 +478,14 @@ ja: domain: ドメイン explanation_html: あなたの投稿のプライバシーを確保したい場合、誰があなたをフォローしているのかを把握している必要があります。 プライベート投稿は、あなたのフォロワーがいる全てのインスタンスに配信されます。 フォロワーのインスタンスの管理者やソフトウェアがあなたのプライバシーを尊重してくれるかどうか怪しい場合は、そのフォロワーを削除した方がよいかもしれません。 followers_count: フォロワー数 - lock_link: 非公開アカウントにする + lock_link: 承認制アカウントにする purge: フォロワーから削除する success: one: 1個のドメインからソフトブロックするフォロワーを処理中... other: "%{count} 個のドメインからソフトブロックするフォロワーを処理中..." true_privacy_html: "プライバシーの保護はエンドツーエンドの暗号化でのみ実現可能であることに留意ください。" - unlocked_warning_html: 誰でもあなたをフォローすることができ、あなたのプライベート投稿をすぐに見ることができます。フォローする人を限定したい場合は%{lock_link}に設定してください。 - unlocked_warning_title: このアカウントは非公開アカウントに設定されていません + unlocked_warning_html: 誰でもあなたをフォローすることができ、フォロワー限定の投稿をすぐに見ることができます。フォローする人を限定したい場合は%{lock_link}に設定してください。 + unlocked_warning_title: このアカウントは承認制アカウントに設定されていません generic: changes_saved_msg: 正常に変更されました! powered_by: powered by %{link} @@ -604,20 +604,6 @@ ja: other: その他 publishing: 投稿 web: ウェブ - push_notifications: - favourite: - title: あなたのトゥートが %{name} さんにお気に入り登録されました - follow: - title: "%{name} さんにフォローされました" - group: - title: "%{count} 件の通知" - mention: - action_boost: ブースト - action_expand: もっと見る - action_favourite: お気に入り - title: "%{name} さんから返信がありました" - reblog: - title: あなたのトゥートが %{name} さんにブーストされました remote_follow: acct: あなたの ユーザー名@ドメイン を入力してください missing_resource: リダイレクト先が見つかりませんでした @@ -694,6 +680,7 @@ ja: video: one: "%{count} 本の動画" other: "%{count} 本の動画" + boosted_from_html: "%{acct_link} からブースト" content_warning: '閲覧注意: %{warning}' disallowed_hashtags: one: '許可されていないハッシュタグが含まれています: %{tags}' @@ -708,7 +695,7 @@ ja: show_more: もっと見る title: '%{name}: "%{quote}"' visibilities: - private: 非公開 + private: フォロワー限定 private_long: フォロワーにのみ表示されます public: 公開 public_long: 誰でも見ることができ、かつ公開タイムラインに表示されます @@ -727,7 +714,7 @@ ja: 基本的なアカウント情報: 当サイトに登録すると、ユーザー名・メールアドレス・パスワードの入力を求められることがあります。また表示名や自己紹介・プロフィール画像・ヘッダー画像といった追加のプロフィールを登録できます。ユーザー名・表示名・自己紹介・プロフィール画像・ヘッダー画像は常に公開されます。 投稿・フォロー・その他公開情報: フォローしているユーザーの一覧は一般公開されます。フォロワーも同様です。メッセージを投稿する際、日時だけでなく投稿に使用したアプリケーション名も記録されます。メッセージには写真や動画といった添付メディアを含むことがあります。「公開」や「未収載」の投稿は一般公開されます。プロフィールに投稿を載せるとそれもまた公開情報となります。投稿はフォロワーに配信されます。場合によっては他のサーバーに配信され、そこにコピーが保存されることを意味します。投稿を削除した場合も同様にフォロワーに配信されます。他の投稿をリブログやお気に入り登録する行動は常に公開されます。 - 「ダイレクト」と「非公開」投稿: すべての投稿はサーバーに保存され、処理されます。「非公開」投稿はフォロワーと投稿に書かれたユーザーに配信されます。「ダイレクト」投稿は投稿に書かれたユーザーにのみ配信されます。場合によっては他のサーバーに配信され、そこにコピーが保存されることを意味します。私たちはこれらの閲覧を一部の許可された者に限定するよう誠意を持って努めます。しかし他のサーバーにおいても同様に扱われるとは限りません。したがって、相手の所属するサーバーを吟味することが重要です。設定で新しいフォロワーの承認または拒否を手動で行うよう切り替えることもできます。サーバー管理者は「ダイレクト」や「非公開」投稿も閲覧する可能性があることを忘れないでください。また受信者がスクリーンショットやコピー、もしくは共有する可能性があることを忘れないでください。いかなる危険な情報もMastodon上で共有しないでください。 + 「ダイレクト」と「フォロワー限定」投稿: すべての投稿はサーバーに保存され、処理されます。「フォロワー限定」投稿はフォロワーと投稿に書かれたユーザーに配信されます。「ダイレクト」投稿は投稿に書かれたユーザーにのみ配信されます。場合によっては他のサーバーに配信され、そこにコピーが保存されることを意味します。私たちはこれらの閲覧を一部の許可された者に限定するよう誠意を持って努めます。しかし他のサーバーにおいても同様に扱われるとは限りません。したがって、相手の所属するサーバーを吟味することが重要です。設定で新しいフォロワーの承認または拒否を手動で行うよう切り替えることもできます。サーバー管理者は「ダイレクト」や「フォロワー限定」投稿も閲覧する可能性があることを忘れないでください。また受信者がスクリーンショットやコピー、もしくは共有する可能性があることを忘れないでください。いかなる危険な情報もMastodon上で共有しないでください。 IPアドレスやその他メタデータ: ログインする際IPアドレスだけでなくブラウザーアプリケーション名を記録します。ログインしたセッションはすべてユーザー設定で見直し、取り消すことができます。使用されている最新のIPアドレスは最大12ヵ月間保存されます。またサーバーへのIPアドレスを含むすべてのリクエストのログを保持することがあります。 @@ -768,7 +755,7 @@ ja: クッキーを使用していますか? - はい。クッキーは (あなたが許可した場合に) WebサイトやサービスがWebブラウザーを介してコンピューターに保存する小さなファイルです。使用することで Web サイトがブラウザーを識別し、登録済みのアカウントがある場合関連付けます。 + はい。クッキーは (あなたが許可した場合に) WebサイトやサービスがWebブラウザーを介してコンピューターに保存する小さなファイルです。使用することでWebサイトがブラウザーを識別し、登録済みのアカウントがある場合関連付けます。 私たちはクッキーを将来の訪問のために設定を保存し呼び出す用途に使用します。 @@ -778,15 +765,19 @@ ja: 私たちは個人を特定できる情報を外部へ販売・取引・その他方法で渡すことはありません。これには当サイトの運営・業務遂行・サービス提供を行ううえで補助する信頼できる第三者をこの機密情報の保護に同意するかぎり含みません。法令の遵守やサイトポリシーの施行、権利・財産・安全の保護に適切と判断した場合、あなたの情報を公開することがあります。 - あなたの公開情報はネットワーク上の他のサーバーにダウンロードされることがあります。相手が異なるサーバーに所属する場合、「公開」と「非公開」投稿はフォロワーの所属するサーバーに配信され、「ダイレクト」投稿は受信者の所属するサーバーに配信されます。 + あなたの公開情報はネットワーク上の他のサーバーにダウンロードされることがあります。相手が異なるサーバーに所属する場合、「公開」と「フォロワー限定」投稿はフォロワーの所属するサーバーに配信され、「ダイレクト」投稿は受信者の所属するサーバーに配信されます。 あなたがアカウントの使用をアプリケーションに許可すると、承認した権限の範囲内で公開プロフィール情報・フォローリスト・フォロワー・リスト・すべての投稿・お気に入り登録にアクセスできます。アプリケーションはメールアドレスやパスワードに決してアクセスできません。 - 児童オンラインプライバシー保護法の遵守 + 児童によるサイト利用について - 当サイト・製品・サービスは13歳以上の人を対象としています。サーバーが米国にあり、あなたが13歳未満の場合、COPPA (Children's Online Privacy Protection Act - 児童オンラインプライバシー保護法) により当サイトを使用できません。 + サーバーがEUまたはEEA圏内にある場合: 当サイト・製品・サービスは16歳以上の人を対象としています。あなたが16歳未満の場合、GDPR (General Data Protection Regulation - EU一般データ保護規則) により当サイトを使用できません。 + + サーバーが米国にある場合: 当サイト・製品・サービスは13歳以上の人を対象としています。あなたが13歳未満の場合、COPPA (Children's Online Privacy Protection Act - 児童オンラインプライバシー保護法) により当サイトを使用できません。 + + サーバーが別の管轄区域にある場合、法的要件は異なることがあります。 @@ -826,7 +817,7 @@ ja: title: アーカイブの取り出し welcome: edit_profile_action: プロフィールを設定 - edit_profile_step: アバター画像やヘッダー画像をアップロードしたり、表示名やその他プロフィールを変更しカスタマイズすることができます。新しいフォロワーからのフォローを許可する前に検討したい場合、アカウントを非公開にすることができます。 + edit_profile_step: アバター画像やヘッダー画像をアップロードしたり、表示名やその他プロフィールを変更しカスタマイズすることができます。新しいフォロワーからのフォローを許可する前に検討したい場合、アカウントを承認制にすることができます。 explanation: 始めるにあたってのアドバイスです final_action: 始めましょう final_step: 'さあ始めましょう! たとえフォロワーがいなくても、あなたの公開した投稿はローカルタイムラインやハッシュタグなどで誰かの目に止まるかもしれません。自己紹介をしたい時は #introductions ハッシュタグを使うといいかもしれません。' @@ -845,5 +836,6 @@ ja: users: invalid_email: メールアドレスが無効です invalid_otp_token: 二段階認証コードが間違っています + otp_lost_help_html: どちらも使用できない場合、%{email} に連絡を取ると解決できるかもしれません seamless_external_login: あなたは外部サービスを介してログインしているため、パスワードとメールアドレスの設定は利用できません。 signed_in_as: '下記でログイン中:' diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 251c0c3d7b..38f411dd5f 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -19,12 +19,12 @@ ko: humane_approach_body: 다른 SNS의 실패를 교훈삼아, Mastodon은 소셜미디어가 잘못 사용되는 것을 막기 위하여 윤리적인 설계를 추구합니다. humane_approach_title: 보다 배려를 의식한 설계를 추구 not_a_product_body: Mastodon은 이익을 추구하는 SNS가 아닙니다. 그러므로 광고와 데이터의 수집 및 분석이 존재하지 않고, 유저를 구속하지도 않습니다. - not_a_product_title: 여러분은 사람이며, 상품이 아닙니다. + not_a_product_title: 여러분은 사람이며, 상품이 아닙니다 real_conversation_body: 자유롭게 사용할 수 있는 500문자의 메세지와 미디어 경고 내용을 바탕으로, 자기자신을 자유롭게 표현할 수 있습니다. real_conversation_title: 진정한 커뮤니케이션을 위하여 within_reach_body: 개발자 친화적인 API에 의해서 실현된 iOS나 Android, 그 외의 여러 Platform들 덕분에 어디서든 친구들과 자유롭게 메세지를 주고 받을 수 있습니다. within_reach_title: 언제나 유저의 곁에서 - generic_description: "%{domain} 은 Mastodon의 인스턴스 입니다." + generic_description: "%{domain} 은 네트워크에 있는 한 서버입니다" hosted_on: "%{domain}에서 호스팅 되는 마스토돈" learn_more: 자세히 other_instances: 다른 인스턴스 @@ -49,13 +49,12 @@ ko: reserved_username: 이 아이디는 예약되어 있습니다 roles: admin: 관리자 + bot: 봇 moderator: 모더레이터 unfollow: 팔로우 해제 admin: account_moderation_notes: - account: 모더레이터 - create: 작성하기 - created_at: 작성 날짜 + create: 모더레이션 노트 작성하기 created_msg: 모더레이션 기록이 성공적으로 작성되었습니다! delete: 삭제 destroyed_msg: 모더레이션 기록이 성공적으로 삭제되었습니다! @@ -72,6 +71,7 @@ ko: title: "%{username}의 이메일 주소 변경" confirm: 확인 confirmed: 확인됨 + confirming: 확인 중 demote: 모더레이터 강등 disable: 비활성화 disable_two_factor_authentication: 2단계 인증을 비활성화 @@ -80,6 +80,7 @@ ko: domain: 도메인 edit: 편집 email: E-mail + email_status: 이메일 상태 enable: 활성화 enabled: 활성화된 feed_url: 피드 URL @@ -118,6 +119,10 @@ ko: push_subscription_expires: PuSH 구독 기간 만료 redownload: 아바타 업데이트 remove_avatar: 아바타 지우기 + resend_confirmation: + already_confirmed: 이 사용자는 이미 확인되었습니다 + send: 다시 확인 이메일 + success: 확인 이메일이 전송되었습니다! reset: 초기화 reset_password: 비밀번호 초기화 resubscribe: 다시 구독 @@ -271,19 +276,15 @@ ko: comment: none: 없음 created_at: 리포트 시각 - delete: 삭제 id: ID mark_as_resolved: 해결 완료 처리 mark_as_unresolved: 미해결로 표시 notes: - create: 노트 추가 - create_and_resolve: 노트를 작성하고 해결됨으로 표시 - create_and_unresolve: 노트 작성과 함께 미해결로 표시 + create: 기록 추가 + create_and_resolve: 기록을 작성하고 해결됨으로 표시 + create_and_unresolve: 기록 작성과 함께 미해결로 표시 delete: 삭제 placeholder: 이 리포트에 대한 조치, 다른 업데이트 사항에 대해 설명합니다… - nsfw: - 'false': NSFW 꺼짐 - 'true': NSFW 켜짐 reopen: 리포트 다시 열기 report: '신고 #%{id}' report_contents: 내용 @@ -358,11 +359,8 @@ ko: delete: 삭제 nsfw_off: NSFW 끄기 nsfw_on: NSFW 켜기 - execute: 실행 failed_to_execute: 실행을 실패하였습니다 media: - hide: 미디어 숨기기 - show: 미디어 보여주기 title: 미디어 no_media: 미디어 없음 title: 계정 툿 @@ -378,6 +376,7 @@ ko: admin_mailer: new_report: body: "%{reporter} 가 %{target} 를 신고했습니다" + body_remote: "%{domain}의 누군가가 %{target}을 신고했습니다" subject: "%{instance} 에 새 신고 등록됨 (#%{id})" application_mailer: notification_preferences: 메일 설정 변경 @@ -467,7 +466,7 @@ ko: archive_takeout: date: 날짜 download: 아카이브 다운로드 - hint_html: 당신의 툿과 업로드 된 미디어의 아카이브를 요청할 수 있습니다. 내보내지는 데이터는 ActivityPub 포맷입니다. 호환 되는 모든 소프트웨어에서 읽을 수 있습니다. + hint_html: 당신의 툿과 업로드 된 미디어의 아카이브를 요청할 수 있습니다. 내보내지는 데이터는 ActivityPub 포맷입니다. 호환 되는 모든 소프트웨어에서 읽을 수 있습니다. 7일마다 새로운 아카이브를 요청할 수 있습니다. in_progress: 당신의 아카이브를 컴파일 중입니다… request: 아카이브 요청하기 size: 크기 @@ -597,20 +596,6 @@ ko: other: 기타 publishing: 퍼블리싱 web: 웹 - push_notifications: - favourite: - title: "%{name} 님이 당신의 툿를 즐겨찾기에 등록했습니다" - follow: - title: "%{name} 님이 나를 팔로우 하고 있습니다" - group: - title: "%{count} 건의 알림" - mention: - action_boost: 부스트 - action_expand: 더보기 - action_favourite: 즐겨찾기 - title: "%{name} 님이 답장을 보냈습니다" - reblog: - title: "%{name} 님이 당신의 툿을 부스트 했습니다" remote_follow: acct: 아이디@도메인을 입력해 주십시오 missing_resource: 리디렉션 대상을 찾을 수 없습니다 @@ -686,6 +671,9 @@ ko: one: "%{count} 영상" other: "%{count} 영상" content_warning: '열람 주의: %{warning}' + disallowed_hashtags: + one: '허용 되지 않은 해시태그를 포함하고 있습니다: %{tags}' + other: '허용되지 않은 해시태그를 포함하고 있습니다: %{tags}' open_in_web: Web으로 열기 over_character_limit: 최대 %{max}자까지 입력할 수 있습니다 pin_errors: @@ -709,6 +697,9 @@ ko: sensitive_content: 민감한 컨텐츠 terms: title: "%{instance} 이용약관과 개인정보 취급 방침" + themes: + contrast: 고대비 + default: 마스토돈 time: formats: default: "%Y년 %m월 %d일 %H:%M" @@ -727,10 +718,10 @@ ko: recovery_codes_regenerated: 복구 코드가 다시 생성되었습니다 recovery_instructions_html: 휴대전화를 분실한 경우, 아래 복구 코드 중 하나를 사용해 계정에 접근할 수 있습니다. 복구 코드는 안전하게 보관해 주십시오. 이 코드를 인쇄해 중요한 서류와 함께 보관하는 것도 좋습니다. setup: 초기 설정 - wrong_code: 코드가 올바르지 않습니다. 서버와 휴대전화 간의 시간이 일치하는지 확인해 주십시오. + wrong_code: 코드가 올바르지 않습니다. 서버와 휴대전화 간의 시각이 일치하나요? user_mailer: backup_ready: - explanation: 당신이 요청한 계정의 풀 백업이 이제 다운로드 가능합니다. + explanation: 당신이 요청한 계정의 풀 백업이 이제 다운로드 가능합니다! subject: 당신의 아카이브를 다운로드 가능합니다 title: 아카이브 테이크 아웃 welcome: @@ -754,5 +745,6 @@ ko: users: invalid_email: 메일 주소가 올바르지 않습니다 invalid_otp_token: 2단계 인증 코드가 올바르지 않습니다 + otp_lost_help_html: 만약 양쪽 모두를 잃어버렸다면 %{email}을 통해 복구할 수 있습니다 seamless_external_login: 외부 서비스를 이용해 로그인 했습니다, 패스워드와 이메일 설정을 할 수 없습니다. signed_in_as: '다음과 같이 로그인 중:' diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 1ccc01a8f1..1fe3b54720 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -5,10 +5,10 @@ nl: about_mastodon_html: Mastodon is een sociaal netwerk dat gebruikt maakt van open webprotocollen en vrije software. Het is net zoals e-mail gedecentraliseerd. about_this: Over deze server administered_by: 'Beheerd door:' - closed_registrations: Registreren op deze server is momenteel uitgeschakeld. + closed_registrations: Registreren op deze server is momenteel niet mogelijk. Je kunt echter een andere server vinden om zo toegang te krijgen tot het netwerk. contact: Contact contact_missing: Niet ingesteld - contact_unavailable: N/A + contact_unavailable: n.v.t description_headline: Wat is %{domain}? domain_count_after: andere servers domain_count_before: Verbonden met @@ -16,11 +16,11 @@ nl: Een goede plek voor richtlijnen De uitgebreide omschrijving is nog niet ingevuld. features: - humane_approach_body: Na van de fouten van andere netwerken te hebben geleerd, tracht Mastodon ethische ontwerpkeuzes te maken om misbruik van social media te voorkomen. + humane_approach_body: Mastodon heeft van de fouten van andere sociale netwerken geleerd en probeert aan de hand van ethische ontwerpkeuzes misbruik van sociale media te voorkomen. humane_approach_title: Een meer menselijke aanpak - not_a_product_body: Mastodon is geen commercieel netwerk. Dus geen advertenties, geen datamining en geen besloten systemen. Er is geen centrale organisatie die alles bepaald. + not_a_product_body: Mastodon is geen commercieel netwerk. Dus geen advertenties, geen datamining en geen besloten systemen. Er is geen centrale organisatie die alles bepaalt. not_a_product_title: Jij bent een persoon, geen product - real_conversation_body: Met 500 karakters tot jouw beschikking, en ondersteuning voor tekst- en media-waarschuwingen, kan je jezelf uiten zoals jij dat wil. + real_conversation_body: Met 500 tekens tot jouw beschikking en ondersteuning voor tekst- en media-waarschuwingen, kan je jezelf uiten zoals jij dat wil. real_conversation_title: Voor echte gesprekken gemaakt within_reach_body: Meerdere apps voor iOS, Android en andere platformen, met dank aan het ontwikkelaarsvriendelijke API-systeem, zorgen ervoor dat je overal op de hoogte blijft. within_reach_title: Altijd binnen bereik @@ -40,22 +40,22 @@ nl: following: Volgend media: Media moved_html: "%{name} is verhuisd naar %{new_profile_link}:" + network_hidden: Deze informatie is niet beschikbaar nothing_here: Hier is niets! - people_followed_by: Mensen die %{name} volgt + people_followed_by: Mensen die %{name} volgen people_who_follow: Mensen die %{name} volgen posts: Toots - posts_with_replies: Toots met reacties + posts_with_replies: Toots en reacties remote_follow: Extern volgen reserved_username: Deze gebruikersnaam is gereserveerd roles: admin: Beheerder - moderator: Mod + bot: Bot + moderator: Moderator unfollow: Ontvolgen admin: account_moderation_notes: - account: Moderator - create: Aanmaken - created_at: Datum + create: Laat een opmerking achter created_msg: Aanmaken van opmerking voor moderatoren geslaagd! delete: Verwijderen destroyed_msg: Verwijderen van opmerking voor moderatoren geslaagd! @@ -66,12 +66,13 @@ nl: change_email: changed_msg: E-mailadres van account succesvol veranderd! current_email: Huidig e-mailadres - label: E-mailadres veranderen + label: E-mailadres wijzigen new_email: Nieuw e-mailadres submit: E-mailadres veranderen - title: E-mailadres veranderen voor %{username} + title: E-mailadres wijzigen voor %{username} confirm: Bevestigen confirmed: Bevestigd + confirming: Bevestiging demote: Degraderen disable: Uitschakelen disable_two_factor_authentication: 2FA uitschakelen @@ -80,6 +81,7 @@ nl: domain: Domein edit: Bewerken email: E-mail + email_status: E-mail Status enable: Inschakelen enabled: Ingeschakeld feed_url: Feed-URL @@ -93,9 +95,9 @@ nl: local: Lokaal remote: Extern title: Locatie - login_status: Aanmeldstatus + login_status: Login status media_attachments: Mediabijlagen - memorialize: Verander naar in memoriam + memorialize: In gedenkpagina veranderen moderation: all: Alles silenced: Genegeerd @@ -112,16 +114,20 @@ nl: outbox_url: Outbox-URL perform_full_suspension: Volledig opschorten profile_url: Profiel-URL - promote: Promoten + promote: Promoveren protocol: Protocol public: Openbaar push_subscription_expires: PuSH-abonnement verloopt op redownload: Avatar vernieuwen remove_avatar: Avatar verwijderen + resend_confirmation: + already_confirmed: Deze gebruiker is al bevestigd + send: Verzend bevestigingsmail opnieuw + success: Bevestigingsmail succesvol verzonden! reset: Opnieuw reset_password: Wachtwoord opnieuw instellen resubscribe: Opnieuw abonneren - role: Permissies + role: Bevoegdheden roles: admin: Beheerder moderator: Moderator @@ -161,7 +167,7 @@ nl: disable_user: Aanmelden voor %{target} is door %{name} uitgeschakeld enable_custom_emoji: Emoji %{target} is door %{name} ingeschakeld enable_user: Inloggen voor %{target} is door %{name} ingeschakeld - memorialize_account: Account %{target} is door %{name} in een in-memoriampagina veranderd + memorialize_account: Account %{target} is door %{name} in een gedenkpagina veranderd promote_user: Gebruiker %{target} is door %{name} gepromoveerd remove_avatar_user: "%{name} verwijderde de avatar van %{target}" reopen_report: "%{name} heeft gerapporteerde toot %{target} heropend" @@ -220,7 +226,7 @@ nl: noop: Geen silence: Negeren suspend: Opschorten - severity: Strengheid + severity: Zwaarte show: affected_accounts: one: Eén account in de database aangepast @@ -269,7 +275,6 @@ nl: comment: none: Geen created_at: Gerapporteerd op - delete: Verwijderen id: ID mark_as_resolved: Markeer als opgelost mark_as_unresolved: Markeer als onopgelost @@ -278,10 +283,7 @@ nl: create_and_resolve: Oplossen met opmerking create_and_unresolve: Heropenen met opmerking delete: Verwijderen - placeholder: Beschrijf welke acties zijn ondernomen of andere opmerkingen over deze gerapporteerde toot… - nsfw: - 'false': Media tonen - 'true': Media verbergen + placeholder: Beschrijf welke acties zijn ondernomen of andere gerelateerde opmerkingen… reopen: Gerapporteerde toot heropenen report: 'Gerapporteerde toot #%{id}' report_contents: Inhoud @@ -356,11 +358,8 @@ nl: delete: Verwijderen nsfw_off: Als niet gevoelig markeren nsfw_on: Als gevoelig markeren - execute: Uitvoeren failed_to_execute: Uitvoeren mislukt media: - hide: Media verbergen - show: Media tonen title: Media no_media: Geen media title: Toots van account @@ -376,6 +375,7 @@ nl: admin_mailer: new_report: body: "%{reporter} heeft %{target} gerapporteerd" + body_remote: Iemand van %{domain} heeft %{target} gerapporteerd subject: Nieuwe toots gerapporteerd op %{instance} (#%{id}) application_mailer: notification_preferences: E-mailvoorkeuren wijzigen @@ -465,7 +465,7 @@ nl: archive_takeout: date: Datum download: Jouw archief downloaden - hint_html: Je kunt een archief opvragen van jouw toots en geüploade media. De geëxporteerde gegevens zijn in ActivityPub-formaat, dat door hiervoor geschikte software valt uit te lezen. + hint_html: Je kunt een archief opvragen van jouw toots en geüploade media. De geëxporteerde gegevens zijn in ActivityPub-formaat, dat door hiervoor geschikte software valt uit te lezen. Je kunt elke 7 dagen een kopie van je archief aanvragen. in_progress: Jouw archief wordt samengesteld... request: Jouw archief opvragen size: Omvang @@ -550,7 +550,7 @@ nl: subject: one: "1 nieuwe melding sinds jouw laatste bezoek \U0001F418" other: "%{count} nieuwe meldingen sinds jouw laatste bezoek \U0001F418" - title: Tijdens jouw afwezigheid… + title: Tijdens jouw afwezigheid... favourite: body: 'Jouw toot werd door %{name} als favoriet gemarkeerd:' subject: "%{name} markeerde jouw toot als favoriet" @@ -595,20 +595,6 @@ nl: other: Overig publishing: Publiceren web: Webapp - push_notifications: - favourite: - title: "%{name} markeerde jouw toot als favoriet" - follow: - title: "%{name} volgt jou nu" - group: - title: "%{count} meldingen" - mention: - action_boost: Boost - action_expand: Meer tonen - action_favourite: Favoriet - title: "%{name} vermeldde jou" - reblog: - title: "%{name} boostte jouw toot" remote_follow: acct: Geef jouw account@domein.tld op waarvandaan je wilt volgen missing_resource: Kon vereiste doorverwijzings-URL voor jouw account niet vinden @@ -683,6 +669,7 @@ nl: video: one: "%{count} video" other: "%{count} video's" + boosted_from_html: Geboost van %{acct_link} content_warning: 'Tekstwaarschuwing: %{warning}' disallowed_hashtags: one: 'bevatte een niet toegestane hashtag: %{tags}' @@ -773,9 +760,13 @@ nl: - Children's Online Privacy Protection Act Compliance + Site usage by children - Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site. + If this server is in the EU or the EEA: Our site, products and services are all directed to people who are at least 16 years old. If you are under the age of 16, per the requirements of the GDPR (General Data Protection Regulation) do not use this site. + + If this server is in the USA: Our site, products and services are all directed to people who are at least 13 years old. If you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site. + + Law requirements can be different if this server is in another jurisdiction. @@ -832,5 +823,6 @@ nl: users: invalid_email: E-mailadres is ongeldig invalid_otp_token: Ongeldige tweestaps-aanmeldcode + otp_lost_help_html: Als je toegang tot beiden kwijt bent geraakt, neem dan contact op via %{email} seamless_external_login: Je bent ingelogd via een externe dienst, daarom zijn wachtwoorden en e-mailinstellingen niet beschikbaar. signed_in_as: 'Ingelogd als:' diff --git a/config/locales/no.yml b/config/locales/no.yml index 8b84182af6..eb1d27a199 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -52,9 +52,7 @@ unfollow: Slutte følge admin: account_moderation_notes: - account: Moderator create: Lag - created_at: Dato created_msg: Moderasjonsnotat laget uten problem! delete: Slett destroyed_msg: Moderasjonsnotat slettet uten problem! @@ -63,6 +61,7 @@ by_domain: Domene confirm: Bekreft confirmed: Bekreftet + confirming: Bekrefte demote: Degrader disable: Deaktiver disable_two_factor_authentication: Skru av 2FA @@ -71,6 +70,7 @@ domain: Domene edit: Redigér email: E-post + email_status: E-poststatus enable: Aktiver enabled: Aktivert feed_url: Feed-URL @@ -108,6 +108,10 @@ public: Offentlig push_subscription_expires: PuSH-abonnent utløper redownload: Oppdater avatar + resend_confirmation: + already_confirmed: Denne brukeren er allerede bekreftet + send: Send bekreftelses-epost på nytt + success: Bekreftelses e-post er vellykket sendt! reset: Tilbakestill reset_password: Nullstill passord resubscribe: Abonner på nytt @@ -244,12 +248,8 @@ are_you_sure: Er du sikker? comment: none: Ingen - delete: Slett id: ID mark_as_resolved: Merk som løst - nsfw: - 'false': Vis mediavedlegg - 'true': Skjul mediavedlegg report: 'Rapportér #%{id}' report_contents: Innhold reported_account: Rapportert konto @@ -314,11 +314,8 @@ delete: Slett nsfw_off: NSFW AV nsfw_on: NSFW PÅ - execute: Utfør failed_to_execute: Utføring mislyktes media: - hide: Skjul media - show: Vis media title: Media no_media: Ingen media title: Kontostatuser @@ -534,20 +531,6 @@ other: Annet publishing: Publisering web: Web - push_notifications: - favourite: - title: "%{name} favoriserte din status" - follow: - title: "%{name} følger deg nå" - group: - title: "%{count} varslinger" - mention: - action_boost: Fremhev - action_expand: Vis mer - action_favourite: Favoritter - title: "%{name} nevnte deg" - reblog: - title: "%{name} fremhevde din status" remote_follow: acct: Tast inn brukernavn@domene som du vil følge fra missing_resource: Kunne ikke finne URLen for din konto diff --git a/config/locales/oc.yml b/config/locales/oc.yml index d5717c0b57..faf4f6d17c 100644 --- a/config/locales/oc.yml +++ b/config/locales/oc.yml @@ -40,22 +40,22 @@ oc: following: Abonaments media: Mèdias moved_html: "%{name} a mudat a %{new_profile_link} :" + network_hidden: Aquesta informacion es pas disponibla nothing_here: I a pas res aquí ! - people_followed_by: Lo mond que %{name} sèc - people_who_follow: Lo mond que sègon %{name} + people_followed_by: Lo monde que %{name} sèc + people_who_follow: Lo monde que sègon %{name} posts: Tuts posts_with_replies: Tuts e responsas remote_follow: Sègre a distància reserved_username: Aqueste nom d’utilizaire es reservat roles: admin: Admin - moderator: Mod + bot: Robòt + moderator: Moderador unfollow: Quitar de sègre admin: account_moderation_notes: - account: Moderator - create: Crear - created_at: Data + create: Crear una nòta created_msg: Nòta de moderacion ben creada ! delete: Suprimir destroyed_msg: Nòta de moderacion ben suprimida ! @@ -72,6 +72,7 @@ oc: title: Cambiar l’adreça a %{username} confirm: Confirmar confirmed: Confirmat + confirming: Confirmacion demote: Retrogradar disable: Desactivar disable_two_factor_authentication: Desactivar 2FA @@ -80,6 +81,7 @@ oc: domain: Domeni edit: Modificar email: Corrièl + email_status: Estat de l’adreça enable: Activar enabled: Activat feed_url: Flux URL @@ -118,13 +120,18 @@ oc: push_subscription_expires: Fin de l’abonament PuSH redownload: Actualizar los avatars remove_avatar: Supriir l’avatar + resend_confirmation: + already_confirmed: Aqueste utilizaire es ja confirmat + send: Tornar mandar lo corrièl de confirmacion + success: Corrièl de confirmacion corrèctament mandat ! reset: Reïnicializar reset_password: Reïnicializar lo senhal resubscribe: Se tornar abonar role: Permissions roles: admin: Administrator - moderator: Moderator + bot: Robòt + moderator: Moderador staff: Personnal user: Uitlizaire salmon_url: URL Salmon @@ -164,10 +171,12 @@ oc: memorialize_account: "%{name} transformèt en memorial la pagina de perfil a %{target}" promote_user: "%{name} promoguèt %{target}" remove_avatar_user: "%{name} suprimèt l’avatar a %{target}" + reopen_report: "%{name} tornèt dobrir lo rapòrt %{target}" reset_password_user: "%{name} reïnicializèt lo senhal a %{target}" - resolve_report: "%{name} anullèt lo rapòrt de %{target}" + resolve_report: "%{name} anullèt lo rapòrt %{target}" silence_account: "%{name} metèt en silenci lo compte a %{target}" suspend_account: "%{name} susprenguèt lo compte a %{target}" + unassigned_report: "%{name} daissèt de tractar lo rapòrt %{target}" unsilence_account: "%{name} levèt lo silenci del compte a %{target}" unsuspend_account: "%{name} restabliguèt lo compte a %{target}" update_custom_emoji: "%{name} metèt a jorn l’emoji %{target}" @@ -262,10 +271,11 @@ oc: report: rapòrt action_taken_by: Mesura menada per are_you_sure: Es segur ? + assign_to_self: Me l’assignar + assigned: Moderador assignat comment: none: Pas cap created_at: Creacion - delete: Suprimir id: ID mark_as_resolved: Marcar coma resolgut mark_as_unresolved: Marcar coma pas resolgut @@ -273,22 +283,23 @@ oc: create: Ajustar una nòta create_and_resolve: Resòlvre amb una nòta create_and_unresolve: Tornar dobrir amb una nòta - placeholder: Explicatz las accions que son estadas menadas o çò qu’es estat fach per aqueste rapòrt… - nsfw: - 'false': Sens contengut sensible - 'true': Contengut sensible activat + delete: Escafar + placeholder: Explicatz las accions que son estadas menadas o quicòm de ligat al senhalament… reopen: Tornar dobrir lo rapòrt report: 'senhalament #%{id}' - report_contents: Contenguts + report_contents: Contengut reported_account: Compte senhalat reported_by: Senhalat per resolved: Resolgut + resolved_msg: Rapòrt corrèctament resolgut ! silence_account: Metre lo compte en silenci status: Estatut suspend_account: Suspendre lo compte target: Cibla title: Senhalament + unassign: Levar unresolved: Pas resolguts + updated_at: Actualizat view: Veire settings: activity_api_enabled: @@ -311,13 +322,13 @@ oc: desc_html: Afichat sus las pagina d’acuèlh quand las inscripcions son tampadas.Podètz utilizar de balisas HTML title: Messatge de barradura de las inscripcions deletion: - desc_html: Autorizar lo mond a suprimir lor compte + desc_html: Autorizar lo monde a suprimir lor compte title: Possibilitat de suprimir lo compte min_invite_role: disabled: Degun title: Autorizat amb invitacions open: - desc_html: Autorizar lo mond a se marcar + desc_html: Autorizar lo monde a se marcar title: Inscripcions show_known_fediverse_at_about_page: desc_html: Un còp activat mostrarà los tuts de totes los fediverse dins l’apercebut. Autrament mostrarà pas que los tuts locals. @@ -346,15 +357,12 @@ oc: back_to_account: Tornar a la pagina Compte batch: delete: Suprimir - nsfw_off: NSFW OFF - nsfw_on: NSFW ON - execute: Lançar + nsfw_off: Marcar coma pas sensible + nsfw_on: Marcar coma sensible failed_to_execute: Fracàs media: - hide: Amagar mèdia - show: Mostrar mèdia title: Mèdia - no_media: Cap mèdia + no_media: Cap de mèdia title: Estatuts del compte with_media: Amb mèdia subscriptions: @@ -368,6 +376,7 @@ oc: admin_mailer: new_report: body: "%{reporter} a senhalat %{target}" + body_remote: Qualqu’un de %{domain} senhalèt %{target} subject: Novèl senhalament per %{instance} (#%{id}) application_mailer: notification_preferences: Cambiar las preferéncias de corrièl @@ -490,9 +499,7 @@ oc: less_than_x_minutes: one: Fa mens d’una minuta other: Fa mens de %{count} minutas - less_than_x_seconds: - one: Fa mens d’una segonda - other: Fa mens de %{count} segondas + less_than_x_seconds: Ara meteis over_x_years: one: Fa mai d’un an other: Fa mai de %{count} ans @@ -529,13 +536,13 @@ oc: '429': Lo servidor mòla (subrecargada) '500': content: Un quicomet a pas foncionat coma caliá. - title: Aquesta pagina es incorrècta + title: Aquesta pagina es pas corrècta noscript_html: Per utilizar l’aplicacion web de Mastodon, mercés d’activar JavaScript. O podètz utilizar una aplicacion per vòstra plataforma coma alernativa. exports: archive_takeout: date: Data download: Telecargar vòstre archiu - hint_html: Podètz demandar un archiu de vòstres tuts e mèdias enviats. Las donadas exportadas seràn al format ActivityPub, ligible pels logicials compatibles. + hint_html: Podètz demandar un archiu de vòstres tuts e mèdias enviats. Las donadas exportadas seràn al format ActivityPub, ligible pels logicials compatibles. Podètz demandar un archiu cada 7 jorns. in_progress: Complilacion de vòstre archiu... request: Demandar vòstre archiu size: Talha @@ -546,7 +553,7 @@ oc: storage: Mèdias gardats followers: domain: Domeni - explanation_html: Se volètz vos assegurar de la confidencialitat de vòstres estatuts, vos cal saber qual sèc vòstre compte. Vòstres estatuts privats son enviats a totas las instàncias qu’an de mond que vos sègon.. Benlèu que volètz repassar vòstra lista e tirar los seguidors s’avètz de dobtes tocant las politica de confidencialitat de lor instàncias. + explanation_html: Se volètz vos assegurar de la confidencialitat de vòstres estatuts, vos cal saber qual sèc vòstre compte. Vòstres estatuts privats son enviats a totas las instàncias qu’an de monde que vos sègon.. Benlèu que volètz repassar vòstra lista e tirar los seguidors s’avètz de dobtes tocant las politicas de confidencialitat dels gestionaris de lor instància o sul logicial qu’utilizan. followers_count: Nombre de seguidors lock_link: Clavar vòstre compte purge: Tirar dels seguidors @@ -554,22 +561,22 @@ oc: one: Soi a blocar los seguidors d’un domeni… other: Soi a blocar los seguidors de %{count} domenis… true_privacy_html: Mèfi que la vertadièra confidencialitat pòt solament èsser amb un chiframent del cap a la fin (end-to-end). - unlocked_warning_html: Tot lo mond pòt vos sègre e veire sulpic vòstres estatuts privats. %{lock_link} per poder repassar e regetar los seguidors. + unlocked_warning_html: Tot lo monde pòt vos sègre e veire sulpic vòstres estatuts privats. %{lock_link} per poder repassar e regetar los seguidors. unlocked_warning_title: Vòstre compte es pas clavat generic: changes_saved_msg: Cambiaments ben realizats ! powered_by: propulsat per %{link} - save_changes: Salvagardar los cambiaments + save_changes: Salvar los cambiaments validation_errors: one: I a quicòm que truca ! Mercés de corregir l’error çai-jos other: I a quicòm que truca ! Mercés de corregir las %{count} errors çai-jos imports: - preface: Podètz importar qualques donadas coma lo mond que seguètz o blocatz a-n aquesta instància d’un fichièr creat d’una autra instància. + preface: Podètz importar qualques donadas coma lo monde que seguètz o blocatz a-n aquesta instància d’un fichièr creat d’una autra instància. success: Vòstras donadas son ben estadas mandadas e seràn tractadas tre que possible types: blocking: Lista de blocatge - following: Lista de mond que seguètz - muting: Lista de mond que volètz pas legir + following: Lista de monde que seguètz + muting: Lista de monde que volètz pas legir upload: Importar in_memoriam_html: En Memòria. invites: @@ -655,9 +662,9 @@ oc: trillion: T unit: '' pagination: - newer: Mai recent + newer: Mai recents next: Seguent - older: Mai ancian + older: Mai ancians prev: Precedent truncate: "…" preferences: @@ -665,20 +672,6 @@ oc: other: Autre publishing: Publicar web: Interfàcia Web - push_notifications: - favourite: - title: "%{name} a mes vòstre estatut en favorit" - follow: - title: "%{name} vos sèc ara" - group: - title: "%{count} notificacions" - mention: - action_boost: Partejar - action_expand: Ne veire mai - action_favourite: Ajustar als favorits - title: "%{name} vos a mencionat" - reblog: - title: "%{name} a partejat vòstre estatut" remote_follow: acct: Picatz vòstre utilizaire@instància que cal utilizar per sègre aqueste utilizaire missing_resource: URL de redireccion pas trobada @@ -753,6 +746,8 @@ oc: video: one: "%{count} vidèo" other: "%{count} vidèos" + boosted_from_html: Partejat de %{acct_link} + content_warning: 'Avertiment de contengut : %{warning}' disallowed_hashtags: one: 'conten una etiqueta desactivada : %{tags}' other: 'conten las etiquetas desactivadas : %{tags}' @@ -769,9 +764,9 @@ oc: private: Seguidors solament private_long: Mostrar pas qu’als seguidors public: Public - public_long: Tot lo mond pòt veire + public_long: Tot lo monde pòt veire unlisted: Pas listat - unlisted_long: Tot lo mond pòt veire mai serà pas visible sul flux public + unlisted_long: Tot lo monde pòt veire mai serà pas visible sul flux public stream_entries: click_to_show: Clicatz per veire pinned: Tut penjat @@ -797,7 +792,7 @@ oc: recovery_codes_regenerated: Los còdis de recuperacion son ben estats tornats generar recovery_instructions_html: Se vos arriba de perdre vòstre mobil, podètz utilizar un dels còdis de recuperacion cai-jos per poder tornar accedir a vòstre compte. Gardatz los còdis en seguretat, per exemple, imprimissètz los e gardatz los amb vòstres documents importants. setup: Paramètres - wrong_code: Lo còdi picat es invalid ! L’ora es la bona sul servidor e lo mobil ? + wrong_code: Lo còdi picat es invalid ! L’ora es bona sul servidor e lo mobil ? user_mailer: backup_ready: explanation: Avètz demandat una salvagarda complèta de vòstre compte Mastodon. Es prèsta per telecargament ! @@ -815,14 +810,15 @@ oc: review_preferences_step: Pensatz de configurar vòstras preferéncias, tal coma los corrièls que volètz recebrer o lo nivèl de confidencialitat de vòstres tuts per defaut. O se l’animacion vos dòna pas enveja de rendre, podètz activar la lectura automatica dels GIF. subject: Benvengut a Mastodon tip_bridge_html: Se venètz de Twitter, podètz trobar vòstres amics sus Mastodon en utilizant l‘aplicacion de Pont. Aquò fonciona pas que s’utilizan lo Pont tanben ! - tip_federated_timeline: Lo flux d’actualitat federat es una vista generala del malhum Mastodon. Mas aquò inclutz solament lo mond que vòstres vesins sègon, doncas es pas complèt. - tip_following: Seguètz l’administrator del servidor per defaut. Per trobar de mond mai interessant, agachatz lo flux d’actualitat local e lo global. - tip_local_timeline: Lo flux d’actualitat local es una vista del mond de %{instance}. Son vòstres vesins dirèctes ! + tip_federated_timeline: Lo flux d’actualitat federat es una vista generala del malhum Mastodon. Mas aquò inclutz solament lo monde que vòstres vesins sègon, doncas es pas complèt. + tip_following: Seguètz l’administrator del servidor per defaut. Per trobar de monde mai interessant, agachatz lo flux d’actualitat local e lo global. + tip_local_timeline: Lo flux d’actualitat local es una vista del monde de %{instance}. Son vòstres vesins dirèctes ! tip_mobile_webapp: Se vòstre navigator mobil nos permet d’apondre Mastodon a l’ecran d‘acuèlh, podètz recebre de notificacions. Aquò se compòrta coma una aplicacion nativa ! tips: Astúcias title: Vos desirem la benvenguda a bòrd %{name} ! users: invalid_email: L’adreça de corrièl es invalida invalid_otp_token: Còdi d’autentificacion en dos temps invalid + otp_lost_help_html: Se perdatz l’accès al dos, podètz benlèu contactar %{email} seamless_external_login: Sètz connectat via un servici extèrn, los paramètres de senhal e de corrièl son doncas pas disponibles. signed_in_as: 'Session a :' diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 519207d38b..ac585368a0 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -36,10 +36,11 @@ pl: what_is_mastodon: Czym jest Mastodon? accounts: follow: Śledź - followers: Śledzących - following: Śledzi + followers: Śledzący + following: Śledzeni media: Zawartość multimedialna moved_html: "%{name} korzysta teraz z konta %{new_profile_link}:" + network_hidden: Ta informacja nie jest dostępna nothing_here: Niczego tu nie ma! people_followed_by: Konta śledzone przez %{name} people_who_follow: Osoby, które śledzą konto %{name} @@ -49,13 +50,12 @@ pl: reserved_username: Ta nazwa użytkownika jest zarezerwowana roles: admin: Administrator + bot: Bot moderator: Moderator unfollow: Przestań śledzić admin: account_moderation_notes: - account: Autor - create: Dodaj - created_at: Data + create: Pozostaw notatkę created_msg: Pomyślnie dodano notatkę moderacyjną! delete: Usuń destroyed_msg: Pomyślnie usunięto notatkę moderacyjną! @@ -72,6 +72,7 @@ pl: title: Zmień adres e-mail dla %{username} confirm: Potwierdź confirmed: Potwierdzono + confirming: Potwierdzanie demote: Degraduj disable: Dezaktywuj disable_two_factor_authentication: Wyłącz uwierzytelnianie dwuetapowe @@ -80,6 +81,7 @@ pl: domain: Domena edit: Edytuj email: Adres e-mail + email_status: Stan e-maila enable: Aktywuj enabled: Aktywowano feed_url: Adres kanału @@ -118,6 +120,10 @@ pl: push_subscription_expires: Subskrypcja PuSH wygasa redownload: Odśwież awatar remove_avatar: Usun awatar + resend_confirmation: + already_confirmed: To konto zostało już potwierdzone + send: Wyślij ponownie e-mail z potwierdzeniem + success: E-mail z potwierdzeniem został wysłany! reset: Resetuj reset_password: Resetuj hasło resubscribe: Ponów subskrypcję @@ -270,7 +276,6 @@ pl: comment: none: Brak created_at: Zgłoszono - delete: Usuń id: ID mark_as_resolved: Oznacz jako rozwiązane mark_as_unresolved: Oznacz jako nierozwiązane @@ -280,9 +285,6 @@ pl: create_and_unresolve: Cofnij rozwiązanie i pozostaw notatkę delete: Usuń placeholder: Opisz wykonane akcje i inne szczegóły dotyczące tego zgłoszenia… - nsfw: - 'false': Nie oznaczaj jako NSFW - 'true': Oznaczaj jako NSFW reopen: Otwórz ponownie report: 'Zgłoszenie #%{id}' report_contents: Zawartość @@ -357,11 +359,8 @@ pl: delete: Usuń nsfw_off: Cofnij NSFW nsfw_on: Oznacz jako NSFW - execute: Wykonaj failed_to_execute: Nie udało się wykonać media: - hide: Ukryj zawartość multimedialną - show: Pokaż zawartość multimedialną title: Media no_media: Bez zawartości multimedialnej title: Wpisy konta @@ -377,6 +376,7 @@ pl: admin_mailer: new_report: body: Użytkownik %{reporter} zgłosił %{target} + body_remote: Użytkownik instancji %{domain} zgłosił %{target} subject: Nowe zgłoszenie na %{instance} (#%{id}) application_mailer: notification_preferences: Zmień ustawienia e-maili @@ -466,7 +466,7 @@ pl: archive_takeout: date: Data download: Pobierz swoje archiwum - hint_html: Możesz uzyskać archiwum swoich wpisów i wysłanej zawartości multimedialnej. Wyeksportowane dane będą dostępne w formacie ActivityPub, który możesz otworzyć w obsługujących go programach. + hint_html: Możesz uzyskać archiwum swoich wpisów i wysłanej zawartości multimedialnej. Wyeksportowane dane będą dostępne w formacie ActivityPub, który możesz otworzyć w obsługujących go programach. Możesz wyeksportować je po 7 dniach od poprzedniego eksportu. in_progress: Tworzenie archiwum… request: Uzyskaj archiwum size: Rozmiar @@ -613,20 +613,6 @@ pl: other: Pozostałe publishing: Publikowanie web: Sieć - push_notifications: - favourite: - title: "%{name} dodał Twój wpis do ulubionych" - follow: - title: "%{name} zaczął Cię śledzić" - group: - title: "%{count} powiadomień" - mention: - action_boost: Podbij - action_expand: Pokaż więcej - action_favourite: Dodaj do ulubionych - title: "%{name} wspomniał o Tobie" - reblog: - title: "%{name} podbił Twój wpis" remote_follow: acct: Podaj swój adres (nazwa@domena), z którego chcesz śledzić missing_resource: Nie udało się znaleźć adresu przekierowania z Twojej domeny @@ -696,7 +682,7 @@ pl: your_apps: Twoje aplikacje statuses: attached: - description: 'Przytwierdzony: %{attached}' + description: 'Załączono: %{attached}' image: few: "%{count} obrazy" many: "%{count} obrazów" @@ -707,6 +693,7 @@ pl: many: "%{count} filmów" one: "%{count} film" other: "%{count} filmów" + boosted_from_html: Podbito przez %{acct_link} content_warning: 'Ostrzeżenie o zawartości: %{warning}' disallowed_hashtags: one: 'zawiera niedozwolony hashtag: %{tags}' @@ -856,5 +843,6 @@ pl: users: invalid_email: Adres e-mail jest niepoprawny invalid_otp_token: Kod uwierzytelniający jest niepoprawny + otp_lost_help_html: Jeżeli utracisz dostęp do obu, możesz skontaktować się z %{email} seamless_external_login: Zalogowano z użyciem zewnętrznej usługi, więc ustawienia hasła i adresu e-mail nie są dostępne. signed_in_as: 'Zalogowany jako:' diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index a575998a8d..89cc26cf36 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -40,6 +40,7 @@ pt-BR: following: Seguindo media: Mídia moved_html: "%{name} se mudou para %{new_profile_link}:" + network_hidden: Essa informação não está disponível nothing_here: Não há nada aqui! people_followed_by: Pessoas que %{name} segue people_who_follow: Pessoas que seguem %{name} @@ -49,13 +50,12 @@ pt-BR: reserved_username: Este usuário está reservado roles: admin: Administrador + bot: Robô moderator: Moderador unfollow: Deixar de seguir admin: account_moderation_notes: - account: Moderador - create: Criar - created_at: Data + create: Criar uma advertência created_msg: Nota de moderação criada com sucesso! delete: Excluir destroyed_msg: Nota de moderação excluída com sucesso! @@ -72,6 +72,7 @@ pt-BR: title: Mudar e-mail para %{username} confirm: Confirmar confirmed: Confirmado + confirming: Confirmando demote: Rebaixar disable: Desativar disable_two_factor_authentication: Desativar 2FA @@ -80,6 +81,7 @@ pt-BR: domain: Domínio edit: Editar email: E-mail + email_status: Estado del correo electrónico enable: Ativar enabled: Ativado feed_url: URL do feed @@ -118,6 +120,10 @@ pt-BR: push_subscription_expires: Inscrição PuSH expira redownload: Atualizar avatar remove_avatar: Remover avatar + resend_confirmation: + already_confirmed: Este usuario ya está confirmado + send: Reenviar el correo electrónico de confirmación + success: "¡Correo electrónico de confirmación enviado con éxito!" reset: Anular reset_password: Modificar senha resubscribe: Reinscrever-se @@ -269,7 +275,6 @@ pt-BR: comment: none: Nenhum created_at: Denunciado - delete: Excluir id: ID mark_as_resolved: Marcar como resolvido mark_as_unresolved: Marcar como não resolvido @@ -278,10 +283,7 @@ pt-BR: create_and_resolve: Resolver com nota create_and_unresolve: Reabrir com nota delete: Excluir - placeholder: Descreva que ações foram tomadas, ou quaisquer atualizações sobre esta denúncia… - nsfw: - 'false': Mostrar mídias anexadas - 'true': Esconder mídias anexadas + placeholder: Descreva que ações foram tomadas, ou quaisquer outras atualizações relacionadas… reopen: Reabrir denúncia report: 'Denúncia #%{id}' report_contents: Conteúdos @@ -356,11 +358,8 @@ pt-BR: delete: Deletar nsfw_off: Marcar como não-sensível nsfw_on: Marcar como sensível - execute: Executar failed_to_execute: Falha em executar media: - hide: Esconder mídia - show: Mostrar mídia title: Mídia no_media: Não há mídia title: Postagens da conta @@ -376,6 +375,7 @@ pt-BR: admin_mailer: new_report: body: "%{reporter} denunciou %{target}" + body_remote: Alguém da instância %{domain} reportou %{target} subject: Nova denúncia sobre %{instance} (#%{id}) application_mailer: notification_preferences: Mudar preferências de e-mail @@ -465,7 +465,7 @@ pt-BR: archive_takeout: date: Data download: Baixe o seu arquivo - hint_html: Você pode pedir um arquivo dos seus toots e mídia enviada. Os dados exportados estarão no formato ActivityPub, que podem ser lidos por qualquer software compatível. + hint_html: Você pode pedir um arquivo dos seus toots e mídia enviada. Os dados exportados estarão no formato ActivityPub, que podem ser lidos por qualquer software compatível. Você pode pedir um arquivo a cada 7 dias. in_progress: Preparando seu arquivo... request: Solicitar o seu arquivo size: Tamanho @@ -550,7 +550,7 @@ pt-BR: subject: one: "Uma nova notificação desde o seu último acesso \U0001F418" other: "%{count} novas notificações desde o seu último acesso \U0001F418" - title: Enquanto você estava ausente… + title: Enquanto você estava ausente... favourite: body: 'Sua postagem foi favoritada por %{name}:' subject: "%{name} favoritou a sua postagem" @@ -595,20 +595,6 @@ pt-BR: other: Outro publishing: Publicação web: Web - push_notifications: - favourite: - title: "%{name} favoritou a sua postagem" - follow: - title: "%{name} está te seguindo" - group: - title: "%{count} notificações" - mention: - action_boost: Compartilhar - action_expand: Mostrar mais - action_favourite: Favoritar - title: "%{name} mencionou você" - reblog: - title: "%{name} compartilhou a sua postagem" remote_follow: acct: Insira o seu usuário@domínio do qual você quer seguir missing_resource: Não foi possível encontrar a URL de direcionamento para a sua conta @@ -682,6 +668,7 @@ pt-BR: video: one: "%{count} vídeo" other: "%{count} vídeos" + boosted_from_html: Compartilhada à partir de %{acct_link} content_warning: 'Aviso de conteúdo: %{warning}' disallowed_hashtags: one: 'continha a hashtag não permitida: %{tags}' @@ -714,77 +701,77 @@ pt-BR: Informação básica de conta: Se você se registrar nesse servidor, podemos pedir que você utilize um nome de usuário, um e-mail e uma senha. Você também pode adicionar informações extras como um nome de exibição e biografia; enviar uma imagem de perfil e imagem de cabeçalho. O nome de usuário, nome de exibição, biografia, imagem de perfil e imagem de cabeçalho são sempre listadas publicamente. - Posts, informação de seguidores e outras informações públicas: A lista de pessoas que você segue é listada publicamente, o mesmo é verdade para quem te segue. Quando você envia uma mensagem, a data e o horário são armazenados, assim como a aplicação que você usou para enviar a mensagem. Mensagens podem conter mídias anexadas, como imagens e vídeos. Posts públicos e não-listados estão disponíveis publicamente. Quando você destaca um post no seu perfil, isso também é uma informação pública. Seus posts são entregues aos seus seguidores e em alguns casos isso significa que eles são enviados para servidores diferentes e cópias são armazenadas nesses servidores. Quando você remove posts, essa informação também é entregue aos seus seguidores. O ato de compartilhar ou favoritar um outro post é sempre público. + Posts, informação de seguidores e outras informações públicas: A lista de pessoas que você segue é listada publicamente, o mesmo é verdade para quem te segue. Quando você envia uma mensagem, a data e o horário são armazenados, assim como a aplicação que você usou para enviar a mensagem. Mensagens podem conter mídias anexadas, como imagens e vídeos. Posts públicos e não-listados estão disponíveis publicamente. Quando você destaca um post no seu perfil, isso também é uma informação pública. Seus posts são entregues aos seus seguidores e em alguns casos isso significa que eles são enviados para servidores diferentes e cópias são armazenadas nesses servidores. Quando você remove posts, essa informação também é entregue aos seus seguidores. O ato de compartilhar ou favoritar um outro post é sempre público. Mensagens diretas e posts somente para seguidores: Todos os posts são armazenados e processados no servidor. Posts somente para seguidores são entregues aos seus seguidores e usuários que são mencionados neles; mensagens diretas são entregues somente aos usuários mencionados nelas. Em alguns casos isso significa que as mensagens são entregues para servidores diferentes e cópias são armazenadas nesses servidores. Nós fazemos esforços substanciais para limitar o acesso dessas mensagens somente para as pessoas autorizadas, mas outros servidores podem não fazer o mesmo. É importante portanto revisar os servidores à qual seus seguidores pertencem. Você pode usar uma opção para aprovar ou rejeitar novos seguidores manualmente nas configurações. Por favor tenha em mente que os operadores do servidor e de qualquer servidores do destinatário podem ver tais mensagens, e que os destinatários podem fazer capturas de tela, copiar ou de outra maneira compartilhar as mensagens. Não compartilhe informação confidencial pelo Mastodon. - IPs and other metadata: When you log in, we record the IP address you log in from, as well as the name of your browser application. All the logged in sessions are available for your review and revocation in the settings. The latest IP address used is stored for up to 12 months. We also may retain server logs which include the IP address of every request to our server. + IPs e outros metadados: Quando você faz se autentica, nos guardamos o endereço de IP que você usou ao se autenticar e o nome do seu navegador da internet. Todas as sessões autenticadas são disponíveis para serem analisadas e revogadas nas configurações. O último endereço de IP usado é guardado por até 12 meses. Nós também podemos reter históricos do servidor que incluem o endereço de IP de todas as requisições ao nosso servidor. - What do we use your information for? + Para que usamos os seus dados? - Any of the information we collect from you may be used in the following ways: + Toda informação que coletamos de você pode ser usada das seguintes maneiras: - To provide the core functionality of Mastodon. You can only interact with other people's content and post your own content when you are logged in. For example, you may follow other people to view their combined posts in your own personalized home timeline. - To aid moderation of the community, for example comparing your IP address with other known ones to determine ban evasion or other violations. - The email address you provide may be used to send you information, notifications about other people interacting with your content or sending you messages, and to respond to inquiries, and/or other requests or questions. + Para prover a funcionalidade básica do Mastodon. Você só pode interagir com o conteúdo de outras pessoas e postar seu próprio conteúdo estando autenticado. Por exemplo, você pode seguir outras pessoas para ver seus posts combinados na sua linha do tempo personalizada. + Para auxiliar na moderação da comunidade, por exemplo ao comparar o seu endereço de IP com outros endereços de IP conhecidos para determinar evasão de banimento e outras violações. + O endereço de email que você prover pode ser usado para lhe enviar informação, notificação sobre outras pessoas interagindo com o seu conteúdo ou lhe enviando mensagens e para responder a questões ou outros pedidos. - How do we protect your information? + Como protegemos as suas informações? - We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information. Among other things, your browser session, as well as the traffic between your applications and the API, are secured with SSL, and your password is hashed using a strong one-way algorithm. You may enable two-factor authentication to further secure access to your account. + Nós implementamos diversas medidas de segurança para manter a segurança das suas informações pessoais quando você as acessa ou as envia. Entre outras coisas, sua sessão do navegador, bem como o tráfego entre as aplicações e a API são asseguradas usando SSL e a sua senha é guardada usando um algoritmo forte de encriptação de mão única. Você pode ativar autenticação em dois fatores como forma de aumentar a segurança no acesso à sua conta. - What is our data retention policy? + Qual é a nossa política de retenção de dados? - We will make a good faith effort to: + Nós fazemos esforços substanciais para: - Retain server logs containing the IP address of all requests to this server, in so far as such logs are kept, no more than 90 days. - Retain the IP addresses associated with registered users no more than 12 months. + Reter o histórico do servidor contendo os endereços de IP de todas as requisições feitas à esse servidor, e com respeito a quanto tempo esses logs são retidos, não mais que 90 dias. + Reter o endereço de IP associado com usuários registrados não mais que 12 meses. - You can request and download an archive of your content, including your posts, media attachments, profile picture, and header image. + Você pode pedir e fazer o download de um arquivo de todo o conteúdo da sua conta, incluindo as suas mensagens, suas mídias anexadas, imagem de perfil e imagem de topo. - You may irreversibly delete your account at any time. + Você pode remover irreversivelmente a sua conta a qualquer momento. - Do we use cookies? + Nós usamos cookies? - Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account. + Sim. Cookies são pequenos arquivos que um site ou serviço transfere ao seu disco rígido do seu computador através do seu navegador da web (se você permitir). Esses cookies permitem ao site conhecer seu navegador e, se você tiver uma conta registrada, associá-lo a sua conta. - We use cookies to understand and save your preferences for future visits. + Nós usamos cookies para compreender e salvar suas preferências para visitas futuras. - Do we disclose any information to outside parties? + Nós compartilhamos qualquer informação para terceiros? - We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety. + Nós não vendemos, trocamos ou transferimos de qualquer maneira informação que pode lhe identificar à terceiros. Isso não inclui terceiros que podemos nos auxiliam a operar o nosso site, realizar nossos negócios ou lhe prestar serviços, contanto que esses terceiros se comprometam a manter essa informação confidencial. Nós podemos também divulgar informação quando acreditamos que é apropriado para obedecer a lei, para fazer cumprir nossas políticas ou proteger nossos direitos, propriedade ou segurança ou o direito, propriedade e segurança de outrem. - Your public content may be downloaded by other servers in the network. Your public and followers-only posts are delivered to the servers where your followers reside, and direct messages are delivered to the servers of the recipients, in so far as those followers or recipients reside on a different server than this. + Seu conteúdo público pode ser descarregado por outros servidores na rede. Suas mensagens públicas e somente para seus seguidores são entregues aos servidores onde seus seguidores resides e as suas mensagens diretas são entregues ao servidor dos usuários mencionados nelas, contanto que esses seguidores ou usuários residam em um servidor diferente deste. - When you authorize an application to use your account, depending on the scope of permissions you approve, it may access your public profile information, your following list, your followers, your lists, all your posts, and your favourites. Applications can never access your e-mail address or password. + Quando você autoriza uma aplicação a usar sua conta, dependendo do escopo de permissões que você aprovar, a aplicação pode acessar sua informação pública, a lista de usuários que você segue, seus seguidores, suas listas, suas mensagens e suas mensagens favoritas. Aplicações nunca podem acessar o seu endereço de e-mail ou senha. - Children's Online Privacy Protection Act Compliance + Conformidade com a COPPA (Children's Online Privacy Protection Act) - Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site. + Nosso site, produto e serviços são direcionados à pessoas que tem ao menos 13 anos de idade. Se esse servidor está hospedado nos EUA e você tem menos de 13 anos, de acordo com os requerimentos da COPPA (Children's Online Privacy Protection Act) não use este site. - Changes to our Privacy Policy + Mudanças à nossa política de privacidade - If we decide to change our privacy policy, we will post those changes on this page. + Se decidirmos mudar nossa política de privacidade, nós iremos disponibilizar as mudanças nesta página. - This document is CC-BY-SA. It was last updated March 7, 2018. + Este documento é CC-BY-SA. Ele foi atualizado pela última vez em 7 de março de 2018. - Originally adapted from the Discourse privacy policy. + Adaptado originalmente a partir da política de privacidade Discourse. title: "%{instance} Termos de Serviço e Política de Privacidade" time: formats: @@ -831,5 +818,6 @@ pt-BR: users: invalid_email: O endereço de e-mail é inválido invalid_otp_token: Código de autenticação inválido + otp_lost_help_html: Se você perder o acesso à ambos, você pode entrar em contato com %{email} seamless_external_login: Você está logado usando um serviço externo, então configurações de e-mail e password não estão disponíveis. signed_in_as: 'Acesso como:' diff --git a/config/locales/pt.yml b/config/locales/pt.yml index fb2a6cad1c..a1370c91d5 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -52,9 +52,7 @@ pt: unfollow: Deixar de seguir admin: account_moderation_notes: - account: Moderador create: Criar - created_at: Data created_msg: Nota de moderação criada com sucesso! delete: Eliminar destroyed_msg: Nota de moderação excluída com sucesso! @@ -63,6 +61,7 @@ pt: by_domain: Domínio confirm: Confirme confirmed: Confirmado + confirming: Confirmer demote: Rebaixar disable: Desativar disable_two_factor_authentication: Desativar 2FA @@ -71,6 +70,7 @@ pt: domain: Domínio edit: Editar email: E-mail + email_status: État de la messagerie enable: Ativar enabled: Ativado feed_url: URL do Feed @@ -108,6 +108,10 @@ pt: public: Público push_subscription_expires: A Inscrição PuSH expira redownload: Atualizar avatar + resend_confirmation: + already_confirmed: Cet utilisateur est déjà confirmé + send: Renvoyer un courriel de confirmation + success: Email de confirmation envoyé avec succès! reset: Restaurar reset_password: Reset palavra-passe resubscribe: Reinscrever @@ -244,12 +248,8 @@ pt: are_you_sure: Tens a certeza? comment: none: Nenhum - delete: Eliminar id: ID mark_as_resolved: Marcar como resolvido - nsfw: - 'false': Mostrar imagens/vídeos - 'true': Esconder imagens/vídeos report: 'Denúncia #%{id}' report_contents: Conteúdos reported_account: Conta denunciada @@ -314,11 +314,8 @@ pt: delete: Eliminar nsfw_off: NSFW OFF nsfw_on: NSFW ON - execute: Executar failed_to_execute: Falhou ao executar media: - hide: Esconder média - show: Mostrar média title: Média no_media: Não há média title: Estado das contas @@ -537,20 +534,6 @@ pt: other: Outro publishing: Publicação web: Web - push_notifications: - favourite: - title: "%{name} adicionou o teu post aos favoritos" - follow: - title: "%{name} começou a seguir-te" - group: - title: "%{count} notificações" - mention: - action_boost: Partilhar - action_expand: Mostrar mais - action_favourite: Adicionar aos favoritos - title: "%{name} mencionou-te" - reblog: - title: "%{name} partilhou o teu post" remote_follow: acct: Entre seu usuário@domínio do qual quer seguir missing_resource: Não foi possível achar a URL de redirecionamento para sua conta diff --git a/config/locales/ru.yml b/config/locales/ru.yml index bf42257581..89aefc1cd9 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -4,6 +4,7 @@ ru: about_hashtag_html: Это публичные статусы, отмеченные хэштегом #%{hashtag}. Вы можете взаимодействовать с ними при наличии у Вас аккаунта в глобальной сети Mastodon. about_mastodon_html: Mastodon - это свободная социальная сеть с открытым исходным кодом. Как децентрализованная альтернатива коммерческим платформам, Mastodon предотвращает риск монополизации Вашего общения одной компанией. Выберите сервер, которому Вы доверяете — что бы Вы ни выбрали, Вы сможете общаться со всеми остальными. Любой может запустить свой собственный узел Mastodon и участвовать в социальной сети совершенно бесшовно. about_this: Об этом узле + administered_by: 'Администратор узла:' closed_registrations: В данный момент регистрация на этом узле закрыта. contact: Связаться contact_missing: Не установлено @@ -39,6 +40,7 @@ ru: following: Подписан(а) media: Медиаконтент moved_html: "%{name} переехал(а) на %{new_profile_link}:" + network_hidden: Эта информация недоступна nothing_here: Здесь ничего нет! people_followed_by: Люди, на которых подписан(а) %{name} people_who_follow: Подписчики %{name} @@ -48,21 +50,29 @@ ru: reserved_username: Имя пользователя зарезервировано roles: admin: Администратор + bot: Бот moderator: Модератор unfollow: Отписаться admin: account_moderation_notes: - account: Модератор create: Создать - created_at: Дата created_msg: Заметка модератора успешно создана! delete: Удалить destroyed_msg: Заметка модератора успешно удалена! accounts: are_you_sure: Вы уверены? + avatar: Аватар by_domain: Домен + change_email: + changed_msg: E-mail аккаунта успешно изменён! + current_email: Текущий e-mail + label: Сменить e-mail + new_email: Новый e-mail + submit: Сменить e-mail + title: Сменить e-mail для %{username} confirm: Подтвердить confirmed: Подтверждено + confirming: Подтверждение demote: Разжаловать disable: Отключить disable_two_factor_authentication: Отключить 2FA @@ -71,6 +81,7 @@ ru: domain: Домен edit: Изменить email: E-mail + email_status: Статус e-mail enable: Включить enabled: Включен feed_url: URL фида @@ -108,6 +119,11 @@ ru: public: Публичный push_subscription_expires: Подписка PuSH истекает redownload: Обновить аватар + remove_avatar: Удалить аватар + resend_confirmation: + already_confirmed: Этот пользователь уже подтвержден + send: Повторно отправить подтверждение по электронной почте + success: Письмо с подтверждением успешно отправлено! reset: Сбросить reset_password: Сбросить пароль resubscribe: Переподписаться @@ -128,13 +144,16 @@ ru: statuses: Статусы subscribe: Подписаться title: Аккаунты + unconfirmed_email: Неподтверждённый e-mail undo_silenced: Снять глушение undo_suspension: Снять блокировку unsubscribe: Отписаться username: Имя пользователя - web: WWW + web: Веб action_logs: actions: + assigned_to_self_report: "%{name} назначил(а) жалобу %{target} на себя" + change_email_user: "%{name} сменил(а) e-mail пользователя %{target}" confirm_user: "%{name} подтвердил(а) e-mail адрес пользователя %{target}" create_custom_emoji: "%{name} загрузил(а) новый эмодзи %{target}" create_domain_block: "%{name} заблокировал(а) домен %{target}" @@ -150,10 +169,13 @@ ru: enable_user: "%{name} включил(а) вход пользователя %{target}" memorialize_account: "%{name} перевел(а) аккаунт пользователя %{target} в режим памятника" promote_user: "%{name} повысил(а) пользователя %{target}" + remove_avatar_user: "%{name} удалил(а) аватар пользователя %{target}" + reopen_report: "%{name} переоткрыл(а) жалобу %{target}" reset_password_user: "%{name} сбросил(а) пароль пользователя %{target}" - resolve_report: "%{name} dismissed report %{target}" + resolve_report: "%{name} решил(а) жалобу %{target}" silence_account: "%{name} заглушил(а) аккаунт %{target}" suspend_account: "%{name} заморозил(а) аккаунт %{target}" + unassigned_report: "%{name} сняла назначение жалобы %{target}" unsilence_account: "%{name} снял(а) глушение аккаунта %{target}" unsuspend_account: "%{name} разморозил(а) аккаунт %{target}" update_custom_emoji: "%{name} обновил(а) эмодзи %{target}" @@ -241,28 +263,44 @@ ru: expired: Истёкшие title: Фильтр title: Приглашения + report_notes: + created_msg: Примечание жалобы создано! + destroyed_msg: Примечание жалобы удалено! reports: + account: + note: заметка + report: жалоба action_taken_by: 'Действие предпринято:' are_you_sure: Вы уверены? + assign_to_self: Назначить себе + assigned: Назначенный модератор comment: none: Нет - delete: Удалить + created_at: Создано id: ID mark_as_resolved: Отметить как разрешенную - nsfw: - 'false': Показать мультимедийные вложения - 'true': Скрыть мультимедийные вложения + mark_as_unresolved: Отметить как неразрешённую + notes: + create: Добавить заметку + create_and_resolve: Разрешить с заметкой + create_and_unresolve: Переоткрыть с заметкой + delete: Удалить + placeholder: Опишите, какие действия были приняты, или любые другие подробности… + reopen: Переоткрыть жалобу report: 'Жалоба #%{id}' report_contents: Содержимое reported_account: Аккаунт нарушителя reported_by: Отправитель жалобы resolved: Разрешено + resolved_msg: Жалоба успешно обработана! silence_account: Заглушить аккаунт status: Статус suspend_account: Блокировать аккаунт target: Цель title: Жалобы + unassign: Снять назначение unresolved: Неразрешенные + updated_at: Обновлена view: Просмотреть settings: activity_api_enabled: @@ -322,11 +360,8 @@ ru: delete: Удалить nsfw_off: Выключить NSFW nsfw_on: Включить NSFW - execute: Выполнить failed_to_execute: Не удалось выполнить media: - hide: Скрыть медиаконтент - show: Показать медиаконтент title: Медиаконтент no_media: Без медиаконтента title: Статусы аккаунта @@ -342,6 +377,7 @@ ru: admin_mailer: new_report: body: "%{reporter} подал(а) жалобу на %{target}" + body_remote: Кто-то с узла %{domain} пожаловался на %{target} subject: Новая жалоба, узел %{instance} (#%{id}) application_mailer: notification_preferences: Изменить настройки e-mail @@ -431,7 +467,7 @@ ru: archive_takeout: date: Дата download: Скачать ваш архив - hint_html: Вы можете запросить архив своих статусов и загруженных медиа-файлов. Экспортированные данные будут в формате ActivityPub, который можно прочесть любой соответствующей программой. + hint_html: Вы можете запросить архив своих статусов и загруженных медиа-файлов. Экспортированные данные будут в формате ActivityPub, который можно прочесть любой соответствующей программой. Запрашивать архив можно каждые 7 дней. in_progress: Собирается ваш архив... request: Запросить ваш архив size: Размер @@ -447,6 +483,8 @@ ru: lock_link: Закройте аккаунт purge: Удалить из подписчиков success: + few: В процессе мягкой блокировки подписчиков с %{count} доменов... + many: В процессе мягкой блокировки подписчиков с %{count} доменов... one: В процессе мягкой блокировки подписчиков с одного домена... other: В процессе мягкой блокировки подписчиков с %{count} доменов... true_privacy_html: Пожалуйста, заметьте, что настоящая конфиденциальность может быть достигнута только при помощи end-to-end шифрования. @@ -457,6 +495,8 @@ ru: powered_by: работает на %{link} save_changes: Сохранить изменения validation_errors: + few: Что-то здесь не так! Пожалуйста, прочитайте о %{count} ошибках ниже + many: Что-то здесь не так! Пожалуйста, прочитайте о %{count} ошибках ниже one: Что-то здесь не так! Пожалуйста, прочитайте об ошибке ниже other: Что-то здесь не так! Пожалуйста, прочитайте о %{count} ошибках ниже imports: @@ -552,7 +592,7 @@ ru: units: billion: млрд million: млн - quadrillion: Q + quadrillion: квадрлн thousand: тыс trillion: трлн unit: '' @@ -567,25 +607,15 @@ ru: other: Другое publishing: Публикация web: WWW - push_notifications: - favourite: - title: Ваш статус понравился %{name} - follow: - title: "%{name} теперь подписан(а) на Вас" - group: - title: "%{count} уведомлений" - mention: - action_boost: Продвинуть - action_expand: Развернуть - action_favourite: Нравится - title: Вас упомянул(а) %{name} - reblog: - title: "%{name} продвинул(а) Ваш статус" remote_follow: acct: Введите username@domain, откуда Вы хотите подписаться missing_resource: Поиск требуемого перенаправления URL для Вашего аккаунта завершился неудачей proceed: Продолжить подписку prompt: 'Вы хотите подписаться на:' + remote_unfollow: + error: Ошибка + title: Заголовок + unfollowed: Отписаны sessions: activity: Последняя активность browser: Браузер @@ -655,7 +685,13 @@ ru: many: "%{count} видео" one: "%{count} видео" other: "%{count} видео" + boosted_from_html: Продвижение польз. %{acct_link} content_warning: 'Спойлер: %{warning}' + disallowed_hashtags: + few: 'содержались запрещённые хэштеги: %{tags}' + many: 'содержались запрещённые хэштеги: %{tags}' + one: 'содержался запрещённый хэштег: %{tags}' + other: 'содержались запрещённые хэштеги: %{tags}' open_in_web: Открыть в WWW over_character_limit: превышен лимит символов (%{max}) pin_errors: @@ -678,9 +714,88 @@ ru: reblogged: продвинул(а) sensitive_content: Чувствительный контент terms: + body_html: | + Privacy Policy + What information do we collect? + + + Basic account information: If you register on this server, you may be asked to enter a username, an e-mail address and a password. You may also enter additional profile information such as a display name and biography, and upload a profile picture and header image. The username, display name, biography, profile picture and header image are always listed publicly. + Posts, following and other public information: The list of people you follow is listed publicly, the same is true for your followers. When you submit a message, the date and time is stored as well as the application you submitted the message from. Messages may contain media attachments, such as pictures and videos. Public and unlisted posts are available publicly. When you feature a post on your profile, that is also publicly available information. Your posts are delivered to your followers, in some cases it means they are delivered to different servers and copies are stored there. When you delete posts, this is likewise delivered to your followers. The action of reblogging or favouriting another post is always public. + Direct and followers-only posts: All posts are stored and processed on the server. Followers-only posts are delivered to your followers and users who are mentioned in them, and direct posts are delivered only to users mentioned in them. In some cases it means they are delivered to different servers and copies are stored there. We make a good faith effort to limit the access to those posts only to authorized persons, but other servers may fail to do so. Therefore it's important to review servers your followers belong to. You may toggle an option to approve and reject new followers manually in the settings. Please keep in mind that the operators of the server and any receiving server may view such messages, and that recipients may screenshot, copy or otherwise re-share them. Do not share any dangerous information over Mastodon. + IPs and other metadata: When you log in, we record the IP address you log in from, as well as the name of your browser application. All the logged in sessions are available for your review and revocation in the settings. The latest IP address used is stored for up to 12 months. We also may retain server logs which include the IP address of every request to our server. + + + + + What do we use your information for? + + Any of the information we collect from you may be used in the following ways: + + + To provide the core functionality of Mastodon. You can only interact with other people's content and post your own content when you are logged in. For example, you may follow other people to view their combined posts in your own personalized home timeline. + To aid moderation of the community, for example comparing your IP address with other known ones to determine ban evasion or other violations. + The email address you provide may be used to send you information, notifications about other people interacting with your content or sending you messages, and to respond to inquiries, and/or other requests or questions. + + + + + How do we protect your information? + + We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information. Among other things, your browser session, as well as the traffic between your applications and the API, are secured with SSL, and your password is hashed using a strong one-way algorithm. You may enable two-factor authentication to further secure access to your account. + + + + What is our data retention policy? + + We will make a good faith effort to: + + + Retain server logs containing the IP address of all requests to this server, in so far as such logs are kept, no more than 90 days. + Retain the IP addresses associated with registered users no more than 12 months. + + + You can request and download an archive of your content, including your posts, media attachments, profile picture, and header image. + + You may irreversibly delete your account at any time. + + + + Do we use cookies? + + Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account. + + We use cookies to understand and save your preferences for future visits. + + + + Do we disclose any information to outside parties? + + We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety. + + Your public content may be downloaded by other servers in the network. Your public and followers-only posts are delivered to the servers where your followers reside, and direct messages are delivered to the servers of the recipients, in so far as those followers or recipients reside on a different server than this. + + When you authorize an application to use your account, depending on the scope of permissions you approve, it may access your public profile information, your following list, your followers, your lists, all your posts, and your favourites. Applications can never access your e-mail address or password. + + + + Children's Online Privacy Protection Act Compliance + + Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site. + + + + Changes to our Privacy Policy + + If we decide to change our privacy policy, we will post those changes on this page. + + This document is CC-BY-SA. It was last updated March 7, 2018. + + Originally adapted from the Discourse privacy policy. title: Условия обслуживания и политика конфиденциальности %{instance} themes: + contrast: Высококонтрастная default: Mastodon + mastodon-light: Mastodon (светлая) time: formats: default: "%b %d, %Y, %H:%M" @@ -725,6 +840,7 @@ ru: title: Добро пожаловать на борт, %{name}! users: invalid_email: Введенный e-mail неверен - invalid_otp_token: Введен неверный код + invalid_otp_token: Введен неверный код двухфакторной аутентификации + otp_lost_help_html: Если Вы потеряли доступ к обоим, свяжитесь с %{email} seamless_external_login: Вы залогинены через сторонний сервис, поэтому настройки e-mail и пароля недоступны. signed_in_as: 'Выполнен вход под именем:' diff --git a/config/locales/simple_form.ar.yml b/config/locales/simple_form.ar.yml index 28cfa8ab74..3d555718c1 100644 --- a/config/locales/simple_form.ar.yml +++ b/config/locales/simple_form.ar.yml @@ -4,22 +4,18 @@ ar: hints: defaults: avatar: ملف PNG أو GIF أو JPG. حجمه على أقصى تصدير 2MB. سيتم تصغيره إلى 400x400px + bot: يُعلِم أنّ هذا الحساب لا يمثل شخصًا digest: تُرسَل إليك بعد مُضيّ مدة مِن خمول نشاطك و فقط إذا ما تلقيت رسائل شخصية مباشِرة أثناء فترة غيابك مِن الشبكة - display_name: - one: 1 حرف باقي - other: %{count} حروف متبقية fields: يُمكنك عرض 4 عناصر على شكل جدول في ملفك الشخصي header: ملف PNG أو GIF أو JPG. حجمه على أقصى تصدير 2MB. سيتم تصغيره إلى 700x335px locked: يتطلب منك الموافقة يدويا على طلبات المتابعة - note: - one: 1 حرف متبقي - other: %{count} حروف متبقية + setting_hide_network: الحسابات التي تُتابعها و التي تُتابِعك على حد سواء لن تُعرَض على صفحتك الشخصية setting_noindex: ذلك يؤثر على حالة ملفك الشخصي و صفحاتك setting_theme: ذلك يؤثر على الشكل الذي سيبدو عليه ماستدون عندما تقوم بالدخول مِن أي جهاز. imports: data: ملف CSV تم تصديره مِن مثيل خادوم ماستدون آخر sessions: - otp: قم بإدخال رمز المصادقة بخطوتين مِن هاتفك أو إستخدم أحد رموز النفاذ الإحتياطية. + otp: 'قم بإدخال رمز المصادقة بخطوتين الذي قام بتوليده تطبيق جهازك أو إستخدم أحد رموز النفاذ الإحتياطية :' user: filtered_languages: سوف يتم تصفية و إخفاء اللغات المختارة من خيوطك العمومية labels: @@ -29,6 +25,7 @@ ar: value: المحتوى defaults: avatar: الصورة الرمزية + bot: إنّ هذا الحساب روبوت آلي confirm_new_password: تأكيد كلمة السر الجديدة confirm_password: تأكيد كلمة السر current_password: كلمة السر الحالية @@ -52,6 +49,7 @@ ar: setting_default_sensitive: إعتبر الوسائط دائما كمحتوى حساس setting_delete_modal: إظهار مربع حوار للتأكيد قبل حذف أي تبويق setting_display_sensitive_media: دائمًا إظهار الوسائط الحساسة + setting_hide_network: إخفِ شبكتك setting_noindex: عدم السماح لمحركات البحث بفهرسة ملفك الشخصي setting_reduce_motion: تخفيض عدد الصور في الوسائط المتحركة setting_system_font_ui: إستخدم الخطوط الإفتراضية للنظام diff --git a/config/locales/simple_form.ca.yml b/config/locales/simple_form.ca.yml index 1b04da90ad..5df7bf77b2 100644 --- a/config/locales/simple_form.ca.yml +++ b/config/locales/simple_form.ca.yml @@ -4,6 +4,7 @@ ca: hints: defaults: avatar: PNG, GIF o JPG. Màxim 2MB. S'escalarà a 400x400px + bot: Aquest compte realitza principalment accions automatitzades i pot no estar controlat per cap persona digest: Només s'envia després d'un llarg període d'inactivitat amb un resum de les mencions que has rebut en la teva absència display_name: one: 1 càracter restant @@ -14,12 +15,13 @@ ca: note: one: 1 càracter restant other: %{count} caràcters restants + setting_hide_network: Qui tu segueixes i els que et segueixen a tu no es mostraran en el teu perfil setting_noindex: Afecta el teu perfil públic i les pàgines d'estat setting_theme: Afecta l'aspecte de Mastodon quan es visita des de qualsevol dispositiu. imports: data: Fitxer CSV exportat des de una altra instància de Mastodon sessions: - otp: Introdueix el codi de dos factors des del teu telèfon o utilitza un dels teus codis de recuperació. + otp: 'Introdueix el codi de dos factors generat per el teu telèfon o utilitza un dels teus codis de recuperació:' user: filtered_languages: Les llengües seleccionades s'eliminaran de les línies de temps públiques labels: @@ -29,6 +31,7 @@ ca: value: Contingut defaults: avatar: Avatar + bot: Aquest compte és un bot confirm_new_password: Confirma la contrasenya nova confirm_password: Confirma la contrasenya current_password: Contrasenya actual @@ -52,6 +55,7 @@ ca: setting_default_sensitive: Marca sempre els elements multimèdia com a sensibles setting_delete_modal: Mostra la finestra de confirmació abans de suprimir un toot setting_display_sensitive_media: Mostra sempre els elements multimèdia marcats com a sensibles + setting_hide_network: Amaga la teva xarxa setting_noindex: Desactivació de la indexació del motor de cerca setting_reduce_motion: Redueix el moviment en animacions setting_system_font_ui: Utilitza el tipus de lletra predeterminat del sistema diff --git a/config/locales/simple_form.co.yml b/config/locales/simple_form.co.yml new file mode 100644 index 0000000000..8d24704359 --- /dev/null +++ b/config/locales/simple_form.co.yml @@ -0,0 +1,81 @@ +--- +co: + simple_form: + hints: + defaults: + avatar: Furmatu PNG, GIF o JPG. 2Mo o menu. Sarà ridottu à 400x400px + bot: Avisa a ghjente chì stu contu ùn riprisenta micca una parsona + digest: Solu mandatu dopu à una longa perioda d’inattività, è solu s’elli ci sò novi missaghji diretti + display_name: + one: Ci ferma 1 caratteru + other: Ci fermanu %{count} caratteri + fields: Pudete avè fin’à 4 elementi mustrati cum’un tavulone nant’à u vostru prufile + header: Furmatu PNG, GIF o JPG. 2Mo o menu. Sarà ridottu à 700x335px + locked: Duvarete appruvà e dumande d’abbunamentu + note: + one: Ci ferma 1 caratteru + other: Ci fermanu %{count} caratteri + setting_noindex: Tocca à u vostru prufile pubblicu è i vostri statuti + setting_theme: Tocca à l’apparenza di Mastodon quandu site cunnettatu·a da qualch’apparechju. + imports: + data: Un fugliale CSV da un’altr’istanza di Mastodon + sessions: + otp: 'Entrate u codice d’identificazione à dui fattori nant’à u vostru telefuninu, o unu di i vostri codici di ricuperazione:' + user: + filtered_languages: Ùn viderete micca e lingue selezziunate nant’à e linee pubbliche + labels: + account: + fields: + name: Label + value: Cuntinutu + defaults: + avatar: Ritrattu di prufile + bot: Stu contu hè un bot + confirm_new_password: Cunfirmà a nova chjave d’accessu + confirm_password: Cunfirmà a chjave d’accessu + current_password: Chjave d’accessu attuale + data: Dati + display_name: Nome pubblicu + email: Indirizzu e-mail + expires_in: Spira dopu à + fields: Metadata di u prufile + filtered_languages: Lingue filtrate + header: Ritrattu di cuprendula + locale: Lingua + locked: Privatizà u contu + max_uses: Numeru massimale d’utilizazione + new_password: Nova chjave d’accessu + note: Descrizzione + otp_attempt: Codice d’identificazione à dui fattori + password: Chjave d’accessu + setting_auto_play_gif: Lettura autumatica di i GIF animati + setting_boost_modal: Mustrà una cunfirmazione per sparte un statutu + setting_default_privacy: Cunfidenzialità di i statuti + setting_default_sensitive: Sempre cunsiderà media cum’è sensibili + setting_delete_modal: Mustrà une cunfirmazione per toglie un statutu + setting_display_sensitive_media: Sempre mustrà media marcati cum’è sensibili + setting_noindex: Dumandà à i motori di ricerca internet d’un pudè micca esse truvatu·a cusì + setting_reduce_motion: Fà chì l’animazione vanu più pianu + setting_system_font_ui: Pulizza di caratteri di u sistemu + setting_theme: Tema di u situ + setting_unfollow_modal: Mustrà una cunfirmazione per siguità qualch’unu + severity: Severità + type: Tippu d’impurtazione + username: Cugnome + username_or_email: Cugnome o Email + interactions: + must_be_follower: Piattà e nutificazione di quelli·e ch’ùn vi seguitanu + must_be_following: Piattà e nutificazione di quelli·e ch’ùn seguitate + must_be_following_dm: Bluccà e missaghji diretti di quelli·e ch’ùn seguitate + notification_emails: + digest: Mandà e-mail di ricapitulazione + favourite: Mandà un’e-mail quandu qualch’unu aghjunghje i mo statuti à i so favuriti + follow: Mandà un’e-mail quandu qualch’unu mi seguita + follow_request: Mandà un’e-mail quandu qualch’unu vole seguitami + mention: Mandà un’e-mail quandu qualch’unu mi mintuva + reblog: Mandà un’e-mail quandu qualch’unu sparte i mo statuti + 'no': Nò + required: + mark: "*" + text: riquisiti + 'yes': Ié diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml index a9d650a268..1bf1cbf78c 100644 --- a/config/locales/simple_form.de.yml +++ b/config/locales/simple_form.de.yml @@ -4,6 +4,7 @@ de: hints: defaults: avatar: PNG, GIF oder JPG. Maximal 2 MB. Wird auf 400×400 px herunterskaliert + bot: Warnt Besucher das dieser Nutzer keine echte Person darstellt digest: Wenn du lange Zeit inaktiv bist, wird dir eine Zusammenfassung von Erwähnungen in deiner Abwesenheit zugeschickt display_name: one: 1 Zeichen verbleibt @@ -19,7 +20,7 @@ de: imports: data: CSV-Datei, die aus einer anderen Mastodon-Instanz exportiert wurde sessions: - otp: Gib den Zwei-Faktor-Authentisierungs-Code von deinem Telefon ein oder benutze einen deiner Wiederherstellungscodes. + otp: 'Gib den Zwei-Faktor-Authentisierungscode von deinem Telefon ein oder benutze einen deiner Wiederherstellungscodes:' user: filtered_languages: Ausgewählte Sprachen werden aus deinen öffentlichen Zeitleisten gefiltert labels: @@ -29,6 +30,7 @@ de: value: Inhalt defaults: avatar: Profilbild + bot: Dies ist ein bot Benutzer confirm_new_password: Neues Passwort bestätigen confirm_password: Passwort bestätigen current_password: Derzeitiges Passwort diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index f635bf441b..851b678e1a 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -4,6 +4,7 @@ en: hints: defaults: avatar: PNG, GIF or JPG. At most 2MB. Will be downscaled to 400x400px + bot: This account mainly performs automated actions and might not be monitored digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence display_name: one: 1 character left @@ -14,12 +15,13 @@ en: note: one: 1 character left other: %{count} characters left + setting_hide_network: Who you follow and who follows you will not be shown on your profile setting_noindex: Affects your public profile and status pages setting_skin: Reskins the selected Mastodon flavour imports: data: CSV file exported from another Mastodon instance sessions: - otp: Enter the Two-factor code from your phone or use one of your recovery codes. + otp: 'Enter the two-factor code generated by your phone app or use one of your recovery codes:' user: filtered_languages: Checked languages will be filtered from public timelines for you labels: @@ -29,6 +31,7 @@ en: value: Content defaults: avatar: Avatar + bot: This is a bot account confirm_new_password: Confirm new password confirm_password: Confirm password current_password: Current password @@ -53,6 +56,7 @@ en: setting_delete_modal: Show confirmation dialog before deleting a toot setting_display_sensitive_media: Always show media marked as sensitive setting_favourite_modal: Show confirmation dialog before favouriting + setting_hide_network: Hide your network setting_noindex: Opt-out of search engine indexing setting_reduce_motion: Reduce motion in animations setting_skin: Skin diff --git a/config/locales/simple_form.eo.yml b/config/locales/simple_form.eo.yml index 41a0c26aab..4027c1b603 100644 --- a/config/locales/simple_form.eo.yml +++ b/config/locales/simple_form.eo.yml @@ -4,26 +4,34 @@ eo: hints: defaults: avatar: Formato PNG, GIF aŭ JPG. Ĝis 2MB. Estos malgrandigita al 400x400px + bot: Tiu konto ĉefe faras aŭtomatajn agojn, kaj povas esti ne kontrolata digest: Sendita nur post longa tempo de neaktiveco, kaj nur se vi ricevis personan mesaĝon en via foresto display_name: one: 1 signo restas other: %{count} signoj restas + fields: Vi povas havi ĝis 4 tabelajn elementojn en via profilo header: Formato PNG, GIF aŭ JPG. Ĝis 2MB. Estos malgrandigita al 700x335px locked: Vi devos aprobi ĉiun peton de sekvado mane note: one: 1 signo restas other: %{count} signoj restas + setting_hide_network: Tiuj, kiujn vi sekvas, kaj tiuj, kiuj sekvas vin ne estos videblaj en via profilo setting_noindex: Influas vian publikan profilon kaj mesaĝajn paĝojn setting_theme: Influas kiel Mastodon aspektas post ensaluto de ajna aparato. imports: data: CSV-dosiero el alia nodo de Mastodon sessions: - otp: Enmetu la kodon de dufaktora aŭtentigo el via telefono aŭ uzu unu el la realiraj kodoj. + otp: 'Enmetu la kodon de dufaktora aŭtentigo el via telefono aŭ uzu unu el viaj realiraj kodoj:' user: filtered_languages: Markitaj lingvoj estos elfiltritaj de publikaj tempolinioj por vi labels: + account: + fields: + name: Etikedo + value: Enhavo defaults: avatar: Profilbildo + bot: Tio estas robota konto confirm_new_password: Konfirmi novan pasvorton confirm_password: Konfirmi pasvorton current_password: Nuna pasvorto @@ -31,6 +39,7 @@ eo: display_name: Publika nomo email: Retadreso expires_in: Eksvalidiĝas post + fields: Profilaj metadatumoj filtered_languages: Filtritaj lingvoj header: Fonbildo locale: Lingvo @@ -46,6 +55,7 @@ eo: setting_default_sensitive: Ĉiam marki aŭdovidaĵojn tiklaj setting_delete_modal: Montri fenestron por konfirmi antaŭ ol forigi mesaĝon setting_display_sensitive_media: Ĉiam montri aŭdovidaĵojn markitajn tiklaj + setting_hide_network: Kaŝi viajn sekvantojn kaj sekvatojn setting_noindex: Ellistiĝi de retserĉila indeksado setting_reduce_motion: Malrapidigi animaciojn setting_system_font_ui: Uzi la dekomencan tiparon de la sistemo diff --git a/config/locales/simple_form.eu.yml b/config/locales/simple_form.eu.yml index d856feac58..22b71bac1b 100644 --- a/config/locales/simple_form.eu.yml +++ b/config/locales/simple_form.eu.yml @@ -28,5 +28,14 @@ eu: filtered_languages: Iragazitako hizkuntzak locale: Hizkuntza new_password: Pasahitz berria - note: Bio + note: Biografia password: Pasahitza + setting_boost_modal: Erakutsi baieztapen elkarrizketa-koadroa bultzada eman aurretik + setting_default_privacy: Mezuaren pribatutasuna + notification_emails: + reblog: Bidali e-mail mezua norbaitek zure mezuari bultzada ematen badio + 'no': Ez + required: + mark: "*" + text: beharrezkoa + 'yes': Bai diff --git a/config/locales/simple_form.fa.yml b/config/locales/simple_form.fa.yml index ddb13ae43a..13ff7801eb 100644 --- a/config/locales/simple_form.fa.yml +++ b/config/locales/simple_form.fa.yml @@ -3,27 +3,35 @@ fa: simple_form: hints: defaults: - avatar: یکی از قالبهای PNG یا GIF یا JPG. بیشترین اندازه ۲ مگابایت. تصویر به اندازهٔ ۱۲۰×۱۲۰ پیکسل تبدیل خواهد شد - digest: پس از مدت طولانی عدم فعالیت فرستاده میشود، شامل خلاصهای از مواردی که در نبودتان از شما نام برده شده + avatar: یکی از قالبهای PNG یا GIF یا JPG. بیشترین اندازه ۲ مگابایت. تصویر به اندازهٔ ۴۰۰×۴۰۰ پیکسل تبدیل خواهد شد + bot: این حساب بیشتر به طور خودکار فعالیت میکند و نظارت پیوستهای روی آن وجود ندارد + digest: تنها وقتی فرستاده میشود که مدتی طولانی فعالیتی نداشته باشید و در این مدت برای شما پیغام خصوصیای نوشته شده باشد display_name: one: 1 حرف باقی مانده other: %{count} حرف باقی مانده + fields: شما میتوانید تا چهار مورد را در یک جدول در نمایهٔ خود نمایش دهید header: یکی از قالبهای PNG یا GIF یا JPG. بیشترین اندازه ۲ مگابایت. تصویر به اندازهٔ ۳۳۵×۷۰۰ پیکسل تبدیل خواهد شد - locked: باید پیگیران تازه را خودتان تأیید کنید. حریم خصوصی پیشفرض نوشتهها را روی پیگیران تنظیم میکند + locked: باید پیگیران تازه را خودتان تأیید کنید note: one: 1 حرف باقی مانده other: %{count} حرف باقی مانده + setting_hide_network: فهرست پیگیران شما و فهرست کسانی که شما پی میگیرید روی نمایهٔ شما دیده نخواهد شد setting_noindex: روی نمایهٔ عمومی و صفحهٔ نوشتههای شما تأثیر میگذارد setting_theme: ظاهر ماستدون را وقتی که از هر دستگاهی به آن وارد میشوید تعیین میکند. imports: data: پروندهٔ CSV که از سرور ماستدون دیگری برونسپاری شده sessions: - otp: کد تأیید دومرحلهای را از تلفن خود وارد کنید یا یکی از کدهای بازیابی را به کار ببرید. + otp: 'کد تأیید دومرحلهای که اپ روی تلفن شما ساخته را وارد کنید یا یکی از کدهای بازیابی را به کار ببرید:' user: filtered_languages: زبانهای انتخابشده از فهرست عمومی نوشتههایی که میبینید حذف میشوند labels: + account: + fields: + name: برچسب + value: محتوا defaults: avatar: تصویر نمایه + bot: این حساب یک ربات است confirm_new_password: تأیید رمز تازه confirm_password: تأیید رمز current_password: رمز فعلی @@ -31,6 +39,7 @@ fa: display_name: نمایش به نام email: نشانی ایمیل expires_in: تاریخ انقضا + fields: اطلاعات تکمیلی نمایه filtered_languages: زبانهای فیلترشده header: تصویر زمینه locale: زبان @@ -44,8 +53,10 @@ fa: setting_boost_modal: نمایش پیغام تأیید پیش از بازبوقیدن setting_default_privacy: حریم خصوصی نوشتهها setting_default_sensitive: همیشه تصاویر را به عنوان حساس علامت بزن - setting_delete_modal: پیش از پاک کردن یک نوشته پیغام تأیید نشان بده - setting_noindex: درخواست از موتورهای جستجو برای لغو فهرستسازی + setting_delete_modal: نمایش پیغام تأیید پیش از پاک کردن یک نوشته + setting_display_sensitive_media: همیشه تصویرهای علامتزدهشده به عنوان حساس را نمایش بده + setting_hide_network: نهفتن شبکهٔ ارتباطی + setting_noindex: درخواست از موتورهای جستجوگر برای ظاهر نشدن در نتایج جستجو setting_reduce_motion: کاستن از حرکت در پویانماییها setting_system_font_ui: بهکاربردن قلم پیشفرض سیستم setting_theme: تم سایت @@ -53,13 +64,14 @@ fa: severity: شدت type: نوع درونریزی username: نام کاربری (تنها حروف انگلیسی) + username_or_email: نام کاربری یا ایمیل interactions: must_be_follower: مسدودکردن اعلانهای همه به جز پیگیران must_be_following: مسدودکردن اعلانهای کسانی که شما پی نمیگیرید must_be_following_dm: مسدودکردن پیغامهای خصوصی کسانی که شما پی نمیگیرید notification_emails: digest: خلاصهکردن چند اعلان در یک ایمیل - favourite: وقتی کسی نوشتهٔ شما پسندید ایمیل بفرست + favourite: وقتی کسی نوشتهٔ شما را پسندید ایمیل بفرست follow: وقتی کسی پیگیر شما شد ایمیل بفرست follow_request: وقتی کسی درخواست پیگیری کرد ایمیل بفرست mention: وقتی کسی از شما نام برد ایمیل بفرست diff --git a/config/locales/simple_form.fi.yml b/config/locales/simple_form.fi.yml index f48e9ab23d..b7b97395a3 100644 --- a/config/locales/simple_form.fi.yml +++ b/config/locales/simple_form.fi.yml @@ -8,6 +8,7 @@ fi: display_name: one: 1 merkki jäljellä other: %{count} merkkiä jäljellä + fields: Sinulla voi olla korkeintaan 4 asiaa profiilissasi taulukossa header: PNG, GIF tai JPG. Enintään 2 Mt. Skaalataan kokoon 700 x 335 px locked: Sinun täytyy hyväksyä seuraajat manuaalisesti note: @@ -22,6 +23,9 @@ fi: user: filtered_languages: Valitut kielet suodatetaan pois julkisilta aikajanoilta labels: + account: + fields: + value: Sisältö defaults: avatar: Profiilikuva confirm_new_password: Vahvista uusi salasana @@ -31,6 +35,7 @@ fi: display_name: Nimimerkki email: Sähköpostiosoite expires_in: Vanhenee + fields: Profiilin metadata filtered_languages: Suodatetut kielet header: Otsakekuva locale: Kieli @@ -38,7 +43,7 @@ fi: max_uses: Käyttökertoja enintään new_password: Uusi salasana note: Kuvaus - otp_attempt: Kaksivaiheisen tunnistautumisen koodi + otp_attempt: Kaksivaiheisen tunnistuksen koodi password: Salasana setting_auto_play_gif: Toista GIF-animaatiot automaattisesti setting_boost_modal: Kysy vahvistusta ennen buustausta diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml index 88e1b88737..4e535cdf46 100644 --- a/config/locales/simple_form.fr.yml +++ b/config/locales/simple_form.fr.yml @@ -4,6 +4,7 @@ fr: hints: defaults: avatar: Au format PNG, GIF ou JPG. 2 Mo maximum. Sera réduit à 400x400px + bot: Ce compte exécute principalement des actions automatisées et pourrait ne pas être surveillé digest: Uniquement envoyé après une longue période d’inactivité et uniquement si vous avez reçu des messages personnels pendant votre absence display_name: one: 1 caractère restant @@ -14,12 +15,13 @@ fr: note: one: 1 caractère restant other: %{count} caractères restants + setting_hide_network: Ceux que vous suivez et ceux qui vous suivent ne seront pas affichés sur votre profil setting_noindex: Affecte votre profil public ainsi que vos statuts setting_theme: Affecte l’apparence de Mastodon quand vous êtes connecté·e depuis n’importe quel appareil. imports: data: Un fichier CSV généré par une autre instance de Mastodon sessions: - otp: Entrez le code d’authentification à deux facteurs depuis votre téléphone ou utilisez un de vos codes de récupération. + otp: 'Entrez le code d’authentification à deux facteurs généré par votre téléphone ou utilisez un de vos codes de récupération :' user: filtered_languages: Les langues sélectionnées seront filtrées hors de vos fils publics pour vous labels: @@ -29,6 +31,7 @@ fr: value: Contenu defaults: avatar: Image de profil + bot: Ceci est un robot confirm_new_password: Confirmation du nouveau mot de passe confirm_password: Confirmation du mot de passe current_password: Mot de passe actuel @@ -52,6 +55,7 @@ fr: setting_default_sensitive: Toujours marquer les médias comme sensibles setting_delete_modal: Afficher une fenêtre de confirmation avant de supprimer un pouet setting_display_sensitive_media: Toujours afficher les médias marqués comme sensibles + setting_hide_network: Cacher votre réseau setting_noindex: Demander aux moteurs de recherche de ne pas indexer vos informations personnelles setting_reduce_motion: Réduire la vitesse des animations setting_system_font_ui: Utiliser la police par défaut du système diff --git a/config/locales/simple_form.gl.yml b/config/locales/simple_form.gl.yml index 72633c7590..bae49a65dc 100644 --- a/config/locales/simple_form.gl.yml +++ b/config/locales/simple_form.gl.yml @@ -3,23 +3,25 @@ gl: simple_form: hints: defaults: - avatar: PNG, GIF ou JPG. Como moito 2MB. Será reducida ate 400x400px + avatar: PNG, GIF ou JPG. Máximo 2MB. Será reducida a 400x400px + bot: Esta conta realiza principalmente accións automatizadas e podería non estar monitorizada digest: Enviar só tras un longo período de inactividade e só si recibeu algunha mensaxe personal na súa ausencia display_name: one: 1 caracter restante other: %{count} caracteres restantes fields: Pode ter ate 4 elementos no seu perfil mostrados como unha táboa - header: PNG, GIF ou JPG. Como moito 2MB. Será reducida a 700x335px - locked: Require que vostede aprove as seguidoras de xeito manual + header: PNG, GIF ou JPG. Máximo 2MB. Será reducida a 700x335px + locked: Require que vostede acepte as seguidoras de xeito manual note: one: 1 caracter restante other: %{count} caracteres restantes + setting_hide_network: Non se mostrará no seu perfil quen a segue e quen a está a seguir setting_noindex: Afecta ao seu perfil público e páxinas de estado setting_theme: Afecta ao aspecto de Mastodon en calquer dispositivo cando está conectada. imports: data: Ficheiro CSV exportado desde outra instancia Mastodon sessions: - otp: Introduza o código de Doble-Factor desde o seu teléfono ou utilice un dos seus códigos de recuperación. + otp: Introduza o código de doble-factor xerado no aplicativo do seu móbil ou utilice un dos seus códigos de recuperación. user: filtered_languages: Os idiomas marcados filtraranse das liñas temporais públicas para vostede labels: @@ -29,10 +31,11 @@ gl: value: Contido defaults: avatar: Avatar + bot: Esta conta é de un bot confirm_new_password: Confirme o novo contrasinal confirm_password: Confirme o contrasinal current_password: Contrasinal actual - data: Data + data: Datos display_name: Nome mostrado email: enderezo correo electrónico expires_in: Caducidade despois de @@ -52,6 +55,7 @@ gl: setting_default_sensitive: Marcar sempre multimedia como sensible setting_delete_modal: Solicitar confirmación antes de eliminar unha mensaxe setting_display_sensitive_media: Mostrar sempre os medios marcados como sensibles + setting_hide_network: Agochar a súa rede setting_noindex: Pedir non aparecer nas buscas dos motores de busca setting_reduce_motion: Reducir o movemento nas animacións setting_system_font_ui: Utilizar a tipografía por defecto do sistema diff --git a/config/locales/simple_form.it.yml b/config/locales/simple_form.it.yml index 5d9ae18f5c..59a4cf525a 100644 --- a/config/locales/simple_form.it.yml +++ b/config/locales/simple_form.it.yml @@ -4,6 +4,7 @@ it: hints: defaults: avatar: PNG, GIF o JPG. Al massimo 2MB. Verranno scalate a 400x400px + bot: Avverte che l'account non rappresenta una persona digest: Inviata solo dopo un lungo periodo di intattività e solo se hai ricevuto qualsiasi messaggio personale in tua assenza display_name: one: 1 carattere rimanente @@ -14,12 +15,13 @@ it: note: one: 1 carattere rimanente other: %{count} caratteri rimanenti + setting_hide_network: Chi segui e chi segue te no saranno mostrati sul tuo profilo setting_noindex: Coinvolge il tuo profilo pubblico e le pagine di stato setting_theme: Coinvolge il modo in cui Mastodon verrà visualizzato quando sarai collegato da qualsiasi dispositivo. imports: data: File CSV esportato da un altra istanza di Mastodon sessions: - otp: Inserisci il codice due-fattori dal tuo telefono o usa uno dei codici di recupero. + otp: 'Inserisci il codice a due fattori generato dall''app del tuo telefono o usa uno dei codici di recupero:' user: filtered_languages: Le lingue selezionate verranno filtrate dalla timeline pubblica per te labels: @@ -29,6 +31,7 @@ it: value: Contenuto defaults: avatar: Avatar + bot: Questo account è un bot confirm_new_password: Conferma nuova password confirm_password: Conferma password current_password: Password corrente @@ -52,6 +55,7 @@ it: setting_default_sensitive: Segna sempre i media come sensibili setting_delete_modal: Mostra dialogo di conferma prima di eliminare un toot setting_display_sensitive_media: Mostra sempre i media segnati come sensibili + setting_hide_network: Nascondi la tua rete setting_noindex: Non indicizzare dai motori di ricerca setting_reduce_motion: Riduci movimento nelle animazioni setting_system_font_ui: Usa il carattere di default del sistema diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 9e4d404056..a3ae822fe4 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -3,19 +3,21 @@ ja: simple_form: hints: defaults: - avatar: 2MBまでのPNGやGIF、JPGが利用可能です。400x400pxまで縮小されます + avatar: 2MBまでのPNG、GIF、JPGが利用可能です。400x400pxまで縮小されます + bot: このアカウントは主に自動で動作し、人が見ていない可能性があります digest: 長期間使用していない場合と不在時に返信を受けた場合のみ送信されます display_name: あと%{count}文字入力できます。 fields: プロフィールに表として4つまでの項目を表示することができます - header: 2MBまでのPNGやGIF、JPGが利用可能です。 700x335pxまで縮小されます + header: 2MBまでのPNG、GIF、JPGが利用可能です。 700x335pxまで縮小されます locked: フォロワーを手動で承認する必要があります note: あと%{count}文字入力できます。 + setting_hide_network: フォローとフォロワーの情報がプロフィールページで見られないようにします setting_noindex: 公開プロフィールおよび各投稿ページに影響します setting_theme: ログインしている全てのデバイスで適用されるデザインです。 imports: data: 他の Mastodon インスタンスからエクスポートしたCSVファイルを選択して下さい sessions: - otp: 携帯電話に表示された2段階認証コードを入力するか、生成したリカバリーコードを使用してください。 + otp: '携帯電話のアプリで生成された二段階認証コードを入力するか、リカバリーコードを使用してください:' user: filtered_languages: 選択した言語があなたの公開タイムラインから取り除かれます labels: @@ -25,6 +27,7 @@ ja: value: 内容 defaults: avatar: アイコン + bot: これは BOT アカウントです confirm_new_password: 新しいパスワード(確認用) confirm_password: パスワード(確認用) current_password: 現在のパスワード @@ -36,7 +39,7 @@ ja: filtered_languages: 除外する言語 header: ヘッダー locale: 言語 - locked: 非公開アカウントにする + locked: 承認制アカウントにする max_uses: 使用できる回数 new_password: 新しいパスワード note: プロフィール @@ -49,6 +52,7 @@ ja: setting_delete_modal: トゥートを削除する前に確認ダイアログを表示する setting_display_sensitive_media: 閲覧注意としてマークされたメディアも常に表示する setting_favourite_modal: お気に入りをする前に確認ダイアログを表示する + setting_hide_network: 繋がりを隠す setting_noindex: 検索エンジンによるインデックスを拒否する setting_reduce_motion: アニメーションの動きを減らす setting_system_font_ui: システムのデフォルトフォントを使う diff --git a/config/locales/simple_form.ko.yml b/config/locales/simple_form.ko.yml index ccb05fd253..9120b58c71 100644 --- a/config/locales/simple_form.ko.yml +++ b/config/locales/simple_form.ko.yml @@ -4,6 +4,7 @@ ko: hints: defaults: avatar: PNG, GIF 혹은 JPG. 최대 2MB. 400x400px로 다운스케일 될 것임 + bot: 사람들에게 계정이 사람이 아님을 알립니다 digest: 오랫동안 활동하지 않았을 때 받은 멘션들에 대한 요약 받기 display_name: one: 1 글자 남음 @@ -19,7 +20,7 @@ ko: imports: data: 다른 마스토돈 인스턴스에서 추출된 CSV 파일 sessions: - otp: 2단계 인증 코드를 휴대전화를 보고 입력하거나, 복구 코드 중 하나를 사용하세요. + otp: '휴대전화에서 생성 된 2단계 인증 코드를 입력하거나, 복구 코드 중 하나를 사용하세요:' user: filtered_languages: 선택된 언어가 공개 타임라인에서 제외 될 것입니다 labels: @@ -29,6 +30,7 @@ ko: value: 내용 defaults: avatar: 아바타 + bot: 이것은 봇 계정입니다 confirm_new_password: 새로운 비밀번호 다시 입력 confirm_password: 현재 비밀번호 다시 입력 current_password: 현재 비밀번호 입력 diff --git a/config/locales/simple_form.nl.yml b/config/locales/simple_form.nl.yml index ec42adfd72..68aa0635d4 100644 --- a/config/locales/simple_form.nl.yml +++ b/config/locales/simple_form.nl.yml @@ -4,7 +4,8 @@ nl: hints: defaults: avatar: PNG, GIF of JPG. Maximaal 2MB. Wordt teruggeschaald naar 400x400px - digest: Wordt alleen na een lange periode van inactiviteit verzonden en alleen wanneer je tijdens jouw afwezigheid persoonlijke berichten ontvangt + bot: Dit is een geautomatiseerd account en wordt mogelijk niet gemonitord + digest: Wordt alleen na een lange periode van inactiviteit verzonden en alleen wanneer je tijdens jouw afwezigheid persoonlijke berichten hebt ontvangen display_name: one: 1 teken over other: %{count} tekens over @@ -14,14 +15,15 @@ nl: note: one: 1 teken over other: %{count} tekens over + setting_hide_network: Wie jij volgt en wie jou volgen wordt niet op jouw profiel getoond setting_noindex: Heeft invloed op jouw openbare profiel en toots setting_theme: Heeft invloed op hoe de webapp van Mastodon eruitziet (op elk apparaat waarmee je inlogt). imports: data: CSV-bestand dat op een andere Mastodonserver werd geëxporteerd sessions: - otp: Voer de tweestaps-aanmeldcode vanaf jouw mobiele telefoon in of gebruik een van jouw herstelcode's. + otp: Voer de tweestaps-aanmeldcode vanaf jouw mobiele telefoon in of gebruik een van jouw herstelcodes. user: - filtered_languages: De geselecteerde talen worden uit de lokale en globale tijdlijn verwijderd + filtered_languages: Geselecteerde talen worden uit de lokale en globale tijdlijn verwijderd labels: account: fields: @@ -29,6 +31,7 @@ nl: value: Inhoud defaults: avatar: Avatar + bot: Dit is een bot-account confirm_new_password: Nieuw wachtwoord bevestigen confirm_password: Wachtwoord bevestigen current_password: Huidig wachtwoord @@ -37,11 +40,11 @@ nl: email: E-mailadres expires_in: Vervalt na fields: Metadata profiel - filtered_languages: Talen filteren + filtered_languages: Gefilterde talen header: Omslagfoto locale: Taal locked: Maak account besloten - max_uses: Max aantal keer te gebruiken + max_uses: Max. aantal keer te gebruiken new_password: Nieuwe wachtwoord note: Bio otp_attempt: Tweestaps-aanmeldcode @@ -52,6 +55,7 @@ nl: setting_default_sensitive: Media altijd als gevoelig markeren setting_delete_modal: Vraag voor het verwijderen van een toot een bevestiging setting_display_sensitive_media: Als gevoelig gemarkeerde media altijd tonen + setting_hide_network: Jouw volgers en wie je volgt verbergen setting_noindex: Jouw toots niet door zoekmachines laten indexeren setting_reduce_motion: Langzamere animaties setting_system_font_ui: Standaardlettertype van jouw systeem gebruiken @@ -59,7 +63,7 @@ nl: setting_unfollow_modal: Vraag voor het ontvolgen van iemand een bevestiging severity: Zwaarte type: Importtype - username: gebruikersnaam + username: Gebruikersnaam username_or_email: Gebruikersnaam of e-mailadres interactions: must_be_follower: Meldingen van mensen die jou niet volgen blokkeren diff --git a/config/locales/simple_form.oc.yml b/config/locales/simple_form.oc.yml index 4ca58c1023..65b27e7166 100644 --- a/config/locales/simple_form.oc.yml +++ b/config/locales/simple_form.oc.yml @@ -4,6 +4,7 @@ oc: hints: defaults: avatar: PNG, GIF o JPG. Maximum 2 Mo. Serà retalhat en 400x400px + bot: Avisar lo monde qu’aqueste compte es pas d’una persona digest: Solament enviat aprèp un long moment d’inactivitat e solament s’avètz recebut de messatges personals pendent vòstra abséncia display_name: one: Demòra encara 1 caractèr @@ -14,12 +15,13 @@ oc: note: one: Demòra encara 1 caractèr other: Demòran encara %{count} caractèrs + setting_hide_network: Vòstre perfil mostrarà pas los que vos sègon e lo monde que seguètz setting_noindex: Aquò es destinat a vòstre perfil public e vòstra pagina d’estatuts setting_theme: Aquò càmbia lo tèma grafic de Mastodon quand sètz connectat qual que siasque lo periferic. imports: data: Fichièr CSV exportat d’una autra instància Mastodon sessions: - otp: Picatz lo còdi d’autentificacion en dos temps (Two factor code) de vòstre mobil o utilizatz un de vòstres còdis de recuperacion. + otp: 'Picatz lo còdi d’autentificacion en dos temps (Two factor code) de vòstra aplicacion mobil o utilizatz un de vòstres còdis de recuperacion :' user: filtered_languages: Las lengas seleccionadas seràn levadas de vòstre flux d’actualitat labels: @@ -29,6 +31,7 @@ oc: value: Contengut defaults: avatar: Avatar + bot: Aquò es lo compte a un robòt confirm_new_password: Confirmacion del nòu senhal confirm_password: Confirmatz lo nòu senhal current_password: Senhal actual @@ -41,7 +44,7 @@ oc: header: Bandièra locale: Lenga locked: Far venir lo compte privat - max_uses: Limit d’utilizacion + max_uses: Limit d’utilizacions new_password: Nòu senhal note: Bio otp_attempt: Còdi Two-factor @@ -52,6 +55,7 @@ oc: setting_default_sensitive: Totjorn marcar los mèdias coma sensibles setting_delete_modal: Afichar una fenèstra de confirmacion abans de suprimir un estatut setting_display_sensitive_media: Totjorn mostrar los mèdias coma sensibles + setting_hide_network: Amagar vòstre malhum setting_noindex: Èsser pas indexat pels motors de recèrca setting_reduce_motion: Reduire la velocitat de las animacions setting_system_font_ui: Utilizar la polissa del sisèma diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml index 8a6d47a015..67a2fc975f 100644 --- a/config/locales/simple_form.pl.yml +++ b/config/locales/simple_form.pl.yml @@ -4,6 +4,7 @@ pl: hints: defaults: avatar: PNG, GIF lub JPG. Maksymalnie 2MB. Zostanie zmniejszony do 400x400px + bot: Informuje użytkowników, że konto nie jest prowadzone przez człowieka digest: Wysyłane tylko po długiej nieaktywności, jeżeli w tym czasie otrzymaleś jakąś wiadomość bezpośrednią display_name: few: Pozostały %{count} znaki. @@ -18,12 +19,13 @@ pl: many: Pozostało %{count} znaków one: Pozostał 1 znak other: Pozostało %{count} znaków + setting_hide_network: Informacje o tym, kto Cię śledzi i kogo śledzisz nie będą widoczne setting_noindex: Wpływa na widoczność strony profilu i Twoich wpisów setting_skin: Zmienia wygląd używanej odmiany Mastodona imports: data: Plik CSV wyeksportowany z innej instancji Mastodona sessions: - otp: Wprowadź kod weryfikacji dwuetapowej z telefonu lub wykorzystaj jeden z kodów zapasowych. + otp: 'Wprowadź kod weryfikacji dwuetapowej z telefonu lub wykorzystaj jeden z kodów zapasowych:' user: filtered_languages: Wpisy w wybranych językach nie będą wyświetlać się na publicznych osiach czasu labels: @@ -33,6 +35,7 @@ pl: value: Zawartość defaults: avatar: Awatar + bot: To konto jest prowadzone przez bota confirm_new_password: Potwierdź nowe hasło confirm_password: Potwierdź hasło current_password: Obecne hasło @@ -57,6 +60,7 @@ pl: setting_delete_modal: Pytaj o potwierdzenie przed usunięciem wpisu setting_display_sensitive_media: Zawsze oznaczaj zawartość multimedialną jako wrażliwą setting_favourite_modal: Pytaj o potwierdzenie przed dodaniem do ulubionych + setting_hide_network: Ukryj swoją sieć setting_noindex: Nie indeksuj mojego profilu w wyszukiwarkach internetowych setting_reduce_motion: Ogranicz ruch w animacjach setting_skin: Motyw diff --git a/config/locales/simple_form.pt-BR.yml b/config/locales/simple_form.pt-BR.yml index cae1f671dd..50ed5eb1a8 100644 --- a/config/locales/simple_form.pt-BR.yml +++ b/config/locales/simple_form.pt-BR.yml @@ -4,6 +4,7 @@ pt-BR: hints: defaults: avatar: PNG, GIF or JPG. Arquivos de até 2MB. Eles serão diminuídos para 400x400px + bot: Essa conta executa principalmente ações automatizadas e pode não ser monitorada digest: Enviado após um longo período de inatividade com um resumo das menções que você recebeu em sua ausência display_name: one: 1 caracter restante @@ -14,12 +15,13 @@ pt-BR: note: one: 1 caracter restante other: %{count} caracteres restantes + setting_hide_network: Quem você segue e quem segue você não aparecerá no seu perfil setting_noindex: Afeta seu perfil público e as páginas de suas postagens setting_theme: Afeta a aparência do Mastodon quando em sua conta em qualquer aparelho. imports: data: Arquivo CSV exportado de outra instância do Mastodon sessions: - otp: Insira o código de autenticação do seu celular ou use um dos códigos de recuperação. + otp: 'Insira o código de autenticação gerado pelo app no seu celular ou use um dos códigos de recuperação:' user: filtered_languages: Selecione os idiomas que devem ser removidos de suas timelines públicas labels: @@ -29,6 +31,7 @@ pt-BR: value: Conteúdo defaults: avatar: Avatar + bot: Essa é a conta de um robô confirm_new_password: Confirmar nova senha confirm_password: Confirmar senha current_password: Senha atual @@ -52,6 +55,7 @@ pt-BR: setting_default_sensitive: Sempre marcar mídia como sensível setting_delete_modal: Mostrar diálogo de confirmação antes de deletar uma postagem setting_display_sensitive_media: Sempre mostrar mídia marcada como sensível + setting_hide_network: Esconder suas conexões setting_noindex: Não quero ser indexado por mecanismos de busca setting_reduce_motion: Reduz movimento em animações setting_system_font_ui: Usar a fonte padrão de seu sistema diff --git a/config/locales/simple_form.ru.yml b/config/locales/simple_form.ru.yml index b8ee5892d3..a6b50b9d2e 100644 --- a/config/locales/simple_form.ru.yml +++ b/config/locales/simple_form.ru.yml @@ -4,12 +4,14 @@ ru: hints: defaults: avatar: PNG, GIF или JPG. Максимально 2MB. Будет уменьшено до 400x400px - digest: Отсылается после долгого периода неактивности с общей информацией упоминаний, полученных в Ваше отсутствие + bot: Этот аккаунт обычно выполяет автоматизированные действия и может не просматриваться владельцем + digest: Отсылается лишь после длительной неактивности, если Вы в это время получали личные сообщения display_name: few: Осталось %{count} символа many: Осталось %{count} символов one: Остался 1 символ other: Осталось %{count} символов + fields: В профиле можно отобразить до 4 пунктов как таблицу header: PNG, GIF или JPG. Максимально 2MB. Будет уменьшено до 700x335px locked: Потребует от Вас ручного подтверждения подписчиков, изменит приватность постов по умолчанию на "только для подписчиков" note: @@ -17,17 +19,23 @@ ru: many: Осталось %{count} символов one: Остался 1 символ other: Осталось %{count} символов + setting_hide_network: Те, на кого Вы подписаны и кто подписан на Вас, не будут отображены в Вашем профиле setting_noindex: Относится к Вашему публичному профилю и страницам статусов setting_theme: Влияет на внешний вид Mastodon при выполненном входе в аккаунт. imports: data: Файл CSV, экспортированный с другого узла Mastodon sessions: - otp: Введите код двухфакторной аутентификации или используйте один из Ваших кодов восстановления. + otp: 'Введите код двухфакторной аутентификации, сгенерированный в мобильном приложении, или используйте один из Ваших кодов восстановления:' user: filtered_languages: Выбранные языки будут убраны из Ваших публичных лет. labels: + account: + fields: + name: Пункт + value: Значение defaults: avatar: Аватар + bot: Это аккаунт бота confirm_new_password: Повторите новый пароль confirm_password: Повторите пароль current_password: Текущий пароль @@ -35,6 +43,7 @@ ru: display_name: Показываемое имя email: Адрес e-mail expires_in: Срок действия + fields: Метаданные профиля filtered_languages: Фильтруемые языки header: Заголовок locale: Язык @@ -50,6 +59,7 @@ ru: setting_default_sensitive: Всегда отмечать медиаконтент как чувствительный setting_delete_modal: Показывать диалог подтверждения перед удалением setting_display_sensitive_media: Всегда показывать медиаконтент, отмеченный как чувствительный + setting_hide_network: Скрыть свои связи setting_noindex: Отказаться от индексации в поисковых машинах setting_reduce_motion: Уменьшить движение в анимации setting_system_font_ui: Использовать шрифт системы по умолчанию diff --git a/config/locales/simple_form.sk.yml b/config/locales/simple_form.sk.yml index 134e62ee37..c6887a3630 100644 --- a/config/locales/simple_form.sk.yml +++ b/config/locales/simple_form.sk.yml @@ -4,22 +4,26 @@ sk: hints: defaults: avatar: PNG, GIF alebo JPG. Maximálne 2MB. Bude zmenšený na 400x400px - digest: Odoslané iba v prípade dlhodobej neprítomnosti, a len ak ste obdŕžali nejaké osobné správy kým ste boli preč + bot: Tento účet vykonáva hlavne automatizované akcie, a je pravdepodobne nespravovaný + digest: Odoslané iba v prípade dlhodobej neprítomnosti, a len ak si obdŕžal/a nejaké osobné správy kým si bol/a preč display_name: + few: Ostávajú ti %{count} znaky one: Ostáva ti 1 znak other: Ostáva ti %{count} znakov fields: Môžeš mať 4 položky na svojom profile zobrazené vo forme tabuľky header: PNG, GIF alebo JPG. Maximálne 2MB. Bude zmenšený na 700x335px - locked: Musíte manuálne schváliť sledujúcich + locked: Vyžaduje manuálne schvalovať sledujúcich note: - one: Ostáva vám 1 znak + few: Ostávajú ti %{count} znaky + one: Ostáva ti 1 znak other: Ostáva ti %{count} znakov - setting_noindex: Ovplyvňuje profil a správy tak, že ich nebude možné nájsť vyhľadávaním - setting_theme: Toto ovplyvní ako bude Mastodon vyzerať pri prihlásení z hociktorého zariadenia. + setting_hide_network: Koho následuješ, a kto následuje teba nebude zobrazené na tvojom profile + setting_noindex: Ovplyvňuje verejný profil a statusy + setting_theme: Toto ovplyvňuje ako Mastodon vyzerá pri prihlásení z hociakého zariadenia. imports: data: CSV súbor vyexportovaný z inej Mastodon inštancie sessions: - otp: Napíš sem dvoj-faktorový kód z telefónu, alebo použite jeden z vašich obnovovacích kódov. + otp: 'Napíš sem dvoj-faktorový kód z telefónu, alebo použi jeden z tvojích obnovovacích kódov:' user: filtered_languages: Zaškrtnuté jazyky budú pre teba vynechané nebudú z verejnej časovej osi labels: @@ -29,6 +33,7 @@ sk: value: Obsah defaults: avatar: Avatar + bot: Toto je automatizovaný bot účet confirm_new_password: Znovu tvoje nové heslo, pre potvrdenie confirm_password: Potvrď heslo current_password: Súčasné heslo @@ -52,18 +57,19 @@ sk: setting_default_sensitive: Označ všetky mediálne súbory ako chúlostivé setting_delete_modal: Zobrazuj potvrdzovacie okno pred vymazaním toot-u setting_display_sensitive_media: Vždy zobraz médiá označené ako chúlostivé + setting_hide_network: Ukri svoju sieť kontaktov setting_noindex: Nezaraďuj príspevky do indexu pre vyhľadávče setting_reduce_motion: Redukovať pohyb v animáciách setting_system_font_ui: Použiť základné systémové písmo - setting_theme: Vzhľad - setting_unfollow_modal: Zobrazovať potvrdzovacie okno pred skončením sledovania iného používateľa + setting_theme: Vzhľad stránky + setting_unfollow_modal: Zobraziť potvrdzovacie okno pred skončením sledovania iného užívateľa severity: Závažnosť type: Typ importu - username: Užívateľské meno - username_or_email: Prezívka, alebo Email + username: Prezývka + username_or_email: Prezívka, alebo email interactions: - must_be_follower: Blokovať notifikácie pod používateľov, ktorí ťa nesledujú - must_be_following: Blokovať notifikácie od ľudí ktorí ťa nesledujú + must_be_follower: Blokovať oznámenia od užívateľov, ktorí ťa nesledujú + must_be_following: Blokovať oznámenia od ľudí ktorých nesleduješ must_be_following_dm: Blokovať súkromné správy od ľudí ktorých nesleduješ notification_emails: digest: Posielať súhrnné emaily diff --git a/config/locales/simple_form.sl.yml b/config/locales/simple_form.sl.yml new file mode 100644 index 0000000000..31d1e11701 --- /dev/null +++ b/config/locales/simple_form.sl.yml @@ -0,0 +1,15 @@ +--- +sl: + simple_form: + hints: + defaults: + avatar: PNG, GIF ali JPG. Največ 2MB. Zmanjšana bo na 400x400px + bot: Opozarja ljudi, da račun ne predstavlja osebe + digest: Pošlje se le po dolgem obdobju nedejavnosti in samo, če ste prejeli osebna sporočila v vaši odsotnosti + display_name: + one: 1 znak ostane + other: %{count} znakov ostane + fields: Na svojem profilu lahko imate do 4 predmete prikazane kot tabelo. + header: PNG, GIF ali JPG. Največ 2MB. Zmanjšana bo na 700x335px + imports: + data: Izvožena CSV datoteka iz drugega Mastodon vozlišča diff --git a/config/locales/simple_form.sv.yml b/config/locales/simple_form.sv.yml index 81ba61fb3b..f027d684b8 100644 --- a/config/locales/simple_form.sv.yml +++ b/config/locales/simple_form.sv.yml @@ -3,7 +3,8 @@ sv: simple_form: hints: defaults: - avatar: Högst 2 MB. Kommer nedskalas till 400x400px + avatar: Högst 2 MB. Kommer att skalas ner till 400x400px + bot: Detta konto utför huvudsakligen automatiserade åtgärder och kanske inte övervakas digest: Skickas endast efter en lång period av inaktivitet och endast om du har fått några personliga meddelanden i din frånvaro display_name: one: 1 tecken kvar @@ -14,12 +15,13 @@ sv: note: one: 1 tecken kvar other: %{count} tecken kvar + setting_hide_network: Vem du följer och vilka som följer dig kommer inte att visas på din profilsida setting_noindex: Påverkar din offentliga profil och statussidor setting_theme: Påverkar hur Mastodon ser ut oavsett från vilken enhet du är inloggad. imports: data: CSV-fil som exporteras från en annan Mastodon-instans sessions: - otp: Ange tvåfaktorkoden från din telefon eller använd någon av dina återställningskoder. + otp: 'Ange tvåfaktorkoden genererad från din telefonapp eller använd någon av dina återställningskoder:' user: filtered_languages: Kontrollerade språk filtreras från offentliga tidslinjer för dig labels: @@ -29,6 +31,7 @@ sv: value: Innehåll defaults: avatar: Avatar + bot: Detta är ett botkonto confirm_new_password: Bekräfta nytt lösenord confirm_password: Bekräfta lösenord current_password: Nuvarande lösenord @@ -52,6 +55,7 @@ sv: setting_default_sensitive: Markera alltid media som känsligt setting_delete_modal: Visa bekräftelsedialog innan du raderar en toot setting_display_sensitive_media: Visa alltid media märkt som känsligt + setting_hide_network: Göm ditt nätverk setting_noindex: Uteslutning av sökmotorindexering setting_reduce_motion: Minska rörelser i animationer setting_system_font_ui: Använd systemets standardfont diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml index 8252757566..5b3b25ab13 100644 --- a/config/locales/simple_form.zh-CN.yml +++ b/config/locales/simple_form.zh-CN.yml @@ -4,22 +4,30 @@ zh-CN: hints: defaults: avatar: 文件大小限制 2MB,只支持 PNG、GIF 或 JPG 格式。图片分辨率将会压缩至 400×400px + bot: 来自这个帐户的绝大多数操作都是自动进行的,并且可能无人监控 digest: 仅在你长时间未登录,且收到了私信时发送 display_name: 还能输入 %{count} 个字符 + fields: 这将会在个人资料页上以表格的形式展示,最多 4 个项目 header: 文件大小限制 2MB,只支持 PNG、GIF 或 JPG 格式。图片分辨率将会压缩至 700×335px locked: 你需要手动审核所有关注请求 note: 还能输入 %{count} 个字符 + setting_hide_network: 你关注的人和关注你的人将不会在你的个人资料页上展示 setting_noindex: 此设置会影响到你的公开个人资料以及嘟文页面 - setting_theme: 此设置会影响到你从任意设备登录时 Mastodon 的显示样式 + setting_theme: 此设置会影响到所有已登录设备上 Mastodon 的显示样式 imports: data: 请上传从其他 Mastodon 实例导出的 CSV 文件 sessions: - otp: 输入你手机上生成的双重认证码,或者任意一个恢复代码。 + otp: 输入你手机应用上生成的双重认证码,或者任意一个恢复代码: user: filtered_languages: 被勾选语言的嘟文将不会出现在你的公共时间轴上 labels: + account: + fields: + name: 标签 + value: 内容 defaults: avatar: 头像 + bot: 这是一个机器人帐户 confirm_new_password: 确认新密码 confirm_password: 确认密码 current_password: 当前密码 @@ -27,6 +35,7 @@ zh-CN: display_name: 昵称 email: 电子邮件地址 expires_in: 失效时间 + fields: 个人资料附加信息 filtered_languages: 语言过滤 header: 个人资料页横幅图片 locale: 语言 @@ -41,6 +50,8 @@ zh-CN: setting_default_privacy: 嘟文默认可见范围 setting_default_sensitive: 总是将我发送的媒体文件标记为敏感内容 setting_delete_modal: 在删除嘟文前询问我 + setting_display_sensitive_media: 总是显示标记为敏感的媒体文件 + setting_hide_network: 隐藏你的社交网络 setting_noindex: 禁止搜索引擎建立索引 setting_reduce_motion: 降低过渡动画效果 setting_system_font_ui: 使用系统默认字体 @@ -49,6 +60,7 @@ zh-CN: severity: 级别 type: 导入数据类型 username: 用户名 + username_or_email: 用户名或电子邮件地址 interactions: must_be_follower: 屏蔽来自未关注我的用户的通知 must_be_following: 屏蔽来自我未关注的用户的通知 diff --git a/config/locales/simple_form.zh-HK.yml b/config/locales/simple_form.zh-HK.yml index a21439a980..06d3f6f6c6 100644 --- a/config/locales/simple_form.zh-HK.yml +++ b/config/locales/simple_form.zh-HK.yml @@ -4,6 +4,7 @@ zh-HK: hints: defaults: avatar: 支援 PNG, GIF 或 JPG 圖片,檔案最大為 2MB,會縮裁成 400x400px + bot: 提醒用戶本帳號是機械人 digest: 僅在你長時間未登錄,且收到了私信時發送 display_name: one: 尚餘 1 個字 @@ -14,12 +15,13 @@ zh-HK: note: one: 尚餘 1 個字 other: 尚餘 %{count} 個字 + setting_hide_network: 你關注的人和關注你的人將不會在你的個人資料頁上顯示 setting_noindex: 此設定會影響到你的公開個人資料以及文章頁面 setting_theme: 此設置會影響到你從任意設備登入時 Mastodon 的顯示樣式。 imports: data: 自其他服務站匯出的 CSV 檔案 sessions: - otp: 輸入你手機上生成的雙重認證碼,或者任意一個恢復代碼。 + otp: 輸入你手機上生成的雙重認證碼,或者任意一個恢復代碼: user: filtered_languages: 下面被選擇的語言的文章將不會出現在你的公共時間軸上 labels: @@ -29,6 +31,7 @@ zh-HK: value: 內容 defaults: avatar: 個人頭像 + bot: 這帳號是機械人 confirm_new_password: 確認新密碼 confirm_password: 確認密碼 current_password: 目前密碼 @@ -52,6 +55,7 @@ zh-HK: setting_default_sensitive: 預設我的內容為敏感內容 setting_delete_modal: 刪推前詢問我 setting_display_sensitive_media: 預設我的媒體為敏感內容 + setting_hide_network: 隱藏你的社交網絡 setting_noindex: 阻止搜尋引擎檢索 setting_reduce_motion: 減低動畫效果 setting_system_font_ui: 使用系統預設字型 diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 8484ac52c0..d887d2de23 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -1,7 +1,7 @@ --- sk: about: - about_hashtag_html: Toto sú verejné toot príspevky otagované #%{tagom}. Ak máš účet niekde vo fediverse, môžeš ich používať. + about_hashtag_html: Toto sú verejné toot príspevky otagované #%{hashtag}. Ak máš účet niekde vo fediverse, môžeš ich používať. about_mastodon_html: Mastodon je sociálna sieť založená na otvorených webových protokoloch. Jej zrojový kód je otvorený a je decentralizovaná podobne ako email. about_this: O tejto instancii administered_by: 'Správca je:' @@ -10,7 +10,7 @@ sk: contact_missing: Nezadané contact_unavailable: Neuvedené description_headline: Čo je %{domain}? - domain_count_after: ďalším inštanciám + domain_count_after: ďalším instanciám domain_count_before: Pripojený k extended_description_html: | Pravidlá @@ -40,6 +40,7 @@ sk: following: Sleduje media: Médiá moved_html: "%{name} účet bol presunutý na %{new_profile_link}:" + network_hidden: Táto informácia nieje k dispozícii nothing_here: Nič tu nie je! people_followed_by: Ľudia, ktorých %{name} sleduje people_who_follow: Ľudia sledujúci %{name} @@ -53,9 +54,7 @@ sk: unfollow: Prestať sledovať admin: account_moderation_notes: - account: Moderátor - create: Vytvoriť - created_at: Dátum + create: Zanechaj poznámku created_msg: Poznámka moderátora bola úspešne vytvorená! delete: Zmazať destroyed_msg: Poznámka moderátora bola úspešne zmazaná! @@ -72,6 +71,7 @@ sk: title: Zmeň email pre %{username} confirm: Potvrdiť confirmed: Potvrdený + confirming: Potvrdzujúci demote: Degradovať disable: Zablokovať disable_two_factor_authentication: Zakázať 2FA @@ -80,6 +80,7 @@ sk: domain: Doména edit: Upraviť email: Email + email_status: Stav Email enable: Povoliť enabled: Povolený feed_url: URL časovej osi @@ -118,6 +119,10 @@ sk: push_subscription_expires: PuSH odoberanie expiruje redownload: Obnoviť avatar remove_avatar: Odstrániť avatár + resend_confirmation: + already_confirmed: Tento používateľ už bol potvrdený + send: Znova odoslať potvrdzovací e-mail + success: Potvrdený e-mail bol úspešne odoslaný! reset: Reset reset_password: Obnoviť heslo resubscribe: Znovu odoberať @@ -169,6 +174,7 @@ sk: resolve_report: "%{name} vyriešili nahlásenie užívateľa %{target}" silence_account: "%{name} utíšil/a účet %{target}" suspend_account: "%{name} zablokoval/a účet používateľa %{target}" + unassigned_report: "%{name} odobral/a report od %{target}" unsilence_account: "%{name} zrušil/a utíšenie účtu používateľa %{target}" unsuspend_account: "%{name} zrušil/a blokovanie účtu používateľa %{target}" update_custom_emoji: "%{name} aktualizoval/a emoji %{target}" @@ -222,6 +228,7 @@ sk: severity: Závažnosť show: affected_accounts: + few: "%{count} účty v databáze ovplyvnených" one: Jeden účet v databáze ovplyvnený other: "%{count} účtov v databáze ovplyvnených" retroactive: @@ -268,7 +275,6 @@ sk: comment: none: Žiadne created_at: Nahlásené - delete: Vymazať id: Identifikácia mark_as_resolved: Označiť ako vyriešené mark_as_unresolved: Označ ako nevyriešené @@ -277,10 +283,7 @@ sk: create_and_resolve: Vyrieš s poznámkou create_and_unresolve: Otvor znovu, s poznámkou delete: Vymaž - placeholder: Opíš aké opatrenia boli urobené, alebo akékoľvek iné aktualizácie k tomuto nahláseniu… - nsfw: - 'false': Odkryť mediálne prílohy - 'true': Skryť mediálne prílohy + placeholder: Opíš aké opatrenia boli urobené, alebo akékoľvek iné súvisiace aktualizácie… reopen: Znovu otvor report report: Nahlásiť report_contents: Obsah @@ -355,11 +358,8 @@ sk: delete: Vymazať nsfw_off: Obsah nieje chúlostivý nsfw_on: Označ obeah aka chúlostivý - execute: Vykonať failed_to_execute: Nepodarilo sa vykonať media: - hide: Skryť médiá - show: Zobraziť médiá title: Médiá no_media: Žiadné médiá title: Statusy na účte @@ -375,6 +375,7 @@ sk: admin_mailer: new_report: body: "%{reporter} nahlásil %{target}" + body_remote: Niekto z %{domain} nahlásil %{target} subject: Nový report pre %{instance} (#%{id}) application_mailer: notification_preferences: Zmeniť e-mailové voľby @@ -396,8 +397,8 @@ sk: change_password: Heslo confirm_email: Potvrdiť email delete_account: Vymazať účet - delete_account_html: Pokiaľ si želáte vymazať svoj účet, môžete tak 1 urobiť tu 2. Budete požiadaný/á o potvrdenie tohto kroku. - didnt_get_confirmation: Neobdŕžali ste kroky pre potvrdenie? + delete_account_html: Pokiaľ chceš vymazať svoj účet, môžeš tak urobiť tu. Budeš požiadaný/á o potvrdenie tohto kroku. + didnt_get_confirmation: Neobdŕžal/a si kroky pre potvrdenie? forgot_password: Zabudli ste heslo? invalid_reset_password_token: Token na obnovu hesla vypršal. Prosím vypítajte si nový. login: Prihlás sa @@ -463,7 +464,7 @@ sk: archive_takeout: date: Dátum download: Stiahni si svoj archív - hint_html: Môžeš si opýtať archív svojích príspevkov a nahratých médií. Exportované dáta budú v ActivityPub formáte, čítateľné hociakým kompatibilným softvérom. + hint_html: Môžeš si opýtať archív svojích príspevkov a nahratých médií. Exportované dáta budú v ActivityPub formáte, čítateľné hociakým kompatibilným softvérom. Archív si je možné vyžiadať každých sedem dní. in_progress: Balím tvoj archív... request: Vyžiadaj si tvoj archív size: Veľkosť @@ -479,9 +480,10 @@ sk: lock_link: Zamknite svoj účet purge: Odstrániť následovateľa success: + few: Počas utišovania sledovateľov z %{count} domén... one: Počas utišovania sledovateľov z jednej domény... other: Počas utišovania sledovateľov z %{count} domén... - true_privacy_html: Prosím majte na vedomí, 1 že ozajstné súkromie sa dá dosiahnúť iba za pomoci end-to-end enkrypcie 2. + true_privacy_html: Prosím ber na vedomie, že ozajstné súkromie sa dá dosiahnúť iba za pomoci end-to-end enkrypcie. unlocked_warning_html: Hocikto ťa môže následovať aby mohol/a ihneď vidieť tvoje súkromné príspevky. %{lock_link} aby si mohla skontrolovať a odmietať sledovateľov. unlocked_warning_title: Tvoj účet nieje zamknutý generic: @@ -489,8 +491,9 @@ sk: powered_by: poháňané vďaka %{link} save_changes: Uložiť zmeny validation_errors: + few: Niečo ešte stále nieje v poriadku! Prosím skontroluj všetky %{count} chyby one: Niečo nieje úplne v poriadku! Prosím skontroluj chybu - other: Niečo ešte stále nieje v poriadku! Prosím skontroluj všetky %{count} chyby + other: Niečo ešte stále nieje v poriadku! Prosím skontroluj všetkých %{count} chýb imports: preface: Môžeš importovať dáta ktoré si exportoval/a z iného Mastodon serveru, ako sú napríklad zoznamy ľudí ktorých sleduješ, alebo blokuješ. success: Tvoje dáta boli nahraté úspešne, a budú teraz spracované v danom čase @@ -513,6 +516,7 @@ sk: expires_in_prompt: Nikdy generate: Vygeneruj max_uses: + few: "%{count} použitia" one: jedno použitie other: "%{count} použití" max_uses_prompt: Bez limitov @@ -543,11 +547,13 @@ sk: body: Tu nájdete krátky súhrn správ ktoré ste zmeškali od svojej poslednj návštevi od %{since} mention: "%{name} ťa spomenul/a v:" new_followers_summary: + few: Taktiež, získal/a si %{count} nových následovníkov za tú dobu čo si bol/a preč. Yay! one: Taktiež, získal/a si jedného nového následovníka zatiaľ čo si bol/a preč. Yay! other: Taktiež, získal/a si %{count} nových následovníkov za tú dobu čo si bol/a preč. Yay! subject: + few: "%{count} nové notifikácie od tvojej poslednej návštevy \U0001F418" one: "1 nová notifikácia od tvojej poslednej návštevy \U0001F418" - other: "%{count} nové notifikácie od tvojej poslednej návštevy \U0001F418" + other: "%{count} nových notifikácií od tvojej poslednej návštevy \U0001F418" title: Zatiaľ čo si bol/a preč… favourite: body: 'Tvoj príspevok bol uložený medi obľúbené užívateľa %{name}:' @@ -592,20 +598,6 @@ sk: other: Ostatné publishing: Publikovanie web: Web - push_notifications: - favourite: - title: "%{name} si obľúbil/a tvoj príspevok" - follow: - title: "%{name} ťa teraz následuje" - group: - title: "%{count} notifikácie" - mention: - action_boost: Pozdvihni - action_expand: Ukáž viac - action_favourite: Obľúbené - title: "%{name} ťa spomenul/a" - reblog: - title: "%{name} vyzdvihli tvoj príspevok" remote_follow: acct: Napíš svoju prezývku@doménu z ktorej chceš následovať missing_resource: Nemôžeme nájsť potrebnú presmerovaciu adresu k tvojmu účtu @@ -638,10 +630,10 @@ sk: title: Sezóna settings: authorized_apps: Autorizované aplikácie - back: Späť do Mastodonu - delete: Zmazanie účtu + back: Späť na Mastodon + delete: Vymazanie účtu development: Vývoj - edit_profile: Upraviť profil + edit_profile: Uprav profil export: Exportovať dáta followers: Povolení sledovatelia import: Importovať @@ -655,9 +647,11 @@ sk: attached: description: 'Priložené: %{attached}' image: + few: "%{count} obrázky" one: "%{count} obrázok" other: "%{count} obrázkov" video: + few: "%{count} videá" one: "%{count} video" other: "%{count} videí" content_warning: 'Varovanie o obsahu: %{warning}' @@ -685,6 +679,7 @@ sk: title: Podmienky užívania, a pravidlá o súkromí pre %{instance} themes: default: Mastodon + mastodon-light: Mastodon (svetlý) time: formats: default: "%b %d, %R, %H:%M" @@ -729,5 +724,6 @@ sk: users: invalid_email: Emailová adresa je neplatná invalid_otp_token: Neplatný kód pre dvojfaktorovú autentikáciu + otp_lost_help_html: Pokiaľ si stratil/a prístup k obom, môžeš dať vedieť %{email} seamless_external_login: Si prihlásená/ý cez externú službu, takže nastavenia hesla a emailu ti niesú prístupné. signed_in_as: 'Prihlásený ako:' diff --git a/config/locales/sl.yml b/config/locales/sl.yml new file mode 100644 index 0000000000..00c4d8fb75 --- /dev/null +++ b/config/locales/sl.yml @@ -0,0 +1,104 @@ +--- +sl: + about: + about_hashtag_html: To so javni tuti, označeni z #%{hashtag}. Z njimi se lahko povežete, če imate račun kjerkoli v fediversu. + about_mastodon_html: Mastodon je socialno omrežje, ki temelji na odprtih spletnih protokolih in prosti ter odprtokodni programski opremi. Je decentraliziran, kot e-pošta. + about_this: O Mastodonu + administered_by: 'Upravlja:' + closed_registrations: Registracije so trenutno zaprte na tem vozlišču. Vendar! Tukaj lahko najdete druga vozlišča, na katerih se prijavite in dostopate do istega omrežja od tam. + contact: Kontakt + contact_missing: Ni nastavljeno + contact_unavailable: Ni na voljo + description_headline: Kaj je %{domain}? + domain_count_after: ostala vozlišča + domain_count_before: Povezan z + extended_description_html: | + Dober prostor za pravila + Razširjen opis še ni bil nastavljen. + features: + humane_approach_title: Bolj human pristop + not_a_product_title: Ti si oseba, ne izdelek + real_conversation_title: Zgrajen za pravi pogovor + within_reach_title: Vedno na dosegu roke + generic_description: "%{domain} je en strežnik v omrežju" + hosted_on: Mastodon gostuje na %{domain} + learn_more: Spoznaj več + other_instances: Seznam vozlišč + source_code: Izvorna koda + status_count_after: statusi + status_count_before: Kdo je avtor + user_count_after: uporabniki + user_count_before: Dom za + what_is_mastodon: Kaj je Mastodon? + accounts: + follow: Sledi + followers: Sledilci + following: Sledim + media: Medij + moved_html: "%{name} se je prestavil na %{new_profile_link}:" + nothing_here: Nič ni tukaj! + people_followed_by: Ljudje, ki jim sledi %{name} + people_who_follow: Ljudje, ki sledijo %{name} + posts: Tuti + posts_with_replies: Tuti in odgovori + remote_follow: Oddaljeno sledenje + reserved_username: Uporabniško ime je zasedeno + roles: + admin: Skrbnik + bot: Robot + moderator: Mod + unfollow: Prenehaj slediti + admin: + account_moderation_notes: + create: Pusti sporočilo + created_msg: Uspešno ustvarjena opomba moderiranja! + delete: Izbriši + accounts: + are_you_sure: Ali si prepričan? + avatar: Avatar + by_domain: Domena + change_email: + changed_msg: E-pošta računa je uspešno spremenjena! + current_email: Trenutna E-pošta + label: Spremeni E-pošto + new_email: Nova E-pošta + submit: Spremeni E-pošto + title: Spremeni E-pošto za %{username} + confirm: Potrdi + confirmed: Potrjeno + confirming: Potrjujem + disable: Onemogoči + disable_two_factor_authentication: Onemogoči 2FA + disabled: Onemogočeno + domain: Domena + edit: Uredi + email: E-pošta + email_status: Stanje E-pošte + enable: Omogoči + enabled: Omogočeno + feed_url: URL vir + followers: Sledilci + followers_url: URL sledilci + follows: Sledi + inbox_url: URl v mapi "Prejeto" + ip: IP + location: + all: Vse + local: Lokalno + remote: Oddaljeni + title: Lokacija + login_status: Stanje prijave + media_attachments: Medijske priloge + memorialize: Spremenite v spomin + moderation: + all: Vse + silenced: Utišan + suspended: Suspendiran + title: Moderiranje + moderation_notes: Opombe moderiranja + most_recent_activity: Zadnja aktivnost + most_recent_ip: Zadnji IP + order: + alphabetic: Po abecedi + most_recent: Najnovejše + title: Red diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml index 742c976d19..15c6b00acf 100644 --- a/config/locales/sr-Latn.yml +++ b/config/locales/sr-Latn.yml @@ -52,9 +52,7 @@ sr-Latn: unfollow: Otprati admin: account_moderation_notes: - account: Moderator create: Napravi - created_at: Datum created_msg: Moderatorska beleška uspešno napravljena! delete: Obriši destroyed_msg: Moderatorska beleška uspešno obrisana! @@ -63,6 +61,7 @@ sr-Latn: by_domain: Domen confirm: Potvrdi confirmed: Potvrđeno + confirming: Potvrđujući demote: Ražaluj disable: Isključi disable_two_factor_authentication: Isključi 2FA @@ -71,6 +70,7 @@ sr-Latn: domain: Domen edit: Izmeni email: E-pošta + email_status: Status e-pošte enable: Uključi enabled: Uključeno feed_url: Adresa dovoda @@ -108,6 +108,10 @@ sr-Latn: public: Javno push_subscription_expires: PuSH subscription expires redownload: Osveži avatar + resend_confirmation: + already_confirmed: Ovaj korisnik je već potvrđen + send: Ponovo pošaljite e-poruku za potvrdu + success: E-mail potvrde je uspešno poslat! reset: Resetuj reset_password: Resetuj lozinku resubscribe: Ponovo se pretplati @@ -246,12 +250,8 @@ sr-Latn: are_you_sure: Da li ste sigurni? comment: none: Ništa - delete: Obriši id: ID mark_as_resolved: Označi kao rešen - nsfw: - 'false': Otkrij medijske priloge - 'true': Sakrij medijske priloge report: 'Prijava #%{id}' report_contents: Sadržaj reported_account: Prijavljeni nalog @@ -310,11 +310,8 @@ sr-Latn: delete: Obriši nsfw_off: NSFW isključen nsfw_on: NSFW uključen - execute: Izvrši failed_to_execute: Neuspelo izvršavanje media: - hide: Sakrij multimediju - show: Prikaži multimediju title: Multimedija no_media: Bez multimedije title: Statusi naloga @@ -528,20 +525,6 @@ sr-Latn: other: Ostali publishing: Objavljivanje web: Veb - push_notifications: - favourite: - title: "%{name} je stavio Vaš status za omiljeni" - follow: - title: "%{name} Vas je zapratio" - group: - title: "%{count} obaveštenja" - mention: - action_boost: Podrži - action_expand: Prikaži još - action_favourite: Omiljeni - title: "%{name} Vas je pomenuo" - reblog: - title: "%{name} je podržao(la) Vaš status" remote_follow: acct: Unesite Vaš korisnik@domen sa koga želite da pratite missing_resource: Ne mogu da nađem zahtevanu adresu preusmeravanja za Vaš nalog diff --git a/config/locales/sr.yml b/config/locales/sr.yml index 0d55910a6c..d34a2ecbf2 100644 --- a/config/locales/sr.yml +++ b/config/locales/sr.yml @@ -52,9 +52,7 @@ sr: unfollow: Отпрати admin: account_moderation_notes: - account: Модератор create: Направи - created_at: Датум created_msg: Модераторска белешка успешно направљена! delete: Обриши destroyed_msg: Модераторска белешка успешно обрисана! @@ -63,6 +61,7 @@ sr: by_domain: Домен confirm: Потврди confirmed: Потврђено + confirming: Потврдување demote: Ражалуј disable: Искључи disable_two_factor_authentication: Искључи 2FA @@ -71,6 +70,7 @@ sr: domain: Домен edit: Измени email: Е-пошта + email_status: Е-пошта статус enable: Укључи enabled: Укључено feed_url: Адреса довода @@ -108,6 +108,10 @@ sr: public: Јавно push_subscription_expires: PuSH subscription expires redownload: Освежи аватар + resend_confirmation: + already_confirmed: Овој корисник е веќе потврден + send: Препрати го е-мајлот за потврда + success: Е-пошта за потврда успешно испратена! reset: Ресетуј reset_password: Ресетуј лозинку resubscribe: Поново се претплати @@ -246,12 +250,8 @@ sr: are_you_sure: Да ли сте сигурни? comment: none: Ништа - delete: Обриши id: ID mark_as_resolved: Означи као решен - nsfw: - 'false': Откриј медијске прилоге - 'true': Сакриј медијске прилоге report: 'Пријава #%{id}' report_contents: Садржај reported_account: Пријављени налог @@ -310,11 +310,8 @@ sr: delete: Обриши nsfw_off: NSFW искључен nsfw_on: NSFW укључен - execute: Изврши failed_to_execute: Неуспело извршавање media: - hide: Сакриј мултимедију - show: Прикажи мултимедију title: Мултимедија no_media: Без мултимедије title: Статуси налога @@ -528,20 +525,6 @@ sr: other: Остали publishing: Објављивање web: Веб - push_notifications: - favourite: - title: "%{name} је ставио Ваш статус за омиљени" - follow: - title: "%{name} Вас је запратио" - group: - title: "%{count} обавештења" - mention: - action_boost: Подржи - action_expand: Прикажи још - action_favourite: Омиљени - title: "%{name} Вас је поменуо" - reblog: - title: "%{name} је подржао(ла) Ваш статус" remote_follow: acct: Унесите Ваш корисник@домен са кога желите да пратите missing_resource: Не могу да нађем захтевану адресу преусмеравања за Ваш налог diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 845248652f..23ea7ddd1a 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -40,6 +40,7 @@ sv: following: Följer media: Media moved_html: "%{name} har flyttat till %{new_profile_link}:" + network_hidden: Denna information är inte tillgänglig nothing_here: Det finns inget här! people_followed_by: Personer som %{name} följer people_who_follow: Personer som följer %{name} @@ -49,13 +50,12 @@ sv: reserved_username: Användarnamnet är reserverat roles: admin: Admin + bot: Bot moderator: Moderator unfollow: Sluta följa admin: account_moderation_notes: - account: Moderator - create: Skapa - created_at: Datum + create: Lämna kommentar created_msg: Modereringsnotering skapad utan problem! delete: Ta bort destroyed_msg: Modereringsnotering borttagen utan problem! @@ -72,6 +72,7 @@ sv: title: Byt E-postadress för %{username} confirm: Bekräfta confirmed: Bekräftad + confirming: Bekräftande demote: Degradera disable: inaktivera disable_two_factor_authentication: Inaktivera 2FA @@ -80,6 +81,7 @@ sv: domain: Domän edit: Redigera email: E-post + email_status: E-poststatus enable: Aktivera enabled: Aktiverad feed_url: Flödes URL @@ -118,6 +120,10 @@ sv: push_subscription_expires: PuSH-prenumerationen löper ut redownload: Uppdatera avatar remove_avatar: Ta bort avatar + resend_confirmation: + already_confirmed: Den här användaren är redan bekräftad + send: Skicka om e-postbekräftelse + success: Bekräftelsemeddelande skickas framgångsrikt! reset: Återställ reset_password: Återställ lösenord resubscribe: Starta en ny prenumeration @@ -269,7 +275,6 @@ sv: comment: none: Ingen created_at: Anmäld - delete: Radera id: ID mark_as_resolved: Markera som löst mark_as_unresolved: Markera som olöst @@ -278,10 +283,7 @@ sv: create_and_resolve: Lös med anteckning create_and_unresolve: Återuppta med anteckning delete: Radera - placeholder: Beskriv vilka åtgärder som vidtagits eller andra uppdateringar till den här anmälan… - nsfw: - 'false': Visa bifogade mediafiler - 'true': Dölj bifogade mediafiler + placeholder: Beskriv vilka åtgärder som vidtagits eller andra uppdateringar till den här anmälan. reopen: Återuppta anmälan report: 'Anmäl #%{id}' report_contents: Innehåll @@ -356,11 +358,8 @@ sv: delete: Radera nsfw_off: Markera som ej känslig nsfw_on: Markera som känslig - execute: Utför failed_to_execute: Misslyckades att utföra media: - hide: Dölj media - show: Visa media title: Media no_media: Ingen media title: Kontostatus @@ -376,6 +375,7 @@ sv: admin_mailer: new_report: body: "%{reporter} har rapporterat %{target}" + body_remote: Någon från %{domain} har rapporterat %{target} subject: Ny rapport för %{instance} (#%{id}) application_mailer: notification_preferences: Ändra e-postinställningar @@ -465,7 +465,7 @@ sv: archive_takeout: date: Datum download: Ladda ner ditt arkiv - hint_html: Du kan begära ett arkiv av dina toots och uppladdad media. Den exporterade datan kommer att vara i ActivityPub-format och läsbar av kompatibel programvara. + hint_html: Du kan begära ett arkiv av dina toots och uppladdad media. Den exporterade datan kommer att vara i ActivityPub-format och läsbar av kompatibel programvara. Du kan begära ett arkiv var sjunde dag. in_progress: Kompilerar ditt arkiv... request: Efterfråga ditt arkiv size: Storlek @@ -595,20 +595,6 @@ sv: other: Annat publishing: Publicering web: Webb - push_notifications: - favourite: - title: "%{name} favoriserade din status" - follow: - title: "%{name} följer nu dig" - group: - title: "%{count} meddelanden" - mention: - action_boost: Knuffa - action_expand: Visa mer - action_favourite: Favoriter - title: "%{name} nämnde dig" - reblog: - title: "%{name} boostade din status" remote_follow: acct: Ange ditt användarnamn@domän du vill följa från missing_resource: Det gick inte att hitta den begärda omdirigeringsadressen för ditt konto @@ -683,6 +669,7 @@ sv: video: one: "%{count} video" other: "%{count} videor" + boosted_from_html: Boosted från %{acct_link} content_warning: 'Innehållsvarning: %{warning}' disallowed_hashtags: one: 'innehöll en otillåten hashtag: %{tags}' @@ -711,6 +698,7 @@ sv: terms: title: "%{instance} Användarvillkor och Sekretesspolicy" themes: + contrast: Hög kontrast default: Mastodon time: formats: @@ -757,5 +745,6 @@ sv: users: invalid_email: E-postadressen är ogiltig invalid_otp_token: Ogiltig tvåfaktorkod + otp_lost_help_html: Om du förlorat åtkomst till båda kan du komma i kontakt med %{email} seamless_external_login: Du är inloggad via en extern tjänst, så lösenord och e-postinställningar är inte tillgängliga. signed_in_as: 'Inloggad som:' diff --git a/config/locales/th.yml b/config/locales/th.yml index 350b93b521..6804dbd13d 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -29,11 +29,13 @@ th: are_you_sure: แน่ใจนะ? confirm: ยืนยัน confirmed: ยึนยันแล้ว + confirming: ยืนยัน disable_two_factor_authentication: Disable 2FA display_name: ชื่อสำหรับดีสเพล domain: โดแมน edit: แก้ไข email: อีเมล์ + email_status: สถานะอีเมล feed_url: Feed URL followers: ผู้ติดตาม follows: ติดตาม @@ -59,6 +61,10 @@ th: profile_url: Profile URL public: สาธารณะ push_subscription_expires: PuSH subscription expires + resend_confirmation: + already_confirmed: ผู้ใช้รายนี้ได้รับการยืนยันแล้ว + send: ส่งอีเมลยืนยันอีกครั้ง + success: ยืนยันอีเมลเรียบร้อยแล้ว! reset_password: รีเซ็ตรหัสผ่าน salmon_url: Salmon URL show: @@ -109,7 +115,6 @@ th: reports: comment: none: None - delete: ลบ id: ไอดี mark_as_resolved: ทำเครื่องหมายว่าจัดการแล้ว report: 'Report #%{id}' diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 6e7aeb77e9..8bafbface2 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -29,10 +29,12 @@ tr: are_you_sure: Emin misiniz? confirm: Onayla confirmed: Onaylandı + confirming: Onaylama display_name: Görünen adınız domain: Sunucu edit: Düzenle email: E-posta + email_status: Email Durumu feed_url: Besleme linki followers: Takipçiler follows: Takip edilen @@ -58,6 +60,10 @@ tr: profile_url: Profil linki public: Herkese açık push_subscription_expires: PuSH aboneliği dolumu + resend_confirmation: + already_confirmed: Bu kullanıcı zaten onaylandı + send: Doğrulama epostasını yeniden gönder + success: Onay e-postası başarıyla gönderildi! reset_password: Parolayı değiştir salmon_url: Salmon Linki show: @@ -108,7 +114,6 @@ tr: reports: comment: none: Yok - delete: Sil id: ID mark_as_resolved: Giderildi olarak işaretle report: 'Şikayet #%{id}' diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 44f64b5c9e..6fe46b4d98 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -100,7 +100,6 @@ uk: reports: comment: none: Немає - delete: Видалити id: ID mark_as_resolved: Відмітити як вирішену report: 'Скарга #%{id}' diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 78c72bd302..357575a3b3 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -4,6 +4,7 @@ zh-CN: about_hashtag_html: 这里展示的是带有话题标签 #%{hashtag} 的公开嘟文。如果你想与他们互动,你需要在任意一个 Mastodon 实例或与其兼容的网站上拥有一个帐户。 about_mastodon_html: Mastodon(长毛象)是一个建立在开放式网络协议和自由、开源软件之上的社交网络,有着类似于电子邮件的分布式设计。 about_this: 关于本实例 + administered_by: 本实例的管理员: closed_registrations: 这个实例目前没有开放注册。不过,你可以前往其他实例注册一个帐户,同样可以加入到这个网络中哦! contact: 联系方式 contact_missing: 未设定 @@ -25,9 +26,9 @@ zh-CN: within_reach_title: 始终触手可及 generic_description: "%{domain} 是这个庞大网络中的一台服务器" hosted_on: 一个在 %{domain} 上运行的 Mastodon 实例 - learn_more: 详细了解 + learn_more: 了解详情 other_instances: 其他实例 - source_code: 源码 + source_code: 源代码 status_count_after: 条嘟文 status_count_before: 他们共嘟出了 user_count_after: 位用户 @@ -39,6 +40,7 @@ zh-CN: following: 正在关注 media: 媒体 moved_html: "%{name} 已经迁移到 %{new_profile_link}:" + network_hidden: 此信息不可用。 nothing_here: 这里神马都没有! people_followed_by: "%{name} 关注的人" people_who_follow: 关注 %{name} 的人 @@ -48,21 +50,29 @@ zh-CN: reserved_username: 此用户名已被保留 roles: admin: 管理员 + bot: 机器人 moderator: 监察员 unfollow: 取消关注 admin: account_moderation_notes: - account: 管理员 create: 新建 - created_at: 日期 created_msg: 管理备忘建立成功! delete: 删除 destroyed_msg: 管理备忘删除成功! accounts: are_you_sure: 你确定吗? + avatar: 头像 by_domain: 域名 + change_email: + changed_msg: 帐户电子邮件地址更改成功! + current_email: 当前的电子邮件地址 + label: 更改电子邮件地址 + new_email: 新的电子邮件地址 + submit: 更改电子邮件地址 + title: 为 %{username} 更改电子邮件地址 confirm: 确认 confirmed: 已确认 + confirming: 确认 demote: 降任 disable: 停用 disable_two_factor_authentication: 停用双重认证 @@ -71,6 +81,7 @@ zh-CN: domain: 域名 edit: 编辑 email: 电子邮件地址 + email_status: 电子邮件地址状态 enable: 启用 enabled: 已启用 feed_url: 订阅 URL @@ -108,6 +119,11 @@ zh-CN: public: 公开页面 push_subscription_expires: PuSH 订阅过期时间 redownload: 刷新头像 + remove_avatar: 删除头像 + resend_confirmation: + already_confirmed: 该用户已被确认 + send: 重发确认邮件 + success: 确认邮件发送成功! reset: 重置 reset_password: 重置密码 resubscribe: 重新订阅 @@ -128,6 +144,7 @@ zh-CN: statuses: 嘟文 subscribe: 订阅 title: 用户 + unconfirmed_email: 待验证的电子邮件地址 undo_silenced: 解除隐藏 undo_suspension: 解除封禁 unsubscribe: 取消订阅 @@ -135,6 +152,8 @@ zh-CN: web: 站内页面 action_logs: actions: + assigned_to_self_report: "%{name} 接管了举报 %{target}" + change_email_user: "%{name} 更改了用户 %{target} 的电子邮件地址" confirm_user: "%{name} 确认了用户 %{target} 的电子邮件地址" create_custom_emoji: "%{name} 添加了新的自定义表情 %{target}" create_domain_block: "%{name} 屏蔽了域名 %{target}" @@ -150,10 +169,13 @@ zh-CN: enable_user: "%{name} 将用户 %{target} 设置为允许登录" memorialize_account: "%{name} 将 %{target} 设置为追悼帐户" promote_user: "%{name} 对用户 %{target} 进行了升任操作" + remove_avatar_user: "%{name} 删除了 %{target} 的头像" + reopen_report: "%{name} 重开了举报 %{target}" reset_password_user: "%{name} 重置了用户 %{target} 的密码" resolve_report: "%{name} 处理了举报 %{target}" silence_account: "%{name} 隐藏了用户 %{target}" suspend_account: "%{name} 封禁了用户 %{target}" + unassigned_report: "%{name} 放弃了举报 %{target} 的接管" unsilence_account: "%{name} 解除了用户 %{target} 的隐藏状态" unsuspend_account: "%{name} 解除了用户 %{target} 的封禁状态" update_custom_emoji: "%{name} 更新了自定义表情 %{target}" @@ -237,28 +259,44 @@ zh-CN: expired: 已失效 title: 筛选 title: 邀请用户 + report_notes: + created_msg: 举报记录建立成功! + destroyed_msg: 举报记录删除成功! reports: + account: + note: 条记录 + report: 条举报 action_taken_by: 操作执行者 are_you_sure: 你确定吗? + assign_to_self: 接管 + assigned: 已接管的监察员 comment: none: 没有 - delete: 删除 + created_at: 举报时间 id: ID mark_as_resolved: 标记为“已处理” - nsfw: - 'false': 取消 NSFW 标记 - 'true': 添加 NSFW 标记 + mark_as_unresolved: 标记为“未处理” + notes: + create: 添加记录 + create_and_resolve: 添加记录并标记为“已处理” + create_and_unresolve: 添加记录并重开 + delete: 删除 + placeholder: 描述已经执行的操作,或其他任何相关的跟进情况 + reopen: 重开举报 report: '举报 #%{id}' report_contents: 内容 reported_account: 举报用户 reported_by: 举报人 resolved: 已处理 + resolved_msg: 举报处理成功! silence_account: 隐藏用户 status: 状态 suspend_account: 封禁用户 target: 被举报人 title: 举报 + unassign: 取消接管 unresolved: 未处理 + updated_at: 更新时间 view: 查看 settings: activity_api_enabled: @@ -270,6 +308,9 @@ zh-CN: contact_information: email: 用于联系的公开电子邮件地址 username: 用于联系的公开用户名 + hero: + desc_html: 用于在首页展示。推荐分辨率 600×100px 以上。未指定的情况下将默认使用本站缩略图 + title: 主题图片 peers_api_enabled: desc_html: 截至目前本实例在网络中已发现的域名 title: 公开已知实例的列表 @@ -286,11 +327,14 @@ zh-CN: open: desc_html: 允许所有人建立帐户 title: 开放注册 + show_known_fediverse_at_about_page: + desc_html: 启用此选项将会在预览中显示来自已知实例的嘟文,否则只会显示本站时间轴的内容 + title: 在时间轴预览中显示已知实例 show_staff_badge: desc_html: 在个人资料页上显示管理人员标志 title: 显示管理人员标志 site_description: - desc_html: 用于首页展示以及 meta 标签中的网站简介。可以使用 HTML 标签,包括 <a> 和 <em>。 + desc_html: 用于首页展示以及 meta 标签中的网站简介。可以使用 HTML 标签,包括 <a> 和 <em> title: 本站简介 site_description_extended: desc_html: 可以填写行为守则、规定、指南或其他本站特有的内容。可以使用 HTML 标签 @@ -310,13 +354,10 @@ zh-CN: back_to_account: 返回帐户信息页 batch: delete: 删除 - nsfw_off: 取消 NSFW 标记 - nsfw_on: 添加 NSFW 标记 - execute: 执行 + nsfw_off: 标记为非敏感内容 + nsfw_on: 标记为敏感内容 failed_to_execute: 执行失败 media: - hide: 隐藏媒体文件 - show: 显示媒体文件 title: 媒体文件 no_media: 不含媒体文件 title: 帐户嘟文 @@ -331,7 +372,8 @@ zh-CN: title: 管理 admin_mailer: new_report: - body: "%{reporter} 举报了用户 %{target}。" + body: "%{reporter} 举报了用户 %{target}" + body_remote: 来自 %{domain} 的用户举报了用户 %{target} subject: 来自 %{instance} 的用户举报(#%{id}) application_mailer: notification_preferences: 更改电子邮件首选项 @@ -350,6 +392,8 @@ zh-CN: your_token: 你的访问令牌 auth: agreement_html: 注册即表示你同意遵守本实例的相关规定和我们的使用条款。 + change_password: 密码 + confirm_email: 确认电子邮件地址 delete_account: 删除帐户 delete_account_html: 如果你想删除你的帐户,请点击这里继续。你需要确认你的操作。 didnt_get_confirmation: 没有收到确认邮件? @@ -359,12 +403,19 @@ zh-CN: logout: 登出 migrate_account: 迁移到另一个帐户 migrate_account_html: 如果你希望引导他人关注另一个帐户,请点击这里进行设置。 + or: 或者 + or_log_in_with: 或通过其他方式登录 + providers: + cas: CAS + saml: SAML register: 注册 + register_elsewhere: 前往其他实例注册 resend_confirmation: 重新发送确认邮件 reset_password: 重置密码 security: 帐户安全 set_new_password: 设置新密码 authorize_follow: + already_following: 你已经在关注此用户了 error: 对不起,寻找这个跨站用户时出错 follow: 关注 follow_request: 关注请求已发送给: @@ -409,6 +460,13 @@ zh-CN: title: 这个页面有问题 noscript_html: 使用 Mastodon 网页版应用需要启用 JavaScript。你也可以选择适用于你的平台的 Mastodon 应用。 exports: + archive_takeout: + date: 日期 + download: 下载你的存档 + hint_html: 你可以请求一份帐户数据存档,其中包含你的嘟文和已上传的媒体文件。导出的数据为 ActivityPub 格式,因而可以被兼容的软件读取。每次允许请求存档的间隔至少为 7 天。 + in_progress: 正在准备你的存档…… + request: 请求你的存档 + size: 大小 blocks: 屏蔽的用户 csv: CSV follows: 关注的用户 @@ -448,6 +506,7 @@ zh-CN: '21600': 6 小时后 '3600': 1 小时后 '43200': 12 小时后 + '604800': 1 周后 '86400': 1 天后 expires_in_prompt: 永不过期 generate: 生成邀请链接 @@ -518,7 +577,9 @@ zh-CN: trillion: T unit: '' pagination: + newer: 更新 next: 下一页 + older: 更早 prev: 上一页 truncate: "…" preferences: @@ -526,25 +587,15 @@ zh-CN: other: 其他 publishing: 发布 web: 站内 - push_notifications: - favourite: - title: "%{name} 收藏了你的嘟文" - follow: - title: "%{name} 关注了你" - group: - title: "%{count} 条新通知" - mention: - action_boost: 转嘟 - action_expand: 显示更多 - action_favourite: 收藏 - title: "%{name} 提到了你" - reblog: - title: "%{name} 转嘟了你的嘟文" remote_follow: acct: 请输入你的“用户名@实例域名” missing_resource: 无法确定你的帐户的跳转 URL proceed: 确认关注 prompt: 你正准备关注: + remote_unfollow: + error: 错误 + title: 标题 + unfollowed: 已取消关注 sessions: activity: 最后一次活跃的时间 browser: 浏览器 @@ -553,12 +604,14 @@ zh-CN: blackberry: Blackberry chrome: Chrome edge: Microsoft Edge + electron: Electron firefox: Firefox generic: 未知浏览器 ie: Internet Explorer micro_messenger: 微信 nokia: Nokia S40 Ovi 浏览器 opera: Opera + otter: Otter phantom_js: PhantomJS qq: QQ浏览器 safari: Safari @@ -586,7 +639,7 @@ zh-CN: title: 会话 settings: authorized_apps: 已授权的应用 - back: 回到 Mastodon + back: 返回 Mastodon delete: 删除帐户 development: 开发 edit_profile: 更改个人资料 @@ -600,6 +653,15 @@ zh-CN: two_factor_authentication: 双重认证 your_apps: 你的应用 statuses: + attached: + description: 附加媒体:%{attached} + image: "%{count} 张图片" + video: "%{count} 个视频" + boosted_from_html: 转嘟自 %{acct_link} + content_warning: 内容警告:%{warning} + disallowed_hashtags: + one: 包含了一个禁止的话题标签:%{tags} + other: 包含了这些禁止的话题标签:%{tags} open_in_web: 在站内打开 over_character_limit: 超过了 %{max} 字的限制 pin_errors: @@ -623,6 +685,10 @@ zh-CN: sensitive_content: 敏感内容 terms: title: "%{instance} 使用条款和隐私权政策" + themes: + contrast: 高对比度 + default: Mastodon + mastodon-light: Mastodon(亮色主题) time: formats: default: "%Y年%-m月%d日 %H:%M" @@ -643,6 +709,10 @@ zh-CN: setup: 设置 wrong_code: 输入的认证码无效!请核对一下你的设备显示的时间,如果正确,你可能需要联系一下实例的管理员,让他们校准服务器的时间。 user_mailer: + backup_ready: + explanation: 你请求了一份 Mastodon 帐户的完整备份。现在你可以下载了! + subject: 你的存档已经准备完毕 + title: 存档导出 welcome: edit_profile_action: 设置个人资料 edit_profile_step: 你可以自定义你的个人资料,包括上传头像、横幅图片、更改昵称等等。如果你想在新的关注者关注你之前对他们进行审核,你也可以选择为你的帐户开启保护。 @@ -664,4 +734,6 @@ zh-CN: users: invalid_email: 输入的电子邮件地址无效 invalid_otp_token: 输入的双重认证代码无效 + otp_lost_help_html: 如果你不慎丢失了所有的代码,请联系 %{email} 寻求帮助 + seamless_external_login: 因为你是通过外部服务登录的,所以密码和电子邮件地址设置都不可用。 signed_in_as: 当前登录的帐户: diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml index a27b0c04c8..c489d8bd49 100644 --- a/config/locales/zh-HK.yml +++ b/config/locales/zh-HK.yml @@ -40,6 +40,7 @@ zh-HK: following: 正在關注 media: 媒體 moved_html: "%{name} 已經轉移到 %{new_profile_link}:" + network_hidden: 此信息不可用 nothing_here: 暫時未有內容可以顯示 people_followed_by: "%{name} 關注的人" people_who_follow: 關注 %{name} 的人 @@ -49,13 +50,12 @@ zh-HK: reserved_username: 此用戶名已被保留 roles: admin: 管理員 - moderator: 監察员 + bot: 機械人 + moderator: 監察員 unfollow: 取消關注 admin: account_moderation_notes: - account: 管理員 - create: 新增 - created_at: 日期 + create: 記錄 created_msg: 管理記錄已新增 delete: 刪除 destroyed_msg: 管理記錄已被刪除 @@ -72,6 +72,7 @@ zh-HK: title: 改變 %{username} 的電郵 confirm: 確定 confirmed: 已確定 + confirming: 確定 demote: 降任 disable: 停用 disable_two_factor_authentication: 停用雙重認證 @@ -80,9 +81,10 @@ zh-HK: domain: 域名 edit: 編輯 email: 電郵地址 + email_status: 电子邮件状态 enable: 啟用 enabled: 已啟用 - feed_url: Feed URL + feed_url: 訂閱 URL followers: 關注者 followers_url: 關注者(Followers)URL follows: 正在關注 @@ -118,13 +120,17 @@ zh-HK: push_subscription_expires: PuSH 訂閱過期 redownload: 更新頭像 remove_avatar: 取消頭像 + resend_confirmation: + already_confirmed: 该用户已被确认 + send: 重发确认邮件 + success: 确认电子邮件成功发送! reset: 重設 reset_password: 重設密碼 resubscribe: 重新訂閱 role: 身份 roles: admin: 管理員 - moderator: 監察员 + moderator: 監察員 staff: 管理人員 user: 普通用戶 salmon_url: Salmon 反饋 URL @@ -223,6 +229,7 @@ zh-HK: severity: 阻隔分級 show: affected_accounts: + one: 資料庫中有 %{count} 個用戶受影響 other: 資料庫中有%{count}個用戶受影響 retroactive: silence: 對此域名的所有用戶取消靜音 @@ -268,7 +275,6 @@ zh-HK: comment: none: 沒有 created_at: 日期 - delete: 刪除 id: ID mark_as_resolved: 標示為「已處理」 mark_as_unresolved: 標示為「未處理」 @@ -277,10 +283,7 @@ zh-HK: create_and_resolve: 建立筆記並標示為「已處理」 create_and_unresolve: 建立筆記並標示為「未處理」 delete: 刪除 - placeholder: 記錄已執行的動作,或其他更新 - nsfw: - 'false': 取消 NSFW 標記 - 'true': 添加 NSFW 標記 + placeholder: 記錄已執行的動作,或其他相關的更新…… reopen: 重開舉報 report: '舉報 #%{id}' report_contents: 舉報內容 @@ -355,11 +358,8 @@ zh-HK: delete: 刪除 nsfw_off: 取消 NSFW 標記 nsfw_on: 添加 NSFW 標記 - execute: 執行 failed_to_execute: 執行失敗 media: - hide: 隱藏媒體檔案 - show: 顯示媒體檔案 title: 媒體檔案 no_media: 不含媒體檔案 title: 帳戶文章 @@ -375,6 +375,7 @@ zh-HK: admin_mailer: new_report: body: "%{reporter} 舉報了用戶 %{target}。" + body_remote: 來自 %{domain} 的用戶舉報了用戶 %{target} subject: 來自 %{instance} 的用戶舉報(#%{id}) application_mailer: notification_preferences: 更改電郵首選項 @@ -464,7 +465,7 @@ zh-HK: archive_takeout: date: 日期 download: 下載檔案 - hint_html: 你可以下載包含你的文章和媒體的檔案。資料以 ActivityPub 格式儲存,可用於相容的軟體。 + hint_html: 你可以下載包含你的文章和媒體的檔案。資料以 ActivityPub 格式儲存,可用於相容的軟體。你可以每七天下載一次。 in_progress: 檔案製作中... request: 下載檔案 size: 檔案大小 @@ -513,7 +514,9 @@ zh-HK: '86400': 1 天後 expires_in_prompt: 永不過期 generate: 生成邀請連結 - max_uses: "%{count} 次" + max_uses: + one: 1 次 + other: "%{count} 次" max_uses_prompt: 無限制 prompt: 生成分享連結,邀請他人在本服務站註冊 table: @@ -580,7 +583,6 @@ zh-HK: quadrillion: Q thousand: K trillion: T - unit: '' pagination: newer: 較新 next: 下一頁 @@ -592,20 +594,6 @@ zh-HK: other: 其他 publishing: 發佈 web: 站内 - push_notifications: - favourite: - title: "%{name} 收藏了你的文章" - follow: - title: "%{name} 關注了你" - group: - title: "%{count} 條新通知" - mention: - action_boost: 轉推 - action_expand: 顯示更多 - action_favourite: 收藏 - title: "%{name} 提到了你" - reblog: - title: "%{name} 轉推了你的文章" remote_follow: acct: 請輸入你的︰用戶名稱@服務點域名 missing_resource: 無法找到你用戶的轉接網址 @@ -672,9 +660,17 @@ zh-HK: statuses: attached: description: 附件: %{attached} - image: "%{count} 張圖片" - video: "%{count} 段影片" - content_warning: 'Content warning: %{warning}' + image: + one: "%{count} 幅圖片" + other: "%{count} 幅圖片" + video: + one: "%{count} 段影片" + other: "%{count} 段影片" + boosted_from_html: 轉推自 %{acct_link} + content_warning: 內容警告: %{warning} + disallowed_hashtags: + one: 包含不允許的標籤: %{tags} + other: 包含不允許的標籤: %{tags} open_in_web: 開啟網頁 over_character_limit: 超過了 %{max} 字的限制 pin_errors: @@ -698,6 +694,10 @@ zh-HK: sensitive_content: 敏感內容 terms: title: "%{instance} 使用條款和隱私權政策" + themes: + contrast: 高對比 + default: 萬象 + mastodon-light: 萬象(亮色主題) time: formats: default: "%Y年%-m月%d日 %H:%M" @@ -743,5 +743,6 @@ zh-HK: users: invalid_email: 電郵地址格式不正確 invalid_otp_token: 雙重認證確認碼不正確 + otp_lost_help_html: 如果你無法訪問這兩者,可以通過 %{email} 與我們聯繫。 seamless_external_login: 由於你是從外部系統登入,所以不能設定密碼和電郵。 signed_in_as: 目前登入的帳戶: diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index f69d22d79a..d8c0f89a96 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -80,7 +80,6 @@ zh-TW: reports: comment: none: 無 - delete: 刪除 id: ID mark_as_resolved: 標記為已解決 report: '檢舉 #%{id}' diff --git a/config/routes.rb b/config/routes.rb index 9d4aa00edb..6dd9975b4e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -14,7 +14,9 @@ Rails.application.routes.draw do end use_doorkeeper do - controllers authorizations: 'oauth/authorizations', authorized_applications: 'oauth/authorized_applications' + controllers authorizations: 'oauth/authorizations', + authorized_applications: 'oauth/authorized_applications', + tokens: 'oauth/tokens' end get '.well-known/host-meta', to: 'well_known/host_meta#show', as: :host_meta, defaults: { format: 'xml' } @@ -144,7 +146,7 @@ Rails.application.routes.draw do end resources :reports, only: [:index, :show, :update] do - resources :reported_statuses, only: [:create, :update, :destroy] + resources :reported_statuses, only: [:create] end resources :report_notes, only: [:create, :destroy] @@ -164,9 +166,14 @@ Rails.application.routes.draw do resource :reset, only: [:create] resource :silence, only: [:create, :destroy] resource :suspension, only: [:create, :destroy] - resource :confirmation, only: [:create] resources :statuses, only: [:index, :create, :update, :destroy] + resource :confirmation, only: [:create] do + collection do + post :resend + end + end + resource :role do member do post :promote @@ -264,6 +271,7 @@ Rails.application.routes.draw do resources :favourites, only: [:index] resources :bookmarks, only: [:index] resources :reports, only: [:index, :create] + resources :trends, only: [:index] namespace :apps do get :verify_credentials, to: 'credentials#show' @@ -319,6 +327,14 @@ Rails.application.routes.draw do resources :lists, only: [:index, :create, :show, :update, :destroy] do resource :accounts, only: [:show, :create, :destroy], controller: 'lists/accounts' end + + namespace :push do + resource :subscription, only: [:create, :show, :update, :destroy] + end + end + + namespace :v2 do + get '/search', to: 'search#index', as: :search end namespace :web do diff --git a/config/settings.yml b/config/settings.yml index a92a0bfd0e..4a3720c2de 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -20,6 +20,7 @@ defaults: &defaults min_invite_role: 'admin' show_staff_badge: true default_sensitive: false + hide_network: false unfollow_modal: false boost_modal: false favourite_modal: false diff --git a/config/webpack/configuration.js b/config/webpack/configuration.js index 47883b68d2..4d0d28582a 100644 --- a/config/webpack/configuration.js +++ b/config/webpack/configuration.js @@ -80,7 +80,10 @@ module.exports = { settings, core, flavours, - env, + env: { + CDN_HOST: env.CDN_HOST, + NODE_ENV: env.NODE_ENV, + }, loadersDir, output, }; diff --git a/config/webpack/production.js b/config/webpack/production.js index e1c681232e..1469a948f2 100644 --- a/config/webpack/production.js +++ b/config/webpack/production.js @@ -8,6 +8,7 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPl const OfflinePlugin = require('offline-plugin'); const { publicPath } = require('./configuration.js'); const path = require('path'); +const { URL } = require('whatwg-url'); let compressionAlgorithm; try { @@ -19,6 +20,21 @@ try { compressionAlgorithm = 'gzip'; } +let attachmentHost; + +if (process.env.S3_ENABLED === 'true') { + if (process.env.S3_CLOUDFRONT_HOST) { + attachmentHost = process.env.S3_CLOUDFRONT_HOST; + } else { + attachmentHost = process.env.S3_HOSTNAME || `s3-${process.env.S3_REGION || 'us-east-1'}.amazonaws.com`; + } +} else if (process.env.SWIFT_ENABLED === 'true') { + const { host } = new URL(process.env.SWIFT_OBJECT_URL); + attachmentHost = host; +} else { + attachmentHost = null; +} + module.exports = merge(sharedConfig, { output: { filename: '[name]-[chunkhash].js', @@ -90,7 +106,7 @@ module.exports = merge(sharedConfig, { '**/*.woff', ], ServiceWorker: { - entry: `imports-loader?process.env=>${encodeURIComponent(JSON.stringify(process.env))}!${encodeURI(path.join(__dirname, '../../app/javascript/mastodon/service_worker/entry.js'))}`, + entry: `imports-loader?ATTACHMENT_HOST=>${encodeURIComponent(JSON.stringify(attachmentHost))}!${encodeURI(path.join(__dirname, '../../app/javascript/mastodon/service_worker/entry.js'))}`, cacheName: 'mastodon', output: '../assets/sw.js', publicPath: '/sw.js', diff --git a/db/migrate/20180416210259_add_uri_to_relationships.rb b/db/migrate/20180416210259_add_uri_to_relationships.rb new file mode 100644 index 0000000000..d8eaca450b --- /dev/null +++ b/db/migrate/20180416210259_add_uri_to_relationships.rb @@ -0,0 +1,7 @@ +class AddUriToRelationships < ActiveRecord::Migration[5.2] + def change + add_column :follows, :uri, :string + add_column :follow_requests, :uri, :string + add_column :blocks, :uri, :string + end +end diff --git a/db/migrate/20180506221944_add_actor_type_to_accounts.rb b/db/migrate/20180506221944_add_actor_type_to_accounts.rb new file mode 100644 index 0000000000..7cfed640f7 --- /dev/null +++ b/db/migrate/20180506221944_add_actor_type_to_accounts.rb @@ -0,0 +1,5 @@ +class AddActorTypeToAccounts < ActiveRecord::Migration[5.2] + def change + add_column :accounts, :actor_type, :string + end +end diff --git a/db/migrate/20180510214435_add_access_token_id_to_web_push_subscriptions.rb b/db/migrate/20180510214435_add_access_token_id_to_web_push_subscriptions.rb new file mode 100644 index 0000000000..94ef8e0f59 --- /dev/null +++ b/db/migrate/20180510214435_add_access_token_id_to_web_push_subscriptions.rb @@ -0,0 +1,6 @@ +class AddAccessTokenIdToWebPushSubscriptions < ActiveRecord::Migration[5.2] + def change + add_reference :web_push_subscriptions, :access_token, null: true, default: nil, foreign_key: { on_delete: :cascade, to_table: :oauth_access_tokens }, index: false + add_reference :web_push_subscriptions, :user, null: true, default: nil, foreign_key: { on_delete: :cascade }, index: false + end +end diff --git a/db/migrate/20180510230049_migrate_web_push_subscriptions.rb b/db/migrate/20180510230049_migrate_web_push_subscriptions.rb new file mode 100644 index 0000000000..6de1bed795 --- /dev/null +++ b/db/migrate/20180510230049_migrate_web_push_subscriptions.rb @@ -0,0 +1,13 @@ +class MigrateWebPushSubscriptions < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def up + add_index :web_push_subscriptions, :user_id, algorithm: :concurrently + add_index :web_push_subscriptions, :access_token_id, algorithm: :concurrently + end + + def down + remove_index :web_push_subscriptions, :user_id + remove_index :web_push_subscriptions, :access_token_id + end +end diff --git a/db/migrate/20180514130000_improve_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb b/db/migrate/20180514130000_improve_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb new file mode 100644 index 0000000000..2573bdf949 --- /dev/null +++ b/db/migrate/20180514130000_improve_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class ImproveIndexOnStatusesForApiV1AccountsAccountIdStatuses < ActiveRecord::Migration[5.1] + disable_ddl_transaction! + + def change + add_index :statuses, [:account_id, :id, :visibility], where: 'visibility IN (0, 1, 2)', algorithm: :concurrently + add_index :statuses, [:account_id, :id], where: 'visibility = 3', algorithm: :concurrently + remove_index :statuses, column: [:account_id, :id, :visibility, :updated_at], order: { id: :desc }, algorithm: :concurrently, name: :index_statuses_20180106 + end +end diff --git a/db/migrate/20180514140000_revert_index_change_on_statuses_for_api_v1_accounts_account_id_statuses.rb b/db/migrate/20180514140000_revert_index_change_on_statuses_for_api_v1_accounts_account_id_statuses.rb new file mode 100644 index 0000000000..b6b9f96ae8 --- /dev/null +++ b/db/migrate/20180514140000_revert_index_change_on_statuses_for_api_v1_accounts_account_id_statuses.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class RevertIndexChangeOnStatusesForApiV1AccountsAccountIdStatuses < ActiveRecord::Migration[5.1] + disable_ddl_transaction! + + def change + safety_assured do + add_index :statuses, [:account_id, :id, :visibility, :updated_at], order: { id: :desc }, algorithm: :concurrently, name: :index_statuses_20180106 + end + + remove_index :statuses, column: [:account_id, :id, :visibility], where: 'visibility IN (0, 1, 2)', algorithm: :concurrently + remove_index :statuses, column: [:account_id, :id], where: 'visibility = 3', algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index 6413db3f08..def0505a6f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2018_04_10_220657) do +ActiveRecord::Schema.define(version: 2018_05_14_140000) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -75,6 +75,7 @@ ActiveRecord::Schema.define(version: 2018_04_10_220657) do t.bigint "moved_to_account_id" t.string "featured_collection_url" t.jsonb "fields" + t.string "actor_type" t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower" t.index ["uri"], name: "index_accounts_on_uri" @@ -111,6 +112,7 @@ ActiveRecord::Schema.define(version: 2018_04_10_220657) do t.datetime "updated_at", null: false t.bigint "account_id", null: false t.bigint "target_account_id", null: false + t.string "uri" t.index ["account_id", "target_account_id"], name: "index_blocks_on_account_id_and_target_account_id", unique: true end @@ -185,6 +187,7 @@ ActiveRecord::Schema.define(version: 2018_04_10_220657) do t.bigint "account_id", null: false t.bigint "target_account_id", null: false t.boolean "show_reblogs", default: true, null: false + t.string "uri" t.index ["account_id", "target_account_id"], name: "index_follow_requests_on_account_id_and_target_account_id", unique: true end @@ -194,6 +197,7 @@ ActiveRecord::Schema.define(version: 2018_04_10_220657) do t.bigint "account_id", null: false t.bigint "target_account_id", null: false t.boolean "show_reblogs", default: true, null: false + t.string "uri" t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true end @@ -556,6 +560,10 @@ ActiveRecord::Schema.define(version: 2018_04_10_220657) do t.json "data" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "access_token_id" + t.bigint "user_id" + t.index ["access_token_id"], name: "index_web_push_subscriptions_on_access_token_id" + t.index ["user_id"], name: "index_web_push_subscriptions_on_user_id" end create_table "web_settings", force: :cascade do |t| @@ -625,5 +633,7 @@ ActiveRecord::Schema.define(version: 2018_04_10_220657) do add_foreign_key "subscriptions", "accounts", name: "fk_9847d1cbb5", on_delete: :cascade add_foreign_key "users", "accounts", name: "fk_50500f500d", on_delete: :cascade add_foreign_key "users", "invites", on_delete: :nullify + add_foreign_key "web_push_subscriptions", "oauth_access_tokens", column: "access_token_id", on_delete: :cascade + add_foreign_key "web_push_subscriptions", "users", on_delete: :cascade add_foreign_key "web_settings", "users", name: "fk_11910667b2", on_delete: :cascade end diff --git a/docker-compose.yml b/docker-compose.yml index 8058326dc6..496fb25487 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,7 +35,7 @@ services: image: tootsuite/mastodon restart: always env_file: .env.production - command: bundle exec rails s -p 3000 -b '0.0.0.0' + command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000 -b '0.0.0.0'" networks: - external_network - internal_network diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 1d63048645..9a7d49674c 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -9,11 +9,11 @@ module Mastodon end def minor - 3 + 4 end def patch - 3 + 0 end def pre @@ -48,5 +48,9 @@ module Mastodon source_base_url end end + + def user_agent + @user_agent ||= "#{HTTP::Request::USER_AGENT} (Mastodon/#{Version}; +http#{Rails.configuration.x.use_https ? 's' : ''}://#{Rails.configuration.x.web_domain}/)" + end end end diff --git a/package.json b/package.json index d861f1723f..a5be28b843 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "emoji-mart": "Gargron/emoji-mart#build", "es6-symbol": "^3.1.1", "escape-html": "^1.0.3", + "exif-js": "^2.3.0", "express": "^4.16.2", "extract-text-webpack-plugin": "^3.0.2", "file-loader": "^0.11.2", @@ -97,6 +98,7 @@ "react-redux-loading-bar": "^2.9.3", "react-router-dom": "^4.1.1", "react-router-scroll-4": "^1.0.0-beta.1", + "react-sparklines": "^1.7.0", "react-swipeable-views": "^0.12.3", "react-textarea-autosize": "^5.2.1", "react-toggle": "^4.0.1", @@ -120,7 +122,8 @@ "webpack-bundle-analyzer": "^2.9.1", "webpack-manifest-plugin": "^1.2.1", "webpack-merge": "^4.1.1", - "websocket.js": "^0.1.12" + "websocket.js": "^0.1.12", + "whatwg-url": "^6.4.1" }, "devDependencies": { "babel-eslint": "^8.2.1", diff --git a/spec/controllers/admin/account_moderation_notes_controller_spec.rb b/spec/controllers/admin/account_moderation_notes_controller_spec.rb index ca4e55c4d4..410ce6543d 100644 --- a/spec/controllers/admin/account_moderation_notes_controller_spec.rb +++ b/spec/controllers/admin/account_moderation_notes_controller_spec.rb @@ -1,4 +1,46 @@ require 'rails_helper' RSpec.describe Admin::AccountModerationNotesController, type: :controller do + render_views + + let(:user) { Fabricate(:user, admin: true) } + let(:target_account) { Fabricate(:account) } + + before do + sign_in user, scope: :user + end + + describe 'POST #create' do + subject { post :create, params: params } + + context 'when parameters are valid' do + let(:params) { { account_moderation_note: { target_account_id: target_account.id, content: 'test content' } } } + + it 'successfully creates a note' do + expect { subject }.to change { AccountModerationNote.count }.by(1) + expect(subject).to redirect_to admin_account_path(target_account.id) + end + end + + context 'when parameters are invalid' do + let(:params) { { account_moderation_note: { target_account_id: target_account.id, content: '' } } } + + it 'falls to create a note' do + expect { subject }.not_to change { AccountModerationNote.count } + expect(subject).to render_template 'admin/accounts/show' + end + end + end + + describe 'DELETE #destroy' do + subject { delete :destroy, params: { id: note.id } } + + let!(:note) { Fabricate(:account_moderation_note, account: account, target_account: target_account) } + let(:account) { Fabricate(:account) } + + it 'destroys note' do + expect { subject }.to change { AccountModerationNote.count }.by(-1) + expect(subject).to redirect_to admin_account_path(target_account.id) + end + end end diff --git a/spec/controllers/admin/confirmations_controller_spec.rb b/spec/controllers/admin/confirmations_controller_spec.rb index 7c80349646..eec2b2f5c4 100644 --- a/spec/controllers/admin/confirmations_controller_spec.rb +++ b/spec/controllers/admin/confirmations_controller_spec.rb @@ -30,4 +30,35 @@ RSpec.describe Admin::ConfirmationsController, type: :controller do expect(response).to have_http_status(404) end end + + describe 'POST #resernd' do + subject { post :resend, params: { account_id: account.id } } + + let(:account) { Fabricate(:account) } + let!(:user) { Fabricate(:user, confirmed_at: confirmed_at, account: account) } + + before do + allow(UserMailer).to receive(:confirmation_instructions) { double(:email, deliver_later: nil) } + end + + context 'when email is not confirmed' do + let(:confirmed_at) { nil } + + it 'resends confirmation mail' do + expect(subject).to redirect_to admin_accounts_path + expect(flash[:notice]).to eq I18n.t('admin.accounts.resend_confirmation.success') + expect(UserMailer).to have_received(:confirmation_instructions).once + end + end + + context 'when email is confirmed' do + let(:confirmed_at) { Time.zone.now } + + it 'does not resend confirmation mail' do + expect(subject).to redirect_to admin_accounts_path + expect(flash[:error]).to eq I18n.t('admin.accounts.resend_confirmation.already_confirmed') + expect(UserMailer).not_to have_received(:confirmation_instructions) + end + end + end end diff --git a/spec/controllers/admin/custom_emojis_controller_spec.rb b/spec/controllers/admin/custom_emojis_controller_spec.rb new file mode 100644 index 0000000000..b7e2894e90 --- /dev/null +++ b/spec/controllers/admin/custom_emojis_controller_spec.rb @@ -0,0 +1,115 @@ +require 'rails_helper' + +describe Admin::CustomEmojisController do + render_views + + let(:user) { Fabricate(:user, admin: true) } + + before do + sign_in user, scope: :user + end + + describe 'GET #index' do + subject { get :index } + + before do + Fabricate(:custom_emoji) + end + + it 'renders index page' do + expect(subject).to have_http_status 200 + expect(subject).to render_template :index + end + end + + describe 'GET #new' do + subject { get :new } + + it 'renders new page' do + expect(subject).to have_http_status 200 + expect(subject).to render_template :new + end + end + + describe 'POST #create' do + subject { post :create, params: { custom_emoji: params } } + + let(:image) { fixture_file_upload(Rails.root.join('spec', 'fixtures', 'files', 'emojo.png'), 'image/png') } + + context 'when parameter is valid' do + let(:params) { { shortcode: 'test', image: image } } + + it 'creates custom emoji' do + expect { subject }.to change { CustomEmoji.count }.by(1) + end + end + + context 'when parameter is invalid' do + let(:params) { { shortcode: 't', image: image } } + + it 'renders new' do + expect(subject).to render_template :new + end + end + end + + describe 'PUT #update' do + let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test') } + let(:image) { fixture_file_upload(Rails.root.join('spec', 'fixtures', 'files', 'emojo.png'), 'image/png') } + + before do + put :update, params: { id: custom_emoji.id, custom_emoji: params } + end + + context 'when parameter is valid' do + let(:params) { { shortcode: 'updated', image: image } } + + it 'succeeds in updating custom emoji' do + expect(flash[:notice]).to eq I18n.t('admin.custom_emojis.updated_msg') + expect(custom_emoji.reload).to have_attributes(shortcode: 'updated') + end + end + + context 'when parameter is invalid' do + let(:params) { { shortcode: 'u', image: image } } + + it 'fails to update custom emoji' do + expect(flash[:alert]).to eq I18n.t('admin.custom_emojis.update_failed_msg') + expect(custom_emoji.reload).to have_attributes(shortcode: 'test') + end + end + end + + describe 'POST #copy' do + subject { post :copy, params: { id: custom_emoji.id } } + + let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test') } + + it 'copies custom emoji' do + expect { subject }.to change { CustomEmoji.where(shortcode: 'test').count }.by(1) + expect(flash[:notice]).to eq I18n.t('admin.custom_emojis.copied_msg') + end + end + + describe 'POST #enable' do + let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test', disabled: true) } + + before { post :enable, params: { id: custom_emoji.id } } + + it 'enables custom emoji' do + expect(response).to redirect_to admin_custom_emojis_path + expect(custom_emoji.reload).to have_attributes(disabled: false) + end + end + + describe 'POST #disable' do + let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'test', disabled: false) } + + before { post :disable, params: { id: custom_emoji.id } } + + it 'enables custom emoji' do + expect(response).to redirect_to admin_custom_emojis_path + expect(custom_emoji.reload).to have_attributes(disabled: true) + end + end +end diff --git a/spec/controllers/admin/invites_controller_spec.rb b/spec/controllers/admin/invites_controller_spec.rb new file mode 100644 index 0000000000..e7d9954118 --- /dev/null +++ b/spec/controllers/admin/invites_controller_spec.rb @@ -0,0 +1,43 @@ +require 'rails_helper' + +describe Admin::InvitesController do + render_views + + let(:user) { Fabricate(:user, admin: true) } + + before do + sign_in user, scope: :user + end + + describe 'GET #index' do + subject { get :index, params: { available: true } } + + let!(:invite) { Fabricate(:invite) } + + it 'renders index page' do + expect(subject).to render_template :index + expect(assigns(:invites)).to include invite + end + end + + describe 'POST #create' do + subject { post :create, params: { invite: { max_uses: '10', expires_in: 1800 } } } + + it 'succeeds to create a invite' do + expect{ subject }.to change { Invite.count }.by(1) + expect(subject).to redirect_to admin_invites_path + expect(Invite.last).to have_attributes(user_id: user.id, max_uses: 10) + end + end + + describe 'DELETE #destroy' do + let!(:invite) { Fabricate(:invite, expires_at: nil) } + + subject { delete :destroy, params: { id: invite.id } } + + it 'expires invite' do + expect(subject).to redirect_to admin_invites_path + expect(invite.reload).to be_expired + end + end +end diff --git a/spec/controllers/admin/report_notes_controller_spec.rb b/spec/controllers/admin/report_notes_controller_spec.rb new file mode 100644 index 0000000000..2c32303fb3 --- /dev/null +++ b/spec/controllers/admin/report_notes_controller_spec.rb @@ -0,0 +1,91 @@ +require 'rails_helper' + +describe Admin::ReportNotesController do + render_views + + let(:user) { Fabricate(:user, admin: true) } + + before do + sign_in user, scope: :user + end + + describe 'POST #create' do + subject { post :create, params: params } + + let(:report) { Fabricate(:report, action_taken: action_taken, action_taken_by_account_id: account_id) } + + context 'when parameter is valid' do + + context 'when report is unsolved' do + let(:action_taken) { false } + let(:account_id) { nil } + + context 'when create_and_resolve flag is on' do + let(:params) { { report_note: { content: 'test content', report_id: report.id }, create_and_resolve: nil } } + + it 'creates a report note and resolves report' do + expect{ subject }.to change{ ReportNote.count }.by(1) + expect(report.reload).to be_action_taken + expect(subject).to redirect_to admin_reports_path + end + end + + context 'when create_and_resolve flag is false' do + let(:params) { { report_note: { content: 'test content', report_id: report.id } } } + + it 'creates a report note and does not resolve report' do + expect{ subject }.to change{ ReportNote.count }.by(1) + expect(report.reload).not_to be_action_taken + expect(subject).to redirect_to admin_report_path(report) + end + end + end + + context 'when report is resolved' do + let(:action_taken) { true } + let(:account_id) { user.account.id } + + context 'when create_and_unresolve flag is on' do + let(:params) { { report_note: { content: 'test content', report_id: report.id }, create_and_unresolve: nil } } + + it 'creates a report note and unresolves report' do + expect{ subject }.to change{ ReportNote.count }.by(1) + expect(report.reload).not_to be_action_taken + expect(subject).to redirect_to admin_report_path(report) + end + end + + context 'when create_and_unresolve flag is false' do + let(:params) { { report_note: { content: 'test content', report_id: report.id } } } + + it 'creates a report note and does not unresolve report' do + expect{ subject }.to change{ ReportNote.count }.by(1) + expect(report.reload).to be_action_taken + expect(subject).to redirect_to admin_report_path(report) + end + end + end + end + + context 'when parameter is invalid' do + let(:params) { { report_note: { content: '', report_id: report.id } } } + let(:action_taken) { false } + let(:account_id) { nil } + + it 'renders admin/reports/show' do + expect(subject).to render_template 'admin/reports/show' + end + end + end + + describe 'DELETE #destroy' do + subject { delete :destroy, params: { id: report_note.id } } + + let!(:report_note) { Fabricate(:report_note) } + + it 'deletes note' do + expect{ subject }.to change{ ReportNote.count }.by(-1) + expect(subject).to redirect_to admin_report_path(report_note.report) + end + end +end diff --git a/spec/controllers/admin/reported_statuses_controller_spec.rb b/spec/controllers/admin/reported_statuses_controller_spec.rb index 29957ed37e..7adbf36b9c 100644 --- a/spec/controllers/admin/reported_statuses_controller_spec.rb +++ b/spec/controllers/admin/reported_statuses_controller_spec.rb @@ -22,7 +22,7 @@ describe Admin::ReportedStatusesController do let(:sensitive) { true } let!(:media_attachment) { Fabricate(:media_attachment, status: status) } - context 'updates sensitive column to true' do + context 'when action is nsfw_on' do it 'updates sensitive column' do is_expected.to change { status.reload.sensitive @@ -30,7 +30,7 @@ describe Admin::ReportedStatusesController do end end - context 'updates sensitive column to false' do + context 'when action is nsfw_off' do let(:action) { 'nsfw_off' } let(:sensitive) { false } @@ -41,35 +41,13 @@ describe Admin::ReportedStatusesController do end end - it 'redirects to report page' do - subject.call - expect(response).to redirect_to(admin_report_path(report)) - end - end + context 'when action is delete' do + let(:action) { 'delete' } - describe 'PATCH #update' do - subject do - -> { patch :update, params: { report_id: report, id: status, status: { sensitive: sensitive } } } - end - - let(:status) { Fabricate(:status, sensitive: !sensitive) } - let(:sensitive) { true } - - context 'updates sensitive column to true' do - it 'updates sensitive column' do - is_expected.to change { - status.reload.sensitive - }.from(false).to(true) - end - end - - context 'updates sensitive column to false' do - let(:sensitive) { false } - - it 'updates sensitive column' do - is_expected.to change { - status.reload.sensitive - }.from(true).to(false) + it 'removes a status' do + allow(RemovalWorker).to receive(:perform_async) + subject.call + expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first) end end @@ -78,15 +56,4 @@ describe Admin::ReportedStatusesController do expect(response).to redirect_to(admin_report_path(report)) end end - - describe 'DELETE #destroy' do - it 'removes a status' do - allow(RemovalWorker).to receive(:perform_async) - - delete :destroy, params: { report_id: report, id: status } - expect(response).to have_http_status(200) - expect(RemovalWorker). - to have_received(:perform_async).with(status.id) - end - end end diff --git a/spec/controllers/admin/roles_controller_spec.rb b/spec/controllers/admin/roles_controller_spec.rb new file mode 100644 index 0000000000..8e0de73cbd --- /dev/null +++ b/spec/controllers/admin/roles_controller_spec.rb @@ -0,0 +1,33 @@ +require 'rails_helper' + +describe Admin::RolesController do + render_views + + let(:admin) { Fabricate(:user, admin: true) } + + before do + sign_in admin, scope: :user + end + + describe 'POST #promote' do + subject { post :promote, params: { account_id: user.account_id } } + + let(:user) { Fabricate(:user, moderator: false, admin: false) } + + it 'promotes user' do + expect(subject).to redirect_to admin_account_path(user.account_id) + expect(user.reload).to be_moderator + end + end + + describe 'POST #demote' do + subject { post :demote, params: { account_id: user.account_id } } + + let(:user) { Fabricate(:user, moderator: true, admin: false) } + + it 'demotes user' do + expect(subject).to redirect_to admin_account_path(user.account_id) + expect(user.reload).not_to be_moderator + end + end +end diff --git a/spec/controllers/admin/statuses_controller_spec.rb b/spec/controllers/admin/statuses_controller_spec.rb index cbaf397865..6afcc14425 100644 --- a/spec/controllers/admin/statuses_controller_spec.rb +++ b/spec/controllers/admin/statuses_controller_spec.rb @@ -34,13 +34,13 @@ describe Admin::StatusesController do describe 'POST #create' do subject do - -> { post :create, params: { account_id: account.id, form_status_batch: { action: action, status_ids: status_ids } } } + -> { post :create, params: { :account_id => account.id, action => '', :form_status_batch => { status_ids: status_ids } } } end let(:action) { 'nsfw_on' } let(:status_ids) { [media_attached_status.id] } - context 'updates sensitive column to true' do + context 'when action is nsfw_on' do it 'updates sensitive column' do is_expected.to change { media_attached_status.reload.sensitive @@ -48,7 +48,7 @@ describe Admin::StatusesController do end end - context 'updates sensitive column to false' do + context 'when action is nsfw_off' do let(:action) { 'nsfw_off' } let(:sensitive) { false } @@ -59,32 +59,13 @@ describe Admin::StatusesController do end end - it 'redirects to account statuses page' do - subject.call - expect(response).to redirect_to(admin_account_statuses_path(account.id)) - end - end + context 'when action is delete' do + let(:action) { 'delete' } - describe 'PATCH #update' do - subject do - -> { patch :update, params: { account_id: account.id, id: media_attached_status, status: { sensitive: sensitive } } } - end - - context 'updates sensitive column to true' do - it 'updates sensitive column' do - is_expected.to change { - media_attached_status.reload.sensitive - }.from(false).to(true) - end - end - - context 'updates sensitive column to false' do - let(:sensitive) { false } - - it 'updates sensitive column' do - is_expected.to change { - media_attached_status.reload.sensitive - }.from(true).to(false) + it 'removes a status' do + allow(RemovalWorker).to receive(:perform_async) + subject.call + expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first) end end @@ -93,15 +74,4 @@ describe Admin::StatusesController do expect(response).to redirect_to(admin_account_statuses_path(account.id)) end end - - describe 'DELETE #destroy' do - it 'removes a status' do - allow(RemovalWorker).to receive(:perform_async) - - delete :destroy, params: { account_id: account.id, id: status } - expect(response).to have_http_status(200) - expect(RemovalWorker). - to have_received(:perform_async).with(status.id) - end - end end diff --git a/spec/controllers/api/v1/push/subscriptions_controller_spec.rb b/spec/controllers/api/v1/push/subscriptions_controller_spec.rb new file mode 100644 index 0000000000..01146294f8 --- /dev/null +++ b/spec/controllers/api/v1/push/subscriptions_controller_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Push::SubscriptionsController do + render_views + + let(:user) { Fabricate(:user) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'push') } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + let(:create_payload) do + { + subscription: { + endpoint: 'https://fcm.googleapis.com/fcm/send/fiuH06a27qE:APA91bHnSiGcLwdaxdyqVXNDR9w1NlztsHb6lyt5WDKOC_Z_Q8BlFxQoR8tWFSXUIDdkyw0EdvxTu63iqamSaqVSevW5LfoFwojws8XYDXv_NRRLH6vo2CdgiN4jgHv5VLt2A8ah6lUX', + keys: { + p256dh: 'BEm_a0bdPDhf0SOsrnB2-ategf1hHoCnpXgQsFj5JCkcoMrMt2WHoPfEYOYPzOIs9mZE8ZUaD7VA5vouy0kEkr8=', + auth: 'eH_C8rq2raXqlcBVDa1gLg==', + }, + } + }.with_indifferent_access + end + + let(:alerts_payload) do + { + data: { + alerts: { + follow: true, + favourite: false, + reblog: true, + mention: false, + } + } + }.with_indifferent_access + end + + describe 'POST #create' do + it 'saves push subscriptions' do + post :create, params: create_payload + + push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint]) + + expect(push_subscription.endpoint).to eq(create_payload[:subscription][:endpoint]) + expect(push_subscription.key_p256dh).to eq(create_payload[:subscription][:keys][:p256dh]) + expect(push_subscription.key_auth).to eq(create_payload[:subscription][:keys][:auth]) + expect(push_subscription.user_id).to eq user.id + expect(push_subscription.access_token_id).to eq token.id + end + + it 'replaces old subscription on repeat calls' do + post :create, params: create_payload + post :create, params: create_payload + + expect(Web::PushSubscription.where(endpoint: create_payload[:subscription][:endpoint]).count).to eq 1 + end + end + + describe 'PUT #update' do + it 'changes alert settings' do + post :create, params: create_payload + put :update, params: alerts_payload + + push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint]) + + expect(push_subscription.data.dig('alerts', 'follow')).to eq(alerts_payload[:data][:alerts][:follow].to_s) + expect(push_subscription.data.dig('alerts', 'favourite')).to eq(alerts_payload[:data][:alerts][:favourite].to_s) + expect(push_subscription.data.dig('alerts', 'reblog')).to eq(alerts_payload[:data][:alerts][:reblog].to_s) + expect(push_subscription.data.dig('alerts', 'mention')).to eq(alerts_payload[:data][:alerts][:mention].to_s) + end + end + + describe 'DELETE #destroy' do + it 'removes the subscription' do + post :create, params: create_payload + delete :destroy + + expect(Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])).to be_nil + end + end +end diff --git a/spec/controllers/api/web/push_subscriptions_controller_spec.rb b/spec/controllers/api/web/push_subscriptions_controller_spec.rb index bbf94c5c66..381cdeab94 100644 --- a/spec/controllers/api/web/push_subscriptions_controller_spec.rb +++ b/spec/controllers/api/web/push_subscriptions_controller_spec.rb @@ -59,10 +59,10 @@ describe Api::Web::PushSubscriptionsController do push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint]) - expect(push_subscription.data['follow']).to eq(alerts_payload[:data][:follow]) - expect(push_subscription.data['favourite']).to eq(alerts_payload[:data][:favourite]) - expect(push_subscription.data['reblog']).to eq(alerts_payload[:data][:reblog]) - expect(push_subscription.data['mention']).to eq(alerts_payload[:data][:mention]) + expect(push_subscription.data['alerts']['follow']).to eq(alerts_payload[:data][:alerts][:follow].to_s) + expect(push_subscription.data['alerts']['favourite']).to eq(alerts_payload[:data][:alerts][:favourite].to_s) + expect(push_subscription.data['alerts']['reblog']).to eq(alerts_payload[:data][:alerts][:reblog].to_s) + expect(push_subscription.data['alerts']['mention']).to eq(alerts_payload[:data][:alerts][:mention].to_s) end end end @@ -81,10 +81,10 @@ describe Api::Web::PushSubscriptionsController do push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint]) - expect(push_subscription.data['follow']).to eq(alerts_payload[:data][:follow]) - expect(push_subscription.data['favourite']).to eq(alerts_payload[:data][:favourite]) - expect(push_subscription.data['reblog']).to eq(alerts_payload[:data][:reblog]) - expect(push_subscription.data['mention']).to eq(alerts_payload[:data][:mention]) + expect(push_subscription.data['alerts']['follow']).to eq(alerts_payload[:data][:alerts][:follow].to_s) + expect(push_subscription.data['alerts']['favourite']).to eq(alerts_payload[:data][:alerts][:favourite].to_s) + expect(push_subscription.data['alerts']['reblog']).to eq(alerts_payload[:data][:alerts][:reblog].to_s) + expect(push_subscription.data['alerts']['mention']).to eq(alerts_payload[:data][:alerts][:mention].to_s) end end end diff --git a/spec/controllers/emojis_controller_spec.rb b/spec/controllers/emojis_controller_spec.rb new file mode 100644 index 0000000000..68bae256d7 --- /dev/null +++ b/spec/controllers/emojis_controller_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +describe EmojisController do + render_views + + let(:emoji) { Fabricate(:custom_emoji) } + + describe 'GET #show' do + subject(:responce) { get :show, params: { id: emoji.id, format: :json } } + subject(:body) { JSON.parse(response.body, symbolize_names: true) } + + it 'returns the right response' do + expect(responce).to have_http_status 200 + expect(body[:name]).to eq ':coolcat:' + end + end +end diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb new file mode 100644 index 0000000000..9f5ab67c30 --- /dev/null +++ b/spec/controllers/invites_controller_spec.rb @@ -0,0 +1,72 @@ +require 'rails_helper' + +describe InvitesController do + render_views + + before do + sign_in user + end + + around do |example| + min_invite_role = Setting.min_invite_role + example.run + Setting.min_invite_role = min_invite_role + end + + describe 'GET #index' do + subject { get :index } + + let(:user) { Fabricate(:user, moderator: false, admin: false) } + let!(:invite) { Fabricate(:invite, user: user) } + + context 'when user is a staff' do + it 'renders index page' do + Setting.min_invite_role = 'user' + expect(subject).to render_template :index + expect(assigns(:invites)).to include invite + expect(assigns(:invites).count).to eq 1 + end + end + + context 'when user is not a staff' do + it 'returns 403' do + Setting.min_invite_role = 'modelator' + expect(subject).to have_http_status 403 + end + end + end + + describe 'POST #create' do + subject { post :create, params: { invite: { max_uses: '10', expires_in: 1800 } } } + + context 'when user is an admin' do + let(:user) { Fabricate(:user, moderator: false, admin: true) } + + it 'succeeds to create a invite' do + expect{ subject }.to change { Invite.count }.by(1) + expect(subject).to redirect_to invites_path + expect(Invite.last).to have_attributes(user_id: user.id, max_uses: 10) + end + end + + context 'when user is not an admin' do + let(:user) { Fabricate(:user, moderator: true, admin: false) } + + it 'returns 403' do + expect(subject).to have_http_status 403 + end + end + end + + describe 'DELETE #create' do + subject { delete :destroy, params: { id: invite.id } } + + let!(:invite) { Fabricate(:invite, user: user, expires_at: nil) } + let(:user) { Fabricate(:user, moderator: false, admin: true) } + + it 'expires invite' do + expect(subject).to redirect_to invites_path + expect(invite.reload).to be_expired + end + end +end diff --git a/spec/controllers/oauth/authorized_applications_controller_spec.rb b/spec/controllers/oauth/authorized_applications_controller_spec.rb index f967b507f0..901e538e95 100644 --- a/spec/controllers/oauth/authorized_applications_controller_spec.rb +++ b/spec/controllers/oauth/authorized_applications_controller_spec.rb @@ -39,4 +39,24 @@ describe Oauth::AuthorizedApplicationsController do include_examples 'stores location for user' end end + + describe 'DELETE #destroy' do + let!(:user) { Fabricate(:user) } + let!(:application) { Fabricate(:application) } + let!(:access_token) { Fabricate(:accessible_access_token, application: application, resource_owner_id: user.id) } + let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) } + + before do + sign_in user, scope: :user + post :destroy, params: { id: application.id } + end + + it 'revokes access tokens for the application' do + expect(Doorkeeper::AccessToken.where(application: application).first.revoked_at).to_not be_nil + end + + it 'removes subscriptions for the application\'s access tokens' do + expect(Web::PushSubscription.where(user: user).count).to eq 0 + end + end end diff --git a/spec/controllers/oauth/tokens_controller_spec.rb b/spec/controllers/oauth/tokens_controller_spec.rb new file mode 100644 index 0000000000..ba8e367a68 --- /dev/null +++ b/spec/controllers/oauth/tokens_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Oauth::TokensController, type: :controller do + describe 'POST #revoke' do + let!(:user) { Fabricate(:user) } + let!(:access_token) { Fabricate(:accessible_access_token, resource_owner_id: user.id) } + let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) } + + before do + post :revoke, params: { token: access_token.token } + end + + it 'revokes the token' do + expect(access_token.reload.revoked_at).to_not be_nil + end + + it 'removes web push subscription for token' do + expect(Web::PushSubscription.where(access_token: access_token).count).to eq 0 + end + end +end diff --git a/spec/fabricators/report_note_fabricator.rb b/spec/fabricators/report_note_fabricator.rb new file mode 100644 index 0000000000..e139efffbc --- /dev/null +++ b/spec/fabricators/report_note_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator(:report_note) do + report + account { Fabricate(:account) } + content "Test Content" +end diff --git a/spec/fabricators/web_push_subscription_fabricator.rb b/spec/fabricators/web_push_subscription_fabricator.rb index 72d11b77cc..97f90675de 100644 --- a/spec/fabricators/web_push_subscription_fabricator.rb +++ b/spec/fabricators/web_push_subscription_fabricator.rb @@ -1,4 +1,4 @@ -Fabricator(:web_push_subscription) do +Fabricator(:web_push_subscription, from: Web::PushSubscription) do endpoint Faker::Internet.url key_p256dh Faker::Internet.password key_auth Faker::Internet.password diff --git a/spec/fabricators/web_setting_fabricator.rb b/spec/fabricators/web_setting_fabricator.rb index e5136829b9..369b86bc18 100644 --- a/spec/fabricators/web_setting_fabricator.rb +++ b/spec/fabricators/web_setting_fabricator.rb @@ -1,3 +1,2 @@ -Fabricator('Web::Setting') do - +Fabricator(:web_setting, from: Web::Setting) do end diff --git a/spec/lib/activitypub/activity/add_spec.rb b/spec/lib/activitypub/activity/add_spec.rb index 3ebab4e373..16db71c880 100644 --- a/spec/lib/activitypub/activity/add_spec.rb +++ b/spec/lib/activitypub/activity/add_spec.rb @@ -18,12 +18,31 @@ RSpec.describe ActivityPub::Activity::Add do describe '#perform' do subject { described_class.new(json, sender) } - before do + it 'creates a pin' do subject.perform + expect(sender.pinned?(status)).to be true end - it 'creates a pin' do - expect(sender.pinned?(status)).to be true + context 'when status was not known before' do + let(:json) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Add', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: 'https://example.com/unknown', + target: sender.featured_collection_url, + }.with_indifferent_access + end + + before do + stub_request(:get, 'https://example.com/unknown').to_return(status: 410) + end + + it 'fetches the status' do + subject.perform + expect(a_request(:get, 'https://example.com/unknown')).to have_been_made.at_least_once + end end end end diff --git a/spec/models/follow_request_spec.rb b/spec/models/follow_request_spec.rb index 7bc93a2aae..4b824c0db6 100644 --- a/spec/models/follow_request_spec.rb +++ b/spec/models/follow_request_spec.rb @@ -7,7 +7,7 @@ RSpec.describe FollowRequest, type: :model do let(:target_account) { Fabricate(:account) } it 'calls Account#follow!, MergeWorker.perform_async, and #destroy!' do - expect(account).to receive(:follow!).with(target_account, reblogs: true) + expect(account).to receive(:follow!).with(target_account, reblogs: true, uri: follow_request.uri) expect(MergeWorker).to receive(:perform_async).with(target_account.id, account.id) expect(follow_request).to receive(:destroy!) follow_request.authorize! diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index 1f5a03877f..03d1a94de1 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -154,7 +154,7 @@ RSpec.describe Status, type: :model do describe '#target' do it 'returns nil if the status is self-contained' do - expect(subject.target).to be_nil + expect(subject.target).to be_nil end it 'returns nil if the status is a reply' do @@ -370,24 +370,25 @@ RSpec.describe Status, type: :model do expect(@results).to_not include(@followed_public_status) end - it 'includes direct statuses mentioning recipient from followed' do - Fabricate(:mention, account: account, status: @followed_direct_status) - expect(@results).to include(@followed_direct_status) - end - it 'does not include direct statuses not mentioning recipient from followed' do expect(@results).to_not include(@followed_direct_status) end - it 'includes direct statuses mentioning recipient from non-followed' do - Fabricate(:mention, account: account, status: @not_followed_direct_status) - expect(@results).to include(@not_followed_direct_status) - end - it 'does not include direct statuses not mentioning recipient from non-followed' do expect(@results).to_not include(@not_followed_direct_status) end + it 'includes direct statuses mentioning recipient from followed' do + Fabricate(:mention, account: account, status: @followed_direct_status) + results2 = Status.as_direct_timeline(account) + expect(results2).to include(@followed_direct_status) + end + + it 'includes direct statuses mentioning recipient from non-followed' do + Fabricate(:mention, account: account, status: @not_followed_direct_status) + results2 = Status.as_direct_timeline(account) + expect(results2).to include(@not_followed_direct_status) + end end describe '.as_public_timeline' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 760214dede..cc8d88cc85 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -75,7 +75,7 @@ RSpec.describe User, type: :model do describe 'inactive' do it 'returns a relation of inactive users' do specified = Fabricate(:user, current_sign_in_at: 15.days.ago) - Fabricate(:user, current_sign_in_at: 13.days.ago) + Fabricate(:user, current_sign_in_at: 6.days.ago) expect(User.inactive).to match_array([specified]) end diff --git a/spec/models/web/push_subscription_spec.rb b/spec/models/web/push_subscription_spec.rb index 574da55ac2..c6665611c6 100644 --- a/spec/models/web/push_subscription_spec.rb +++ b/spec/models/web/push_subscription_spec.rb @@ -2,20 +2,8 @@ require 'rails_helper' RSpec.describe Web::PushSubscription, type: :model do let(:alerts) { { mention: true, reblog: false, follow: true, follow_request: false, favourite: true } } - let(:payload_no_alerts) { Web::PushSubscription.new(id: 1, endpoint: 'a', key_p256dh: 'c', key_auth: 'd').as_payload } - let(:payload_alerts) { Web::PushSubscription.new(id: 1, endpoint: 'a', key_p256dh: 'c', key_auth: 'd', data: { alerts: alerts }).as_payload } let(:push_subscription) { Web::PushSubscription.new(data: { alerts: alerts }) } - describe '#as_payload' do - it 'only returns id and endpoint' do - expect(payload_no_alerts.keys).to eq [:id, :endpoint] - end - - it 'returns alerts if set' do - expect(payload_alerts.keys).to eq [:id, :endpoint, :alerts] - end - end - describe '#pushable?' do it 'obeys alert settings' do expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Mention'))).to eq true diff --git a/spec/services/resolve_account_service_spec.rb b/spec/services/resolve_account_service_spec.rb index f4c810f758..dd7561587d 100644 --- a/spec/services/resolve_account_service_spec.rb +++ b/spec/services/resolve_account_service_spec.rb @@ -116,6 +116,7 @@ RSpec.describe ResolveAccountService, type: :service do expect(account.activitypub?).to eq true expect(account.domain).to eq 'ap.example.com' expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox' + expect(account.actor_type).to eq 'Person' end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9030329373..0cd1f91d02 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,20 +1,17 @@ -#require 'rspec/retry' -require 'simplecov' - GC.disable -SimpleCov.start 'rails' do - add_group 'Services', 'app/services' - add_group 'Presenters', 'app/presenters' - add_group 'Validators', 'app/validators' +if ENV['DISABLE_SIMPLECOV'] != 'true' + require 'simplecov' + SimpleCov.start 'rails' do + add_group 'Services', 'app/services' + add_group 'Presenters', 'app/presenters' + add_group 'Validators', 'app/validators' + end end gc_counter = -1 RSpec.configure do |config| - #config.verbose_retry = true - #config.display_try_failure_messages = true - config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true end @@ -29,10 +26,6 @@ RSpec.configure do |config| end end - #config.around :each do |ex| - # ex.run_with_retry retry: 3 - #end - config.before :suite do Chewy.strategy(:bypass) end diff --git a/streaming/index.js b/streaming/index.js index 48bab8078e..4eaf668658 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -241,7 +241,9 @@ const startWorker = (workerId) => { const PUBLIC_STREAMS = [ 'public', + 'public:media', 'public:local', + 'public:local:media', 'hashtag', 'hashtag:local', ]; @@ -459,11 +461,17 @@ const startWorker = (workerId) => { }); app.get('/api/v1/streaming/public', (req, res) => { - streamFrom('timeline:public', req, streamToHttp(req, res), streamHttpEnd(req), true); + const onlyMedia = req.query.only_media === '1' || req.query.only_media === 'true'; + const channel = onlyMedia ? 'timeline:public:media' : 'timeline:public'; + + streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req), true); }); app.get('/api/v1/streaming/public/local', (req, res) => { - streamFrom('timeline:public:local', req, streamToHttp(req, res), streamHttpEnd(req), true); + const onlyMedia = req.query.only_media === '1' || req.query.only_media === 'true'; + const channel = onlyMedia ? 'timeline:public:local:media' : 'timeline:public:local'; + + streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req), true); }); app.get('/api/v1/streaming/direct', (req, res) => { @@ -521,6 +529,12 @@ const startWorker = (workerId) => { case 'public:local': streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; + case 'public:media': + streamFrom('timeline:public:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); + break; + case 'public:local:media': + streamFrom('timeline:public:local:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); + break; case 'direct': streamFrom(`timeline:direct:${req.accountId}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; diff --git a/yarn.lock b/yarn.lock index 0513845eb8..4a9d182e7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2605,6 +2605,10 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +exif-js@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/exif-js/-/exif-js-2.3.0.tgz#9d10819bf571f873813e7640241255ab9ce1a814" + expand-brackets@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" @@ -4387,6 +4391,10 @@ lodash.restparam@^3.0.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + lodash.tail@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" @@ -6142,6 +6150,12 @@ react-router@^4.2.0: prop-types "^15.5.4" warning "^3.0.0" +react-sparklines@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/react-sparklines/-/react-sparklines-1.7.0.tgz#9b1d97e8c8610095eeb2ad658d2e1fcf91f91a60" + dependencies: + prop-types "^15.5.10" + react-swipeable-views-core@^0.12.11: version "0.12.11" resolved "https://registry.yarnpkg.com/react-swipeable-views-core/-/react-swipeable-views-core-0.12.11.tgz#3cf2b4daffbb36f9d69bd19bf5b2d5370b6b2c1b" @@ -7269,6 +7283,12 @@ tough-cookie@^2.3.2, tough-cookie@~2.3.0, tough-cookie@~2.3.3: dependencies: punycode "^1.4.1" +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + dependencies: + punycode "^2.1.0" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -7506,7 +7526,7 @@ webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" -webidl-conversions@^4.0.0: +webidl-conversions@^4.0.0, webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" @@ -7649,6 +7669,14 @@ whatwg-url@^4.3.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" +whatwg-url@^6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.4.1.tgz#fdb94b440fd4ad836202c16e9737d511f012fd67" + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + whet.extend@~0.9.9: version "0.9.9" resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1"
<a>
<em>
A descrizzione stesa ùn hè micca stata riempiuta.
Any of the information we collect from you may be used in the following ways:
We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information. Among other things, your browser session, as well as the traffic between your applications and the API, are secured with SSL, and your password is hashed using a strong one-way algorithm. You may enable two-factor authentication to further secure access to your account.
We will make a good faith effort to:
You can request and download an archive of your content, including your posts, media attachments, profile picture, and header image.
You may irreversibly delete your account at any time.
Yes. Cookies are small files that a site or its service provider transfers to your computer’s hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account.
We use cookies to understand and save your preferences for future visits.
We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety.
Your public content may be downloaded by other servers in the network. Your public and followers-only posts are delivered to the servers where your followers reside, and direct messages are delivered to the servers of the recipients, in so far as those followers or recipients reside on a different server than this.
When you authorize an application to use your account, depending on the scope of permissions you approve, it may access your public profile information, your following list, your followers, your lists, all your posts, and your favourites. Applications can never access your e-mail address or password.
Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA (Children’s Online Privacy Protection Act) do not use this site.
If we decide to change our privacy policy, we will post those changes on this page.
This document is CC-BY-SA. It was last updated March 7, 2018.
Originally adapted from the Discourse privacy policy.
Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site.
If this server is in the EU or the EEA: Our site, products and services are all directed to people who are at least 16 years old. If you are under the age of 16, per the requirements of the GDPR (General Data Protection Regulation) do not use this site.
If this server is in the USA: Our site, products and services are all directed to people who are at least 13 years old. If you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site.
Law requirements can be different if this server is in another jurisdiction.
La descrizione estesa non è ancora stata preparata.
はい。クッキーは (あなたが許可した場合に) WebサイトやサービスがWebブラウザーを介してコンピューターに保存する小さなファイルです。使用することで Web サイトがブラウザーを識別し、登録済みのアカウントがある場合関連付けます。
はい。クッキーは (あなたが許可した場合に) WebサイトやサービスがWebブラウザーを介してコンピューターに保存する小さなファイルです。使用することでWebサイトがブラウザーを識別し、登録済みのアカウントがある場合関連付けます。
私たちはクッキーを将来の訪問のために設定を保存し呼び出す用途に使用します。
私たちは個人を特定できる情報を外部へ販売・取引・その他方法で渡すことはありません。これには当サイトの運営・業務遂行・サービス提供を行ううえで補助する信頼できる第三者をこの機密情報の保護に同意するかぎり含みません。法令の遵守やサイトポリシーの施行、権利・財産・安全の保護に適切と判断した場合、あなたの情報を公開することがあります。
あなたの公開情報はネットワーク上の他のサーバーにダウンロードされることがあります。相手が異なるサーバーに所属する場合、「公開」と「非公開」投稿はフォロワーの所属するサーバーに配信され、「ダイレクト」投稿は受信者の所属するサーバーに配信されます。
あなたの公開情報はネットワーク上の他のサーバーにダウンロードされることがあります。相手が異なるサーバーに所属する場合、「公開」と「フォロワー限定」投稿はフォロワーの所属するサーバーに配信され、「ダイレクト」投稿は受信者の所属するサーバーに配信されます。
あなたがアカウントの使用をアプリケーションに許可すると、承認した権限の範囲内で公開プロフィール情報・フォローリスト・フォロワー・リスト・すべての投稿・お気に入り登録にアクセスできます。アプリケーションはメールアドレスやパスワードに決してアクセスできません。
当サイト・製品・サービスは13歳以上の人を対象としています。サーバーが米国にあり、あなたが13歳未満の場合、COPPA (Children's Online Privacy Protection Act - 児童オンラインプライバシー保護法) により当サイトを使用できません。
サーバーがEUまたはEEA圏内にある場合: 当サイト・製品・サービスは16歳以上の人を対象としています。あなたが16歳未満の場合、GDPR (General Data Protection Regulation - EU一般データ保護規則) により当サイトを使用できません。
サーバーが米国にある場合: 当サイト・製品・サービスは13歳以上の人を対象としています。あなたが13歳未満の場合、COPPA (Children's Online Privacy Protection Act - 児童オンラインプライバシー保護法) により当サイトを使用できません。
サーバーが別の管轄区域にある場合、法的要件は異なることがあります。
De uitgebreide omschrijving is nog niet ingevuld.
Toda informação que coletamos de você pode ser usada das seguintes maneiras:
Nós implementamos diversas medidas de segurança para manter a segurança das suas informações pessoais quando você as acessa ou as envia. Entre outras coisas, sua sessão do navegador, bem como o tráfego entre as aplicações e a API são asseguradas usando SSL e a sua senha é guardada usando um algoritmo forte de encriptação de mão única. Você pode ativar autenticação em dois fatores como forma de aumentar a segurança no acesso à sua conta.
Nós fazemos esforços substanciais para:
Você pode pedir e fazer o download de um arquivo de todo o conteúdo da sua conta, incluindo as suas mensagens, suas mídias anexadas, imagem de perfil e imagem de topo.
Você pode remover irreversivelmente a sua conta a qualquer momento.
Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account.
Sim. Cookies são pequenos arquivos que um site ou serviço transfere ao seu disco rígido do seu computador através do seu navegador da web (se você permitir). Esses cookies permitem ao site conhecer seu navegador e, se você tiver uma conta registrada, associá-lo a sua conta.
Nós usamos cookies para compreender e salvar suas preferências para visitas futuras.
Nós não vendemos, trocamos ou transferimos de qualquer maneira informação que pode lhe identificar à terceiros. Isso não inclui terceiros que podemos nos auxiliam a operar o nosso site, realizar nossos negócios ou lhe prestar serviços, contanto que esses terceiros se comprometam a manter essa informação confidencial. Nós podemos também divulgar informação quando acreditamos que é apropriado para obedecer a lei, para fazer cumprir nossas políticas ou proteger nossos direitos, propriedade ou segurança ou o direito, propriedade e segurança de outrem.
Seu conteúdo público pode ser descarregado por outros servidores na rede. Suas mensagens públicas e somente para seus seguidores são entregues aos servidores onde seus seguidores resides e as suas mensagens diretas são entregues ao servidor dos usuários mencionados nelas, contanto que esses seguidores ou usuários residam em um servidor diferente deste.
Quando você autoriza uma aplicação a usar sua conta, dependendo do escopo de permissões que você aprovar, a aplicação pode acessar sua informação pública, a lista de usuários que você segue, seus seguidores, suas listas, suas mensagens e suas mensagens favoritas. Aplicações nunca podem acessar o seu endereço de e-mail ou senha.
Nosso site, produto e serviços são direcionados à pessoas que tem ao menos 13 anos de idade. Se esse servidor está hospedado nos EUA e você tem menos de 13 anos, de acordo com os requerimentos da COPPA (Children's Online Privacy Protection Act) não use este site.
Se decidirmos mudar nossa política de privacidade, nós iremos disponibilizar as mudanças nesta página.
Este documento é CC-BY-SA. Ele foi atualizado pela última vez em 7 de março de 2018.
Adaptado originalmente a partir da política de privacidade Discourse.
Razširjen opis še ni bil nastavljen.