diff options
author | kibigo! <marrus-sh@users.noreply.github.com> | 2017-07-12 02:03:17 -0700 |
---|---|---|
committer | kibigo! <marrus-sh@users.noreply.github.com> | 2017-07-12 02:03:17 -0700 |
commit | 79d898ae0ad8c0e66bd63ec3e0904e9e5e7894e8 (patch) | |
tree | ee8d832ed2f11e9afe62daf0e586a86004eb8d98 | |
parent | bcf7ee48e94cd2e4d2de28e8854e7f0e2b5cad1f (diff) | |
parent | 056b5ed72f6d980bceeb49eb249b8365fe8fce66 (diff) |
Merge upstream!! #64 <3 <3
340 files changed, 4979 insertions, 2320 deletions
diff --git a/.babelrc b/.babelrc index 292d52e27..19968964e 100644 --- a/.babelrc +++ b/.babelrc @@ -44,6 +44,7 @@ ] } ], + "transform-react-inline-elements", [ "transform-runtime", { diff --git a/.env.nanobox b/.env.nanobox index 73abefdc6..7920c47b9 100644 --- a/.env.nanobox +++ b/.env.nanobox @@ -69,7 +69,7 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io # PAPERCLIP_ROOT_URL=/system # Optional asset host for multi-server setups -# CDN_HOST=assets.example.com +# CDN_HOST=https://assets.example.com # S3 (optional) # S3_ENABLED=true diff --git a/.travis.yml b/.travis.yml index 4bb332666..4d4dc0893 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,6 +32,7 @@ addons: - g++-6 - libprotobuf-dev - protobuf-compiler + - libicu-dev rvm: - 2.3.4 diff --git a/Aptfile b/Aptfile index 0456343ef..3af0956e3 100644 --- a/Aptfile +++ b/Aptfile @@ -3,3 +3,4 @@ libprotobuf-dev ffmpeg libxdamage1 libxfixes3 +libicu-dev diff --git a/Dockerfile b/Dockerfile index 7033cddd4..97a691393 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,7 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit ffmpeg \ file \ git \ + icu-dev \ imagemagick@edge \ libpq \ libxml2 \ diff --git a/Gemfile b/Gemfile index aecd82702..b52685cba 100644 --- a/Gemfile +++ b/Gemfile @@ -18,9 +18,11 @@ gem 'aws-sdk', '~> 2.9' gem 'paperclip', '~> 5.1' gem 'paperclip-av-transcoder', '~> 0.6' +gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.5' gem 'bootsnap' gem 'browser' +gem 'charlock_holmes', '~> 0.7.3' gem 'cld3', '~> 3.1' gem 'devise', '~> 4.2' gem 'devise-two-factor', '~> 3.0' @@ -35,6 +37,7 @@ gem 'http_accept_language', '~> 2.1' gem 'httplog', '~> 0.99' gem 'kaminari', '~> 1.0' gem 'link_header', '~> 0.0' +gem 'mime-types', '~> 3.1' gem 'nokogiri', '~> 1.7' gem 'oj', '~> 3.0' gem 'ostatus2', '~> 2.0' diff --git a/Gemfile.lock b/Gemfile.lock index c19f31e01..ab430f4c3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -24,6 +24,11 @@ GEM erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) + active_model_serializers (0.10.6) + actionpack (>= 4.1, < 6) + activemodel (>= 4.1, < 6) + case_transform (>= 0.2) + jsonapi-renderer (>= 0.1.1.beta1, < 0.2) active_record_query_trace (1.5.4) activejob (5.1.2) activesupport (= 5.1.2) @@ -41,7 +46,7 @@ GEM tzinfo (~> 1.1) addressable (2.5.1) public_suffix (~> 2.0, >= 2.0.2) - airbrussh (1.2.0) + airbrussh (1.3.0) sshkit (>= 1.6.1, != 1.7.0) annotate (2.7.2) activerecord (>= 3.2, < 6.0) @@ -52,13 +57,13 @@ GEM encryptor (~> 3.0.0) av (0.9.0) cocaine (~> 0.5.3) - aws-sdk (2.9.37) - aws-sdk-resources (= 2.9.37) - aws-sdk-core (2.9.37) + aws-sdk (2.10.6) + aws-sdk-resources (= 2.10.6) + aws-sdk-core (2.10.6) aws-sigv4 (~> 1.0) jmespath (~> 1.0) - aws-sdk-resources (2.9.37) - aws-sdk-core (= 2.9.37) + aws-sdk-resources (2.10.6) + aws-sdk-core (= 2.10.6) aws-sigv4 (1.0.0) bcrypt (3.1.11) better_errors (2.1.1) @@ -67,7 +72,7 @@ GEM rack (>= 0.9.0) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - bootsnap (1.0.0) + bootsnap (1.1.1) msgpack (~> 1.0) brakeman (3.6.2) browser (2.4.0) @@ -78,7 +83,7 @@ GEM bundler-audit (0.5.0) bundler (~> 1.2) thor (~> 0.18) - capistrano (3.8.1) + capistrano (3.8.2) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) @@ -94,15 +99,18 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (2.14.2) + capybara (2.14.4) addressable mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) + case_transform (0.2) + activesupport + charlock_holmes (0.7.3) chunky_png (1.3.8) - cld3 (3.1.2) + cld3 (3.1.3) ffi (>= 1.1.0, < 1.10.0) climate_control (0.2.0) cocaine (0.5.8) @@ -142,9 +150,9 @@ GEM thread thread_safe encryptor (3.0.0) - erubi (1.6.0) + erubi (1.6.1) erubis (2.7.0) - et-orbi (1.0.4) + et-orbi (1.0.5) tzinfo execjs (2.7.0) fabrication (2.16.1) @@ -161,7 +169,7 @@ GEM addressable (~> 2.4) http (~> 2.0) nokogiri (~> 1.6) - hamlit (2.8.1) + hamlit (2.8.4) temple (>= 0.8.0) thor tilt @@ -182,9 +190,9 @@ GEM http-cookie (1.0.3) domain_name (~> 0.5) http-form_data (1.0.3) - http_accept_language (2.1.0) + http_accept_language (2.1.1) http_parser.rb (0.6.0) - httplog (0.99.3) + httplog (0.99.4) colorize rack i18n (0.8.4) @@ -200,6 +208,7 @@ GEM terminal-table (>= 1.5.1) jmespath (1.3.1) json (2.1.0) + jsonapi-renderer (0.1.2) kaminari (1.0.1) activesupport (>= 4.1.0) kaminari-actionview (= 1.0.1) @@ -249,8 +258,8 @@ GEM mini_portile2 (~> 2.2.0) nokogumbo (1.4.13) nokogiri - oj (3.1.0) - openssl (2.0.3) + oj (3.2.0) + openssl (2.0.4) orm_adapter (0.5.0) ostatus2 (2.0.1) addressable (~> 2.4) @@ -272,7 +281,7 @@ GEM parallel parser (2.4.0.0) ast (~> 2.2) - pg (0.20.0) + pg (0.21.0) pghero (1.7.0) activerecord pkg-config (1.2.3) @@ -374,7 +383,7 @@ GEM rspec-expectations (~> 3.6.0) rspec-mocks (~> 3.6.0) rspec-support (~> 3.6.0) - rspec-sidekiq (3.0.1) + rspec-sidekiq (3.0.3) rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) rspec-support (3.6.0) @@ -395,10 +404,10 @@ GEM nokogiri (>= 1.4.4) nokogumbo (~> 1.4.1) sass (3.4.24) - scss_lint (0.53.0) + scss_lint (0.54.0) rake (>= 0.9, < 13) sass (~> 3.4.20) - sidekiq (5.0.2) + sidekiq (5.0.3) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) rack-protection (>= 1.5.0) @@ -406,7 +415,7 @@ GEM sidekiq-bulk (0.1.1) activesupport sidekiq - sidekiq-scheduler (2.1.5) + sidekiq-scheduler (2.1.7) redis (~> 3) rufus-scheduler (~> 3.2) sidekiq (>= 3) @@ -443,7 +452,7 @@ GEM thread (0.2.2) thread_safe (0.3.6) tilt (2.0.7) - twitter-text (1.14.5) + twitter-text (1.14.6) unf (~> 0.1.0) tzinfo (1.2.3) thread_safe (~> 0.1) @@ -454,7 +463,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.4) - unicode-display_width (1.2.1) + unicode-display_width (1.3.0) uniform_notifier (1.10.0) warden (1.2.7) rack (>= 1.0) @@ -476,6 +485,7 @@ PLATFORMS ruby DEPENDENCIES + active_model_serializers (~> 0.10) active_record_query_trace (~> 1.5) addressable (~> 2.5) annotate (~> 2.7) @@ -492,6 +502,7 @@ DEPENDENCIES capistrano-rbenv (~> 2.1) capistrano-yarn (~> 2.0) capybara (~> 2.14) + charlock_holmes (~> 0.7.3) cld3 (~> 3.1) climate_control (~> 0.2) devise (~> 4.2) @@ -516,6 +527,7 @@ DEPENDENCIES link_header (~> 0.0) lograge (~> 0.5) microformats2 (~> 3.0) + mime-types (~> 3.1) nokogiri (~> 1.7) oj (~> 3.0) ostatus2 (~> 2.0) diff --git a/Vagrantfile b/Vagrantfile index 1f56fcfb3..cbe6623b3 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -37,6 +37,7 @@ sudo apt-get install \ yarn \ libprotobuf-dev \ libreadline-dev \ + libicu-dev \ -y # Install rvm diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 04e7ddacf..47690e81e 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -2,9 +2,12 @@ class AboutController < ApplicationController before_action :set_body_classes - before_action :set_instance_presenter, only: [:show, :more] + before_action :set_instance_presenter, only: [:show, :more, :terms] - def show; end + def show + serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) + @initial_state_json = serializable_resource.to_json + end def more; end @@ -15,6 +18,7 @@ class AboutController < ApplicationController def new_user User.new.tap(&:build_account) end + helper_method :new_user def set_instance_presenter @@ -24,4 +28,11 @@ class AboutController < ApplicationController def set_body_classes @body_classes = 'about-body' end + + def initial_state_params + { + settings: {}, + token: current_session&.token, + } + end end diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index ef2f8c4c2..7bceee2cd 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -22,8 +22,8 @@ module Admin end def redownload - @account.avatar = @account.avatar_remote_url - @account.header = @account.header_remote_url + @account.reset_avatar! + @account.reset_header! @account.save! redirect_to admin_account_path(@account.id) diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index fcd42c79c..5985d6282 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -8,13 +8,21 @@ module Admin site_title site_description site_extended_description + site_terms open_registrations closed_registrations_message + open_deletion + timeline_preview + ).freeze + + BOOLEAN_SETTINGS = %w( + open_registrations + open_deletion + timeline_preview ).freeze - BOOLEAN_SETTINGS = %w(open_registrations).freeze def edit - @settings = Setting.all_as_records + @admin_settings = Form::AdminSettings.new end def update @@ -23,19 +31,19 @@ module Admin setting.update(value: value_for_update(key, value)) end - flash[:notice] = 'Success!' + flash[:notice] = I18n.t('generic.changes_saved_msg') redirect_to edit_admin_settings_path end private def settings_params - params.permit(ADMIN_SETTINGS) + params.require(:form_admin_settings).permit(ADMIN_SETTINGS) end def value_for_update(key, value) if BOOLEAN_SETTINGS.include?(key) - value == 'true' + value == '1' else value end diff --git a/app/controllers/api/oembed_controller.rb b/app/controllers/api/oembed_controller.rb index 6e3e34d96..f8c87dd16 100644 --- a/app/controllers/api/oembed_controller.rb +++ b/app/controllers/api/oembed_controller.rb @@ -5,8 +5,7 @@ class Api::OEmbedController < Api::BaseController def show @stream_entry = find_stream_entry.stream_entry - @width = maxwidth_or_default - @height = maxheight_or_default + render json: @stream_entry, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default end private diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 1cf52ff10..073808532 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -6,13 +6,13 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController def show @account = current_account - render 'api/v1/accounts/show' + render json: @account, serializer: REST::CredentialAccountSerializer end def update current_account.update!(account_params) @account = current_account - render 'api/v1/accounts/show' + render json: @account, serializer: REST::CredentialAccountSerializer end private diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index 81aae56d3..80b0bef40 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -9,7 +9,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController def index @accounts = load_accounts - render 'api/v1/accounts/index' + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 63c6d54b2..55cffdf37 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -9,7 +9,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController def index @accounts = load_accounts - render 'api/v1/accounts/index' + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb index cb923ab91..a88cf2021 100644 --- a/app/controllers/api/v1/accounts/relationships_controller.rb +++ b/app/controllers/api/v1/accounts/relationships_controller.rb @@ -8,16 +8,15 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController def index @accounts = Account.where(id: account_ids).select('id') - @following = Account.following_map(account_ids, current_user.account_id) - @followed_by = Account.followed_by_map(account_ids, current_user.account_id) - @blocking = Account.blocking_map(account_ids, current_user.account_id) - @muting = Account.muting_map(account_ids, current_user.account_id) - @requested = Account.requested_map(account_ids, current_user.account_id) - @domain_blocking = Account.domain_blocking_map(account_ids, current_user.account_id) + render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships end private + def relationships + AccountRelationshipsPresenter.new(@accounts, current_user.account_id) + end + def account_ids @_account_ids ||= Array(params[:id]).map(&:to_i) end diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb index c4a8f97f2..2a5cac547 100644 --- a/app/controllers/api/v1/accounts/search_controller.rb +++ b/app/controllers/api/v1/accounts/search_controller.rb @@ -8,8 +8,7 @@ class Api::V1::Accounts::SearchController < Api::BaseController def show @accounts = account_search - - render 'api/v1/accounts/index' + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 504ed8c07..d9ae5c089 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -9,6 +9,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController def index @statuses = load_statuses + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private @@ -18,9 +19,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def load_statuses - cached_account_statuses.tap do |statuses| - set_maps(statuses) - end + cached_account_statuses end def cached_account_statuses diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 8fc0dd36f..f621aa245 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -8,49 +8,38 @@ class Api::V1::AccountsController < Api::BaseController respond_to :json - def show; end + def show + render json: @account, serializer: REST::AccountSerializer + end def follow FollowService.new.call(current_user.account, @account.acct) - set_relationship - render :relationship + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end def block BlockService.new.call(current_user.account, @account) - - @following = { @account.id => false } - @followed_by = { @account.id => false } - @blocking = { @account.id => true } - @requested = { @account.id => false } - @muting = { @account.id => current_account.muting?(@account.id) } - @domain_blocking = { @account.id => current_account.domain_blocking?(@account.domain) } - - render :relationship + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end def mute MuteService.new.call(current_user.account, @account) - set_relationship - render :relationship + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end def unfollow UnfollowService.new.call(current_user.account, @account) - set_relationship - render :relationship + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end def unblock UnblockService.new.call(current_user.account, @account) - set_relationship - render :relationship + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end def unmute UnmuteService.new.call(current_user.account, @account) - set_relationship - render :relationship + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end private @@ -59,12 +48,7 @@ class Api::V1::AccountsController < Api::BaseController @account = Account.find(params[:id]) end - def set_relationship - @following = Account.following_map([@account.id], current_user.account_id) - @followed_by = Account.followed_by_map([@account.id], current_user.account_id) - @blocking = Account.blocking_map([@account.id], current_user.account_id) - @muting = Account.muting_map([@account.id], current_user.account_id) - @requested = Account.requested_map([@account.id], current_user.account_id) - @domain_blocking = Account.domain_blocking_map([@account.id], current_user.account_id) + def relationships + AccountRelationshipsPresenter.new([@account.id], current_user.account_id) end end diff --git a/app/controllers/api/v1/apps_controller.rb b/app/controllers/api/v1/apps_controller.rb index 98e908948..44a27b20a 100644 --- a/app/controllers/api/v1/apps_controller.rb +++ b/app/controllers/api/v1/apps_controller.rb @@ -5,6 +5,7 @@ class Api::V1::AppsController < Api::BaseController def create @app = Doorkeeper::Application.create!(application_options) + render json: @app, serializer: REST::ApplicationSerializer end private diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb index 1702953cf..a412e4341 100644 --- a/app/controllers/api/v1/blocks_controller.rb +++ b/app/controllers/api/v1/blocks_controller.rb @@ -9,6 +9,7 @@ class Api::V1::BlocksController < Api::BaseController def index @accounts = load_accounts + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb index fe0819a3f..92c0a62a9 100644 --- a/app/controllers/api/v1/favourites_controller.rb +++ b/app/controllers/api/v1/favourites_controller.rb @@ -9,14 +9,13 @@ class Api::V1::FavouritesController < Api::BaseController def index @statuses = load_statuses + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private def load_statuses - cached_favourites.tap do |statuses| - set_maps(statuses) - end + cached_favourites end def cached_favourites diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index eed22ef4f..b9f50d784 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -7,6 +7,7 @@ class Api::V1::FollowRequestsController < Api::BaseController def index @accounts = load_accounts + render json: @accounts, each_serializer: REST::AccountSerializer end def authorize diff --git a/app/controllers/api/v1/follows_controller.rb b/app/controllers/api/v1/follows_controller.rb index bcdb4e177..e01ae5c01 100644 --- a/app/controllers/api/v1/follows_controller.rb +++ b/app/controllers/api/v1/follows_controller.rb @@ -10,7 +10,7 @@ class Api::V1::FollowsController < Api::BaseController raise ActiveRecord::RecordNotFound if follow_params[:uri].blank? @account = FollowService.new.call(current_user.account, target_uri).try(:target_account) - render :show + render json: @account, serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index ce2181879..1c6971c18 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -3,5 +3,7 @@ class Api::V1::InstancesController < Api::BaseController respond_to :json - def show; end + def show + render json: {}, serializer: REST::InstanceSerializer + end end diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb index 25a331319..8a1992fca 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -11,6 +11,7 @@ class Api::V1::MediaController < Api::BaseController def create @media = current_account.media_attachments.create!(file: media_params[:file]) + render json: @media, serializer: REST::MediaAttachmentSerializer rescue Paperclip::Errors::NotIdentifiedByImageMagickError render json: file_type_error, status: 422 rescue Paperclip::Error diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb index 2a353df03..0c43cb943 100644 --- a/app/controllers/api/v1/mutes_controller.rb +++ b/app/controllers/api/v1/mutes_controller.rb @@ -9,6 +9,7 @@ class Api::V1::MutesController < Api::BaseController def index @accounts = load_accounts + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index a28e99f2f..8910b77e9 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -11,11 +11,12 @@ class Api::V1::NotificationsController < Api::BaseController def index @notifications = load_notifications - set_maps_for_notification_target_statuses + render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id) end def show @notification = current_account.notifications.find(params[:id]) + render json: @notification, serializer: REST::NotificationSerializer end def clear @@ -46,10 +47,6 @@ class Api::V1::NotificationsController < Api::BaseController current_account.notifications.browserable(exclude_types) end - def set_maps_for_notification_target_statuses - set_maps target_statuses_from_notifications - end - def target_statuses_from_notifications @notifications.reject { |notification| notification.target_status.nil? }.map(&:target_status) end diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb index 8e7070d07..9592cd4bd 100644 --- a/app/controllers/api/v1/reports_controller.rb +++ b/app/controllers/api/v1/reports_controller.rb @@ -9,6 +9,7 @@ class Api::V1::ReportsController < Api::BaseController def index @reports = current_account.reports + render json: @reports, each_serializer: REST::ReportSerializer end def create @@ -20,7 +21,7 @@ class Api::V1::ReportsController < Api::BaseController User.admins.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later } - render :show + render json: @report, serializer: REST::ReportSerializer end private diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb index 8b832148c..bc5b8e5d4 100644 --- a/app/controllers/api/v1/search_controller.rb +++ b/app/controllers/api/v1/search_controller.rb @@ -3,10 +3,14 @@ class Api::V1::SearchController < Api::BaseController RESULTS_LIMIT = 5 + before_action -> { doorkeeper_authorize! :read } + before_action :require_user! + respond_to :json def index - @search = OpenStruct.new(search_results) + @search = Search.new(search_results) + render json: @search, serializer: REST::SearchSerializer end private diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb index e58184939..f95cf9457 100644 --- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb @@ -11,7 +11,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController def index @accounts = load_accounts - render 'api/v1/statuses/accounts' + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/statuses/favourites_controller.rb b/app/controllers/api/v1/statuses/favourites_controller.rb index b6fb13cc0..4c4b0c160 100644 --- a/app/controllers/api/v1/statuses/favourites_controller.rb +++ b/app/controllers/api/v1/statuses/favourites_controller.rb @@ -10,7 +10,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController def create @status = favourited_status - render 'api/v1/statuses/show' + render json: @status, serializer: REST::StatusSerializer end def destroy @@ -19,7 +19,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController UnfavouriteWorker.perform_async(current_user.account_id, @status.id) - render 'api/v1/statuses/show' + render json: @status, serializer: REST::StatusSerializer end private diff --git a/app/controllers/api/v1/statuses/mutes_controller.rb b/app/controllers/api/v1/statuses/mutes_controller.rb index eab88f2ef..a4bf0acdd 100644 --- a/app/controllers/api/v1/statuses/mutes_controller.rb +++ b/app/controllers/api/v1/statuses/mutes_controller.rb @@ -14,14 +14,14 @@ class Api::V1::Statuses::MutesController < Api::BaseController current_account.mute_conversation!(@conversation) @mutes_map = { @conversation.id => true } - render 'api/v1/statuses/show' + render json: @status, serializer: REST::StatusSerializer end def destroy current_account.unmute_conversation!(@conversation) @mutes_map = { @conversation.id => false } - render 'api/v1/statuses/show' + render json: @status, serializer: REST::StatusSerializer end private diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb index 43593d3c5..175217e6e 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -11,7 +11,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController def index @accounts = load_accounts - render 'api/v1/statuses/accounts' + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index ee9c5b3a6..f7f4b5a5c 100644 --- a/app/controllers/api/v1/statuses/reblogs_controller.rb +++ b/app/controllers/api/v1/statuses/reblogs_controller.rb @@ -10,7 +10,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController def create @status = ReblogService.new.call(current_user.account, status_for_reblog) - render 'api/v1/statuses/show' + render json: @status, serializer: REST::StatusSerializer end def destroy @@ -20,7 +20,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController authorize status_for_destroy, :unreblog? RemovalWorker.perform_async(status_for_destroy.id) - render 'api/v1/statuses/show' + render json: @status, serializer: REST::StatusSerializer end private diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 9aa1cbc4d..9c7124d0f 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -13,6 +13,7 @@ class Api::V1::StatusesController < Api::BaseController def show cached = Rails.cache.read(@status.cache_key) @status = cached unless cached.nil? + render json: @status, serializer: REST::StatusSerializer end def context @@ -21,15 +22,20 @@ class Api::V1::StatusesController < Api::BaseController loaded_ancestors = cache_collection(ancestors_results, Status) loaded_descendants = cache_collection(descendants_results, Status) - @context = OpenStruct.new(ancestors: loaded_ancestors, descendants: loaded_descendants) - statuses = [@status] + @context[:ancestors] + @context[:descendants] + @context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants) + statuses = [@status] + @context.ancestors + @context.descendants - set_maps(statuses) + render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id) end def card @card = PreviewCard.find_by(status: @status) - render_empty if @card.nil? + + if @card.nil? + render_empty + else + render json: @card, serializer: REST::PreviewCardSerializer + end end def create @@ -43,7 +49,7 @@ class Api::V1::StatusesController < Api::BaseController application: doorkeeper_token.application, idempotency: request.headers['Idempotency-Key']) - render :show + render json: @status, serializer: REST::StatusSerializer end def destroy diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb index 511d2f65d..3dd27710c 100644 --- a/app/controllers/api/v1/timelines/home_controller.rb +++ b/app/controllers/api/v1/timelines/home_controller.rb @@ -9,15 +9,13 @@ class Api::V1::Timelines::HomeController < Api::BaseController def show @statuses = load_statuses - render 'api/v1/timelines/show' + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private def load_statuses - cached_home_statuses.tap do |statuses| - set_maps(statuses) - end + cached_home_statuses end def cached_home_statuses diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index 305451cc7..49887778e 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -7,15 +7,13 @@ class Api::V1::Timelines::PublicController < Api::BaseController def show @statuses = load_statuses - render 'api/v1/timelines/show' + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private def load_statuses - cached_public_statuses.tap do |statuses| - set_maps(statuses) - end + cached_public_statuses end def cached_public_statuses diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index 50afca7c7..08db04a39 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -8,7 +8,7 @@ class Api::V1::Timelines::TagController < Api::BaseController def show @statuses = load_statuses - render 'api/v1/timelines/show' + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private @@ -18,9 +18,7 @@ class Api::V1::Timelines::TagController < Api::BaseController end def load_statuses - cached_tagged_statuses.tap do |statuses| - set_maps(statuses) - end + cached_tagged_statuses end def cached_tagged_statuses diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 865fcd125..b3c2db02b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -70,7 +70,7 @@ class ApplicationController < ActionController::Base end def current_session - @current_session ||= SessionActivation.find_by(session_id: session['auth_id']) + @current_session ||= SessionActivation.find_by(session_id: cookies.signed['_session_id']) end def cache_collection(raw, klass) diff --git a/app/controllers/authorize_follows_controller.rb b/app/controllers/authorize_follows_controller.rb index da4ef022a..dccd1c209 100644 --- a/app/controllers/authorize_follows_controller.rb +++ b/app/controllers/authorize_follows_controller.rb @@ -15,7 +15,7 @@ class AuthorizeFollowsController < ApplicationController if @account.nil? render :error else - redirect_to web_url("accounts/#{@account.id}") + render :success end rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError render :error diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 6209a3ae9..8a8b9ec76 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -2,13 +2,10 @@ class HomeController < ApplicationController before_action :authenticate_user! + before_action :set_initial_state_json def index - @body_classes = 'app-body' - @token = current_session.token - @web_settings = Web::Setting.find_by(user: current_user)&.data || {} - @admin = Account.find_local(Setting.site_contact_username) - @streaming_api_base_url = Rails.configuration.x.streaming_api_base_url + @body_classes = 'app-body' end private @@ -16,4 +13,18 @@ class HomeController < ApplicationController def authenticate_user! redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in? end + + def set_initial_state_json + serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) + @initial_state_json = serializable_resource.to_json + end + + def initial_state_params + { + settings: Web::Setting.find_by(user: current_user)&.data || {}, + current_account: current_account, + token: current_session.token, + admin: Account.find_local(Setting.site_contact_username), + } + end end diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 71f5a7c04..cac5b0ba8 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -34,9 +34,11 @@ class Settings::PreferencesController < ApplicationController def user_settings_params params.require(:user).permit( :setting_default_privacy, + :setting_default_sensitive, :setting_boost_modal, :setting_delete_modal, :setting_auto_play_gif, + :setting_system_font_ui, notification_emails: %i(follow follow_request reblog favourite mention digest), interactions: %i(must_be_follower must_be_following) ) diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 0dfa30e56..6a57b3d63 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -6,15 +6,21 @@ module Admin::FilterHelper FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS - def filter_link_to(text, more_params) - new_url = filtered_url_for(more_params) - link_to text, new_url, class: filter_link_class(new_url) + def filter_link_to(text, link_to_params, link_class_params = link_to_params) + new_url = filtered_url_for(link_to_params) + new_class = filtered_url_for(link_class_params) + link_to text, new_url, class: filter_link_class(new_class) end def table_link_to(icon, text, path, options = {}) link_to safe_join([fa_icon(icon), text]), path, options.merge(class: 'table-action-link') end + def selected?(more_params) + new_url = filtered_url_for(more_params) + filter_link_class(new_url) == 'selected' ? true : false + end + private def filter_params(more_params) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 36c37fae0..9f50d8bdb 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -31,7 +31,11 @@ module ApplicationHelper Rails.env.production? ? site_title : "#{site_title} (Dev)" end - def fa_icon(icon) - content_tag(:i, nil, class: 'fa ' + icon.split(' ').map { |cl| "fa-#{cl}" }.join(' ')) + def fa_icon(icon, attributes = {}) + class_names = attributes[:class]&.split(' ') || [] + class_names << 'fa' + class_names += icon.split(' ').map { |cl| "fa-#{cl}" } + + content_tag(:i, nil, attributes.merge(class: class_names.join(' '))) end end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 847eff2e7..af950aa63 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -19,6 +19,7 @@ module SettingsHelper io: 'Ido', it: 'Italiano', ja: '日本語', + ko: '한국어', nl: 'Nederlands', no: 'Norsk', oc: 'Occitan', diff --git a/app/javascript/fonts/montserrat/Montserrat-Medium.ttf b/app/javascript/fonts/montserrat/Montserrat-Medium.ttf new file mode 100644 index 000000000..88d70b89c --- /dev/null +++ b/app/javascript/fonts/montserrat/Montserrat-Medium.ttf Binary files differdiff --git a/app/javascript/images/cloud2.png b/app/javascript/images/cloud2.png new file mode 100644 index 000000000..f325ca6de --- /dev/null +++ b/app/javascript/images/cloud2.png Binary files differdiff --git a/app/javascript/images/cloud3.png b/app/javascript/images/cloud3.png new file mode 100644 index 000000000..ab194d0b8 --- /dev/null +++ b/app/javascript/images/cloud3.png Binary files differdiff --git a/app/javascript/images/cloud4.png b/app/javascript/images/cloud4.png new file mode 100644 index 000000000..98323f5a2 --- /dev/null +++ b/app/javascript/images/cloud4.png Binary files differdiff --git a/app/javascript/images/elephant-fren.png b/app/javascript/images/elephant-fren.png new file mode 100644 index 000000000..3b64edf08 --- /dev/null +++ b/app/javascript/images/elephant-fren.png Binary files differdiff --git a/app/javascript/images/logo.svg b/app/javascript/images/logo.svg index c233db842..16cb3a944 100644 --- a/app/javascript/images/logo.svg +++ b/app/javascript/images/logo.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" height="1000" width="1000"><path d="M500 0a500 500 0 0 0-353.553 146.447 500 500 0 1 0 707.106 707.106A500 500 0 0 0 500 0zm-.059 280.05h107.12c-19.071 13.424-26.187 51.016-27.12 73.843V562.05c0 44.32-35.68 80-80 80s-80-35.68-80-80v-202c0-44.32 35.68-80 80-80zm-.441 52c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zm-279.059 7.9c44.32 0 80 35.68 80 80v206.157c.933 22.827 8.049 60.42 27.12 73.842H220.44c-44.32 0-80-35.68-80-80v-200c0-44.32 35.68-80 80-80zm559.12 0c44.32 0 80 35.68 80 80v200c0 44.32-35.68 80-80 80H672.44c19.071-13.424 26.187-51.016 27.12-73.843V419.95c0-44.32 35.68-80 80-80zM220 392c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm560 0c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm-280.5 40.05c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zM220 491.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zM499.5 532c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zM220 591.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28z" fill="#189efc"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" height="1000" width="1000"><path d="M500 0a500 500 0 0 0-353.553 146.447 500 500 0 1 0 707.106 707.106A500 500 0 0 0 500 0zm-.059 280.05h107.12c-19.071 13.424-26.187 51.016-27.12 73.843V562.05c0 44.32-35.68 80-80 80s-80-35.68-80-80v-202c0-44.32 35.68-80 80-80zm-.441 52c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zm-279.059 7.9c44.32 0 80 35.68 80 80v206.157c.933 22.827 8.049 60.42 27.12 73.842H220.44c-44.32 0-80-35.68-80-80v-200c0-44.32 35.68-80 80-80zm559.12 0c44.32 0 80 35.68 80 80v200c0 44.32-35.68 80-80 80H672.44c19.071-13.424 26.187-51.016 27.12-73.843V419.95c0-44.32 35.68-80 80-80zM220 392c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm560 0c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm-280.5 40.05c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zM220 491.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zM499.5 532c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zM220 591.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28z" fill="#fff"/></svg> diff --git a/app/javascript/mastodon/actions/bundles.js b/app/javascript/mastodon/actions/bundles.js new file mode 100644 index 000000000..ecc9c8f7d --- /dev/null +++ b/app/javascript/mastodon/actions/bundles.js @@ -0,0 +1,25 @@ +export const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST'; +export const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS'; +export const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL'; + +export function fetchBundleRequest(skipLoading) { + return { + type: BUNDLE_FETCH_REQUEST, + skipLoading, + }; +} + +export function fetchBundleSuccess(skipLoading) { + return { + type: BUNDLE_FETCH_SUCCESS, + skipLoading, + }; +} + +export function fetchBundleFail(error, skipLoading) { + return { + type: BUNDLE_FETCH_FAIL, + error, + skipLoading, + }; +} diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index cda636139..c7d248122 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -1,5 +1,5 @@ import api, { getLinks } from '../api'; -import Immutable from 'immutable'; +import { List as ImmutableList } from 'immutable'; import IntlMessageFormat from 'intl-messageformat'; import { fetchRelationships } from './accounts'; import { defineMessages } from 'react-intl'; @@ -124,7 +124,7 @@ export function refreshNotificationsFail(error, skipLoading) { export function expandNotifications() { return (dispatch, getState) => { - const items = getState().getIn(['notifications', 'items'], Immutable.List()); + const items = getState().getIn(['notifications', 'items'], ImmutableList()); if (getState().getIn(['notifications', 'isLoading']) || items.size === 0) { return; diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js index 601cea001..0597d265e 100644 --- a/app/javascript/mastodon/actions/store.js +++ b/app/javascript/mastodon/actions/store.js @@ -1,10 +1,11 @@ -import Immutable from 'immutable'; +import { Iterable, fromJS } from 'immutable'; export const STORE_HYDRATE = 'STORE_HYDRATE'; +export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; const convertState = rawState => - Immutable.fromJS(rawState, (k, v) => - Immutable.Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x => + fromJS(rawState, (k, v) => + Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x => Number.isNaN(x * 1) ? x : x * 1)); export function hydrateStore(rawState) { diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index cb4410eba..dd14cb1cd 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -1,5 +1,5 @@ import api, { getLinks } from '../api'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE'; @@ -66,13 +66,13 @@ export function refreshTimelineRequest(timeline, skipLoading) { export function refreshTimeline(timelineId, path, params = {}) { return function (dispatch, getState) { - const timeline = getState().getIn(['timelines', timelineId], Immutable.Map()); + const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); if (timeline.get('isLoading') || timeline.get('online')) { return; } - const ids = timeline.get('items', Immutable.List()); + const ids = timeline.get('items', ImmutableList()); const newestId = ids.size > 0 ? ids.first() : null; let skipLoading = timeline.get('loaded'); @@ -111,8 +111,8 @@ export function refreshTimelineFail(timeline, error, skipLoading) { export function expandTimeline(timelineId, path, params = {}) { return (dispatch, getState) => { - const timeline = getState().getIn(['timelines', timelineId], Immutable.Map()); - const ids = timeline.get('items', Immutable.List()); + const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); + const ids = timeline.get('items', ImmutableList()); if (timeline.get('isLoading') || ids.size === 0) { return; diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js index 027d01767..e9f041be6 100644 --- a/app/javascript/mastodon/components/column_header.js +++ b/app/javascript/mastodon/components/column_header.js @@ -10,7 +10,7 @@ export default class ColumnHeader extends React.PureComponent { }; static propTypes = { - title: PropTypes.string.isRequired, + title: PropTypes.node.isRequired, icon: PropTypes.string.isRequired, active: PropTypes.bool, multiColumn: PropTypes.bool, diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index 12e1b44fa..98323b069 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -14,6 +14,7 @@ export default class DropdownMenu extends React.PureComponent { size: PropTypes.number.isRequired, direction: PropTypes.string, ariaLabel: PropTypes.string, + disabled: PropTypes.bool, }; static defaultProps = { @@ -68,9 +69,19 @@ export default class DropdownMenu extends React.PureComponent { } render () { - const { icon, items, size, direction, ariaLabel } = this.props; - const { expanded } = this.state; + const { icon, items, size, direction, ariaLabel, disabled } = this.props; + const { expanded } = this.state; const directionClass = (direction === 'left') ? 'dropdown__left' : 'dropdown__right'; + const iconStyle = { fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }; + const iconClassname = `fa fa-fw fa-${icon} dropdown__icon`; + + if (disabled) { + return ( + <div className='icon-button disabled' style={iconStyle} aria-label={ariaLabel}> + <i className={iconClassname} aria-hidden /> + </div> + ); + } const dropdownItems = expanded && ( <ul className='dropdown__content-list'> @@ -80,8 +91,8 @@ export default class DropdownMenu extends React.PureComponent { return ( <Dropdown ref={this.setRef} onShow={this.handleShow} onHide={this.handleHide}> - <DropdownTrigger className='icon-button' style={{ fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }} aria-label={ariaLabel}> - <i className={`fa fa-fw fa-${icon} dropdown__icon`} aria-hidden /> + <DropdownTrigger className='icon-button' style={iconStyle} aria-label={ariaLabel}> + <i className={iconClassname} aria-hidden /> </DropdownTrigger> <DropdownContent className={directionClass}> diff --git a/app/javascript/mastodon/components/permalink.js b/app/javascript/mastodon/components/permalink.js index 0b7d0a65a..d726d37a2 100644 --- a/app/javascript/mastodon/components/permalink.js +++ b/app/javascript/mastodon/components/permalink.js @@ -15,7 +15,7 @@ export default class Permalink extends React.PureComponent { }; handleClick = (e) => { - if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); this.context.router.history.push(this.props.to); } @@ -25,7 +25,7 @@ export default class Permalink extends React.PureComponent { const { href, children, className, ...other } = this.props; return ( - <a href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}> + <a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}> {children} </a> ); diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js index 398f7d243..8287375c4 100644 --- a/app/javascript/mastodon/containers/mastodon.js +++ b/app/javascript/mastodon/containers/mastodon.js @@ -22,14 +22,15 @@ import { getLocale } from '../locales'; const { localeData, messages } = getLocale(); addLocaleData(localeData); -const store = configureStore(); +export const store = configureStore(); const initialState = JSON.parse(document.getElementById('initial-state').textContent); try { initialState.local_settings = JSON.parse(localStorage.getItem('mastodon-settings')); } catch (e) { initialState.local_settings = {}; } -store.dispatch(hydrateStore(initialState)); +const hydrateAction = hydrateStore(initialState); +store.dispatch(hydrateAction); export default class Mastodon extends React.PureComponent { diff --git a/app/javascript/mastodon/containers/timeline_container.js b/app/javascript/mastodon/containers/timeline_container.js new file mode 100644 index 000000000..6b545ef09 --- /dev/null +++ b/app/javascript/mastodon/containers/timeline_container.js @@ -0,0 +1,39 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import PropTypes from 'prop-types'; +import configureStore from '../store/configureStore'; +import { hydrateStore } from '../actions/store'; +import { IntlProvider, addLocaleData } from 'react-intl'; +import { getLocale } from '../locales'; +import PublicTimeline from '../features/standalone/public_timeline'; + +const { localeData, messages } = getLocale(); +addLocaleData(localeData); + +const store = configureStore(); +const initialStateContainer = document.getElementById('initial-state'); + +if (initialStateContainer !== null) { + const initialState = JSON.parse(initialStateContainer.textContent); + store.dispatch(hydrateStore(initialState)); +} + +export default class TimelineContainer extends React.PureComponent { + + static propTypes = { + locale: PropTypes.string.isRequired, + }; + + render () { + const { locale } = this.props; + + return ( + <IntlProvider locale={locale} messages={messages}> + <Provider store={store}> + <PublicTimeline /> + </Provider> + </IntlProvider> + ); + } + +} diff --git a/app/javascript/mastodon/emoji.js b/app/javascript/mastodon/emoji.js index 01d01fb72..7043d5f3a 100644 --- a/app/javascript/mastodon/emoji.js +++ b/app/javascript/mastodon/emoji.js @@ -1,35 +1,55 @@ import emojione from 'emojione'; - -const toImage = str => shortnameToImage(unicodeToImage(str)); - -const unicodeToImage = str => { - const mappedUnicode = emojione.mapUnicodeToShort(); - - return str.replace(emojione.regUnicode, unicodeChar => { - if (typeof unicodeChar === 'undefined' || unicodeChar === '' || !(unicodeChar in emojione.jsEscapeMap)) { - return unicodeChar; +import Trie from 'substring-trie'; + +const mappedUnicode = emojione.mapUnicodeToShort(); +const trie = new Trie(Object.keys(emojione.jsEscapeMap)); + +function emojify(str) { + // This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.) + // and replacing valid shortnames like :smile: and :wink: as well as unicode strings + // that _aren't_ within tags with an <img> version. + // The goal is to be the same as an emojione.regShortNames/regUnicode replacement, but faster. + let i = -1; + let insideTag = false; + let insideShortname = false; + let shortnameStartIndex = -1; + let match; + while (++i < str.length) { + const char = str.charAt(i); + if (insideShortname && char === ':') { + const shortname = str.substring(shortnameStartIndex, i + 1); + if (shortname in emojione.emojioneList) { + const unicode = emojione.emojioneList[shortname].unicode[emojione.emojioneList[shortname].unicode.length - 1]; + const alt = emojione.convert(unicode.toUpperCase()); + const replacement = `<img draggable="false" class="emojione" alt="${alt}" title="${shortname}" src="/emoji/${unicode}.svg" />`; + str = str.substring(0, shortnameStartIndex) + replacement + str.substring(i + 1); + i += (replacement.length - shortname.length - 1); // jump ahead the length we've added to the string + } else { + i--; // stray colon, try again + } + insideShortname = false; + } else if (insideTag && char === '>') { + insideTag = false; + } else if (char === '<') { + insideTag = true; + insideShortname = false; + } else if (!insideTag && char === ':') { + insideShortname = true; + shortnameStartIndex = i; + } else if (!insideTag && (match = trie.search(str.substring(i)))) { + const unicodeStr = match; + if (unicodeStr in emojione.jsEscapeMap) { + const unicode = emojione.jsEscapeMap[unicodeStr]; + const short = mappedUnicode[unicode]; + const filename = emojione.emojioneList[short].fname; + const alt = emojione.convert(unicode.toUpperCase()); + const replacement = `<img draggable="false" class="emojione" alt="${alt}" title="${short}" src="/emoji/${filename}.svg" />`; + str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length); + i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string + } } - - const unicode = emojione.jsEscapeMap[unicodeChar]; - const short = mappedUnicode[unicode]; - const filename = emojione.emojioneList[short].fname; - const alt = emojione.convert(unicode.toUpperCase()); - - return `<img draggable="false" class="emojione" alt="${alt}" title="${short}" src="/emoji/${filename}.svg" />`; - }); -}; - -const shortnameToImage = str => str.replace(emojione.regShortNames, shortname => { - if (typeof shortname === 'undefined' || shortname === '' || !(shortname in emojione.emojioneList)) { - return shortname; } + return str; +} - const unicode = emojione.emojioneList[shortname].unicode[emojione.emojioneList[shortname].unicode.length - 1]; - const alt = emojione.convert(unicode.toUpperCase()); - - return `<img draggable="false" class="emojione" alt="${alt}" title="${shortname}" src="/emoji/${unicode}.svg" />`; -}); - -export default function emojify(text) { - return toImage(text); -}; +export default emojify; diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js index 955d0000e..3c8b63114 100644 --- a/app/javascript/mastodon/features/account_timeline/index.js +++ b/app/javascript/mastodon/features/account_timeline/index.js @@ -9,11 +9,11 @@ import LoadingIndicator from '../../components/loading_indicator'; import Column from '../ui/components/column'; import HeaderContainer from './containers/header_container'; import ColumnBackButton from '../../components/column_back_button'; -import Immutable from 'immutable'; +import { List as ImmutableList } from 'immutable'; import ImmutablePureComponent from 'react-immutable-pure-component'; const mapStateToProps = (state, props) => ({ - statusIds: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'items'], Immutable.List()), + statusIds: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'items'], ImmutableList()), isLoading: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'isLoading']), hasMore: !!state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'next']), me: state.getIn(['meta', 'me']), diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js index c83dbb63e..83c66a5d5 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -2,6 +2,7 @@ import React from 'react'; import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; +import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; const messages = defineMessages({ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, @@ -50,7 +51,7 @@ export default class EmojiPickerDropdown extends React.PureComponent { this.setState({ active: true }); if (!EmojiPicker) { this.setState({ loading: true }); - import(/* webpackChunkName: "emojione_picker" */ 'emojione-picker').then(TheEmojiPicker => { + EmojiPickerAsync().then(TheEmojiPicker => { EmojiPicker = TheEmojiPicker.default; this.setState({ loading: false }); }).catch(() => { diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index 3ec205f2c..69bead689 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -2,6 +2,7 @@ import React from 'react'; import ComposeFormContainer from './containers/compose_form_container'; import NavigationContainer from './containers/navigation_container'; import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; import { mountCompose, unmountCompose } from '../../actions/compose'; import { openModal } from '../../actions/modal'; @@ -15,6 +16,8 @@ import SearchResultsContainer from './containers/search_results_container'; const messages = defineMessages({ start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, + home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, + notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' }, community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' }, @@ -22,6 +25,7 @@ const messages = defineMessages({ }); const mapStateToProps = state => ({ + columns: state.getIn(['settings', 'columns']), showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), }); @@ -31,6 +35,7 @@ export default class Compose extends React.PureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, + columns: ImmutablePropTypes.list.isRequired, multiColumn: PropTypes.bool, showSearch: PropTypes.bool, intl: PropTypes.object.isRequired, @@ -60,11 +65,22 @@ export default class Compose extends React.PureComponent { let header = ''; if (multiColumn) { + const { columns } = this.props; header = ( <div className='drawer__header'> <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)}><i role='img' aria-label={intl.formatMessage(messages.start)} className='fa fa-fw fa-asterisk' /></Link> - <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)}><i role='img' aria-label={intl.formatMessage(messages.community)} className='fa fa-fw fa-users' /></Link> - <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)}><i role='img' aria-label={intl.formatMessage(messages.public)} className='fa fa-fw fa-globe' /></Link> + {!columns.some(column => column.get('id') === 'HOME') && ( + <Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)}><i role='img' className='fa fa-fw fa-home' aria-label={intl.formatMessage(messages.home_timeline)} /></Link> + )} + {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && ( + <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)}><i role='img' className='fa fa-fw fa-bell' aria-label={intl.formatMessage(messages.notifications)} /></Link> + )} + {!columns.some(column => column.get('id') === 'COMMUNITY') && ( + <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)}><i role='img' aria-label={intl.formatMessage(messages.community)} className='fa fa-fw fa-users' /></Link> + )} + {!columns.some(column => column.get('id') === 'PUBLIC') && ( + <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)}><i role='img' aria-label={intl.formatMessage(messages.public)} className='fa fa-fw fa-globe' /></Link> + )} <a onClick={this.openSettings} role='button' tabIndex='0' className='drawer__tab' title={intl.formatMessage(messages.settings)}><i role='img' aria-label={intl.formatMessage(messages.settings)} className='fa fa-fw fa-cogs' /></a> <a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)}><i role='img' aria-label={intl.formatMessage(messages.logout)} className='fa fa-fw fa-sign-out' /></a> </div> diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index d4e1555b2..fb8f0d3cc 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -11,7 +11,7 @@ import { ScrollContainer } from 'react-router-scroll'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ColumnSettingsContainer from './containers/column_settings_container'; import { createSelector } from 'reselect'; -import Immutable from 'immutable'; +import { List as ImmutableList } from 'immutable'; import LoadMore from '../../components/load_more'; import { debounce } from 'lodash'; @@ -20,7 +20,7 @@ const messages = defineMessages({ }); const getNotifications = createSelector([ - state => Immutable.List(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()), + 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')))); @@ -122,7 +122,7 @@ export default class Notifications extends React.PureComponent { let unread = ''; let scrollContainer = ''; - if (!isLoading && notifications.size > 0 && hasMore) { + if (!isLoading && hasMore) { loadMore = <LoadMore onClick={this.handleLoadMore} />; } @@ -132,7 +132,7 @@ export default class Notifications extends React.PureComponent { if (isLoading && this.scrollableArea) { scrollableArea = this.scrollableArea; - } else if (notifications.size > 0) { + } else if (notifications.size > 0 || hasMore) { scrollableArea = ( <div className='scrollable' onScroll={this.handleScroll} ref={this.setRef}> {unread} diff --git a/app/javascript/mastodon/features/report/containers/status_check_box_container.js b/app/javascript/mastodon/features/report/containers/status_check_box_container.js index 8997718a2..48cd0319b 100644 --- a/app/javascript/mastodon/features/report/containers/status_check_box_container.js +++ b/app/javascript/mastodon/features/report/containers/status_check_box_container.js @@ -1,11 +1,11 @@ import { connect } from 'react-redux'; import StatusCheckBox from '../components/status_check_box'; import { toggleStatusReport } from '../../../actions/reports'; -import Immutable from 'immutable'; +import { Set as ImmutableSet } from 'immutable'; const mapStateToProps = (state, { id }) => ({ status: state.getIn(['statuses', id]), - checked: state.getIn(['reports', 'new', 'status_ids'], Immutable.Set()).includes(id), + checked: state.getIn(['reports', 'new', 'status_ids'], ImmutableSet()).includes(id), }); const mapDispatchToProps = (dispatch, { id }) => ({ diff --git a/app/javascript/mastodon/features/standalone/public_timeline/index.js b/app/javascript/mastodon/features/standalone/public_timeline/index.js new file mode 100644 index 000000000..de4b5320a --- /dev/null +++ b/app/javascript/mastodon/features/standalone/public_timeline/index.js @@ -0,0 +1,76 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import StatusListContainer from '../../ui/containers/status_list_container'; +import { + refreshPublicTimeline, + expandPublicTimeline, +} from '../../../actions/timelines'; +import Column from '../../../components/column'; +import ColumnHeader from '../../../components/column_header'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' }, +}); + +@connect() +@injectIntl +export default class PublicTimeline 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(refreshPublicTimeline()); + + this.polling = setInterval(() => { + dispatch(refreshPublicTimeline()); + }, 3000); + } + + componentWillUnmount () { + if (typeof this.polling !== 'undefined') { + clearInterval(this.polling); + this.polling = null; + } + } + + handleLoadMore = () => { + this.props.dispatch(expandPublicTimeline()); + } + + render () { + const { intl } = this.props; + + return ( + <Column ref={this.setRef}> + <ColumnHeader + icon='globe' + title={intl.formatMessage(messages.title)} + onClick={this.handleHeaderClick} + /> + + <StatusListContainer + timelineId='public' + loadMore={this.handleLoadMore} + scrollKey='standalone_public_timeline' + trackScroll={false} + /> + </Column> + ); + } + +} diff --git a/app/javascript/mastodon/features/ui/components/bundle.js b/app/javascript/mastodon/features/ui/components/bundle.js new file mode 100644 index 000000000..72798f690 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/bundle.js @@ -0,0 +1,101 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const emptyComponent = () => null; +const noop = () => { }; + +class Bundle extends React.Component { + + static propTypes = { + fetchComponent: PropTypes.func.isRequired, + loading: PropTypes.func, + error: PropTypes.func, + children: PropTypes.func.isRequired, + renderDelay: PropTypes.number, + onFetch: PropTypes.func, + onFetchSuccess: PropTypes.func, + onFetchFail: PropTypes.func, + } + + static defaultProps = { + loading: emptyComponent, + error: emptyComponent, + renderDelay: 0, + onFetch: noop, + onFetchSuccess: noop, + onFetchFail: noop, + } + + static cache = {} + + state = { + mod: undefined, + forceRender: false, + } + + componentWillMount() { + this.load(this.props); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.fetchComponent !== this.props.fetchComponent) { + this.load(nextProps); + } + } + + componentWillUnmount () { + if (this.timeout) { + clearTimeout(this.timeout); + } + } + + load = (props) => { + const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props; + + this.setState({ mod: undefined }); + onFetch(); + + if (renderDelay !== 0) { + this.timestamp = new Date(); + this.timeout = setTimeout(() => this.setState({ forceRender: true }), renderDelay); + } + + if (Bundle.cache[fetchComponent.name]) { + const mod = Bundle.cache[fetchComponent.name]; + + this.setState({ mod: mod.default }); + onFetchSuccess(); + return Promise.resolve(); + } + + return fetchComponent() + .then((mod) => { + Bundle.cache[fetchComponent.name] = mod; + this.setState({ mod: mod.default }); + onFetchSuccess(); + }) + .catch((error) => { + this.setState({ mod: null }); + onFetchFail(error); + }); + } + + render() { + const { loading: Loading, error: Error, children, renderDelay } = this.props; + const { mod, forceRender } = this.state; + const elapsed = this.timestamp ? (new Date() - this.timestamp) : renderDelay; + + if (mod === undefined) { + return (elapsed >= renderDelay || forceRender) ? <Loading /> : null; + } + + if (mod === null) { + return <Error onRetry={this.load} />; + } + + return children(mod); + } + +} + +export default Bundle; diff --git a/app/javascript/mastodon/features/ui/components/bundle_column_error.js b/app/javascript/mastodon/features/ui/components/bundle_column_error.js new file mode 100644 index 000000000..cd124746a --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/bundle_column_error.js @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl } from 'react-intl'; + +import Column from './column'; +import ColumnHeader from './column_header'; +import ColumnBackButtonSlim from '../../../components/column_back_button_slim'; +import IconButton from '../../../components/icon_button'; + +const messages = defineMessages({ + title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' }, + body: { id: 'bundle_column_error.body', defaultMessage: 'Something went wrong while loading this component.' }, + retry: { id: 'bundle_column_error.retry', defaultMessage: 'Try again' }, +}); + +class BundleColumnError extends React.Component { + + static propTypes = { + onRetry: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + } + + handleRetry = () => { + this.props.onRetry(); + } + + render () { + const { intl: { formatMessage } } = this.props; + + return ( + <Column> + <ColumnHeader icon='exclamation-circle' type={formatMessage(messages.title)} /> + <ColumnBackButtonSlim /> + <div className='error-column'> + <IconButton title={formatMessage(messages.retry)} icon='refresh' onClick={this.handleRetry} size={64} /> + {formatMessage(messages.body)} + </div> + </Column> + ); + } + +} + +export default injectIntl(BundleColumnError); diff --git a/app/javascript/mastodon/features/ui/components/bundle_modal_error.js b/app/javascript/mastodon/features/ui/components/bundle_modal_error.js new file mode 100644 index 000000000..928bfe1f7 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/bundle_modal_error.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl } from 'react-intl'; + +import IconButton from '../../../components/icon_button'; + +const messages = defineMessages({ + error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this component.' }, + retry: { id: 'bundle_modal_error.retry', defaultMessage: 'Try again' }, + close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' }, +}); + +class BundleModalError extends React.Component { + + static propTypes = { + onRetry: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + } + + handleRetry = () => { + this.props.onRetry(); + } + + render () { + const { onClose, intl: { formatMessage } } = this.props; + + // Keep the markup in sync with <ModalLoading /> + // (make sure they have the same dimensions) + return ( + <div className='modal-root__modal error-modal'> + <div className='error-modal__body'> + <IconButton title={formatMessage(messages.retry)} icon='refresh' onClick={this.handleRetry} size={64} /> + {formatMessage(messages.error)} + </div> + + <div className='error-modal__footer'> + <div> + <button + onClick={onClose} + className='error-modal__nav onboarding-modal__skip' + > + {formatMessage(messages.close)} + </button> + </div> + </div> + </div> + ); + } + +} + +export default injectIntl(BundleModalError); diff --git a/app/javascript/mastodon/features/ui/components/column_loading.js b/app/javascript/mastodon/features/ui/components/column_loading.js new file mode 100644 index 000000000..7ecfaf77a --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/column_loading.js @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Column from '../../../components/column'; +import ColumnHeader from '../../../components/column_header'; + +const ColumnLoading = ({ title = '', icon = ' ' }) => ( + <Column> + <ColumnHeader icon={icon} title={title} multiColumn={false} /> + <div className='scrollable' /> + </Column> +); + +ColumnLoading.propTypes = { + title: PropTypes.node, + icon: PropTypes.string, +}; + +export default ColumnLoading; diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 3c3e9425d..cbc185a7d 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -2,14 +2,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import ReactSwipeable from 'react-swipeable'; -import HomeTimeline from '../../home_timeline'; -import Notifications from '../../notifications'; -import PublicTimeline from '../../public_timeline'; -import CommunityTimeline from '../../community_timeline'; -import HashtagTimeline from '../../hashtag_timeline'; -import Compose from '../../compose'; -import { getPreviousLink, getNextLink } from './tabs_bar'; + +import ReactSwipeableViews from 'react-swipeable-views'; +import { links, getIndex, getLink } from './tabs_bar'; + +import BundleContainer from '../containers/bundle_container'; +import ColumnLoading from './column_loading'; +import BundleColumnError from './bundle_column_error'; +import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline } from '../../ui/util/async-components'; const componentMap = { 'COMPOSE': Compose, @@ -32,39 +32,61 @@ export default class ColumnsArea extends ImmutablePureComponent { children: PropTypes.node, }; - handleRightSwipe = () => { - const previousLink = getPreviousLink(this.context.router.history.location.pathname); + handleSwipe = (index) => { + window.requestAnimationFrame(() => { + window.requestAnimationFrame(() => { + this.context.router.history.push(getLink(index)); + }); + }); + } - if (previousLink) { - this.context.router.history.push(previousLink); - } + renderView = (link, index) => { + const columnIndex = getIndex(this.context.router.history.location.pathname); + const title = link.props.children[1] && React.cloneElement(link.props.children[1]); + const icon = (link.props.children[0] || link.props.children).props.className.split(' ')[2].split('-')[1]; + + const view = (index === columnIndex) ? + React.cloneElement(this.props.children) : + <ColumnLoading title={title} icon={icon} />; + + return ( + <div className='columns-area' key={index}> + {view} + </div> + ); } - handleLeftSwipe = () => { - const previousLink = getNextLink(this.context.router.history.location.pathname); + renderLoading = () => { + return <ColumnLoading />; + } - if (previousLink) { - this.context.router.history.push(previousLink); - } - }; + renderError = (props) => { + return <BundleColumnError {...props} />; + } render () { const { columns, children, singleColumn } = this.props; + const columnIndex = getIndex(this.context.router.history.location.pathname); + if (singleColumn) { - return ( - <ReactSwipeable onSwipedLeft={this.handleLeftSwipe} onSwipedRight={this.handleRightSwipe} className='columns-area'> - {children} - </ReactSwipeable> - ); + return columnIndex !== -1 ? ( + <ReactSwipeableViews index={columnIndex} onChangeIndex={this.handleSwipe} animateTransitions={false} style={{ height: '100%' }}> + {links.map(this.renderView)} + </ReactSwipeableViews> + ) : <div className='columns-area'>{children}</div>; } return ( <div className='columns-area'> {columns.map(column => { - const SpecificComponent = componentMap[column.get('id')]; const params = column.get('params', null) === null ? null : column.get('params').toJS(); - return <SpecificComponent key={column.get('uuid')} columnId={column.get('uuid')} params={params} multiColumn />; + + return ( + <BundleContainer key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading} error={this.renderError}> + {SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn />} + </BundleContainer> + ); })} {React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))} diff --git a/app/javascript/mastodon/features/ui/components/image_loader.js b/app/javascript/mastodon/features/ui/components/image_loader.js index 52c3a898b..aad594380 100644 --- a/app/javascript/mastodon/features/ui/components/image_loader.js +++ b/app/javascript/mastodon/features/ui/components/image_loader.js @@ -8,12 +8,14 @@ export default class ImageLoader extends React.PureComponent { alt: PropTypes.string, src: PropTypes.string.isRequired, previewSrc: PropTypes.string.isRequired, - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, + width: PropTypes.number, + height: PropTypes.number, } static defaultProps = { alt: '', + width: null, + height: null, }; state = { @@ -46,8 +48,8 @@ export default class ImageLoader extends React.PureComponent { this.setState({ loading: true, error: false }); Promise.all([ this.loadPreviewCanvas(props), - this.loadOriginalImage(props), - ]) + this.hasSize() && this.loadOriginalImage(props), + ].filter(Boolean)) .then(() => { this.setState({ loading: false, error: false }); this.clearPreviewCanvas(); @@ -106,6 +108,11 @@ export default class ImageLoader extends React.PureComponent { this.removers = []; } + hasSize () { + const { width, height } = this.props; + return typeof width === 'number' && typeof height === 'number'; + } + setCanvasRef = c => { this.canvas = c; } @@ -116,6 +123,7 @@ export default class ImageLoader extends React.PureComponent { const className = classNames('image-loader', { 'image-loader--loading': loading, + 'image-loader--amorphous': !this.hasSize(), }); return ( @@ -125,6 +133,7 @@ export default class ImageLoader extends React.PureComponent { width={width} height={height} ref={this.setCanvasRef} + style={{ opacity: loading ? 1 : 0 }} /> {!loading && ( diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index 8bb81ca01..d869fffa6 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -1,5 +1,5 @@ import React from 'react'; -import ReactSwipeable from 'react-swipeable'; +import ReactSwipeableViews from 'react-swipeable-views'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import ExtendedVideoPlayer from '../../../components/extended_video_player'; @@ -26,12 +26,16 @@ export default class MediaModal extends ImmutablePureComponent { index: null, }; + handleSwipe = (index) => { + this.setState({ index: (index) % this.props.media.size }); + } + handleNextClick = () => { this.setState({ index: (this.getIndex() + 1) % this.props.media.size }); } handlePrevClick = () => { - this.setState({ index: (this.getIndex() - 1) % this.props.media.size }); + this.setState({ index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size }); } handleKeyUp = (e) => { @@ -74,7 +78,12 @@ export default class MediaModal extends ImmutablePureComponent { } if (attachment.get('type') === 'image') { - content = <ImageLoader previewSrc={attachment.get('preview_url')} src={url} width={attachment.getIn(['meta', 'original', 'width'])} height={attachment.getIn(['meta', 'original', 'height'])} />; + content = media.map((image) => { + const width = image.getIn(['meta', 'original', 'width']) || null; + const height = image.getIn(['meta', 'original', 'height']) || null; + + return <ImageLoader previewSrc={image.get('preview_url')} src={image.get('url')} width={width} height={height} key={image.get('preview_url')} />; + }).toArray(); } else if (attachment.get('type') === 'gifv') { content = <ExtendedVideoPlayer src={url} muted controls={false} />; } @@ -85,9 +94,9 @@ export default class MediaModal extends ImmutablePureComponent { <div className='media-modal__content'> <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} /> - <ReactSwipeable onSwipedRight={this.handlePrevClick} onSwipedLeft={this.handleNextClick}> + <ReactSwipeableViews onChangeIndex={this.handleSwipe} index={index} animateHeight> {content} - </ReactSwipeable> + </ReactSwipeableViews> </div> {rightNav} diff --git a/app/javascript/mastodon/features/ui/components/modal_loading.js b/app/javascript/mastodon/features/ui/components/modal_loading.js new file mode 100644 index 000000000..f403ca4c9 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/modal_loading.js @@ -0,0 +1,20 @@ +import React from 'react'; + +import LoadingIndicator from '../../../components/loading_indicator'; + +// Keep the markup in sync with <BundleModalError /> +// (make sure they have the same dimensions) +const ModalLoading = () => ( + <div className='modal-root__modal error-modal'> + <div className='error-modal__body'> + <LoadingIndicator /> + </div> + <div className='error-modal__footer'> + <div> + <button className='error-modal__nav onboarding-modal__skip' /> + </div> + </div> + </div> +); + +export default ModalLoading; diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index 3777c1bf6..de4f44ce6 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -1,14 +1,19 @@ import React from 'react'; import PropTypes from 'prop-types'; -import MediaModal from './media_modal'; -import OnboardingModal from './onboarding_modal'; -import VideoModal from './video_modal'; -import BoostModal from './boost_modal'; -import ConfirmationModal from './confirmation_modal'; -import ReportModal from './report_modal'; -import SettingsContainer from '../../../../glitch/containers/settings'; import TransitionMotion from 'react-motion/lib/TransitionMotion'; import spring from 'react-motion/lib/spring'; +import BundleContainer from '../containers/bundle_container'; +import BundleModalError from './bundle_modal_error'; +import ModalLoading from './modal_loading'; +import { + MediaModal, + OnboardingModal, + VideoModal, + BoostModal, + ConfirmationModal, + ReportModal, + SettingsModal, +} from '../../../features/ui/util/async-components'; const MODAL_COMPONENTS = { 'MEDIA': MediaModal, @@ -17,7 +22,7 @@ const MODAL_COMPONENTS = { 'BOOST': BoostModal, 'CONFIRM': ConfirmationModal, 'REPORT': ReportModal, - 'SETTINGS': SettingsContainer, + 'SETTINGS': SettingsModal, }; export default class ModalRoot extends React.PureComponent { @@ -51,6 +56,22 @@ export default class ModalRoot extends React.PureComponent { return { opacity: spring(0), scale: spring(0.98) }; } + renderModal = (SpecificComponent) => { + const { props, onClose } = this.props; + + return <SpecificComponent {...props} onClose={onClose} />; + } + + renderLoading = () => { + return <ModalLoading />; + } + + renderError = (props) => { + const { onClose } = this.props; + + return <BundleModalError {...props} onClose={onClose} />; + } + render () { const { type, props, onClose } = this.props; const visible = !!type; @@ -72,18 +93,14 @@ export default class ModalRoot extends React.PureComponent { > {interpolatedStyles => <div className='modal-root'> - {interpolatedStyles.map(({ key, data: { type, props }, style }) => { - const SpecificComponent = MODAL_COMPONENTS[type]; - - return ( - <div key={key} style={{ pointerEvents: visible ? 'auto' : 'none' }}> - <div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity }} onClick={onClose} /> - <div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}> - <SpecificComponent {...props} onClose={onClose} /> - </div> + {interpolatedStyles.map(({ key, data: { type }, style }) => ( + <div key={key} style={{ pointerEvents: visible ? 'auto' : 'none' }}> + <div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity }} onClick={onClose} /> + <div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}> + <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>{this.renderModal}</BundleContainer> </div> - ); - })} + </div> + ))} </div> } </TransitionMotion> diff --git a/app/javascript/mastodon/features/ui/components/onboarding_modal.js b/app/javascript/mastodon/features/ui/components/onboarding_modal.js index dab5e47ea..1b1cb00da 100644 --- a/app/javascript/mastodon/features/ui/components/onboarding_modal.js +++ b/app/javascript/mastodon/features/ui/components/onboarding_modal.js @@ -3,16 +3,14 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ReactSwipeable from 'react-swipeable'; +import ReactSwipeableViews from 'react-swipeable-views'; import classNames from 'classnames'; import Permalink from '../../../components/permalink'; -import TransitionMotion from 'react-motion/lib/TransitionMotion'; -import spring from 'react-motion/lib/spring'; import ComposeForm from '../../compose/components/compose_form'; import Search from '../../compose/components/search'; import NavigationBar from '../../compose/components/navigation_bar'; import ColumnHeader from './column_header'; -import Immutable from 'immutable'; +import { List as ImmutableList } from 'immutable'; const noop = () => { }; @@ -50,7 +48,7 @@ const PageTwo = ({ me }) => ( </div> <ComposeForm text='Awoo! #introductions' - suggestions={Immutable.List()} + suggestions={ImmutableList()} mentionedDomains={[]} spoiler={false} onChange={noop} @@ -227,6 +225,10 @@ export default class OnboardingModal extends React.PureComponent { })); } + handleSwipe = (index) => { + this.setState({ currentIndex: index }); + } + handleKeyUp = ({ key }) => { switch (key) { case 'ArrowLeft': @@ -263,30 +265,18 @@ export default class OnboardingModal extends React.PureComponent { </button> ); - const styles = pages.map((data, i) => ({ - key: `page-${i}`, - data, - style: { - opacity: spring(i === currentIndex ? 1 : 0), - }, - })); - return ( <div className='modal-root__modal onboarding-modal'> - <TransitionMotion styles={styles}> - {interpolatedStyles => ( - <ReactSwipeable onSwipedRight={this.handlePrev} onSwipedLeft={this.handleNext} className='onboarding-modal__pager'> - {interpolatedStyles.map(({ key, data, style }, i) => { - const className = classNames('onboarding-modal__page__wrapper', { - 'onboarding-modal__page__wrapper--active': i === currentIndex, - }); - return ( - <div key={key} style={style} className={className}>{data}</div> - ); - })} - </ReactSwipeable> - )} - </TransitionMotion> + <ReactSwipeableViews index={currentIndex} onChangeIndex={this.handleSwipe} className='onboarding-modal__pager'> + {pages.map((page, i) => { + const className = classNames('onboarding-modal__page__wrapper', { + 'onboarding-modal__page__wrapper--active': i === currentIndex, + }); + return ( + <div key={i} className={className}>{page}</div> + ); + })} + </ReactSwipeableViews> <div className='onboarding-modal__paginator'> <div> diff --git a/app/javascript/mastodon/features/ui/components/report_modal.js b/app/javascript/mastodon/features/ui/components/report_modal.js index c989d2c9b..b5dfa422e 100644 --- a/app/javascript/mastodon/features/ui/components/report_modal.js +++ b/app/javascript/mastodon/features/ui/components/report_modal.js @@ -7,7 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { makeGetAccount } from '../../../selectors'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; import StatusCheckBox from '../../report/containers/status_check_box_container'; -import Immutable from 'immutable'; +import { OrderedSet } from 'immutable'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Button from '../../../components/button'; @@ -26,7 +26,7 @@ const makeMapStateToProps = () => { isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']), account: getAccount(state, accountId), comment: state.getIn(['reports', 'new', 'comment']), - statusIds: Immutable.OrderedSet(state.getIn(['timelines', `account:${accountId}`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])), + statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])), }; }; diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js index c2e6c88b5..b4153ff45 100644 --- a/app/javascript/mastodon/features/ui/components/tabs_bar.js +++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js @@ -2,7 +2,7 @@ import React from 'react'; import NavLink from 'react-router-dom/NavLink'; import { FormattedMessage } from 'react-intl'; -const links = [ +export const links = [ <NavLink className='tabs-bar__link primary' activeClassName='active' to='/statuses/new'><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></NavLink>, <NavLink className='tabs-bar__link primary' activeClassName='active' to='/timelines/home'><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>, <NavLink className='tabs-bar__link primary' activeClassName='active' to='/notifications'><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>, @@ -13,25 +13,13 @@ const links = [ <NavLink className='tabs-bar__link primary' activeClassName='active' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started'><i className='fa fa-fw fa-asterisk' /></NavLink>, ]; -export function getPreviousLink (path) { - const index = links.findIndex(link => link.props.to === path); - - if (index > 0) { - return links[index - 1].props.to; - } - - return null; -}; - -export function getNextLink (path) { - const index = links.findIndex(link => link.props.to === path); - - if (index !== -1 && index < links.length - 1) { - return links[index + 1].props.to; - } +export function getIndex (path) { + return links.findIndex(link => link.props.to === path); +} - return null; -}; +export function getLink (index) { + return links[index].props.to; +} export default class TabsBar extends React.Component { diff --git a/app/javascript/mastodon/features/ui/containers/bundle_container.js b/app/javascript/mastodon/features/ui/containers/bundle_container.js new file mode 100644 index 000000000..7e3f0c3a6 --- /dev/null +++ b/app/javascript/mastodon/features/ui/containers/bundle_container.js @@ -0,0 +1,19 @@ +import { connect } from 'react-redux'; + +import Bundle from '../components/bundle'; + +import { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from '../../../actions/bundles'; + +const mapDispatchToProps = dispatch => ({ + onFetch () { + dispatch(fetchBundleRequest()); + }, + onFetchSuccess () { + dispatch(fetchBundleSuccess()); + }, + onFetchFail (error) { + dispatch(fetchBundleFail(error)); + }, +}); + +export default connect(null, mapDispatchToProps)(Bundle); 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 45ad6209b..1b2e1056a 100644 --- a/app/javascript/mastodon/features/ui/containers/status_list_container.js +++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js @@ -1,13 +1,13 @@ import { connect } from 'react-redux'; import StatusList from '../../../components/status_list'; import { scrollTopTimeline } from '../../../actions/timelines'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { createSelector } from 'reselect'; import { debounce } from 'lodash'; const makeGetStatusIds = () => createSelector([ - (state, { type }) => state.getIn(['settings', type], Immutable.Map()), - (state, { type }) => state.getIn(['timelines', type, 'items'], Immutable.List()), + (state, { type }) => state.getIn(['settings', type], ImmutableMap()), + (state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()), (state) => state.get('statuses'), (state) => state.getIn(['meta', 'me']), ], (columnSettings, statusIds, statuses, me) => statusIds.filter(id => { diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 824963a53..5a0398eb4 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -1,6 +1,5 @@ import React from 'react'; -import Switch from 'react-router-dom/Switch'; -import Route from 'react-router-dom/Route'; +import classNames from 'classnames'; import Redirect from 'react-router-dom/Redirect'; import NotificationsContainer from './containers/notifications_container'; import PropTypes from 'prop-types'; @@ -13,66 +12,37 @@ import { debounce } from 'lodash'; import { uploadCompose } from '../../actions/compose'; import { refreshHomeTimeline } from '../../actions/timelines'; import { refreshNotifications } from '../../actions/notifications'; +import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import UploadArea from './components/upload_area'; import ColumnsAreaContainer from './containers/columns_area_container'; -import Status from '../../features/status'; -import GettingStarted from '../../features/getting_started'; -import PublicTimeline from '../../features/public_timeline'; -import CommunityTimeline from '../../features/community_timeline'; -import AccountTimeline from '../../features/account_timeline'; -import AccountGallery from '../../features/account_gallery'; -import HomeTimeline from '../../features/home_timeline'; -import Compose from '../../features/compose'; -import Followers from '../../features/followers'; -import Following from '../../features/following'; -import Reblogs from '../../features/reblogs'; -import Favourites from '../../features/favourites'; -import HashtagTimeline from '../../features/hashtag_timeline'; -import Notifications from '../../features/notifications'; -import FollowRequests from '../../features/follow_requests'; -import GenericNotFound from '../../features/generic_not_found'; -import FavouritedStatuses from '../../features/favourited_statuses'; -import Blocks from '../../features/blocks'; -import Mutes from '../../features/mutes'; - -// Small wrapper to pass multiColumn to the route components -const WrappedSwitch = ({ multiColumn, children }) => ( - <Switch> - {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))} - </Switch> -); - -WrappedSwitch.propTypes = { - multiColumn: PropTypes.bool, - children: PropTypes.node, -}; - -// Small Wraper to extract the params from the route and pass -// them to the rendered component, together with the content to -// be rendered inside (the children) -class WrappedRoute extends React.Component { - - static propTypes = { - component: PropTypes.func.isRequired, - content: PropTypes.node, - multiColumn: PropTypes.bool, - } - - renderComponent = ({ match: { params } }) => { - const { component: Component, content, multiColumn } = this.props; - - return <Component params={params} multiColumn={multiColumn}>{content}</Component>; - } - - render () { - const { component: Component, content, ...rest } = this.props; - - return <Route {...rest} render={this.renderComponent} />; - } - -} +import { + Compose, + Status, + GettingStarted, + PublicTimeline, + CommunityTimeline, + AccountTimeline, + AccountGallery, + HomeTimeline, + Followers, + Following, + Reblogs, + Favourites, + HashtagTimeline, + Notifications, + FollowRequests, + GenericNotFound, + FavouritedStatuses, + Blocks, + Mutes, +} from './util/async-components'; + +// Dummy import, to make sure that <Status /> ends up in the application bundle. +// Without this it ends up in ~8 very commonly used bundles. +import '../../../glitch/components/status'; const mapStateToProps = state => ({ + systemFontUi: state.getIn(['meta', 'system_font_ui']), layout: state.getIn(['local_settings', 'layout']), isWide: state.getIn(['local_settings', 'stretch']), }); @@ -85,6 +55,7 @@ export default class UI extends React.PureComponent { children: PropTypes.node, layout: PropTypes.string, isWide: PropTypes.bool, + systemFontUi: PropTypes.bool, }; state = { @@ -194,8 +165,13 @@ export default class UI extends React.PureComponent { } }; + const className = classNames('ui', columnsClass(layout), { + 'wide': isWide, + 'system-font': this.props.systemFontUi, + }); + return ( - <div className={'ui ' + columnsClass(layout) + (isWide ? ' wide' : '')} ref={this.setRef}> + <div className={className} ref={this.setRef}> <TabsBar /> <ColumnsAreaContainer singleColumn={isMobile(width, layout)}> <WrappedSwitch> diff --git a/app/javascript/mastodon/features/ui/util/get_rect_from_entry.js b/app/javascript/mastodon/features/ui/util/get_rect_from_entry.js new file mode 100644 index 000000000..c266cd7dc --- /dev/null +++ b/app/javascript/mastodon/features/ui/util/get_rect_from_entry.js @@ -0,0 +1,21 @@ + +// Get the bounding client rect from an IntersectionObserver entry. +// This is to work around a bug in Chrome: https://crbug.com/737228 + +let hasBoundingRectBug; + +function getRectFromEntry(entry) { + if (typeof hasBoundingRectBug !== 'boolean') { + const boundingRect = entry.target.getBoundingClientRect(); + const observerRect = entry.boundingClientRect; + hasBoundingRectBug = boundingRect.height !== observerRect.height || + boundingRect.top !== observerRect.top || + boundingRect.width !== observerRect.width || + boundingRect.bottom !== observerRect.bottom || + boundingRect.left !== observerRect.left || + boundingRect.right !== observerRect.right; + } + return hasBoundingRectBug ? entry.target.getBoundingClientRect() : entry.boundingClientRect; +} + +export default getRectFromEntry; diff --git a/app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js b/app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js index 0e959f9ae..2b24c6583 100644 --- a/app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js +++ b/app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js @@ -37,9 +37,18 @@ class IntersectionObserverWrapper { } } + unobserve (id, node) { + if (this.observer) { + delete this.callbacks[id]; + this.observer.unobserve(node); + } + } + disconnect () { if (this.observer) { + this.callbacks = {}; this.observer.disconnect(); + this.observer = null; } } diff --git a/app/javascript/mastodon/features/ui/util/react_router_helpers.js b/app/javascript/mastodon/features/ui/util/react_router_helpers.js new file mode 100644 index 000000000..ede578e56 --- /dev/null +++ b/app/javascript/mastodon/features/ui/util/react_router_helpers.js @@ -0,0 +1,57 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Switch from 'react-router-dom/Switch'; +import Route from 'react-router-dom/Route'; + +import ColumnLoading from '../components/column_loading'; +import BundleColumnError from '../components/bundle_column_error'; +import BundleContainer from '../containers/bundle_container'; + +// Small wrapper to pass multiColumn to the route components +export const WrappedSwitch = ({ multiColumn, children }) => ( + <Switch> + {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))} + </Switch> +); + +WrappedSwitch.propTypes = { + multiColumn: PropTypes.bool, + children: PropTypes.node, +}; + +// Small Wraper to extract the params from the route and pass +// them to the rendered component, together with the content to +// be rendered inside (the children) +export class WrappedRoute extends React.Component { + + static propTypes = { + component: PropTypes.func.isRequired, + content: PropTypes.node, + multiColumn: PropTypes.bool, + } + + renderComponent = ({ match }) => { + const { component, content, multiColumn } = this.props; + + return ( + <BundleContainer fetchComponent={component} loading={this.renderLoading} error={this.renderError}> + {Component => <Component params={match.params} multiColumn={multiColumn}>{content}</Component>} + </BundleContainer> + ); + } + + renderLoading = () => { + return <ColumnLoading />; + } + + renderError = (props) => { + return <BundleColumnError {...props} />; + } + + render () { + const { component: Component, content, ...rest } = this.props; + + return <Route {...rest} render={this.renderComponent} />; + } + +} diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index c13bc73d3..6992e7e0f 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -18,6 +18,12 @@ "account.unfollow": "إلغاء المتابعة", "account.unmute": "إلغاء الكتم عن @{name}", "boost_modal.combo": "يمكنك ضغط {combo} لتخطّي هذه في المرّة القادمة", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "الحسابات المحجوبة", "column.community": "الخيط العام المحلي", "column.favourites": "المفضلة", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "لا تقم بإدراجه على الخيوط العامة", "privacy.unlisted.short": "غير مدرج", "reply_indicator.cancel": "إلغاء", - "report.heading": "تقرير جديد", "report.placeholder": "تعليقات إضافية", "report.submit": "إرسال", "report.target": "إبلاغ", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index 3b6f228c6..7a56e1446 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -18,6 +18,12 @@ "account.unfollow": "Не следвай", "account.unmute": "Unmute @{name}", "boost_modal.combo": "You can press {combo} to skip this next time", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Blocked users", "column.community": "Local timeline", "column.favourites": "Favourites", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Do not show in public timelines", "privacy.unlisted.short": "Unlisted", "reply_indicator.cancel": "Отказ", - "report.heading": "New report", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 8e8c95d56..b2673915a 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -18,6 +18,12 @@ "account.unfollow": "Deixar de seguir", "account.unmute": "Treure silenci de @{name}", "boost_modal.combo": "Pots premer {combo} per saltar-te això el proper cop", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Usuaris bloquejats", "column.community": "Línia de temps local", "column.favourites": "Favorits", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "No publicar en línies de temps públiques", "privacy.unlisted.short": "No llistat", "reply_indicator.cancel": "Cancel·lar", - "report.heading": "Nou informe", "report.placeholder": "Comentaris addicionals", "report.submit": "Enviar", "report.target": "Informes", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 55499c0a3..4b62403c3 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -18,6 +18,12 @@ "account.unfollow": "Entfolgen", "account.unmute": "@{name} nicht mehr stummschalten", "boost_modal.combo": "Du kannst {combo} drücken, um dies beim nächsten Mal zu überspringen", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Blockierte Benutzer", "column.community": "Lokale Zeitleiste", "column.favourites": "Favoriten", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Nicht in öffentlichen Zeitleisten anzeigen", "privacy.unlisted.short": "Nicht gelistet", "reply_indicator.cancel": "Abbrechen", - "report.heading": "Neue Meldung", "report.placeholder": "Zusätzliche Kommentare", "report.submit": "Absenden", "report.target": "Melden", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index a5ff686a0..36d82ec1a 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -644,6 +644,14 @@ "id": "getting_started.heading" }, { + "defaultMessage": "Home", + "id": "tabs_bar.home" + }, + { + "defaultMessage": "Notifications", + "id": "tabs_bar.notifications" + }, + { "defaultMessage": "Federated timeline", "id": "navigation_bar.public_timeline" }, @@ -959,27 +967,6 @@ { "descriptors": [ { - "defaultMessage": "New report", - "id": "report.heading" - }, - { - "defaultMessage": "Additional comments", - "id": "report.placeholder" - }, - { - "defaultMessage": "Submit", - "id": "report.submit" - }, - { - "defaultMessage": "Reporting", - "id": "report.target" - } - ], - "path": "app/javascript/mastodon/features/report/index.json" - }, - { - "descriptors": [ - { "defaultMessage": "Delete", "id": "status.delete" }, @@ -1039,6 +1026,40 @@ { "descriptors": [ { + "defaultMessage": "Network error", + "id": "bundle_column_error.title" + }, + { + "defaultMessage": "Something went wrong while loading this component.", + "id": "bundle_column_error.body" + }, + { + "defaultMessage": "Try again", + "id": "bundle_column_error.retry" + } + ], + "path": "app/javascript/mastodon/features/ui/components/bundle_column_error.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Something went wrong while loading this component.", + "id": "bundle_modal_error.message" + }, + { + "defaultMessage": "Try again", + "id": "bundle_modal_error.retry" + }, + { + "defaultMessage": "Close", + "id": "bundle_modal_error.close" + } + ], + "path": "app/javascript/mastodon/features/ui/components/bundle_modal_error.json" + }, + { + "descriptors": [ + { "defaultMessage": "Cancel", "id": "confirmation_modal.cancel" } diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index b286ed2d3..cf29e38da 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -18,6 +18,12 @@ "account.unfollow": "Unfollow", "account.unmute": "Unmute @{name}", "boost_modal.combo": "You can press {combo} to skip this next time", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Blocked users", "column.community": "Local timeline", "column.favourites": "Favourites", @@ -141,7 +147,6 @@ "privacy.unlisted.long": "Do not post to public timelines", "privacy.unlisted.short": "Unlisted", "reply_indicator.cancel": "Cancel", - "report.heading": "Report {target}", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting {target}", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 330fe831d..2648a6840 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -18,6 +18,12 @@ "account.unfollow": "Malsekvi", "account.unmute": "Unmute @{name}", "boost_modal.combo": "You can press {combo} to skip this next time", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Blocked users", "column.community": "Loka tempolinio", "column.favourites": "Favourites", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Do not show in public timelines", "privacy.unlisted.short": "Unlisted", "reply_indicator.cancel": "Rezigni", - "report.heading": "New report", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 6469aa6f2..c42930380 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -18,6 +18,12 @@ "account.unfollow": "Dejar de seguir", "account.unmute": "Unmute @{name}", "boost_modal.combo": "You can press {combo} to skip this next time", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Usuarios bloqueados", "column.community": "Historia local", "column.favourites": "Favoritos", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "No mostrar en la historia federada", "privacy.unlisted.short": "Sin federar", "reply_indicator.cancel": "Cancelar", - "report.heading": "New report", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index 3835caab1..c9f1888b5 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -18,6 +18,12 @@ "account.unfollow": "پایان پیگیری", "account.unmute": "باصدا کردن @{name}", "boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "کاربران مسدودشده", "column.community": "نوشتههای محلی", "column.favourites": "پسندیدهها", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "عمومی، ولی فهرست نکن", "privacy.unlisted.short": "فهرستنشده", "reply_indicator.cancel": "لغو", - "report.heading": "گزارش تازه", "report.placeholder": "توضیح اضافه", "report.submit": "بفرست", "report.target": "گزارشدادن", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index dae911799..b836d2f5d 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -18,6 +18,12 @@ "account.unfollow": "Lopeta seuraaminen", "account.unmute": "Unmute @{name}", "boost_modal.combo": "You can press {combo} to skip this next time", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Blocked users", "column.community": "Paikallinen aikajana", "column.favourites": "Favourites", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Do not show in public timelines", "privacy.unlisted.short": "Unlisted", "reply_indicator.cancel": "Peruuta", - "report.heading": "New report", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 1a69235c8..eaa01638c 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -7,7 +7,7 @@ "account.followers": "Abonné⋅e⋅s", "account.follows": "Abonnements", "account.follows_you": "Vous suit", - "account.media": "Media", + "account.media": "Média", "account.mention": "Mentionner", "account.mute": "Masquer", "account.posts": "Statuts", @@ -18,6 +18,12 @@ "account.unfollow": "Ne plus suivre", "account.unmute": "Ne plus masquer", "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Comptes bloqués", "column.community": "Fil public local", "column.favourites": "Favoris", @@ -31,10 +37,10 @@ "column_header.unpin": "Retirer", "column_subheading.navigation": "Navigation", "column_subheading.settings": "Paramètres", - "compose_form.lock_disclaimer": "Votre compte n'est pas {locked}. Tout le monde peut vous suivre et voir vos pouets restreints.", + "compose_form.lock_disclaimer": "Votre compte n’est pas {locked}. Tout le monde peut vous suivre et voir vos pouets restreints.", "compose_form.lock_disclaimer.lock": "verrouillé", - "compose_form.placeholder": "Qu’avez-vous en tête ?", - "compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodon. Si {domains} {domainsCount, plural, one {n’est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n’y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d’une autre manière à d’autres personnes imprévues.", + "compose_form.placeholder": "Qu’avez-vous en tête ?", + "compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodon. Si {domains} {domainsCount, plural, one {n’est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n’y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d’une autre manière à d’autres personnes imprévues.", "compose_form.publish": "Pouet ", "compose_form.publish_loud": "{publish}!", "compose_form.sensitive": "Marquer le média comme délicat", @@ -42,13 +48,13 @@ "compose_form.spoiler_placeholder": "Avertissement", "confirmation_modal.cancel": "Annuler", "confirmations.block.confirm": "Bloquer", - "confirmations.block.message": "Confirmez vous le blocage de {name} ?", + "confirmations.block.message": "Confirmez vous le blocage de {name} ?", "confirmations.delete.confirm": "Supprimer", - "confirmations.delete.message": "Confirmez vous la suppression de ce pouet ?", + "confirmations.delete.message": "Confirmez vous la suppression de ce pouet ?", "confirmations.domain_block.confirm": "Masquer le domaine entier", - "confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier ? Dans la plupart des cas, quelques blocages ou silenciations ciblés sont suffisants et préférables.", + "confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier ? Dans la plupart des cas, quelques blocages ou silenciations ciblés sont suffisants et préférables.", "confirmations.mute.confirm": "Silencer", - "confirmations.mute.message": "Confirmez vous la silenciation {name} ?", + "confirmations.mute.message": "Confirmez vous la silenciation {name} ?", "emoji_button.activity": "Activités", "emoji_button.flags": "Drapeaux", "emoji_button.food": "Boire et manger", @@ -59,20 +65,20 @@ "emoji_button.search": "Recherche…", "emoji_button.symbols": "Symboles", "emoji_button.travel": "Lieux et voyages", - "empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir !", + "empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir !", "empty_column.hashtag": "Il n’y a encore aucun contenu relatif à ce hashtag", - "empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres utilisateurs⋅trices.", + "empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres utilisateur⋅ice⋅s.", "empty_column.home.inactivity": "Votre accueil est vide. Si vous ne vous êtes pas connecté⋅e depuis un moment, il se remplira automatiquement très bientôt.", "empty_column.home.public_timeline": "le fil public", - "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateurs⋅trices pour débuter la conversation.", - "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateurs⋅trices d’autres instances pour remplir le fil public.", + "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateur⋅ice⋅s pour débuter la conversation.", + "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateur⋅ice⋅s d’autres instances pour remplir le fil public.", "follow_request.authorize": "Autoriser", "follow_request.reject": "Rejeter", "getting_started.appsshort": "Applications", "getting_started.faq": "FAQ", "getting_started.heading": "Pour commencer", "getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer et envoyer vos commentaires et rapports de bogues via {github} sur GitHub.", - "getting_started.userguide": "Guide d'utilisation", + "getting_started.userguide": "Guide d’utilisation", "home.column_settings.advanced": "Avancé", "home.column_settings.basic": "Basique", "home.column_settings.filter_regex": "Filtrer avec une expression rationnelle", @@ -93,37 +99,37 @@ "navigation_bar.mutes": "Comptes silencés", "navigation_bar.preferences": "Préférences", "navigation_bar.public_timeline": "Fil public global", - "notification.favourite": "{name} a ajouté à ses favoris :", + "notification.favourite": "{name} a ajouté à ses favoris :", "notification.follow": "{name} vous suit.", - "notification.mention": "{name} vous a mentionné⋅e :", - "notification.reblog": "{name} a partagé votre statut :", + "notification.mention": "{name} vous a mentionné⋅e :", + "notification.reblog": "{name} a partagé votre statut :", "notifications.clear": "Nettoyer", - "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?", + "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?", "notifications.column_settings.alert": "Notifications locales", - "notifications.column_settings.favourite": "Favoris :", - "notifications.column_settings.follow": "Nouveaux⋅elles abonn⋅é⋅s :", - "notifications.column_settings.mention": "Mentions :", - "notifications.column_settings.reblog": "Partages :", + "notifications.column_settings.favourite": "Favoris :", + "notifications.column_settings.follow": "Nouveaux⋅elles abonn⋅é⋅s :", + "notifications.column_settings.mention": "Mentions :", + "notifications.column_settings.reblog": "Partages :", "notifications.column_settings.show": "Afficher dans la colonne", "notifications.column_settings.sound": "Émettre un son", "onboarding.done": "Effectué", "onboarding.next": "Suivant", - "onboarding.page_five.public_timelines": "Le fil public global affiche les posts de tou⋅te⋅s les utilisateurs⋅trices suivi⋅es par les membres de {domain}. Le fil public local est identique mais se limite aux utilisateurs⋅trices de {domain}.", - "onboarding.page_four.home": "L’Accueil affiche les posts de tou⋅te⋅s les utilisateurs⋅trices que vous suivez", + "onboarding.page_five.public_timelines": "Le fil public global affiche les posts de tou⋅te⋅s les utilisateur⋅ice⋅s suivi⋅es par les membres de {domain}. Le fil public local est identique mais se limite aux utilisateur⋅ice⋅s de {domain}.", + "onboarding.page_four.home": "L’Accueil affiche les posts de tou⋅te⋅s les utilisateur⋅ice⋅s que vous suivez", "onboarding.page_four.notifications": "Les Notifications vous informent lorsque quelqu’un interagit avec vous", "onboarding.page_one.federation": "Mastodon est un réseau social qui appartient à tou⋅te⋅s.", - "onboarding.page_one.handle": "Vous êtes sur {domain}, une des nombreuses instances indépendantes de Mastodon. Votre nom d’utilisateur⋅trice complet est {handle}", - "onboarding.page_one.welcome": "Bienvenue sur Mastodon !", + "onboarding.page_one.handle": "Vous êtes sur {domain}, une des nombreuses instances indépendantes de Mastodon. Votre nom d’utilisateur⋅ice complet est {handle}", + "onboarding.page_one.welcome": "Bienvenue sur Mastodon !", "onboarding.page_six.admin": "L’administrateur⋅trice de votre instance est {admin}", "onboarding.page_six.almost_done": "Nous y sommes presque…", "onboarding.page_six.appetoot": "Bon Appetoot!", "onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres. Et maintenant… Bon Appetoot!", "onboarding.page_six.github": "Mastodon est un logiciel libre, gratuit et open-source. Vous pouvez rapporter des bogues, suggérer des fonctionnalités, ou contribuer à son développement sur {github}.", "onboarding.page_six.guidelines": "règles de la communauté", - "onboarding.page_six.read_guidelines": "S’il vous plaît, n’oubliez pas de lire les {guidelines} !", + "onboarding.page_six.read_guidelines": "S’il vous plaît, n’oubliez pas de lire les {guidelines} !", "onboarding.page_six.various_app": "applications mobiles", "onboarding.page_three.profile": "Modifiez votre profil pour changer votre avatar, votre description ainsi que votre nom. Vous y trouverez également d’autres préférences.", - "onboarding.page_three.search": "Utilisez la barre de recherche pour trouver des utilisateurs⋅trices et regarder des hashtags tels que {illustration} et {introductions}. Pour trouver quelqu’un qui n’est pas sur cette instance, utilisez son nom d’utilisateur⋅trice complet.", + "onboarding.page_three.search": "Utilisez la barre de recherche pour trouver des utilisateur⋅ice⋅s et regarder des hashtags tels que {illustration} et {introductions}. Pour trouver quelqu’un qui n’est pas sur cette instance, utilisez son nom d’utilisateur⋅ice complet.", "onboarding.page_two.compose": "Écrivez depuis la colonne de composition. Vous pouvez ajouter des images, changer les réglages de confidentialité, et ajouter des avertissements de contenu (Content Warning) grâce aux icônes en dessous.", "onboarding.skip": "Passer", "privacy.change": "Ajuster la confidentialité du message", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Ne pas afficher dans les fils publics", "privacy.unlisted.short": "Non-listé", "reply_indicator.cancel": "Annuler", - "report.heading": "Nouveau signalement", "report.placeholder": "Commentaires additionnels", "report.submit": "Envoyer", "report.target": "Signalement", @@ -151,7 +156,7 @@ "status.mute_conversation": "Masquer la conversation", "status.open": "Déplier ce statut", "status.reblog": "Partager", - "status.reblogged_by": "{name} a partagé :", + "status.reblogged_by": "{name} a partagé :", "status.reply": "Répondre", "status.replyAll": "Répondre au fil", "status.report": "Signaler @{name}", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index db3d00394..98c7ea021 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -18,6 +18,12 @@ "account.unfollow": "הפסקת מעקב", "account.unmute": "הפסקת השתקת @{name}", "boost_modal.combo": "ניתן להקיש {combo} כדי לדלג בפעם הבאה", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "חסימות", "column.community": "ציר זמן מקומי", "column.favourites": "חיבובים", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "לא יופיע בפידים הציבוריים המשותפים", "privacy.unlisted.short": "לא לפיד הכללי", "reply_indicator.cancel": "ביטול", - "report.heading": "דווח חדש", "report.placeholder": "הערות נוספות", "report.submit": "שליחה", "report.target": "דיווח", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index f85eb8a3f..fdf5c11c0 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -18,6 +18,12 @@ "account.unfollow": "Prestani slijediti", "account.unmute": "Poništi utišavanje @{name}", "boost_modal.combo": "Možeš pritisnuti {combo} kako bi ovo preskočio sljedeći put", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Blokirani korisnici", "column.community": "Lokalni timeline", "column.favourites": "Favoriti", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Ne prikazuj u javnim timelineovima", "privacy.unlisted.short": "Unlisted", "reply_indicator.cancel": "Otkaži", - "report.heading": "Nova prijava", "report.placeholder": "Dodatni komentari", "report.submit": "Pošalji", "report.target": "Prijavljivanje", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 350410c4b..baf762c8d 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -18,6 +18,12 @@ "account.unfollow": "Követés abbahagyása", "account.unmute": "Unmute @{name}", "boost_modal.combo": "You can press {combo} to skip this next time", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Blocked users", "column.community": "Local timeline", "column.favourites": "Favourites", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Do not show in public timelines", "privacy.unlisted.short": "Unlisted", "reply_indicator.cancel": "Mégsem", - "report.heading": "New report", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting", diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index 6e9bc5ba9..6f6d688e9 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -18,6 +18,12 @@ "account.unfollow": "Berhenti mengikuti", "account.unmute": "Berhenti membisukan @{name}", "boost_modal.combo": "Anda dapat menekan {combo} untuk melewati ini", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Pengguna diblokir", "column.community": "Linimasa Lokal", "column.favourites": "Favorit", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Tidak ditampilkan di linimasa publik", "privacy.unlisted.short": "Tak Terdaftar", "reply_indicator.cancel": "Batal", - "report.heading": "Laporan baru", "report.placeholder": "Komentar tambahan", "report.submit": "Kirim", "report.target": "Melaporkan", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index 005dd4f56..25e0adc8a 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -18,6 +18,12 @@ "account.unfollow": "Ne plus sequar", "account.unmute": "Ne plus celar @{name}", "boost_modal.combo": "Tu povas presar sur {combo} por omisar co en la venonta foyo", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Blokusita uzeri", "column.community": "Lokala tempolineo", "column.favourites": "Favorati", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Ne montrar en publika tempolinei", "privacy.unlisted.short": "Ne enlistigota", "reply_indicator.cancel": "Nihiligar", - "report.heading": "Nova denunco", "report.placeholder": "Plusa komenti", "report.submit": "Sendar", "report.target": "Denuncante", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 4a5b218e8..4881b0f08 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -18,6 +18,12 @@ "account.unfollow": "Non seguire", "account.unmute": "Non silenziare @{name}", "boost_modal.combo": "Puoi premere {combo} per saltare questo passaggio la prossima volta", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Utenti bloccati", "column.community": "Timeline locale", "column.favourites": "Apprezzati", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Non mostrare sulla timeline pubblica", "privacy.unlisted.short": "Non elencato", "reply_indicator.cancel": "Annulla", - "report.heading": "Nuova segnalazione", "report.placeholder": "Commenti aggiuntivi", "report.submit": "Invia", "report.target": "Invio la segnalazione", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index cb8074b5d..f62072852 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -18,6 +18,12 @@ "account.unfollow": "フォロー解除", "account.unmute": "ミュート解除", "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。", + "bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。", + "bundle_column_error.retry": "再試行", + "bundle_column_error.title": "ネットワークエラー", + "bundle_modal_error.close": "閉じる", + "bundle_modal_error.message": "コンポーネントの読み込み中に問題が発生しました。", + "bundle_modal_error.retry": "再試行", "column.blocks": "ブロックしたユーザー", "column.community": "ローカルタイムライン", "column.favourites": "お気に入り", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "公開TLで表示しない", "privacy.unlisted.short": "未収載", "reply_indicator.cancel": "キャンセル", - "report.heading": "新規通報", "report.placeholder": "コメント", "report.submit": "通報する", "report.target": "問題のユーザー", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json new file mode 100644 index 000000000..5e1aaac85 --- /dev/null +++ b/app/javascript/mastodon/locales/ko.json @@ -0,0 +1,181 @@ +{ + "account.block": "차단", + "account.block_domain": "{domain} 전체를 숨김", + "account.disclaimer": "이 사용자는 다른 인스턴스에 소속되어 있으므로, 수치가 정확하지 않을 수도 있습니다.", + "account.edit_profile": "프로필 편집", + "account.follow": "팔로우", + "account.followers": "팔로워", + "account.follows": "팔로우", + "account.follows_you": "날 팔로우합니다", + "account.media": "미디어", + "account.mention": "답장", + "account.mute": "뮤트", + "account.posts": "포스트", + "account.report": "신고", + "account.requested": "승인 대기 중", + "account.unblock": "차단 해제", + "account.unblock_domain": "{domain} 숨김 해제", + "account.unfollow": "팔로우 해제", + "account.unmute": "뮤트 해제", + "boost_modal.combo": "다음부터 {combo}를 누르면 이 과정을 건너뛸 수 있습니다.", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", + "column.blocks": "차단 중인 사용자", + "column.community": "로컬 타임라인", + "column.favourites": "즐겨찾기", + "column.follow_requests": "팔로우 요청", + "column.home": "홈", + "column.mutes": "뮤트 중인 사용자", + "column.notifications": "알림", + "column.public": "연합 타임라인", + "column_back_button.label": "돌아가기", + "column_header.pin": "고정하기", + "column_header.unpin": "고정 해제", + "column_subheading.navigation": "내비게이션", + "column_subheading.settings": "설정", + "compose_form.lock_disclaimer": "이 계정은 {locked}로 설정 되어 있지 않습니다. 누구나 이 계정을 팔로우 할 수 있으며, 팔로워 공개의 포스팅을 볼 수 있습니다.", + "compose_form.lock_disclaimer.lock": "비공개", + "compose_form.placeholder": "지금 무엇을 하고 있나요?", + "compose_form.privacy_disclaimer": "이 계정의 비공개 포스트는 멘션된 사용자가 소속된 {domains}으로 전송됩니다. {domainsCount, plural, one {이 서버를} other {이 서버들을}} 신뢰할 수 있습니까? 포스팅의 프라이버시 보호는 Mastodon 서버에서만 유효합니다. {domains}가 Mastodon 인스턴스가 아닐 경우, 이 투고가 사적인 것으로 취급되지 않은 채 부스트 되거나 원하지 않는 사용자에게 보여질 가능성이 있습니다.", + "compose_form.publish": "Toot", + "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive": "이 미디어를 민감한 미디어로 취급", + "compose_form.spoiler": "텍스트 숨기기", + "compose_form.spoiler_placeholder": "경고", + "confirmation_modal.cancel": "취소", + "confirmations.block.confirm": "차단", + "confirmations.block.message": "정말로 {name}를 차단하시겠습니까?", + "confirmations.delete.confirm": "삭제", + "confirmations.delete.message": "정말로 삭제하시겠습니까?", + "confirmations.domain_block.confirm": "도메인 전체를 숨김", + "confirmations.domain_block.message": "정말로 {domain} 전체를 숨기시겠습니까? 대부분의 경우 개별 차단이나 뮤트로 충분합니다.", + "confirmations.mute.confirm": "뮤트", + "confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?", + "emoji_button.activity": "활동", + "emoji_button.flags": "국기", + "emoji_button.food": "음식", + "emoji_button.label": "emoji를 추가", + "emoji_button.nature": "자연", + "emoji_button.objects": "물건", + "emoji_button.people": "사람들", + "emoji_button.search": "검색...", + "emoji_button.symbols": "기호", + "emoji_button.travel": "여행과 장소", + "empty_column.community": "로컬 타임라인에 아무 것도 없습니다. 아무거나 적어 보세요!", + "empty_column.hashtag": "이 해시태그는 아직 사용되지 않았습니다.", + "empty_column.home": "아직 아무도 팔로우 하고 있지 않습니다. {public}를 보러 가거나, 검색하여 다른 사용자를 찾아 보세요.", + "empty_column.home.inactivity": "홈 피드에 아무 것도 없습니다. 한동안 활동하지 않은 경우 곧 원래대로 돌아올 것입니다.", + "empty_column.home.public_timeline": "연합 타임라인", + "empty_column.notifications": "아직 알림이 없습니다. 다른 사람과 대화를 시작해 보세요!", + "empty_column.public": "여기엔 아직 아무 것도 없습니다! 공개적으로 무언가 포스팅하거나, 다른 인스턴스 유저를 팔로우 해서 가득 채워보세요!", + "follow_request.authorize": "허가", + "follow_request.reject": "거부", + "getting_started.appsshort": "어플리케이션", + "getting_started.faq": "자주 있는 질문", + "getting_started.heading": "시작", + "getting_started.open_source_notice": "Mastodon은 오픈 소스 소프트웨어입니다. 누구나 GitHub({github})에서 개발에 참여하거나, 문제를 보고할 수 있습니다.", + "getting_started.userguide": "사용자 가이드", + "home.column_settings.advanced": "고급 사용자용", + "home.column_settings.basic": "기본 설정", + "home.column_settings.filter_regex": "정규 표현식으로 필터링", + "home.column_settings.show_reblogs": "부스트 표시", + "home.column_settings.show_replies": "답글 표시", + "home.settings": "컬럼 설정", + "lightbox.close": "닫기", + "loading_indicator.label": "불러오는 중...", + "media_gallery.toggle_visible": "표시 전환", + "missing_indicator.label": "찾을 수 없습니다", + "navigation_bar.blocks": "차단한 사용자", + "navigation_bar.community_timeline": "로컬 타임라인", + "navigation_bar.edit_profile": "프로필 편집", + "navigation_bar.favourites": "즐겨찾기", + "navigation_bar.follow_requests": "팔로우 요청", + "navigation_bar.info": "이 인스턴스에 대해서", + "navigation_bar.logout": "로그아웃", + "navigation_bar.mutes": "뮤트 중인 사용자", + "navigation_bar.preferences": "사용자 설정", + "navigation_bar.public_timeline": "연합 타임라인", + "notification.favourite": "{name}님이 즐겨찾기 했습니다", + "notification.follow": "{name}님이 나를 팔로우 했습니다", + "notification.mention": "{name}님이 답글을 보냈습니다", + "notification.reblog": "{name}님이 부스트 했습니다", + "notifications.clear": "알림 지우기", + "notifications.clear_confirmation": "정말로 알림을 삭제하시겠습니까?", + "notifications.column_settings.alert": "데스크탑 알림", + "notifications.column_settings.favourite": "즐겨찾기", + "notifications.column_settings.follow": "새 팔로워", + "notifications.column_settings.mention": "답글", + "notifications.column_settings.reblog": "부스트", + "notifications.column_settings.show": "컬럼에 표시", + "notifications.column_settings.sound": "효과음 재생", + "onboarding.done": "완료", + "onboarding.next": "다음", + "onboarding.page_five.public_timelines": "연합 타임라인에서는 {domain}의 사람들이 팔로우 중인 Mastodon 전체 인스턴스의 공개 포스트를 표시합니다. 로컬 타임라인에서는 {domain} 만의 공개 포스트를 표시합니다.", + "onboarding.page_four.home": "홈 타임라인에서는 내가 팔로우 중인 사람들의 포스트를 표시합니다.", + "onboarding.page_four.notifications": "알림에서는 다른 사람들과의 연결을 표시합니다.", + "onboarding.page_one.federation": "Mastodon은 누구나 참가할 수 있는 SNS입니다.", + "onboarding.page_one.handle": "여러분은 지금 수많은 Mastodon 인스턴스 중 하나인 {domain}에 있습니다. 당신의 유저 이름은 {handle} 입니다.", + "onboarding.page_one.welcome": "Mastodon에 어서 오세요!", + "onboarding.page_six.admin": "이 인스턴스의 관리자는 {admin}입니다.", + "onboarding.page_six.almost_done": "이상입니다.", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "iOS、Android 또는 다른 플랫폼에서 사용할 수 있는 {apps}이 있습니다.", + "onboarding.page_six.github": "Mastodon는 오픈 소스 소프트웨어입니다. 버그 보고나 기능 추가 요청, 기여는 {github}에서 할 수 있습니다.", + "onboarding.page_six.guidelines": "커뮤니티 가이드라인", + "onboarding.page_six.read_guidelines": "{guidelines}을 확인하는 것을 잊지 마세요.", + "onboarding.page_six.various_app": "다양한 모바일 어플리케이션", + "onboarding.page_three.profile": "[프로필 편집] 에서 자기 소개나 이름을 변경할 수 있습니다. 또한 다른 설정도 변경할 수 있습니다.", + "onboarding.page_three.search": "검색 바에서 {illustration} 나 {introductions} 와 같이 특정 해시태그가 달린 포스트를 보거나, 사용자를 찾을 수 있습니다.", + "onboarding.page_two.compose": "이 폼에서 포스팅 할 수 있습니다. 이미지나 공개 범위 설정, 스포일러 경고 설정은 아래 아이콘으로 설정할 수 있습니다.", + "onboarding.skip": "건너뛰기", + "privacy.change": "포스트의 프라이버시 설정을 변경", + "privacy.direct.long": "멘션한 사용자에게만 공개", + "privacy.direct.short": "다이렉트", + "privacy.private.long": "팔로워에게만 공개", + "privacy.private.short": "비공개", + "privacy.public.long": "공개 타임라인에 표시", + "privacy.public.short": "공개", + "privacy.unlisted.long": "공개 타임라인에 표시하지 않음", + "privacy.unlisted.short": "Unlisted", + "reply_indicator.cancel": "취소", + "report.placeholder": "코멘트", + "report.submit": "신고하기", + "report.target": "문제가 된 사용자", + "search.placeholder": "검색", + "search_results.total": "{count, number}건의 결과", + "status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다", + "status.delete": "삭제", + "status.favourite": "즐겨찾기", + "status.load_more": "더 보기", + "status.media_hidden": "미디어 숨겨짐", + "status.mention": "답장", + "status.mute_conversation": "이 대화를 뮤트", + "status.open": "상세 정보 표시", + "status.reblog": "부스트", + "status.reblogged_by": "{name}님이 부스트 했습니다", + "status.reply": "답장", + "status.replyAll": "전원에게 답장", + "status.report": "신고", + "status.sensitive_toggle": "클릭해서 표시하기", + "status.sensitive_warning": "민감한 미디어", + "status.show_less": "숨기기", + "status.show_more": "더 보기", + "status.unmute_conversation": "이 대화의 뮤트 해제하기", + "tabs_bar.compose": "포스트", + "tabs_bar.federated_timeline": "연합", + "tabs_bar.home": "홈", + "tabs_bar.local_timeline": "로컬", + "tabs_bar.notifications": "알림", + "upload_area.title": "드래그 & 드롭으로 업로드", + "upload_button.label": "미디어 추가", + "upload_form.undo": "재시도", + "upload_progress.label": "업로드 중...", + "video_player.expand": "동영상 자세히 보기", + "video_player.toggle_sound": "소리 토글하기", + "video_player.toggle_visible": "표시 전환", + "video_player.video_error": "동영상 재생에 실패했습니다" +} diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 38ca6518a..479d157f3 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -10,7 +10,7 @@ "account.media": "Media", "account.mention": "Vermeld @{name}", "account.mute": "Negeer @{name}", - "account.posts": "Berichten", + "account.posts": "Toots", "account.report": "Rapporteer @{name}", "account.requested": "Wacht op goedkeuring", "account.unblock": "Deblokkeer @{name}", @@ -18,11 +18,17 @@ "account.unfollow": "Ontvolgen", "account.unmute": "@{name} niet meer negeren", "boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Geblokkeerde gebruikers", "column.community": "Lokale tijdlijn", "column.favourites": "Favorieten", "column.follow_requests": "Volgverzoeken", - "column.home": "Jouw tijdlijn", + "column.home": "Start", "column.mutes": "Genegeerde gebruikers", "column.notifications": "Meldingen", "column.public": "Globale tijdlijn", @@ -62,7 +68,7 @@ "empty_column.community": "De lokale tijdlijn is leeg. Toot iets in het openbaar om de bal aan het rollen te krijgen!", "empty_column.hashtag": "Er is nog niks te vinden onder deze hashtag.", "empty_column.home": "Jij volgt nog niemand. Bezoek {public} of gebruik het zoekvenster om andere mensen te ontmoeten.", - "empty_column.home.inactivity": "Jouw tijdlijn is leeg. Wanneer je een tijdje inactief bent geweest wordt deze snel opnieuw aangemaakt.", + "empty_column.home.inactivity": "Deze tijdlijn is leeg. Wanneer je een tijdje inactief bent geweest wordt deze snel opnieuw aangemaakt.", "empty_column.home.public_timeline": "de globale tijdlijn", "empty_column.notifications": "Je hebt nog geen meldingen. Heb interactie met andere mensen om het gesprek aan te gaan.", "empty_column.public": "Er is hier helemaal niks! Toot iets in het openbaar of volg mensen van andere Mastodon-servers om het te vullen.", @@ -109,7 +115,7 @@ "onboarding.done": "Klaar", "onboarding.next": "Volgende", "onboarding.page_five.public_timelines": "De lokale tijdlijn toont openbare toots van iedereen op {domain}. De globale tijdlijn toont openbare toots van iedereen die door gebruikers van {domain} worden gevolgd, dus ook mensen van andere Mastodon-servers. Dit zijn de openbare tijdlijnen en vormen een uitstekende manier om nieuwe mensen te ontdekken.", - "onboarding.page_four.home": "Jouw tijdlijn laat toots zien van mensen die jij volgt.", + "onboarding.page_four.home": "Deze tijdlijn laat toots zien van mensen die jij volgt.", "onboarding.page_four.notifications": "De kolom met meldingen toont alle interacties die je met andere Mastodon-gebruikers hebt.", "onboarding.page_one.federation": "Mastodon is een netwerk van onafhankelijke servers die samen een groot sociaal netwerk vormen.", "onboarding.page_one.handle": "Je bevindt je nu op {domain}, dus is jouw volledige Mastodon-adres {handle}", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Niet op openbare tijdlijnen tonen", "privacy.unlisted.short": "Minder openbaar", "reply_indicator.cancel": "Annuleren", - "report.heading": "Rapporteren", "report.placeholder": "Extra opmerkingen", "report.submit": "Verzenden", "report.target": "Rapporteren van", @@ -162,7 +167,7 @@ "status.unmute_conversation": "Conversatie niet meer negeren", "tabs_bar.compose": "Schrijven", "tabs_bar.federated_timeline": "Globaal", - "tabs_bar.home": "Jouw tijdlijn", + "tabs_bar.home": "Start", "tabs_bar.local_timeline": "Lokaal", "tabs_bar.notifications": "Meldingen", "upload_area.title": "Hierin slepen om te uploaden", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index a3c956279..4bbf14938 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -18,6 +18,12 @@ "account.unfollow": "Avfølg", "account.unmute": "Avdemp @{name}", "boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Blokkerte brukere", "column.community": "Lokal tidslinje", "column.favourites": "Likt", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Ikke vis i offentlige tidslinjer", "privacy.unlisted.short": "Uoppført", "reply_indicator.cancel": "Avbryt", - "report.heading": "Ny rapport", "report.placeholder": "Tilleggskommentarer", "report.submit": "Send inn", "report.target": "Rapporterer", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index a2a82ae9f..2c119ef41 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -18,6 +18,12 @@ "account.unfollow": "Quitar de sègre", "account.unmute": "Quitar de rescondre @{name}", "boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Personas blocadas", "column.community": "Flux d’actualitat public local", "column.favourites": "Favorits", @@ -34,7 +40,7 @@ "compose_form.lock_disclaimer": "Vòstre compte es pas {locked}. Tot lo mond pòt vos sègre e veire los estatuts reservats als seguidors.", "compose_form.lock_disclaimer.lock": "clavat", "compose_form.placeholder": "A de qué pensatz ?", - "compose_form.privacy_disclaimer": "Vòstre estatut privat serà enviat a las personas mencionadas sus {domains}. Vos fisatz d’aqueste{domainsCount, plural, one { servidor} other {s servidors}} per divulgar pas vòstre estatut ? Los estatuts privats foncionan pas que sus las instàncias a Mastodons. Se {domains} {domainsCount, plural, one {es pas una instància a Mastodon} other {son pas d'instàncias a Mastodon}}, i aurà pas d’indicacion disent que vòstre estatut es privat e poirà èsser partejat o èsser visible a de mond pas prevists", + "compose_form.privacy_disclaimer": "Vòstre estatut privat serà enviat a las personas mencionadas sus {domains}. Vos fisatz d’aqueste {domainsCount, plural, one { servidor} other {s servidors}} per divulgar pas vòstre estatut ? Los estatuts privats foncionan pas que sus las instàncias de Mastodon. Se {domains} {domainsCount, plural, one {es pas una instància a Mastodon} other {son pas d'instàncias a Mastodon}}, i aurà pas d’indicacion disent que vòstre estatut es privat e poirà èsser partejat o èsser visible a de mond pas prevists", "compose_form.publish": "Tut", "compose_form.publish_loud": "{publish} !", "compose_form.sensitive": "Marcar lo mèdia coma sensible", @@ -51,7 +57,7 @@ "confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?", "emoji_button.activity": "Activitat", "emoji_button.flags": "Drapèus", - "emoji_button.food": "Manjar e beure", + "emoji_button.food": "Beure e manjar", "emoji_button.label": "Inserir un emoji", "emoji_button.nature": "Natura", "emoji_button.objects": "Objèctes", @@ -136,10 +142,9 @@ "privacy.unlisted.long": "Mostrar pas dins los fluxes publics", "privacy.unlisted.short": "Pas-listat", "reply_indicator.cancel": "Anullar", - "report.heading": "Nòu senhalament", "report.placeholder": "Comentaris addicionals", - "report.submit": "Mandat", - "report.target": "Senhalament", + "report.submit": "Mandar", + "report.target": "Senhalar {target}", "search.placeholder": "Recercar", "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}", "status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index bf425501f..ac63ec40f 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -6,7 +6,7 @@ "account.follow": "Obserwuj", "account.followers": "Obserwujący", "account.follows": "Obserwacje", - "account.follows_you": "Obserwują cię", + "account.follows_you": "Obserwuje cię", "account.media": "Media", "account.mention": "Wspomnij o @{name}", "account.mute": "Wycisz @{name}", @@ -18,6 +18,12 @@ "account.unfollow": "Przestań obserwować", "account.unmute": "Cofnij wyciszenie @{name}", "boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem", + "bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.", + "bundle_column_error.retry": "Spróbuj ponownie", + "bundle_column_error.title": "Błąd sieci", + "bundle_modal_error.close": "Zamknij", + "bundle_modal_error.message": "Coś poszło nie tak podczas ładowania tego składnika.", + "bundle_modal_error.retry": "Spróbuj ponownie", "column.blocks": "Zablokowani użytkownicy", "column.community": "Lokalna oś czasu", "column.favourites": "Ulubione", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Niewidoczne na publicznych osiach czasu", "privacy.unlisted.short": "Niewidoczne", "reply_indicator.cancel": "Anuluj", - "report.heading": "Zgłoś {target}", "report.placeholder": "Dodatkowe komentarze", "report.submit": "Wyślij", "report.target": "Zgłaszanie {target}", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index 5e5834a0e..b199a39ce 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -18,6 +18,12 @@ "account.unfollow": "Deixar de seguir", "account.unmute": "Não silenciar @{name}", "boost_modal.combo": "Pode clicar {combo} para não voltar a ver", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Utilizadores Bloqueados", "column.community": "Local", "column.favourites": "Favoritos", @@ -72,7 +78,6 @@ "getting_started.faq": "FAQ", "getting_started.heading": "Primeiros passos", "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}.", - "getting_started.support": "{faq} • {userguide} • {apps}", "getting_started.userguide": "User Guide", "home.column_settings.advanced": "Avançado", "home.column_settings.basic": "Básico", @@ -107,7 +112,6 @@ "notifications.column_settings.reblog": "Partilhas:", "notifications.column_settings.show": "Mostrar nas colunas", "notifications.column_settings.sound": "Reproduzir som", - "notifications.settings": "Parâmetros da listagem de Notificações", "onboarding.done": "Done", "onboarding.next": "Next", "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", @@ -138,7 +142,6 @@ "privacy.unlisted.long": "Não publicar nos feeds públicos", "privacy.unlisted.short": "Não listar", "reply_indicator.cancel": "Cancelar", - "report.heading": "Nova denúncia", "report.placeholder": "Comentários adicionais", "report.submit": "Enviar", "report.target": "Denunciar", diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json index 4ebfe0c60..b199a39ce 100644 --- a/app/javascript/mastodon/locales/pt.json +++ b/app/javascript/mastodon/locales/pt.json @@ -18,6 +18,12 @@ "account.unfollow": "Deixar de seguir", "account.unmute": "Não silenciar @{name}", "boost_modal.combo": "Pode clicar {combo} para não voltar a ver", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Utilizadores Bloqueados", "column.community": "Local", "column.favourites": "Favoritos", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Não publicar nos feeds públicos", "privacy.unlisted.short": "Não listar", "reply_indicator.cancel": "Cancelar", - "report.heading": "Nova denúncia", "report.placeholder": "Comentários adicionais", "report.submit": "Enviar", "report.target": "Denunciar", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index f561f0151..f9f48a48d 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -18,6 +18,12 @@ "account.unfollow": "Отписаться", "account.unmute": "Снять глушение", "boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Список блокировки", "column.community": "Локальная лента", "column.favourites": "Понравившееся", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Не показывать в лентах", "privacy.unlisted.short": "Скрытый", "reply_indicator.cancel": "Отмена", - "report.heading": "Новая жалоба", "report.placeholder": "Комментарий", "report.submit": "Отправить", "report.target": "Жалуемся на", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index 608d911e9..8a39beacb 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -18,6 +18,12 @@ "account.unfollow": "Unfollow", "account.unmute": "Unmute @{name}", "boost_modal.combo": "You can press {combo} to skip this next time", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Blocked users", "column.community": "Local timeline", "column.favourites": "Favourites", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Do not post to public timelines", "privacy.unlisted.short": "Unlisted", "reply_indicator.cancel": "Cancel", - "report.heading": "New report", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index 7512971f5..203e4a09e 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -18,6 +18,12 @@ "account.unfollow": "Takipten vazgeç", "account.unmute": "Sesi aç @{name}", "boost_modal.combo": "Bir dahaki sefere {combo} tuşuna basabilirsiniz", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Engellenen kullanıcılar", "column.community": "Yerel zaman tüneli", "column.favourites": "Favoriler", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Herkese açık zaman tüneline gönderme", "privacy.unlisted.short": "Listelenmemiş", "reply_indicator.cancel": "İptal", - "report.heading": "Yeni rapor", "report.placeholder": "Ek yorumlar", "report.submit": "Gönder", "report.target": "Raporlama", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index a117c854b..c0f4a8dbb 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -18,6 +18,12 @@ "account.unfollow": "Відписатися", "account.unmute": "Зняти глушення", "boost_modal.combo": "Ви можете натиснути {combo}, щоб пропустити це наступного разу", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "Заблоковані користувачі", "column.community": "Локальна стрічка", "column.favourites": "Вподобане", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Не показувати у публічних стрічках", "privacy.unlisted.short": "Прихований", "reply_indicator.cancel": "Відмінити", - "report.heading": "Нова скарга", "report.placeholder": "Додаткові коментарі", "report.submit": "Відправити", "report.target": "Скаржимося на", diff --git a/app/javascript/mastodon/locales/whitelist_ko.json b/app/javascript/mastodon/locales/whitelist_ko.json new file mode 100644 index 000000000..0d4f101c7 --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_ko.json @@ -0,0 +1,2 @@ +[ +] diff --git a/app/javascript/mastodon/locales/whitelist_zh-HK.json b/app/javascript/mastodon/locales/whitelist_zh-HK.json index d5fddc3bc..0d4f101c7 100644 --- a/app/javascript/mastodon/locales/whitelist_zh-HK.json +++ b/app/javascript/mastodon/locales/whitelist_zh-HK.json @@ -1,3 +1,2 @@ [ - "getting_started.support" ] diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 68648f2dd..998e1c8da 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -18,6 +18,12 @@ "account.unfollow": "取消关注", "account.unmute": "取消 @{name} 的静音", "boost_modal.combo": "如你想在下次路过时显示,请按{combo},", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "屏蔽用户", "column.community": "本站时间轴", "column.favourites": "赞过的嘟文", @@ -72,7 +78,6 @@ "getting_started.faq": "FAQ", "getting_started.heading": "开始使用", "getting_started.open_source_notice": "Mastodon 是一个开放源码的软件。你可以在官方 GitHub ({github}) 贡献或者回报问题。", - "getting_started.support": "{faq} • {userguide} • {apps}", "getting_started.userguide": "User Guide", "home.column_settings.advanced": "高端", "home.column_settings.basic": "基本", @@ -107,7 +112,6 @@ "notifications.column_settings.reblog": "你的嘟文被转嘟:", "notifications.column_settings.show": "在通知栏显示", "notifications.column_settings.sound": "播放音效", - "notifications.settings": "字段设置", "onboarding.done": "出发!", "onboarding.next": "下一步", "onboarding.page_five.public_timelines": "本站时间轴显示来自 {domain} 的所有人的公共嘟文。 跨站公共时间轴显示 {domain} 上的各位关注的来自所有Mastodon服务器实例上的人发表的公共嘟文。这些就是寻人好去处的公共时间轴啦。", @@ -138,7 +142,6 @@ "privacy.unlisted.long": "公开,但不在公共时间轴显示", "privacy.unlisted.short": "公开", "reply_indicator.cancel": "取消", - "report.heading": "举报", "report.placeholder": "额外消息", "report.submit": "提交", "report.target": "Reporting", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index a9844a625..1079d5429 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -18,6 +18,12 @@ "account.unfollow": "取消關注", "account.unmute": "取消 @{name} 的靜音", "boost_modal.combo": "如你想在下次路過這顯示,請按{combo},", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "封鎖用戶", "column.community": "本站時間軸", "column.favourites": "喜歡的文章", @@ -72,7 +78,6 @@ "getting_started.faq": "常見問題", "getting_started.heading": "開始使用", "getting_started.open_source_notice": "Mastodon(萬象)是一個開放源碼的軟件。你可以在官方 GitHub ({github}) 貢獻或者回報問題。", - "getting_started.support": "{faq} • {userguide} • {apps}", "getting_started.userguide": "使用指南", "home.column_settings.advanced": "進階", "home.column_settings.basic": "基本", @@ -107,7 +112,6 @@ "notifications.column_settings.reblog": "轉推你的文章:", "notifications.column_settings.show": "在通知欄顯示", "notifications.column_settings.sound": "播放音效", - "notifications.settings": "欄位設定", "onboarding.done": "開始使用", "onboarding.next": "繼續", "onboarding.page_five.public_timelines": "「本站時間軸」顯示在 {domain} 各用戶的公開文章。「跨站時間軸」顯示在 {domain} 各人關注的所有用戶(包括其他服務站)的公開文章。這些都是「公共時間軸」,是認識新朋友的好地方。", @@ -138,7 +142,6 @@ "privacy.unlisted.long": "公開,但不在公共時間軸顯示", "privacy.unlisted.short": "公開", "reply_indicator.cancel": "取消", - "report.heading": "舉報", "report.placeholder": "額外訊息", "report.submit": "提交", "report.target": "舉報", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 5497becf0..6240b8879 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -18,6 +18,12 @@ "account.unfollow": "取消關注", "account.unmute": "不再消音 @{name}", "boost_modal.combo": "下次你可以按 {combo} 來跳過", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", "column.blocks": "封鎖的使用者", "column.community": "本地時間軸", "column.favourites": "最愛", @@ -72,7 +78,6 @@ "getting_started.faq": "FAQ", "getting_started.heading": "馬上開始", "getting_started.open_source_notice": "Mastodon 是開源軟體。你可以在 GitHub {github} 上做出貢獻或是回報問題。", - "getting_started.support": "{faq} • {userguide} • {apps}", "getting_started.userguide": "使用者指南", "home.column_settings.advanced": "進階", "home.column_settings.basic": "基本", @@ -107,7 +112,6 @@ "notifications.column_settings.reblog": "轉推:", "notifications.column_settings.show": "顯示在欄位中", "notifications.column_settings.sound": "播放音效", - "notifications.settings": "欄位設定", "onboarding.done": "完成", "onboarding.next": "下一步", "onboarding.page_five.public_timelines": "本地時間軸顯示 {domain} 上所有人的公開貼文。聯盟時間軸顯示 {domain} 上所有人關注的公開貼文。這就是公開時間軸,發現新朋友的好地方。", @@ -138,7 +142,6 @@ "privacy.unlisted.long": "不要貼到公開時間軸", "privacy.unlisted.short": "不列出來", "reply_indicator.cancel": "取消", - "report.heading": "新的通報", "report.placeholder": "更多訊息", "report.submit": "送出", "report.target": "通報中", diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js index f14b6a825..90c2c5da2 100644 --- a/app/javascript/mastodon/main.js +++ b/app/javascript/mastodon/main.js @@ -1,9 +1,5 @@ const perf = require('./performance'); -// import default stylesheet with variables -require('font-awesome/css/font-awesome.css'); -require('mastodon-application-style'); - function onDomContentLoaded(callback) { if (document.readyState !== 'loading') { callback(); @@ -20,6 +16,14 @@ function main() { require.context('../images/', true); + if (window.history && history.replaceState) { + const { pathname, search, hash } = window.location; + const path = pathname + search + hash; + if (!(/^\/web[$/]/).test(path)) { + history.replaceState(null, document.title, `/web${path}`); + } + } + onDomContentLoaded(() => { const mountNode = document.getElementById('mastodon'); const props = JSON.parse(mountNode.getAttribute('data-props')); diff --git a/app/javascript/mastodon/reducers/accounts.js b/app/javascript/mastodon/reducers/accounts.js index 7b7074317..4d7c3adc9 100644 --- a/app/javascript/mastodon/reducers/accounts.js +++ b/app/javascript/mastodon/reducers/accounts.js @@ -44,7 +44,7 @@ import { FAVOURITED_STATUSES_EXPAND_SUCCESS, } from '../actions/favourites'; import { STORE_HYDRATE } from '../actions/store'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, fromJS } from 'immutable'; const normalizeAccount = (state, account) => { account = { ...account }; @@ -53,7 +53,7 @@ const normalizeAccount = (state, account) => { delete account.following_count; delete account.statuses_count; - return state.set(account.id, Immutable.fromJS(account)); + return state.set(account.id, fromJS(account)); }; const normalizeAccounts = (state, accounts) => { @@ -82,7 +82,7 @@ const normalizeAccountsFromStatuses = (state, statuses) => { return state; }; -const initialState = Immutable.Map(); +const initialState = ImmutableMap(); export default function accounts(state = initialState, action) { switch(action.type) { diff --git a/app/javascript/mastodon/reducers/accounts_counters.js b/app/javascript/mastodon/reducers/accounts_counters.js index eb8a4f83d..4423e1b50 100644 --- a/app/javascript/mastodon/reducers/accounts_counters.js +++ b/app/javascript/mastodon/reducers/accounts_counters.js @@ -46,9 +46,9 @@ import { FAVOURITED_STATUSES_EXPAND_SUCCESS, } from '../actions/favourites'; import { STORE_HYDRATE } from '../actions/store'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, fromJS } from 'immutable'; -const normalizeAccount = (state, account) => state.set(account.id, Immutable.fromJS({ +const normalizeAccount = (state, account) => state.set(account.id, fromJS({ followers_count: account.followers_count, following_count: account.following_count, statuses_count: account.statuses_count, @@ -80,12 +80,12 @@ const normalizeAccountsFromStatuses = (state, statuses) => { return state; }; -const initialState = Immutable.Map(); +const initialState = ImmutableMap(); export default function accountsCounters(state = initialState, action) { switch(action.type) { case STORE_HYDRATE: - return state.merge(action.state.get('accounts').map(item => Immutable.fromJS({ + return state.merge(action.state.get('accounts').map(item => fromJS({ followers_count: item.get('followers_count'), following_count: item.get('following_count'), statuses_count: item.get('statuses_count'), diff --git a/app/javascript/mastodon/reducers/alerts.js b/app/javascript/mastodon/reducers/alerts.js index aaea9775f..089d920c3 100644 --- a/app/javascript/mastodon/reducers/alerts.js +++ b/app/javascript/mastodon/reducers/alerts.js @@ -3,14 +3,14 @@ import { ALERT_DISMISS, ALERT_CLEAR, } from '../actions/alerts'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -const initialState = Immutable.List([]); +const initialState = ImmutableList([]); export default function alerts(state = initialState, action) { switch(action.type) { case ALERT_SHOW: - return state.push(Immutable.Map({ + return state.push(ImmutableMap({ key: state.size > 0 ? state.last().get('key') + 1 : 0, title: action.title, message: action.message, diff --git a/app/javascript/mastodon/reducers/cards.js b/app/javascript/mastodon/reducers/cards.js index 3c9395011..4d86b0d7e 100644 --- a/app/javascript/mastodon/reducers/cards.js +++ b/app/javascript/mastodon/reducers/cards.js @@ -1,13 +1,13 @@ import { STATUS_CARD_FETCH_SUCCESS } from '../actions/cards'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, fromJS } from 'immutable'; -const initialState = Immutable.Map(); +const initialState = ImmutableMap(); export default function cards(state = initialState, action) { switch(action.type) { case STATUS_CARD_FETCH_SUCCESS: - return state.set(action.id, Immutable.fromJS(action.card)); + return state.set(action.id, fromJS(action.card)); default: return state; } diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 21d801f2a..6ac7b4b4a 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -25,12 +25,12 @@ import { } from '../actions/compose'; import { TIMELINE_DELETE } from '../actions/timelines'; import { STORE_HYDRATE } from '../actions/store'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import uuid from '../uuid'; -const initialState = Immutable.Map({ +const initialState = ImmutableMap({ mounted: false, - advanced_options: Immutable.Map({ + advanced_options: ImmutableMap({ do_not_federate: false, }), sensitive: false, @@ -44,20 +44,21 @@ const initialState = Immutable.Map({ is_submitting: false, is_uploading: false, progress: 0, - media_attachments: Immutable.List(), + media_attachments: ImmutableList(), suggestion_token: null, - suggestions: Immutable.List(), + suggestions: ImmutableList(), me: null, - default_advanced_options: Immutable.Map({ + default_advanced_options: ImmutableMap({ do_not_federate: false, }), default_privacy: 'public', + default_sensitive: false, resetFileKey: Math.floor((Math.random() * 0x10000)), idempotencyKey: null, }); function statusToTextMentions(state, status) { - let set = Immutable.OrderedSet([]); + let set = ImmutableOrderedSet([]); let me = state.get('me'); if (status.getIn(['account', 'id']) !== me) { @@ -83,6 +84,8 @@ function clearAll(state) { }; function appendMedia(state, media) { + const prevSize = state.get('media_attachments').size; + return state.withMutations(map => { map.update('media_attachments', list => list.push(media)); map.set('is_uploading', false); @@ -90,6 +93,10 @@ function appendMedia(state, media) { map.update('text', oldText => `${oldText.trim()} ${media.get('text_url')}`); map.set('focusDate', new Date()); map.set('idempotencyKey', uuid()); + + if (prevSize === 0 && state.get('default_sensitive')) { + map.set('sensitive', true); + } }); }; @@ -112,7 +119,7 @@ const insertSuggestion = (state, position, token, completion) => { return state.withMutations(map => { map.update('text', oldText => `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`); map.set('suggestion_token', null); - map.update('suggestions', Immutable.List(), list => list.clear()); + map.update('suggestions', ImmutableList(), list => list.clear()); map.set('focusDate', new Date()); map.set('idempotencyKey', uuid()); }); @@ -180,7 +187,7 @@ export default function compose(state = initialState, action) { map.set('in_reply_to', action.status.get('id')); map.set('text', statusToTextMentions(state, action.status)); map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy'))); - map.set('advanced_options', new Immutable.Map({ + map.set('advanced_options', new ImmutableMap({ do_not_federate: /👁\ufe0f?<\/p>$/.test(action.status.get('content')), })); map.set('focusDate', new Date()); @@ -216,7 +223,7 @@ export default function compose(state = initialState, action) { map.set('is_uploading', true); }); case COMPOSE_UPLOAD_SUCCESS: - return appendMedia(state, Immutable.fromJS(action.media)); + return appendMedia(state, fromJS(action.media)); case COMPOSE_UPLOAD_FAIL: return state.set('is_uploading', false); case COMPOSE_UPLOAD_UNDO: @@ -229,9 +236,9 @@ export default function compose(state = initialState, action) { .set('focusDate', new Date()) .set('idempotencyKey', uuid()); case COMPOSE_SUGGESTIONS_CLEAR: - return state.update('suggestions', Immutable.List(), list => list.clear()).set('suggestion_token', null); + return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null); case COMPOSE_SUGGESTIONS_READY: - return state.set('suggestions', Immutable.List(action.accounts.map(item => item.id))).set('suggestion_token', action.token); + return state.set('suggestions', ImmutableList(action.accounts.map(item => item.id))).set('suggestion_token', action.token); case COMPOSE_SUGGESTION_SELECT: return insertSuggestion(state, action.position, action.token, action.completion); case TIMELINE_DELETE: diff --git a/app/javascript/mastodon/reducers/contexts.js b/app/javascript/mastodon/reducers/contexts.js index 8a24f5f7a..9bfc09aa7 100644 --- a/app/javascript/mastodon/reducers/contexts.js +++ b/app/javascript/mastodon/reducers/contexts.js @@ -1,10 +1,10 @@ import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; import { TIMELINE_DELETE } from '../actions/timelines'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; -const initialState = Immutable.Map({ - ancestors: Immutable.Map(), - descendants: Immutable.Map(), +const initialState = ImmutableMap({ + ancestors: ImmutableMap(), + descendants: ImmutableMap(), }); const normalizeContext = (state, id, ancestors, descendants) => { @@ -18,12 +18,12 @@ const normalizeContext = (state, id, ancestors, descendants) => { }; const deleteFromContexts = (state, id) => { - state.getIn(['descendants', id], Immutable.List()).forEach(descendantId => { - state = state.updateIn(['ancestors', descendantId], Immutable.List(), list => list.filterNot(itemId => itemId === id)); + state.getIn(['descendants', id], ImmutableList()).forEach(descendantId => { + state = state.updateIn(['ancestors', descendantId], ImmutableList(), list => list.filterNot(itemId => itemId === id)); }); - state.getIn(['ancestors', id], Immutable.List()).forEach(ancestorId => { - state = state.updateIn(['descendants', ancestorId], Immutable.List(), list => list.filterNot(itemId => itemId === id)); + state.getIn(['ancestors', id], ImmutableList()).forEach(ancestorId => { + state = state.updateIn(['descendants', ancestorId], ImmutableList(), list => list.filterNot(itemId => itemId === id)); }); state = state.deleteIn(['descendants', id]).deleteIn(['ancestors', id]); @@ -34,7 +34,7 @@ const deleteFromContexts = (state, id) => { export default function contexts(state = initialState, action) { switch(action.type) { case CONTEXT_FETCH_SUCCESS: - return normalizeContext(state, action.id, Immutable.fromJS(action.ancestors), Immutable.fromJS(action.descendants)); + return normalizeContext(state, action.id, fromJS(action.ancestors), fromJS(action.descendants)); case TIMELINE_DELETE: return deleteFromContexts(state, action.id); default: diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index a3f5cb58e..35f30f601 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -1,7 +1,6 @@ import { combineReducers } from 'redux-immutable'; import timelines from './timelines'; import meta from './meta'; -import compose from './compose'; import alerts from './alerts'; import { loadingBarReducer } from 'react-redux-loading-bar'; import modal from './modal'; @@ -9,21 +8,21 @@ import user_lists from './user_lists'; import accounts from './accounts'; import accounts_counters from './accounts_counters'; import statuses from './statuses'; -import media_attachments from './media_attachments'; import relationships from './relationships'; -import search from './search'; -import notifications from './notifications'; import settings from './settings'; import local_settings from '../../glitch/reducers/local_settings'; import status_lists from './status_lists'; import cards from './cards'; import reports from './reports'; import contexts from './contexts'; +import compose from './compose'; +import search from './search'; +import media_attachments from './media_attachments'; +import notifications from './notifications'; -export default combineReducers({ +const reducers = { timelines, meta, - compose, alerts, loadingBar: loadingBarReducer, modal, @@ -31,14 +30,17 @@ export default combineReducers({ status_lists, accounts, accounts_counters, - media_attachments, statuses, relationships, - search, - notifications, settings, local_settings, cards, reports, contexts, -}); + compose, + search, + media_attachments, + notifications, +}; + +export default combineReducers(reducers); diff --git a/app/javascript/mastodon/reducers/media_attachments.js b/app/javascript/mastodon/reducers/media_attachments.js index 85bea4f0b..24119f628 100644 --- a/app/javascript/mastodon/reducers/media_attachments.js +++ b/app/javascript/mastodon/reducers/media_attachments.js @@ -1,7 +1,7 @@ import { STORE_HYDRATE } from '../actions/store'; -import Immutable from 'immutable'; +import { Map as ImmutableMap } from 'immutable'; -const initialState = Immutable.Map({ +const initialState = ImmutableMap({ accept_content_types: [], }); diff --git a/app/javascript/mastodon/reducers/meta.js b/app/javascript/mastodon/reducers/meta.js index 1551228ec..119ef9d8f 100644 --- a/app/javascript/mastodon/reducers/meta.js +++ b/app/javascript/mastodon/reducers/meta.js @@ -1,7 +1,7 @@ import { STORE_HYDRATE } from '../actions/store'; -import Immutable from 'immutable'; +import { Map as ImmutableMap } from 'immutable'; -const initialState = Immutable.Map({ +const initialState = ImmutableMap({ streaming_api_base_url: null, access_token: null, me: null, diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 0c1cf5b0f..0063d24e4 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -11,10 +11,10 @@ import { } from '../actions/notifications'; import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts'; import { TIMELINE_DELETE } from '../actions/timelines'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -const initialState = Immutable.Map({ - items: Immutable.List(), +const initialState = ImmutableMap({ + items: ImmutableList(), next: null, top: true, unread: 0, @@ -22,7 +22,7 @@ const initialState = Immutable.Map({ isLoading: true, }); -const notificationToMap = notification => Immutable.Map({ +const notificationToMap = notification => ImmutableMap({ id: notification.id, type: notification.type, account: notification.account.id, @@ -46,7 +46,7 @@ const normalizeNotification = (state, notification) => { }; const normalizeNotifications = (state, notifications, next) => { - let items = Immutable.List(); + let items = ImmutableList(); const loaded = state.get('loaded'); notifications.forEach((n, i) => { @@ -64,7 +64,7 @@ const normalizeNotifications = (state, notifications, next) => { }; const appendNormalizedNotifications = (state, notifications, next) => { - let items = Immutable.List(); + let items = ImmutableList(); notifications.forEach((n, i) => { items = items.set(i, notificationToMap(n)); @@ -110,7 +110,7 @@ export default function notifications(state = initialState, action) { case ACCOUNT_BLOCK_SUCCESS: return filterNotifications(state, action.relationship); case NOTIFICATIONS_CLEAR: - return state.set('items', Immutable.List()).set('next', null); + return state.set('items', ImmutableList()).set('next', null); case TIMELINE_DELETE: return deleteByStatus(state, action.id); default: diff --git a/app/javascript/mastodon/reducers/relationships.js b/app/javascript/mastodon/reducers/relationships.js index b6607860c..c7b04a668 100644 --- a/app/javascript/mastodon/reducers/relationships.js +++ b/app/javascript/mastodon/reducers/relationships.js @@ -11,9 +11,9 @@ import { DOMAIN_BLOCK_SUCCESS, DOMAIN_UNBLOCK_SUCCESS, } from '../actions/domain_blocks'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, fromJS } from 'immutable'; -const normalizeRelationship = (state, relationship) => state.set(relationship.id, Immutable.fromJS(relationship)); +const normalizeRelationship = (state, relationship) => state.set(relationship.id, fromJS(relationship)); const normalizeRelationships = (state, relationships) => { relationships.forEach(relationship => { @@ -23,7 +23,7 @@ const normalizeRelationships = (state, relationships) => { return state; }; -const initialState = Immutable.Map(); +const initialState = ImmutableMap(); export default function relationships(state = initialState, action) { switch(action.type) { diff --git a/app/javascript/mastodon/reducers/reports.js b/app/javascript/mastodon/reducers/reports.js index ad35eaa05..283c5b6f5 100644 --- a/app/javascript/mastodon/reducers/reports.js +++ b/app/javascript/mastodon/reducers/reports.js @@ -7,13 +7,13 @@ import { REPORT_STATUS_TOGGLE, REPORT_COMMENT_CHANGE, } from '../actions/reports'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable'; -const initialState = Immutable.Map({ - new: Immutable.Map({ +const initialState = ImmutableMap({ + new: ImmutableMap({ isSubmitting: false, account_id: null, - status_ids: Immutable.Set(), + status_ids: ImmutableSet(), comment: '', }), }); @@ -26,14 +26,14 @@ export default function reports(state = initialState, action) { map.setIn(['new', 'account_id'], action.account.get('id')); if (state.getIn(['new', 'account_id']) !== action.account.get('id')) { - map.setIn(['new', 'status_ids'], action.status ? Immutable.Set([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : Immutable.Set()); + map.setIn(['new', 'status_ids'], action.status ? ImmutableSet([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : ImmutableSet()); map.setIn(['new', 'comment'], ''); } else { - map.updateIn(['new', 'status_ids'], Immutable.Set(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id')))); + map.updateIn(['new', 'status_ids'], ImmutableSet(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id')))); } }); case REPORT_STATUS_TOGGLE: - return state.updateIn(['new', 'status_ids'], Immutable.Set(), set => { + return state.updateIn(['new', 'status_ids'], ImmutableSet(), set => { if (action.checked) { return set.add(action.statusId); } @@ -50,7 +50,7 @@ export default function reports(state = initialState, action) { case REPORT_SUBMIT_SUCCESS: return state.withMutations(map => { map.setIn(['new', 'account_id'], null); - map.setIn(['new', 'status_ids'], Immutable.Set()); + map.setIn(['new', 'status_ids'], ImmutableSet()); map.setIn(['new', 'comment'], ''); map.setIn(['new', 'isSubmitting'], false); }); diff --git a/app/javascript/mastodon/reducers/search.js b/app/javascript/mastodon/reducers/search.js index 0a3adac05..08d90e4e8 100644 --- a/app/javascript/mastodon/reducers/search.js +++ b/app/javascript/mastodon/reducers/search.js @@ -5,13 +5,13 @@ import { SEARCH_SHOW, } from '../actions/search'; import { COMPOSE_MENTION, COMPOSE_REPLY } from '../actions/compose'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -const initialState = Immutable.Map({ +const initialState = ImmutableMap({ value: '', submitted: false, hidden: false, - results: Immutable.Map(), + results: ImmutableMap(), }); export default function search(state = initialState, action) { @@ -21,7 +21,7 @@ export default function search(state = initialState, action) { case SEARCH_CLEAR: return state.withMutations(map => { map.set('value', ''); - map.set('results', Immutable.Map()); + map.set('results', ImmutableMap()); map.set('submitted', false); map.set('hidden', false); }); @@ -31,10 +31,10 @@ export default function search(state = initialState, action) { case COMPOSE_MENTION: return state.set('hidden', true); case SEARCH_FETCH_SUCCESS: - return state.set('results', Immutable.Map({ - accounts: Immutable.List(action.results.accounts.map(item => item.id)), - statuses: Immutable.List(action.results.statuses.map(item => item.id)), - hashtags: Immutable.List(action.results.hashtags), + 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), })).set('submitted', true); default: return state; diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index 9a15a1fe3..1bdee7356 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -1,40 +1,40 @@ import { SETTING_CHANGE } from '../actions/settings'; import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE } from '../actions/columns'; import { STORE_HYDRATE } from '../actions/store'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, fromJS } from 'immutable'; import uuid from '../uuid'; -const initialState = Immutable.Map({ +const initialState = ImmutableMap({ onboarded: false, layout: 'auto', - home: Immutable.Map({ - shows: Immutable.Map({ + home: ImmutableMap({ + shows: ImmutableMap({ reblog: true, reply: true, }), - regex: Immutable.Map({ + regex: ImmutableMap({ body: '', }), }), - notifications: Immutable.Map({ - alerts: Immutable.Map({ + notifications: ImmutableMap({ + alerts: ImmutableMap({ follow: true, favourite: true, reblog: true, mention: true, }), - shows: Immutable.Map({ + shows: ImmutableMap({ follow: true, favourite: true, reblog: true, mention: true, }), - sounds: Immutable.Map({ + sounds: ImmutableMap({ follow: true, favourite: true, reblog: true, @@ -42,20 +42,20 @@ const initialState = Immutable.Map({ }), }), - community: Immutable.Map({ - regex: Immutable.Map({ + community: ImmutableMap({ + regex: ImmutableMap({ body: '', }), }), - public: Immutable.Map({ - regex: Immutable.Map({ + public: ImmutableMap({ + regex: ImmutableMap({ body: '', }), }), }); -const defaultColumns = Immutable.fromJS([ +const defaultColumns = fromJS([ { id: 'COMPOSE', uuid: uuid(), params: {} }, { id: 'HOME', uuid: uuid(), params: {} }, { id: 'NOTIFICATIONS', uuid: uuid(), params: {} }, @@ -83,7 +83,7 @@ export default function settings(state = initialState, action) { case SETTING_CHANGE: return state.setIn(action.key, action.value); case COLUMN_ADD: - return state.update('columns', list => list.push(Immutable.fromJS({ id: action.id, uuid: uuid(), params: action.params }))); + return state.update('columns', list => list.push(fromJS({ id: action.id, uuid: uuid(), params: action.params }))); case COLUMN_REMOVE: return state.update('columns', list => list.filterNot(item => item.get('uuid') === action.uuid)); case COLUMN_MOVE: diff --git a/app/javascript/mastodon/reducers/status_lists.js b/app/javascript/mastodon/reducers/status_lists.js index 7d00f6d30..bbc973302 100644 --- a/app/javascript/mastodon/reducers/status_lists.js +++ b/app/javascript/mastodon/reducers/status_lists.js @@ -2,13 +2,13 @@ import { FAVOURITED_STATUSES_FETCH_SUCCESS, FAVOURITED_STATUSES_EXPAND_SUCCESS, } from '../actions/favourites'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -const initialState = Immutable.Map({ - favourites: Immutable.Map({ +const initialState = ImmutableMap({ + favourites: ImmutableMap({ next: null, loaded: false, - items: Immutable.List(), + items: ImmutableList(), }), }); @@ -16,7 +16,7 @@ const normalizeList = (state, listType, statuses, next) => { return state.update(listType, listMap => listMap.withMutations(map => { map.set('next', next); map.set('loaded', true); - map.set('items', Immutable.List(statuses.map(item => item.id))); + map.set('items', ImmutableList(statuses.map(item => item.id))); })); }; diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index 691135ff7..b1b1d0988 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -33,7 +33,7 @@ import { FAVOURITED_STATUSES_EXPAND_SUCCESS, } from '../actions/favourites'; import { SEARCH_FETCH_SUCCESS } from '../actions/search'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, fromJS } from 'immutable'; const normalizeStatus = (state, status) => { if (!status) { @@ -51,7 +51,7 @@ const normalizeStatus = (state, status) => { const searchContent = [status.spoiler_text, status.content].join(' ').replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n'); normalStatus.search_index = new DOMParser().parseFromString(searchContent, 'text/html').documentElement.textContent; - return state.update(status.id, Immutable.Map(), map => map.mergeDeep(Immutable.fromJS(normalStatus))); + return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus))); }; const normalizeStatuses = (state, statuses) => { @@ -82,7 +82,7 @@ const filterStatuses = (state, relationship) => { return state; }; -const initialState = Immutable.Map(); +const initialState = ImmutableMap(); export default function statuses(state = initialState, action) { switch(action.type) { diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index 1b738a16a..065e89f96 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -15,25 +15,25 @@ import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS, } from '../actions/accounts'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; -const initialState = Immutable.Map(); +const initialState = ImmutableMap(); -const initialTimeline = Immutable.Map({ +const initialTimeline = ImmutableMap({ unread: 0, online: false, top: true, loaded: false, isLoading: false, next: false, - items: Immutable.List(), + items: ImmutableList(), }); const normalizeTimeline = (state, timeline, statuses, next) => { - const ids = Immutable.List(statuses.map(status => status.get('id'))); + const ids = ImmutableList(statuses.map(status => status.get('id'))); const wasLoaded = state.getIn([timeline, 'loaded']); const hadNext = state.getIn([timeline, 'next']); - const oldIds = state.getIn([timeline, 'items'], Immutable.List()); + const oldIds = state.getIn([timeline, 'items'], ImmutableList()); return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { mMap.set('loaded', true); @@ -44,8 +44,8 @@ const normalizeTimeline = (state, timeline, statuses, next) => { }; const appendNormalizedTimeline = (state, timeline, statuses, next) => { - const ids = Immutable.List(statuses.map(status => status.get('id'))); - const oldIds = state.getIn([timeline, 'items'], Immutable.List()); + const ids = ImmutableList(statuses.map(status => status.get('id'))); + const oldIds = state.getIn([timeline, 'items'], ImmutableList()); return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { mMap.set('isLoading', false); @@ -56,7 +56,7 @@ const appendNormalizedTimeline = (state, timeline, statuses, next) => { const updateTimeline = (state, timeline, status, references) => { const top = state.getIn([timeline, 'top']); - const ids = state.getIn([timeline, 'items'], Immutable.List()); + const ids = state.getIn([timeline, 'items'], ImmutableList()); const includesId = ids.includes(status.get('id')); const unread = state.getIn([timeline, 'unread'], 0); @@ -124,11 +124,11 @@ export default function timelines(state = initialState, action) { case TIMELINE_EXPAND_FAIL: return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false)); case TIMELINE_REFRESH_SUCCESS: - return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.next); + return normalizeTimeline(state, action.timeline, fromJS(action.statuses), action.next); case TIMELINE_EXPAND_SUCCESS: - return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.next); + return appendNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next); case TIMELINE_UPDATE: - return updateTimeline(state, action.timeline, Immutable.fromJS(action.status), action.references); + return updateTimeline(state, action.timeline, fromJS(action.status), action.references); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf); case ACCOUNT_BLOCK_SUCCESS: diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js index 83bf1be1b..8db18c5dc 100644 --- a/app/javascript/mastodon/reducers/user_lists.js +++ b/app/javascript/mastodon/reducers/user_lists.js @@ -20,22 +20,22 @@ import { MUTES_FETCH_SUCCESS, MUTES_EXPAND_SUCCESS, } from '../actions/mutes'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -const initialState = Immutable.Map({ - followers: Immutable.Map(), - following: Immutable.Map(), - reblogged_by: Immutable.Map(), - favourited_by: Immutable.Map(), - follow_requests: Immutable.Map(), - blocks: Immutable.Map(), - mutes: Immutable.Map(), +const initialState = ImmutableMap({ + followers: ImmutableMap(), + following: ImmutableMap(), + reblogged_by: ImmutableMap(), + favourited_by: ImmutableMap(), + follow_requests: ImmutableMap(), + blocks: ImmutableMap(), + mutes: ImmutableMap(), }); const normalizeList = (state, type, id, accounts, next) => { - return state.setIn([type, id], Immutable.Map({ + return state.setIn([type, id], ImmutableMap({ next, - items: Immutable.List(accounts.map(item => item.id)), + items: ImmutableList(accounts.map(item => item.id)), })); }; @@ -56,22 +56,22 @@ export default function userLists(state = initialState, action) { case FOLLOWING_EXPAND_SUCCESS: return appendToList(state, 'following', action.id, action.accounts, action.next); case REBLOGS_FETCH_SUCCESS: - return state.setIn(['reblogged_by', action.id], Immutable.List(action.accounts.map(item => item.id))); + return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id))); case FAVOURITES_FETCH_SUCCESS: - return state.setIn(['favourited_by', action.id], Immutable.List(action.accounts.map(item => item.id))); + return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id))); case FOLLOW_REQUESTS_FETCH_SUCCESS: - return state.setIn(['follow_requests', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next); + return state.setIn(['follow_requests', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next); case FOLLOW_REQUESTS_EXPAND_SUCCESS: return state.updateIn(['follow_requests', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next); case FOLLOW_REQUEST_AUTHORIZE_SUCCESS: case FOLLOW_REQUEST_REJECT_SUCCESS: return state.updateIn(['follow_requests', 'items'], list => list.filterNot(item => item === action.id)); case BLOCKS_FETCH_SUCCESS: - return state.setIn(['blocks', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next); + return state.setIn(['blocks', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next); case BLOCKS_EXPAND_SUCCESS: return state.updateIn(['blocks', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next); case MUTES_FETCH_SUCCESS: - return state.setIn(['mutes', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); + return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); case MUTES_EXPAND_SUCCESS: return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); default: diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index 07d9a2629..d26d1b727 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -1,5 +1,5 @@ import { createSelector } from 'reselect'; -import Immutable from 'immutable'; +import { List as ImmutableList } from 'immutable'; const getAccountBase = (state, id) => state.getIn(['accounts', id], null); const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null); @@ -73,10 +73,10 @@ export const makeGetNotification = () => { }; export const getAccountGallery = createSelector([ - (state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], Immutable.List()), + (state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], ImmutableList()), state => state.get('statuses'), ], (statusIds, statuses) => { - let medias = Immutable.List(); + let medias = ImmutableList(); statusIds.forEach(statusId => { const status = statuses.get(statusId); diff --git a/app/javascript/packs/common.js b/app/javascript/packs/common.js index 9d63d8f98..a0cb91ae4 100644 --- a/app/javascript/packs/common.js +++ b/app/javascript/packs/common.js @@ -1,2 +1,7 @@ import { start } from 'rails-ujs'; + +// import default stylesheet with variables +require('font-awesome/css/font-awesome.css'); +require('mastodon-application-style'); + start(); diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index 8a3ae0b3c..06cc1b53a 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -5,6 +5,9 @@ import emojify from '../mastodon/emoji'; import { getLocale } from '../mastodon/locales'; import loadPolyfills from '../mastodon/load_polyfills'; import { processBio } from '../glitch/util/bio_metadata'; +import TimelineContainer from '../mastodon/containers/timeline_container'; +import React from 'react'; +import ReactDOM from 'react-dom'; require.context('../images/', true); @@ -37,6 +40,13 @@ function loaded() { const datetime = new Date(content.getAttribute('datetime')); content.textContent = relativeFormat.format(datetime);; }); + + const mountNode = document.getElementById('mastodon-timeline'); + + if (mountNode !== null) { + const props = JSON.parse(mountNode.getAttribute('data-props')); + ReactDOM.render(<TimelineContainer {...props} />, mountNode); + } } function main() { @@ -54,8 +64,12 @@ function main() { } }); - delegate(document, '.media-spoiler', 'click', ({ target }) => { - target.style.display = 'none'; + delegate(document, '.activity-stream .media-spoiler-wrapper .media-spoiler', 'click', function() { + this.parentNode.classList.add('media-spoiler-wrapper__visible'); + }); + + delegate(document, '.activity-stream .media-spoiler-wrapper .spoiler-button', 'click', function() { + this.parentNode.classList.remove('media-spoiler-wrapper__visible'); }); delegate(document, '.webapp-btn', 'click', ({ target, button }) => { diff --git a/app/javascript/styles/about.scss b/app/javascript/styles/about.scss index 7145d0092..5716163be 100644 --- a/app/javascript/styles/about.scss +++ b/app/javascript/styles/about.scss @@ -116,10 +116,6 @@ .wrapper { padding: 20px; } - - .features-list { - display: block; - } } } @@ -299,80 +295,438 @@ } } -.features-list { +.features-list__row { display: flex; - margin-bottom: 20px; + padding: 10px 0; + justify-content: space-between; + + &:first-child { + padding-top: 0; + } - .features-list__column { - flex: 1 1 0; + .visual { + flex: 0 0 auto; + display: flex; + align-items: center; + margin-left: 15px; - ul { - list-style: none; + .fa { + display: block; + color: $ui-primary-color; + font-size: 48px; } + } - li { - margin: 0; + .text { + font-size: 16px; + line-height: 30px; + color: lighten($ui-base-color, 26%); + + h6 { + font-weight: 500; + color: $ui-primary-color; } } } -.screenshot-with-signup { - display: flex; - margin-bottom: 20px; - - .mascot { - flex: 1 1 auto; - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; +.landing-page { + .header-wrapper { + padding-top: 15px; + background: $ui-base-color; + background: linear-gradient(150deg, lighten($ui-base-color, 8%), $ui-base-color); + position: relative; - img { - display: block; + .mascot-container { + max-width: 800px; margin: 0 auto; - max-width: 100%; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 100%; + } + + .mascot { + position: absolute; + bottom: -14px; + width: auto; height: auto; + left: 60px; + z-index: 3; + } + } + + p, + li { + font: inherit; + font-weight: inherit; + margin-bottom: 0; + } + + .header { + line-height: 30px; + overflow: hidden; + + .container { + display: flex; + justify-content: space-between; + } + + .hero { + margin-top: 50px; + align-items: center; + position: relative; + + .floats { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + + img { + position: absolute; + transition: all 0.1s linear; + animation-name: floating; + animation-duration: 1.7s; + animation-iteration-count: infinite; + animation-direction: alternate; + animation-timing-function: linear; + z-index: 2; + } + + .float-1 { + height: 170px; + right: -120px; + bottom: 0; + } + + .float-2 { + height: 100px; + right: 210px; + bottom: 0; + animation-delay: 0.2s; + } + + .float-3 { + height: 140px; + right: 110px; + top: -30px; + animation-delay: 0.1s; + } + } + + .simple_form, + .closed-registrations-message { + background: darken($ui-base-color, 4%); + width: 280px; + padding: 15px 20px; + border-radius: 4px 4px 0 0; + line-height: initial; + position: relative; + z-index: 4; + + .actions { + margin-bottom: 0; + + button, + .button, + .block-button { + margin-bottom: 0; + } + } + } + + .heading { + position: relative; + z-index: 4; + padding-bottom: 150px; + } + + .closed-registrations-message { + min-height: 330px; + display: flex; + flex-direction: column; + justify-content: space-between; + } + } + + ul { + list-style: none; + margin: 0; + + li { + display: inline-block; + vertical-align: bottom; + margin: 0; + + &:first-child a { + padding-left: 0; + } + + &:last-child a { + padding-right: 0; + } + } + } + + .links { + position: relative; + z-index: 4; + + a { + display: flex; + justify-content: center; + align-items: center; + color: $ui-primary-color; + text-decoration: none; + padding: 12px 16px; + line-height: 32px; + font-family: 'mastodon-font-display', sans-serif; + font-weight: 500; + font-size: 14px; + + &:hover { + color: $ui-secondary-color; + } + } + + .brand { + a { + padding-left: 0; + color: $white; + } + + img { + width: 32px; + height: 32px; + margin-right: 10px; + } + } + } + } + + .container { + width: 100%; + box-sizing: border-box; + max-width: 800px; + margin: 0 auto; + } + + .wrapper { + max-width: 800px; + margin: 0 auto; + padding: 0; + } + + .learn-more-cta { + background: darken($ui-base-color, 4%); + padding: 50px 0; + } + + h3 { + font-family: 'mastodon-font-display', sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 500; + margin-bottom: 20px; + color: $ui-primary-color; + } + + p { + font-size: 16px; + line-height: 30px; + color: lighten($ui-base-color, 26%); + } + + .features { + padding: 50px 0; + + .container { + display: flex; } } - .simple_form, - .closed-registrations-message { - width: 300px; + #mastodon-timeline { + -webkit-overflow-scrolling: touch; + -ms-overflow-style: -ms-autohiding-scrollbar; + font-family: 'mastodon-font-sans-serif', sans-serif; + font-size: 13px; + line-height: 18px; + font-weight: 400; + color: $primary-text-color; + width: 330px; + margin-right: 30px; flex: 0 0 auto; - background: rgba(darken($ui-base-color, 7%), 0.5); - padding: 14px; - border-radius: 4px; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.4); + background: $ui-base-color; + overflow: hidden; + box-shadow: 0 0 6px rgba($black, 0.1); - .actions { - margin-bottom: 0; + .column { + padding: 0; + border-radius: 4px; + overflow: hidden; + height: 100%; } - .info { - text-align: center; + .scrollable { + height: 400px; + } + + p { + font-size: inherit; + line-height: inherit; + font-weight: inherit; + color: $primary-text-color; a { color: $ui-secondary-color; + text-decoration: none; } } } - @media screen and (max-width: 625px) { - .mascot { + .about-mastodon { + max-width: 675px; + + p { + margin-bottom: 20px; + } + + .features-list { + margin-top: 20px; + } + } + + em { + display: inline; + margin: 0; + padding: 0; + font-weight: 500; + background: transparent; + font-family: inherit; + font-size: inherit; + line-height: inherit; + color: $ui-primary-color; + } + + h1 { + font-family: 'mastodon-font-display', sans-serif; + font-size: 26px; + line-height: 30px; + margin-bottom: 0; + font-weight: 500; + color: $ui-secondary-color; + + small { + font-family: 'mastodon-font-sans-serif', sans-serif; + display: block; + font-size: 18px; + font-weight: 400; + color: lighten($ui-base-color, 26%); + } + } + + .footer-links { + padding-bottom: 50px; + text-align: right; + color: lighten($ui-base-color, 26%); + + p { + font-size: 14px; + } + + a { + color: inherit; + text-decoration: underline; + } + } + + @media screen and (max-width: 800px) { + .container { + padding: 0 20px; + } + + .header-wrapper .mascot { + left: 20px; + } + } + + @media screen and (max-width: 689px) { + .header-wrapper .mascot { display: none; } + } - .simple_form, - .closed-registrations-message { - flex: auto; + @media screen and (max-width: 675px) { + .header-wrapper { + padding-top: 0; + } + + .header .container, + .features .container { + display: block; + } + + .links { + padding-top: 15px; + background: darken($ui-base-color, 4%); + } + + .header { + padding-top: 0; + + .hero { + margin-top: 30px; + padding: 0; + + .heading { + padding-bottom: 20px; + } + } + + .floats { + display: none; + } + + .heading, + .nav { + text-align: center; + } + + .heading h1 { + padding: 30px 0; + } + + .hero { + .simple_form, + .closed-registrations-message { + background: darken($ui-base-color, 8%); + width: 100%; + border-radius: 0; + box-sizing: border-box; + } + } + } + + #mastodon-timeline { + height: 70vh; + width: 100%; + margin-bottom: 50px; } } } -.closed-registrations-message { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; +@keyframes floating { + from { + transform: translate(0, 0); + } + + 65% { + transform: translate(0, 4px); + } + + to { + transform: translate(0, -0); + } } diff --git a/app/javascript/styles/basics.scss b/app/javascript/styles/basics.scss index 70a5be367..182ea36a4 100644 --- a/app/javascript/styles/basics.scss +++ b/app/javascript/styles/basics.scss @@ -1,6 +1,6 @@ body { font-family: 'mastodon-font-sans-serif', sans-serif; - background: $ui-base-color url('../images/background-photo.jpg'); + background: $ui-base-color; background-size: cover; background-attachment: fixed; font-size: 13px; @@ -11,6 +11,8 @@ body { text-rendering: optimizelegibility; font-feature-settings: "kern"; text-size-adjust: none; + -webkit-tap-highlight-color: rgba(0,0,0,0); + -webkit-tap-highlight-color: transparent; &.app-body { position: fixed; @@ -20,6 +22,11 @@ body { background: $ui-base-color; } + &.about-body { + background: darken($ui-base-color, 8%); + padding-bottom: 0; + } + &.embed { background: transparent; margin: 0; @@ -61,3 +68,18 @@ button { align-items: center; justify-content: center; } + +.system-font { + // system-ui => standard property (Chrome/Android WebView 56+, Opera 43+, Safari 11+) + // -apple-system => Safari <11 specific + // BlinkMacSystemFont => Chrome <56 on macOS specific + // Segoe UI => Windows 7/8/10 + // Oxygen => KDE + // Ubuntu => Unity/Ubuntu + // Cantarell => GNOME + // Fira Sans => Firefox OS + // Droid Sans => Older Androids (<4.0) + // Helvetica Neue => Older macOS <10.11 + // mastodon-font-sans-serif => web-font (Roboto) fallback and newer Androids (>=4.0) + font-family: system-ui, -apple-system,BlinkMacSystemFont, "Segoe UI","Oxygen", "Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",mastodon-font-sans-serif, sans-serif; +} diff --git a/app/javascript/styles/boost.scss b/app/javascript/styles/boost.scss index 9cad7a4f5..e44df2ea4 100644 --- a/app/javascript/styles/boost.scss +++ b/app/javascript/styles/boost.scss @@ -1,12 +1,15 @@ -@function url-friendly-colour($colour) { - @return '%23' + str-slice('#{$colour}', 2, -1) +@function hex-color($color) { + @if type-of($color) == 'color' { + $color: str-slice(ie-hex-str($color), 4); + } + @return '%23' + unquote($color) } button.icon-button i.fa-retweet { - background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour(lighten($ui-base-color, 26%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour($ui-highlight-color)}' stroke-width='0'/></svg>"); + background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color(lighten($ui-base-color, 26%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color($ui-highlight-color)}' stroke-width='0'/></svg>"); &:hover { - background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour(lighten($ui-base-color, 33%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour($ui-highlight-color)}' stroke-width='0'/></svg>"); + background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color(lighten($ui-base-color, 33%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color($ui-highlight-color)}' stroke-width='0'/></svg>"); } } @@ -23,3 +26,11 @@ button.icon-button.disabled i.fa-retweet { background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{lighten($ui-base-color, 16%)}' stroke-width='0'/></svg>"); } } + +// Mastodon gave us this one, but I'm not sure if it's better. - @kibi@glitch.social + +/* +button.icon-button.disabled i.fa-retweet { + background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color(lighten($ui-base-color, 13%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color($ui-highlight-color)}' stroke-width='0'/></svg>"); +} +*/ diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 49d3c9873..6cca3666a 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -42,8 +42,38 @@ cursor: default; } + &.button-alternative { + font-size: 16px; + line-height: 36px; + height: auto; + color: $ui-base-color; + background: $ui-primary-color; + text-transform: none; + padding: 4px 16px; + + &:active, + &:focus, + &:hover { + background-color: lighten($ui-primary-color, 4%); + } + } + &.button-secondary { - // + font-size: 16px; + line-height: 36px; + height: auto; + color: $ui-primary-color; + text-transform: none; + background: transparent; + padding: 3px 15px; + border: 1px solid $ui-primary-color; + + &:active, + &:focus, + &:hover { + border-color: lighten($ui-primary-color, 4%); + color: lighten($ui-primary-color, 4%); + } } &.button--block { @@ -1327,6 +1357,20 @@ height: 100%; background-image: none; } + + &.image-loader--amorphous { + position: static; + + .image-loader__preview-canvas { + display: none; + } + + .image-loader__img { + position: static; + width: auto; + height: auto; + } + } } .navigation-bar { @@ -1463,6 +1507,23 @@ .columns-area { padding: 0; } + + .react-swipeable-view-container .columns-area { + height: calc(100% - 20px) !important; + } +} + +.react-swipeable-view-container { + &, + .columns-area, + .drawer, + .column { + height: 100%; + } +} + +.react-swipeable-view-container > * { + height: 100%; } .column { @@ -1510,8 +1571,7 @@ .drawer__tab { display: block; flex: 1 1 auto; - padding: 15px; - padding-bottom: 13px; + padding: 15px 5px 13px; color: $ui-primary-color; text-decoration: none; text-align: center; @@ -2537,7 +2597,8 @@ button.icon-button.active i.fa-retweet { vertical-align: middle; } -.empty-column-indicator { +.empty-column-indicator, +.error-column { color: lighten($ui-base-color, 20%); background: $ui-base-color; text-align: center; @@ -2563,6 +2624,10 @@ button.icon-button.active i.fa-retweet { } } +.error-column { + flex-direction: column; +} + @keyframes pulse { 0% { opacity: 1; @@ -3206,7 +3271,7 @@ button.icon-button.active i.fa-retweet { video { max-width: 80vw; max-height: 80vh; - width: auto; + width: 100%; height: auto; } @@ -3214,6 +3279,7 @@ button.icon-button.active i.fa-retweet { canvas { display: block; background: url('../images/void.png') repeat; + object-fit: contain; } } @@ -3224,7 +3290,8 @@ button.icon-button.active i.fa-retweet { z-index: 100; } -.onboarding-modal { +.onboarding-modal, +.error-modal { background: $ui-secondary-color; color: $ui-base-color; border-radius: 8px; @@ -3238,6 +3305,26 @@ button.icon-button.active i.fa-retweet { width: 80vw; max-width: 520px; max-height: 420px; + + .react-swipeable-view-container > div { + width: 100%; + height: 100%; + box-sizing: border-box; + padding: 25px; + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + display: flex; + user-select: text; + } +} + +.error-modal__body { + height: 80vh; + width: 80vw; + max-width: 520px; + max-height: 420px; position: relative; & > div { @@ -3258,6 +3345,14 @@ button.icon-button.active i.fa-retweet { } } +.error-modal__body { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; +} + @media screen and (max-width: 550px) { .onboarding-modal { width: 100%; @@ -3274,7 +3369,8 @@ button.icon-button.active i.fa-retweet { } } -.onboarding-modal__paginator { +.onboarding-modal__paginator, +.error-modal__footer { flex: 0 0 auto; background: darken($ui-secondary-color, 8%); display: flex; @@ -3284,7 +3380,8 @@ button.icon-button.active i.fa-retweet { min-width: 33px; } - .onboarding-modal__nav { + .onboarding-modal__nav, + .error-modal__nav { color: darken($ui-secondary-color, 34%); background-color: transparent; border: 0; @@ -3307,6 +3404,10 @@ button.icon-button.active i.fa-retweet { } } +.error-modal__footer { + justify-content: center; +} + .onboarding-modal__dots { flex: 1 1 auto; display: flex; @@ -3676,6 +3777,7 @@ button.icon-button.active i.fa-retweet { .report-modal__statuses { min-height: 20vh; + max-height: 40vh; overflow-y: auto; overflow-x: hidden; } @@ -3831,8 +3933,7 @@ button.icon-button.active i.fa-retweet { .media-gallery__item-thumbnail { cursor: zoom-in; - display: flex; - align-items: center; + display: block; text-decoration: none; width: 100%; height: 100%; diff --git a/app/javascript/styles/containers.scss b/app/javascript/styles/containers.scss index 68f73e0c0..44d4c1118 100644 --- a/app/javascript/styles/containers.scss +++ b/app/javascript/styles/containers.scss @@ -10,52 +10,36 @@ } .logo-container { - max-width: 400px; margin: 100px auto; - margin-bottom: 0; - cursor: default; + margin-bottom: 50px; @media screen and (max-width: 360px) { margin: 30px auto; } h1 { - display: block; - text-align: center; - color: $primary-text-color; - font-size: 48px; - font-weight: 500; + display: flex; + justify-content: center; + align-items: center; img { - display: block; - margin: 20px auto; - width: 180px; - height: 180px; + width: 32px; + height: 32px; + margin-right: 10px; } a { - color: inherit; + display: flex; + justify-content: center; + align-items: center; + color: $primary-text-color; text-decoration: none; outline: 0; - - img { - opacity: 0.8; - transition: opacity 0.8s ease; - } - - &:hover { - img { - opacity: 1; - transition-duration: 0.2s; - } - } - } - - small { - display: block; - font-size: 12px; - font-weight: 400; - font-family: 'mastodon-font-monospace', monospace; + padding: 12px 16px; + line-height: 32px; + font-family: 'mastodon-font-display', sans-serif; + font-weight: 500; + font-size: 14px; } } } diff --git a/app/javascript/styles/fonts/montserrat.scss b/app/javascript/styles/fonts/montserrat.scss index e4012ab02..206f1865e 100644 --- a/app/javascript/styles/fonts/montserrat.scss +++ b/app/javascript/styles/fonts/montserrat.scss @@ -7,3 +7,11 @@ font-weight: 400; font-style: normal; } + +@font-face { + font-family: 'mastodon-font-display'; + src: local('Montserrat'), + url('../fonts/montserrat/Montserrat-Medium.ttf') format('truetype'); + font-weight: 500; + font-style: normal; +} diff --git a/app/javascript/styles/forms.scss b/app/javascript/styles/forms.scss index 7a181f36b..e1de36d55 100644 --- a/app/javascript/styles/forms.scss +++ b/app/javascript/styles/forms.scss @@ -24,6 +24,20 @@ code { p.hint { margin-bottom: 15px; + color: lighten($ui-base-color, 32%); + + &.subtle-hint { + text-align: center; + font-size: 12px; + line-height: 18px; + margin-top: 15px; + margin-bottom: 0; + color: lighten($ui-base-color, 26%); + + a { + color: $ui-primary-color; + } + } } strong { @@ -43,10 +57,7 @@ code { } } - .input.file, - .input.select, - .input.radio_buttons, - .input.check_boxes { + .input.with_label { padding: 15px 0; margin-bottom: 0; @@ -57,6 +68,44 @@ code { display: block; padding-top: 5px; } + + &.boolean { + padding: initial; + margin-bottom: initial; + + .label_input > label { + font-family: inherit; + font-size: 14px; + color: $primary-text-color; + display: block; + width: auto; + } + + label.checkbox { + position: relative; + padding-left: 25px; + flex: 1 1 auto; + } + } + } + + .input.with_block_label { + & > label { + font-family: inherit; + font-size: 16px; + color: $primary-text-color; + display: block; + padding-top: 5px; + } + + .hint { + margin-bottom: 15px; + } + + li { + float: left; + width: 50%; + } } .fields-group { @@ -92,7 +141,7 @@ code { input[type=checkbox] { position: absolute; left: 0; - top: 1px; + top: 5px; margin: 0; } @@ -102,6 +151,29 @@ code { } } + .check_boxes { + .checkbox { + label { + font-family: inherit; + font-size: 14px; + color: $primary-text-color; + display: block; + width: auto; + position: relative; + padding-top: 5px; + padding-left: 25px; + flex: 1 1 auto; + } + + input[type=checkbox] { + position: absolute; + left: 0; + top: 5px; + margin: 0; + } + } + } + input[type=text], input[type=number], input[type=email], @@ -197,8 +269,6 @@ code { &:active, &:focus { - position: relative; - top: 1px; background-color: darken($ui-highlight-color, 5%); } @@ -219,6 +289,27 @@ code { select { font-size: 16px; } + + .input-with-append { + position: relative; + + .input input { + padding-right: 127px; + } + + .append { + position: absolute; + right: 0; + top: 0; + padding: 7px 4px; + padding-bottom: 9px; + font-size: 16px; + color: lighten($ui-base-color, 26%); + font-family: inherit; + pointer-events: none; + cursor: default; + } + } } .flash-message { @@ -240,7 +331,7 @@ code { text-align: center; a { - color: $primary-text-color; + color: $ui-primary-color; text-decoration: none; &:hover { @@ -357,21 +448,11 @@ code { } } -.user_filtered_languages { - & > label { - font-family: inherit; - font-size: 16px; - color: $primary-text-color; - display: block; - padding-top: 5px; - } - - .hint { - margin-bottom: 15px; - } +.post-follow-actions { + text-align: center; + color: $ui-primary-color; - li { - float: left; - width: 50%; + div { + margin-bottom: 4px; } } diff --git a/app/javascript/styles/stream_entries.scss b/app/javascript/styles/stream_entries.scss index 8ecf31fd5..a9111d7c9 100644 --- a/app/javascript/styles/stream_entries.scss +++ b/app/javascript/styles/stream_entries.scss @@ -326,6 +326,18 @@ } } + .media-spoiler-wrapper { + &.media-spoiler-wrapper__visible { + .media-spoiler { + display: none; + } + + .spoiler-button { + display: block; + } + } + } + .pre-header { padding: 14px 0; padding-left: (48px + 14px * 2); diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 1885eff26..35b18fa1b 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -38,9 +38,7 @@ class FeedManager end def trim(type, account_id) - return unless redis.zcard(key(type, account_id)) > FeedManager::MAX_ITEMS - last = redis.zrevrange(key(type, account_id), FeedManager::MAX_ITEMS - 1, FeedManager::MAX_ITEMS - 1) - redis.zremrangebyscore(key(type, account_id), '-inf', "(#{last.last}") + redis.zremrangebyrank(key(type, account_id), '0', (-(FeedManager::MAX_ITEMS + 1)).to_s) end def push_update_required?(timeline_type, account_id) diff --git a/app/lib/inline_rabl_scope.rb b/app/lib/inline_rabl_scope.rb deleted file mode 100644 index 26adcb03a..000000000 --- a/app/lib/inline_rabl_scope.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -class InlineRablScope - include RoutingHelper - - def initialize(account) - @account = account - end - - def current_user - @account.try(:user) - end - - def current_account - @account - end -end diff --git a/app/lib/inline_renderer.rb b/app/lib/inline_renderer.rb index 8e04ad1d5..7cd9758ec 100644 --- a/app/lib/inline_renderer.rb +++ b/app/lib/inline_renderer.rb @@ -1,13 +1,33 @@ # frozen_string_literal: true class InlineRenderer - def self.render(status, current_account, template) - Rabl::Renderer.new( - template, - status, - view_path: 'app/views', - format: :json, - scope: InlineRablScope.new(current_account) - ).render + def initialize(object, current_account, template) + @object = object + @current_account = current_account + @template = template + end + + def render + case @template + when :status + serializer = REST::StatusSerializer + when :notification + serializer = REST::NotificationSerializer + else + return + end + + serializable_resource = ActiveModelSerializers::SerializableResource.new(@object, serializer: serializer, scope: current_user, scope_name: :current_user) + serializable_resource.as_json + end + + def self.render(object, current_account, template) + new(object, current_account, template).render + end + + private + + def current_user + @current_account&.user end end diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index af264bbd5..e0e92b19d 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -18,9 +18,11 @@ class UserSettingsDecorator user.settings['notification_emails'] = merged_notification_emails user.settings['interactions'] = merged_interactions user.settings['default_privacy'] = default_privacy_preference + user.settings['default_sensitive'] = default_sensitive_preference user.settings['boost_modal'] = boost_modal_preference user.settings['delete_modal'] = delete_modal_preference user.settings['auto_play_gif'] = auto_play_gif_preference + user.settings['system_font_ui'] = system_font_ui_preference end def merged_notification_emails @@ -35,6 +37,10 @@ class UserSettingsDecorator settings['setting_default_privacy'] end + def default_sensitive_preference + boolean_cast_setting 'setting_default_sensitive' + end + def boost_modal_preference boolean_cast_setting 'setting_boost_modal' end @@ -43,6 +49,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_delete_modal' end + def system_font_ui_preference + boolean_cast_setting 'setting_system_font_ui' + end + def auto_play_gif_preference boolean_cast_setting 'setting_auto_play_gif' end diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index fc19a6d40..fd9223533 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class AdminMailer < ApplicationMailer + helper StreamEntriesHelper + def new_report(recipient, report) @report = report @me = recipient diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 2e730c19b..95b770ff1 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ApplicationMailer < ActionMailer::Base - default from: ENV.fetch('SMTP_FROM_ADDRESS') { 'notifications@localhost' } layout 'mailer' helper :instance diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 6abf9c9ca..1517c027e 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class UserMailer < Devise::Mailer - default from: ENV.fetch('SMTP_FROM_ADDRESS') { 'notifications@localhost' } layout 'mailer' helper :instance diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb index 4a412ee3d..b4f169649 100644 --- a/app/models/concerns/remotable.rb +++ b/app/models/concerns/remotable.rb @@ -6,11 +6,16 @@ module Remotable included do attachment_definitions.each_key do |attachment_name| - attribute_name = "#{attachment_name}_remote_url".to_sym - method_name = "#{attribute_name}=".to_sym + attribute_name = "#{attachment_name}_remote_url".to_sym + method_name = "#{attribute_name}=".to_sym + alt_method_name = "reset_#{attachment_name}!".to_sym define_method method_name do |url| - parsed_url = Addressable::URI.parse(url).normalize + begin + parsed_url = Addressable::URI.parse(url).normalize + rescue Addressable::URI::InvalidURIError + return + end return if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty? || self[attribute_name] == url @@ -26,10 +31,20 @@ module Remotable send("#{attachment_name}_file_name=", filename) self[attribute_name] = url if has_attribute?(attribute_name) - rescue HTTP::TimeoutError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError => e + rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError => e Rails.logger.debug "Error fetching remote #{attachment_name}: #{e}" + nil end end + + define_method alt_method_name do + url = self[attribute_name] + + return if url.blank? + + self[attribute_name] = '' + send(method_name, url) + end end end end diff --git a/app/models/context.rb b/app/models/context.rb new file mode 100644 index 000000000..cc667999e --- /dev/null +++ b/app/models/context.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Context < ActiveModelSerializers::Model + attributes :ancestors, :descendants +end diff --git a/app/models/feed.rb b/app/models/feed.rb index 5125e51ff..beb4a8de3 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -20,8 +20,7 @@ class Feed max_id = '+inf' if max_id.blank? since_id = '-inf' if since_id.blank? unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:last).map(&:to_i) - status_map = Status.where(id: unhydrated).cache_ids.map { |s| [s.id, s] }.to_h - unhydrated.map { |id| status_map[id] }.compact + Status.where(id: unhydrated).cache_ids end def from_database(limit, max_id, since_id) diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb new file mode 100644 index 000000000..c3a04ba65 --- /dev/null +++ b/app/models/form/admin_settings.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class Form::AdminSettings + include ActiveModel::Model + + delegate( + :site_contact_username, + :site_contact_username=, + :site_contact_email, + :site_contact_email=, + :site_title, + :site_title=, + :site_description, + :site_description=, + :site_extended_description, + :site_extended_description=, + :site_terms, + :site_terms=, + :open_registrations, + :open_registrations=, + :closed_registrations_message, + :closed_registrations_message=, + :open_deletion, + :open_deletion=, + :timeline_preview, + :timeline_preview=, + to: Setting + ) +end diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 340109ab6..1e8c6d00a 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -18,6 +18,8 @@ # file_meta :json # +require 'mime/types' + class MediaAttachment < ApplicationRecord self.inheritance_column = nil diff --git a/app/models/search.rb b/app/models/search.rb new file mode 100644 index 000000000..676c2a7f8 --- /dev/null +++ b/app/models/search.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Search < ActiveModelSerializers::Model + attributes :accounts, :statuses, :hashtags +end diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb index 02a918e8a..887e3e3bd 100644 --- a/app/models/session_activation.rb +++ b/app/models/session_activation.rb @@ -69,9 +69,7 @@ class SessionActivation < ApplicationRecord def assign_access_token superapp = Doorkeeper::Application.find_by(superapp: true) - return if superapp.nil? - - self.access_token = Doorkeeper::AccessToken.create!(application_id: superapp.id, + self.access_token = Doorkeeper::AccessToken.create!(application_id: superapp&.id, resource_owner_id: user_id, scopes: 'read write follow', expires_in: Doorkeeper.configuration.access_token_expires_in, diff --git a/app/models/user.rb b/app/models/user.rb index c31a0c644..c80115a08 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -79,6 +79,10 @@ class User < ApplicationRecord settings.default_privacy || (account.locked? ? 'private' : 'public') end + def setting_default_sensitive + settings.default_sensitive + end + def setting_boost_modal settings.boost_modal end @@ -91,6 +95,10 @@ class User < ApplicationRecord settings.auto_play_gif end + def setting_system_font_ui + settings.system_font_ui + end + def activate_session(request) session_activations.activate(session_id: SecureRandom.hex, user_agent: request.user_agent, diff --git a/app/presenters/account_relationships_presenter.rb b/app/presenters/account_relationships_presenter.rb new file mode 100644 index 000000000..657807863 --- /dev/null +++ b/app/presenters/account_relationships_presenter.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AccountRelationshipsPresenter + attr_reader :following, :followed_by, :blocking, + :muting, :requested, :domain_blocking + + def initialize(account_ids, current_account_id) + @following = Account.following_map(account_ids, current_account_id) + @followed_by = Account.followed_by_map(account_ids, current_account_id) + @blocking = Account.blocking_map(account_ids, current_account_id) + @muting = Account.muting_map(account_ids, current_account_id) + @requested = Account.requested_map(account_ids, current_account_id) + @domain_blocking = Account.domain_blocking_map(account_ids, current_account_id) + end +end diff --git a/app/presenters/initial_state_presenter.rb b/app/presenters/initial_state_presenter.rb new file mode 100644 index 000000000..75fef28a8 --- /dev/null +++ b/app/presenters/initial_state_presenter.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class InitialStatePresenter < ActiveModelSerializers::Model + attributes :settings, :token, :current_account, :admin +end diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb index c96eaa1cb..19bedcc21 100644 --- a/app/presenters/instance_presenter.rb +++ b/app/presenters/instance_presenter.rb @@ -5,8 +5,10 @@ class InstancePresenter :closed_registrations_message, :site_contact_email, :open_registrations, + :site_title, :site_description, :site_extended_description, + :site_terms, to: Setting ) diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb new file mode 100644 index 000000000..caf00791a --- /dev/null +++ b/app/presenters/status_relationships_presenter.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class StatusRelationshipsPresenter + attr_reader :reblogs_map, :favourites_map, :mutes_map + + def initialize(statuses, current_account_id = nil) + if current_account_id.nil? + @reblogs_map = {} + @favourites_map = {} + @mutes_map = {} + else + status_ids = statuses.compact.flat_map { |s| [s.id, s.reblog_of_id] }.uniq + conversation_ids = statuses.compact.map(&:conversation_id).compact.uniq + @reblogs_map = Status.reblogs_map(status_ids, current_account_id) + @favourites_map = Status.favourites_map(status_ids, current_account_id) + @mutes_map = Status.mutes_map(conversation_ids, current_account_id) + end + end +end diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb new file mode 100644 index 000000000..6751c9411 --- /dev/null +++ b/app/serializers/initial_state_serializer.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +class InitialStateSerializer < ActiveModel::Serializer + attributes :meta, :compose, :accounts, + :media_attachments, :settings + + def meta + store = { + streaming_api_base_url: Rails.configuration.x.streaming_api_base_url, + access_token: object.token, + locale: I18n.locale, + domain: Rails.configuration.x.local_domain, + admin: object.admin&.id, + } + + if object.current_account + store[:me] = object.current_account.id + store[:boost_modal] = object.current_account.user.setting_boost_modal + store[:delete_modal] = object.current_account.user.setting_delete_modal + store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif + store[:system_font_ui] = object.current_account.user.setting_system_font_ui + end + + store + end + + def compose + store = {} + + if object.current_account + store[:me] = object.current_account.id + store[:default_privacy] = object.current_account.user.setting_default_privacy + store[:default_sensitive] = object.current_account.user.setting_default_sensitive + end + + store + end + + def accounts + store = {} + store[object.current_account.id] = ActiveModelSerializers::SerializableResource.new(object.current_account, serializer: REST::AccountSerializer) if object.current_account + store[object.admin.id] = ActiveModelSerializers::SerializableResource.new(object.admin, serializer: REST::AccountSerializer) if object.admin + store + end + + def media_attachments + { accept_content_types: MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES } + end +end diff --git a/app/serializers/oembed_serializer.rb b/app/serializers/oembed_serializer.rb new file mode 100644 index 000000000..78376d253 --- /dev/null +++ b/app/serializers/oembed_serializer.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class OEmbedSerializer < ActiveModel::Serializer + include RoutingHelper + include ActionView::Helpers::TagHelper + + attributes :type, :version, :title, :author_name, + :author_url, :provider_name, :provider_url, + :cache_age, :html, :width, :height + + def type + 'rich' + end + + def version + '1.0' + end + + def author_name + object.account.display_name.presence || object.account.username + end + + def author_url + account_url(object.account) + end + + def provider_name + Rails.configuration.x.local_domain + end + + def provider_url + root_url + end + + def cache_age + 86_400 + end + + def html + tag :iframe, + src: embed_account_stream_entry_url(object.account, object), + style: 'width: 100%; overflow: hidden', + frameborder: '0', + scrolling: 'no', + width: width, + height: height + end + + def width + instance_options[:width] + end + + def height + instance_options[:height] + end +end diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb new file mode 100644 index 000000000..012a4fd18 --- /dev/null +++ b/app/serializers/rest/account_serializer.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class REST::AccountSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :id, :username, :acct, :display_name, :locked, :created_at, + :note, :url, :avatar, :avatar_static, :header, :header_static, + :followers_count, :following_count, :statuses_count + + def note + Formatter.instance.simplified_format(object) + end + + def url + TagManager.instance.url_for(object) + end + + def avatar + full_asset_url(object.avatar_original_url) + end + + def avatar_static + full_asset_url(object.avatar_static_url) + end + + def header + full_asset_url(object.header_original_url) + end + + def header_static + full_asset_url(object.header_static_url) + end +end diff --git a/app/serializers/rest/application_serializer.rb b/app/serializers/rest/application_serializer.rb new file mode 100644 index 000000000..868a62f1e --- /dev/null +++ b/app/serializers/rest/application_serializer.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class REST::ApplicationSerializer < ActiveModel::Serializer + attributes :id, :name, :website, :redirect_uri, + :client_id, :client_secret + + def client_id + object.uid + end + + def client_secret + object.secret + end +end diff --git a/app/serializers/rest/context_serializer.rb b/app/serializers/rest/context_serializer.rb new file mode 100644 index 000000000..44515c85d --- /dev/null +++ b/app/serializers/rest/context_serializer.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class REST::ContextSerializer < ActiveModel::Serializer + has_many :ancestors, serializer: REST::StatusSerializer + has_many :descendants, serializer: REST::StatusSerializer +end diff --git a/app/serializers/rest/credential_account_serializer.rb b/app/serializers/rest/credential_account_serializer.rb new file mode 100644 index 000000000..870d8b71f --- /dev/null +++ b/app/serializers/rest/credential_account_serializer.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class REST::CredentialAccountSerializer < REST::AccountSerializer + attributes :source + + def source + user = object.user + { + privacy: user.setting_default_privacy, + sensitive: user.setting_default_sensitive, + note: object.note, + } + end +end diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb new file mode 100644 index 000000000..8e32f9cb3 --- /dev/null +++ b/app/serializers/rest/instance_serializer.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class REST::InstanceSerializer < ActiveModel::Serializer + attributes :uri, :title, :description, :email, + :version, :urls + + def uri + Rails.configuration.x.local_domain + end + + def title + Setting.site_title + end + + def description + Setting.site_description + end + + def email + Setting.site_contact_email + end + + def version + Mastodon::Version.to_s + end + + def urls + { streaming_api: Rails.configuration.x.streaming_api_base_url } + end +end diff --git a/app/serializers/rest/media_attachment_serializer.rb b/app/serializers/rest/media_attachment_serializer.rb new file mode 100644 index 000000000..9055b8db4 --- /dev/null +++ b/app/serializers/rest/media_attachment_serializer.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class REST::MediaAttachmentSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :id, :type, :url, :preview_url, + :remote_url, :text_url, :meta + + def url + full_asset_url(object.file.url(:original)) + end + + def preview_url + full_asset_url(object.file.url(:small)) + end + + def text_url + object.local? ? medium_url(object) : nil + end + + def meta + object.file.meta + end +end diff --git a/app/serializers/rest/notification_serializer.rb b/app/serializers/rest/notification_serializer.rb new file mode 100644 index 000000000..f95d099a3 --- /dev/null +++ b/app/serializers/rest/notification_serializer.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class REST::NotificationSerializer < ActiveModel::Serializer + attributes :id, :type, :created_at + + belongs_to :from_account, key: :account, serializer: REST::AccountSerializer + belongs_to :target_status, key: :status, if: :status_type?, serializer: REST::StatusSerializer + + def status_type? + [:favourite, :reblog, :mention].include?(object.type) + end +end diff --git a/app/serializers/rest/preview_card_serializer.rb b/app/serializers/rest/preview_card_serializer.rb new file mode 100644 index 000000000..9c460332c --- /dev/null +++ b/app/serializers/rest/preview_card_serializer.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class REST::PreviewCardSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :url, :title, :description, :type, + :author_name, :author_url, :provider_name, + :provider_url, :html, :width, :height, + :image + + def image + object.image? ? full_asset_url(object.image.url(:original)) : nil + end +end diff --git a/app/serializers/rest/relationship_serializer.rb b/app/serializers/rest/relationship_serializer.rb new file mode 100644 index 000000000..1d431aa1b --- /dev/null +++ b/app/serializers/rest/relationship_serializer.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class REST::RelationshipSerializer < ActiveModel::Serializer + attributes :id, :following, :followed_by, :blocking, + :muting, :requested, :domain_blocking + + def following + instance_options[:relationships].following[object.id] || false + end + + def followed_by + instance_options[:relationships].followed_by[object.id] || false + end + + def blocking + instance_options[:relationships].blocking[object.id] || false + end + + def muting + instance_options[:relationships].muting[object.id] || false + end + + def requested + instance_options[:relationships].requested[object.id] || false + end + + def domain_blocking + instance_options[:relationships].domain_blocking[object.id] || false + end +end diff --git a/app/serializers/rest/report_serializer.rb b/app/serializers/rest/report_serializer.rb new file mode 100644 index 000000000..0c6bd6556 --- /dev/null +++ b/app/serializers/rest/report_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class REST::ReportSerializer < ActiveModel::Serializer + attributes :id, :action_taken +end diff --git a/app/serializers/rest/search_serializer.rb b/app/serializers/rest/search_serializer.rb new file mode 100644 index 000000000..157f543ae --- /dev/null +++ b/app/serializers/rest/search_serializer.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class REST::SearchSerializer < ActiveModel::Serializer + attributes :hashtags + + has_many :accounts, serializer: REST::AccountSerializer + has_many :statuses, serializer: REST::StatusSerializer + + def hashtags + object.hashtags.map(&:name) + end +end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb new file mode 100644 index 000000000..246b12a90 --- /dev/null +++ b/app/serializers/rest/status_serializer.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +class REST::StatusSerializer < ActiveModel::Serializer + attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id, + :sensitive, :spoiler_text, :visibility, :language, + :uri, :content, :url, :reblogs_count, :favourites_count + + attribute :favourited, if: :current_user? + attribute :reblogged, if: :current_user? + attribute :muted, if: :current_user? + + belongs_to :reblog, serializer: REST::StatusSerializer + belongs_to :application + belongs_to :account, serializer: REST::AccountSerializer + + has_many :media_attachments, serializer: REST::MediaAttachmentSerializer + has_many :mentions + has_many :tags + + def current_user? + !current_user.nil? + end + + def uri + TagManager.instance.uri_for(object) + end + + def content + Formatter.instance.format(object) + end + + def url + TagManager.instance.url_for(object) + end + + def favourited + if instance_options && instance_options[:relationships] + instance_options[:relationships].favourites_map[object.id] || false + else + current_user.account.favourited?(object) + end + end + + def reblogged + if instance_options && instance_options[:relationships] + instance_options[:relationships].reblogs_map[object.id] || false + else + current_user.account.reblogged?(object) + end + end + + def muted + if instance_options && instance_options[:relationships] + instance_options[:relationships].mutes_map[object.conversation_id] || false + else + current_user.account.muting_conversation?(object.conversation) + end + end + + class ApplicationSerializer < ActiveModel::Serializer + attributes :name, :website + end + + class MentionSerializer < ActiveModel::Serializer + attributes :id, :username, :url, :acct + + def id + object.account_id + end + + def username + object.account_username + end + + def url + TagManager.instance.url_for(object.account) + end + + def acct + object.account_acct + end + end + + class TagSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :name, :url + + def url + tag_url(object) + end + end +end diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 3b74696d5..47a47a735 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -54,7 +54,7 @@ class FanOutOnWriteService < BaseService end def render_anonymous_payload(status) - @payload = InlineRenderer.render(status, nil, 'api/v1/statuses/show') + @payload = InlineRenderer.render(status, nil, :status) @payload = Oj.dump(event: :update, payload: @payload) end diff --git a/app/services/fetch_atom_service.rb b/app/services/fetch_atom_service.rb index 8f42db0aa..d430b22e9 100644 --- a/app/services/fetch_atom_service.rb +++ b/app/services/fetch_atom_service.rb @@ -20,6 +20,10 @@ class FetchAtomService < BaseService process_html(fetch(url)) rescue OpenSSL::SSL::SSLError => e Rails.logger.debug "SSL error: #{e}" + nil + rescue HTTP::ConnectionError => e + Rails.logger.debug "HTTP ConnectionError: #{e}" + nil end private diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index c2df7b2f0..6ef3abb66 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -18,6 +18,8 @@ class FetchLinkCardService < BaseService return if res.code != 200 || res.mime_type != 'text/html' attempt_opengraph(card, url) unless attempt_oembed(card, url) + rescue HTTP::ConnectionError, OpenSSL::SSL::SSLError + nil end private @@ -82,7 +84,13 @@ class FetchLinkCardService < BaseService return if response.code != 200 || response.mime_type != 'text/html' - page = Nokogiri::HTML(response.to_s) + html = response.to_s + + detector = CharlockHolmes::EncodingDetector.new + detector.strip_tags = true + + guess = detector.detect(html, response.charset) + page = Nokogiri::HTML(html, nil, guess&.fetch(:encoding)) card.type = :link card.title = meta_property(page, 'og:title') || page.at_xpath('//title')&.content diff --git a/app/services/fetch_remote_account_service.rb b/app/services/fetch_remote_account_service.rb index 8eed0d454..1efac365b 100644 --- a/app/services/fetch_remote_account_service.rb +++ b/app/services/fetch_remote_account_service.rb @@ -32,5 +32,8 @@ class FetchRemoteAccountService < BaseService rescue Nokogiri::XML::XPath::SyntaxError Rails.logger.debug 'Invalid XML or missing namespace' nil + rescue Goldfinger::NotFoundError, Goldfinger::Error + Rails.logger.debug 'Exceptions related to Goldfinger occurs' + nil end end diff --git a/app/services/fetch_remote_resource_service.rb b/app/services/fetch_remote_resource_service.rb index 5dfa3a0ff..2c1c1f05f 100644 --- a/app/services/fetch_remote_resource_service.rb +++ b/app/services/fetch_remote_resource_service.rb @@ -5,6 +5,9 @@ class FetchRemoteResourceService < BaseService def call(url) @url = url + + return process_local_url if local_url? + process_url unless fetched_atom_feed.nil? end @@ -38,4 +41,29 @@ class FetchRemoteResourceService < BaseService def xml_data @_xml_data ||= Nokogiri::XML(body, nil, 'utf-8') end + + def local_url? + TagManager.instance.local_url?(@url) + end + + def process_local_url + recognized_params = Rails.application.routes.recognize_path(@url) + + return unless recognized_params[:action] == 'show' + + if recognized_params[:controller] == 'stream_entries' + status = StreamEntry.find_by(id: recognized_params[:id])&.status + check_local_status(status) + elsif recognized_params[:controller] == 'statuses' + status = Status.find_by(id: recognized_params[:id]) + check_local_status(status) + elsif recognized_params[:controller] == 'accounts' + Account.find_local(recognized_params[:username]) + end + end + + def check_local_status(status) + return if status.nil? + status if status.public_visibility? || status.unlisted_visibility? + end end diff --git a/app/services/fetch_remote_status_service.rb b/app/services/fetch_remote_status_service.rb index f414813ad..4cfd33d90 100644 --- a/app/services/fetch_remote_status_service.rb +++ b/app/services/fetch_remote_status_service.rb @@ -33,6 +33,9 @@ class FetchRemoteStatusService < BaseService rescue Nokogiri::XML::XPath::SyntaxError Rails.logger.debug 'Invalid XML or missing namespace' nil + rescue Goldfinger::NotFoundError, Goldfinger::Error + Rails.logger.debug 'Exceptions related to Goldfinger occurs' + nil end def confirmed_domain?(domain, account) diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 422d5f97e..407d385ea 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -60,7 +60,7 @@ class NotifyService < BaseService def create_notification @notification.save! return unless @notification.browserable? - Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, 'api/v1/notifications/show'))) + Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification))) end def send_email diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb index 83765bb05..85635a008 100644 --- a/app/services/precompute_feed_service.rb +++ b/app/services/precompute_feed_service.rb @@ -13,21 +13,16 @@ class PrecomputeFeedService < BaseService attr_reader :account def populate_feed - redis.pipelined do - statuses.each do |status| - process_status(status) - end + pairs = statuses.reverse_each.lazy.reject(&method(:status_filtered?)).map(&method(:process_status)).to_a + redis.pipelined do + redis.zadd(account_home_key, pairs) if pairs.any? redis.del("account:#{@account.id}:regeneration") end end def process_status(status) - add_status_to_feed(status) unless status_filtered?(status) - end - - def add_status_to_feed(status) - redis.zadd(account_home_key, status.id, status.reblog? ? status.reblog_of_id : status.id) + [status.id, status.reblog? ? status.reblog_of_id : status.id] end def status_filtered?(status) diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb index fbdf92caa..c335d2159 100644 --- a/app/services/process_feed_service.rb +++ b/app/services/process_feed_service.rb @@ -20,8 +20,6 @@ class ProcessFeedService < BaseService end class ProcessEntry - include AuthorExtractor - def call(xml, account) @account = account @xml = xml @@ -42,7 +40,7 @@ class ProcessFeedService < BaseService private def create_status - if redis.exists("delete_upon_arrival:#{id}") + if redis.exists("delete_upon_arrival:#{@account.id}:#{id}") Rails.logger.debug "Delete for status #{id} was queued, ignoring" return end @@ -99,15 +97,13 @@ class ProcessFeedService < BaseService def delete_status Rails.logger.debug "Deleting remote status #{id}" - status = Status.find_by(uri: id) + status = Status.find_by(uri: id, account: @account) if status.nil? - redis.setex("delete_upon_arrival:#{id}", 6 * 3_600, id) + redis.setex("delete_upon_arrival:#{@account.id}:#{id}", 6 * 3_600, id) else RemoveStatusService.new.call(status) end - - nil end def skip_unsupported_type? @@ -128,18 +124,7 @@ class ProcessFeedService < BaseService return [status, false] unless status.nil? - # If status embeds an author, find that author - # If that author cannot be found, don't record the status (do not misattribute) - if account?(entry) - begin - account = author_from_xml(entry) - return [nil, false] if account.nil? - rescue Goldfinger::Error - return [nil, false] - end - else - account = @account - end + account = @account return [nil, false] if account.suspended? diff --git a/app/views/about/_features.html.haml b/app/views/about/_features.html.haml new file mode 100644 index 000000000..8fbc6b760 --- /dev/null +++ b/app/views/about/_features.html.haml @@ -0,0 +1,25 @@ +.features-list + .features-list__row + .text + %h6= t 'about.features.real_conversation_title' + = t 'about.features.real_conversation_body' + .visual + = fa_icon 'fw comments' + .features-list__row + .text + %h6= t 'about.features.not_a_product_title' + = t 'about.features.not_a_product_body' + .visual + = fa_icon 'fw users' + .features-list__row + .text + %h6= t 'about.features.within_reach_title' + = t 'about.features.within_reach_body' + .visual + = fa_icon 'fw mobile' + .features-list__row + .text + %h6= t 'about.features.humane_approach_title' + = t 'about.features.humane_approach_body' + .visual + = fa_icon 'fw leaf' diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml index 4f38c1ecc..eeeb0088f 100644 --- a/app/views/about/_registration.html.haml +++ b/app/views/about/_registration.html.haml @@ -1,10 +1,13 @@ = simple_form_for(new_user, url: user_registration_path) do |f| = f.simple_fields_for :account do |account_fields| - = account_fields.input :username, - autofocus: true, - placeholder: t('simple_form.labels.defaults.username'), - required: true, - input_html: { 'aria-label' => t('simple_form.labels.defaults.username') } + .input-with-append + = account_fields.input :username, + autofocus: true, + placeholder: t('simple_form.labels.defaults.username'), + required: true, + input_html: { 'aria-label' => t('simple_form.labels.defaults.username') } + .append + = "@#{site_hostname}" = f.input :email, placeholder: t('simple_form.labels.defaults.email'), @@ -22,9 +25,6 @@ input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') } .actions - = f.button :button, t('about.get_started'), type: :submit + = f.button :button, t('auth.register'), type: :submit, class: 'button button-alternative' - .info - = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' - · - = link_to t('about.about_this'), about_more_path + %p.hint.subtle-hint=t('auth.agreement_html', rules_path: about_more_path, terms_path: terms_path) diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml index 2a7f8c752..f75f87c99 100644 --- a/app/views/about/show.html.haml +++ b/app/views/about/show.html.haml @@ -1,4 +1,5 @@ - content_for :header_tags do + %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' - content_for :page_title do @@ -9,79 +10,70 @@ %meta{ property: 'og:url', content: about_url }/ %meta{ property: 'og:type', content: 'website' }/ %meta{ property: 'og:title', content: site_hostname }/ - %meta{ property: 'og:description', content: strip_tags(@instance_presenter.site_description.presence || t('about.about_mastodon')) }/ + %meta{ property: 'og:description', content: strip_tags(@instance_presenter.site_description.presence || t('about.about_mastodon_html')) }/ %meta{ property: 'og:image', content: asset_pack_path('mastodon_small.jpg', protocol: :request) }/ %meta{ property: 'og:image:width', content: '400' }/ %meta{ property: 'og:image:height', content: '400' }/ %meta{ property: 'twitter:card', content: 'summary' }/ -.wrapper - %h1 - = image_tag asset_pack_path('logo.png') - = Setting.site_title +.landing-page + .header-wrapper + .mascot-container + = image_tag asset_pack_path('elephant-fren.png'), class: 'mascot' - %p!= t('about.about_mastodon') + .header + .container.links + .brand + = link_to root_url do + = image_tag asset_pack_path('logo.svg') + Mastodon - .screenshot-with-signup - .mascot= image_tag asset_pack_path('fluffy-elephant-friend.png') + %ul.nav + %li + - if user_signed_in? + = link_to t('settings.back'), root_url, class: 'webapp-btn' + - else + = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' + %li= link_to t('about.about_this'), about_more_path + %li= link_to t('about.other_instances'), 'https://joinmastodon.org/' - - if @instance_presenter.open_registrations - = render 'registration' - - else - .closed-registrations-message - - if @instance_presenter.closed_registrations_message.blank? - %p= t('about.closed_registrations') + .container.hero + .floats + = image_tag asset_pack_path('cloud2.png'), class: 'float-1' + = image_tag asset_pack_path('cloud3.png'), class: 'float-2' + = image_tag asset_pack_path('cloud4.png'), class: 'float-3' + .heading + %h1 + = @instance_presenter.site_title + %small= t 'about.hosted_on', domain: site_hostname + - if @instance_presenter.open_registrations + = render 'registration' - else - != @instance_presenter.closed_registrations_message - .info - = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' - · - = link_to t('about.other_instances'), 'https://instances.mastodon.xyz/' - · - = link_to t('about.about_this'), about_more_path + .closed-registrations-message + %div + - if @instance_presenter.closed_registrations_message.blank? + %p= t('about.closed_registrations') + - else + = @instance_presenter.closed_registrations_message.html_safe + = link_to t('about.find_another_instance'), 'https://joinmastodon.org', class: 'button button-alternative button--block' - %h3= t('about.features_headline') + .learn-more-cta + .container + %h3= t('about.description_headline', domain: site_hostname) + %p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname) - .features-list - .features-list__column - %ul.fa-ul - %li - = fa_icon('li check-square') - = t 'about.features.chronology' - %li - = fa_icon('li check-square') - = t 'about.features.public' - %li - = fa_icon('li check-square') - = t 'about.features.characters' - %li - = fa_icon('li check-square') - = t 'about.features.gifv' - .features-list__column - %ul.fa-ul - %li - = fa_icon('li check-square') - = t 'about.features.privacy' - %li - = fa_icon('li check-square') - = t 'about.features.blocks' - %li - = fa_icon('li check-square') - = t 'about.features.ethics' - %li - = fa_icon('li check-square') - = t 'about.features.api' + .features + .container + - if Setting.timeline_preview + #mastodon-timeline{ data: { props: Oj.dump(default_props) } } - - unless @instance_presenter.site_description.blank? - %h3= t('about.description_headline', domain: site_hostname) - %p!= @instance_presenter.site_description - - .actions - .info - = link_to t('about.terms'), terms_path - · - = link_to t('about.apps'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' - · - = link_to t('about.source_code'), 'https://github.com/chronister/mastodon' - · - = link_to t('about.other_instances'), 'https://instances.mastodon.xyz/' + .about-mastodon + %h3= t 'about.what_is_mastodon' + %p= t 'about.about_mastodon_html' + %a.button.button-secondary{ href: 'https://joinmastodon.org' }= t 'about.learn_more' + = render 'features' + .footer-links + .container + %p + = link_to t('about.source_code'), 'https://github.com/tootsuite/mastodon' + = " (#{@instance_presenter.version_number})" diff --git a/app/views/about/terms.en.html.haml b/app/views/about/terms.en.html.haml deleted file mode 100644 index 7e0fb94c2..000000000 --- a/app/views/about/terms.en.html.haml +++ /dev/null @@ -1,76 +0,0 @@ -- content_for :page_title do - #{site_hostname} Terms of Service and Privacy Policy - -.wrapper - %h2 Privacy Policy - - %h3#collect What information do we collect? - - %p We collect information from you when you register on our site and gather data when you participate in the forum by reading, writing, and evaluating the content shared here. - - %p When registering on our site, you may be asked to enter your name and e-mail address. You may, however, visit our site without registering. Your e-mail address will be verified by an email containing a unique link. If that link is visited, we know that you control the e-mail address. - - %p When registered and posting, we record the IP address that the post originated from. We also may retain server logs which include the IP address of every request to our server. - - %h3#use What do we use your information for? - - %p Any of the information we collect from you may be used in one of the following ways: - - %ul - %li To personalize your experience — your information helps us to better respond to your individual needs. - %li To improve our site — we continually strive to improve our site offerings based on the information and feedback we receive from you. - %li To improve customer service — your information helps us to more effectively respond to your customer service requests and support needs. - %li To send periodic emails — The email address you provide may be used to send you information, notifications that you request about changes to topics or in response to your user name, respond to inquiries, and/or other requests or questions. - - %h3#protect How do we protect your information? - - %p We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information. - - %h3#data-retention What is your data retention policy? - - %p We will make a good faith effort to: - - %ul - %li Retain server logs containing the IP address of all requests to this server no more than 90 days. - %li Retain the IP addresses associated with registered users and their posts no more than 5 years. - - %h3#cookies Do we use cookies? - - %p 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. - - %p We use cookies to understand and save your preferences for future visits and compile aggregate data about site traffic and site interaction so that we can offer better site experiences and tools in the future. We may contract with third-party service providers to assist us in better understanding our site visitors. These service providers are not permitted to use the information collected on our behalf except to help us conduct and improve our business. - - %h3#disclose Do we disclose any information to outside parties? - - %p 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. However, non-personally identifiable visitor information may be provided to other parties for marketing, advertising, or other uses. - - %h3#third-party Third party links - - %p Occasionally, at our discretion, we may include or offer third party products or services on our site. These third party sites have separate and independent privacy policies. We therefore have no responsibility or liability for the content and activities of these linked sites. Nonetheless, we seek to protect the integrity of our site and welcome any feedback about these sites. - - %h3#coppa Children's Online Privacy Protection Act Compliance - - %p - 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 - = surround '(', '),' do - = link_to 'Children\'s Online Privacy Protection Act', 'https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act' - do not use this site. - - %h3#online Online Privacy Policy Only - - %p This online privacy policy applies only to information collected through our site and not to information collected offline. - - %h3#consent Your Consent - - %p By using our site, you consent to our web site privacy policy. - - %h3#changes Changes to our Privacy Policy - - %p If we decide to change our privacy policy, we will post those changes on this page. - - %p This document is CC-BY-SA. It was last updated May 31, 2013. - - %p - Originally adapted from the - = succeed '.' do - = link_to 'Discourse privacy policy', 'https://github.com/discourse/discourse' diff --git a/app/views/about/terms.html.haml b/app/views/about/terms.html.haml new file mode 100644 index 000000000..58064f0be --- /dev/null +++ b/app/views/about/terms.html.haml @@ -0,0 +1,8 @@ +- content_for :page_title do + = t('terms.title', instance: site_hostname) + +.wrapper + - if @instance_presenter.site_terms.present? + = raw @instance_presenter.site_terms + - else + = t('terms.body_html') diff --git a/app/views/about/terms.ja.html.haml b/app/views/about/terms.ja.html.haml deleted file mode 100644 index 5c546b3e0..000000000 --- a/app/views/about/terms.ja.html.haml +++ /dev/null @@ -1,76 +0,0 @@ -- content_for :page_title do - #{site_hostname} 利用規約・プライバシーポリシー - -.wrapper - %h2 プライバシーポリシー - - %h3#collect どのような情報を収集するのですか? - - %p あなたがこのサイトに登録すると、ここで共有された情報を読んだり、書いたり、評価したりして、フォーラムでの情報を集める事ができます。 - - %p このサイトに登録する際には、名前とメールアドレスの入力を求めることがあります。ただし、登録をすることなくこのサイトを利用することも可能です。あなたのメールアドレスは、固有のリンクを含んだメールで確認されます。そのリンクにアクセスした場合にメールアドレスを制御することとなります。 - - %p アカウントを登録し、投稿を行った際にはその投稿が行われたIPアドレスを記録します。また、このサーバーに対する全てのリクエストはIPアドレスを含むサーバーログとして保管されます。 - - %h3#use 自分の情報を何に使うのですか? - - %p このサイトで収集された情報は、次のいくつかの方法で使用されます: - - %ul - %li パーソナライズ・エクスペリエンス — あなたの情報は、あなたや他のユーザーのニーズに対応するために役立ちます。 - %li サイトの改善・最適化 — このサービスはあなたから受け取った情報やフィードバックに基づいて提供されるサイトの改善を行いつづけます。 - %li サービスの向上 — あなたの情報は、ユーザーからの要求やサポートへより効果的に対応するために役立ちます。 - %li 定期メールの送信 — メールアドレスは、情報の送信、トピックの変更やユーザー名に関係するお知らせ、お問い合わせに関する返答、その他のリクエストや質問に関してお知らせするために使用されます。 - - %h3#protect 自分の情報はどのように保護されるのですか? - - %p このサービスはあなたの個人情報の入力、送信、またはアクセスに際してあなたの個人情報の安全性を維持するために様々なセキュリティ手段をとっています。 - - %h3#data-retention データ保持のポリシーはどのようになっていますか? - - %p このサービスはデータ保持に関して次のことを行うよう努めます。: - - %ul - %li このサーバーへのすべての要求に対して、IPアドレスを含むサーバーログを90日以内に渡って保持します。 - %li 登録されたユーザーとその投稿に関連付けされたIPアドレスを5年以内に渡って保持します。 - - %h3#cookies クッキーを使用していますか? - - %p はい。クッキーはあなたがウェブブラウザ上で許可した場合にコンピュータのストレージに転送される小さなファイルです。これらのクッキーを使用すると、サイトでブラウザが識別され、登録済みのアカウントを持っている場合は登録済みのアカウントに関連付けがされます。 - - %p クッキーを使用して、今後再度閲覧された場合に前回のデータから設定を呼び出したり、今後の改善のためにサイトのトラフィックやサイトの相互作用に関する集計データを作成します。このサービスは、サイトを訪れた方との理解を深めるために、第三者のサービス提供者と契約することがあります。これらのサービス提供者というものは、このサービスでの業務を行ったり、改善するためにこのサービスの代わって収集された情報を使用することはできません。 - - %h3#disclose このサイトは外部に何らかの情報を開示していますか? - - %p 私たちは、個人を特定出来る情報を外部へ販売、取引、または他の方法で渡すことはありません。これには、このサイトを操作したり、業務を行ったり、サービスを提供するのに役立つ信頼できる第三者は含まれません。法令遵守、サイトポリシーの施行、このサービスや他の人の権利、財産または安全の保護のために適切であると判断した場合に、あなたの情報を公開する場合があります。ただし、マーケティングや広告、その他の目的で匿名での訪問者情報を他者へ提供することができます。 - - %h3#third-party サードパーティのリンク - - %p 必要に応じて、このサービスの方針にもとづいてこのサイトや第三者のサービスを提供することがあります。これらの第三者のサイトには、個別の独立したプライバシーポリシーがあります。従って、これらのリンク先のサイトに関するコンテンツや活動にかんしては一切責任を負いません。ですが、サイトの完全性やこれらのサイトに関するフィードバックは非常に重要なものであると認識しております。 - - %h3#coppa 子供のオンライン・プライバシー保護法 - - %p - このサイト、製品、サービスはすべて13歳以上の人を対象としております。このサーバーが米国にあり、13歳未満の場合はCOPPA - = surround '(', '),' do - = link_to 'Children\'s Online Privacy Protection Act', 'https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act' - にもとづいてこのサイトを使用しないでください。 - - %h3#online オンライン限定のプライバシーポリシー - - %p このオンライン・プライバシーポリシーは、このサイトを通じて収集された情報のみに適用され、オフラインで収集される情報には適用されません。 - - %h3#consent あなたの同意 - - %p このサービスを使用することにより、このサイトのプライバシーポリシーに同意するものとします。 - - %h3#changes プライバシーポリシーの変更 - - %p プライバシーポリシーを変更する場合は、このページへ変更内容を掲載します。 - - %p この文章のライセンスはCC-BY-SAです。このページは2017年5月6日が最終更新です。 - - %p - オリジナルの出典 - = succeed '.' do - = link_to 'Discourse privacy policy', 'https://github.com/discourse/discourse' diff --git a/app/views/about/terms.no.html.haml b/app/views/about/terms.no.html.haml deleted file mode 100644 index 46f62950d..000000000 --- a/app/views/about/terms.no.html.haml +++ /dev/null @@ -1,76 +0,0 @@ -- content_for :page_title do - #{site_hostname} Personvern og villkår for bruk av nettstedet - -.wrapper - %h2 Personvernserklæring - - %h3#collect Hvilke opplysninger samler vi? - - %p Vi samler opplysninger fra deg når du registrerer deg på nettstedet vårt, og vi samler data når du deltar på forumet ved å lese, skrive og evaluere innholdet som deles her. - - %p Når du registrerer deg på nettstedet vårt, kan du bli bedt om å oppgi navnet og e-postadressen din. Imidlertid kan du besøke nettstedet vårt uten å registrere deg. E-postadressen din vil bli bekreftet med en e-post som inneholder en unik lenke. Hvis siden den lenker til, blir besøkt, vet vi at du har kontroll over e-postadressen. - - %p Når du registrerer deg og skriver innlegg, registrerer vi IP-adressen som innlegget stammer fra. Vi kan også oppbevare logger som inkluderer IP-adressen til alle forespørslene sendt til tjeneren vår. - - %h3#use Hva bruker vi opplysningene dine til? - - %p Alle opplysningene vi samler fra deg, kan bli brukt på en av følgende måter: - - %ul - %li For å gjøre opplevelsen din mer personlig. Opplysningene dine hjelper oss å svare bedre på dine individuelle behov. - %li For å forbedre nettstedet vårt. Vi jobber konstant for å forbedre nettstedets tilbud basert på opplysningene og tilbakemeldingene vi mottar fra deg. - %li For å forbedre vår kundeservice. Dine opplysninger hjelper oss å svare mer effektivt på dine forespørsler sendt til kundeservice eller behov om støtte. - %li For å sende periodiske e-poster. E-postadressen du oppgir, kan bli brukt til å sende deg informasjon, påminnelser som du ber om ved endringer av emner eller ved svar til brukernavnet ditt, til henvendelser, og/eller andre forspørsler eller andre spørsmål. - - %h3#protect Hvordan sikrer vi opplysningene? - - %p Vi gjennomfører flere sikkerhetstiltak for å holde personopplysningene dine sikre når du skriver inn, lagrer eller henter dem. - - %h3#data-retention Hva er retningslinjene deres for lagring av data? - - %p Vi vil forsøke i god tro å: - - %ul - %li Ikke oppbevare tjener-logger som inneholder IP-adressen til alle forespørslene til denne tjeneren i lenger enn i 90 dager. - %li Ikke oppbevare IP-adressene forbundet med registrerte brukere og deres innlegg lenger enn i 5 år. - - %h3#cookies Bruker vi informasjonskapsler? - - %p Ja. Informasjonskapsler er små filer som et nettsted eller dets tjenesteleverandør overfører til harddisken på datamaskinen din gjennom nettleseren din (dersom du tillater det). Disse informasjonskapslene gjør det mulig for nettstedet å gjenkjenne nettleseren din og, dersom du har en konto, knytte nettleseren til den. - - %p Vi bruker informasjonskapsler for å forstå og lagre preferansene dine for fremtidige besøk og for å samle aggregatdata om trafikk på og samhandling med nettstedet slik at vi kan tilby bedre opplevelser og verktøy på nettstedet i fremtiden. Vi kan inngå avtaler med tredjeparts tjenesteleverandører for å bistå oss i å forstå besøkerne våres bedre. Disse tjenesteleverandørene har ikke lov til å bruke opplysningene samlet på våres vegne unntatt til å hjelpe oss å gjennomføre og forbedre anliggendet vårt. - - %h3#disclose Gir vi noen opplysninger videre til andre parter? - - %p Vi verken selger, handler med eller overfører på noen annen måte til andre parter dine identifiserbare personopplysninger. Dette inkluderer ikke tredjeparter som har vår tillit og bistår oss i å drive nettstedet, utføre våre anliggender eller yter tjenester til deg, så lenge disse partene samtykker til å behandle disse opplysningene fortrolig. Vi kan også frigi opplysningene dine dersom vi tror at å frigi dem er hensiktsmessig for å overholde loven, håndheve nettstedet retningslinjer eller beskytte våre og andres rettigheter. Imidlertid kan opplysninger som ikke er personlig identifiserbare, bli delt med andre parter for markedsføring, reklame eller annet bruk. - - %h3#third-party Tredjeparts lenker - - %p Av og til, etter skjønn, kan vil inkludere eller tilby tredjeparts produkter eller tjenester på nettstedet vårt. Disse tredjeparts nettstedene har separate og selvstendige personvernerklæringer. Vi bærer derfor intet ansvar eller forpliktelser for innholdet eller aktivitetene til disse nettstedene det lenkes til. Ikke mindre prøver vi å bevare vår eget nettsteds integritet og ønsker enhver tilbakemelding om disse nettstedene velkomne. - - %h3#coppa Overensstemmelse med Children's Online Privacy Protection Act - - %p - Nettstedet er rettet mot folk som er minst 13 år gamle. Dersom denne tjeneren er i USA, og du er under 13 år i henhold til kravene i COPPA - = surround '(', '),' do - = link_to 'Children\'s Online Privacy Protection Act', 'https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act' - ikke bruk dette nettstedet. - - %h3#online Personvernerklæring bare for nettet - - %p Denne nett-personvernerklæringen gjelder bare for informasjon samlet gjennom nettstedet vårt og ikke for opplysninger samlet når en er frakoblet. - - %h3#consent Ditt samtykke - - %p Ved å bruke dette nettstedet samtykker du til nettstedets personvernerklæring. - - %h3#changes Endringer i vår personvernerklæring - - %p Dersom vi beslutter å endre personvernerklæringen vår, vil vi publisere disse endringene på denne siden. - - %p Dette dokumentet er lisensiert under CC-BY-SA. De ble sist oppdatert 12. april 2017. - - %p - Dokumentet er en adoptert og endret versjon fra - = succeed '.' do - = link_to 'Discourse privacy policy', 'https://github.com/discourse/discourse' diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index 6d2849c32..07c8d1632 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -6,14 +6,30 @@ %strong= t('admin.accounts.location.title') %ul %li= filter_link_to t('admin.accounts.location.all'), local: nil, remote: nil - %li= filter_link_to t('admin.accounts.location.local'), local: '1', remote: nil - %li= filter_link_to t('admin.accounts.location.remote'), remote: '1', local: nil + %li + - if selected? local: '1', remote: nil + = filter_link_to t('admin.accounts.location.local'), {local: nil, remote: nil}, {local: '1', remote: nil} + - else + = filter_link_to t('admin.accounts.location.local'), local: '1', remote: nil + %li + - if selected? remote: '1', local: nil + = filter_link_to t('admin.accounts.location.remote'), {remote: nil, local: nil}, {remote: '1', local: nil} + - else + = filter_link_to t('admin.accounts.location.remote'), remote: '1', local: nil .filter-subset %strong= t('admin.accounts.moderation.title') %ul %li= filter_link_to t('admin.accounts.moderation.all'), silenced: nil, suspended: nil - %li= filter_link_to t('admin.accounts.moderation.silenced'), silenced: '1' - %li= filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1' + %li + - if selected? silenced: '1' + = filter_link_to t('admin.accounts.moderation.silenced'), {silenced: nil}, {silenced: '1'} + - else + = filter_link_to t('admin.accounts.moderation.silenced'), silenced: '1' + %li + - if selected? suspended: '1' + = filter_link_to t('admin.accounts.moderation.suspended'), {suspended: nil}, {suspended: '1'} + - else + = filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1' .filter-subset %strong= t('admin.accounts.order.title') %ul diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index 965b71180..44486cb42 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -4,11 +4,11 @@ .report-accounts .report-accounts__item %strong= t('admin.reports.reported_account') - = render 'authorize_follows/card', account: @report.target_account + = render 'authorize_follows/card', account: @report.target_account, admin: true = render 'admin/accounts/card', account: @report.target_account .report-accounts__item %strong= t('admin.reports.reported_by') - = render 'authorize_follows/card', account: @report.account + = render 'authorize_follows/card', account: @report.account, admin: true = render 'admin/accounts/card', account: @report.account %p diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index edb69e360..9f8a6640b 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -1,58 +1,32 @@ - content_for :page_title do = t('admin.settings.title') -= form_tag(admin_settings_path, method: :put, class: 'simple_form', style: 'max-width: 100%') do - %table.table - %thead - %tr - %th{ width: '40%' } - = t('admin.settings.setting') - %th - %tbody - %tr - %td - %strong= t('admin.settings.contact_information.label') - %td= text_field_tag :site_contact_username, - @settings['site_contact_username'].value, - place_holder: t('admin.settings.contact_information.username') - %tr - %td - %strong= t('admin.accounts.email') - %td= text_field_tag :site_contact_email, - @settings['site_contact_email'].value, - place_holder: t('admin.settings.contact_information.email') - %tr - %td - %strong= t('admin.settings.site_title') - %td= text_field_tag :site_title, - @settings['site_title'].value - %tr - %td - %strong= t('admin.settings.site_description.title') - %p= t('admin.settings.site_description.desc_html') - %td= text_area_tag :site_description, - @settings['site_description'].value, - rows: 8 - %tr - %td - %strong= t('admin.settings.site_description_extended.title') - %p= t('admin.settings.site_description_extended.desc_html') - %td= text_area_tag :site_extended_description, - @settings['site_extended_description'].value, - rows: 8 - %tr - %td - %strong= t('admin.settings.registrations.open.title') - %td - = select_tag :open_registrations, - options_for_select({ t('admin.settings.registrations.open.disabled') => false, t('admin.settings.registrations.open.enabled') => true }, @settings['open_registrations'].value) - %tr - %td - %strong= t('admin.settings.registrations.closed_message.title') - %p= t('admin.settings.registrations.closed_message.desc_html') - %td= text_area_tag :closed_registrations_message, - @settings['closed_registrations_message'].value, - rows: 8 - - .simple_form.actions - = button_tag t('generic.save_changes'), type: :submit, class: :btn += simple_form_for @admin_settings, url: admin_settings_path, html: { method: :patch } do |f| + .fields-group + = f.input :site_title, placeholder: t('admin.settings.site_title') + = f.input :site_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description.title'), hint: t('admin.settings.site_description.desc_html'), input_html: { rows: 8 } + = f.input :site_contact_username, placeholder: t('admin.settings.contact_information.username') + = f.input :site_contact_email, placeholder: t('admin.settings.contact_information.email') + + %hr/ + + .fields-group + = f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html') + + .fields-group + = f.input :open_registrations, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.open.title'), hint: t('admin.settings.registrations.open.desc_html') + + .fields-group + = f.input :open_deletion, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.deletion.title'), hint: t('admin.settings.registrations.deletion.desc_html') + + .fields-group + = f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, label: t('admin.settings.registrations.closed_message.title'), hint: t('admin.settings.registrations.closed_message.desc_html'), input_html: { rows: 8 } + + %hr/ + + .fields-group + = f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } + = f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 } + + .actions + = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/admin/subscriptions/_subscription.html.haml b/app/views/admin/subscriptions/_subscription.html.haml index 024788e13..1dec8e396 100644 --- a/app/views/admin/subscriptions/_subscription.html.haml +++ b/app/views/admin/subscriptions/_subscription.html.haml @@ -7,10 +7,12 @@ - if subscription.confirmed? %i.fa.fa-check %td{ style: "color: #{subscription.expired? ? 'red' : 'inherit'};" } - = precede subscription.expired? ? '-' : '' do - = time_ago_in_words(subscription.expires_at) + %time.time-ago{ datetime: subscription.expires_at.iso8601, title: l(subscription.expires_at) } + = precede subscription.expired? ? '-' : '' do + = time_ago_in_words(subscription.expires_at) %td - if subscription.last_successful_delivery_at? - = l subscription.last_successful_delivery_at + %time.formatted{ datetime: subscription.last_successful_delivery_at.iso8601, title: l(subscription.last_successful_delivery_at) } + = l subscription.last_successful_delivery_at - else %i.fa.fa-times diff --git a/app/views/api/oembed/show.json.rabl b/app/views/api/oembed/show.json.rabl deleted file mode 100644 index 11dcec538..000000000 --- a/app/views/api/oembed/show.json.rabl +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true -object @stream_entry - -node(:type) { 'rich' } -node(:version) { '1.0' } -node(:title, &:title) -node(:author_name) { |entry| entry.account.display_name.blank? ? entry.account.username : entry.account.display_name } -node(:author_url) { |entry| account_url(entry.account) } -node(:provider_name) { site_hostname } -node(:provider_url) { root_url } -node(:cache_age) { 86_400 } -node(:html) { |entry| "<iframe src=\"#{embed_account_stream_entry_url(entry.account, entry)}\" style=\"width: 100%; overflow: hidden\" frameborder=\"0\" width=\"#{@width}\" height=\"#{@height}\" scrolling=\"no\"></iframe>" } -node(:width) { @width } -node(:height) { @height } diff --git a/app/views/api/v1/accounts/index.rabl b/app/views/api/v1/accounts/index.rabl deleted file mode 100644 index 9f3b13a53..000000000 --- a/app/views/api/v1/accounts/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @accounts -extends 'api/v1/accounts/show' diff --git a/app/views/api/v1/accounts/relationship.rabl b/app/views/api/v1/accounts/relationship.rabl deleted file mode 100644 index 4f7763d9d..000000000 --- a/app/views/api/v1/accounts/relationship.rabl +++ /dev/null @@ -1,9 +0,0 @@ -object @account - -attribute :id -node(:following) { |account| @following[account.id] || false } -node(:followed_by) { |account| @followed_by[account.id] || false } -node(:blocking) { |account| @blocking[account.id] || false } -node(:muting) { |account| @muting[account.id] || false } -node(:requested) { |account| @requested[account.id] || false } -node(:domain_blocking) { |account| @domain_blocking[account.id] || false } diff --git a/app/views/api/v1/accounts/relationships/index.rabl b/app/views/api/v1/accounts/relationships/index.rabl deleted file mode 100644 index 022ea2ac4..000000000 --- a/app/views/api/v1/accounts/relationships/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @accounts -extends 'api/v1/accounts/relationship' diff --git a/app/views/api/v1/accounts/show.rabl b/app/views/api/v1/accounts/show.rabl deleted file mode 100644 index 8826aa22d..000000000 --- a/app/views/api/v1/accounts/show.rabl +++ /dev/null @@ -1,12 +0,0 @@ -object @account - -attributes :id, :username, :acct, :display_name, :locked, :created_at - -node(:note) { |account| Formatter.instance.simplified_format(account) } -node(:url) { |account| TagManager.instance.url_for(account) } -node(:avatar) { |account| full_asset_url(account.avatar_original_url) } -node(:avatar_static) { |account| full_asset_url(account.avatar_static_url) } -node(:header) { |account| full_asset_url(account.header_original_url) } -node(:header_static) { |account| full_asset_url(account.header_static_url) } - -attributes :followers_count, :following_count, :statuses_count diff --git a/app/views/api/v1/accounts/statuses/index.rabl b/app/views/api/v1/accounts/statuses/index.rabl deleted file mode 100644 index 44d29d91b..000000000 --- a/app/views/api/v1/accounts/statuses/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @statuses -extends 'api/v1/statuses/show' diff --git a/app/views/api/v1/apps/create.rabl b/app/views/api/v1/apps/create.rabl deleted file mode 100644 index 1ff6469a4..000000000 --- a/app/views/api/v1/apps/create.rabl +++ /dev/null @@ -1,4 +0,0 @@ -object @app -attributes :id, :redirect_uri -node(:client_id) { |app| app.uid } -node(:client_secret) { |app| app.secret } diff --git a/app/views/api/v1/apps/show.rabl b/app/views/api/v1/apps/show.rabl deleted file mode 100644 index 6d9e607db..000000000 --- a/app/views/api/v1/apps/show.rabl +++ /dev/null @@ -1,3 +0,0 @@ -object @application - -attributes :name, :website diff --git a/app/views/api/v1/blocks/index.rabl b/app/views/api/v1/blocks/index.rabl deleted file mode 100644 index 9f3b13a53..000000000 --- a/app/views/api/v1/blocks/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @accounts -extends 'api/v1/accounts/show' diff --git a/app/views/api/v1/favourites/index.rabl b/app/views/api/v1/favourites/index.rabl deleted file mode 100644 index 44d29d91b..000000000 --- a/app/views/api/v1/favourites/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @statuses -extends 'api/v1/statuses/show' diff --git a/app/views/api/v1/follow_requests/index.rabl b/app/views/api/v1/follow_requests/index.rabl deleted file mode 100644 index 9f3b13a53..000000000 --- a/app/views/api/v1/follow_requests/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @accounts -extends 'api/v1/accounts/show' diff --git a/app/views/api/v1/follows/show.rabl b/app/views/api/v1/follows/show.rabl deleted file mode 100644 index e07106164..000000000 --- a/app/views/api/v1/follows/show.rabl +++ /dev/null @@ -1,2 +0,0 @@ -object @account -extends('api/v1/accounts/show') diff --git a/app/views/api/v1/instances/show.rabl b/app/views/api/v1/instances/show.rabl deleted file mode 100644 index 05fb65031..000000000 --- a/app/views/api/v1/instances/show.rabl +++ /dev/null @@ -1,10 +0,0 @@ -object false - -node(:uri) { site_hostname } -node(:title) { Setting.site_title } -node(:description) { Setting.site_description } -node(:email) { Setting.site_contact_email } -node(:version) { Mastodon::Version.to_s } -node :urls do - { :streaming_api => Rails.configuration.x.streaming_api_base_url } -end diff --git a/app/views/api/v1/media/create.rabl b/app/views/api/v1/media/create.rabl deleted file mode 100644 index 53c13bbda..000000000 --- a/app/views/api/v1/media/create.rabl +++ /dev/null @@ -1,7 +0,0 @@ -object @media -attribute :id, :type - -node(:url) { |media| full_asset_url(media.file.url(:original)) } -node(:preview_url) { |media| full_asset_url(media.file.url(:small)) } -node(:text_url) { |media| medium_url(media) } -node(:meta) { |media| media.file.meta } diff --git a/app/views/api/v1/mutes/index.rabl b/app/views/api/v1/mutes/index.rabl deleted file mode 100644 index 9f3b13a53..000000000 --- a/app/views/api/v1/mutes/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @accounts -extends 'api/v1/accounts/show' diff --git a/app/views/api/v1/notifications/index.rabl b/app/views/api/v1/notifications/index.rabl deleted file mode 100644 index 6abc3da36..000000000 --- a/app/views/api/v1/notifications/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @notifications -extends 'api/v1/notifications/show' diff --git a/app/views/api/v1/notifications/show.rabl b/app/views/api/v1/notifications/show.rabl deleted file mode 100644 index ca34f2d5d..000000000 --- a/app/views/api/v1/notifications/show.rabl +++ /dev/null @@ -1,11 +0,0 @@ -object @notification - -attributes :id, :type, :created_at - -child from_account: :account do - extends 'api/v1/accounts/show' -end - -node(:status, if: lambda { |n| [:favourite, :reblog, :mention].include?(n.type) }) do |n| - partial 'api/v1/statuses/show', object: n.target_status -end diff --git a/app/views/api/v1/reports/index.rabl b/app/views/api/v1/reports/index.rabl deleted file mode 100644 index 4f0794027..000000000 --- a/app/views/api/v1/reports/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @reports -extends 'api/v1/reports/show' diff --git a/app/views/api/v1/reports/show.rabl b/app/views/api/v1/reports/show.rabl deleted file mode 100644 index 006db51e3..000000000 --- a/app/views/api/v1/reports/show.rabl +++ /dev/null @@ -1,2 +0,0 @@ -object @report -attributes :id, :action_taken diff --git a/app/views/api/v1/search/index.rabl b/app/views/api/v1/search/index.rabl deleted file mode 100644 index 8d1640f2d..000000000 --- a/app/views/api/v1/search/index.rabl +++ /dev/null @@ -1,13 +0,0 @@ -object @search - -child :accounts, object_root: false do - extends 'api/v1/accounts/show' -end - -node(:hashtags) do |search| - search.hashtags.map(&:name) -end - -child :statuses, object_root: false do - extends 'api/v1/statuses/show' -end diff --git a/app/views/api/v1/statuses/_media.rabl b/app/views/api/v1/statuses/_media.rabl deleted file mode 100644 index 07ac31888..000000000 --- a/app/views/api/v1/statuses/_media.rabl +++ /dev/null @@ -1,6 +0,0 @@ -attributes :id, :remote_url, :type - -node(:url) { |media| full_asset_url(media.file.url(:original)) } -node(:preview_url) { |media| full_asset_url(media.file.url(:small)) } -node(:text_url) { |media| media.local? ? medium_url(media) : nil } -node(:meta) { |media| media.file.meta } diff --git a/app/views/api/v1/statuses/_mention.rabl b/app/views/api/v1/statuses/_mention.rabl deleted file mode 100644 index 8c95fc9bd..000000000 --- a/app/views/api/v1/statuses/_mention.rabl +++ /dev/null @@ -1,4 +0,0 @@ -node(:url) { |mention| TagManager.instance.url_for(mention.account) } -node(:acct) { |mention| mention.account_acct } -node(:id) { |mention| mention.account_id } -node(:username) { |mention| mention.account_username } diff --git a/app/views/api/v1/statuses/_show.rabl b/app/views/api/v1/statuses/_show.rabl deleted file mode 100644 index fe3ec89ab..000000000 --- a/app/views/api/v1/statuses/_show.rabl +++ /dev/null @@ -1,29 +0,0 @@ -attributes :id, :created_at, :in_reply_to_id, - :in_reply_to_account_id, :sensitive, - :spoiler_text, :visibility, :language - -node(:uri) { |status| TagManager.instance.uri_for(status) } -node(:content) { |status| Formatter.instance.format(status) } -node(:url) { |status| TagManager.instance.url_for(status) } -node(:reblogs_count) { |status| defined?(@reblogs_counts_map) ? (@reblogs_counts_map[status.id] || 0) : status.reblogs_count } -node(:favourites_count) { |status| defined?(@favourites_counts_map) ? (@favourites_counts_map[status.id] || 0) : status.favourites_count } - -child :application do - extends 'api/v1/apps/show' -end - -child :account do - extends 'api/v1/accounts/show' -end - -child :media_attachments, object_root: false do - extends 'api/v1/statuses/_media' -end - -child :mentions, object_root: false do - extends 'api/v1/statuses/_mention' -end - -child :tags, object_root: false do - extends 'api/v1/statuses/_tags' -end diff --git a/app/views/api/v1/statuses/_tags.rabl b/app/views/api/v1/statuses/_tags.rabl deleted file mode 100644 index 25e7b0fac..000000000 --- a/app/views/api/v1/statuses/_tags.rabl +++ /dev/null @@ -1,2 +0,0 @@ -attribute :name -node(:url) { |tag| tag_url(tag) } diff --git a/app/views/api/v1/statuses/accounts.rabl b/app/views/api/v1/statuses/accounts.rabl deleted file mode 100644 index 9f3b13a53..000000000 --- a/app/views/api/v1/statuses/accounts.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @accounts -extends 'api/v1/accounts/show' diff --git a/app/views/api/v1/statuses/card.rabl b/app/views/api/v1/statuses/card.rabl deleted file mode 100644 index 5d8d7af3b..000000000 --- a/app/views/api/v1/statuses/card.rabl +++ /dev/null @@ -1,7 +0,0 @@ -object @card - -attributes :url, :title, :description, :type, - :author_name, :author_url, :provider_name, - :provider_url, :html, :width, :height - -node(:image) { |card| card.image? ? full_asset_url(card.image.url(:original)) : nil } diff --git a/app/views/api/v1/statuses/context.rabl b/app/views/api/v1/statuses/context.rabl deleted file mode 100644 index 0b62f26d5..000000000 --- a/app/views/api/v1/statuses/context.rabl +++ /dev/null @@ -1,9 +0,0 @@ -object @context - -node :ancestors do |context| - partial 'api/v1/statuses/index', object: context.ancestors -end - -node :descendants do |context| - partial 'api/v1/statuses/index', object: context.descendants -end diff --git a/app/views/api/v1/statuses/index.rabl b/app/views/api/v1/statuses/index.rabl deleted file mode 100644 index 0a0ed13c5..000000000 --- a/app/views/api/v1/statuses/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @statuses -extends('api/v1/statuses/show') diff --git a/app/views/api/v1/statuses/show.rabl b/app/views/api/v1/statuses/show.rabl deleted file mode 100644 index 4b33fb2c3..000000000 --- a/app/views/api/v1/statuses/show.rabl +++ /dev/null @@ -1,15 +0,0 @@ -object @status - -extends 'api/v1/statuses/_show' - -node(:favourited, if: proc { !current_account.nil? }) { |status| defined?(@favourites_map) ? @favourites_map[status.id] : current_account.favourited?(status) } -node(:reblogged, if: proc { !current_account.nil? }) { |status| defined?(@reblogs_map) ? @reblogs_map[status.id] : current_account.reblogged?(status) } -node(:muted, if: proc { !current_account.nil? }) { |status| defined?(@mutes_map) ? @mutes_map[status.conversation_id] : current_account.muting_conversation?(status.conversation) } - -child reblog: :reblog do - extends 'api/v1/statuses/_show' - - node(:favourited, if: proc { !current_account.nil? }) { |status| defined?(@favourites_map) ? @favourites_map[status.id] : current_account.favourited?(status) } - node(:reblogged, if: proc { !current_account.nil? }) { |status| defined?(@reblogs_map) ? @reblogs_map[status.id] : current_account.reblogged?(status) } - node(:muted, if: proc { !current_account.nil? }) { false } -end diff --git a/app/views/api/v1/timelines/show.rabl b/app/views/api/v1/timelines/show.rabl deleted file mode 100644 index 0a0ed13c5..000000000 --- a/app/views/api/v1/timelines/show.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @statuses -extends('api/v1/statuses/show') diff --git a/app/views/auth/registrations/_sessions.html.haml b/app/views/auth/registrations/_sessions.html.haml index 11c0d4e31..4521aad0a 100644 --- a/app/views/auth/registrations/_sessions.html.haml +++ b/app/views/auth/registrations/_sessions.html.haml @@ -11,9 +11,10 @@ - @sessions.each do |session| %tr %td - %span{ title: session.user_agent }= fa_icon session_device_icon(session) - = ' ' - = t 'sessions.description', browser: t("sessions.browsers.#{session.browser}"), platform: t("sessions.platforms.#{session.platform}") + %span{ title: session.user_agent }< + = fa_icon "#{session_device_icon(session)} fw", 'aria-label' => session_device_icon(session) + = ' ' + = t 'sessions.description', browser: t("sessions.browsers.#{session.browser}"), platform: t("sessions.platforms.#{session.platform}") %td %samp= session.ip %td diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml index 82d5483dd..af7ee2b28 100644 --- a/app/views/auth/registrations/new.html.haml +++ b/app/views/auth/registrations/new.html.haml @@ -5,7 +5,10 @@ = render 'shared/error_messages', object: resource = f.simple_fields_for :account do |ff| - = ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') } + .input-with-append + = ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') } + .append + = "@#{site_hostname}" = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } = f.input :password, autocomplete: 'off', placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') } @@ -14,4 +17,5 @@ .actions = f.button :button, t('auth.register'), type: :submit + %p.hint.subtle-hint=t('auth.agreement_html', rules_path: about_more_path, terms_path: terms_path) .form-footer= render 'auth/shared/links' diff --git a/app/views/authorize_follows/_card.html.haml b/app/views/authorize_follows/_card.html.haml index 13d9c7719..e81e292ba 100644 --- a/app/views/authorize_follows/_card.html.haml +++ b/app/views/authorize_follows/_card.html.haml @@ -4,7 +4,8 @@ = image_tag account.avatar.url(:original), alt: '', width: 48, height: 48, class: 'avatar' %span.display-name - = link_to TagManager.instance.url_for(account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do + - 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) %span @#{account.acct} diff --git a/app/views/authorize_follows/success.html.haml b/app/views/authorize_follows/success.html.haml new file mode 100644 index 000000000..f0b495689 --- /dev/null +++ b/app/views/authorize_follows/success.html.haml @@ -0,0 +1,16 @@ +- content_for :page_title do + = t('authorize_follow.title', acct: @account.acct) + +.form-container + .follow-prompt + - if @account.locked? + %h2= t('authorize_follow.follow_request') + - else + %h2= t('authorize_follow.following') + + = render 'card', account: @account + + .post-follow-actions + %div= link_to t('authorize_follow.post_follow.web'), web_url("accounts/#{@account.id}"), class: 'button button--block' + %div= link_to t('authorize_follow.post_follow.return'), @account.url, class: 'button button--block' + %div= t('authorize_follow.post_follow.close') diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 33c978c89..71dcb54c6 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -1,5 +1,5 @@ - content_for :header_tags do - %script#initial-state{ type: 'application/json' }!= json_escape(render(file: 'home/initial_state', formats: :json)) + %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) = javascript_pack_tag 'application', integrity: true, crossorigin: 'anonymous' diff --git a/app/views/home/initial_state.json.rabl b/app/views/home/initial_state.json.rabl deleted file mode 100644 index e305f8e7a..000000000 --- a/app/views/home/initial_state.json.rabl +++ /dev/null @@ -1,37 +0,0 @@ -object false - -node(:meta) do - { - streaming_api_base_url: @streaming_api_base_url, - access_token: @token, - locale: I18n.locale, - domain: site_hostname, - me: current_account.id, - admin: @admin.try(:id), - boost_modal: current_account.user.setting_boost_modal, - delete_modal: current_account.user.setting_delete_modal, - auto_play_gif: current_account.user.setting_auto_play_gif, - } -end - -node(:compose) do - { - me: current_account.id, - default_privacy: current_account.user.setting_default_privacy, - } -end - -node(:accounts) do - store = {} - store[current_account.id] = partial('api/v1/accounts/show', object: current_account) - store[@admin.id] = partial('api/v1/accounts/show', object: @admin) unless @admin.nil? - store -end - -node(:media_attachments) do - { - accept_content_types: MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES - } -end - -node(:settings) { @web_settings } diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index f991bc74f..ef97fb127 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -18,8 +18,16 @@ = ' - ' = title - = stylesheet_pack_tag 'application', media: 'all' + = stylesheet_pack_tag 'common', media: 'all' = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' + + = javascript_pack_tag 'features/getting_started', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag 'features/compose', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag 'features/home_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag 'features/notifications', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag 'features/community_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag 'features/public_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous' = csrf_meta_tags diff --git a/app/views/layouts/auth.html.haml b/app/views/layouts/auth.html.haml index e5429a8ed..b4f1bd0f3 100644 --- a/app/views/layouts/auth.html.haml +++ b/app/views/layouts/auth.html.haml @@ -6,7 +6,8 @@ .logo-container %h1 = link_to root_path do - = image_tag asset_pack_path('logo.png') + = image_tag asset_pack_path('logo.svg') + Mastodon .form-container = render 'flashes' diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml index 5680c1ff9..4826f32f7 100644 --- a/app/views/layouts/embedded.html.haml +++ b/app/views/layouts/embedded.html.haml @@ -2,7 +2,7 @@ %html{ lang: I18n.locale } %head %meta{ charset: 'utf-8' }/ - = stylesheet_pack_tag 'application', media: 'all' + = stylesheet_pack_tag 'common', media: 'all' = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous' = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index 721ce6a21..56a261ab6 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -24,6 +24,8 @@ = f.input :setting_default_privacy, collection: Status.visibilities.keys - ['direct'], wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), content_tag(:span, I18n.t("statuses.visibilities.#{visibility}_long"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' + = f.input :setting_default_sensitive, as: :boolean, wrapper: :with_label + .fields-group = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff| = ff.input :follow, as: :boolean, wrapper: :with_label @@ -44,6 +46,7 @@ .fields-group = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label + = f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/stream_entries/_content_spoiler.html.haml b/app/views/stream_entries/_content_spoiler.html.haml index 0bd6314d0..fb42d3f57 100644 --- a/app/views/stream_entries/_content_spoiler.html.haml +++ b/app/views/stream_entries/_content_spoiler.html.haml @@ -1,3 +1,7 @@ -.media-spoiler>< - %span= t('stream_entries.sensitive_content') - %span= t('stream_entries.click_to_show') +.media-spoiler-wrapper{ class: sensitive == false && 'media-spoiler-wrapper__visible' }>< + .spoiler-button + .icon-button.overlayed + %i.fa.fa-fw.fa-eye + .media-spoiler + %span= t('stream_entries.sensitive_content') + %span= t('stream_entries.click_to_show') diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index 589d647ce..157a7e7fb 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -18,13 +18,11 @@ - unless status.media_attachments.empty? - if status.media_attachments.first.video? .video-player>< - - if status.sensitive? - = render partial: 'stream_entries/content_spoiler' + = render partial: 'stream_entries/content_spoiler', locals: { sensitive: status.sensitive? } %video.u-video{ src: status.media_attachments.first.file.url(:original), loop: true } - else .detailed-status__attachments>< - - if status.sensitive? - = render partial: 'stream_entries/content_spoiler' + = render partial: 'stream_entries/content_spoiler', locals: { sensitive: status.sensitive? } .status__attachments__inner< - status.media_attachments.each do |media| = render partial: 'stream_entries/media', locals: { media: media } diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index 88750180f..b44f9820f 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -23,8 +23,7 @@ - unless status.media_attachments.empty? .status__attachments>< - - if status.sensitive? - = render partial: 'stream_entries/content_spoiler' + = render partial: 'stream_entries/content_spoiler', locals: { sensitive: status.sensitive? } - if status.media_attachments.first.video? .status__attachments__inner< .video-item< diff --git a/app/views/user_mailer/confirmation_instructions.fr.html.erb b/app/views/user_mailer/confirmation_instructions.fr.html.erb index b0b3d0f51..fe3f0a010 100644 --- a/app/views/user_mailer/confirmation_instructions.fr.html.erb +++ b/app/views/user_mailer/confirmation_instructions.fr.html.erb @@ -5,10 +5,10 @@ <p>Pour confirmer votre inscription, merci de cliquer sur le lien suivant : <br> <%= link_to 'Confirmer mon compte', confirmation_url(@resource, confirmation_token: @token) %></p> -<p>Après votre première connexion, vous pourrez accéder à la documentation de l'outil.</p> +<p>Après votre première connexion, vous pourrez accéder à la documentation de l’outil.</p> <p>Pensez également à jeter un œil à nos <%= link_to 'conditions d\'utilisation', terms_url %>.</p> <p>Amicalement,</p> -<p>L'équipe <%= @instance %></p> \ No newline at end of file +<p>L’équipe <%= @instance %></p> diff --git a/app/views/user_mailer/confirmation_instructions.fr.text.erb b/app/views/user_mailer/confirmation_instructions.fr.text.erb index cf8e39689..7730715f8 100644 --- a/app/views/user_mailer/confirmation_instructions.fr.text.erb +++ b/app/views/user_mailer/confirmation_instructions.fr.text.erb @@ -5,10 +5,10 @@ Vous venez de vous créer un compte sur <%= @instance %> et nous vous en remerci Pour confirmer votre inscription, merci de cliquer sur le lien suivant : <%= confirmation_url(@resource, confirmation_token: @token) %> -Après votre première connexion, vous pourrez accéder à la documentation de l'outil. +Après votre première connexion, vous pourrez accéder à la documentation de l’outil. -Pour rappel, nos conditions d'utilisation sont indiquées ici <%= terms_url %> +Pour rappel, nos conditions d’utilisation sont indiquées ici <%= terms_url %> Amicalement, -L'équipe <%= @instance %> \ No newline at end of file +L’équipe <%= @instance %> diff --git a/app/views/user_mailer/password_change.pl.html.erb b/app/views/user_mailer/password_change.pl.html.erb index 46423483a..a7cb15a05 100644 --- a/app/views/user_mailer/password_change.pl.html.erb +++ b/app/views/user_mailer/password_change.pl.html.erb @@ -1,3 +1,3 @@ <p>Witaj, <%= @resource.email %>!</p> -<p>Informujemy, że ostatnio zmieniono Twoje hasło Mastodona.</p> +<p>Informujemy, że ostatnio zmieniono Twoje hasło na <%= @instance %>.</p> diff --git a/app/views/user_mailer/password_change.pl.text.erb b/app/views/user_mailer/password_change.pl.text.erb index 85d5e1175..bd2efee0f 100644 --- a/app/views/user_mailer/password_change.pl.text.erb +++ b/app/views/user_mailer/password_change.pl.text.erb @@ -1,3 +1,3 @@ Witaj, <%= @resource.email %>! -Informujemy, że ostatnio zmieniono Twoje hasło Mastodona. +Informujemy, że ostatnio zmieniono Twoje hasło na <%= @instance %>. diff --git a/app/views/user_mailer/reset_password_instructions.fr.html.erb b/app/views/user_mailer/reset_password_instructions.fr.html.erb index 95789e387..db55c5884 100644 --- a/app/views/user_mailer/reset_password_instructions.fr.html.erb +++ b/app/views/user_mailer/reset_password_instructions.fr.html.erb @@ -1,8 +1,8 @@ <p>Bonjour <%= @resource.email %> !</p> -<p>Quelqu'un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous.</p> +<p>Quelqu’un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous.</p> <p><%= link_to 'Modifier mon mot de passe', edit_password_url(@resource, reset_password_token: @token) %></p> -<p>Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer ce message.</p> -<p>Votre mot de passe ne sera pas modifié tant que vous n'accéderez pas au lien ci-dessus et n'en choisirez pas un nouveau.</p> +<p>Si vous n’êtes pas à l’origine de cette demande, vous pouvez ignorer ce message.</p> +<p>Votre mot de passe ne sera pas modifié tant que vous n’accéderez pas au lien ci-dessus et n’en choisirez pas un nouveau.</p> diff --git a/app/views/user_mailer/reset_password_instructions.fr.text.erb b/app/views/user_mailer/reset_password_instructions.fr.text.erb index 73160cb4c..07fa3644a 100644 --- a/app/views/user_mailer/reset_password_instructions.fr.text.erb +++ b/app/views/user_mailer/reset_password_instructions.fr.text.erb @@ -1,8 +1,8 @@ Bonjour <%= @resource.email %> ! -Quelqu'un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous. +Quelqu’un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous. <%= edit_password_url(@resource, reset_password_token: @token) %> -Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer ce message. -Votre mot de passe ne sera pas modifié tant que vous n'accéderez pas au lien ci-dessus et n'en choisirez pas un nouveau. +Si vous n’êtes pas à l’origine de cette demande, vous pouvez ignorer ce message. +Votre mot de passe ne sera pas modifié tant que vous n’accéderez pas au lien ci-dessus et n’en choisirez pas un nouveau. diff --git a/app/views/user_mailer/reset_password_instructions.pl.html.erb b/app/views/user_mailer/reset_password_instructions.pl.html.erb index f4d67c724..2a9913a1d 100644 --- a/app/views/user_mailer/reset_password_instructions.pl.html.erb +++ b/app/views/user_mailer/reset_password_instructions.pl.html.erb @@ -1,6 +1,7 @@ <p>Witaj, <%= @resource.email %>!</p> -<p>Ktoś próbował zmienić Twoje hasło na Mastodonie. Możesz zrobić to klikając w poniższy link.</p> +<p>Ktoś próbował zmienić Twoje hasło na <%= @instance %>. Możesz zrobić to klikając w +poniższy link.</p> <p><%= link_to 'Zmień moje hasło', edit_password_url(@resource, reset_password_token: @token) %></p> diff --git a/app/views/user_mailer/reset_password_instructions.pl.text.erb b/app/views/user_mailer/reset_password_instructions.pl.text.erb index 78d1cab0b..2b34afc48 100644 --- a/app/views/user_mailer/reset_password_instructions.pl.text.erb +++ b/app/views/user_mailer/reset_password_instructions.pl.text.erb @@ -1,6 +1,7 @@ Witaj, <%= @resource.email %>! -Ktoś próbował zmienić Twoje hasło na Mastodonie. Możesz zrobić to klikając w poniższy link. +Ktoś próbował zmienić Twoje hasło na <%= @instance %>. Możesz zrobić to klikając w +poniższy link. <%= edit_password_url(@resource, reset_password_token: @token) %> diff --git a/app/workers/pubsubhubbub/subscribe_worker.rb b/app/workers/pubsubhubbub/subscribe_worker.rb index 5b0956b6b..6865e7136 100644 --- a/app/workers/pubsubhubbub/subscribe_worker.rb +++ b/app/workers/pubsubhubbub/subscribe_worker.rb @@ -3,7 +3,20 @@ class Pubsubhubbub::SubscribeWorker include Sidekiq::Worker - sidekiq_options queue: 'push' + sidekiq_options queue: 'push', retry: 10, unique: :until_executed + + sidekiq_retry_in do |count| + case count + when 0 + 30.minutes.seconds + when 1 + 2.hours.seconds + when 2 + 12.hours.seconds + else + 24.hours.seconds * (count - 2) + end + end def perform(account_id) account = Account.find(account_id) diff --git a/app/workers/push_update_worker.rb b/app/workers/push_update_worker.rb index fbcdcf634..697cbd6a6 100644 --- a/app/workers/push_update_worker.rb +++ b/app/workers/push_update_worker.rb @@ -6,7 +6,7 @@ class PushUpdateWorker def perform(account_id, status_id) account = Account.find(account_id) status = Status.find(status_id) - message = InlineRenderer.render(status, account, 'api/v1/statuses/show') + message = InlineRenderer.render(status, account, :status) Redis.current.publish("timeline:#{account.id}", Oj.dump(event: :update, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i)) rescue ActiveRecord::RecordNotFound diff --git a/bin/webpack-dev-server b/bin/webpack-dev-server index a867f2c01..0beec3175 100755 --- a/bin/webpack-dev-server +++ b/bin/webpack-dev-server @@ -23,7 +23,7 @@ end begin dev_server = YAML.load_file(CONFIG_FILE)["development"]["dev_server"] - DEV_SERVER_HOST = "http#{"s" if args('--https') || dev_server["https"]}://#{args('--host') || dev_server["host"]}:#{args('--port') || dev_server["port"]}" + DEV_SERVER_HOST = "http#{"s" if args('--https') || dev_server["https"]}://#{dev_server["host"]}:#{args('--port') || dev_server["port"]}" rescue Errno::ENOENT, NoMethodError puts "Webpack dev_server configuration not found in #{CONFIG_FILE}." diff --git a/boxfile.yml b/boxfile.yml index ef847d4a0..330223110 100644 --- a/boxfile.yml +++ b/boxfile.yml @@ -153,8 +153,59 @@ worker.sidekiq: data.db: image: nanobox/postgresql:9.5 + cron: + - id: backup + schedule: '0 3 * * *' + command: | + PGPASSWORD=${DATA_POSTGRES_PASS} pg_dump -U ${DATA_POSTGRES_USER} -w -Fc -O gonano | + gzip | + curl -k -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/backup-${HOSTNAME}-$(date -u +%Y-%m-%d.%H-%M-%S).sql.gz --data-binary @- && + curl -k -s -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/ | + json_pp | + grep ${HOSTNAME} | + sort | + head -n-${BACKUP_COUNT:-1} | + sed 's/.*: "\(.*\)".*/\1/' | + while read file + do + curl -k -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/${file} -X DELETE + done + data.redis: image: nanobox/redis:3.0 + cron: + - id: backup + schedule: '0 3 * * *' + command: | + curl -k -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/backup-${HOSTNAME}-$(date -u +%Y-%m-%d.%H-%M-%S).rdb --data-binary @/data/var/db/redis/dump.rdb && + curl -k -s -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/ | + json_pp | + grep ${HOSTNAME} | + sort | + head -n-${BACKUP_COUNT:-1} | + sed 's/.*: "\(.*\)".*/\1/' | + while read file + do + curl -k -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/${file} -X DELETE + done + data.storage: image: nanobox/unfs:0.9 + + cron: + - id: backup + schedule: '0 3 * * *' + command: | + tar cz -C /data/var/db/unfs/ | + curl -k -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/backup-${HOSTNAME}-$(date -u +%Y-%m-%d.%H-%M-%S).tgz --data-binary @- && + curl -k -s -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/ | + json_pp | + grep ${HOSTNAME} | + sort | + head -n-${BACKUP_COUNT:-1} | + sed 's/.*: "\(.*\)".*/\1/' | + while read file + do + curl -k -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/${file} -X DELETE + done diff --git a/config/application.rb b/config/application.rb index dd63017b3..ed5fdb7f7 100644 --- a/config/application.rb +++ b/config/application.rb @@ -45,6 +45,7 @@ module Mastodon :io, :it, :ja, + :ko, :nl, :no, :oc, diff --git a/config/environments/development.rb b/config/environments/development.rb index c81cf7bbe..406fa970b 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -62,6 +62,8 @@ Rails.application.configure do # routes, locales, etc. This feature depends on the listen gem. # config.file_watcher = ActiveSupport::EventedFileUpdateChecker + config.action_mailer.default_options = { from: 'notifications@localhost' } + # If using a Heroku, Vagrant or generic remote development environment, # use letter_opener_web, accessible at /letter_opener. # Otherwise, use letter_opener, which launches a browser window to view sent mail. diff --git a/config/environments/production.rb b/config/environments/production.rb index 68229531d..d71e410bf 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -73,6 +73,8 @@ Rails.application.configure do config.action_mailer.perform_caching = false # E-mails + config.action_mailer.default_options = { from: ENV.fetch('SMTP_FROM_ADDRESS') } + config.action_mailer.smtp_settings = { :port => ENV['SMTP_PORT'], :address => ENV['SMTP_SERVER'], diff --git a/config/environments/test.rb b/config/environments/test.rb index db98263a6..bde69eba1 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -30,6 +30,8 @@ Rails.application.configure do config.action_controller.allow_forgery_protection = false config.action_mailer.perform_caching = false + config.action_mailer.default_options = { from: 'notifications@localhost' } + # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. diff --git a/config/initializers/active_model_serializers.rb b/config/initializers/active_model_serializers.rb new file mode 100644 index 000000000..b0230267d --- /dev/null +++ b/config/initializers/active_model_serializers.rb @@ -0,0 +1,3 @@ +ActiveModelSerializers.config.tap do |config| + config.default_includes = '**' +end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index d51471d30..bf61ea0ea 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -1,17 +1,29 @@ Warden::Manager.after_set_user except: :fetch do |user, warden| - SessionActivation.deactivate warden.raw_session['auth_id'] - warden.raw_session['auth_id'] = user.activate_session(warden.request) + SessionActivation.deactivate warden.cookies.signed['_session_id'] + + warden.cookies.signed['_session_id'] = { + value: user.activate_session(warden.request), + expires: 1.year.from_now, + httponly: true, + } end Warden::Manager.after_fetch do |user, warden| - unless user.session_active?(warden.raw_session['auth_id']) + if user.session_active?(warden.cookies.signed['_session_id'] || warden.raw_session['auth_id']) + warden.cookies.signed['_session_id'] = { + value: warden.cookies.signed['_session_id'] || warden.raw_session['auth_id'], + expires: 1.year.from_now, + httponly: true, + } + else warden.logout throw :warden, message: :unauthenticated end end Warden::Manager.before_logout do |_, warden| - SessionActivation.deactivate warden.raw_session['auth_id'] + SessionActivation.deactivate warden.cookies.signed['_session_id'] + warden.cookies.delete('_session_id') end Devise.setup do |config| diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb index ca7531748..5983918cd 100644 --- a/config/initializers/simple_form.rb +++ b/config/initializers/simple_form.rb @@ -50,14 +50,14 @@ SimpleForm.setup do |config| # b.use :full_error, wrap_with: { tag: :span, class: :error } end - config.wrappers :with_label, class: :input, hint_class: :field_with_hint, error_class: :field_with_errors do |b| + config.wrappers :with_label, class: [:input, :with_label], hint_class: :field_with_hint, error_class: :field_with_errors do |b| b.use :html5 b.use :label_input, wrap_with: { tag: :div, class: :label_input } b.use :hint, wrap_with: { tag: :span, class: :hint } b.use :error, wrap_with: { tag: :span, class: :error } end - config.wrappers :with_block_label, class: :input, hint_class: :field_with_hint, error_class: :field_with_errors do |b| + config.wrappers :with_block_label, class: [:input, :with_block_label], hint_class: :field_with_hint, error_class: :field_with_errors do |b| b.use :html5 b.use :label b.use :hint, wrap_with: { tag: :span, class: :hint } diff --git a/config/locales/activerecord.fr.yml b/config/locales/activerecord.fr.yml new file mode 100644 index 000000000..858777c0e --- /dev/null +++ b/config/locales/activerecord.fr.yml @@ -0,0 +1,12 @@ +fr: + activerecord: + errors: + models: + account: + attributes: + username: + invalid: seulement des lettres, des nombres et des tirets bas + status: + attributes: + reblog: + taken: du statut existe déjà diff --git a/config/locales/activerecord.ja.yml b/config/locales/activerecord.ja.yml index 6e6b48496..975912f0f 100644 --- a/config/locales/activerecord.ja.yml +++ b/config/locales/activerecord.ja.yml @@ -1,5 +1,8 @@ ja: activerecord: + attributes: + user: + email: メールアドレス errors: models: account: diff --git a/config/locales/activerecord.pl.yml b/config/locales/activerecord.pl.yml index 627f612bb..f82e1b875 100644 --- a/config/locales/activerecord.pl.yml +++ b/config/locales/activerecord.pl.yml @@ -1,5 +1,8 @@ pl: activerecord: + attributes: + user: + email: adres e-mail errors: models: account: diff --git a/config/locales/ar.yml b/config/locales/ar.yml index 4d045dee1..d395dc9c3 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -3,23 +3,12 @@ ar: about: about_mastodon: ماستدون شبكة إجتماعية <em>حرة و مفتوحة المصدر</em>. هو بديل <em>لامركزي</em> لمنصات تجارية ، يمكنك من تجنب احتكار شركة واحدة للإتصالات الخاصة بك. يمكنك اختيار أي خادم تثق فيه. أيهما تختار، يمكنك التفاعل مع أي شخص آخر على الشبكة. يمكن لأي شخص تنصيب و تشغيل خادم ماستدون خاص به والمشاركة في <em>الشبكات الاجتماعية</em> بكل شفافية. about_this: عن مثيل الخادوم هذا - apps: التطبيقات business_email: 'البريد الإلكتروني المهني :' closed_registrations: التسجيلات في مثيل الخادوم هذا مُغلقة حاليًا. contact: للتواصل معنا description_headline: ما هو %{domain}? domain_count_after: خوادم أخرى domain_count_before: متصل بـ - features: - api: واجهة برمجة مفتوحة للتطبيقات والخدمات - blocks: نص منسق وأدوات كتم - characters: 500 حرف في كل رسالة - chronology: خيوط متسلسلة زمنيا - ethics: 'تصميم أخلاقي : لا إعلانات و لا تعقُّب' - gifv: مجموعات صور GIFV وأشرطة فيديو قصيرة - privacy: إعدادات مدققة لخصوصية كل منشور - public: الخيوط الزمنية العمومية - features_headline: ما الذي يجعل ماستدون فريدًا ؟ get_started: إبدأ الآن links: الروابط other_instances: خوادم أخرى @@ -93,7 +82,7 @@ ar: blocking: قائمة المحظورين following: قائمة المستخدمين المتبوعين upload: تحميل - landing_strip_html: <strong>%{name}</strong> is a user on %{link_to_root_path}. You can follow them or interact with them if you have an account anywhere in the fediverse.. + landing_strip_html: "<strong>%{name}</strong> is a user on %{link_to_root_path}. You can follow them or interact with them if you have an account anywhere in the fediverse.." landing_strip_signup_html: If you don't, you can <a href="%{sign_up_path}">sign up here</a>. media_attachments: validations: diff --git a/config/locales/bg.yml b/config/locales/bg.yml index 76d6a2605..042d609b0 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -3,23 +3,12 @@ bg: about: about_mastodon: Mastodon е <em>безплатен</em> сървър с <em>отворен код</em> за социални мрежи. Като <em>децентрализирана</em> алтернатива на комерсиалните платформи, той позволява избягването на риска от монополизация на твоята комуникация от единични компании. Изберете си сървър, на който се доверявате, и ще можете да контактувате с всички останали. Всеки може да пусне Mastodon и лесно да вземе участие в <em>социалната мрежа</em>. about_this: За тази инстанция - apps: Приложения business_email: 'Служебен e-mail:' closed_registrations: В момента регистрациите за тази инстанция са затворени. contact: За контакти description_headline: Какво е %{domain}? domain_count_after: други инстанции domain_count_before: Свързани към - features: - api: Отворено API за приложения и услуги - blocks: Богат на инструменти за блокиране и заглушаване - characters: Публикации от 500 символа - chronology: Публикациите се показват хронологично - ethics: 'Етичен дизайн: без реклами и проследяване' - gifv: GIFV комплекти и кратки видео клипове - privacy: Настройване на поверителността за всяка публикация - public: Публични канали - features_headline: Какво откроява Mastodon get_started: Първи стъпки links: Връзки other_instances: Други инстанции @@ -93,7 +82,7 @@ bg: blocking: Списък на блокираните following: Списък на последователите upload: Качване - landing_strip_html: <strong>%{name}</strong> е потребител от %{link_to_root_path}. Можеш да ги следваш, или да контактуваш с тях, ако имаш акаунт където и да е из федерираната вселена на Mastodon. + landing_strip_html: "<strong>%{name}</strong> е потребител от %{link_to_root_path}. Можеш да ги следваш, или да контактуваш с тях, ако имаш акаунт където и да е из федерираната вселена на Mastodon." landing_strip_signup_html: Ако нямаш акаунт, можеш да си <a href="%{sign_up_path}">създадеш ето тук</a>. media_attachments: validations: diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 2fbc63ef9..f63aee3e6 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -3,23 +3,12 @@ ca: about: about_mastodon: Mastodon és un servidor de xarxa social <em>lliure i de codi obert</em>. Una alternativa <em>descentralitzada</em> a plataformes comercials, que evita el risc que una única companyia monopolitzi la teva comunicació. Qualsevol pot executar Mastodon i participar sense problemes en la <em>xarxa social</em>. about_this: Sobre aquesta instància - apps: Apps business_email: 'Adreça de contacte:' closed_registrations: Els registres estan actualment tancats en aquesta instància. contact: Contacte description_headline: Què es %{domain}? domain_count_after: altres instàncies domain_count_before: Connectat a - features: - api: API pública per a aplicacions i serveis - blocks: Moderació de contingut - characters: 500 caràcters per publicació - chronology: Les histories son cronològiques - ethics: 'Disseny ètic: sense anuncis, sense rastrejos' - gifv: Vídeos curts i GIFV - privacy: Configuracions de privacitat ajustables - public: Història federada - features_headline: El que distingeix a Mastodon get_started: Començar links: Vincles other_instances: Altres instàncies @@ -166,17 +155,13 @@ ca: settings: contact_information: email: Introduir una adreça de correu electrònic pùblica - label: Informació de contacte username: Introduir un nom d'usuari registrations: closed_message: desc_html: Apareix en la primera pàgina quan es tanquen els registres<br>Pot utilitzar etiquetes HTML title: Missatge de registre tancat open: - disabled: Desactivat - enabled: Activat title: Registre obert - setting: Ajust site_description: desc_html: Es mostra com un paràgraf a la pàgina principal i s'utilitza com una etiqueta meta.<br>Pots utilitzar etiquetes HTML, en particular <code><a></code> i <code><em></code>. title: Descripció del lloc @@ -203,8 +188,8 @@ ca: change_password: Canviar contrasenya delete_account: Esborrar el compte delete_account_html: Si vols esborrar el teu compte pots <a href="%{path}">fer-ho aquí</a>. S'et demanarà confirmació. - didnt_get_confirmation: "No vas rebre el correu de confirmació?" - forgot_password: "Has oblidat la contrasenya?" + didnt_get_confirmation: No vas rebre el correu de confirmació? + forgot_password: Has oblidat la contrasenya? login: Iniciar sessió logout: Tancar sessió register: Enregistrarse diff --git a/config/locales/de.yml b/config/locales/de.yml index f2841d0b7..b084aca31 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -3,23 +3,12 @@ de: about: about_mastodon: Mastodon ist ein <em>freier, quelloffener</em> sozialer Netzwerkserver. Als <em>dezentralisierte</em> Alternative zu kommerziellen Plattformen verhindert es die Risiken, die entstehen, wenn eine einzelne Firma deine Kommunikation monopolisiert. Jeder kann Mastodon verwenden und ganz einfach am <em>sozialen Netzwerk</em> teilnehmen. about_this: Über diese Instanz - apps: Apps business_email: 'Geschäftliche E-Mail:' closed_registrations: Die Registrierung ist auf dieser Instanz momentan geschlossen. contact: Kontakt description_headline: Was ist %{domain}? domain_count_after: andere Instanzen domain_count_before: Verbunden mit - features: - api: Offene API für Apps und Dienste - blocks: Mächtige Block- und Stummschaltungswerkzeuge - characters: 500 Zeichen pro Beitrag - chronology: Zeitleisten sind chronologisch - ethics: 'Ethisches Design: keine Werbung, kein Tracking' - gifv: GIFV-Sets und kurze Videos - privacy: Granulare Privatsphäre-Einstellungen für jeden Beitrag - public: Öffentliche Zeitleisten - features_headline: Was Mastodon einzigartig macht get_started: Erste Schritte links: Links other_instances: Andere Instanzen @@ -140,17 +129,13 @@ de: settings: contact_information: email: Eine öffentliche E-Mail-Adresse angeben - label: Kontaktinformationen username: Einen Benutzernamen angeben registrations: closed_message: desc_html: Wird auf der Frontseite angezeigt, wenn die Registrierung geschlossen ist<br>Du kannst HTML-Tags benutzen title: Nachricht über geschlossene Registrierung open: - disabled: Deaktiviert - enabled: Aktiviert title: Offene Registrierung - setting: Einstellung site_description: desc_html: Wird als Absatz auf der Frontseite angezeigt und als Meta-Tag benutzt.<br>Du kannst HTML-Tags benutzen, insbesondere <code><a></code> und <code><em></code>. title: Seitenbeschreibung diff --git a/config/locales/devise.fr.yml b/config/locales/devise.fr.yml index c4dbc62e0..6805e4f38 100644 --- a/config/locales/devise.fr.yml +++ b/config/locales/devise.fr.yml @@ -3,8 +3,8 @@ fr: devise: confirmations: confirmed: Votre compte a été validé. - send_instructions: Vous allez recevoir les instructions nécessaires à la confirmation de votre compte dans quelques minutes. - send_paranoid_instructions: Si votre adresse e-mail existe dans notre base de données, vous allez bientôt recevoir un courriel contenant les instructions de confirmation de votre compte. + send_instructions: Vous allez recevoir les instructions nécessaires à la confirmation de votre compte dans quelques minutes. S’il vous plaît, dans le cas où vous ne recevriez pas ce message, vérifiez votre dossier d’indésirables. + send_paranoid_instructions: Si votre adresse électronique existe dans notre base de données, vous allez bientôt recevoir un courriel contenant les instructions de confirmation de votre compte. S’il vous plaît, dans le cas où vous ne recevriez pas ce message, vérifiez votre dossier d’indésirables. failure: already_authenticated: Vous êtes déjà connecté⋅e inactive: Votre compte n’est pas encore activé. @@ -25,12 +25,12 @@ fr: unlock_instructions: subject: Instructions pour déverrouiller votre compte omniauth_callbacks: - failure: 'Nous n’avons pas pu vous authentifier via %{kind} : ''%{reason}''.' + failure: 'Nous n’avons pas pu vous authentifier via %{kind} : ''%{reason}''.' success: Authentifié avec succès via %{kind}. passwords: - no_token: Vous ne pouvez accéder à cette page sans passer par un courriel de réinitialisation de mot de passe. Si vous êtes passé⋅e par un courriel de ce type, assurez-vous d'utiliser l'URL complète. - send_instructions: Vous allez recevoir les instructions de réinitialisation du mot de passe dans quelques instants - send_paranoid_instructions: Si votre addresse e-mail existe dans notre base de données, vous allez recevoir un lien de réinitialisation par courriel + no_token: Vous ne pouvez accéder à cette page sans passer par un courriel de réinitialisation de mot de passe. Si vous êtes passé⋅e par un courriel de ce type, assurez-vous d’utiliser l’URL complète. + send_instructions: Vous allez recevoir les instructions de réinitialisation du mot de passe dans quelques instants. S’il vous plaît, dans le cas où vous ne recevriez pas ce message, vérifiez votre dossier d’indésirables. + send_paranoid_instructions: Si votre addresse électronique existe dans notre base de données, vous allez recevoir un lien de réinitialisation par courriel. S’il vous plaît, dans le cas où vous ne recevriez pas ce message, vérifiez votre dossier d’indésirables. updated: Votre mot de passe a été modifié avec succès, vous êtes maintenant connecté⋅e updated_not_active: Votre mot de passe a été modifié avec succès. registrations: @@ -46,8 +46,8 @@ fr: signed_in: Connecté. signed_out: Déconnecté. unlocks: - send_instructions: Vous allez recevoir les instructions nécessaires au déverrouillage de votre compte dans quelques instants - send_paranoid_instructions: Si votre compte existe, vous allez bientôt recevoir un courriel contenant les instructions pour le déverrouiller. + send_instructions: Vous allez recevoir les instructions nécessaires au déverrouillage de votre compte dans quelques instants. S’il vous plaît, dans le cas où vous ne recevriez pas ce message, vérifiez votre dossier d’indésirables. + send_paranoid_instructions: Si votre compte existe, vous allez bientôt recevoir un courriel contenant les instructions pour le déverrouiller. S’il vous plaît, dans le cas où vous ne recevriez pas ce message, vérifiez votre dossier d’indésirables. unlocked: Votre compte a été déverrouillé avec succès, vous êtes maintenant connecté⋅e. errors: messages: diff --git a/config/locales/devise.pl.yml b/config/locales/devise.pl.yml index 792e0d81e..1c692f7a8 100644 --- a/config/locales/devise.pl.yml +++ b/config/locales/devise.pl.yml @@ -8,10 +8,10 @@ pl: failure: already_authenticated: Jesteś już zalogowany/zalogowana. inactive: Twoje konto nie zostało jeszcze aktywowane. - invalid: Błędne %{authentication_keys} lub hasło. + invalid: Nieprawidłowy %{authentication_keys} lub hasło. last_attempt: Masz jeszcze jedną próbę; Twoje konto zostanie zablokowane jeśli się nie powiedzie. locked: Twoje konto zostało zablokowane. - not_found_in_database: Błędne %{authentication_keys} lub hasło. + not_found_in_database: Nieprawidłowy %{authentication_keys} lub hasło. timeout: Twoja sesja wygasła. Zaloguj się ponownie aby kontynuować.. unauthenticated: Zapisz się lub zaloguj aby kontynuować. unconfirmed: Zweryfikuj adres e-mail aby kontynuować. diff --git a/config/locales/doorkeeper.fr.yml b/config/locales/doorkeeper.fr.yml index 24538bc48..0e74532c1 100644 --- a/config/locales/doorkeeper.fr.yml +++ b/config/locales/doorkeeper.fr.yml @@ -6,12 +6,12 @@ fr: remote_follow: attributes: acct: - blank: Le nom d'utilisateur ne doit pas être vide + blank: Le nom d’utilisateur ne doit pas être vide activerecord: attributes: doorkeeper/application: name: Nom - redirect_uri: L'URL de redirection + redirect_uri: L’URL de redirection errors: messages: record_invalid: Données invalides @@ -50,17 +50,17 @@ fr: edit: Modifier submit: Envoyer confirmations: - destroy: Êtes-vous certain ? + destroy: Êtes-vous certain·e ? edit: - title: Modifier l'application + title: Modifier l’application form: - error: Oups ! Vérifier votre formulaire pour des erreurs possibles + error: Oups ! Vérifier votre formulaire pour des erreurs possibles help: native_redirect_uri: Utiliser %{native_redirect_uri} pour les tests locaux redirect_uri: Utiliser une ligne par URL scopes: Séparer les portées avec des espaces. Laisser vide pour utiliser les portées par défaut. index: - callback_url: URL de retour d'appel + callback_url: URL de retour d’appel name: Nom new: Nouvelle application title: Vos applications @@ -68,11 +68,11 @@ fr: title: Nouvelle application show: actions: Actions - application_id: ID de l'application - callback_urls: URL du retour d'appel + application_id: ID de l’application + callback_urls: URL du retour d’appel scopes: Portées secret: Secret - title: 'Application : %{name}' + title: 'Application : %{name}' authorizations: buttons: authorize: Autoriser @@ -81,15 +81,15 @@ fr: title: Une erreur est survenue new: able_to: Cette application pourra - prompt: Autoriser %{client_name} à utiliser votre compte ? + prompt: Autoriser %{client_name} à utiliser votre compte ? title: Autorisation requise show: - title: Code d'autorisation + title: Code d’autorisation authorized_applications: buttons: revoke: Annuler confirmations: - revoke: Êtes-vous certain ? + revoke: Êtes-vous certain·e ? index: application: Application created_at: Créé le @@ -98,24 +98,24 @@ fr: title: Vos applications autorisées errors: messages: - access_denied: Le propriétaire de la ressource ou le serveur d'autorisation a refusé la requête. - credential_flow_not_configured: Le flux des identifiants du mot de passe du propriétaire de la ressource a échoué car Doorkeeper.configure.resource_owner_from_credentials n'est pas configuré. - invalid_client: L'authentification du client a échoué à cause d'un client inconnu, d'aucune authentification de client incluse ou d'une méthode d'authentification non prise en charge. - invalid_grant: Le consentement d'autorisation accordé n'est pas valide, a expiré, est annulé, ne concorde pas avec l'URL de redirection utilisée dans la requête d'autorisation ou a été émis à un autre client. - invalid_redirect_uri: L'URL de redirection n'est pas valide. + access_denied: Le propriétaire de la ressource ou le serveur d’autorisation a refusé la requête. + credential_flow_not_configured: Le flux des identifiants du mot de passe du propriétaire de la ressource a échoué car Doorkeeper.configure.resource_owner_from_credentials n’est pas configuré. + invalid_client: L’authentification du client a échoué à cause d’un client inconnu, d’aucune authentification de client incluse ou d’une méthode d’authentification non prise en charge. + invalid_grant: Le consentement d’autorisation accordé n’est pas valide, a expiré, est annulé, ne concorde pas avec l’URL de redirection utilisée dans la requête d’autorisation ou a été émis à un autre client. + invalid_redirect_uri: L’URL de redirection n’est pas valide. invalid_request: La requête omet un paramètre requis, inclut une valeur de paramètre non prise en charge ou est autrement mal formée. invalid_resource_owner: Les identifiants fournis par le propriétaire de la ressource ne sont pas valides ou le propriétaire de la ressource ne peut être trouvé - invalid_scope: La portée demandée n'est pas valide, est inconnue ou mal formée. + invalid_scope: La portée demandée n’est pas valide, est inconnue ou mal formée. invalid_token: - expired: Le jeton d'accès a expiré - revoked: Le jeton d'accès a été révoqué - unknown: Le jeton d'accès n'est pas valide - resource_owner_authenticator_not_configured: La recherche du propriétaire de la ressource a échoué car Doorkeeper.configure.resource_owner_authenticator n'est pas configuré. - server_error: Le serveur d'autorisation a rencontré une condition inattendue l'empêchant de faire aboutir la requête. - temporarily_unavailable: Le serveur d'autorisation est actuellement incapable de traiter la requête à cause d'une surcharge ou d'une maintenance temporaire du serveur. - unauthorized_client: Le client n'est pas autorisé à effectuer cette requête à l'aide de cette méthode. - unsupported_grant_type: Le type de consentement d'autorisation n'est pas pris en charge par le serveur d'autorisation. - unsupported_response_type: Le serveur d'autorisation ne prend pas en charge ce type de réponse. + expired: Le jeton d’accès a expiré + revoked: Le jeton d’accès a été révoqué + unknown: Le jeton d’accès n’est pas valide + resource_owner_authenticator_not_configured: La recherche du propriétaire de la ressource a échoué car Doorkeeper.configure.resource_owner_authenticator n’est pas configuré. + server_error: Le serveur d’autorisation a rencontré une condition inattendue l’empêchant de faire aboutir la requête. + temporarily_unavailable: Le serveur d’autorisation est actuellement incapable de traiter la requête à cause d’une surcharge ou d’une maintenance temporaire du serveur. + unauthorized_client: Le client n’est pas autorisé à effectuer cette requête à l’aide de cette méthode. + unsupported_grant_type: Le type de consentement d’autorisation n’est pas pris en charge par le serveur d’autorisation. + unsupported_response_type: Le serveur d’autorisation ne prend pas en charge ce type de réponse. flash: applications: create: diff --git a/config/locales/en.yml b/config/locales/en.yml index 944c24c60..c9b5d9ab8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,28 +1,30 @@ --- en: about: - about_mastodon: Mastodon is a <em>free, open-source</em> social network. A <em>decentralized</em> alternative to commercial platforms, it avoids the risks of a single company monopolizing your communication. Pick a server that you trust — whichever you choose, you can interact with everyone else. Anyone can run their own Mastodon instance and participate in the <em>social network</em> seamlessly. - about_this: About this instance - apps: Apps + about_mastodon_html: Mastodon is a social network based on open web protocols and free, open-source software. It is decentralized like e-mail. + about_this: About business_email: 'Business e-mail:' - closed_registrations: Registrations are currently closed on this instance. + closed_registrations: Registrations are currently closed on this instance. However! You can find a different instance to make an account on and get access to the very same network from there. contact: Contact description_headline: What is %{domain}? domain_count_after: other instances domain_count_before: Connected to features: - api: Open API for apps and services - blocks: Rich block and muting tools - characters: 500 characters per post - chronology: Timelines are chronological - ethics: 'Ethical design: no ads, no tracking' - gifv: GIFV sets and short videos - privacy: Granular, per-post privacy settings - public: Public timelines - features_headline: What sets Mastodon apart + humane_approach_body: Learning from failures of other networks, Mastodon aims to make ethical design choices to combat the misuse of social media. + humane_approach_title: A more humane approach + not_a_product_body: Mastodon is not a commercial network. No advertising, no data mining, no walled gardens. There is no central authority. + not_a_product_title: You’re a person, not a product + real_conversation_body: With 500 characters at your disposal and support for granular content and media warnings, you can express yourself the way you want to. + real_conversation_title: Built for real conversation + within_reach_body: Multiple apps for iOS, Android, and other platforms thanks to a developer-friendly API ecosystem allow you to keep up with your friends anywhere. + within_reach_title: Always within reach + find_another_instance: Find another instance + generic_description: "%{domain} is one server in the network" get_started: Get started + hosted_on: Mastodon hosted on %{domain} + learn_more: Learn more links: Links - other_instances: Other instances + other_instances: Instance list source_code: Source code status_count_after: statuses status_count_before: Who authored @@ -30,6 +32,7 @@ en: user_count_after: users user_count_before: Home to version: Version + what_is_mastodon: What is Mastodon? accounts: follow: Follow followers: Followers @@ -165,25 +168,31 @@ en: view: View settings: contact_information: - email: Enter a public e-mail address - label: Contact information - username: Enter a username + email: Business e-mail + username: Contact username registrations: closed_message: - desc_html: Displayed on frontpage when registrations are closed<br>You can use HTML tags + desc_html: Displayed on frontpage when registrations are closed. You can use HTML tags title: Closed registration message + deletion: + desc_html: Allow anyone to delete their account + title: Open account deletion open: - disabled: Disabled - enabled: Enabled + desc_html: Allow anyone to create an account title: Open registration - setting: Setting site_description: - desc_html: Displayed as a paragraph on the frontpage and used as a meta tag.<br>You can use HTML tags, in particular <code><a></code> and <code><em></code>. - title: Site description + desc_html: Introductory paragraph on the frontpage and in meta tags. You can use HTML tags, in particular <code><a></code> and <code><em></code>. + title: Instance description site_description_extended: - desc_html: Displayed on extended information page<br>You can use HTML tags - title: Extended site description - site_title: Site title + desc_html: A good place for your code of conduct, rules, guidelines and other things that set your instance apart. You can use HTML tags + title: Custom extended information + site_terms: + desc_html: You can write your own privacy policy, terms of service or other legalese. You can use HTML tags + title: Custom terms of service + site_title: Instance name + timeline_preview: + desc_html: Display public timeline on landing page + title: Timeline preview title: Site Settings subscriptions: callback_url: Callback URL @@ -204,6 +213,7 @@ en: applications: invalid_url: The provided URL is invalid auth: + agreement_html: By signing up you agree to <a href="%{rules_path}">our terms of service</a> and <a href="%{terms_path}">privacy policy</a>. change_password: Security delete_account: Delete account delete_account_html: If you wish to delete your account, you can <a href="%{path}">proceed here</a>. You will be asked for confirmation. @@ -218,6 +228,12 @@ en: authorize_follow: error: Unfortunately, there was an error looking up the remote account follow: Follow + follow_request: 'You have sent a follow request to:' + following: 'Success! You are now following:' + post_follow: + close: Or, you can just close this window. + return: Return to the user's profile + web: Go to web prompt_html: 'You (<strong>%{self}</strong>) have requested to follow:' title: Follow %{acct} datetime: @@ -387,6 +403,76 @@ en: click_to_show: Click to show reblogged: boosted sensitive_content: Sensitive content + terms: + body_html: | + <h2>Privacy Policy</h2> + + <h3 id="collect">What information do we collect?</h3> + + <p>We collect information from you when you register on our site and gather data when you participate in the forum by reading, writing, and evaluating the content shared here.</p> + + <p>When registering on our site, you may be asked to enter your name and e-mail address. You may, however, visit our site without registering. Your e-mail address will be verified by an email containing a unique link. If that link is visited, we know that you control the e-mail address.</p> + + <p>When registered and posting, we record the IP address that the post originated from. We also may retain server logs which include the IP address of every request to our server.</p> + + <h3 id="use">What do we use your information for?</h3> + + <p>Any of the information we collect from you may be used in one of the following ways:</p> + + <ul> + <li>To personalize your experience — your information helps us to better respond to your individual needs.</li> + <li>To improve our site — we continually strive to improve our site offerings based on the information and feedback we receive from you.</li> + <li>To improve customer service — your information helps us to more effectively respond to your customer service requests and support needs.</li> + <li>To send periodic emails — The email address you provide may be used to send you information, notifications that you request about changes to topics or in response to your user name, respond to inquiries, and/or other requests or questions.</li> + </ul> + + <h3 id="protect">How do we protect your information?</h3> + + <p>We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information.</p> + + <h3 id="data-retention">What is your data retention policy?</h3> + + <p>We will make a good faith effort to:</p> + + <ul> + <li>Retain server logs containing the IP address of all requests to this server no more than 90 days.</li> + <li>Retain the IP addresses associated with registered users and their posts no more than 5 years.</li> + </ul> + + <h3 id="cookies">Do we use cookies?</h3> + + <p>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.</p> + + <p>We use cookies to understand and save your preferences for future visits and compile aggregate data about site traffic and site interaction so that we can offer better site experiences and tools in the future. We may contract with third-party service providers to assist us in better understanding our site visitors. These service providers are not permitted to use the information collected on our behalf except to help us conduct and improve our business.</p> + + <h3 id="disclose">Do we disclose any information to outside parties?</h3> + + <p>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. However, non-personally identifiable visitor information may be provided to other parties for marketing, advertising, or other uses.</p> + + <h3 id="third-party">Third party links</h3> + + <p>Occasionally, at our discretion, we may include or offer third party products or services on our site. These third party sites have separate and independent privacy policies. We therefore have no responsibility or liability for the content and activities of these linked sites. Nonetheless, we seek to protect the integrity of our site and welcome any feedback about these sites.</p> + + <h3 id="coppa">Children's Online Privacy Protection Act Compliance</h3> + + <p>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 (<a href="https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act">Children's Online Privacy Protection Act</a>) do not use this site.</p> + + <h3 id="online">Online Privacy Policy Only</h3> + + <p>This online privacy policy applies only to information collected through our site and not to information collected offline.</p> + + <h3 id="consent">Your Consent</h3> + + <p>By using our site, you consent to our web site privacy policy.</p> + + <h3 id="changes">Changes to our Privacy Policy</h3> + + <p>If we decide to change our privacy policy, we will post those changes on this page.</p> + + <p>This document is CC-BY-SA. It was last updated May 31, 2013.</p> + + <p>Originally adapted from the <a href="https://github.com/discourse/discourse">Discourse privacy policy</a>. + title: "%{instance} Terms of Service and Privacy Policy" time: formats: default: "%b %d, %Y, %H:%M" diff --git a/config/locales/eo.yml b/config/locales/eo.yml index 8b42415df..d47a5db0e 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -3,22 +3,11 @@ eo: about: about_mastodon: Mastodon estas <em>senpaga, malfermitkoda</em> socia reto. Ĝi estas <em>sencentra</em> alia eblo al komercaj servoj. Ĝi evitigas, ke unusola firmao regu vian tutan komunikadon. Elektu servilon, kiun vi fidas. Kiu ajn estas via elekto, vi povas interagi kun ĉiuj aliaj uzantoj. Iu ajn povas krei sian propran aperaĵon de Mastodon en sia servilo, kaj partopreni en la <em>socia reto</em> tute glate. about_this: Pri tiu aperaĵo - apps: Aplikaĵoj business_email: 'Profesia retpoŝt-adreso:' contact: Kontakti description_headline: Kio estas %{domain}? domain_count_after: aliaj aperaĵoj domain_count_before: Konektita al - features: - api: Malfermita API por aplikaĵoj kaj servoj - blocks: Kompletaj iloj por bloki kaj kaŝi - characters: Po 500 signoj por ĉiu mesaĝo - chronology: Tempolinioj laŭtempaj - ethics: 'Etike kreita: neniu reklamo, neniu ŝpurado' - gifv: Eblo diskonigi etajn videojn kaj GIFV - privacy: Videbleco agordita laŭ la mesaĝo - public: Publikaj tempolinioj - features_headline: Kiel Mastodon estas malsimila get_started: Komenci links: Ligiloj other_instances: Aliaj aperaĵoj @@ -92,7 +81,7 @@ eo: blocking: Listo de blokitoj following: Listo de sekvatoj upload: Alporti - landing_strip_html: <strong>%{name}</strong> estas uzanto en %{link_to_root_path}. Vi povas sekvi tiun aŭ interagi kun tiu, se vi havas konton ie ajn en la Fediverse. + landing_strip_html: "<strong>%{name}</strong> estas uzanto en %{link_to_root_path}. Vi povas sekvi tiun aŭ interagi kun tiu, se vi havas konton ie ajn en la Fediverse." landing_strip_signup_html: Se vi ne havas, vi povas <a href="%{sign_up_path}">membriĝi ĉi tie.</a>. notification_mailer: digest: @@ -105,19 +94,19 @@ eo: one: "1 nova sciigo ekde via lasta vizito \U0001F418" other: "%{count} novaj sciigoj ekde via lasta vizito \U0001F418" favourite: - body: '%{name} favoris vian mesaĝon:' + body: "%{name} favoris vian mesaĝon:" subject: "%{name} favoris vian mesaĝon" follow: body: "%{name} eksekvis vin:" subject: "%{name} eksekvis vin" follow_request: body: "%{name} petis sekvi vin:" - subject: '%{name} petis sekvi vin' + subject: "%{name} petis sekvi vin" mention: - body: '%{name} menciis vin en:' - subject: '%{name} menciis vin' + body: "%{name} menciis vin en:" + subject: "%{name} menciis vin" reblog: - body: '%{name} diskonigis vian mesaĝon:' + body: "%{name} diskonigis vian mesaĝon:" subject: "%{name} diskonigis vian mesaĝon" pagination: next: Sekva diff --git a/config/locales/es.yml b/config/locales/es.yml index f587bb4ec..c051c9a08 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -3,23 +3,12 @@ es: about: about_mastodon: Mastodon es un servidor de red social <em>libre y de código abierto</em>. Una alternativa <em>descentralizada</em> a plataformas comerciales, que evita el riesgo de que una única compañía monopolice tu comunicación. Cualquiera puede ejecutar Mastodon y participar sin problemas en la <em>red social</em>. about_this: Acerca de esta instancia - apps: Apps business_email: 'Correo de negocios:' closed_registrations: Los registros están actualmente cerrados en esta instancia. contact: Contacto description_headline: "¿Qué es %{domain}?" domain_count_after: otras instancias domain_count_before: Conectado a - features: - api: API pública para aplicaciones y servicios - blocks: Moderación de contenido - characters: 500 caracteres por publicación - chronology: Las historias son cronológicas - ethics: 'Diseño etico: sin anuncios, sin rastreos' - gifv: Videos cortos y GIFV - privacy: Configuraciones de privacidad ajustables - public: Historia federada - features_headline: Lo que distingue a Mastodon get_started: Comenzar links: Enlaces other_instances: Otras instancias @@ -93,7 +82,7 @@ es: blocking: Lista de bloqueados following: Lista de seguidos upload: Cargar - landing_strip_html: <strong>%{name}</strong> es un usuario en %{link_to_root_path}. Puedes seguirlo(a) o interactuar con el o ella si tienes una cuenta en cualquier parte del fediverse. + landing_strip_html: "<strong>%{name}</strong> es un usuario en %{link_to_root_path}. Puedes seguirlo(a) o interactuar con el o ella si tienes una cuenta en cualquier parte del fediverse." landing_strip_signup_html: Si no tienes una, puedes <a href="%{sign_up_path}">registrar aquí</a>. media_attachments: validations: diff --git a/config/locales/fa.yml b/config/locales/fa.yml index 515443608..ade76d670 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -3,23 +3,12 @@ fa: about: about_mastodon: ماستدون (Mastodon) یک شبکهٔ اجتماعی <em>آزاد و کدباز</em> است. یک جایگزین <em>غیرمتمرکز</em> برای شبکههای تجاری، که نمیگذارد ارتباطهای شما را یک شرکت در انحصار خود بگیرد. یک سرور مورد اعتماد را انتخاب کنید — هر سروری که باشد، همچنان میتوانید با سرورهای دیگر ارتباط داشته باشید. هر کسی میتواند سرور ماستدون خود را راه بیندازد و در <em>شبکهٔ اجتماعی</em> سهیم شود. about_this: دربارهٔ این سرور - apps: برنامهها business_email: 'ایمیل کاری:' closed_registrations: امکان ثبت نام روی این سرور هماینک فعال نیست. contact: تماس description_headline: "%{domain} چیست؟" domain_count_after: سرور دیگر domain_count_before: متصل به - features: - api: رابط برنامهنویسی برای برنامهها و سرویسهای دیگر - blocks: ابزارهای قدرتمند برای مسدود یا بیصدا کردن دیگران - characters: ۵۰۰ حرف برای هر نوشته - chronology: نمایش نوشتههای دیگران به ترتیب زمانی - ethics: 'اخلاقمدار: بدون تبلیغات، بدون ردگیری' - gifv: تصاویر متحرک و ویدیوهای کوتاه - privacy: تنظیمات حریم خصوصی جداگانه برای هر نوشته - public: نمایش نوشتههای عمومی دیگران از همهجا - features_headline: برگهای برندهٔ ماستدون get_started: آغاز کنید links: پیوندها other_instances: سرورهای دیگر @@ -152,17 +141,13 @@ fa: settings: contact_information: email: یک نشانی ایمیل عمومی وارد کنید - label: اطلاعات تماس username: یک نام کاربری وارد کنید registrations: closed_message: desc_html: وقتی امکان ثبت نام روی سرور فعال نباشد در صفحهٔ اصلی نمایش مییابد<br>میتوانید HTML بنویسید title: پیغام برای فعالنبودن ثبت نام open: - disabled: غیرفعال - enabled: فعال title: امکان ثبت نام - setting: تنظیمات site_description: desc_html: روی صفحهٔ اصلی نمایش مییابد و همچنین به عنوان تگهای HTML.<br>میتوانید HTML بنویسید, بهویژه <code><a></code> و <code><em></code>. title: دربارهٔ سایت diff --git a/config/locales/fi.yml b/config/locales/fi.yml index c20bf690b..a2488530f 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -3,22 +3,11 @@ fi: about: about_mastodon: Mastodon on <em>ilmainen, avoimeen lähdekoodiin perustuva</em> sosiaalinen verkosto. <em>Hajautettu</em> vaihtoehto kaupallisille alustoille, se välttää eiskit yhden yrityksen monopolisoinnin sinun viestinnässäsi. Valitse palvelin mihin luotat — minkä tahansa valitset, voit vuorovaikuttaa muiden kanssa. Kuka tahansa voi luoda Mastodon palvelimen ja ottaa osaa <em>sosiaaliseen verkkoon</em> saumattomasti. about_this: Tietoja tästä palvelimesta - apps: Ohjelmat business_email: 'Business e-mail:' contact: Ota yhteyttä description_headline: Mikä on %{domain}? domain_count_after: muuhun palvelimeen domain_count_before: Yhdistyneenä - features: - api: Avoin API ohjelmille ja palveluille - blocks: Rikkaat esto- ja hiljennystyökalut - characters: 500 kirjainta per viesti - chronology: Aikajana on kronologisessa järjestyksessä - ethics: 'Eettinen suunnittelu: ei mainoksia, ei seurantaa' - gifv: GIFV settejä ja lyhyitä videoita - privacy: Julkaisukohtainen yksityisyysasetus - public: Julkiset aikajanat - features_headline: Mikä erottaa Mastodonin muista get_started: Aloita käyttö links: Linkit other_instances: Muut palvelimet @@ -92,7 +81,7 @@ fi: blocking: Estetyt lista following: Seuratut lista upload: Lähetä - landing_strip_html: <strong>%{name}</strong> on käyttäjä domainilla %{link_to_root_path}. Voit seurata tai vuorovaikuttaa heidän kanssaan jos sinulla on tili yleisessä verkossa. + landing_strip_html: "<strong>%{name}</strong> on käyttäjä domainilla %{link_to_root_path}. Voit seurata tai vuorovaikuttaa heidän kanssaan jos sinulla on tili yleisessä verkossa." landing_strip_signup_html: Jos sinulla ei ole tiliä, voit <a href="%{sign_up_path}">rekisteröityä täällä</a>. notification_mailer: digest: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index dfe5ff990..c2efd0c85 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -3,23 +3,12 @@ fr: about: about_mastodon: Mastodon est un serveur <em>libre</em> de réseautage social. Alternative <em>décentralisée</em> aux plateformes commerciales, la monopolisation de vos communications par une entreprise unique est évitée. Tout un chacun peut faire tourner Mastodon et participer au <em>réseau social</em> de manière transparente. about_this: À propos de cette instance - apps: Applications business_email: Courriel professionnel closed_registrations: Les inscriptions sont actuellement fermées sur cette instance. contact: Contact - description_headline: Qu'est-ce que %{domain} ? + description_headline: Qu’est-ce que %{domain} ? domain_count_after: autres instances domain_count_before: Connectés à - features: - api: API ouverte aux apps et services - blocks: Outils complets de bloquage et masquage - characters: 500 caractères par post - chronology: Fil chronologique - ethics: Pas de pubs, pas de pistage - gifv: Partage de vidéos et de GIFs - privacy: Réglages de confidentialité au niveau des posts - public: Fils publics - features_headline: Ce qui rend Mastodon différent get_started: Rejoindre le réseau links: Liens other_instances: Autres instances @@ -27,21 +16,35 @@ fr: status_count_after: posts status_count_before: Ayant publié terms: Conditions d’utilisation - user_count_after: utilisateurs⋅trices + user_count_after: utilisateur⋅ice⋅s user_count_before: Abrite + version: Version accounts: follow: Suivre followers: Abonné⋅es following: Abonnements - nothing_here: Rien à voir ici ! + nothing_here: Rien à voir ici ! people_followed_by: Personnes suivies par %{name} people_who_follow: Personnes qui suivent %{name} posts: Statuts remote_follow: Suivre à distance + reserved_username: Ce nom d’utilisateur⋅ice est réservé unfollow: Ne plus suivre + activitypub: + activity: + announce: + name: "%{account_name} a partagé une activité." + create: + name: "%{account_name} a créé une note." + outbox: + name: Boîte d’envoi de %{account_name} + summary: Liste d’activités de %{account_name} admin: accounts: - are_you_sure: Êtes-vous certain ? + are_you_sure: Êtes-vous certain⋅e ? + confirm: Confirmer + confirmed: Confirmé + disable_two_factor_authentication: Désactiver l’authentification à deux facteurs display_name: Nom affiché domain: Domaine edit: Éditer @@ -49,6 +52,7 @@ fr: feed_url: URL du flux followers: Abonné⋅es follows: Abonnements + ip: Adresse IP location: all: Tous local: Local @@ -70,15 +74,25 @@ fr: perform_full_suspension: Effectuer une suspension complète profile_url: URL du profil public: Public - push_subscription_expires: Expiration de l'abonnement PuSH + push_subscription_expires: Expiration de l’abonnement PuSH + redownload: Rafraîchir les avatars + reset: Réinitialiser reset_password: Réinitialiser le mot de passe + resubscribe: Se réabonner salmon_url: URL Salmon + search: Rechercher + show: + created_reports: Signalements créés par ce compte + report: signalement + targeted_reports: Signalements créés visant ce compte silence: Rendre muet statuses: Statuts + subscribe: S’abonner title: Comptes undo_silenced: Annuler le silence undo_suspension: Annuler la suspension - username: Nom d'utilisateur + unsubscribe: Se désabonner + username: Nom d’utilisateur⋅ice web: Web domain_blocks: add_new: Ajouter @@ -87,14 +101,14 @@ fr: domain: Domaine new: create: Créer le blocage - hint: Le blocage de domaine n'empêchera pas la création de comptes dans la base de données, mais il appliquera automatiquement et rétrospectivement des méthodes de modération spécifiques sur ces comptes. + hint: Le blocage de domaine n’empêchera pas la création de comptes dans la base de données, mais il appliquera automatiquement et rétrospectivement des méthodes de modération spécifiques sur ces comptes. severity: desc_html: "<strong>Silence</strong> rendra les messages des comptes concernés invisibles à ceux qui ne les suivent pas. <strong>Suspend</strong> supprimera tout le contenu des comptes concernés, les médias, et les données du profil." silence: Muet suspend: Suspendre title: Nouveau blocage de domaine reject_media: Fichiers média rejetés - reject_media_hint: Supprime localement les fichiers média stockés et refuse d'en télécharger ultérieurement. Ne concerne pas les suspensions. + reject_media_hint: Supprime localement les fichiers média stockés et refuse d’en télécharger ultérieurement. Ne concerne pas les suspensions. severities: silence: Rendre muet suspend: Suspendre @@ -110,14 +124,24 @@ fr: undo: Annuler title: Blocage de domaines undo: Annuler + instances: + account_count: Comptes connus + domain_name: Domaine + title: Instances connues reports: + action_taken_by: Intervention de + are_you_sure: Êtes vous certain⋅e? comment: label: Commentaire none: Aucun delete: Supprimer id: ID mark_as_resolved: Marquer comme résolu + nsfw: + 'false': Ré-afficher les médias + 'true': Masquer les médias report: 'Signalement #%{id}' + report_contents: Contenu reported_account: Compte signalé reported_by: Signalé par resolved: Résolus @@ -131,22 +155,18 @@ fr: settings: contact_information: email: Entrez une adresse courriel publique - label: Informations de contact - username: Entrez un nom d'utilisateur + username: Entrez un nom d’utilisateur⋅ice registrations: closed_message: - desc_html: Affiché sur la page d'accueil lorsque les inscriptions sont fermées<br>Vous pouvez utiliser des balises HTML + desc_html: Affiché sur la page d’accueil lorsque les inscriptions sont fermées<br>Vous pouvez utiliser des balises HTML title: Message de fermeture des inscriptions open: - disabled: Désactivées - enabled: Activées title: Inscriptions - setting: Paramètre site_description: - desc_html: Affichée sous la forme d'un paragraphe sur la page d'accueil et utilisée comme balise meta.<br>Vous pouvez utiliser des balises HTML, en particulier <code><a></code> et <code><em></code>. + desc_html: Affichée sous la forme d’un paragraphe sur la page d’accueil et utilisée comme balise meta.<br>Vous pouvez utiliser des balises HTML, en particulier <code><a></code> et <code><em></code>. title: Description du site site_description_extended: - desc_html: Affichée sur la page d'informations complémentaires du site<br>Vous pouvez utiliser des balises HTML + desc_html: Affichée sur la page d’informations complémentaires du site<br>Vous pouvez utiliser des balises HTML title: Description étendue du site site_title: Titre du site title: Paramètres du site @@ -158,16 +178,22 @@ fr: title: PubSubHubbub topic: Sujet title: Administration + admin_mailer: + new_report: + body: "%{reporter} a signalé %{target}" + subject: Nouveau signalement sur %{instance} (#%{id}) application_mailer: - settings: 'Changer les préférences courriel : %{link}' + settings: 'Changer les préférences courriel : %{link}' signature: Notifications de Mastodon depuis %{instance} - view: 'Voir :' + view: 'Voir :' applications: - invalid_url: L'URL fournie est invalide + invalid_url: L’URL fournie est invalide auth: - change_password: Changer de mot de passe - didnt_get_confirmation: Vous n’avez pas reçu les consignes de confirmation ? - forgot_password: Mot de passe oublié ? + change_password: Sécurité + delete_account: Supprimer le compte + delete_account_html: Si vous désirez supprimer votre compte, vous pouvez cliquer ici. Il vous sera demandé de confirmer cette action. + didnt_get_confirmation: Vous n’avez pas reçu les consignes de confirmation ? + forgot_password: Mot de passe oublié ? login: Se connecter logout: Se déconnecter register: S’inscrire @@ -177,7 +203,7 @@ fr: authorize_follow: error: Malheureusement, il y a eu une erreur en cherchant les détails du compte distant follow: Suivre - prompt_html: 'Vous (<strong>%{self}</strong>) avez demandé à suivre :' + prompt_html: 'Vous (<strong>%{self}</strong>) avez demandé à suivre :' title: Suivre %{acct} datetime: distance_in_words: @@ -189,9 +215,9 @@ fr: almost_x_years: one: un an other: "%{count} ans" - half_a_minute: A l'instant + half_a_minute: À l’instant less_than_x_minutes: "%{count}min" - less_than_x_seconds: A l'instant + less_than_x_seconds: À l’instant over_x_years: one: un an other: "%{count} ans" @@ -199,89 +225,154 @@ fr: x_minutes: "%{count}min" x_months: "%{count}mois" x_seconds: "%{count}s" + deletes: + bad_password_msg: Bien essayé ! Mot de passe incorrect + confirm_password: Entrez votre mot de passe pour vérifier votre identité + description_html: Cela va supprimer votre compte et le désactiver de manière <strong>permanente et irréversible</strong>. Votre nom d’utilisateur⋅ice restera réservé afin d’éviter la confusion + proceed: Supprimer compte + success_msg: Votre compte a été supprimé avec succès + warning_html: Seule la suppression du contenu depuis cette instance est garantie. Le contenu qui a été partagé est susceptible de laisser des traces. Les serveurs hors-lignes ainsi que ceux n’étant plus abonnés à vos publications ne mettront pas leur base de données à jour. + warning_title: Disponibilité du contenu disséminé errors: - '404': La page que vous recherchez n'existe pas. - '410': La page que vous recherchez n'existe plus. + '403': Vous n’avez pas accès à cette page. + '404': La page que vous recherchez n’existe pas. + '410': La page que vous recherchez n’existe plus. '422': - content: Vérification de sécurité échouée. Bloquez-vous les cookies ? + content: Vérification de sécurité échouée. Bloquez-vous les cookies ? title: Vérification de sécurité échouée + '429': Trop de requêtes émises dans un délai donné. + noscript: Pour utiliser Mastodon, veuillez activer JavaScript exports: blocks: Vous bloquez csv: CSV follows: Vous suivez mutes: Vous faites taire storage: Médias stockés + followers: + domain: Domaine + explanation_html: Si vous voulez être sûr⋅e que vos status restent privés, vous devez savoir qui vous suit. <strong>Vos status privés seront diffusés à toutes les instances des utilisateur⋅ice⋅s qui vous suivent</strong>. Vous voudrez peut-être les passer en revue et les supprimer si vous n’êtes pas sûr⋅e que votre vie privée sera respectée par l’administration ou le logiciel de ces instances. + followers_count: Nombre d’abonné⋅es + lock_link: Rendez votre compte privé + purge: Retirer de la liste d’abonné⋅es + success: + one: Suppression des abonné⋅es venant d’un domaine en cours… + other: Suppression des abonné⋅es venant de %{count} domaines en cours… + true_privacy_html: Soyez conscient⋅es <strong>qu’une vraie confidentialité ne peut être atteinte que par un chiffrement de bout-en-bout</strong>. + unlocked_warning_html: N’importe qui peut vous suivre et voir vos status privés. %{lock_link} afin de pouvoir vérifier et rejeter des abonné⋅es. + unlocked_warning_title: Votre compte n’est pas privé generic: - changes_saved_msg: Les modifications ont été enregistrées avec succès ! + changes_saved_msg: Les modifications ont été enregistrées avec succès ! powered_by: propulsé par %{link} save_changes: Enregistrer les modifications validation_errors: - one: Quelque chose ne va pas ! Vérifiez l’erreur ci-dessous. - other: Certaines choses ne vont pas ! Vérifiez les erreurs ci-dessous. + one: Quelque chose ne va pas ! Vérifiez l’erreur ci-dessous. + other: Certaines choses ne vont pas ! Vérifiez les erreurs ci-dessous. imports: preface: Vous pouvez importer certaines données comme les personnes que vous suivez ou bloquez sur votre compte sur cette instance à partir de fichiers créés sur une autre instance. success: Vos données ont été importées avec succès et seront traitées en temps et en heure types: - blocking: Liste d'utilisateurs⋅trices bloqué⋅es - following: Liste d'utilisateurs⋅trices suivi⋅es - muting: Liste d'utilisateurs⋅trices que vous faites taire + blocking: Liste d’utilisateur⋅ice⋅s bloqué⋅es + following: Liste d’utilisateur⋅ice⋅s suivi⋅es + muting: Liste d’utilisateur⋅ice⋅s que vous faites taire upload: Importer landing_strip_html: <strong>%{name}</strong> utilise %{link_to_root_path}. Vous pouvez le/la suivre et interagir si vous possédez un compte quelque part dans le "fediverse". - landing_strip_signup_html: Si ce n'est pas le cas, vous pouvez <a href="%{sign_up_path}">en créer un ici</a>. + landing_strip_signup_html: Si ce n’est pas le cas, vous pouvez <a href="%{sign_up_path}">en créer un ici</a>. media_attachments: validations: images_and_video: Impossible de joindre une vidéo à un statut contenant déjà des images too_many: Impossible de joindre plus de 4 fichiers notification_mailer: digest: - body: 'Voici ce que vous avez raté sur ${instance} depuis votre dernière visite (%{}) :' + body: 'Voici ce que vous avez raté sur ${instance} depuis votre dernière visite (%{}) :' mention: "%{name} vous a mentionné⋅e" new_followers_summary: - one: Vous avez un⋅e nouvel⋅le abonné⋅e ! Youpi ! - other: Vous avez %{count} nouveaux⋅elles abonné⋅e⋅s ! Incroyable ! + one: Vous avez un⋅e nouvel⋅le abonné⋅e ! Youpi ! + other: Vous avez %{count} nouveaux⋅elles abonné⋅e⋅s ! Incroyable ! subject: one: "Une nouvelle notification depuis votre dernière visite \U0001F418" other: "%{count} nouvelles notifications depuis votre dernière visite \U0001F418" favourite: - body: "%{name} a ajouté votre post à ses favoris :" + body: "%{name} a ajouté votre post à ses favoris :" subject: "%{name} a ajouté votre post à ses favoris" follow: - body: "%{name} vous suit !" + body: "%{name} vous suit !" subject: "%{name} vous suit" follow_request: body: "%{name} a demandé à vous suivre" - subject: 'Abonné⋅es en attente : %{name}' + subject: 'Abonné⋅es en attente : %{name}' mention: - body: "%{name} vous a mentionné⋅e dans :" + body: "%{name} vous a mentionné⋅e dans :" subject: "%{name} vous a mentionné⋅e" reblog: - body: "%{name} a partagé votre statut :" + body: "%{name} a partagé votre statut :" subject: "%{name} a partagé votre statut" pagination: next: Suivant prev: Précédent remote_follow: acct: Entrez votre pseudo@instance depuis lequel vous voulez suivre ce⋅tte utilisateur⋅trice - missing_resource: L'URL de redirection n'a pas pu être trouvée + missing_resource: L’URL de redirection n’a pas pu être trouvée proceed: Continuez pour suivre - prompt: 'Vous allez suivre :' + prompt: 'Vous allez suivre :' + sessions: + activity: Dernière activité + browser: Navigateur + browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + firefox: Firefox + generic: Navigateur inconnu + ie: Internet Explorer + micro_messenger: MicroMessenger + nokia: Nokia S40 Ovi Browser + opera: Opera + phantom_js: PhantomJS + qq: QQ Browser + safari: Safari + uc_browser: UCBrowser + weibo: Weibo + current_session: Session courante + description: "%{browser} sur %{platform}" + explanation: Ceci est la liste des navigateurs actuellement connectés à votre compte Mastodon. + ip: Adresse IP + platforms: + adobe_air: Adobe Air + android: Android + blackberry: Blackberry + chrome_os: ChromeOS + firefox_os: Firefox OS + ios: iOS + linux: Linux + mac: Mac + other: système inconnu + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone + title: Sessions settings: authorized_apps: Applications autorisées back: Retour vers Mastodon + delete: Suppression de compte edit_profile: Modifier le profil export: Export de données + followers: Abonné⋅es autorisé⋅es import: Import de données preferences: Préférences settings: Réglages - two_factor_authentication: Identification à deux facteurs (Two-factor auth) + two_factor_authentication: Identification à deux facteurs statuses: open_in_web: Ouvrir sur le web over_character_limit: limite de caractères dépassée de %{max} caractères show_more: Afficher plus visibilities: private: Abonné⋅es uniquement + private_long: Seul⋅es vos abonné⋅es verront vos status public: Public + public_long: Tout le monde peut voir vos status unlisted: Public sans être affiché sur le fil public + unlisted_long: Tout le monde peut voir vos status mais ils ne seront pas sur listés sur les fils publics stream_entries: click_to_show: Cliquer pour afficher reblogged: partagé @@ -291,18 +382,20 @@ fr: default: "%d %b %Y, %H:%M" two_factor_authentication: code_hint: Entrez le code généré par votre application pour confirmer - description_html: Si vous activez <strong>l'identification à deux facteurs</strong>, vous devrez être en possession de votre téléphone afin de générer un code de connexion. + description_html: Si vous activez <strong>l’identification à deux facteurs</strong>, vous devrez être en possession de votre téléphone afin de générer un code de connexion. disable: Désactiver enable: Activer + enabled: L’authentification à deux facteurs est activée enabled_success: Identification à deux facteurs activée avec succès generate_recovery_codes: Générer les codes de récupération instructions_html: "<strong>Scannez ce QR code grâce à Google Authenticator, Authy ou une application similaire sur votre téléphone</strong>. Désormais, cette application générera des jetons que vous devrez saisir à chaque connexion." lost_recovery_codes: Les codes de récupération vous permettent de retrouver les accès à votre comptre si vous perdez votre téléphone. Si vous perdez vos codes de récupération, vous pouvez les générer à nouveau ici. Vos anciens codes de récupération seront invalidés. - manual_instructions: 'Si vous ne pouvez pas scanner ce QR code et devez l''entrer manuellement, voici le secret en clair :' + manual_instructions: 'Si vous ne pouvez pas scanner ce QR code et devez l’entrer manuellement, voici le secret en clair :' + recovery_codes: Codes de récupération recovery_codes_regenerated: Codes de récupération régénérés avec succès - recovery_instructions_html: Si vous perdez l'accès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour récupérer l'accès à votre compte. Conservez les codes de récupération en toute sécurité, par exemple, en les imprimant et en les stockant avec vos autres documents importants. + recovery_instructions_html: Si vous perdez l’accès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour récupérer l’accès à votre compte. Conservez les codes de récupération en toute sécurité, par exemple, en les imprimant et en les stockant avec vos autres documents importants. setup: Installer - wrong_code: Les codes entrés sont incorrects ! L'heure du serveur et celle de votre appareil sont-elles correctes ? + wrong_code: Les codes entrés sont incorrects ! L’heure du serveur et celle de votre appareil sont-elles correctes ? users: - invalid_email: L'adresse courriel est invalide - invalid_otp_token: Le code d'authentification à deux facteurs est invalide + invalid_email: L’adresse courriel est invalide + invalid_otp_token: Le code d’authentification à deux facteurs est invalide diff --git a/config/locales/he.yml b/config/locales/he.yml index ec7d972ec..21f8f1dc4 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -3,23 +3,12 @@ he: about: about_mastodon: מסטודון היא רשת חברתית <em>חופשית, מבוססת תוכנה חופשית ("קוד פתוח")</em>. כאלטרנטיבה <em>בלתי ריכוזית</em> לפלטפרומות המסחריות, מסטודון מאפשרת להמנע מהסיכונים הנלווים להפקדת התקשורת שלך בידי חברה יחידה. שמת את מבטחך בשרת אחד — לא משנה במי בחרת, תמיד אפשר לדבר עם כל שאר המשתמשים. לכל מי שרוצה יש את האפשרות להקים שרת מסטודון עצמאי, ולהשתתף ב<em>רשת החברתית</em> באופן חלק. about_this: אודות שרת זה - apps: ישומים business_email: 'דוא"ל עסקי:' closed_registrations: הרשמות סגורות לשרת זה לעת עתה. contact: צור קשר description_headline: מהו %{domain}? domain_count_after: שרתים אחרים domain_count_before: מחובר אל - features: - api: API פתוח לישומים ושירותים - blocks: כלי חסימה והשתקה חזקים - characters: 500 תווים להודעה - chronology: הטורים כרונולוגיים - ethics: 'תכנון מוסרי: אין פרסומות, אין מעקב' - gifv: GIFV וסרטונים קצרים - privacy: אפשרויות פרטיוּת נפרדות לכל הודעה - public: טורים פומביים - features_headline: מה מייחד קהילות מבוססות מסטודון get_started: בואו נתחיל links: קישורים other_instances: שרתים אחרים @@ -160,17 +149,13 @@ he: settings: contact_information: email: נא להקליד כתובת דוא"ל פומבית - label: פרטי התקשרות username: נא להכניס שם משתמש registrations: closed_message: desc_html: מוצג על הדף הראשי כאשר ההרשמות סגורות<br>ניתן להשתמש בתגיות HTML title: מסר סגירת הרשמות open: - disabled: מבוטל - enabled: מופעל title: הרשמה פתוחה - setting: הגדרה site_description: desc_html: מוצג כפסקה על הדף הראשי ומשמש כתגית מטא.<br>ניתן להשתמש בתגיות HTML, ובמיוחד ב־<code><a></code> ו־<code><em></code>. title: תיאור האתר diff --git a/config/locales/hr.yml b/config/locales/hr.yml index f873fc7ed..8297ca629 100644 --- a/config/locales/hr.yml +++ b/config/locales/hr.yml @@ -3,23 +3,12 @@ hr: about: about_mastodon: Mastodon je <em>besplatna, open-source</em> socijalna mreža. <em>Decentralizirana</em> alternativa komercijalnim platformama, izbjegava rizik toga da jedna tvrtka monopolizira vašu komunikaciju. Izaberite server kojem ćete vjerovati — koji god odabrali, moći ćete komunicirati sa svima ostalima. Bilo tko može imati svoju vlastitu Mastodon instancu i sudjelovati u <em>socijalnoj mreži</em> bez problema. about_this: O ovoj instanci - apps: Aplikacije business_email: 'Poslovni e-mail:' closed_registrations: Registracije na ovoj instanci su trenutno zatvorene. contact: Kontakt description_headline: Što je %{domain}? domain_count_after: druge instance domain_count_before: Spojen na - features: - api: Otvoren API za aplikacije i servise - blocks: Bogati alati za blokiranje i ušutkivanje - characters: 500 znakova po postu - chronology: Timelines su kronološke - ethics: 'Etički dizajn: bez oglasa, bez praćenja' - gifv: GIFV setovi i kratki videi - privacy: Granularne postavke privatnosti, po postu - public: Javne timelines - features_headline: Po čemu se Mastodon razlikuje get_started: Započni links: Linkovi other_instances: Druge instance @@ -94,7 +83,7 @@ hr: following: Lista onih koje slijedim muting: Lista utišanih upload: Upload - landing_strip_html: <strong>%{name}</strong> je korisnik na %{link_to_root_path}. Možeš ih slijediti ili komunicirati s njima ako imaš račun igdje u fediversu. + landing_strip_html: "<strong>%{name}</strong> je korisnik na %{link_to_root_path}. Možeš ih slijediti ili komunicirati s njima ako imaš račun igdje u fediversu." landing_strip_signup_html: Ako nemaš, možeš se <a href="%{sign_up_path}">registrirati ovdje</a>. notification_mailer: digest: diff --git a/config/locales/id.yml b/config/locales/id.yml index fc4ffd046..e3fe96331 100644 --- a/config/locales/id.yml +++ b/config/locales/id.yml @@ -3,23 +3,12 @@ id: about: about_mastodon: Mastodon adalah sebuah jejaring sosial <em>terbuka, open-source</em. Sebuah alternatif <em>desentralisasi</em> dari platform komersial, menjauhkan anda resiko dari sebuah perusahaan yang memonopoli komunikasi anda. Pilih server yang anda percayai — apapun yang anda pilih, anda tetap dapat berinteraksi dengan semua orang. Semua orang dapat menjalankan server Mastodon sendiri dan berpartisipasi dalam <em>jejaring sosial</em> dengan mudah. about_this: Tentang server ini - apps: Apl business_email: 'E-mail bisnis:' closed_registrations: Pendaftaran untuk server ini sedang ditutup. contact: Kontak description_headline: Apa itu %{domain}? domain_count_after: server lain domain_count_before: Terhubung dengan - features: - api: API terbuka untuk aplikasi dan layanan lain - blocks: Aneka ragam fitur blokir dan pembisuan - characters: 500 karakter per posting - chronology: Linimasa berurutan - ethics: 'Desain etis: tanpa iklan, tidak ada pelacakan' - gifv: Fitur GIFV dan video pendek - privacy: Terperinci, pengaturan privasi per postingan - public: Linimasa publik - features_headline: Yang berbeda dari Mastodon get_started: Mulai links: Link other_instances: Server lain @@ -151,17 +140,13 @@ id: settings: contact_information: email: Masukkan alamat email - label: Informasi kontak username: Masukkan nama pengguna registrations: closed_message: desc_html: Ditampilkan pada halaman depan saat pendaftaran ditutup<br>Anda bisa menggunakan tag HTML title: Pesan penutupan pendaftaran open: - disabled: Dinonaktifkan - enabled: Diaktifkan title: Pendaftaran terbuka - setting: Pengaturan site_description: desc_html: Ditampilkan sebagai sebuah paragraf di halaman depan dan digunakan sebagai tag meta.<br>Anda bisa menggunakan tag HTML, khususnya <code><a></code> dan <code><em></code>. title: Deskripsi situs diff --git a/config/locales/io.yml b/config/locales/io.yml index db430b0fe..b587d4bc6 100644 --- a/config/locales/io.yml +++ b/config/locales/io.yml @@ -3,23 +3,12 @@ io: about: about_mastodon: Mastodon esas <em>gratuita, apertitkodexa</em> sociala reto. Ol esas <em>sencentra</em> altra alternativo a komercala servadi. Ol evitigas, ke sola firmo guvernez tua tota komunikadol. Selektez servero, quan tu fidas. Irge qua esas tua selekto, tu povas komunikar kun omna altra uzeri. Irgu povas krear sua propra instaluro di Mastodon en sua servero, e partoprenar en la <em>sociala reto</em> tote glate. about_this: Pri ta instaluro - apps: Apliki business_email: 'Profesionala retpost-adreso:' closed_registrations: Membresko ne nun esas posible en ta instaluro. contact: Kontaktar description_headline: Quo esas %{domain}? domain_count_after: altra instaluri domain_count_before: Konektita ad - features: - api: Apertita API por apliki e servadi - blocks: Kompleta utensili por blokusar e celar - characters: Til 500 signi por singla mesajo - chronology: Tempolinei seguntempa - ethics: 'Etike kreita: nula anunco, nula trakado' - gifv: Posibleso diskononigar mikra videi e GIFV - privacy: Videbleso ajustita segun la mesajo - public: Publika tempolinei - features_headline: Quale Mastodon esas diferanta get_started: Komencar links: Ligili other_instances: Altra instaluri @@ -139,17 +128,13 @@ io: settings: contact_information: email: Enter a public e-mail address - label: Contact information username: Enter a username registrations: closed_message: desc_html: Displayed on frontpage when registrations are closed<br>You can use HTML tags title: Closed registration message open: - disabled: Disabled - enabled: Enabled title: Open registration - setting: Setting site_description: desc_html: Displayed as a paragraph on the frontpage and used as a meta tag.<br>You can use HTML tags, in particular <code><a></code> and <code><em></code>. title: Site description diff --git a/config/locales/it.yml b/config/locales/it.yml index a96a459df..5c014c61d 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -3,23 +3,12 @@ it: about: about_mastodon: Mastodon è un social network <em>gratuito e open-source</em>. Un'alternativa <em>decentralizzata</em> 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 <em>social network</em>. about_this: A proposito di questo server - apps: Applicazioni business_email: 'Email di lavoro:' closed_registrations: Al momento le iscrizioni a questo server sono chiuse. contact: Contatti description_headline: Cos'è %{domain}? domain_count_after: altri server domain_count_before: Connesso a - features: - api: API aperto per applicazioni e servizi - blocks: Potenti strumenti di blocco e silenziamento - characters: 500 caratteri per status - chronology: Le timeline sono cronologiche - ethics: 'Design etico: niente pubblicità, niente tracking' - gifv: Set di GIFV e brevi video - privacy: Opzioni di privacy mirate per-post - public: Timeline pubbliche - features_headline: Cosa rende Mastodon migliore get_started: Inizia links: Links other_instances: Altri server @@ -93,7 +82,7 @@ it: blocking: Lista dei bloccati following: Lista dei seguaci upload: Carica - landing_strip_html: <strong>%{name}</strong> è un utente su %{link_to_root_path}. Puoi seguirlo o interagire con lui se possiedi un account ovunque nel fediverse. + landing_strip_html: "<strong>%{name}</strong> è 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 <a href="%{sign_up_path}">iscriverti qui</a>. media_attachments: validations: diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 80169339d..d57fe8da2 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -3,23 +3,12 @@ ja: about: about_mastodon: Mastodon は<em>自由でオープンソース</em>なソーシャルネットワークです。商用プラットフォームの代替となる<em>分散型</em>を採用し、あなたのやりとりが一つの会社によって独占されるのを防ぎます。信頼できるインスタンスを選択してください — どのインスタンスを選んでも、誰とでもやりとりすることができます。 だれでも自分の Mastodon インスタンスを作ることができ、シームレスに<em>ソーシャルネットワーク</em>に参加できます。 about_this: このインスタンスについて - apps: アプリ business_email: 'ビジネスメールアドレス:' closed_registrations: 現在このインスタンスでの新規登録は受け付けていません。 contact: 連絡先 description_headline: "%{domain} とは?" domain_count_after: 個のインスタンス domain_count_before: 接続中 - features: - api: アプリやその他サービスにAPIを公開 - blocks: 豊富なブロックやミュート機能 - characters: 1つの投稿は500文字まで利用可能 - chronology: 時系列順のタイムライン - ethics: 広告もトラッキングもありません - gifv: GIFVや短い動画にも対応 - privacy: 投稿ごとに公開範囲を細かく設定可能 - public: 公開タイムライン - features_headline: Mastodon の特徴 get_started: 参加する links: リンク other_instances: 他のインスタンス @@ -166,23 +155,22 @@ ja: settings: contact_information: email: 公開するメールアドレスを入力 - label: 連絡先情報 username: ユーザー名を入力 registrations: closed_message: desc_html: 新規登録を停止しているときにフロントページに表示されます。<br>HTMLタグが利用可能です。 title: 新規登録停止時のメッセージ open: - disabled: 無効 - enabled: 有効 title: 新規登録を受け付ける - setting: 設定 site_description: desc_html: トップページへの表示と meta タグに使用されます。<br>HTMLタグ、特に<code><a></code> と <code><em></code>が利用可能です。 title: サイトの説明文 site_description_extended: desc_html: インスタンスについてのページに表示されます。<br>HTMLタグが利用可能です。 title: サイトの詳細な説明 + site_terms: + desc_html: プライバシーポリシーのページに表示されます。<br>HTMLタグが利用可能です。 + title: サイトのプライバシーポリシー site_title: サイトのタイトル title: サイト設定 subscriptions: @@ -193,6 +181,10 @@ ja: title: PubSubHubbub topic: トピック title: 管理 + admin_mailer: + new_report: + body: "%{reporter} が %{target} を通報しました" + subject: "%{instance} の新しい通報 (#%{id})" application_mailer: settings: 'メール設定の変更: %{link}' signature: Mastodon %{instance} インスタンスからの通知 @@ -200,7 +192,7 @@ ja: applications: invalid_url: URLが無効です auth: - change_password: ログイン情報 + change_password: セキュリティ delete_account: アカウントの削除 delete_account_html: アカウントを削除したい場合、<a href="%{path}">こちら</a>から手続きが行えます。削除前には確認画面があります。 didnt_get_confirmation: 確認メールを受信できませんか? @@ -320,6 +312,43 @@ ja: missing_resource: リダイレクト先が見つかりませんでした proceed: フォローする prompt: 'フォローしようとしています:' + sessions: + activity: 最後のアクティビティ + browser: ブラウザ + browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + firefox: Firefox + generic: 不明なブラウザ + ie: Internet Explorer + micro_messenger: MicroMessenger + nokia: Nokia S40 Ovi Browser + opera: Opera + phantom_js: PhantomJS + qq: QQ Browser + safari: Safari + uc_browser: UCBrowser + weibo: Weibo + current_session: 現在のセッション + description: "%{browser} on %{platform}" + explanation: あなたの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: 不明なプラットフォーム + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone + title: セッション settings: authorized_apps: 認証済みアプリ back: 戻る @@ -344,8 +373,78 @@ ja: unlisted_long: 誰でも見ることができますが、公開タイムラインには表示されません stream_entries: click_to_show: クリックして表示 - reblogged: ブーストされました + reblogged: さんにブーストされました sensitive_content: 閲覧注意 + terms: + body_html: | + <h2>プライバシーポリシー</h2> + + <h3 id="collect">どのような情報を収集するのですか?</h3> + + <p>あなたがこのサイトに登録すると、ここで共有された情報を読んだり、書いたり、評価したりして、フォーラムでの情報を集める事ができます。</p> + + <p>このサイトに登録する際には、名前とメールアドレスの入力を求めることがあります。ただし、登録をすることなくこのサイトを利用することも可能です。あなたのメールアドレスは、固有のリンクを含んだメールで確認されます。そのリンクにアクセスした場合にメールアドレスを制御することとなります。</p> + + <p>アカウントを登録し、投稿を行った際にはその投稿が行われたIPアドレスを記録します。また、このサーバーに対する全てのリクエストはIPアドレスを含むサーバーログとして保管されます。</p> + + <h3 id="use">自分の情報を何に使うのですか?</h3> + + <p>このサイトで収集された情報は、次のいくつかの方法で使用されます:</p> + + <ul> + <li>パーソナライズ・エクスペリエンス — あなたの情報は、あなたや他のユーザーのニーズに対応するために役立ちます。</li> + <li>サイトの改善・最適化 — このサービスはあなたから受け取った情報やフィードバックに基づいて提供されるサイトの改善を行いつづけます。</li> + <li>サービスの向上 — あなたの情報は、ユーザーからの要求やサポートへより効果的に対応するために役立ちます。</li> + <li>定期メールの送信 — メールアドレスは、情報の送信、トピックの変更やユーザー名に関係するお知らせ、お問い合わせに関する返答、その他のリクエストや質問に関してお知らせするために使用されます。</li> + </ul> + + <h3 id="protect">自分の情報はどのように保護されるのですか?</h3> + + <p>このサービスはあなたの個人情報の入力、送信、またはアクセスに際してあなたの個人情報の安全性を維持するために様々なセキュリティ手段をとっています。</p> + + <h3 id="data-retention>データ保持のポリシーはどのようになっていますか?</h3> + + <p>このサービスはデータ保持に関して次のことを行うよう努めます。:</p> + + <ul> + <li>このサーバーへのすべての要求に対して、IPアドレスを含むサーバーログを90日以内に渡って保持します。</li> + <li>登録されたユーザーとその投稿に関連付けされたIPアドレスを5年以内に渡って保持します。</li> + </ul> + + <h3 id="cookies">クッキーを使用していますか?</h3> + + <p>はい。クッキーはあなたがウェブブラウザ上で許可した場合にコンピュータのストレージに転送される小さなファイルです。これらのクッキーを使用すると、サイトでブラウザが識別され、登録済みのアカウントを持っている場合は登録済みのアカウントに関連付けがされます。</p> + + <p>クッキーを使用して、今後再度閲覧された場合に前回のデータから設定を呼び出したり、今後の改善のためにサイトのトラフィックやサイトの相互作用に関する集計データを作成します。このサービスは、サイトを訪れた方との理解を深めるために、第三者のサービス提供者と契約することがあります。これらのサービス提供者というものは、このサービスでの業務を行ったり、改善するためにこのサービスの代わって収集された情報を使用することはできません。</p> + + <h3 id="disclose">このサイトは外部に何らかの情報を開示していますか?</h3> + + <p>私たちは、個人を特定出来る情報を外部へ販売、取引、または他の方法で渡すことはありません。これには、このサイトを操作したり、業務を行ったり、サービスを提供するのに役立つ信頼できる第三者は含まれません。法令遵守、サイトポリシーの施行、このサービスや他の人の権利、財産または安全の保護のために適切であると判断した場合に、あなたの情報を公開する場合があります。ただし、マーケティングや広告、その他の目的で匿名での訪問者情報を他者へ提供することができます。</p> + + <h3 id="third-party">サードパーティのリンク</h3> + + <p>必要に応じて、このサービスの方針にもとづいてこのサイトや第三者のサービスを提供することがあります。これらの第三者のサイトには、個別の独立したプライバシーポリシーがあります。従って、これらのリンク先のサイトに関するコンテンツや活動にかんしては一切責任を負いません。ですが、サイトの完全性やこれらのサイトに関するフィードバックは非常に重要なものであると認識しております。</p> + + <h3 id="coppa">子供のオンライン・プライバシー保護法</h3> + + <p>このサイト、製品、サービスはすべて13歳以上の人を対象としております。このサーバーが米国にあり、13歳未満の場合はCOPPA (<a href="https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act">Children's Online Privacy Protection Act</a>) にもとづいてこのサイトを使用しないでください。</p> + + <h3 id="online">オンライン限定のプライバシーポリシー</h3> + + <p>このオンライン・プライバシーポリシーは、このサイトを通じて収集された情報のみに適用され、オフラインで収集される情報には適用されません。</p> + + <h3 id="consent">あなたの同意</h3> + + <p>このサービスを使用することにより、このサイトのプライバシーポリシーに同意するものとします。</p> + + <h3 id="changes">プライバシーポリシーの変更</h3> + + <p>プライバシーポリシーを変更する場合は、このページへ変更内容を掲載します。</p> + + <p>この文章のライセンスはCC-BY-SAです。このページは2017年5月6日が最終更新です。</p> + + <p>オリジナルの出典 <a href="https://github.com/discourse/discourse">Discourse privacy policy</a>. + title: "%{instance} 利用規約・プライバシーポリシー" time: formats: default: "%Y年%m月%d日 %H:%M" @@ -354,13 +453,15 @@ ja: description_html: "<strong>二段階認証</strong>を有効にするとログイン時、電話でコードを受け取る必要があります。" disable: 無効 enable: 有効 + enabled: 二段階認証は有効になっています enabled_success: 二段階認証が有効になりました generate_recovery_codes: リカバリーコードを生成 instructions_html: "<strong>Google Authenticatorか、もしくはほかのTOTPアプリでこのQRコードをスキャンしてください。</strong>これ以降、ログインするときはそのアプリで生成されるコードが必要になります。" lost_recovery_codes: リカバリーコードを使用すると携帯電話を紛失した場合でもアカウントにアクセスできるようになります。 リカバリーコードを紛失した場合もここで再生成することができますが、古いリカバリーコードは無効になります。 manual_instructions: 'QRコードがスキャンできず、手動での登録を希望の場合はこのシークレットコードを利用してください。:' + recovery_codes: リカバリーコード recovery_codes_regenerated: リカバリーコードが再生成されました。 - recovery_instructions_html: 携帯電話を紛失した場合、以下の内どれかのリカバリーコードを使用してアカウントへアクセスすることができます。 リカバリーコードは印刷して安全に保管してください。 + recovery_instructions_html: 携帯電話を紛失した場合、以下の内どれかのリカバリーコードを使用してアカウントへアクセスすることができます。<strong>リカバリーコードは大切に保全してください。</strong>たとえば印刷してほかの重要な書類と一緒に保管することができます。 setup: 初期設定 wrong_code: コードが間違っています。サーバー上の時間とデバイス上の時間が一致していることを確認してください。 users: diff --git a/config/locales/ko.yml b/config/locales/ko.yml new file mode 100644 index 000000000..bafc19993 --- /dev/null +++ b/config/locales/ko.yml @@ -0,0 +1,396 @@ +--- +ko: + about: + about_mastodon: Mastodon 은<em>자유로운 오픈 소스</em>소셜 네트워크입니다. 상용 플랫폼의 대체로써 <em>분산형 구조</em>를 채택해, 여러분의 대화가 한 회사에 독점되는 것을 방지합니다. 신뢰할 수 있는 인스턴스를 선택하세요 — 어떤 인스턴스를 고르더라도, 누구와도 대화할 수 있습니다. 누구나 자신만의 Mastodon 인스턴스를 만들 수 있으며, Seamless하게 <em>소셜 네트워크</em>에 참가할 수 있습니다. + about_this: 이 인스턴스에 대해서 + business_email: '비즈니스 메일 주소:' + closed_registrations: 현재 이 인스턴스에서는 신규 등록을 받고 있지 않습니다. + contact: 연락처 + description_headline: "%{domain} 는 무엇인가요?" + domain_count_after: 개의 인스턴스 + domain_count_before: 연결됨 + get_started: 참가하기 + links: 링크 + other_instances: 다른 인스턴스 + source_code: 소스 코드 + status_count_after: Toot + status_count_before: Toot 수 + terms: 개인 정보 보호 정책 + user_count_after: 명 + user_count_before: 사용자 수 + version: 버전 + accounts: + follow: 팔로우 + followers: 팔로워 + following: 팔로잉 + nothing_here: 아무 것도 없습니다. + people_followed_by: "%{name} 님이 팔로우 중인 계정" + people_who_follow: "%{name} 님을 팔로우 중인 계정" + posts: 포스트 + remote_follow: 리모트 팔로우 + reserved_username: 이 아이디는 예약되어 있습니다. + unfollow: 팔로우 해제 + activitypub: + activity: + announce: + name: "%{account_name} 님이 액티비티를 공유했습니다" + create: + name: "%{account_name} 님이 노트를 작성했습니다" + outbox: + name: "%{account_name} 님의 송신함" + summary: "%{account_name} 님의 액티비티 모음" + admin: + accounts: + are_you_sure: 정말로 실행하시겠습니까? + confirm: 확인 + confirmed: 확인됨 + disable_two_factor_authentication: 2단계 인증을 비활성화 + display_name: 이름 + domain: 도메인 + edit: 편집 + email: E-mail + feed_url: 피드 URL + followers: 팔로워 수 + follows: 팔로잉 수 + ip: IP + location: + all: 전체 + local: 로컬 + remote: 리모트 + title: 위치 + media_attachments: 첨부된 미디어 + moderation: + all: 전체 + silenced: 침묵 중 + suspended: 정지 중 + title: 모더레이션 + most_recent_activity: 최근 활동 + most_recent_ip: 최근 IP + not_subscribed: 구독하지 않음 + order: + alphabetic: 알파벳 순 + most_recent: 최근 활동 순 + title: 순서 + perform_full_suspension: 완전히 정지시키기 + profile_url: 프로필 URL + public: 전체 공개 + push_subscription_expires: PuSH 구독 기간 만료 + redownload: 아바타 업데이트 + reset: 초기화 + reset_password: 비밀번호 초기화 + resubscribe: 다시 구독 + salmon_url: Salmon URL + search: 검색 + show: + created_reports: 이 계정에서 제출된 신고 + report: 신고 + targeted_reports: 이 계정에 대한 신고 + silence: 침묵 + statuses: Toot 수 + subscribe: 구독하기 + title: 계정 + undo_silenced: 침묵 해제 + undo_suspension: 정지 해제 + unsubscribe: 구독 해제 + username: 아이디 + web: Web + domain_blocks: + add_new: 추가하기 + created_msg: 도메인 차단 처리를 완료했습니다. + destroyed_msg: 도메인 차단을 해제했습니다. + domain: 도메인 + new: + create: 차단 추가 + hint: 도메인 차단은 내부 데이터베이스에 계정이 생성되는 것까지는 막을 수 없지만, 그 도메인에서 생성된 계정에 자동적으로 특정한 모더레이션을 적용하게 할 수 있습니다. + severity: + desc_html: "<strong>침묵</strong>은 계정을 팔로우 하지 않고 있는 사람들에겐 계정의 Toot을 보이지 않게 합니다. <strong>정지</strong>는 계정의 컨텐츠, 미디어, 프로필 데이터를 삭제합니다." + silence: 침묵 + suspend: 정지 + title: 새로운 도메인 차단 + reject_media: 미디어 파일 거부하기 + reject_media_hint: 로컬에 저장된 미디어 파일을 삭제하고, 이후로도 다운로드를 거부합니다. 정지하고는 관계 없습니다. + severities: + silence: 침묵 + suspend: 정지 + severity: 심각도 + show: + affected_accounts: + one: 데이터베이스 중 1개의 계정에 영향을 끼칩니다 + other: 데이터베이스 중 %{count}개의 계정에 영향을 끼칩니다 + retroactive: + silence: 이 도메인에 존재하는 모든 계정의 침묵를 해제 + suspend: 이 도메인에 존재하는 모든 계정의 계정 정지를 해제 + title: "%{domain}의 도메인 차단을 해제" + undo: 실행 취소 + title: 도메인 차단 + undo: 실행 취소 + instances: + account_count: 알려진 계정의 수 + domain_name: 도메인 이름 + title: 알려진 인스턴스들 + reports: + action_taken_by: 신고 처리자 + are_you_sure: 정말로 실행하시겠습니까? + comment: + label: 코멘트 + none: 없음 + delete: 삭제 + id: ID + mark_as_resolved: 해결 완료 처리 + nsfw: + 'false': NSFW 꺼짐 + 'true': NSFW 켜짐 + report: '신고 #%{id}' + report_contents: 내용 + reported_account: 신고 대상 계정 + reported_by: 신고자 + resolved: 해결됨 + silence_account: 계정을 침묵 처리 + status: 상태 + suspend_account: 계정을 정지 + target: 대상 + title: 신고 + unresolved: 미해결 + view: 표시 + settings: + contact_information: + email: 공개할 메일 주소를 입력 + username: 아이디를 입력 + registrations: + closed_message: + desc_html: 신규 등록을 받지 않을 때 프론트 페이지에 표시됩니다. <br>HTML 태그를 사용할 수 있습니다. + title: 신규 등록 정지 시 메시지 + open: + title: 신규 등록을 받음 + site_description: + desc_html: 탑 페이지와 meta 태그에 사용됩니다.<br>HTML 태그, 예를 들어<code><a></code> 태그와 <code><em></code> 태그를 사용할 수 있습니다. + title: 사이트 설명 + site_description_extended: + desc_html: 인스턴스 정보 페이지에 표시됩니다.<br>HTML 태그를 사용할 수 있습니다. + title: 사이트 상세 설명 + site_title: 사이트 이름 + title: 사이트 설정 + subscriptions: + callback_url: 콜백 URL + confirmed: 확인됨 + expires_in: 기한 + last_delivery: 최종 발송 + title: PubSubHubbub + topic: 토픽 + title: 관리 + admin_mailer: + new_report: + body: "%{reporter} 가 %{target} 를 신고했습니다" + subject: "%{instance} 에 새 신고 등록됨 (#%{id})" + application_mailer: + settings: '메일 설정을 변경: %{link}' + signature: Mastodon %{instance} 인스턴스로에서 알림 + view: 'View:' + applications: + invalid_url: 올바르지 않은 URL입니다 + auth: + change_password: 보안 + delete_account: 계정 삭제 + delete_account_html: 계정을 삭제하고 싶은 경우, <a href="%{path}">여기서</a> 삭제할 수 있습니다. 삭제 전 확인 화면이 표시됩니다. + didnt_get_confirmation: 확인 메일을 받지 못하셨습니까? + forgot_password: 비밀번호를 잊어버리셨습니까? + login: 로그인 + logout: 로그아웃 + register: 등록하기 + resend_confirmation: 확인 메일을 다시 보내기 + reset_password: 비밀번호 재설정 + set_new_password: 새 비밀번호 + authorize_follow: + error: 리모트 팔로우 도중 오류가 발생했습니다. + follow: 팔로우 + prompt_html: '나(<strong>%{self}</strong>) 는 아래 계정의 팔로우를 요청했습니다:' + title: "%{acct} 를 팔로우" + datetime: + distance_in_words: + about_x_hours: "%{count}시간" + about_x_months: "%{count}월" + about_x_years: "%{count}년" + almost_x_years: "%{count}년" + half_a_minute: 지금 + less_than_x_minutes: "%{count}분" + less_than_x_seconds: 지금 + over_x_years: "%{count}년" + x_days: "%{count}일" + x_minutes: "%{count}분" + x_months: "%{count}월" + x_seconds: "%{count}초" + deletes: + bad_password_msg: 비밀번호가 올바르지 않습니다 + confirm_password: 본인 확인을 위해, 현재 사용 중인 비밀번호를 입력해 주십시오. + description_html: 계정에 업로드된 모든 컨텐츠가 삭제되며, 계정은 비활성화 됩니다. 이것은 영구적으로 이루어지는 것이므로 <strong>되돌릴 수 없습니다</strong>. 사칭 행위를 방지하기 위해 같은 아이디로 다시 등록하는 것은 불가능합니다. + proceed: 계정 삭제 + success_msg: 계정이 정상적으로 삭제되었습니다. + warning_html: 삭제가 보장되는 것은 이 인스턴스 상에서의 컨텐츠에 한합니다. 타 인스턴스 등, 외부에 멀리 공유된 컨텐츠는 흔적이 남아 삭제되지 않는 경우도 있습니다. 그리고 현재 접속이 불가능한 서버나, 업데이트를 받지 않게 된 서버에 대해서는 삭제가 반영되지 않을 수도 있습니다. + warning_title: 공유된 컨텐츠에 대해서 + errors: + '403': 이 페이지를 표시할 권한이 없습니다 + '404': 페이지를 찾을 수 없습니다 + '410': 이 페이지는 더 이상 존재하지 않습니다 + '422': + content: 보안 인증에 실패했습니다. Cookie를 차단하고 있진 않습니까? + title: 보안 인증 실패 + '429': 요청 횟수 제한에 도달했습니다. + noscript: Mastodon을 사용하기 위해서는 JavaScript를 켜 주십시오. + exports: + blocks: 차단 + csv: CSV + follows: 팔로우 + mutes: 뮤트 + storage: 미디어 + followers: + domain: 도메인 + explanation_html: 프라이버시를 확보하고 싶은 경우, 누가 여러분을 팔로우 하고 있는지 파악해둘 필요가 있습니다. <strong>프라이빗 포스팅은 여러분의 팔로워가 소속하는 모든 인스턴스로 배달됩니다</strong>. 팔로워가 소속된 인스턴스 관리자나 소프트웨어가 여러분의 프라이버시를 존중하고 있는지 잘 모를 경우, 그 팔로워를 삭제하는 것이 좋을 수도 있습니다. + followers_count: 팔로워 수 + lock_link: 비공개 계정 + purge: 팔로워에서 삭제 + success: + one: 1개 도메인에서 팔로워를 soft-block 처리 중... + other: "%{count}개 도메인에서 팔로워를 soft-block 처리 중..." + true_privacy_html: "<strong>프라이버시 보호는 End-to-End 암호화로만 이루어 질 수 있다는 것에 유의</strong>해 주십시오." + unlocked_warning_html: 누구든 여러분을 팔로우 할 수 있으며, 여러분의 프라이빗 투고를 볼 수 있습니다. 팔로우 할 수 있는 사람을 제한하고 싶은 경우 %{lock_link}에서 설정해 주십시오. + unlocked_warning_title: 이 계정은 비공개로 설정되어 있지 않습니다. + generic: + changes_saved_msg: 정상적으로 변경되었습니다. + powered_by: powered by %{link} + save_changes: 변경 사항을 저장 + validation_errors: + one: 오류가 발생했습니다. 아래 오류를 확인해 주십시오 + other: 오류가 발생했습니다. 아래 %{count}개 오류를 확인해 주십시오 + imports: + preface: 다른 인스턴스에서 내보내기 한 파일에서 팔로우 / 차단 정보를 이 인스턴스 계정으로 불러올 수 있습니다. + success: 파일이 정상적으로 업로드 되었으며, 현재 처리 중입니다. 잠시 후 다시 확인해 주십시오. + types: + blocking: 차단한 계정 목록 + following: 팔로우 중인 계정 목록 + muting: 뮤트 중인 계정 목록 + upload: 업로드 + landing_strip_html: "<strong>%{name}</strong> 님은 %{link_to_root_path} 인스턴스의 사용자입니다. 계정을 가지고 있다면 팔로우 하거나 대화할 수 있습니다." + landing_strip_signup_html: 아직 계정이 없다면 <a href="%{sign_up_path}">여기서</a> 등록할 수 있습니다. + media_attachments: + validations: + images_and_video: 이미 사진이 첨부되어 있으므로 동영상을 첨부할 수 없습니다. + too_many: 최대 4개까지 첨부할 수 있습니다. + notification_mailer: + digest: + body: "%{instance} 에서 마지막 로그인 뒤로 일어난 일:" + mention: "%{name} 님이 답장했습니다:" + new_followers_summary: + one: 새 팔로워가 생겼습니다! + other: "%{count} 명의 팔로워가 생겼습니다!" + subject: + one: "1건의 새로운 알림 \U0001F418" + other: "%{count}건의 새로운 알림 \U0001F418" + favourite: + body: "%{name} 님이 내 Toot을 즐겨찾기에 등록했습니다." + subject: "%{name} 님이 내 Toot을 즐겨찾기에 등록했습니다" + follow: + body: "%{name} 님이 나를 팔로우 했습니다" + subject: "%{name} 님이 나를 팔로우 했습니다" + follow_request: + body: "%{name} 님이 내게 팔로우 요청을 보냈습니다." + subject: "%{name} 님이 보낸 팔로우 요청" + mention: + body: "%{name} 님이 답장을 보냈습니다:" + subject: "%{name} 님이 답장을 보냈습니다" + reblog: + body: "%{name} 님이 내 Toot을 부스트 했습니다:" + subject: "%{name} 님이 내 Toot을 부스트 했습니다" + pagination: + next: 다음 + prev: 이전 + truncate: "…" + remote_follow: + acct: 아이디@도메인을 입력해 주십시오 + missing_resource: 리디렉션 대상을 찾을 수 없습니다 + proceed: 팔로우 하기 + prompt: 팔로우 하려 하고 있습니다 + sessions: + activity: 마지막 활동 + browser: 브라우저 + browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + firefox: Firefox + generic: 알 수 없는 브라우저 + ie: Internet Explorer + micro_messenger: MicroMessenger + nokia: Nokia S40 Ovi Browser + opera: Opera + phantom_js: PhantomJS + qq: QQ Browser + safari: Safari + uc_browser: UCBrowser + weibo: Weibo + current_session: 현재 세션 + description: "%{browser} on %{platform}" + explanation: 내 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: 알 수 없는 플랫폼 + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone + title: 세션 + settings: + authorized_apps: 인증된 어플리케이션 + back: 돌아가기 + delete: 계정 삭제 + edit_profile: 프로필 편집 + export: 데이터 내보내기 + followers: 신뢰 중인 인스턴스 + import: 데이터 가져오기 + preferences: 사용자 설정 + settings: 설정 + two_factor_authentication: 2단계 인증 + statuses: + open_in_web: Web으로 열기 + over_character_limit: 최대 %{max}자까지 입력할 수 있습니다 + show_more: 더 보기 + visibilities: + private: 비공개 + private_long: 팔로워에게만 표시됩니다 + public: 공개 + public_long: 누구나 볼 수 있으며, 공개 타임라인에 표시됩니다 + unlisted: Unlisted + unlisted_long: 누구나 볼 수 있지만, 공개 타임라인에는 표시되지 않습니다 + stream_entries: + click_to_show: 클릭해서 표시 + reblogged: 님이 부스트 했습니다 + sensitive_content: 민감한 컨텐츠 + time: + formats: + default: "%Y년 %m월 %d일 %H:%M" + two_factor_authentication: + code_hint: 확인하기 위해서 인증 어플리케이션에서 표시된 코드를 입력해 주십시오 + description_html: "<strong>2단계 인증</strong>을 활성화 하면 로그인 시 전화로 인증 코드를 받을 필요가 있습니다." + disable: 비활성화 + enable: 활성화 + enabled: 2단계 인증이 활성화 되어 있습니다 + enabled_success: 2단계 인증이 활성화 되었습니다 + generate_recovery_codes: 복구 코드 생성 + instructions_html: "<strong>Google Authenticator, 또는 타 TOTP 어플리케이션에서 이 QR 코드를 스캔해 주십시오.</strong> 이후 로그인 시에는 이 어플리케이션에서 생성되는 코드가 필요합니다." + lost_recovery_codes: 복구 코드를 사용하면 휴대전화를 분실한 경우에도 계정에 접근할 수 있게 됩니다. 복구 코드를 분실한 경우에도 여기서 다시 생성할 수 있지만, 예전 복구 코드는 비활성화 됩니다. + manual_instructions: 'QR 코드를 스캔할 수 없어 수동으로 등록을 원하시는 경우 이 비밀 코드를 사용해 주십시오: ' + recovery_codes: 복구 코드 + recovery_codes_regenerated: 복구 코드가 다시 생성되었습니다. + recovery_instructions_html: 휴대전화를 분실한 경우, 아래 복구 코드 중 하나를 사용해 계정에 접근할 수 있습니다. <strong>복구 코드는 안전하게 보관해 주십시오.</strong> 이 코드를 인쇄해 중요한 서류와 함께 보관하는 것도 좋습니다. + setup: 초기 설정 + wrong_code: 코드가 올바르지 않습니다. 서버와 휴대전화 간의 시간이 일치하는지 확인해 주십시오. + users: + invalid_email: 메일 주소가 올바르지 않습니다 + invalid_otp_token: 2단계 인증 코드가 올바르지 않습니다 diff --git a/config/locales/nl.yml b/config/locales/nl.yml index d9b02e09c..dfc58f6b3 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -3,23 +3,12 @@ nl: about: about_mastodon: Mastodon is een <em>vrij, gratis en open-source</em> sociaal netwerk. Een <em>gedecentraliseerd</em> alternatief voor commerciële platforms. Het voorkomt de risico's van een enkel bedrijf dat jouw communicatie monopoliseert. Kies een server die je vertrouwt — welke je ook kiest, je kunt met elke andere server communiceren. Iedereen kan een eigen Mastodon-server draaien en naadloos deelnemen in het <em>sociale netwerk</em>. about_this: Over deze server - apps: Apps business_email: 'E-mailadres:' closed_registrations: Registreren op deze server is momenteel uitgeschakeld. contact: Contact description_headline: Wat is %{domain}? domain_count_after: andere servers domain_count_before: Verbonden met - features: - api: Open API voor apps en diensten - blocks: Uitgebreide blokkeer- en negeerhulpmiddelen - characters: 500 tekens per bericht - chronology: Tijdlijnen zijn chronologisch - ethics: 'Ethisch design: geen advertenties, geen spionage' - gifv: GIFV-sets en korte video's - privacy: Nauwkeurige privacyinstellingen per toot (bericht) - public: Openbare tijdlijnen - features_headline: Wat maakt Mastodon anders get_started: Beginnen links: Links other_instances: Andere servers @@ -36,24 +25,43 @@ nl: nothing_here: Hier is niets! people_followed_by: Mensen die %{name} volgt people_who_follow: Mensen die %{name} volgen - posts: Berichten + posts: Toots remote_follow: Extern volgen unfollow: Ontvolgen admin: + reports: + action_taken_by: Actie uitgevoerd door + are_you_sure: Weet je het zeker? + comment: + label: Opmerking + none: Geen + delete: Verwijderen + id: ID + mark_as_resolved: Markeer als opgelost + nsfw: + 'false': Media tonen + 'true': Media verbergen + report: 'Gerapporteerde toot #%{id}' + reported_account: Gerapporteerde account + reported_by: Gerapporteerd door + resolved: Opgelost + silence_account: Account stilzwijgen + status: Toot + suspend_account: Account blokkeren + target: Target + title: Gerapporteerde toots + unresolved: Onopgelost + view: Weergeven settings: contact_information: email: Vul een openbaar gebruikt e-mailadres in - label: Contactgegevens username: Vul een gebruikersnaam in registrations: closed_message: desc_html: Wordt op de voorpagina weergegeven wanneer registratie van nieuwe accounts is uitgeschakeld<br>En ook hier kan je HTML gebruiken title: Bericht wanneer registratie is uitgeschakeld open: - disabled: Uitgeschakeld - enabled: Ingeschakeld title: Open registratie - setting: Instelling site_description: desc_html: Dit wordt als een alinea op de voorpagina getoond en gebruikt als meta-tag in de paginabron.<br>Je kan HTML gebruiken, zoals <code><a></code> en <code><em></code>. title: Omschrijving Mastodon-server @@ -62,24 +70,11 @@ nl: title: Uitgebreide omschrijving Mastodon-server site_title: Naam Mastodon-server title: Server-instellingen - admin.reports: - comment: - label: Opmerking - none: Geen - delete: Verwijderen - id: ID - mark_as_resolved: Markeer als opgelost - report: 'Gerapporteerde toot #%{id}' - reported_account: Gerapporteerde account - reported_by: Gerapporteerd door - resolved: Opgelost - silence_account: Account stilzwijgen - status: Toot - suspend_account: Account blokkeren - target: Target - title: Gerapporteerde toots - unresolved: Onopgelost - view: Weergeven + title: Beheer + admin_mailer: + new_report: + body: "%{reporter} heeft %{target} gerapporteerd" + subject: Nieuwe toots gerapporteerd op %{instance} (#%{id}) application_mailer: settings: 'E-mailvoorkeuren wijzigen: %{link}' signature: Mastodon-meldingen van %{instance} @@ -87,7 +82,9 @@ nl: applications: invalid_url: De opgegevens URL is ongeldig auth: - change_password: Inloggegevens + change_password: Beveiliging + delete_account: Account verwijderen + delete_account_html: Wanneer je jouw account graag wilt verwijderen, kan je dat <a href="%{path}">hier doen</a>. We vragen jou daar om een bevestiging. didnt_get_confirmation: Geen bevestigingsinstructies ontvangen? forgot_password: Wachtwoord vergeten? login: Aanmelden @@ -115,12 +112,23 @@ nl: x_minutes: "%{count}m" x_months: "%{count}ma" x_seconds: "%{count}s" + deletes: + bad_password_msg: Goed geprobeerd hackers! Ongeldig wachtwoord + confirm_password: Voer jouw huidige wachtwoord in om jouw identiteit te bevestigen + description_html: Hierdoor worden alle gegevens van jouw account <strong>permanent, onomkeerbaar</strong> verwijderd en wordt deze gedeactiveerd. Om toekomstige identiteitsdiefstal te voorkomen, kan op deze server jouw gebruikersnaam niet meer gebruikt worden. + proceed: Account verwijderen + success_msg: Jouw account is succesvol verwijderd + warning_html: We kunnen alleen garanderen dat jouw gegevens op deze server worden verwijderd. Berichten (toots), incl. media, die veel zijn gedeeld laten mogelijk sporen achter. Offline servers en servers die niet meer op jouw updates zijn geabonneerd zullen niet hun databases updaten. + warning_title: Verwijdering gegevens op andere servers errors: + '403': Jij hebt geen toestemming om deze pagina te bekijken. '404': De pagina waarnaar jij op zoek bent bestaat niet. '410': De pagina waarnaar jij op zoek bent bestaat niet meer. '422': content: Veiligheidsverificatie mislukt. Blokkeer je toevallig cookies? title: Veiligheidsverificatie mislukt + '429': Te veel verbindingsaanvragen. + noscript: Schakel JavaScript in om Mastodon te kunnen gebruiken. exports: blocks: Jij blokkeert csv: CSV @@ -141,7 +149,7 @@ nl: unlocked_warning_title: Jouw account is niet besloten generic: changes_saved_msg: Wijzigingen succesvol opgeslagen! - powered_by: mogelijk gemaakt door %{link} + powered_by: wordt mogelijk gemaakt door %{link} save_changes: Wijzigingen opslaan validation_errors: one: Er is iets niet helemaal goed! Bekijk onderstaande fout @@ -154,7 +162,7 @@ nl: following: Volglijst muting: Negeerlijst upload: Uploaden - landing_strip_html: <strong>%{name}</strong> is een gebruiker op %{link_to_root_path}. Je kunt deze volgen en ermee communiceren als je ergens in deze fediverse een account hebt. + landing_strip_html: "<strong>%{name}</strong> is een gebruiker op %{link_to_root_path}. Je kunt deze volgen en ermee communiceren als je ergens in deze fediverse een account hebt." landing_strip_signup_html: Als je dat niet hebt, kun je je <a href="%{sign_up_path}">hier registreren</a>. notification_mailer: digest: @@ -189,6 +197,43 @@ nl: missing_resource: Kon vereiste doorverwijzings-URL voor jouw account niet vinden proceed: Ga door om te volgen prompt: 'Jij gaat volgen:' + sessions: + activity: Laatst actief + browser: Webbrowser + browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + firefox: Firefox + generic: Onbekende webbrowser + ie: Internet Explorer + micro_messenger: MicroMessenger + nokia: Nokia S40 Ovi Browser + opera: Opera + phantom_js: PhantomJS + qq: QQ Browser + safari: Safari + uc_browser: UCBrowser + weibo: Weibo + current_session: Huidige sessie + description: "%{browser} op %{platform}" + explanation: Dit zijn de webbrowsers die momenteel met jouw Mastodon-account zijn ingelogd. + 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: Onbekend platform + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone + title: Sessies settings: authorized_apps: Geautoriseerde apps back: Terug naar Mastodon @@ -226,7 +271,7 @@ nl: generate_recovery_codes: Herstelcodes genereren instructions_html: "<strong>Scan deze QR-code in Google Authenticator of een soortgelijke app op jouw mobiele telefoon</strong>. Van nu af aan genereert deze app aanmeldcodes die je bij het aanmelden moet invoeren." lost_recovery_codes: Met herstelcodes kun je toegang tot jouw account krijgen wanneer je jouw telefoon bent kwijtgeraakt. Wanneer je jouw herstelcodes bent kwijtgeraakt, kan je ze hier opnieuw genereren. Jouw oude herstelcodes zijn daarna ongeldig. - manual_instructions: 'Hieronder vind je de geheime code in platte tekst. Voor het geval je de QR-code niet kunt scannen en het handmatig moet invoeren.' + manual_instructions: Hieronder vind je de geheime code in platte tekst. Voor het geval je de QR-code niet kunt scannen en het handmatig moet invoeren. recovery_codes_regenerated: Opnieuw genereren herstelcodes geslaagd recovery_instructions_html: Wanneer je ooit de toegang verliest tot jouw telefoon, kan je met behulp van een van de herstelcodes hieronder opnieuw toegang krijgen tot jouw account. Zorg ervoor dat je de herstelcodes op een veilige plek bewaard. (Je kunt ze bijvoorbeeld printen en ze samen met andere belangrijke documenten bewaren.) setup: Instellen diff --git a/config/locales/no.yml b/config/locales/no.yml index f71c08c6a..004e1ff80 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -3,23 +3,12 @@ about: about_mastodon: Mastodon er et sosialt nettverk laget med <em>fri programvare</em>. Et <em>desentralisert</em> alternativ til kommersielle plattformer. Slik kan det unngå risikoene ved å ha et enkelt selskap som monopoliserer din kommunikasjon. Velg en tjener du stoler på — uansett hvilken du velger så kan du kommunisere med alle andre. Alle kan kjøre sin egen Mastodon og delta sømløst i det sosiale nettverket. about_this: Om denne instansen - apps: Applikasjoner business_email: 'Bedriftsepost:' closed_registrations: Registreringer er for øyeblikket lukket på denne instansen. contact: Kontakt description_headline: Hva er %{domain}? domain_count_after: andre instanser domain_count_before: Koblet til - features: - api: Åpent API for applikasjoner og tjenester - blocks: Rikholdige blokkeringsverktøy - characters: 500 tegn per status - chronology: Tidslinjer er kronologiske - ethics: 'Etisk design: Ingen reklame, ingen sporing' - gifv: Støtte for GIFV og korte videoer - privacy: Finmaskede personvernsinnstillinger - public: Felles tidslinjer - features_headline: Hva skiller Mastodon fra andre sosiale nettverk get_started: Kom i gang links: Lenker other_instances: Andre instanser @@ -153,17 +142,13 @@ settings: contact_information: email: Skriv en offentlig e-postadresse - label: Kontaktinformasjon username: Skriv brukernavn registrations: closed_message: desc_html: Vises på forsiden når registreringer er lukket<br>Du kan bruke HTML-tagger title: Melding for lukket registrering open: - disabled: På - enabled: Av title: Åpen registrering - setting: Innstilling site_description: desc_html: Vises som et avsnitt på forsiden og brukes som en meta-tagg.<br> Du kan bruke HTML-tagger, spesielt <code><a></code> og <code><em></code>. title: Nettstedsbeskrivelse @@ -321,6 +306,76 @@ click_to_show: Klikk for å vise reblogged: fremhevde sensitive_content: Følsomt innhold + terms: + body_html: | + <h2>Personvernserklæring</h2> + + <h3 id='collect'>Hvilke opplysninger samler vi?</h3> + + <p>Vi samler opplysninger fra deg når du registrerer deg på nettstedet vårt, og vi samler data når du deltar på forumet ved å lese, skrive og evaluere innholdet som deles her.</p> + + <p>Når du registrerer deg på nettstedet vårt, kan du bli bedt om å oppgi navnet og e-postadressen din. Imidlertid kan du besøke nettstedet vårt uten å registrere deg. E-postadressen din vil bli bekreftet med en e-post som inneholder en unik lenke. Hvis siden den lenker til, blir besøkt, vet vi at du har kontroll over e-postadressen.</p> + + <p>Når du registrerer deg og skriver innlegg, registrerer vi IP-adressen som innlegget stammer fra. Vi kan også oppbevare logger som inkluderer IP-adressen til alle forespørslene sendt til tjeneren vår.</p> + + <h3 id='use'>Hva bruker vi opplysningene dine til?</h3> + + <p>Alle opplysningene vi samler fra deg, kan bli brukt på en av følgende måter:</p> + + <ul> + <li>For å gjøre opplevelsen din mer personlig. Opplysningene dine hjelper oss å svare bedre på dine individuelle behov.</li> + <li>For å forbedre nettstedet vårt. Vi jobber konstant for å forbedre nettstedets tilbud basert på opplysningene og tilbakemeldingene vi mottar fra deg.</li> + <li>For å forbedre vår kundeservice. Dine opplysninger hjelper oss å svare mer effektivt på dine forespørsler sendt til kundeservice eller behov om støtte.</li> + <li>For å sende periodiske e-poster. E-postadressen du oppgir, kan bli brukt til å sende deg informasjon, påminnelser som du ber om ved endringer av emner eller ved svar til brukernavnet ditt, til henvendelser, og/eller andre forspørsler eller andre spørsmål.</li> + </ul> + + <h3 id='protect'>Hvordan sikrer vi opplysningene?</h3> + + <p>Vi gjennomfører flere sikkerhetstiltak for å holde personopplysningene dine sikre når du skriver inn, lagrer eller henter dem.</p> + + <h3 id='data-retention'>Hva er retningslinjene deres for lagring av data?</h3> + + <p>Vi vil forsøke i god tro å:</p> + + <ul> + <li>Ikke oppbevare tjener-logger som inneholder IP-adressen til alle forespørslene til denne tjeneren i lenger enn i 90 dager.</li> + <li>Ikke oppbevare IP-adressene forbundet med registrerte brukere og deres innlegg lenger enn i 5 år.</li> + </ul> + + <h3 id='cookies'>Bruker vi informasjonskapsler?</h3> + + <p>Ja. Informasjonskapsler er små filer som et nettsted eller dets tjenesteleverandør overfører til harddisken på datamaskinen din gjennom nettleseren din (dersom du tillater det). Disse informasjonskapslene gjør det mulig for nettstedet å gjenkjenne nettleseren din og, dersom du har en konto, knytte nettleseren til den.</p> + + <p>Vi bruker informasjonskapsler for å forstå og lagre preferansene dine for fremtidige besøk og for å samle aggregatdata om trafikk på og samhandling med nettstedet slik at vi kan tilby bedre opplevelser og verktøy på nettstedet i fremtiden. Vi kan inngå avtaler med tredjeparts tjenesteleverandører for å bistå oss i å forstå besøkerne våres bedre. Disse tjenesteleverandørene har ikke lov til å bruke opplysningene samlet på våres vegne unntatt til å hjelpe oss å gjennomføre og forbedre anliggendet vårt.</p> + + <h3 id='disclose'>Gir vi noen opplysninger videre til andre parter?</h3> + + <p>Vi verken selger, handler med eller overfører på noen annen måte til andre parter dine identifiserbare personopplysninger. Dette inkluderer ikke tredjeparter som har vår tillit og bistår oss i å drive nettstedet, utføre våre anliggender eller yter tjenester til deg, så lenge disse partene samtykker til å behandle disse opplysningene fortrolig. Vi kan også frigi opplysningene dine dersom vi tror at å frigi dem er hensiktsmessig for å overholde loven, håndheve nettstedet retningslinjer eller beskytte våre og andres rettigheter. Imidlertid kan opplysninger som ikke er personlig identifiserbare, bli delt med andre parter for markedsføring, reklame eller annet bruk.</p> + + <h3 id='third-party'>Tredjeparts lenker</h3> + + <p>Av og til, etter skjønn, kan vil inkludere eller tilby tredjeparts produkter eller tjenester på nettstedet vårt. Disse tredjeparts nettstedene har separate og selvstendige personvernerklæringer. Vi bærer derfor intet ansvar eller forpliktelser for innholdet eller aktivitetene til disse nettstedene det lenkes til. Ikke mindre prøver vi å bevare vår eget nettsteds integritet og ønsker enhver tilbakemelding om disse nettstedene velkomne.</p> + + <h3 id='coppa'>Overensstemmelse med Children's Online Privacy Protection Act</h3> + + <p>Nettstedet er rettet mot folk som er minst 13 år gamle. Dersom denne tjeneren er i USA, og du er under 13 år i henhold til kravene i COPPA (<a href="https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act">Children's Online Privacy Protection Act</a>), ikke bruk dette nettstedet.</p> + + <h3 id='online'>Personvernerklæring bare for nettet</h3> + + <p>Denne nett-personvernerklæringen gjelder bare for informasjon samlet gjennom nettstedet vårt og ikke for opplysninger samlet når en er frakoblet.</p> + + <h3 id='consent'>Ditt samtykke</h3> + + <p>Ved å bruke dette nettstedet samtykker du til nettstedets personvernerklæring.</p> + + <h3 id='changes'>Endringer i vår personvernerklæring</h3> + + <p>Dersom vi beslutter å endre personvernerklæringen vår, vil vi publisere disse endringene på denne siden.</p> + + <p>Dette dokumentet er lisensiert under CC-BY-SA. De ble sist oppdatert 12. april 2017.</p> + + <p>Dokumentet er en adoptert og endret versjon fra <a href="https://github.com/discourse/discourse">Discourse privacy policy</a>.</p> + title: "%{instance} Personvern og villkår for bruk av nettstedet" time: formats: default: "%-d. %b %Y, %H:%M" diff --git a/config/locales/oc.yml b/config/locales/oc.yml index c882b43a1..91a6ca791 100644 --- a/config/locales/oc.yml +++ b/config/locales/oc.yml @@ -3,23 +3,12 @@ oc: about: about_mastodon: Mastodon es un malhum social <em>liure e open-source</em>. Una alternativa <em>decentralizada</em> a las platformas comercialas, aquò evita qu’una sola companiá monopolize vòstra comunicacion. Causissètz une servidor que vos fisatz, quina que siague vòstra causida, podètz interagir amb tot lo mond. Qual que siague pòt aver son instància Mastodon e participar al <em>malhum social</em> sens cap de problèmas. about_this: A prepaus d’aquesta instància - apps: Aplicacions business_email: 'Corrièl professional :' closed_registrations: Las inscripcions son clavadas pel moment sus aquesta instància. contact: Contacte description_headline: Qué es %{domain} ? domain_count_after: autras instàncias domain_count_before: Connectat a - features: - api: API dubèrta per las aplicacions e servicis - blocks: Aisinas complètas per blocar e rescondre - characters: 500 caractèrs per publicacion - chronology: Flux d’actualitat cronologic - ethics: 'Ethical design: pas cap de reclama o traçador' - gifv: Partatge de GIFs e vidèos cortas - privacy: Nivèl de confidencialitat configurable per cada publicacion - public: Fluxes d’actualitat publics - features_headline: Çò que fa que Mastodon es diferent get_started: Venètz al malhum links: Ligams other_instances: Autras instàncias @@ -160,17 +149,13 @@ oc: settings: contact_information: email: Picatz una adreça de corrièl - label: Informacions de contacte username: Picatz un nom d’utilizaire registrations: closed_message: desc_html: Afichat sus las pagina d’acuèlh quand las inscripcions son tampadas.<br>Podètz utilizar de balisas HTML title: Messatge de barradura de las inscripcions open: - disabled: Desactivadas - enabled: Activadas title: Inscripcions - setting: Paramètre site_description: desc_html: Afichada jos la forma de paragrafe sus la pagina d’acuèlh e utilizada coma balisa meta.<br> Podètz utilizar de balisas HTML, coma <code><a></code> et <code><em></code>. title: Descripcion del site @@ -220,7 +205,7 @@ oc: - dv - ds abbr_month_names: - - + - - gen - feb - mar @@ -242,11 +227,11 @@ oc: - divendres - dissabte formats: - default: "%d/%m/%Y" - long: Lo %B %d de %Y - short: "%b %d" + default: "%e/%m/%Y" + long: Lo %e %B de %Y + short: "%e %b" month_names: - - + - - de genièr - de febrièr - de març diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 113d7f235..c6588e846 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -3,23 +3,12 @@ pl: about: about_mastodon: Mastodon jest <em>wolną i otwartą</em> siecią społecznościową, <em>zdecentralizowaną</em> alternatywą dla zamkniętych, komercyjnych platform. Pozwala uniknąć ryzyka monopolizacji Twojej komunikacji przez jedną korporację. Wybierz serwer, któremu ufasz — nie ograniczy to Twoich możliwości komunikacji z innymi osobami w sieci. Każdy może też uruchomić własną instancję Mastodona i dołączyć do reszty tej <em>sieci społecznościowej</em>. about_this: O tej instancji - apps: Aplikacje business_email: 'Służbowy adres e-mail:' closed_registrations: Rejestracja na tej instancji jest obecnie zamknięta. contact: Kontakt description_headline: Czym jest %{domain}? domain_count_after: instancji domain_count_before: Serwer połączony z - features: - api: Otwarte API dla aplikacji i usług - blocks: Rozbudowane narzędzia blokowania i ukrywania - characters: 500 znaków na wpis - chronology: Chronologiczny porządek wyświetlania - ethics: 'Etyczne założenia: nie śledzimy, bez reklam' - gifv: obsługa GIFV i krótkich wideo - privacy: Precyzyjne ustawienia widoczności poszczególnych postów - public: Publiczne osie czasu - features_headline: Co wyróżnia Mastodona get_started: Rozpocznijmy! links: Odnośniki other_instances: Inne instancje @@ -61,8 +50,8 @@ pl: edit: Edytuj email: Adres e-mail feed_url: Adres kanału - followers: Obserwujący - follows: Obserwacje + followers: Śledzący + follows: Śledzeni ip: Adres IP location: all: Wszystkie @@ -114,7 +103,7 @@ pl: create: Utwórz blokadę hint: Blokada domen nie zabroni tworzenia wpisów kont w bazie danych, ale pozwoli na automatyczną moderację kont do nich należących. severity: - desc_html: "<strong>Wyciszenie</strong> uczyni wpisy użytkownika niewidoczne dla osób, które go nie obserwują. <strong>Zawieszenie</strong> spowoduje usunięcie całej zawartości dodanej przez użytkownika." + desc_html: "<strong>Wyciszenie</strong> uczyni wpisy użytkownika widoczne tylko dla osób, które go śledzą. <strong>Zawieszenie</strong> spowoduje usunięcie całej zawartości dodanej przez użytkownika." silence: Wycisz suspend: Zawieś title: Nowa blokada domen @@ -166,23 +155,22 @@ pl: settings: contact_information: email: Wprowadź publiczny adres e-mail - label: Informacje kontaktowe username: Wprowadź nazwę użytkownika registrations: closed_message: desc_html: Wyświetlana na stronie głównej, gdy możliwość otwarej rejestracji<br>nie jest dostępna. Możesz korzystać z tagów HTML title: Wiadomość o nieaktywnej rejestracji open: - disabled: Nieaktywna - enabled: Aktywna title: Otwarta rejestracja - setting: Ustawienie site_description: desc_html: Wyświetlany jako nagłówek na stronie głównej oraz jako meta tag.<br>Możesz korzystać z tagów HTML, w szczególności z <code><a></code> i <code><em></code>. title: Opis strony site_description_extended: desc_html: Wyświetlany w rozszerzonych informacjach o stronie<br>Możesz korzystać z tagów HTML title: Extended site description + site_terms: + desc_html: Wyświetlana na stronie zasad użytkowania<br>Możesz używać tagów HTML + title: Polityka prywatności strony site_title: Tytuł strony title: Ustawienia strony subscriptions: @@ -195,7 +183,7 @@ pl: title: Administracja admin_mailer: new_report: - body: "Użytkownik %{reporter} zgłosił %{target}" + body: Użytkownik %{reporter} zgłosił %{target} subject: Nowe zgłoszenie na %{instance} (#%{id}) application_mailer: settings: 'Zmień ustawienia powiadamiania: %{link}' @@ -206,9 +194,9 @@ pl: auth: change_password: Bezpieczeństwo delete_account: Usunięcie konta - delete_account_html: Jeżeli próbowałeś usunąć konto, <a href="%{path}">przejdź tutaj</a>. Otrzymasz prośbę o potwierdzenie. + delete_account_html: Jeżeli chcesz usunąć konto, <a href="%{path}">przejdź tutaj</a>. Otrzymasz prośbę o potwierdzenie. didnt_get_confirmation: Nie otrzymałeś instrukcji weryfikacji? - forgot_password: Zapomniane hasło + forgot_password: Nie pamiętasz hasła? login: Zaloguj się logout: Wyloguj się register: Rejestracja @@ -218,6 +206,12 @@ pl: authorize_follow: error: Niestety, podczas sprawdzania zdalnego konta wystąpił błąd follow: Śledź + follow_request: 'Wysłano prośbę o pozwolenie na obserwację:' + following: 'Pomyślnie! Od teraz śledzisz:' + post_follow: + close: Ewentualnie, możesz po prostu zamknąć tą stronę. + return: Powróć do strony użytkownika + web: Przejdź do sieci prompt_html: 'Ty (<strong>%{self}</strong>) chcesz śledzić:' title: Śledź %{acct} datetime: @@ -259,15 +253,15 @@ pl: storage: Urządzenie przechowujące dane followers: domain: Domena - explanation_html: Jeżeli chcesz mieć pewność, kto może przeczytać Twoje statusy, musisz kontrolować, kto Cię obserwuje. <strong>Twoje prywatne statusy są dostarczane na te instancje, na których jesteś obserwowany</strong>. Możesz sprawdzać swoich obserwowanych i blokować ich, jeśli nie ufasz właścicielom lub oprogramowaniu danej instancji. - followers_count: Liczba obserwujących + explanation_html: Jeżeli chcesz mieć pewność, kto może przeczytać Twoje statusy, musisz kontrolować, kto śledzi Twój profil. <strong>Twoje prywatne statusy są dostarczane na te instancje, na których jesteś śledzony</strong>. Możesz sprawdzać, kto Cię śledzi i blokować ich, jeśli nie ufasz właścicielom lub oprogramowaniu danej instancji. + followers_count: Liczba śledzących lock_link: Zablokuj swoje konto - purge: Usuń z obserwujących + purge: Przestań śledzić success: - one: W trakcie usuwania obserwujcych z jednej domeny… - other: W trakcie usuwania obserwujących z %{count} domen… + one: W trakcie usuwania śledzących z jednej domeny… + other: W trakcie usuwania śledzących z %{count} domen… true_privacy_html: Pamiętaj, że <strong>rzeczywista prywatność może zostać uzyskana wyłącznie dzięki szyfrowaniu end-to-end</strong>. - unlocked_warning_html: Każdy może cię zaobserwować, aby natychmiastowo zobaczyć twoje statusy. %{lock_link} aby móc kontrolować obserwujących. + unlocked_warning_html: Każdy może cię śledzić, aby natychmiastowo zobaczyć twoje statusy. %{lock_link} aby móc kontrolować, kto Cię śledzi. unlocked_warning_title: Twoje konto nie jest zablokowane generic: changes_saved_msg: Ustawienia zapisane! @@ -371,7 +365,7 @@ pl: delete: Usuń konto edit_profile: Edytuj profil export: Eksportuj dane - followers: Autoryzowani obserwujący + followers: Autoryzowani śledzący import: Importuj dane preferences: Preferencje settings: Ustawienia @@ -382,15 +376,85 @@ pl: show_more: Pokaż więcej visibilities: private: Tylko dla śledzących - private_long: Widoczne tylko obserwowanych + private_long: Widoczne tylko dla śledzących public: Publiczny public_long: Widoczne dla wszystkich unlisted: Niewypisany unlisted_long: Widoczne dla wszystkich, ale nie wyświetlane na publicznych osiach czasu stream_entries: click_to_show: Naciśnij aby wyświetlić - reblogged: podbito + reblogged: podbił sensitive_content: Wrażliwa zawartość + terms: + body_html: | + <h2>Polityka prywatności</h2> + + <h3 id="collect">Jakie informacje zbieramy?</h3> + + <p>Zbieramy informacje podane przy rejestracji i treści utworzone w trakcie korzystania z serwisu.</p> + + <p>Podczas rejestracji, możesz otrzymać prośbę o podanie adresu e-mail. Możesz jednak odwiedzać stronę bez rejestracji. Adres zostanie zweryfikowany przez kliknięcie w link wysłany w wiadomości. Dzięki temu wiemy, że jesteś właścicielem tego adresu.</p> + + <p>Podczas rejestracji i tworzenia postów, Twój adres IP jest zapisywany na naszych serwerach. Możemy też przechowywać adres IP użyty przy każdej operacji w serwisie.</p> + + <h3 id="use">Jak wykorzystujemy zebrane informacje?</h3> + + <p>Zebrane informacje mogą zostać w jednym z następujących celach:</p> + + <ul> + <li>Aby poprawić wrażenia — informacje o Tobie pomagają w dostosowywaniu serwisu do Twoich potrzeb.</li> + <li>Aby usprawnić stronę — nieustannie staramy się ulepszyć stronę na podstawie informacji o Tobie i Twoich opinii.</li> + <li>Aby usprawnić obsługę klienta — informacje pomogą obsłudze klienta utrzymywać kontakt z Tobą.</li> + <li>Aby okazjonalnie wysyłać wiadomości e-mail — Na podany adres e-mail mogą zostać wysłane wiadomości o wspomnieniu o Tobie we wpisach, przejrzeniu Twojego zgłoszenia i innych interakcji z Tobą.</li> + </ul> + + <h3 id="protect">Jak zabezpieczamy dane?</h3> + + <p>Korzystamy z wielu zabezpieczeń, aby utrudnić osobom niepowołanym dostęp do danych, które wprowadzasz, publikujesz i czytasz.</p> + + <h3 id="data-retention">Jak długo przechowujecie dane?</h3> + + <p>Dołożymy wszelkich starań, aby przechowywać:</p> + + <ul> + <li>dzienniki serwera zawierające adresy IP przypisane do każdych operacji nie dłużej niż 90 dni.</li> + <li>adresy IP przypisane do użytkowników i ich wpisów nie dłużej niż 5 lat.</li> + </ul> + + <h3 id="cookies">Czy używamy plików cookies?</h3> + + <p>Tak. Pliki cookies (zwane często ciasteczkami) są małymi zbiorami danych przechowywanych na Twoim dysku przez stronę internetową, aby rozpoznawać przeglądarkę i powiązać ją (jeżeli jesteś zarejestrowany/a) z Twoim kontem, jeżeli na to pozwolisz.</p> + + <p>Możemy używać ciasteczek, aby skonfigurować stronę na podstawie zapisanych preferencji, oraz dostosować ją do potrzeb innych użytkowników. Możemy korzystać z usług firm trzecich pomagających w zrozumieniu potrzeb użytkownika. Te usługi nie mogą korzystać ze zdobytych danych w celach innych niż analiza pomagająca ulepszać ten serwis.</p> + + <h3 id="disclose">Czy przekazujecie dane podmiotów trzecim?</h3> + + <p>Nie dokonujemy transakcji danych pozwalających na identyfikację Twojej osoby umieszczonych na tym serwisie. Nie oznacza to, że nie przekazujemy ich zaufanym podmiotom, które korzystają z nich poufnie. Możemy jednak udostępniać dane, jeżeli jest to wymagane prawnie, lub dla utrzymania bezpieczeństwa strony i innych użytkowników. W celach marketingowych (i podobnych) mogą zostać użyte jedynie dane niepozwalające na identyfikację osoby.</p> + + <h3 id="third-party">Odnośniki do treści stron trzecich</h3> + + <p>Czasem na stronie mogą pojawić się odnośniki do stron trzecich. Mają one odrębne regulaminy i politykę prywatności. Nie odpowiadamy więc za zawartość tych stron. Dokładamy jednak wszelkich starań, aby nie stanowiły one zagrożenia, prosimy jednak o opinie na temat ich wykorzystania.</p> + + <h3 id="coppa">Children's Online Privacy Protection Act Compliance</h3> + + <p>Ta strona i usługa jest przeznaczona dla osób, które ukończyły 13 lat. Jeżeli serwer znajduje się na terenie USA i nie masz ukończonych 13 lat, zgodnie z amerykańską ustawą COPPA (<a href="https://pl.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act">Children's Online Privacy Protection Act</a>) nie możesz korzystać z tego serwisu.</p> + + <h3 id="online">Polityka prywatności dotyczy tylko Internetu</h3> + + <p>Ta polityka prywatności dotyczy jedynie danych zbieranych w Internecie, nie tych, które przechowywane są na Twoim kompurerze, np. pliki cookies.</p> + + <h3 id="consent">Wyrażenie zgody</h3> + + <p>Korzystanie ze strony jest równoznaczne z akceptacją naszej polityki prywatności.</p> + + <h3 id="changes">Zmiany w naszej polityce prywatności</h3> + + <p>Jeżeli zdecydujemy się na zmiany w polityce prywatności, zmiany pojawią się na tej stronie.</p> + + <p>Dokument jest dostępny na licencji CC-BY-SA. Ostatnio modyfikowany 31 maja 2013, przetłumaczony 4 lipca 2017. Tłumaczenie (mimo dołożenia wszelkich starań) może nie być w pełni poprawne.</p> + + <p>Tekst bazuje na <a href="https://github.com/discourse/discourse">polityce prywatności Discourse</a>. + title: Zasady korzystania i polityka prywatności %{instance} time: formats: default: "%b %d, %Y, %H:%M" diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 973a8d401..355c20d05 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -3,23 +3,12 @@ pt-BR: about: about_mastodon: Mastodon é um servidor de rede social <em>grátis, e open-source</em>. Uma alternativa <em>descentralizada</em> ás plataformas comerciais, que evita o risco de uma única empresa monopolizar a sua comunicação. Escolha um servidor que você confie — qualquer um que escolher, você poderá interagir com todo o resto. Qualquer um pode ter uma instância Mastodon e assim participar na <em>rede social federada</em> sem problemas. about_this: Sobre essa instância - apps: Aplicações business_email: 'Email comercial:' closed_registrations: Registros estão fechadas para essa instância. contact: Contato description_headline: O que é %{domain}? domain_count_after: outras instâncias domain_count_before: Conectado a - features: - api: Aberto para API de aplicações e serviços - blocks: Bloqueos e ferramentas para mudar - characters: 500 caracteres por post - chronology: Timeline são cronologicas - ethics: 'Design ético: sem propaganda, sem tracking' - gifv: GIFV e vídeos curtos - privacy: Granular, privacidade setada por post - public: Timelines públicas - features_headline: O que torna Mastodon diferente get_started: Comece aqui links: Links other_instances: Outras instâncias @@ -152,17 +141,13 @@ pt-BR: settings: contact_information: email: Entre um endereço de email público - label: Informação de contato username: Entre com usuário registrations: closed_message: desc_html: Mostrar na página inicial quando registros estão fecados<br/>Você pode usar tags HTML title: Mensagem de registro fechados open: - disabled: Desabilitado - enabled: Habilitado title: Aberto para registro - setting: Preferências site_description: desc_html: Mostrar como parágrafo e usado como meta tag.<br/>Vôce pode usar tags HTML, em particular <code><a></code> e <code><em></code>. title: Descrição do site diff --git a/config/locales/pt.yml b/config/locales/pt.yml index dff2898c0..40be8a6c5 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -3,23 +3,12 @@ pt: about: about_mastodon: Mastodon é uma rede social <em>grátis e em código aberto</em>. Uma alternativa <em>descentralizada</em> às plataformas comerciais, que evita o risco de uma única empresa monopolizar a tua comunicação. Escolhe um servidor que confies, não importa qual, pois vais poder comunicar com todos os outros. Qualquer um pode criar uma instância Mastodon e participar nesta <em>rede social</em>. about_this: Sobre esta instância - apps: Aplicações business_email: 'Email comercial:' closed_registrations: Novos registos estão fechados nesta instância. contact: Contacto description_headline: O que é o %{domain}? domain_count_after: outras instâncias domain_count_before: Ligado a - features: - api: API aberta para aplicações e serviços - blocks: Ferramentas para silenciar e bloquear - characters: 500 caracteres por post - chronology: Timelines cronológicas - ethics: 'Design ético: sem públicidade ou tracking' - gifv: GIFV e pequenos vídeos - privacy: Privacidade granular por post - public: Timelines públicas - features_headline: O que torna Mastodon diferente get_started: Começar links: Links other_instances: Outras instâncias @@ -147,17 +136,13 @@ pt: settings: contact_information: email: Inserir um endereço de email para tornar público - label: Informação de contacto username: Insira um nome de utilizador registrations: closed_message: desc_html: Mostrar na página inicial quando registos estão encerrados<br/>Podes usar tags HTML title: Mensagem de registos encerrados open: - disabled: Desabilitado - enabled: Habilitado title: Aceitar novos registos - setting: Preferências site_description: desc_html: Mostrar como parágrafo na página inicial e usado como meta tag.<br/>Podes usar tags HTML, em particular <code><a></code> e <code><em></code>. title: Descrição do site diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 9cf067d88..5cfc2b1ca 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -3,23 +3,12 @@ ru: about: about_mastodon: Mastodon - это <em>свободная</em> социальная сеть с <em>открытым исходным кодом</em>. Как <em>децентрализованная</em> альтернатива коммерческим платформам, Mastodon предотвращает риск монополизации Вашего общения одной компанией. Выберите сервер, которому Вы доверяете — что бы Вы ни выбрали, Вы сможете общаться со всеми остальными. Любой может запустить свой собственный узел Mastodon и участвовать в <em>социальной сети</em> совершенно бесшовно. about_this: Об этом узле - apps: Приложения business_email: 'Деловой e-mail:' closed_registrations: В данный момент регистрация на этом узле закрыта. contact: Связаться description_headline: Что такое %{domain}? domain_count_after: другими узлами domain_count_before: Связан с - features: - api: Открытый API для приложений и сервисов - blocks: Продвинутые инструменты блокирования и глушения - characters: 500 символов на пост - chronology: Хронологические ленты - ethics: 'Этичный дизайн: нет рекламы, нет слежения' - gifv: GIFV и короткие видео - privacy: Тонкие настройки приватности для каждого поста - public: Публичные ленты - features_headline: Что выделяет Mastodon get_started: Начать links: Ссылки other_instances: Другие узлы @@ -134,17 +123,13 @@ ru: settings: contact_information: email: Введите публичный e-mail - label: Контактная информация username: Введите имя пользователя registrations: closed_message: desc_html: Отображается на титульной странице, когда закрыта регистрация<br>Можно использовать HTML-теги title: Сообщение о закрытой регистрации open: - disabled: Закрыта - enabled: Открыта title: Открыть регистрацию - setting: Настройка site_description: desc_html: Отображается в качестве параграфа на титульной странице и используется в качестве мета-тега.<br>Можно использовать HTML-теги, в особенности <code><a></code> и <code><em></code>. title: Описание сайта @@ -292,6 +277,43 @@ ru: missing_resource: Поиск требуемого перенаправления URL для Вашего аккаунта завершился неудачей proceed: Продолжить подписку prompt: 'Вы хотите подписаться на:' + sessions: + activity: Последняя активность + browser: Браузер + browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + firefox: Firefox + generic: Неизвестный браузер + ie: Internet Explorer + micro_messenger: MicroMessenger + nokia: Nokia S40 Ovi Browser + opera: Opera + phantom_js: PhantomJS + qq: QQ Browser + safari: Safari + uc_browser: UCBrowser + weibo: Weibo + current_session: Текущая сессия + description: "%{browser} на %{platform}" + explanation: Это веб-браузеры, в которых на данный момент выполнен вход в Ваш аккаунт 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: неизвестной платформе + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone + title: Сессии settings: authorized_apps: Авторизованные приложения back: Назад в Mastodon diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 3e769fb96..fc5ab5ec8 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -37,7 +37,9 @@ en: setting_auto_play_gif: Auto-play animated GIFs setting_boost_modal: Show confirmation dialog before boosting setting_default_privacy: Post privacy + setting_default_sensitive: Always mark media as sensitive setting_delete_modal: Show confirmation dialog before deleting a toot + setting_system_font_ui: Use system's default font severity: Severity type: Import type username: Username diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml index ae4975143..8717a4abd 100644 --- a/config/locales/simple_form.fr.yml +++ b/config/locales/simple_form.fr.yml @@ -4,12 +4,20 @@ fr: hints: defaults: avatar: Au format PNG, GIF ou JPG. 2Mo maximum. Sera réduit à 120x120px - display_name: 30 caractères maximum + display_name: + one: <span class="name-counter">1</span> caractère restant + other: <span class="name-counter">%{count}</span> caractères restants header: Au format PNG, GIF ou JPG. 2Mo maximum. Sera réduit à 700x335px - locked: Vous devrez approuver chaque abonné⋅e et vos statuts ne s'afficheront qu'à vos abonné⋅es - note: 160 caractères maximum + locked: Vous devrez approuver chaque abonné⋅e et vos statuts ne s’afficheront qu’à vos abonné⋅es + note: + one: <span class="note-counter">1</span> caractère restant + other: <span class="note-counter">%{count}</span> caractères restants 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. + user: + filtered_languages: Les langues sélectionnées seront retirées de vos fils publics. labels: defaults: avatar: Image de profil @@ -21,15 +29,18 @@ fr: email: Adresse courriel header: Image d’en-tête locale: Langue - locked: Rendre le compte privé + locked: Verrouiller le compte new_password: Nouveau mot de passe note: Présentation - otp_attempt: Code d'identification à deux facteurs + otp_attempt: Code d’identification à deux facteurs password: Mot de passe + setting_auto_play_gif: Lire automatiquement les GIFs animés setting_boost_modal: Afficher un dialogue de confirmation avant de partager setting_default_privacy: Confidentialité des statuts + setting_delete_modal: Afficher un dialogue de confirmation avant de supprimer un pouet + setting_system_font_ui: Utiliser la police par défaut du système severity: Séverité - type: Type d'import + type: Type d’import username: Identifiant interactions: must_be_follower: Masquer les notifications des personnes qui ne vous suivent pas @@ -38,7 +49,7 @@ fr: digest: Envoyer des courriels récapitulatifs favourite: Envoyer un courriel lorsque quelqu’un ajoute mes statuts à ses favoris follow: Envoyer un courriel lorsque quelqu’un me suit - follow_request: Envoyer un courriel lorsque quelqu'un demande à me suivre + follow_request: Envoyer un courriel lorsque quelqu’un demande à me suivre mention: Envoyer un courriel lorsque quelqu’un me mentionne reblog: Envoyer un courriel lorsque quelqu’un partage mes statuts 'no': Non diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index b9f11d7b3..9342398a8 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -34,7 +34,9 @@ ja: setting_auto_play_gif: アニメーションGIFを自動再生する setting_boost_modal: ブーストする前に確認ダイアログを表示する setting_default_privacy: 投稿の公開範囲 + setting_default_sensitive: メディアを常に閲覧注意としてマークする setting_delete_modal: トゥートを削除する前に確認ダイアログを表示する + setting_system_font_ui: システムのデフォルトフォントを使う severity: 重大性 type: インポートする項目 username: ユーザー名 diff --git a/config/locales/simple_form.ko.yml b/config/locales/simple_form.ko.yml new file mode 100644 index 000000000..b7dbc8bef --- /dev/null +++ b/config/locales/simple_form.ko.yml @@ -0,0 +1,58 @@ +--- +ko: + simple_form: + hints: + defaults: + avatar: PNG, GIF 혹은 JPG. 최대 2MB. 120x120px로 다운스케일 됨 + display_name: + one: <span class="name-counter">1</span> 글자 남음 + other: <span class="name-counter">%{count}</span> 글자 남음 + header: PNG, GIF 혹은 JPG. 최대 2MB. 700x335px로 다운스케일 됨 + locked: 수동으로 팔로워를 승인하고, 기본 Toot 프라이버시 설정을 팔로워 전용으로 변경 + note: + one: <span class="note-counter">1</span> 글자 남음 + other: <span class="note-counter">%{count}</span> 글자 남음 + imports: + data: 다른 마스토돈 인스턴스에서 추출된 CSV 파일 + sessions: + otp: 2단계 인증 코드를 휴대전화를 보고 입력하거나, 복구 코드 중 하나를 사용 + user: + filtered_languages: 선택된 언어가 공개 타임라인에서 제외 될 것입니다. + labels: + defaults: + avatar: 아바타 + confirm_new_password: 새로운 비밀번호 다시 입력 + confirm_password: 현재 비밀번호 다시 입력 + current_password: 현재 비밀번호 입력 + data: 데이터 + display_name: 표시되는 이름 + email: 이메일 주소 + header: 헤더 + locale: 언어 + locked: 계정 잠금 + new_password: 새로운 비밀번호 입력 + note: 자기소개 + otp_attempt: 2단계 인증 코드 + password: 비밀번호 + setting_auto_play_gif: 애니메이션 GIF를 자동 재생 + setting_boost_modal: 부스트 전 확인 창을 보여주기 + setting_default_privacy: Toot 프라이버시 + setting_delete_modal: Toot 삭제 전 확인 창을 보여주기 + severity: 심각도 + type: 불러오기 종류 + username: 유저 이름 + interactions: + must_be_follower: 나를 팔로우 하지 않는 사람에게서 온 알림을 차단 + must_be_following: 내가 팔로우 하지 않는 사람에게서 온 알림을 차단 + notification_emails: + digest: 요약 이메일 보내기 + favourite: 누군가 내 상태를 즐겨찾기로 등록했을 때 이메일 보내기 + follow: 누군가 나를 팔로우 했을 때 이메일 보내기 + follow_request: 누군가 나를 팔로우 하길 원할 때 이메일 보내기 + mention: 누군가 나에게 답장했을 때 이메일 보내기 + reblog: 누군가 내 Toot을 부스트 했을 때 이메일 보내기 + 'no': '아니오' + required: + mark: "*" + text: 필수 항목 + 'yes': '네' diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml index 09e76eba0..e4c4d7c8c 100644 --- a/config/locales/simple_form.pl.yml +++ b/config/locales/simple_form.pl.yml @@ -42,7 +42,9 @@ pl: setting_auto_play_gif: Automatycznie odtwarzaj animowane GIFy setting_boost_modal: Pytaj o potwierdzenie przed podbiciem setting_default_privacy: Widoczność posta + setting_default_sensitive: Zawsze oznaczaj zawartość multimedialną jako wrażliwą setting_delete_modal: Pytaj o potwierdzenie przed usunięciem postu + setting_system_font_ui: Używaj domyślnej czcionki systemu severity: Priorytet type: Typ importu username: Nazwa użytkownika diff --git a/config/locales/th.yml b/config/locales/th.yml index 322e5e74b..263babdd0 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -3,23 +3,12 @@ th: about: about_mastodon: แมสโทดอน เป็น <em>ดีเซ็นทรัลไลซ์</em><em>ฟรีโอเพ่นซอร์ส</em> โซเชี่ยวเน็ตเวริ์ค. เป็นทางเลือกทดแทนโซเชี่ยวเน็ตเวิร์คที่ทำเป็นธุรกิจการค้า, ป้องกันการผูกขาดช่องทางการสื่อสารของคุณ. เลือกเซร์ฟเวอร์ที่คุณไว้ใจ — ที่คุณเลือกได้เอง, สื่อสารกับคนที่คุณต้องการได้เสมอ. ใครๆก็รันแมสโทดอนอินซะแตนซ์ได้ และ เชื่อมต่อกับ<em>โซเชี่ยวเน็ตเวิร์ค</em> โดยไม่มีอะไรมาขวางกั้น. about_this: เกี่ยวกับอินซะแตนซ์นี้ - apps: แอ๊ฟ business_email: 'อีเมล์ธุรกิจ:' closed_registrations: อินซะแตนซ์นี้ปิดรับลงทะเบียนแล้ว. contact: ติดต่อ description_headline: โดเมนคือ %{domain} ? domain_count_after: อินซะแตนซ์อื่นๆ domain_count_before: เชื่อมต่อกับ - features: - api: API เปิดสำหรับ Apps และ Services - blocks: มีเครื่องมือสำหรับ Block และ Mute - characters: เขียนได้ 500 ตัวอักษรต่อโพสต์ - chronology: Timelines are chronological - ethics: 'ออกแบบด้วยจรรยาบรรณ: ไม่มีโฆษณา, ไม่มีการแทรค' - gifv: รองรับภาพ GIFV และ วีดีโอสั้น - privacy: Granular, per-post privacy settings - public: ไทม์ไลน์สาธารณะ - features_headline: What sets Mastodon apart get_started: เริ่มกันเลย links: ลิงก์ other_instances: อินซะแตนซ์อื่นๆ @@ -153,17 +142,13 @@ th: settings: contact_information: email: กรอกที่อยู่อีเมล์สาธารณะ - label: ข้อมูลที่ติดต่อ username: กรอกชื่อผู้ใช้ registrations: closed_message: desc_html: Displayed on frontpage when registrations are closed<br> ใช้ HTML tags ได้ title: ปิดข้อความลงทะเบียน open: - disabled: ปิดการใช้งาน - enabled: ปิดใช้งาน title: เปิดรับลงทะเบียน - setting: ตั้งค่า site_description: desc_html: Displayed as a paragraph on the frontpage and used as a meta tag.<br> ใช้ HTML tags ได้, in particular <code><a></code> และ <code><em></code>. title: คำอธิบายไซต์ diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 0e33e2efe..e7864cc57 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -3,23 +3,12 @@ tr: about: about_mastodon: Mastodon <em>ücretsiz ve açık kaynaklı</em> bir sosyal ağdır. <em>Merkezileştirilmemiş</em> yapısı sayesinde diğer ticari sosyal platformların aksine iletişimininizin tek bir firmada tutulmasının/yönetilmesinin önüne geçer. Güvendiğiniz bir sunucuyu seçerek oradaki kişilerle etkileşimde bulunabilirsiniz. Herkes kendi Mastodon sunucusunu kurabilir ve sorunsuz bir şekilde Mastodon <em>sosyal ağına</em> dahil edebilir. about_this: Bu sunucu hakkında - apps: Uygulamalar business_email: 'İş e-postası:' closed_registrations: Bu sunucu şu anda yeni kayıt almamaktadır. contact: İletişim description_headline: Peki %{domain} nedir? domain_count_after: sunucu var. domain_count_before: Bağlı olduğu - features: - api: Uygulama ve servisler için açık API - blocks: Zengin blok ve iletişim araçları - characters: 500 karakterlik gönderiler - chronology: Kronolojik zaman tüneli - ethics: 'Etik tasarım: reklam ve izleme yok' - gifv: GIFV ve diğer video türleri - privacy: Gönderi bazlı gizlilik - public: Herkese açık zaman tünelleri - features_headline: Mastodon'ı diğerlerinden ayıran nedir? get_started: Kayıt ol links: Bağlantılar other_instances: Diğer sunucular @@ -152,17 +141,13 @@ tr: settings: contact_information: email: Herkese açık e-posta adresiniz - label: İletişim bilgisi username: Bir kullanıcı adı giriniz registrations: closed_message: desc_html: Kayıt alımları kapatıldığında ana sayfada görüntülenecek mesajdır. <br> HTML etiketleri kullanabilirsiniz. title: Kayıt alımları kapatılma mesajı open: - disabled: Kapalı - enabled: Açık title: Kayıt alımları - setting: Ayar adı site_description: desc_html: Ana sayfada paragraf olarak görüntülenecek bilgidir.<br>Özellikle <code><a></code> ve <code><em></code> olmak suretiyle HTML etiketlerini kullanabilirsiniz. title: Site açıklaması diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 1327c1a7b..129fc5bb7 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -3,23 +3,12 @@ uk: about: about_mastodon: Mastodon - це <em>вільна</em> соціальна мережа з <em>відкритим вихідним кодом</em>. Вона є <em>децентралізованою</em> альтернативою комерційним платформам, що дозволяє уникнути ризиків монополізації вашого спілкування однією компанією. Виберіть сервер, якому ви довіряєте — що б ви не вибрали, Ви зможете спілкуватись з усіма іншими. Будь-який користувач може запустити власну інстанцію Mastodon та без проблем брати участь в <em>соціальній мережі</em>. about_this: Про цю інстанцію - apps: Додатки business_email: 'Діловий email:' closed_registrations: На даний момент реєстрація на цій інстанції закрита. contact: Зв'язатися description_headline: Що таке %{domain}? domain_count_after: іншими інстанціями domain_count_before: Зв'язаний з - features: - api: Відкритий API для додаків та сервісів - blocks: Продвинуті інструменти самомодерації - characters: 500 символів на пост - chronology: Хронологічні стрічки - ethics: 'Этичний дизайн: немає реклами, немає стеження' - gifv: GIFV та короткі відео - privacy: Тонкі налаштування приватності для кожного поста - public: Публічні стрічки - features_headline: Що виділяє Mastodon get_started: Почати links: Посилання other_instances: Інші інстанції @@ -134,17 +123,13 @@ uk: settings: contact_information: email: Введіть публічний email - label: Контактна інформація username: Введіть ім'я користувача registrations: closed_message: desc_html: Відображається на титульній сторінці, коли реєстрація закрита <br>Можна використовувати HTML-теги title: Повідомлення про закриту реєстрацію open: - disabled: Закрита - enabled: Відкрита title: Відкрити реєстрацію - setting: Налаштування site_description: desc_html: Відображається у якості параграфа на титульній сторінці та використовується у якості мета-тега.<br>Можна використовувати HTML-теги, особливо <code><a></code> і <code><em></code>. title: Опис сайту diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 6c8e9fc6d..650d4bd15 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -3,23 +3,12 @@ zh-CN: about: about_mastodon: Mastodon(长毛象)是一个<em>自由、开放源码</em>的社交网站。它是一个分布式的服务,避免你的通信被单一商业机构垄断操控。请你选择一家你信任的 Mastodon 实例,在上面创建帐号,然后你就可以和任一 Mastodon 实例上的用户互通,享受无缝的<em>社交</em>交流。 about_this: 关于本实例 - apps: 应用程序 business_email: 商业电邮︰ closed_registrations: 这个实例目前不开放注册 _(:3」∠)_ contact: 联络 description_headline: 关于 %{domain} domain_count_after: 个其它实例 domain_count_before: 现已接入 - features: - api: 开放 API,供各式应用程序及服务接入 - blocks: 完善的封锁用户、静音功能 - characters: 每篇嘟文最多 500 字 - chronology: 纯粹按时间排序,不作多余处理 - ethics: 良心设计︰没有广告,不追踪你的使用行为 - gifv: 支持显示 GIFV 动图小视频 - privacy: 可逐篇嘟文设置隐私 - public: 提供公共时间轴 - features_headline: 是什么让 Mastodon 与众不同 get_started: 上手使用 links: 链接 other_instances: 其它实例 @@ -159,17 +148,13 @@ zh-CN: settings: contact_information: email: 输入一个公开的电邮地址 - label: 联系数据 username: 输入用户名称 registrations: closed_message: desc_html: 当本站暂停接受注册时,会显示这个消息。<br/> 可使用 HTML title: 暂停注册消息 open: - disabled: 停用 - enabled: 启用 title: 开放注册 - setting: 设置 site_description: desc_html: 在首页显示,及在 meta 标签中用作网站介绍。<br>你可以在此使用 HTML 标签,尤其是<code><a></code> 和 <code><em></code>。 title: 本站介绍 diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml index 4d8262c5b..d2db78be1 100644 --- a/config/locales/zh-HK.yml +++ b/config/locales/zh-HK.yml @@ -3,23 +3,12 @@ zh-HK: about: about_mastodon: Mastodon(萬象)是<em>自由、開源</em>的社交網絡。服務站<em>各自獨立而互連</em>,避免單一商業機構壟斷。找你所信任的服務站,建立帳號,你即可與任何服務站上的用戶溝通,享受無縫的<em>網絡交流</em>。 about_this: 關於本服務站 - apps: 應用程式 business_email: 聯絡網站管理者︰ closed_registrations: 本服務站暫時停止接受登記。 contact: 聯絡 description_headline: 關於 %{domain} domain_count_after: 個其他服務站 domain_count_before: 已連接至 - features: - api: 開放 API,供各式應用程式及服務連入 - blocks: 完善的封鎖用戶、靜音功能 - characters: 每篇文章最多 500 字 - chronology: 時間軸忠實按時排序,不多餘處理 - ethics: 良心設計︰無廣告,不追蹤用戶 - gifv: 支援顯示 GIFV 短片圖組 - privacy: 可逐篇文章設定私隱度 - public: 公共時間軸 - features_headline: 甚麼讓 Mastodon 與眾不同 get_started: 立即登記 links: 連結 other_instances: 其他服務站 @@ -152,17 +141,13 @@ zh-HK: settings: contact_information: email: 輸入一個公開的電郵地址 - label: 聯絡資料 username: 輸入用戶名稱 registrations: closed_message: desc_html: 當本站暫停接受註冊時,會顯示這個訊息。<br/> 可使用 HTML title: 暫停註冊訊息 open: - disabled: 停用 - enabled: 啟用 title: 開放註冊 - setting: 設定 site_description: desc_html: 在首頁顯示,及在 meta 標籤使用作網站介紹。<br/> 你可以在此使用 <code><a></code> 和 <code><em></code> 等 HTML 標籤。 title: 本站介紹 diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 754175a7a..67aa2830f 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -3,23 +3,12 @@ zh-TW: about: about_mastodon: Mastodon (長毛象)是一個<em>自由、開放原始碼</em>的社群網站。它是一個分散式的服務,避免您的通訊被單一商業機構壟斷操控。請您選擇一家您信任的 Mastodon 服務站,在上面建立帳號,然後您就可以和任一 Mastodon 服務站上的使用者互通,享受無縫的<em>社群網路</em>交流。 about_this: 關於本服務站 - apps: 應用程式 business_email: 商務信箱︰ closed_registrations: 本服務站暫時停止接受註冊。 contact: 聯絡我們 description_headline: 關於 %{domain} domain_count_after: 個服務站相連 domain_count_before: 與其他 - features: - api: 開放 API,供各式應用程式及服務串接 - blocks: 完善的封鎖使用者、靜音功能 - characters: 每篇文章最多 500 字 - chronology: 時間軸按時序顯示文章,不作多餘處理 - ethics: 良心設計︰沒有廣告,不追蹤您的使用行為 - gifv: 支援顯示 GIFV 短片 - privacy: 可逐篇文章調整隱私設定 - public: 公開時間軸 - features_headline: Mastodon 與眾不同之處 get_started: 立即註冊 links: 連結 other_instances: 其他服務站 @@ -113,17 +102,13 @@ zh-TW: settings: contact_information: email: 請輸入輸入一個公開電子信箱 - label: 聯絡資訊 username: 請輸入使用者名稱 registrations: closed_message: desc_html: 關閉註冊時顯示在首頁的內容,可使用 HTML 標籤。 title: 關閉註冊訊息 open: - disabled: 停用 - enabled: 啟用 title: 開放註冊 - setting: 設定 site_description: desc_html: 顯示在首頁並且作為 meta 標籤的短文。<br>可使用 HTML 標籤,包括 <code><a></code> 及 <code><em></code>。 title: 網站描述 diff --git a/config/settings.yml b/config/settings.yml index 19d2ca7be..32776515c 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -10,14 +10,17 @@ defaults: &defaults site_title: 'dev.glitch.social' site_description: '' site_extended_description: '' + site_terms: '' site_contact_username: '' site_contact_email: '' open_registrations: true closed_registrations_message: '' open_deletion: true + timeline_preview: true boost_modal: false - auto_play_gif: true + auto_play_gif: false delete_modal: true + system_font_ui: false notification_emails: follow: false reblog: false diff --git a/config/webpack/production.js b/config/webpack/production.js index 0d2c9acfb..303fca81b 100644 --- a/config/webpack/production.js +++ b/config/webpack/production.js @@ -12,7 +12,6 @@ module.exports = merge(sharedConfig, { stats: 'normal', plugins: [ - new webpack.optimize.ModuleConcatenationPlugin(), new webpack.optimize.UglifyJsPlugin({ sourceMap: true, mangle: true, diff --git a/config/webpack/translationRunner.js b/config/webpack/translationRunner.js index 097099b48..d616c7839 100644 --- a/config/webpack/translationRunner.js +++ b/config/webpack/translationRunner.js @@ -2,7 +2,7 @@ const fs = require('fs'); const path = require('path'); const { default: manageTranslations } = require('react-intl-translations-manager'); -const RFC5646_REGEXP = /^[a-z]{2,3}(?:|[A-Z]+)$/; +const RFC5646_REGEXP = /^[a-z]{2,3}(?:|-[A-Z]+)$/; const rootDirectory = path.resolve(__dirname, '..', '..'); const translationsDirectory = path.resolve(rootDirectory, 'app', 'javascript', 'mastodon', 'locales'); diff --git a/config/webpacker.yml b/config/webpacker.yml index c1cd6e93b..aa429a1dd 100644 --- a/config/webpacker.yml +++ b/config/webpacker.yml @@ -19,7 +19,7 @@ development: <<: *default dev_server: - host: 0.0.0.0 + host: 127.0.0.1 port: 8080 https: false diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 44f3e4390..3c92ce417 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 6 + 7 end def pre diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index 0e182c755..b2b352858 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -74,7 +74,7 @@ namespace :mastodon do end namespace :media do - desc 'Removes media attachments that have not been assigned to any status for longer than a day' + desc 'Removes media attachments that have not been assigned to any status for longer than a day (deprecated)' task clear: :environment do # No-op # This task is now executed via sidekiq-scheduler @@ -100,6 +100,18 @@ namespace :mastodon do MediaAttachment.where(file_file_name: nil).where.not(type: :unknown).in_batches.update_all(type: :unknown) Rails.logger.debug 'Done!' end + + desc 'Redownload avatars/headers of remote users. Optionally limit to a particular domain with DOMAIN' + task redownload_avatars: :environment do + accounts = Account.remote + accounts = accounts.where(domain: ENV['DOMAIN']) if ENV['DOMAIN'].present? + + accounts.find_each do |account| + account.reset_avatar! + account.reset_header! + account.save + end + end end namespace :push do @@ -111,7 +123,7 @@ namespace :mastodon do end end - desc 'Re-subscribes to soon expiring PuSH subscriptions' + desc 'Re-subscribes to soon expiring PuSH subscriptions (deprecated)' task refresh: :environment do # No-op # This task is now executed via sidekiq-scheduler @@ -119,13 +131,13 @@ namespace :mastodon do end namespace :feeds do - desc 'Clear timelines of inactive users' + desc 'Clear timelines of inactive users (deprecated)' task clear: :environment do # No-op # This task is now executed via sidekiq-scheduler end - desc 'Clears all timelines' + desc 'Clear all timelines without regenerating them' task clear_all: :environment do Redis.current.keys('feed:*').each { |key| Redis.current.del(key) } end @@ -151,7 +163,7 @@ namespace :mastodon do end end - desc 'List all admin users' + desc 'List e-mails of all admin users' task admins: :environment do puts 'Admin user emails:' puts User.admins.map(&:email).join("\n") @@ -161,16 +173,12 @@ namespace :mastodon do namespace :settings do desc 'Open registrations on this instance' task open_registrations: :environment do - setting = Setting.where(var: 'open_registrations').first - setting.value = true - setting.save + Setting.open_registrations = true end desc 'Close registrations on this instance' task close_registrations: :environment do - setting = Setting.where(var: 'open_registrations').first - setting.value = false - setting.save + Setting.open_registrations = false end end diff --git a/nanobox/nginx-local.conf b/nanobox/nginx-local.conf index 023328733..f56339cac 100644 --- a/nanobox/nginx-local.conf +++ b/nanobox/nginx-local.conf @@ -27,8 +27,8 @@ http { } map $http_upgrade $connection_upgrade { - default upgrade; - '' close; + default upgrade; + '' close; } # Configuration for Nginx @@ -38,6 +38,8 @@ http { root /app/public; + client_max_body_size 8M; + location / { try_files $uri @rails; } diff --git a/nanobox/nginx-stream.conf.erb b/nanobox/nginx-stream.conf.erb index b39d3ff1d..2a047dd9f 100644 --- a/nanobox/nginx-stream.conf.erb +++ b/nanobox/nginx-stream.conf.erb @@ -22,8 +22,8 @@ http { } map $http_upgrade $connection_upgrade { - default upgrade; - '' close; + default upgrade; + '' close; } # Configuration for Nginx diff --git a/nanobox/nginx-web.conf.erb b/nanobox/nginx-web.conf.erb index 55245bf28..24cd17cff 100644 --- a/nanobox/nginx-web.conf.erb +++ b/nanobox/nginx-web.conf.erb @@ -22,8 +22,8 @@ http { } map $http_upgrade $connection_upgrade { - default upgrade; - '' close; + default upgrade; + '' close; } # Configuration for Nginx @@ -36,6 +36,8 @@ http { root /app/public; + client_max_body_size 8M; + location / { try_files $uri @rails; } diff --git a/package.json b/package.json index 60f9af1e9..4c5a3f1d9 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "array-includes": "^3.0.3", "autoprefixer": "^7.1.0", "axios": "^0.16.2", - "babel-cli": "^6.24.1", "babel-core": "^6.25.0", "babel-loader": "^7.1.0", "babel-plugin-lodash": "^3.2.11", @@ -33,6 +32,7 @@ "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", "babel-plugin-transform-object-rest-spread": "^6.23.0", + "babel-plugin-transform-react-inline-elements": "^6.22.0", "babel-plugin-transform-react-jsx-self": "^6.22.0", "babel-plugin-transform-react-jsx-source": "^6.22.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.6", @@ -71,7 +71,7 @@ "pg": "^6.4.0", "postcss-loader": "^2.0.6", "postcss-smart-import": "^0.7.4", - "precss": "^1.4.0", + "precss": "^2.0.0", "prop-types": "^15.5.10", "punycode": "^2.1.0", "rails-ujs": "^5.1.2", @@ -89,7 +89,7 @@ "react-router-dom": "^4.1.1", "react-router-scroll": "ytase/react-router-scroll#build", "react-simple-dropdown": "^3.0.0", - "react-swipeable": "^4.0.1", + "react-swipeable-views": "^0.12.3", "react-textarea-autosize": "^5.0.7", "react-toggle": "^4.0.1", "redis": "^2.7.1", @@ -103,10 +103,11 @@ "sass-loader": "^6.0.6", "stringz": "^0.2.2", "style-loader": "^0.18.2", + "substring-trie": "^1.0.0", "throng": "^4.0.0", "tiny-queue": "^0.2.1", "uuid": "^3.1.0", - "uws": "^0.14.5", + "uws": "^8.14.0", "webpack": "^3.0.0", "webpack-bundle-analyzer": "^2.8.2", "webpack-manifest-plugin": "^1.1.0", @@ -118,17 +119,17 @@ "@storybook/react": "^3.1.6", "babel-eslint": "^7.2.3", "chai": "^4.0.1", - "chai-enzyme": "^0.7.1", + "chai-enzyme": "^0.8.0", "enzyme": "^2.9.1", "eslint": "^3.19.0", "eslint-plugin-jsx-a11y": "^4.0.0", "eslint-plugin-react": "^6.10.3", - "jsdom": "^10.1.0", + "jsdom": "^11.0.0", "mocha": "^3.4.1", "react-intl-translations-manager": "^5.0.0", "react-test-renderer": "^15.6.1", "sinon": "^2.3.5", - "webpack-dev-server": "lencioni/webpack-dev-server#patch-1", + "webpack-dev-server": "^2.5.1", "yargs": "^8.0.2" }, "optionalDependencies": { diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb index 305260475..8be27d866 100644 --- a/spec/controllers/admin/accounts_controller_spec.rb +++ b/spec/controllers/admin/accounts_controller_spec.rb @@ -3,11 +3,64 @@ require 'rails_helper' RSpec.describe Admin::AccountsController, type: :controller do render_views + let(:user) { Fabricate(:user, admin: true) } + before do - sign_in Fabricate(:user, admin: true), scope: :user + sign_in user, scope: :user end describe 'GET #index' do + around do |example| + default_per_page = Account.default_per_page + Account.paginates_per 1 + example.run + Account.paginates_per default_per_page + end + + it 'filters with parameters' do + new = AccountFilter.method(:new) + + expect(AccountFilter).to receive(:new) do |params| + h = params.to_h + + expect(h[:local]).to eq '1' + expect(h[:remote]).to eq '1' + expect(h[:by_domain]).to eq 'domain' + expect(h[:silenced]).to eq '1' + expect(h[:recent]).to eq '1' + expect(h[:suspended]).to eq '1' + expect(h[:username]).to eq 'username' + expect(h[:display_name]).to eq 'display name' + expect(h[:email]).to eq 'local-part@domain' + expect(h[:ip]).to eq '0.0.0.42' + + new.call({}) + end + + get :index, params: { + local: '1', + remote: '1', + by_domain: 'domain', + silenced: '1', + recent: '1', + suspended: '1', + username: 'username', + display_name: 'display name', + email: 'local-part@domain', + ip: '0.0.0.42' + } + end + + it 'paginates accounts' do + Fabricate(:account) + + get :index, params: { page: 2 } + + accounts = assigns(:accounts) + expect(accounts.count).to eq 1 + expect(accounts.klass).to be Account + end + it 'returns http success' do get :index expect(response).to have_http_status(:success) diff --git a/spec/controllers/admin/settings_controller_spec.rb b/spec/controllers/admin/settings_controller_spec.rb index d9dde3c92..609bc762b 100644 --- a/spec/controllers/admin/settings_controller_spec.rb +++ b/spec/controllers/admin/settings_controller_spec.rb @@ -31,7 +31,7 @@ RSpec.describe Admin::SettingsController, type: :controller do it 'cannot create a setting value for a non-admin key' do expect(Setting.new_setting_key).to be_blank - patch :update, params: { new_setting_key: 'New key value' } + patch :update, params: { form_admin_settings: { new_setting_key: 'New key value' } } expect(response).to redirect_to(edit_admin_settings_path) expect(Setting.new_setting_key).to be_nil @@ -40,7 +40,7 @@ RSpec.describe Admin::SettingsController, type: :controller do it 'creates a settings value that didnt exist before for eligible key' do expect(Setting.site_extended_description).to be_blank - patch :update, params: { site_extended_description: 'New key value' } + patch :update, params: { form_admin_settings: { site_extended_description: 'New key value' } } expect(response).to redirect_to(edit_admin_settings_path) expect(Setting.site_extended_description).to eq 'New key value' @@ -56,7 +56,7 @@ RSpec.describe Admin::SettingsController, type: :controller do it 'updates a settings value' do Setting.site_title = 'Original' - patch :update, params: { site_title: 'New title' } + patch :update, params: { form_admin_settings: { site_title: 'New title' } } expect(response).to redirect_to(edit_admin_settings_path) expect(Setting.site_title).to eq 'New title' @@ -72,7 +72,7 @@ RSpec.describe Admin::SettingsController, type: :controller do it 'typecasts open_registrations to boolean' do Setting.open_registrations = false - patch :update, params: { open_registrations: 'true' } + patch :update, params: { form_admin_settings: { open_registrations: '1' } } expect(response).to redirect_to(edit_admin_settings_path) expect(Setting.open_registrations).to eq true diff --git a/spec/controllers/authorize_follows_controller_spec.rb b/spec/controllers/authorize_follows_controller_spec.rb index b801aa661..26e46a23c 100644 --- a/spec/controllers/authorize_follows_controller_spec.rb +++ b/spec/controllers/authorize_follows_controller_spec.rb @@ -94,7 +94,7 @@ describe AuthorizeFollowsController do end it 'follows account when found' do - target_account = double(id: '123') + target_account = Fabricate(:account) result_account = double(target_account: target_account) service = double allow(FollowService).to receive(:new).and_return(service) @@ -103,7 +103,7 @@ describe AuthorizeFollowsController do post :create, params: { acct: 'acct:user@hostname' } expect(service).to have_received(:call).with(account, 'user@hostname') - expect(response).to redirect_to(web_url('accounts/123')) + expect(response).to render_template(:success) end end end diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb index cc1dbe5a1..d44d720b1 100644 --- a/spec/controllers/home_controller_spec.rb +++ b/spec/controllers/home_controller_spec.rb @@ -23,41 +23,14 @@ RSpec.describe HomeController, type: :controller do expect(assigns(:body_classes)).to eq 'app-body' end - it 'assigns @token' do - app = Doorkeeper::Application.create!(name: 'Web', superapp: true, redirect_uri: Doorkeeper.configuration.native_redirect_uri) - allow(Doorkeeper.configuration).to receive(:access_token_expires_in).and_return(42) - - subject - token = Doorkeeper::AccessToken.find_by(token: assigns(:token)) - - expect(token.application).to eq app - expect(token.resource_owner_id).to eq user.id - expect(token.scopes).to eq Doorkeeper::OAuth::Scopes.from_string('read write follow') - expect(token.expires_in_seconds).to eq 42 - expect(token.use_refresh_token?).to eq false - end - - it 'assigns @web_settings for {} if not available' do - subject - expect(assigns(:web_settings)).to eq({}) - end - - it 'assigns @web_settings for Web::Setting if available' do - setting = Fabricate('Web::Setting', data: '{"home":{}}', user: user) - subject - expect(assigns(:web_settings)).to eq setting.data - end - - it 'assigns @admin' do - admin = Fabricate(:account) - Setting.site_contact_username = admin.username - subject - expect(assigns(:admin)).to eq admin - end - - it 'assigns streaming_api_base_url' do + it 'assigns @initial_state_json' do subject - expect(assigns(:streaming_api_base_url)).to eq 'ws://localhost:4000' + initial_state_json = json_str_to_hash(assigns(:initial_state_json)) + expect(initial_state_json[:meta]).to_not be_nil + expect(initial_state_json[:compose]).to_not be_nil + expect(initial_state_json[:accounts]).to_not be_nil + expect(initial_state_json[:settings]).to_not be_nil + expect(initial_state_json[:media_attachments]).to_not be_nil end end end diff --git a/spec/controllers/manifests_controller_spec.rb b/spec/controllers/manifests_controller_spec.rb new file mode 100644 index 000000000..6f188fa35 --- /dev/null +++ b/spec/controllers/manifests_controller_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' + +describe ManifestsController do + render_views + + describe 'GET #show' do + before do + get :show, format: :json + end + + it 'assigns @instance_presenter' do + expect(assigns(:instance_presenter)).to be_kind_of InstancePresenter + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/fixtures/requests/koi8-r.txt b/spec/fixtures/requests/koi8-r.txt new file mode 100644 index 000000000..d4242af01 --- /dev/null +++ b/spec/fixtures/requests/koi8-r.txt @@ -0,0 +1,20 @@ +HTTP/1.1 200 OK +Server: nginx/1.11.10 +Date: Tue, 04 Jul 2017 16:43:39 GMT +Content-Type: text/html +Content-Length: 273 +Connection: keep-alive +Last-Modified: Tue, 04 Jul 2017 16:41:34 GMT +Accept-Ranges: bytes + +<HTML> +<HEAD> + <META NAME="GENERATOR" CONTENT="Adobe PageMill 3.0J Mac"> + <META HTTP-EQUIV="Content-Type" CONTENT="text/html;CHARSET=koi8-r"> + <TITLE> XVI . .</TITLE> +</HEAD> +<BODY> +<P><CENTER><B><FONT SIZE="+2"> XVI . .</FONT></B><BR> +<HR><BR> +</BODY> +</HTML> diff --git a/spec/fixtures/requests/sjis.txt b/spec/fixtures/requests/sjis.txt new file mode 100644 index 000000000..faf18d35c --- /dev/null +++ b/spec/fixtures/requests/sjis.txt @@ -0,0 +1,20 @@ +HTTP/1.1 200 OK +Server: nginx/1.11.10 +Date: Tue, 04 Jul 2017 16:43:39 GMT +Content-Type: text/html +Content-Length: 273 +Connection: keep-alive +Last-Modified: Tue, 04 Jul 2017 16:41:34 GMT +Accept-Ranges: bytes + +<HTML> +<HEAD> + <META NAME="GENERATOR" CONTENT="Adobe PageMill 3.0J Mac"> + <META HTTP-EQUIV="Content-Type" CONTENT="text/html;CHARSET=x-sjis"> + <TITLE>SJIS̃y[W</TITLE> +</HEAD> +<BODY> +<P><CENTER><B><FONT SIZE="+2">N܂ĂLOlĂ̂̎łłBԂɈӖ҂͐ǂȔ܂܂ł\グ邽ɂ͎QlA邽Aɂ܂ȂB炢Ȃ̂͂ǂ㌎ł邾BĉcɔRKɉ]ł͂͂Ȃw}ƂoȂȂāA͎͉̐̂A{炩Av̂̂̂ɂ]ƌmanɂ֎Q悤ɓɂłȂ̂ŁA\ɕςĂłōlBႦƂǂ܂̂ۂނ݂ƂłāA̎ł͐\ĂƂĐԂɕׂ̂ɍsȂȁB</FONT></B><BR> +<HR><BR> +</BODY> +</HTML> diff --git a/spec/fixtures/requests/sjis_with_wrong_charset.txt b/spec/fixtures/requests/sjis_with_wrong_charset.txt new file mode 100644 index 000000000..456750c6b --- /dev/null +++ b/spec/fixtures/requests/sjis_with_wrong_charset.txt @@ -0,0 +1,20 @@ +HTTP/1.1 200 OK +Server: nginx/1.11.10 +Date: Tue, 04 Jul 2017 16:43:39 GMT +Content-Type: text/html; charset=utf-8 +Content-Length: 273 +Connection: keep-alive +Last-Modified: Tue, 04 Jul 2017 16:41:34 GMT +Accept-Ranges: bytes + +<HTML> +<HEAD> + <META NAME="GENERATOR" CONTENT="Adobe PageMill 3.0J Mac"> + <META HTTP-EQUIV="Content-Type" CONTENT="text/html;CHARSET=x-sjis"> + <TITLE>SJIS̃y[W</TITLE> +</HEAD> +<BODY> +<P><CENTER><B><FONT SIZE="+2">N܂ĂLOlĂ̂̎łłBԂɈӖ҂͐ǂȔ܂܂ł\グ邽ɂ͎QlA邽Aɂ܂ȂB炢Ȃ̂͂ǂ㌎ł邾BĉcɔRKɉ]ł͂͂Ȃw}ƂoȂȂāA͎͉̐̂A{炩Av̂̂̂ɂ]ƌmanɂ֎Q悤ɓɂłȂ̂ŁA\ɕςĂłōlBႦƂǂ܂̂ۂނ݂ƂłāA̎ł͐\ĂƂĐԂɕׂ̂ɍsȂȁB</FONT></B><BR> +<HR><BR> +</BODY> +</HTML> diff --git a/spec/javascript/components/display_name.test.js b/spec/javascript/components/display_name.test.js index d6dc7edc0..ad9288d4d 100644 --- a/spec/javascript/components/display_name.test.js +++ b/spec/javascript/components/display_name.test.js @@ -1,12 +1,12 @@ import { expect } from 'chai'; import { render } from 'enzyme'; -import Immutable from 'immutable'; +import { fromJS } from 'immutable'; import React from 'react'; import DisplayName from '../../../app/javascript/mastodon/components/display_name'; describe('<DisplayName />', () => { it('renders display name + account name', () => { - const account = Immutable.fromJS({ + const account = fromJS({ username: 'bar', acct: 'bar@baz', display_name: 'Foo', @@ -16,7 +16,7 @@ describe('<DisplayName />', () => { }); it('renders the username + account name if display name is empty', () => { - const account = Immutable.fromJS({ + const account = fromJS({ username: 'bar', acct: 'bar@baz', display_name: '', diff --git a/spec/javascript/components/emojify.test.js b/spec/javascript/components/emojify.test.js new file mode 100644 index 000000000..3e8b25af9 --- /dev/null +++ b/spec/javascript/components/emojify.test.js @@ -0,0 +1,83 @@ +import { expect } from 'chai'; +import emojify from '../../../app/javascript/mastodon/emoji'; + +describe('emojify', () => { + it('does a basic emojify', () => { + expect(emojify(':smile:')).to.equal( + '<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" />'); + }); + + it('does a double emojify', () => { + expect(emojify(':smile: and :wink:')).to.equal( + '<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" /> and <img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" />'); + }); + + it('works with random colons', () => { + expect(emojify(':smile: : :wink:')).to.equal( + '<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" /> : <img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" />'); + expect(emojify(':smile::::wink:')).to.equal( + '<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" />::<img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" />'); + expect(emojify(':smile:::::wink:')).to.equal( + '<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" />:::<img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" />'); + }); + + it('works with tags', () => { + expect(emojify('<p>:smile:</p>')).to.equal( + '<p><img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" /></p>'); + expect(emojify('<p>:smile:</p> and <p>:wink:</p>')).to.equal( + '<p><img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" /></p> and <p><img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" /></p>'); + }); + + it('ignores unknown shortcodes', () => { + expect(emojify(':foobarbazfake:')).to.equal(':foobarbazfake:'); + }); + + it('ignores shortcodes inside of tags', () => { + expect(emojify('<p data-foo=":smile:"></p>')).to.equal('<p data-foo=":smile:"></p>'); + }); + + it('works with unclosed tags', () => { + expect(emojify('hello>')).to.equal('hello>'); + expect(emojify('<hello')).to.equal('<hello'); + }); + + it('works with unclosed shortcodes', () => { + expect(emojify('smile:')).to.equal('smile:'); + expect(emojify(':smile')).to.equal(':smile'); + }); + + it('does two emoji next to each other', () => { + expect(emojify(':smile::wink:')).to.equal( + '<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" /><img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" />'); + }); + + it('does unicode', () => { + expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).to.equal( + '<img draggable="false" class="emojione" alt="👩👩👦👦" title=":family_wwbb:" src="/emoji/1f469-1f469-1f466-1f466.svg" />'); + expect(emojify('\uD83D\uDC68\uD83D\uDC69\uD83D\uDC67\uD83D\uDC67')).to.equal( + '<img draggable="false" class="emojione" alt="👨👩👧👧" title=":family_mwgg:" src="/emoji/1f468-1f469-1f467-1f467.svg" />'); + expect(emojify('\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66')).to.equal('<img draggable="false" class="emojione" alt="👩👩👦" title=":family_wwb:" src="/emoji/1f469-1f469-1f466.svg" />'); + expect(emojify('\u2757')).to.equal( + '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />'); + }); + + it('does multiple unicode', () => { + expect(emojify('\u2757 #\uFE0F\u20E3')).to.equal( + '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" />'); + expect(emojify('\u2757#\uFE0F\u20E3')).to.equal( + '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" />'); + expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).to.equal( + '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" /> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />'); + expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).to.equal( + 'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" /> bar'); + }); + + it('does mixed unicode and shortnames', () => { + expect(emojify(':smile:#\uFE0F\u20E3:wink:\u2757')).to.equal('<img draggable="false" class="emojione" alt="😄" title=":smile:" src="/emoji/1f604.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/0023-20e3.svg" /><img draggable="false" class="emojione" alt="😉" title=":wink:" src="/emoji/1f609.svg" /><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />'); + }); + + it('ignores unicode inside of tags', () => { + expect(emojify('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>')).to.equal('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>'); + }); + +}); diff --git a/spec/javascript/setup.js b/spec/javascript/setup.js index 7d4b2866e..c9c8aed07 100644 --- a/spec/javascript/setup.js +++ b/spec/javascript/setup.js @@ -1,19 +1,13 @@ -import { jsdom } from 'jsdom/lib/old-api'; +import { JSDOM } from 'jsdom'; import chai from 'chai'; import chaiEnzyme from 'chai-enzyme'; chai.use(chaiEnzyme()); -var exposedProperties = ['window', 'navigator', 'document']; - -global.document = jsdom(''); -global.window = document.defaultView; -Object.keys(document.defaultView).forEach((property) => { +const { window } = new JSDOM('', { + userAgent: 'node.js', +}); +Object.keys(window).forEach(property => { if (typeof global[property] === 'undefined') { - exposedProperties.push(property); - global[property] = document.defaultView[property]; + global[property] = window[property]; } }); - -global.navigator = { - userAgent: 'node.js', -}; diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index bf474c354..4bdc96866 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -131,4 +131,17 @@ RSpec.describe FeedManager do end end end + + describe '#push' do + it 'trims timelines if they will have more than FeedManager::MAX_ITEMS' do + account = Fabricate(:account) + status = Fabricate(:status) + members = FeedManager::MAX_ITEMS.times.map { |count| [count, count] } + Redis.current.zadd("feed:type:#{account.id}", members) + + FeedManager.instance.push('type', account, status) + + expect(Redis.current.zcard("feed:type:#{account.id}")).to eq FeedManager::MAX_ITEMS + end + end end diff --git a/spec/lib/inline_rabl_scope_spec.rb b/spec/lib/inline_rabl_scope_spec.rb deleted file mode 100644 index 3fff176e4..000000000 --- a/spec/lib/inline_rabl_scope_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe InlineRablScope do - describe '#current_account' do - it 'returns the given account' do - account = Fabricate(:account) - expect(InlineRablScope.new(account).current_account).to eq account - end - end - - describe '#current_user' do - it 'returns nil if the given account is nil' do - expect(InlineRablScope.new(nil).current_user).to eq nil - end - - it 'returns user of account if the given account is not nil' do - user = Fabricate(:user) - expect(InlineRablScope.new(user.account).current_user).to eq user - end - end -end diff --git a/spec/lib/user_settings_decorator_spec.rb b/spec/lib/user_settings_decorator_spec.rb index 66e42fa0e..a67487779 100644 --- a/spec/lib/user_settings_decorator_spec.rb +++ b/spec/lib/user_settings_decorator_spec.rb @@ -28,6 +28,13 @@ describe UserSettingsDecorator do expect(user.settings['default_privacy']).to eq 'public' end + it 'updates the user settings value for sensitive' do + values = { 'setting_default_sensitive' => '1' } + + settings.update(values) + expect(user.settings['default_sensitive']).to eq true + end + it 'updates the user settings value for boost modal' do values = { 'setting_boost_modal' => '1' } @@ -48,5 +55,12 @@ describe UserSettingsDecorator do settings.update(values) expect(user.settings['auto_play_gif']).to eq false end + + it 'updates the user settings value for system font in UI' do + values = { 'setting_system_font_ui' => '0' } + + settings.update(values) + expect(user.settings['system_font_ui']).to eq false + end end end diff --git a/spec/models/feed_spec.rb b/spec/models/feed_spec.rb index 15033e9eb..1c377c17f 100644 --- a/spec/models/feed_spec.rb +++ b/spec/models/feed_spec.rb @@ -2,19 +2,19 @@ require 'rails_helper' RSpec.describe Feed, type: :model do describe '#get' do - it 'gets statuses with ids in the range, maintining the order from Redis' do + it 'gets statuses with ids in the range' do account = Fabricate(:account) Fabricate(:status, account: account, id: 1) Fabricate(:status, account: account, id: 2) Fabricate(:status, account: account, id: 3) Fabricate(:status, account: account, id: 10) - redis = double(zrevrangebyscore: [['val2', 2.0], ['val1', 1.0], ['val3', 3.0], ['deleted', 4.0]], exists: false) - allow(Redis).to receive(:current).and_return(redis) + Redis.current.zadd(FeedManager.instance.key(:home, account.id), + [[4, 'deleted'], [3, 'val3'], [2, 'val2'], [1, 'val1']]) feed = Feed.new(:home, account) results = feed.get(3) - expect(results.map(&:id)).to eq [2, 1, 3] + expect(results.map(&:id)).to eq [3, 2] expect(results.first.attributes.keys).to eq %w(id updated_at) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a6df3fb26..2019ec0f6 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -184,6 +184,14 @@ RSpec.describe User, type: :model do expect(user.setting_auto_play_gif).to eq false end end + + describe '#setting_system_font_ui' do + it 'returns system font ui setting' do + user = Fabricate(:user) + user.settings[:system_font_ui] = false + expect(user.setting_system_font_ui).to eq false + end + end describe '#setting_boost_modal' do it 'returns boost modal setting' do diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index cfc9eec9e..4f7399505 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -13,17 +13,23 @@ Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } ActiveRecord::Migration.maintain_test_schema! WebMock.disable_net_connect! +Redis.current = Redis::Namespace.new("mastodon_test#{ENV['TEST_ENV_NUMBER']}", redis: Redis.current) Sidekiq::Testing.inline! Sidekiq::Logging.logger = nil Devise::Test::ControllerHelpers.module_eval do alias_method :original_sign_in, :sign_in - def sign_in(resource, deprecated = nil, scope: nil) + def sign_in(resource, _deprecated = nil, scope: nil) original_sign_in(resource, scope: scope) - SessionActivation.deactivate warden.raw_session["auth_id"] - warden.raw_session["auth_id"] = resource.activate_session(warden.request) + SessionActivation.deactivate warden.cookies.signed['_session_id'] + + warden.cookies.signed['_session_id'] = { + value: resource.activate_session(warden.request), + expires: 1.year.from_now, + httponly: true, + } end end @@ -43,6 +49,11 @@ RSpec.configure do |config| https = ENV['LOCAL_HTTPS'] == 'true' Capybara.app_host = "http#{https ? 's' : ''}://#{ENV.fetch('LOCAL_DOMAIN')}" end + + config.after :each do + keys = Redis.current.keys + Redis.current.del(keys) if keys.any? + end end RSpec::Sidekiq.configure do |config| diff --git a/spec/requests/localization_spec.rb b/spec/requests/localization_spec.rb index 2f7a5e91e..f625a93a4 100644 --- a/spec/requests/localization_spec.rb +++ b/spec/requests/localization_spec.rb @@ -6,13 +6,13 @@ describe 'Localization' do after(:all) do I18n.locale = I18n.default_locale end - + it 'uses a specific region when provided' do headers = { 'Accept-Language' => 'zh-HK' } get "/about", headers: headers expect(response.body).to include( - I18n.t('about.about_mastodon', locale: 'zh-HK') + I18n.t('about.about_mastodon_html', locale: 'zh-HK') ) end @@ -21,7 +21,7 @@ describe 'Localization' do get "/about", headers: headers expect(response.body).to include( - I18n.t('about.about_mastodon', locale: 'es') + I18n.t('about.about_mastodon_html', locale: 'es') ) end it 'falls back to english when locale is missing' do @@ -29,7 +29,7 @@ describe 'Localization' do get "/about", headers: headers expect(response.body).to include( - I18n.t('about.about_mastodon', locale: 'en') + I18n.t('about.about_mastodon_html', locale: 'en') ) end end diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb index 9df41cf55..698eb0324 100644 --- a/spec/services/fetch_link_card_service_spec.rb +++ b/spec/services/fetch_link_card_service_spec.rb @@ -6,6 +6,12 @@ RSpec.describe FetchLinkCardService do before do stub_request(:head, 'http://example.xn--fiqs8s/').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) stub_request(:get, 'http://example.xn--fiqs8s/').to_return(request_fixture('idn.txt')) + stub_request(:head, 'http://example.com/sjis').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) + stub_request(:get, 'http://example.com/sjis').to_return(request_fixture('sjis.txt')) + stub_request(:head, 'http://example.com/sjis_with_wrong_charset').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) + stub_request(:get, 'http://example.com/sjis_with_wrong_charset').to_return(request_fixture('sjis_with_wrong_charset.txt')) + stub_request(:head, 'http://example.com/koi8-r').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) + stub_request(:get, 'http://example.com/koi8-r').to_return(request_fixture('koi8-r.txt')) stub_request(:head, 'https://github.com/qbi/WannaCry').to_return(status: 404) subject.call(status) @@ -19,6 +25,33 @@ RSpec.describe FetchLinkCardService do expect(a_request(:get, 'http://example.xn--fiqs8s/')).to have_been_made.at_least_once end end + + context do + let(:status) { Fabricate(:status, text: 'Check out http://example.com/sjis') } + + it 'works with SJIS' do + expect(a_request(:get, 'http://example.com/sjis')).to have_been_made.at_least_once + expect(status.preview_card.title).to eq("SJISのページ") + end + end + + context do + let(:status) { Fabricate(:status, text: 'Check out http://example.com/sjis_with_wrong_charset') } + + it 'works with SJIS even with wrong charset header' do + expect(a_request(:get, 'http://example.com/sjis_with_wrong_charset')).to have_been_made.at_least_once + expect(status.preview_card.title).to eq("SJISのページ") + end + end + + context do + let(:status) { Fabricate(:status, text: 'Check out http://example.com/koi8-r') } + + it 'works with koi8-r' do + expect(a_request(:get, 'http://example.com/koi8-r')).to have_been_made.at_least_once + expect(status.preview_card.title).to eq("Московя начинаетъ только въ XVI ст. привлекать внимане иностранцевъ.") + end + end end context 'in a remote status' do diff --git a/spec/services/precompute_feed_service_spec.rb b/spec/services/precompute_feed_service_spec.rb index 9f56b0256..dbd08ac1b 100644 --- a/spec/services/precompute_feed_service_spec.rb +++ b/spec/services/precompute_feed_service_spec.rb @@ -11,12 +11,29 @@ RSpec.describe PrecomputeFeedService do account = Fabricate(:account) followed_account = Fabricate(:account) Fabricate(:follow, account: account, target_account: followed_account) - status = Fabricate(:status, account: followed_account) + reblog = Fabricate(:status, account: followed_account) + status = Fabricate(:status, account: account, reblog: reblog) - expected_redis_args = FeedManager.instance.key(:home, account.id), status.id, status.id - expect_any_instance_of(Redis).to receive(:zadd).with(*expected_redis_args) + subject.call(account) + + expect(Redis.current.zscore(FeedManager.instance.key(:home, account.id), reblog.id)).to eq status.id + end + + it 'does not raise an error even if it could not find any status' do + account = Fabricate(:account) + subject.call(account) + end + + it 'filters statuses' do + account = Fabricate(:account) + muted_account = Fabricate(:account) + Fabricate(:mute, account: account, target_account: muted_account) + reblog = Fabricate(:status, account: muted_account) + status = Fabricate(:status, account: account, reblog: reblog) subject.call(account) + + expect(Redis.current.zscore(FeedManager.instance.key(:home, account.id), reblog.id)).to eq nil end end end diff --git a/spec/views/about/show.html.haml_spec.rb b/spec/views/about/show.html.haml_spec.rb index 2c5130d84..c0ead6349 100644 --- a/spec/views/about/show.html.haml_spec.rb +++ b/spec/views/about/show.html.haml_spec.rb @@ -10,10 +10,11 @@ describe 'about/show.html.haml', without_verify_partial_doubles: true do it 'has valid open graph tags' do instance_presenter = double(:instance_presenter, - site_description: 'something', - open_registrations: false, - closed_registrations_message: 'yes', - ) + site_title: 'something', + site_description: 'something', + version_number: '1.0', + open_registrations: false, + closed_registrations_message: 'yes') assign(:instance_presenter, instance_presenter) render diff --git a/spec/workers/scheduler/feed_cleanup_scheduler_spec.rb b/spec/workers/scheduler/feed_cleanup_scheduler_spec.rb index 4c709a2c9..b8487b03f 100644 --- a/spec/workers/scheduler/feed_cleanup_scheduler_spec.rb +++ b/spec/workers/scheduler/feed_cleanup_scheduler_spec.rb @@ -7,10 +7,13 @@ describe Scheduler::FeedCleanupScheduler do let!(:inactive_user) { Fabricate(:user, current_sign_in_at: 22.days.ago) } it 'clears feeds of inactives' do - expect_any_instance_of(Redis).to receive(:del).with(feed_key_for(inactive_user)) - expect_any_instance_of(Redis).not_to receive(:del).with(feed_key_for(active_user)) + Redis.current.zadd(feed_key_for(inactive_user), 1, 1) + Redis.current.zadd(feed_key_for(active_user), 1, 1) subject.perform + + expect(Redis.current.zcard(feed_key_for(inactive_user))).to eq 0 + expect(Redis.current.zcard(feed_key_for(active_user))).to eq 1 end def feed_key_for(user) diff --git a/streaming/index.js b/streaming/index.js index 400456d24..c7e0de96c 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -262,11 +262,12 @@ const startWorker = (workerId) => { const { event, payload, queued_at } = JSON.parse(message); const transmit = () => { - const now = new Date().getTime(); - const delta = now - queued_at; + const now = new Date().getTime(); + const delta = now - queued_at; + const encodedPayload = typeof payload === 'number' ? payload : JSON.stringify(payload); - log.silly(req.requestId, `Transmitting for ${req.accountId}: ${event} ${payload} Delay: ${delta}ms`); - output(event, payload); + log.silly(req.requestId, `Transmitting for ${req.accountId}: ${event} ${encodedPayload} Delay: ${delta}ms`); + output(event, encodedPayload); }; if (notificationOnly && event !== 'notification') { @@ -282,7 +283,7 @@ const startWorker = (workerId) => { return; } - const unpackedPayload = JSON.parse(payload); + const unpackedPayload = payload; const targetAccountIds = [unpackedPayload.account.id].concat(unpackedPayload.mentions.map(item => item.id)); const accountDomain = unpackedPayload.account.acct.split('@')[1]; @@ -466,6 +467,7 @@ const startWorker = (workerId) => { const onExit = () => { log.info(`Worker ${workerId} exiting, bye bye`); server.close(); + process.exit(0); }; const onError = (err) => { diff --git a/yarn.lock b/yarn.lock index b8af49d62..fba802e0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -121,6 +121,10 @@ react-split-pane "^0.1.63" redux "^3.6.0" +"@types/node@^6.0.46": + version "6.0.78" + resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.78.tgz#5d4a3f579c1524e01ee21bf474e6fba09198f470" + abab@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d" @@ -251,7 +255,7 @@ ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" -any-promise@^0.1.0, any-promise@~0.1.0: +any-promise@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-0.1.0.tgz#830b680aa7e56f33451d4b049f3bd8044498ee27" @@ -456,27 +460,6 @@ axios@^0.16.2: follow-redirects "^1.2.3" is-buffer "^1.1.5" -babel-cli@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-cli/-/babel-cli-6.24.1.tgz#207cd705bba61489b2ea41b5312341cf6aca2283" - dependencies: - babel-core "^6.24.1" - babel-polyfill "^6.23.0" - babel-register "^6.24.1" - babel-runtime "^6.22.0" - commander "^2.8.1" - convert-source-map "^1.1.0" - fs-readdir-recursive "^1.0.0" - glob "^7.0.0" - lodash "^4.2.0" - output-file-sync "^1.1.0" - path-is-absolute "^1.0.0" - slash "^1.0.0" - source-map "^0.5.0" - v8flags "^2.0.10" - optionalDependencies: - chokidar "^1.6.1" - babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" @@ -1044,6 +1027,12 @@ babel-plugin-transform-react-display-name@^6.23.0: dependencies: babel-runtime "^6.22.0" +babel-plugin-transform-react-inline-elements@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-inline-elements/-/babel-plugin-transform-react-inline-elements-6.22.0.tgz#6687211a32b49a52f22c573a2b5504a25ef17c53" + dependencies: + babel-runtime "^6.22.0" + babel-plugin-transform-react-jsx-self@6.22.0, babel-plugin-transform-react-jsx-self@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz#df6d80a9da2612a121e6ddd7558bcbecf06e636e" @@ -1091,14 +1080,6 @@ babel-plugin-transform-strict-mode@^6.24.1: babel-runtime "^6.22.0" babel-types "^6.24.1" -babel-polyfill@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.23.0.tgz#8364ca62df8eafb830499f699177466c3b03499d" - dependencies: - babel-runtime "^6.22.0" - core-js "^2.4.0" - regenerator-runtime "^0.10.0" - babel-preset-env@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.4.0.tgz#c8e02a3bcc7792f23cded68e0355b9d4c28f0f7a" @@ -1284,7 +1265,7 @@ babel-register@^6.24.1: mkdirp "^0.5.1" source-map-support "^0.4.2" -babel-runtime@6.x.x, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.5.0, babel-runtime@^6.9.2: +babel-runtime@6.x.x, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.20.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.5.0, babel-runtime@^6.9.2: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" dependencies: @@ -1342,10 +1323,6 @@ balanced-match@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.1.0.tgz#b504bd05869b39259dd0c5efc35d843176dccc4a" -balanced-match@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.2.1.tgz#7bc658b4bed61eee424ad74f75f5c3e2c4df3cc7" - balanced-match@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" @@ -1545,6 +1522,10 @@ callsites@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" +camelcase-css@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-1.0.1.tgz#157c4238265f5cf94a1dffde86446552cbf3f705" + camelcase-keys@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" @@ -1600,9 +1581,9 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" -chai-enzyme@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/chai-enzyme/-/chai-enzyme-0.7.1.tgz#a945c81989bcc4fd96af6263f9c0a9c668f29b66" +chai-enzyme@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/chai-enzyme/-/chai-enzyme-0.8.0.tgz#609c552a1dcdb091f435e1e281cc4f2149a33be1" dependencies: html "^1.0.0" react-element-to-jsx-string "^5.0.0" @@ -1653,7 +1634,7 @@ cheerio@^0.22.0: lodash.reject "^4.4.0" lodash.some "^4.4.0" -chokidar@^1.4.3, chokidar@^1.6.0, chokidar@^1.6.1: +chokidar@^1.4.3, chokidar@^1.6.0: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" dependencies: @@ -2021,7 +2002,7 @@ crypto-random-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" -css-color-function@^1.2.0: +css-color-function@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/css-color-function/-/css-color-function-1.3.0.tgz#72c767baf978f01b8a8a94f42f17ba5d22a776fc" dependencies: @@ -2343,7 +2324,7 @@ doctrine@^2.0.0: esutils "^2.0.2" isarray "^1.0.0" -"dom-helpers@^2.4.0 || ^3.0.0", dom-helpers@^3.0.0: +"dom-helpers@^2.4.0 || ^3.0.0", dom-helpers@^3.0.0, dom-helpers@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.2.1.tgz#3203e07fed217bd1f424b019735582fc37b2825a" @@ -2531,8 +2512,8 @@ es-to-primitive@^1.1.1: is-symbol "^1.0.1" es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14: - version "0.10.23" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.23.tgz#7578b51be974207a5487821b56538c224e4e7b38" + version "0.10.24" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.24.tgz#a55877c9924bc0c8d9bd3c2cbe17495ac1709b14" dependencies: es6-iterator "2" es6-symbol "~3.1" @@ -3026,15 +3007,6 @@ fresh@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" -fs-extra@^0.24.0: - version "0.24.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.24.0.tgz#d4e4342a96675cb7846633a6099249332b539952" - dependencies: - graceful-fs "^4.1.2" - jsonfile "^2.1.0" - path-is-absolute "^1.0.0" - rimraf "^2.2.8" - fs-extra@^0.30.0: version "0.30.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0" @@ -3045,16 +3017,6 @@ fs-extra@^0.30.0: path-is-absolute "^1.0.0" rimraf "^2.2.8" -fs-promise@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/fs-promise/-/fs-promise-0.3.1.tgz#bf34050368f24d6dc9dfc6688ab5cead8f86842a" - dependencies: - any-promise "~0.1.0" - -fs-readdir-recursive@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -3204,16 +3166,6 @@ glob@7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^5.0.3: - version "5.0.15" - resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@~7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" @@ -3236,17 +3188,6 @@ globals@^9.0.0, globals@^9.14.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" -globby@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-3.0.1.tgz#2094af8421e19152150d5893eb6416b312d9a22f" - dependencies: - array-union "^1.0.1" - arrify "^1.0.0" - glob "^5.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^1.0.0" - globby@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" @@ -3282,7 +3223,7 @@ gonzales-pe@^4.0.3: dependencies: minimist "1.1.x" -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9: +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -3911,9 +3852,9 @@ jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" -jsdom@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-10.1.0.tgz#7765e00fd5c3567f34985a1c86ff466a61dacc6a" +jsdom@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.0.0.tgz#1ee507cb2c0b16c875002476b1a8557d951353e5" dependencies: abab "^1.0.3" acorn "^4.0.4" @@ -3925,7 +3866,7 @@ jsdom@^10.1.0: escodegen "^1.6.1" html-encoding-sniffer "^1.0.1" nwmatcher ">= 1.3.9 < 2.0.0" - parse5 "^1.5.1" + parse5 "^3.0.2" pn "^1.0.0" request "^2.79.0" request-promise-native "^1.0.3" @@ -4002,7 +3943,7 @@ jsx-ast-utils@^1.0.0, jsx-ast-utils@^1.3.4: version "1.4.1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1" -keycode@^2.1.8: +keycode@^2.1.7, keycode@^2.1.8: version "2.1.9" resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.9.tgz#964a23c54e4889405b4861a5c9f0480d45141dfa" @@ -4434,7 +4375,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" -"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: +minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: @@ -4860,14 +4801,6 @@ osenv@0, osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -output-file-sync@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-1.1.2.tgz#d0a33eefe61a205facb90092e826598d5245ce76" - dependencies: - graceful-fs "^4.1.4" - mkdirp "^0.5.1" - object-assign "^4.1.0" - p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -4919,9 +4852,11 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" -parse5@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94" +parse5@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.2.tgz#05eff57f0ef4577fb144a79f8b9a967a6cc44510" + dependencies: + "@types/node" "^6.0.46" parseurl@~1.3.1: version "1.3.1" @@ -5054,22 +4989,12 @@ pify@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" -pinkie-promise@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-1.0.0.tgz#d1da67f5482563bb7cf57f286ae2822ecfbf3670" - dependencies: - pinkie "^1.0.0" - pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" dependencies: pinkie "^2.0.0" -pinkie@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-1.0.0.tgz#5a47f28ba1015d0201bda7bf0f358e47bec8c7e4" - pinkie@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" @@ -5109,7 +5034,7 @@ postcss-advanced-variables@1.2.2: dependencies: postcss "^5.0.10" -postcss-atroot@^0.1.2: +postcss-atroot@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/postcss-atroot/-/postcss-atroot-0.1.3.tgz#6752c0230c745140549345b2b0e30ebeda01a405" dependencies: @@ -5123,12 +5048,12 @@ postcss-calc@^5.2.0: postcss-message-helpers "^2.0.0" reduce-css-calc "^1.2.6" -postcss-color-function@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-color-function/-/postcss-color-function-2.0.1.tgz#9ad226f550e8a7c7f8b8a77860545b6dd7f55241" +postcss-color-function@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-function/-/postcss-color-function-4.0.0.tgz#7e0106f4f6a1ecb1ad5b3a8553ace5e828aae187" dependencies: - css-color-function "^1.2.0" - postcss "^5.0.4" + css-color-function "^1.3.0" + postcss "^6.0.1" postcss-message-helpers "^2.0.0" postcss-value-parser "^3.3.0" @@ -5147,26 +5072,25 @@ postcss-convert-values@^2.3.4: postcss "^5.0.11" postcss-value-parser "^3.1.2" -postcss-custom-media@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-5.0.1.tgz#138d25a184bf2eb54de12d55a6c01c30a9d8bd81" +postcss-custom-media@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-6.0.0.tgz#be532784110ecb295044fb5395a18006eb21a737" dependencies: - postcss "^5.0.0" + postcss "^6.0.1" -postcss-custom-properties@^5.0.0: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-5.0.2.tgz#9719d78f2da9cf9f53810aebc23d4656130aceb1" +postcss-custom-properties@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-6.1.0.tgz#9caf1151ac41b1e9e64d3a2ff9ece996ca18977d" dependencies: - balanced-match "^0.4.2" - postcss "^5.0.0" + balanced-match "^1.0.0" + postcss "^6.0.3" -postcss-custom-selectors@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-3.0.0.tgz#8f81249f5ed07a8d0917cf6a39fe5b056b7f96ac" +postcss-custom-selectors@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-4.0.1.tgz#781382f94c52e727ef5ca4776ea2adf49a611382" dependencies: - balanced-match "^0.2.0" - postcss "^5.0.0" - postcss-selector-matches "^2.0.0" + postcss "^6.0.1" + postcss-selector-matches "^3.0.0" postcss-discard-comments@^2.0.4: version "2.0.4" @@ -5199,7 +5123,7 @@ postcss-discard-unused@^2.2.1: postcss "^5.0.14" uniqs "^2.0.0" -postcss-extend@^1.0.1: +postcss-extend@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/postcss-extend/-/postcss-extend-1.0.5.tgz#5ea98bf787ba3cacf4df4609743f80a833b1d0e7" dependencies: @@ -5218,6 +5142,23 @@ postcss-flexbugs-fixes@^3.0.0: dependencies: postcss "^6.0.1" +postcss-import@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-10.0.0.tgz#4c85c97b099136cc5ea0240dc1dfdbfde4e2ebbe" + dependencies: + object-assign "^4.0.1" + postcss "^6.0.1" + postcss-value-parser "^3.2.3" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-1.0.0.tgz#ccee5aa3b1970dd457008e79438165f66919ba30" + dependencies: + camelcase-css "^1.0.1" + postcss "^6.0.1" + postcss-load-config@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a" @@ -5250,11 +5191,11 @@ postcss-loader@^2.0.5, postcss-loader@^2.0.6: postcss-load-config "^1.2.0" schema-utils "^0.3.0" -postcss-media-minmax@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-2.1.2.tgz#444c5cf8926ab5e4fd8a2509e9297e751649cdf8" +postcss-media-minmax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-3.0.0.tgz#675256037a43ef40bc4f0760bfd06d4dc69d48d2" dependencies: - postcss "^5.0.4" + postcss "^6.0.1" postcss-merge-idents@^2.1.5: version "2.1.7" @@ -5317,13 +5258,15 @@ postcss-minify-selectors@^2.0.4: postcss "^5.0.14" postcss-selector-parser "^2.0.0" -postcss-mixins@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/postcss-mixins/-/postcss-mixins-2.1.1.tgz#b141a0803efa8e2d744867f8d91596890cf9241b" +postcss-mixins@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-mixins/-/postcss-mixins-6.0.1.tgz#f5c9726259a6103733b43daa6a8b67dd0ed7aa47" dependencies: - globby "^3.0.1" - postcss "^5.0.10" - postcss-simple-vars "^1.0.1" + globby "^6.1.0" + postcss "^6.0.3" + postcss-js "^1.0.0" + postcss-simple-vars "^4.0.0" + sugarss "^1.0.0" postcss-modules-extract-imports@^1.0.0: version "1.2.0" @@ -5352,17 +5295,17 @@ postcss-modules-values@^1.1.0: icss-replace-symbols "^1.1.0" postcss "^6.0.1" -postcss-nested@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-1.0.1.tgz#91f28f4e6e23d567241ac154558a0cfab4cc0d8f" +postcss-nested@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-2.0.2.tgz#f38fad547f5c3747160aec3bb34745819252974a" dependencies: - postcss "^5.2.17" + postcss "^6.0.1" -postcss-nesting@^2.0.6: - version "2.3.1" - resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-2.3.1.tgz#94a6b6a4ef707fbec20a87fee5c957759b4e01cf" +postcss-nesting@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-4.0.1.tgz#8fc2ce40cbfcfab7ee24e7b68fb6ebe84b641469" dependencies: - postcss "^5.0.19" + postcss "^6.0.1" postcss-normalize-charset@^1.1.0: version "1.1.1" @@ -5386,17 +5329,14 @@ postcss-ordered-values@^2.1.0: postcss "^5.0.4" postcss-value-parser "^3.0.1" -postcss-partial-import@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/postcss-partial-import/-/postcss-partial-import-1.3.0.tgz#2f4b773a76c7b0a69b389dcf475c4d362d0d2576" +postcss-partial-import@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-partial-import/-/postcss-partial-import-4.1.0.tgz#f6c3e78e7bbeda4d9dab96d360367b90b353f9a4" dependencies: - fs-extra "^0.24.0" - fs-promise "^0.3.1" - object-assign "^4.0.1" - postcss "^5.0.5" - string-hash "^1.1.0" + glob "^7.1.1" + postcss-import "^10.0.0" -postcss-property-lookup@^1.1.3: +postcss-property-lookup@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/postcss-property-lookup/-/postcss-property-lookup-1.2.1.tgz#30450a1361b7aae758bbedd5201fbe057bb8270b" dependencies: @@ -5439,19 +5379,19 @@ postcss-scss@^1.0.0: dependencies: postcss "^6.0.3" -postcss-selector-matches@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-2.0.5.tgz#fa0f43be57b68e77aa4cd11807023492a131027f" +postcss-selector-matches@^3.0.0, postcss-selector-matches@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-3.0.1.tgz#e5634011e13950881861bbdd58c2d0111ffc96ab" dependencies: balanced-match "^0.4.2" - postcss "^5.0.0" + postcss "^6.0.1" -postcss-selector-not@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-2.0.0.tgz#c73ad21a3f75234bee7fee269e154fd6a869798d" +postcss-selector-not@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-3.0.1.tgz#2e4db2f0965336c01e7cec7db6c60dff767335d9" dependencies: - balanced-match "^0.2.0" - postcss "^5.0.0" + balanced-match "^0.4.2" + postcss "^6.0.1" postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2: version "2.2.3" @@ -5461,11 +5401,11 @@ postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2: indexes-of "^1.0.1" uniq "^1.0.1" -postcss-simple-vars@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postcss-simple-vars/-/postcss-simple-vars-1.2.0.tgz#2e6689921144b74114e765353275a3c32143f150" +postcss-simple-vars@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-simple-vars/-/postcss-simple-vars-4.0.0.tgz#d49e082897d9a4824f2268fa91d969d943e2ea76" dependencies: - postcss "^5.0.13" + postcss "^6.0.1" postcss-smart-import@^0.7.4: version "0.7.4" @@ -5512,7 +5452,7 @@ postcss-zindex@^2.0.1: postcss "^5.0.4" uniqs "^2.0.0" -postcss@^5.0.0, postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.19, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16, postcss@^5.2.17, postcss@^5.2.6: +postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16, postcss@^5.2.6: version "5.2.17" resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.17.tgz#cf4f597b864d65c8a492b2eabe9d706c879c388b" dependencies: @@ -5551,26 +5491,26 @@ precond@0.2: version "0.2.3" resolved "https://registry.yarnpkg.com/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac" -precss@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/precss/-/precss-1.4.0.tgz#8d7c3ae70f10a00a3955287f85a66e0f8b31cda3" +precss@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/precss/-/precss-2.0.0.tgz#7f567e3318e06d44c8fdbf9e58452e8358bf4b71" dependencies: - postcss "^5.0.10" + postcss "^6.0.3" postcss-advanced-variables "1.2.2" - postcss-atroot "^0.1.2" - postcss-color-function "^2.0.0" - postcss-custom-media "^5.0.0" - postcss-custom-properties "^5.0.0" - postcss-custom-selectors "^3.0.0" - postcss-extend "^1.0.1" - postcss-media-minmax "^2.1.0" - postcss-mixins "^2.1.0" - postcss-nested "^1.0.0" - postcss-nesting "^2.0.6" - postcss-partial-import "^1.3.0" - postcss-property-lookup "^1.1.3" - postcss-selector-matches "^2.0.0" - postcss-selector-not "^2.0.0" + postcss-atroot "^0.1.3" + postcss-color-function "^4.0.0" + postcss-custom-media "^6.0.0" + postcss-custom-properties "^6.1.0" + postcss-custom-selectors "^4.0.1" + postcss-extend "^1.0.5" + postcss-media-minmax "^3.0.0" + postcss-mixins "^6.0.1" + postcss-nested "^2.0.2" + postcss-nesting "^4.0.1" + postcss-partial-import "^4.1.0" + postcss-property-lookup "^1.2.1" + postcss-selector-matches "^3.0.1" + postcss-selector-not "^3.0.1" prelude-ls@~1.1.2: version "1.1.2" @@ -5777,6 +5717,15 @@ react-element-to-jsx-string@^5.0.0: stringify-object "2.4.0" traverse "^0.6.6" +react-event-listener@^0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.4.5.tgz#e3e895a0970cf14ee8f890113af68197abf3d0b1" + dependencies: + babel-runtime "^6.20.0" + fbjs "^0.8.4" + prop-types "^15.5.4" + warning "^3.0.0" + react-html-attributes@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/react-html-attributes/-/react-html-attributes-1.3.0.tgz#c97896e9cac47ad9c4e6618b835029a826f5d28c" @@ -5941,11 +5890,34 @@ react-style-proptype@^3.0.0: dependencies: prop-types "^15.5.4" -react-swipeable@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/react-swipeable/-/react-swipeable-4.0.1.tgz#2cb3a04a52ccebf5361881b30e233dc13ee4b115" +react-swipeable-views-core@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/react-swipeable-views-core/-/react-swipeable-views-core-0.11.1.tgz#61d046799f90725bbf91a0eb3abcab805c774cac" dependencies: - prop-types "^15.5.8" + babel-runtime "^6.23.0" + warning "^3.0.0" + +react-swipeable-views-utils@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/react-swipeable-views-utils/-/react-swipeable-views-utils-0.12.0.tgz#4ff11f20a8da0561f623876d9fd691116e1a6a03" + dependencies: + babel-runtime "^6.23.0" + fbjs "^0.8.4" + keycode "^2.1.7" + prop-types "^15.5.4" + react-event-listener "^0.4.5" + react-swipeable-views-core "^0.11.1" + +react-swipeable-views@^0.12.3: + version "0.12.3" + resolved "https://registry.yarnpkg.com/react-swipeable-views/-/react-swipeable-views-0.12.3.tgz#b0d3f417bcbcd06afda2f8437c15e8360a568744" + dependencies: + babel-runtime "^6.23.0" + dom-helpers "^3.2.1" + prop-types "^15.5.4" + react-swipeable-views-core "^0.11.1" + react-swipeable-views-utils "^0.12.0" + warning "^3.0.0" react-test-renderer@^15.6.1: version "15.6.1" @@ -6289,7 +6261,7 @@ resolve-url@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" -resolve@^1.1.6, resolve@^1.3.3: +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5" dependencies: @@ -6745,10 +6717,6 @@ strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" -string-hash@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" - string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -6854,6 +6822,10 @@ style-loader@^0.18.2: loader-utils "^1.0.2" schema-utils "^0.3.0" +substring-trie@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/substring-trie/-/substring-trie-1.0.0.tgz#5a7ecb83aefcca7b3720f7897cf69e97023be143" + sugarss@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-1.0.0.tgz#65e51b3958432fb70d5451a68bb33e32d0cf1ef7" @@ -7148,10 +7120,6 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" -user-home@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" - user-home@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" @@ -7180,15 +7148,9 @@ uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" -uws@^0.14.5: - version "0.14.5" - resolved "https://registry.yarnpkg.com/uws/-/uws-0.14.5.tgz#67aaf33c46b2a587a5f6666d00f7691328f149dc" - -v8flags@^2.0.10: - version "2.1.1" - resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4" - dependencies: - user-home "^1.1.1" +uws@^8.14.0: + version "8.14.0" + resolved "https://registry.yarnpkg.com/uws/-/uws-8.14.0.tgz#acc1488d13ecb23fe2f942a7eafb06681fa91431" validate-npm-package-license@^3.0.1: version "3.0.1" @@ -7274,9 +7236,9 @@ webpack-dev-middleware@^1.10.2, webpack-dev-middleware@^1.11.0: path-is-absolute "^1.0.0" range-parser "^1.0.3" -webpack-dev-server@lencioni/webpack-dev-server#patch-1: - version "2.5.0" - resolved "https://codeload.github.com/lencioni/webpack-dev-server/tar.gz/8978059d9b880c6c28908a6ed4608e27d40f2f69" +webpack-dev-server@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.5.1.tgz#a02e726a87bb603db5d71abb7d6d2649bf10c769" dependencies: ansi-html "0.0.7" bonjour "^3.5.0" |