diff --git a/.eslintrc.js b/.eslintrc.js index bbdfa7de27..259c86157b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -27,6 +27,7 @@ module.exports = { 'import', 'promise', '@typescript-eslint', + 'formatjs', ], parserOptions: { @@ -71,7 +72,7 @@ module.exports = { 'comma-style': ['warn', 'last'], 'consistent-return': 'error', 'dot-notation': 'error', - eqeqeq: 'error', + eqeqeq: ['error', 'always', { 'null': 'ignore' }], indent: ['warn', 2], 'jsx-quotes': ['error', 'prefer-single'], 'no-case-declarations': 'off', @@ -218,6 +219,25 @@ module.exports = { 'promise/no-callback-in-promise': 'off', 'promise/no-nesting': 'off', 'promise/no-promise-in-callback': 'off', + + 'formatjs/blocklist-elements': 'error', + 'formatjs/enforce-default-message': ['error', 'literal'], + 'formatjs/enforce-description': 'off', // description values not currently used + 'formatjs/enforce-id': 'off', // Explicit IDs are used in the project + 'formatjs/enforce-placeholders': 'off', // Issues in short_number.jsx + 'formatjs/enforce-plural-rules': 'error', + 'formatjs/no-camel-case': 'off', // disabledAccount is only non-conforming + 'formatjs/no-complex-selectors': 'error', + 'formatjs/no-emoji': 'error', + 'formatjs/no-id': 'off', // IDs are used for translation keys + 'formatjs/no-invalid-icu': 'error', + 'formatjs/no-literal-string-in-jsx': 'off', // Should be looked at, but mainly flagging punctuation outside of strings + 'formatjs/no-multiple-plurals': 'off', // Only used by hashtag.jsx + 'formatjs/no-multiple-whitespaces': 'error', + 'formatjs/no-offset': 'error', + 'formatjs/no-useless-message': 'error', + 'formatjs/prefer-formatted-message': 'error', + 'formatjs/prefer-pound-in-plural': 'error', }, overrides: [ diff --git a/.github/workflows/build-nightly.yml b/.github/workflows/build-nightly.yml new file mode 100644 index 0000000000..501db6e9c6 --- /dev/null +++ b/.github/workflows/build-nightly.yml @@ -0,0 +1,54 @@ +name: Build nightly container image +on: + workflow_dispatch: + schedule: + - cron: '0 2 * * *' # run at 2 AM UTC +permissions: + contents: read + packages: write + +jobs: + build-nightly-image: + runs-on: ubuntu-latest + + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + steps: + - uses: actions/checkout@v3 + - uses: hadolint/hadolint-action@v3.1.0 + - uses: docker/setup-qemu-action@v2 + - uses: docker/setup-buildx-action@v2 + + - name: Log in to the Github Container registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/metadata-action@v4 + id: meta + with: + images: | + ghcr.io/mastodon/mastodon + flavor: | + latest=auto + tags: | + type=raw,value=nightly + type=schedule,pattern=nightly-{{date 'YYYY-MM-DD' tz='Etc/UTC'}} + labels: | + org.opencontainers.image.description=Nightly build image used for testing purposes + + - uses: docker/build-push-action@v4 + with: + context: . + platforms: linux/amd64,linux/arm64 + provenance: false + builder: ${{ steps.buildx.outputs.name }} + push: ${{ github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index d1aa8468ae..6b8d6fdfcd 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -104,7 +104,6 @@ jobs: fail-fast: false matrix: ruby-version: - - '2.7' - '3.0' - '3.1' - '.ruby-version' @@ -136,10 +135,6 @@ jobs: ruby-version: ${{ matrix.ruby-version}} bundler-cache: true - - name: Update system gems - if: matrix.ruby-version == '2.7' - run: gem update --system - - name: Load database schema run: './bin/rails db:create db:schema:load db:seed' diff --git a/.rubocop.yml b/.rubocop.yml index e6a0c2d14e..b510c43031 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -13,7 +13,7 @@ require: - rubocop-capybara AllCops: - TargetRubyVersion: 2.7 # Set to minimum supported version of CI + TargetRubyVersion: 3.0 # Set to minimum supported version of CI DisplayCopNames: true DisplayStyleGuide: true ExtraDetails: true diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index dc7e21dc54..26c89ca780 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.48.1. +# using RuboCop version 1.50.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -132,7 +132,6 @@ Lint/DuplicateBranch: Lint/EmptyBlock: Exclude: - 'spec/controllers/api/v2/search_controller_spec.rb' - - 'spec/controllers/application_controller_spec.rb' - 'spec/fabricators/access_token_fabricator.rb' - 'spec/fabricators/conversation_fabricator.rb' - 'spec/fabricators/system_key_fabricator.rb' @@ -174,11 +173,6 @@ Lint/EmptyClass: Exclude: - 'spec/controllers/api/base_controller_spec.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -Lint/NonDeterministicRequireOrder: - Exclude: - - 'spec/rails_helper.rb' - Lint/NonLocalExitFromIterator: Exclude: - 'app/helpers/jsonld_helper.rb' @@ -251,7 +245,6 @@ Metrics/ModuleLength: - 'app/controllers/concerns/signature_verification.rb' - 'app/helpers/application_helper.rb' - 'app/helpers/jsonld_helper.rb' - - 'app/helpers/statuses_helper.rb' - 'app/models/concerns/account_interactions.rb' - 'app/models/concerns/has_user_settings.rb' @@ -370,6 +363,7 @@ Performance/MethodObjectAsBlock: - 'spec/models/export_spec.rb' # This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowRegexpMatch. Performance/RedundantEqualityComparisonBlock: Exclude: - 'spec/requests/link_headers_spec.rb' @@ -699,7 +693,6 @@ RSpec/HookArgument: RSpec/InstanceVariable: Exclude: - 'spec/controllers/api/v1/streaming_controller_spec.rb' - - 'spec/controllers/application_controller_spec.rb' - 'spec/controllers/auth/confirmations_controller_spec.rb' - 'spec/controllers/auth/passwords_controller_spec.rb' - 'spec/controllers/auth/sessions_controller_spec.rb' @@ -753,7 +746,6 @@ RSpec/LetSetup: - 'spec/controllers/following_accounts_controller_spec.rb' - 'spec/controllers/oauth/authorized_applications_controller_spec.rb' - 'spec/controllers/oauth/tokens_controller_spec.rb' - - 'spec/controllers/tags_controller_spec.rb' - 'spec/lib/activitypub/activity/delete_spec.rb' - 'spec/lib/vacuum/preview_cards_vacuum_spec.rb' - 'spec/models/account_spec.rb' @@ -780,29 +772,6 @@ RSpec/LetSetup: - 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb' - 'spec/workers/scheduler/user_cleanup_scheduler_spec.rb' -# This cop supports safe autocorrection (--autocorrect). -RSpec/MatchArray: - Exclude: - - 'spec/controllers/activitypub/followers_synchronizations_controller_spec.rb' - - 'spec/controllers/admin/export_domain_blocks_controller_spec.rb' - - 'spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb' - - 'spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb' - - 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb' - - 'spec/controllers/api/v1/bookmarks_controller_spec.rb' - - 'spec/controllers/api/v1/favourites_controller_spec.rb' - - 'spec/controllers/api/v1/reports_controller_spec.rb' - - 'spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb' - - 'spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb' - - 'spec/models/account_filter_spec.rb' - - 'spec/models/account_spec.rb' - - 'spec/models/account_statuses_cleanup_policy_spec.rb' - - 'spec/models/custom_emoji_filter_spec.rb' - - 'spec/models/status_spec.rb' - - 'spec/models/user_spec.rb' - - 'spec/presenters/familiar_followers_presenter_spec.rb' - - 'spec/services/activitypub/fetch_featured_collection_service_spec.rb' - - 'spec/services/update_status_service_spec.rb' - RSpec/MessageChain: Exclude: - 'spec/controllers/api/v1/media_controller_spec.rb' @@ -842,7 +811,6 @@ RSpec/MissingExampleGroupArgument: - 'spec/controllers/api/v1/admin/account_actions_controller_spec.rb' - 'spec/controllers/api/v1/admin/domain_allows_controller_spec.rb' - 'spec/controllers/api/v1/statuses_controller_spec.rb' - - 'spec/controllers/application_controller_spec.rb' - 'spec/controllers/auth/registrations_controller_spec.rb' - 'spec/features/log_in_spec.rb' - 'spec/lib/activitypub/activity/undo_spec.rb' @@ -1225,9 +1193,6 @@ Rails/ActiveRecordCallbacksOrder: Rails/ApplicationController: Exclude: - 'app/controllers/health_controller.rb' - - 'app/controllers/well_known/host_meta_controller.rb' - - 'app/controllers/well_known/nodeinfo_controller.rb' - - 'app/controllers/well_known/webfinger_controller.rb' # Configuration parameters: Database, Include. # SupportedDatabases: mysql, postgresql @@ -1405,14 +1370,6 @@ Rails/HasManyOrHasOneDependent: - 'app/models/user.rb' - 'app/models/web/push_subscription.rb' -# Configuration parameters: Include. -# Include: app/helpers/**/*.rb -Rails/HelperInstanceVariable: - Exclude: - - 'app/helpers/application_helper.rb' - - 'app/helpers/instance_helper.rb' - - 'app/helpers/jsonld_helper.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Include. # Include: spec/**/*, test/**/* @@ -1502,15 +1459,6 @@ Rails/RakeEnvironment: - 'lib/tasks/repo.rake' - 'lib/tasks/statistics.rake' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Include. -# Include: spec/controllers/**/*.rb, spec/requests/**/*.rb, test/controllers/**/*.rb, test/integration/**/*.rb -Rails/ResponseParsedBody: - Exclude: - - 'spec/controllers/follower_accounts_controller_spec.rb' - - 'spec/controllers/following_accounts_controller_spec.rb' - - 'spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb' - # Configuration parameters: Include. # Include: db/**/*.rb Rails/ReversibleMigration: @@ -2256,16 +2204,11 @@ Style/MapToHash: # SupportedStyles: literals, strict Style/MutableConstant: Exclude: - - 'app/lib/link_details_extractor.rb' - 'app/models/account.rb' - - 'app/models/custom_emoji.rb' - 'app/models/tag.rb' - - 'app/services/account_search_service.rb' - 'app/services/delete_account_service.rb' - - 'app/services/fetch_link_card_service.rb' - - 'app/services/resolve_url_service.rb' - 'config/initializers/twitter_regex.rb' - - 'lib/mastodon/snowflake.rb' + - 'lib/mastodon/migration_warning.rb' - 'spec/controllers/api/base_controller_spec.rb' # This cop supports safe autocorrection (--autocorrect). @@ -2273,12 +2216,6 @@ Style/NilLambda: Exclude: - 'config/initializers/paperclip.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: MinDigits, Strict, AllowedNumbers, AllowedPatterns. -Style/NumericLiterals: - Exclude: - - 'config/initializers/strong_migrations.rb' - # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? Style/OptionalBooleanParameter: @@ -2388,7 +2325,6 @@ Style/Semicolon: Exclude: - 'spec/services/activitypub/process_status_update_service_spec.rb' - 'spec/validators/blacklisted_email_validator_spec.rb' - - 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. diff --git a/Gemfile b/Gemfile index 3c4ad3555e..3301b83cc0 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ # frozen_string_literal: true source 'https://rubygems.org' -ruby '>= 2.7.0', '< 3.3.0' +ruby '>= 3.0.0' gem 'pkg-config', '~> 1.5' @@ -9,10 +9,10 @@ gem 'puma', '~> 6.2' gem 'rails', '~> 6.1.7' gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 1.2' -gem 'rack', '~> 2.2.6' +gem 'rack', '~> 2.2.7' gem 'haml-rails', '~>2.0' -gem 'pg', '~> 1.4' +gem 'pg', '~> 1.5' gem 'makara', '~> 0.5' gem 'pghero' gem 'dotenv-rails', '~> 2.8' @@ -30,7 +30,10 @@ gem 'browser' gem 'charlock_holmes', '~> 0.7.7' gem 'chewy', '~> 7.3' gem 'devise', '~> 4.9' -gem 'devise-two-factor', '~> 4.0' +# The below `v4.x` branch allows attr_encrypted 4.x, which is required for Rails 7. +# Once a new gem version is pushed, we can go back to released gem and off of github branch. +gem 'devise-two-factor', github: 'tinfoil/devise-two-factor', branch: 'v4.x' +gem 'attr_encrypted', '~> 4.0' group :pam_authentication, optional: true do gem 'devise_pam_authenticatable2', '~> 9.2' @@ -76,7 +79,7 @@ gem 'redcarpet', '~> 3.6' gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'rqrcode', '~> 2.1' -gem 'ruby-progressbar', '~> 1.11' +gem 'ruby-progressbar', '~> 1.13' gem 'sanitize', '~> 6.0' gem 'scenic', '~> 1.7' gem 'sidekiq', '~> 6.5' @@ -121,7 +124,7 @@ group :test do gem 'capybara', '~> 3.39' gem 'climate_control' gem 'faker', '~> 3.2' - gem 'json-schema', '~> 3.0' + gem 'json-schema', '~> 4.0' gem 'rack-test', '~> 2.1' gem 'rails-controller-testing', '~> 1.0' gem 'rspec_junit_formatter', '~> 0.6' diff --git a/Gemfile.lock b/Gemfile.lock index 15fc0cdb11..4395403fa3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -27,6 +27,18 @@ GIT rails-settings-cached (0.6.6) rails (>= 4.2.0) +GIT + remote: https://github.com/tinfoil/devise-two-factor.git + revision: e685f91ce62d036259885fbe31fcb4fa930bcfcb + branch: v4.x + specs: + devise-two-factor (4.0.2) + activesupport (< 7.1) + attr_encrypted (>= 1.3, < 5, != 2) + devise (~> 4.0) + railties (< 7.1) + rotp (~> 6.0) + GEM remote: https://rubygems.org/ specs: @@ -104,12 +116,12 @@ GEM activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) ast (2.4.2) - attr_encrypted (3.1.0) + attr_encrypted (4.0.0) encryptor (~> 3.0.0) attr_required (1.0.1) awrence (1.2.1) aws-eventstream (1.2.0) - aws-partitions (1.743.0) + aws-partitions (1.752.0) aws-sdk-core (3.171.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) @@ -118,7 +130,7 @@ GEM aws-sdk-kms (1.63.0) aws-sdk-core (~> 3, >= 3.165.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.120.1) + aws-sdk-s3 (1.121.0) aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) @@ -142,7 +154,7 @@ GEM blurhash (0.1.7) bootsnap (1.16.0) msgpack (~> 1.2) - brakeman (5.4.0) + brakeman (5.4.1) browser (5.3.1) brpoplpush-redis_script (0.1.3) concurrent-ruby (~> 1.0, >= 1.0.5) @@ -179,7 +191,7 @@ GEM activesupport cbor (0.5.9.6) charlock_holmes (0.7.7) - chewy (7.3.0) + chewy (7.3.2) activesupport (>= 5.2) elasticsearch (>= 7.12.0, < 7.14.0) elasticsearch-dsl @@ -189,7 +201,7 @@ GEM coderay (1.1.3) color_diff (0.1) concurrent-ruby (1.2.2) - connection_pool (2.3.0) + connection_pool (2.4.0) cose (1.3.0) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) @@ -206,12 +218,6 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-two-factor (4.0.2) - activesupport (< 7.1) - attr_encrypted (>= 1.3, < 4, != 2) - devise (~> 4.0) - railties (< 7.1) - rotp (~> 6.0) devise_pam_authenticatable2 (9.2.0) devise (>= 4.0.0) rpam2 (~> 4.0) @@ -241,7 +247,7 @@ GEM erubi (1.12.0) et-orbi (1.2.7) tzinfo - excon (0.97.1) + excon (0.99.0) fabrication (2.30.0) faker (3.2.0) i18n (>= 1.8.11, < 2) @@ -364,7 +370,7 @@ GEM json-ld-preloaded (3.2.2) json-ld (~> 3.2) rdf (~> 3.2) - json-schema (3.0.0) + json-schema (4.0.0) addressable (>= 2.8) jsonapi-renderer (0.2.2) jwt (2.7.0) @@ -416,11 +422,11 @@ GEM method_source (1.0.0) mime-types (3.4.1) mime-types-data (~> 3.2015) - mime-types-data (3.2022.0105) + mime-types-data (3.2023.0218.1) mini_mime (1.1.2) mini_portile2 (2.8.1) minitest (5.18.0) - msgpack (1.6.0) + msgpack (1.7.0) multi_json (1.15.0) multipart-post (2.3.0) net-http (0.3.2) @@ -437,7 +443,7 @@ GEM net-ssh (>= 2.6.5, < 8.0.0) net-smtp (0.3.3) net-protocol - net-ssh (7.0.1) + net-ssh (7.1.0) nio4r (2.5.9) nokogiri (1.14.3) mini_portile2 (~> 2.8.0) @@ -480,18 +486,18 @@ GEM openssl (> 2.0) orm_adapter (0.5.0) ox (2.14.16) - parallel (1.22.1) - parser (3.2.2.0) + parallel (1.23.0) + parser (3.2.2.1) ast (~> 2.4.1) parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.4.6) - pghero (3.3.2) + pg (1.5.2) + pghero (3.3.3) activerecord (>= 6) pkg-config (1.5.1) posix-spawn (0.3.15) - premailer (1.18.0) + premailer (1.21.0) addressable css_parser (>= 1.12.0) htmlentities (>= 4.0.0) @@ -501,13 +507,13 @@ GEM premailer (~> 1.7, >= 1.7.9) private_address_check (0.5.0) public_suffix (5.0.1) - puma (6.2.1) + puma (6.2.2) nio4r (~> 2.0) pundit (2.3.0) activesupport (>= 3.0.0) raabro (1.4.0) racc (1.6.2) - rack (2.2.6.4) + rack (2.2.7) rack-attack (6.6.1) rack (>= 1.0, < 3) rack-cors (2.0.1) @@ -567,7 +573,7 @@ GEM redis (>= 4) redlock (1.3.2) redis (>= 3.0.0, < 6.0) - regexp_parser (2.7.0) + regexp_parser (2.8.0) request_store (1.5.1) rack (>= 1.4) responders (3.1.0) @@ -580,12 +586,12 @@ GEM chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) - rspec-core (3.12.1) + rspec-core (3.12.2) rspec-support (~> 3.12.0) - rspec-expectations (3.12.2) + rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-mocks (3.12.3) + rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-rails (6.0.1) @@ -603,7 +609,7 @@ GEM rspec_chunked (0.6) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.49.0) + rubocop (1.50.2) json (~> 2.3) parallel (~> 1.10) parser (>= 3.2.0.0) @@ -615,7 +621,7 @@ GEM unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.28.0) parser (>= 3.2.1.0) - rubocop-capybara (2.17.1) + rubocop-capybara (2.18.0) rubocop (~> 1.41) rubocop-performance (1.17.1) rubocop (>= 1.7.0, < 2.0) @@ -771,6 +777,7 @@ DEPENDENCIES active_model_serializers (~> 0.10) addressable (~> 2.8) annotate (~> 3.2) + attr_encrypted (~> 4.0) aws-sdk-s3 (~> 1.120) better_errors (~> 2.9) binding_of_caller (~> 1.0) @@ -792,7 +799,7 @@ DEPENDENCIES concurrent-ruby connection_pool devise (~> 4.9) - devise-two-factor (~> 4.0) + devise-two-factor! devise_pam_authenticatable2 (~> 9.2) discard (~> 1.2) doorkeeper (~> 5.6) @@ -817,7 +824,7 @@ DEPENDENCIES idn-ruby json-ld json-ld-preloaded (~> 3.2) - json-schema (~> 3.0) + json-schema (~> 4.0) kaminari (~> 1.2) kt-paperclip (~> 7.1)! letter_opener (~> 1.8) @@ -840,7 +847,7 @@ DEPENDENCIES omniauth_openid_connect (~> 0.6.1) ox (~> 2.14) parslet - pg (~> 1.4) + pg (~> 1.5) pghero pkg-config (~> 1.5) posix-spawn @@ -849,7 +856,7 @@ DEPENDENCIES public_suffix (~> 5.0) puma (~> 6.2) pundit (~> 2.3) - rack (~> 2.2.6) + rack (~> 2.2.7) rack-attack (~> 6.6) rack-cors (~> 2.0) rack-test (~> 2.1) @@ -871,7 +878,7 @@ DEPENDENCIES rubocop-performance rubocop-rails rubocop-rspec - ruby-progressbar (~> 1.11) + ruby-progressbar (~> 1.13) sanitize (~> 6.0) scenic (~> 1.7) sidekiq (~> 6.5) diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 1043486140..c4b7e9c9d2 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -8,7 +8,7 @@ class AboutController < ApplicationController before_action :set_instance_presenter def show - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? end private diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 4d03a04b77..929bb54aa7 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -7,8 +7,9 @@ class AccountsController < ApplicationController include AccountControllerConcern include SignatureAuthentication + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } - before_action :set_cache_headers skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) } skip_before_action :require_functional!, unless: :whitelist_mode? @@ -16,7 +17,7 @@ class AccountsController < ApplicationController def show respond_to do |format| format.html do - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? @rss_url = rss_url end diff --git a/app/controllers/activitypub/base_controller.rb b/app/controllers/activitypub/base_controller.rb index b8a7e0ab96..388d4b9e1d 100644 --- a/app/controllers/activitypub/base_controller.rb +++ b/app/controllers/activitypub/base_controller.rb @@ -7,10 +7,6 @@ class ActivityPub::BaseController < Api::BaseController private - def set_cache_headers - response.headers['Vary'] = 'Signature' if authorized_fetch_mode? - end - def skip_temporary_suspension_response? false end diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index 23d8740711..4ed59388ff 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -4,11 +4,12 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController include SignatureVerification include AccountOwnedConcern + vary_by -> { 'Signature' if authorized_fetch_mode? } + before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :set_items before_action :set_size before_action :set_type - before_action :set_cache_headers def show expires_in 3.minutes, public: public_fetch_mode? diff --git a/app/controllers/activitypub/followers_synchronizations_controller.rb b/app/controllers/activitypub/followers_synchronizations_controller.rb index 4e445bcb1f..976caa3445 100644 --- a/app/controllers/activitypub/followers_synchronizations_controller.rb +++ b/app/controllers/activitypub/followers_synchronizations_controller.rb @@ -4,9 +4,10 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro include SignatureVerification include AccountOwnedConcern + vary_by -> { 'Signature' if authorized_fetch_mode? } + before_action :require_account_signature! before_action :set_items - before_action :set_cache_headers def show expires_in 0, public: false diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 60d201f763..bf10ba762a 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -6,9 +6,10 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController include SignatureVerification include AccountOwnedConcern + vary_by -> { 'Signature' if authorized_fetch_mode? || page_requested? } + before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :set_statuses - before_action :set_cache_headers def show if page_requested? @@ -16,6 +17,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController else expires_in(3.minutes, public: public_fetch_mode?) end + render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end @@ -80,8 +82,4 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController def set_account @account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative end - - def set_cache_headers - response.headers['Vary'] = 'Signature' if authorized_fetch_mode? || page_requested? - end end diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index 8e0f9de2ee..c38ff89d1c 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -7,9 +7,10 @@ class ActivityPub::RepliesController < ActivityPub::BaseController DESCENDANTS_LIMIT = 60 + vary_by -> { 'Signature' if authorized_fetch_mode? } + before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :set_status - before_action :set_cache_headers before_action :set_replies def index diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index c645ce12bb..a71bb61298 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -9,6 +9,8 @@ module Admin before_action :set_pack before_action :set_body_classes + before_action :set_cache_headers + after_action :verify_authorized private @@ -21,6 +23,10 @@ module Admin use_pack 'admin' end + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end + def set_user @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) end diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 41f3ce2ee3..2629ab782f 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -6,13 +6,14 @@ class Api::BaseController < ApplicationController include RateLimitHeaders include AccessTokenTrackingConcern + include ApiCachingConcern - skip_before_action :store_current_location skip_before_action :require_functional!, unless: :whitelist_mode? before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access? before_action :require_not_suspended! - before_action :set_cache_headers + + vary_by 'Authorization' protect_from_forgery with: :null_session @@ -148,10 +149,6 @@ class Api::BaseController < ApplicationController doorkeeper_authorize!(*scopes) if doorkeeper_token end - def set_cache_headers - response.headers['Cache-Control'] = 'private, no-store' - end - def disallow_unauthenticated_api_access? ENV['DISALLOW_UNAUTHENTICATED_API_ACCESS'] == 'true' || Rails.configuration.x.whitelist_mode end diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index 68952de893..1a996d362a 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -6,6 +6,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController after_action :insert_pagination_headers def index + cache_if_unauthenticated! @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer end diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 0a4d2ae7b3..6e6ebae43b 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -6,6 +6,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController after_action :insert_pagination_headers def index + cache_if_unauthenticated! @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer end diff --git a/app/controllers/api/v1/accounts/lookup_controller.rb b/app/controllers/api/v1/accounts/lookup_controller.rb index 8597f891d6..6d63398781 100644 --- a/app/controllers/api/v1/accounts/lookup_controller.rb +++ b/app/controllers/api/v1/accounts/lookup_controller.rb @@ -5,6 +5,7 @@ class Api::V1::Accounts::LookupController < Api::BaseController before_action :set_account def show + cache_if_unauthenticated! render json: @account, serializer: REST::AccountSerializer end diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 7ed48cf658..51f541bd23 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -7,6 +7,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) } def index + cache_if_unauthenticated! @statuses = load_statuses render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 7dff66efac..8af4242ba3 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -18,6 +18,7 @@ class Api::V1::AccountsController < Api::BaseController override_rate_limit_headers :follow, family: :follows def show + cache_if_unauthenticated! render json: @account, serializer: REST::AccountSerializer end diff --git a/app/controllers/api/v1/custom_emojis_controller.rb b/app/controllers/api/v1/custom_emojis_controller.rb index 78b791a2a2..a29c4c1c65 100644 --- a/app/controllers/api/v1/custom_emojis_controller.rb +++ b/app/controllers/api/v1/custom_emojis_controller.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true class Api::V1::CustomEmojisController < Api::BaseController + vary_by '', unless: :disallow_unauthenticated_api_access? skip_before_action :set_cache_headers skip_before_action :require_authenticated_user!, unless: :whitelist_mode? def index - expires_in 3.minutes, public: true + cache_even_if_authenticated! unless disallow_unauthenticated_api_access? render_with_cache(each_serializer: REST::CustomEmojiSerializer) { CustomEmoji.listed.includes(:category) } end end diff --git a/app/controllers/api/v1/directories_controller.rb b/app/controllers/api/v1/directories_controller.rb index c91543e3a3..c0585e8599 100644 --- a/app/controllers/api/v1/directories_controller.rb +++ b/app/controllers/api/v1/directories_controller.rb @@ -5,6 +5,7 @@ class Api::V1::DirectoriesController < Api::BaseController before_action :set_accounts def show + cache_if_unauthenticated! render json: @accounts, each_serializer: REST::AccountSerializer end diff --git a/app/controllers/api/v1/instances/activity_controller.rb b/app/controllers/api/v1/instances/activity_controller.rb index bad61425a5..3d55d990af 100644 --- a/app/controllers/api/v1/instances/activity_controller.rb +++ b/app/controllers/api/v1/instances/activity_controller.rb @@ -3,11 +3,12 @@ class Api::V1::Instances::ActivityController < Api::BaseController before_action :require_enabled_api! - skip_before_action :set_cache_headers skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + vary_by '' + def show - expires_in 1.day, public: true + cache_even_if_authenticated! render_with_cache json: :activity, expires_in: 1.day end diff --git a/app/controllers/api/v1/instances/domain_blocks_controller.rb b/app/controllers/api/v1/instances/domain_blocks_controller.rb index 37a6906fb6..e954c45897 100644 --- a/app/controllers/api/v1/instances/domain_blocks_controller.rb +++ b/app/controllers/api/v1/instances/domain_blocks_controller.rb @@ -6,8 +6,15 @@ class Api::V1::Instances::DomainBlocksController < Api::BaseController before_action :require_enabled_api! before_action :set_domain_blocks + vary_by '', if: -> { Setting.show_domain_blocks == 'all' } + def index - expires_in 3.minutes, public: true + if Setting.show_domain_blocks == 'all' + cache_even_if_authenticated! + else + cache_if_unauthenticated! + end + render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: (Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)) end diff --git a/app/controllers/api/v1/instances/extended_descriptions_controller.rb b/app/controllers/api/v1/instances/extended_descriptions_controller.rb index c72e16cff2..a0665725bd 100644 --- a/app/controllers/api/v1/instances/extended_descriptions_controller.rb +++ b/app/controllers/api/v1/instances/extended_descriptions_controller.rb @@ -2,11 +2,19 @@ class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_around_action :set_locale before_action :set_extended_description + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode + def current_user + super if whitelist_mode? + end + def show - expires_in 3.minutes, public: true + cache_even_if_authenticated! render json: @extended_description, serializer: REST::ExtendedDescriptionSerializer end diff --git a/app/controllers/api/v1/instances/peers_controller.rb b/app/controllers/api/v1/instances/peers_controller.rb index 2877fec52d..70281362a8 100644 --- a/app/controllers/api/v1/instances/peers_controller.rb +++ b/app/controllers/api/v1/instances/peers_controller.rb @@ -3,11 +3,18 @@ class Api::V1::Instances::PeersController < Api::BaseController before_action :require_enabled_api! - skip_before_action :set_cache_headers skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_around_action :set_locale + + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode + def current_user + super if whitelist_mode? + end def index - expires_in 1.day, public: true + cache_even_if_authenticated! render_with_cache(expires_in: 1.day) { Instance.where.not(domain: DomainBlock.select(:domain)).pluck(:domain) } end diff --git a/app/controllers/api/v1/instances/privacy_policies_controller.rb b/app/controllers/api/v1/instances/privacy_policies_controller.rb index dbd69f54d4..36889f7335 100644 --- a/app/controllers/api/v1/instances/privacy_policies_controller.rb +++ b/app/controllers/api/v1/instances/privacy_policies_controller.rb @@ -5,8 +5,10 @@ class Api::V1::Instances::PrivacyPoliciesController < Api::BaseController before_action :set_privacy_policy + vary_by '' + def show - expires_in 1.day, public: true + cache_even_if_authenticated! render json: @privacy_policy, serializer: REST::PrivacyPolicySerializer end diff --git a/app/controllers/api/v1/instances/rules_controller.rb b/app/controllers/api/v1/instances/rules_controller.rb index 93cf3c7594..d3eeca3262 100644 --- a/app/controllers/api/v1/instances/rules_controller.rb +++ b/app/controllers/api/v1/instances/rules_controller.rb @@ -2,10 +2,19 @@ class Api::V1::Instances::RulesController < Api::BaseController skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_around_action :set_locale before_action :set_rules + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode + def current_user + super if whitelist_mode? + end + def index + cache_even_if_authenticated! render json: @rules, each_serializer: REST::RuleSerializer end diff --git a/app/controllers/api/v1/instances/translation_languages_controller.rb b/app/controllers/api/v1/instances/translation_languages_controller.rb index 3910a499e8..c4680cccb8 100644 --- a/app/controllers/api/v1/instances/translation_languages_controller.rb +++ b/app/controllers/api/v1/instances/translation_languages_controller.rb @@ -5,8 +5,10 @@ class Api::V1::Instances::TranslationLanguagesController < Api::BaseController before_action :set_languages + vary_by '' + def show - expires_in 1.day, public: true + cache_even_if_authenticated! render json: @languages end diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index 913319a869..5a6701ff96 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -1,11 +1,18 @@ # frozen_string_literal: true class Api::V1::InstancesController < Api::BaseController - skip_before_action :set_cache_headers skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_around_action :set_locale + + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode + def current_user + super if whitelist_mode? + end def show - expires_in 3.minutes, public: true + cache_even_if_authenticated! render_with_cache json: InstancePresenter.new, serializer: REST::V1::InstanceSerializer, root: 'instance' end end diff --git a/app/controllers/api/v1/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb index 6435e9f0dc..ffc70a8496 100644 --- a/app/controllers/api/v1/polls_controller.rb +++ b/app/controllers/api/v1/polls_controller.rb @@ -8,6 +8,7 @@ class Api::V1::PollsController < Api::BaseController before_action :refresh_poll def show + cache_if_unauthenticated! render json: @poll, serializer: REST::PollSerializer, include_results: true end 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 b138fa2650..73eb11e711 100644 --- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb @@ -8,6 +8,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController after_action :insert_pagination_headers def index + cache_if_unauthenticated! @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer end diff --git a/app/controllers/api/v1/statuses/histories_controller.rb b/app/controllers/api/v1/statuses/histories_controller.rb index 7fe73a6f54..dff2425d06 100644 --- a/app/controllers/api/v1/statuses/histories_controller.rb +++ b/app/controllers/api/v1/statuses/histories_controller.rb @@ -7,6 +7,7 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController before_action :set_status def show + cache_if_unauthenticated! render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer end 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 4b545f9826..41672e7539 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -8,6 +8,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController after_action :insert_pagination_headers def index + cache_if_unauthenticated! @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 8dcf6331ed..960f8cf765 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -24,11 +24,14 @@ class Api::V1::StatusesController < Api::BaseController DESCENDANTS_DEPTH_LIMIT = 20 def show + cache_if_unauthenticated! @status = cache_collection([@status], Status).first render json: @status, serializer: REST::StatusSerializer end def context + cache_if_unauthenticated! + ancestors_limit = CONTEXT_LIMIT descendants_limit = CONTEXT_LIMIT descendants_depth_limit = nil diff --git a/app/controllers/api/v1/tags_controller.rb b/app/controllers/api/v1/tags_controller.rb index a08fd21877..284ec85937 100644 --- a/app/controllers/api/v1/tags_controller.rb +++ b/app/controllers/api/v1/tags_controller.rb @@ -8,6 +8,7 @@ class Api::V1::TagsController < Api::BaseController override_rate_limit_headers :follow, family: :follows def show + cache_if_unauthenticated! render json: @tag, serializer: REST::TagSerializer end diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index 4675af9214..6af504ff63 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -5,6 +5,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController after_action :insert_pagination_headers, unless: -> { @statuses.empty? } def show + cache_if_unauthenticated! @statuses = load_statuses render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index 64a1db58df..9cd7b99046 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -5,6 +5,7 @@ class Api::V1::Timelines::TagController < Api::BaseController after_action :insert_pagination_headers, unless: -> { @statuses.empty? } def show + cache_if_unauthenticated! @statuses = load_statuses render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end diff --git a/app/controllers/api/v1/trends/links_controller.rb b/app/controllers/api/v1/trends/links_controller.rb index 3ce20fb786..57cfa0b7e4 100644 --- a/app/controllers/api/v1/trends/links_controller.rb +++ b/app/controllers/api/v1/trends/links_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Api::V1::Trends::LinksController < Api::BaseController + vary_by 'Authorization, Accept-Language' + before_action :set_links after_action :insert_pagination_headers @@ -8,6 +10,7 @@ class Api::V1::Trends::LinksController < Api::BaseController DEFAULT_LINKS_LIMIT = 10 def index + cache_if_unauthenticated! render json: @links, each_serializer: REST::Trends::LinkSerializer end diff --git a/app/controllers/api/v1/trends/statuses_controller.rb b/app/controllers/api/v1/trends/statuses_controller.rb index deebb6f785..5485c450d0 100644 --- a/app/controllers/api/v1/trends/statuses_controller.rb +++ b/app/controllers/api/v1/trends/statuses_controller.rb @@ -1,12 +1,15 @@ # frozen_string_literal: true class Api::V1::Trends::StatusesController < Api::BaseController + vary_by 'Authorization, Accept-Language' + before_action :require_user!, only: [:index], if: :require_auth? before_action :set_statuses after_action :insert_pagination_headers def index + cache_if_unauthenticated! render json: @statuses, each_serializer: REST::StatusSerializer end diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb index 9dd9abdfe0..6cc8194def 100644 --- a/app/controllers/api/v1/trends/tags_controller.rb +++ b/app/controllers/api/v1/trends/tags_controller.rb @@ -8,6 +8,7 @@ class Api::V1::Trends::TagsController < Api::BaseController DEFAULT_TAGS_LIMIT = (ENV['MAX_TRENDING_TAGS'] || 10).to_i def index + cache_if_unauthenticated! render json: @tags, each_serializer: REST::TagSerializer, relationships: TagRelationshipsPresenter.new(@tags, current_user&.account_id) end diff --git a/app/controllers/api/v2/instances_controller.rb b/app/controllers/api/v2/instances_controller.rb index bcd90cff22..8346e28830 100644 --- a/app/controllers/api/v2/instances_controller.rb +++ b/app/controllers/api/v2/instances_controller.rb @@ -2,7 +2,7 @@ class Api::V2::InstancesController < Api::V1::InstancesController def show - expires_in 3.minutes, public: true + cache_even_if_authenticated! render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance' end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 906761f6f7..7c09040fbf 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -21,6 +21,8 @@ class ApplicationController < ActionController::Base helper_method :omniauth_only? helper_method :sso_account_settings helper_method :whitelist_mode? + helper_method :body_class_string + helper_method :skip_csrf_meta_tags? rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request rescue_from Mastodon::NotPermittedError, with: :forbidden @@ -37,9 +39,11 @@ class ApplicationController < ActionController::Base service_unavailable end - before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? + before_action :store_referrer, except: :raise_not_found, if: :devise_controller? before_action :require_functional!, if: :user_signed_in? + before_action :set_cache_control_defaults + skip_before_action :verify_authenticity_token, only: :raise_not_found def raise_not_found @@ -56,14 +60,25 @@ class ApplicationController < ActionController::Base !authorized_fetch_mode? end - def store_current_location - store_location_for(:user, request.url) unless [:json, :rss].include?(request.format&.to_sym) + def store_referrer + return if request.referer.blank? + + redirect_uri = URI(request.referer) + return if redirect_uri.path.start_with?('/auth') + + stored_url = redirect_uri.to_s if redirect_uri.host == request.host && redirect_uri.port == request.port + + store_location_for(:user, stored_url) end def require_functional! redirect_to edit_user_registration_path unless current_user.functional? end + def skip_csrf_meta_tags? + false + end + def after_sign_out_path_for(_resource_or_scope) if ENV['OMNIAUTH_ONLY'] == 'true' && ENV['OIDC_ENABLED'] == 'true' '/auth/auth/openid_connect/logout' @@ -127,7 +142,7 @@ class ApplicationController < ActionController::Base end def sso_account_settings - ENV.fetch('SSO_ACCOUNT_SETTINGS') + ENV.fetch('SSO_ACCOUNT_SETTINGS', nil) end def current_account @@ -142,6 +157,10 @@ class ApplicationController < ActionController::Base @current_session = SessionActivation.find_by(session_id: cookies.signed['_session_id']) if cookies.signed['_session_id'].present? end + def body_class_string + @body_classes || '' + end + def respond_with_error(code) respond_to do |format| format.any do @@ -151,4 +170,8 @@ class ApplicationController < ActionController::Base format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code } end end + + def set_cache_control_defaults + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index d2f1bea938..2322de465a 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -157,6 +157,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def set_cache_headers - response.headers['Cache-Control'] = 'private, no-store' + response.cache_control.replace(private: true, no_store: true) end end diff --git a/app/controllers/concerns/api_caching_concern.rb b/app/controllers/concerns/api_caching_concern.rb new file mode 100644 index 0000000000..705abce80f --- /dev/null +++ b/app/controllers/concerns/api_caching_concern.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module ApiCachingConcern + extend ActiveSupport::Concern + + def cache_if_unauthenticated! + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? + end + + def cache_even_if_authenticated! + expires_in(5.minutes, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless whitelist_mode? + end +end diff --git a/app/controllers/concerns/cache_concern.rb b/app/controllers/concerns/cache_concern.rb index a5a9ba3e1f..55ebe1bd64 100644 --- a/app/controllers/concerns/cache_concern.rb +++ b/app/controllers/concerns/cache_concern.rb @@ -155,8 +155,30 @@ module CacheConcern end end + class_methods do + def vary_by(value, **kwargs) + before_action(**kwargs) do |controller| + response.headers['Vary'] = value.respond_to?(:call) ? controller.instance_exec(&value) : value + end + end + end + + included do + after_action :enforce_cache_control! + end + + # Prevents high-entropy headers such as `Cookie`, `Signature` or `Authorization` + # from being used as cache keys, while allowing to `Vary` on them (to not serve + # anonymous cached data to authenticated requests when authentication matters) + def enforce_cache_control! + vary = response.headers['Vary']&.split&.map { |x| x.strip.downcase } + return unless vary.present? && %w(cookie authorization signature).any? { |header| vary.include?(header) && request.headers[header].present? } + + response.cache_control.replace(private: true, no_store: true) + end + def render_with_cache(**options) - raise ArgumentError, 'only JSON render calls are supported' unless options.key?(:json) || block_given? + raise ArgumentError, 'Only JSON render calls are supported' unless options.key?(:json) || block_given? key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields].nil? ? nil : options[:fields].join(',')].compact.join(':') expires_in = options.delete(:expires_in) || 3.minutes @@ -176,10 +198,6 @@ module CacheConcern end end - def set_cache_headers - response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature' - end - def cache_collection(raw, klass) return raw unless klass.respond_to?(:with_includes) diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index 7ba7a57e3d..96c31566e0 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -7,6 +7,12 @@ module WebAppControllerConcern prepend_before_action :redirect_unauthenticated_to_permalinks! before_action :set_pack before_action :set_app_body_class + + vary_by 'Accept, Accept-Language, Cookie' + end + + def skip_csrf_meta_tags? + current_user.nil? end def set_app_body_class diff --git a/app/controllers/custom_css_controller.rb b/app/controllers/custom_css_controller.rb index 9270c467dc..e7a02ea89c 100644 --- a/app/controllers/custom_css_controller.rb +++ b/app/controllers/custom_css_controller.rb @@ -1,18 +1,8 @@ # frozen_string_literal: true -class CustomCssController < ApplicationController - skip_before_action :store_current_location - skip_before_action :require_functional! - skip_before_action :update_user_sign_in - skip_before_action :set_session_activity - - skip_around_action :set_locale - - before_action :set_cache_headers - +class CustomCssController < ActionController::Base # rubocop:disable Rails/ApplicationController def show expires_in 3.minutes, public: true - request.session_options[:skip] = true render content_type: 'text/css' end end diff --git a/app/controllers/disputes/base_controller.rb b/app/controllers/disputes/base_controller.rb index 7830c55247..f51f44c620 100644 --- a/app/controllers/disputes/base_controller.rb +++ b/app/controllers/disputes/base_controller.rb @@ -10,6 +10,7 @@ class Disputes::BaseController < ApplicationController before_action :set_body_classes before_action :authenticate_user! before_action :set_pack + before_action :set_cache_headers private @@ -20,4 +21,8 @@ class Disputes::BaseController < ApplicationController def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb index 41f1e1c5ca..72bc56de04 100644 --- a/app/controllers/emojis_controller.rb +++ b/app/controllers/emojis_controller.rb @@ -2,15 +2,12 @@ class EmojisController < ApplicationController before_action :set_emoji - before_action :set_cache_headers + + vary_by -> { 'Signature' if authorized_fetch_mode? } def show - respond_to do |format| - format.json do - expires_in 3.minutes, public: true - render_with_cache json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter - end - end + expires_in 3.minutes, public: true + render_with_cache json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter end private diff --git a/app/controllers/filters/statuses_controller.rb b/app/controllers/filters/statuses_controller.rb index 86d11fcb93..97206c7eda 100644 --- a/app/controllers/filters/statuses_controller.rb +++ b/app/controllers/filters/statuses_controller.rb @@ -8,6 +8,7 @@ class Filters::StatusesController < ApplicationController before_action :set_status_filters before_action :set_pack before_action :set_body_classes + before_action :set_cache_headers PER_PAGE = 20 @@ -49,4 +50,8 @@ class Filters::StatusesController < ApplicationController def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb index 2ab3b0a744..203241aff1 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -7,6 +7,7 @@ class FiltersController < ApplicationController before_action :set_filter, only: [:edit, :update, :destroy] before_action :set_pack before_action :set_body_classes + before_action :set_cache_headers def index @filters = current_account.custom_filters.includes(:keywords, :statuses).order(:phrase) @@ -59,4 +60,8 @@ class FiltersController < ApplicationController def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 1f5ed30de9..2e55cf6c34 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -5,8 +5,9 @@ class FollowerAccountsController < ApplicationController include SignatureVerification include WebAppControllerConcern + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } - before_action :set_cache_headers skip_around_action :set_locale, if: -> { request.format == :json } skip_before_action :require_functional!, unless: :whitelist_mode? @@ -14,7 +15,7 @@ class FollowerAccountsController < ApplicationController def index respond_to do |format| format.html do - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? end format.json do diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index febd13c975..2aa31bdf08 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -5,8 +5,9 @@ class FollowingAccountsController < ApplicationController include SignatureVerification include WebAppControllerConcern + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } - before_action :set_cache_headers skip_around_action :set_locale, if: -> { request.format == :json } skip_before_action :require_functional!, unless: :whitelist_mode? @@ -14,7 +15,7 @@ class FollowingAccountsController < ApplicationController def index respond_to do |format| format.html do - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? end format.json do diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index d8ee82a7a2..ee940e6707 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -6,7 +6,7 @@ class HomeController < ApplicationController before_action :set_instance_presenter def index - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? end private diff --git a/app/controllers/instance_actors_controller.rb b/app/controllers/instance_actors_controller.rb index 0853897f20..8422d74bc3 100644 --- a/app/controllers/instance_actors_controller.rb +++ b/app/controllers/instance_actors_controller.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true -class InstanceActorsController < ApplicationController - include AccountControllerConcern +class InstanceActorsController < ActivityPub::BaseController + vary_by '' - skip_before_action :check_account_confirmation - skip_around_action :set_locale + serialization_scope nil + + before_action :set_account + skip_before_action :require_functional! + skip_before_action :update_user_sign_in def show expires_in 10.minutes, public: true diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 0b3c082dce..2db4bc5cbd 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -8,6 +8,7 @@ class InvitesController < ApplicationController before_action :authenticate_user! before_action :set_pack before_action :set_body_classes + before_action :set_cache_headers def index authorize :invite, :create? @@ -54,4 +55,8 @@ class InvitesController < ApplicationController def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/manifests_controller.rb b/app/controllers/manifests_controller.rb index 960510f601..4fba9198f3 100644 --- a/app/controllers/manifests_controller.rb +++ b/app/controllers/manifests_controller.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true -class ManifestsController < ApplicationController - skip_before_action :store_current_location - skip_before_action :require_functional! +class ManifestsController < ActionController::Base # rubocop:disable Rails/ApplicationController + # Prevent `active_model_serializer`'s `ActionController::Serialization` from calling `current_user` + # and thus re-issuing session cookies + serialization_scope nil def show expires_in 3.minutes, public: true diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index 37c5dcb996..41e20add62 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -3,7 +3,6 @@ class MediaController < ApplicationController include Authorization - skip_before_action :store_current_location skip_before_action :require_functional!, unless: :whitelist_mode? before_action :authenticate_user!, if: :whitelist_mode? diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index f29b69a24a..1b5486c122 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -6,7 +6,6 @@ class MediaProxyController < ApplicationController include Redisable include Lockable - skip_before_action :store_current_location skip_before_action :require_functional! before_action :authenticate_user!, if: :whitelist_mode? diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index d6e7d0800c..62fc9c1b0d 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -39,6 +39,6 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController end def set_cache_headers - response.headers['Cache-Control'] = 'private, no-store' + response.cache_control.replace(private: true, no_store: true) end end diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index b2564a7915..efae7e35f8 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -8,6 +8,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio before_action :set_pack before_action :require_not_suspended!, only: :destroy before_action :set_body_classes + before_action :set_cache_headers skip_before_action :require_functional! @@ -35,4 +36,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio def require_not_suspended! forbidden if current_account.suspended? end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/privacy_controller.rb b/app/controllers/privacy_controller.rb index 2c98bf3bf4..070ee8a06a 100644 --- a/app/controllers/privacy_controller.rb +++ b/app/controllers/privacy_controller.rb @@ -8,7 +8,7 @@ class PrivacyController < ApplicationController before_action :set_instance_presenter def show - expires_in 0, public: true if current_account.nil? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? end private diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index 52cf1e0c18..f83098f731 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -8,6 +8,7 @@ class RelationshipsController < ApplicationController before_action :set_pack before_action :set_relationships, only: :show before_action :set_body_classes + before_action :set_cache_headers helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship? @@ -75,4 +76,8 @@ class RelationshipsController < ApplicationController def set_pack use_pack 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb index bf17b918cc..56aeb49aa0 100644 --- a/app/controllers/settings/base_controller.rb +++ b/app/controllers/settings/base_controller.rb @@ -19,7 +19,7 @@ class Settings::BaseController < ApplicationController end def set_cache_headers - response.headers['Cache-Control'] = 'private, no-store' + response.cache_control.replace(private: true, no_store: true) end def require_not_suspended! diff --git a/app/controllers/statuses_cleanup_controller.rb b/app/controllers/statuses_cleanup_controller.rb index 0e7bb835f5..3ed1860a00 100644 --- a/app/controllers/statuses_cleanup_controller.rb +++ b/app/controllers/statuses_cleanup_controller.rb @@ -7,6 +7,7 @@ class StatusesCleanupController < ApplicationController before_action :set_policy before_action :set_body_classes before_action :set_pack + before_action :set_cache_headers def show; end @@ -41,4 +42,8 @@ class StatusesCleanupController < ApplicationController def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 15c0812641..5758085988 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -6,11 +6,12 @@ class StatusesController < ApplicationController include Authorization include AccountOwnedConcern + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_status before_action :set_instance_presenter before_action :redirect_to_original, only: :show - before_action :set_cache_headers before_action :set_body_classes, only: :embed after_action :set_link_headers @@ -29,7 +30,7 @@ class StatusesController < ApplicationController end format.json do - expires_in 3.minutes, public: @status.distributable? && public_fetch_mode? + expires_in 3.minutes, public: true if @status.distributable? && public_fetch_mode? render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter end end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 4b747c9add..7e249dbea5 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -7,6 +7,8 @@ class TagsController < ApplicationController PAGE_SIZE = 20 PAGE_SIZE_MAX = 200 + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :authenticate_user!, if: :whitelist_mode? before_action :set_local @@ -19,7 +21,7 @@ class TagsController < ApplicationController def show respond_to do |format| format.html do - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? end format.rss do diff --git a/app/controllers/well_known/host_meta_controller.rb b/app/controllers/well_known/host_meta_controller.rb index 2fd6bc7cc9..201da9fbc3 100644 --- a/app/controllers/well_known/host_meta_controller.rb +++ b/app/controllers/well_known/host_meta_controller.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true module WellKnown - class HostMetaController < ActionController::Base + class HostMetaController < ActionController::Base # rubocop:disable Rails/ApplicationController include RoutingHelper - before_action { response.headers['Vary'] = 'Accept' } - def show @webfinger_template = "#{webfinger_url}?resource={uri}" expires_in 3.days, public: true diff --git a/app/controllers/well_known/nodeinfo_controller.rb b/app/controllers/well_known/nodeinfo_controller.rb index 11a699ebc8..e20e8c62a0 100644 --- a/app/controllers/well_known/nodeinfo_controller.rb +++ b/app/controllers/well_known/nodeinfo_controller.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true module WellKnown - class NodeInfoController < ActionController::Base + class NodeInfoController < ActionController::Base # rubocop:disable Rails/ApplicationController include CacheConcern - before_action { response.headers['Vary'] = 'Accept' } + # Prevent `active_model_serializer`'s `ActionController::Serialization` from calling `current_user` + # and thus re-issuing session cookies + serialization_scope nil def index expires_in 3.days, public: true diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb index 2b296ea3be..a06253f456 100644 --- a/app/controllers/well_known/webfinger_controller.rb +++ b/app/controllers/well_known/webfinger_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module WellKnown - class WebfingerController < ActionController::Base + class WebfingerController < ActionController::Base # rubocop:disable Rails/ApplicationController include RoutingHelper before_action :set_account @@ -34,7 +34,12 @@ module WellKnown end def check_account_suspension - expires_in(3.minutes, public: true) && gone if @account.suspended_permanently? + gone if @account.suspended_permanently? + end + + def gone + expires_in(3.minutes, public: true) + head 410 end def bad_request @@ -46,9 +51,5 @@ module WellKnown expires_in(3.minutes, public: true) head 404 end - - def gone - head 410 - end end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1228ce36c5..e3da47d9b0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -155,20 +155,8 @@ module ApplicationHelper tag(:meta, content: content, property: property) end - def react_component(name, props = {}, &block) - if block.nil? - content_tag(:div, nil, data: { component: name.to_s.camelcase, props: Oj.dump(props) }) - else - content_tag(:div, data: { component: name.to_s.camelcase, props: Oj.dump(props) }, &block) - end - end - - def react_admin_component(name, props = {}) - content_tag(:div, nil, data: { 'admin-component': name.to_s.camelcase, props: Oj.dump({ locale: I18n.locale }.merge(props)) }) - end - def body_classes - output = (@body_classes || '').split + output = body_class_string.split output << "flavour-#{current_flavour.parameterize}" output << "skin-#{current_skin.parameterize}" output << 'system-font' if current_account&.user&.setting_system_font_ui diff --git a/app/helpers/instance_helper.rb b/app/helpers/instance_helper.rb index bedfe6f304..893afdd51f 100644 --- a/app/helpers/instance_helper.rb +++ b/app/helpers/instance_helper.rb @@ -9,13 +9,17 @@ module InstanceHelper @site_hostname ||= Addressable::URI.parse("//#{Rails.configuration.x.local_domain}").display_uri.host end - def description_for_sign_up - prefix = if @invite.present? - I18n.t('auth.description.prefix_invited_by_user', name: @invite.user.account.username) - else - I18n.t('auth.description.prefix_sign_up') - end + def description_for_sign_up(invite = nil) + safe_join([description_prefix(invite), I18n.t('auth.description.suffix')], ' ') + end - safe_join([prefix, I18n.t('auth.description.suffix')], ' ') + private + + def description_prefix(invite) + if invite.present? + I18n.t('auth.description.prefix_invited_by_user', name: invite.user.account.username) + else + I18n.t('auth.description.prefix_sign_up') + end end end diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index 24362b61e7..ce3ff094f6 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -63,11 +63,11 @@ module JsonLdHelper uri.nil? || !uri.start_with?('http://', 'https://') end - def invalid_origin?(url) - return true if unsupported_uri_scheme?(url) + def non_matching_uri_hosts?(base_url, comparison_url) + return true if unsupported_uri_scheme?(comparison_url) - needle = Addressable::URI.parse(url).host - haystack = Addressable::URI.parse(@account.uri).host + needle = Addressable::URI.parse(comparison_url).host + haystack = Addressable::URI.parse(base_url).host !haystack.casecmp(needle).zero? end diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index bbf0a97fc3..00e1e5178b 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -201,7 +201,6 @@ module LanguagesHelper sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze, smj: ['Lule Sami', 'Julevsámegiella'].freeze, szl: ['Silesian', 'ślůnsko godka'].freeze, - tai: ['Tai', 'ภาษาไท or ภาษาไต'].freeze, tok: ['Toki Pona', 'toki pona'].freeze, zba: ['Balaibalan', 'باليبلن'].freeze, zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze, diff --git a/app/helpers/media_component_helper.rb b/app/helpers/media_component_helper.rb new file mode 100644 index 0000000000..a57d0b4b62 --- /dev/null +++ b/app/helpers/media_component_helper.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +module MediaComponentHelper + def render_video_component(status, **options) + video = status.ordered_media_attachments.first + + meta = video.file.meta || {} + + component_params = { + sensitive: sensitive_viewer?(status, current_account), + src: full_asset_url(video.file.url(:original)), + preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), + alt: video.description, + blurhash: video.blurhash, + frameRate: meta.dig('original', 'frame_rate'), + inline: true, + media: [ + serialize_media_attachment(video), + ].as_json, + }.merge(**options) + + react_component :video, component_params do + render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } + end + end + + def render_audio_component(status, **options) + audio = status.ordered_media_attachments.first + + meta = audio.file.meta || {} + + component_params = { + src: full_asset_url(audio.file.url(:original)), + poster: full_asset_url(audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url), + alt: audio.description, + backgroundColor: meta.dig('colors', 'background'), + foregroundColor: meta.dig('colors', 'foreground'), + accentColor: meta.dig('colors', 'accent'), + duration: meta.dig('original', 'duration'), + }.merge(**options) + + react_component :audio, component_params do + render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } + end + end + + def render_media_gallery_component(status, **options) + component_params = { + sensitive: sensitive_viewer?(status, current_account), + autoplay: prefers_autoplay?, + media: status.ordered_media_attachments.map { |a| serialize_media_attachment(a).as_json }, + }.merge(**options) + + react_component :media_gallery, component_params do + render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } + end + end + + def render_card_component(status, **options) + component_params = { + sensitive: sensitive_viewer?(status, current_account), + card: serialize_status_card(status).as_json, + }.merge(**options) + + react_component :card, component_params + end + + def render_poll_component(status, **options) + component_params = { + disabled: true, + poll: serialize_status_poll(status).as_json, + }.merge(**options) + + react_component :poll, component_params do + render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: prefers_autoplay? } + end + end + + private + + def serialize_media_attachment(attachment) + ActiveModelSerializers::SerializableResource.new( + attachment, + serializer: REST::MediaAttachmentSerializer + ) + end + + def serialize_status_card(status) + ActiveModelSerializers::SerializableResource.new( + status.preview_card, + serializer: REST::PreviewCardSerializer + ) + end + + def serialize_status_poll(status) + ActiveModelSerializers::SerializableResource.new( + status.preloadable_poll, + serializer: REST::PollSerializer, + scope: current_user, + scope_name: :current_user + ) + end + + def sensitive_viewer?(status, account) + if !account.nil? && account.id == status.account_id + status.sensitive + else + status.account.sensitized? || status.sensitive + end + end +end diff --git a/app/helpers/react_component_helper.rb b/app/helpers/react_component_helper.rb new file mode 100644 index 0000000000..fc08de13dd --- /dev/null +++ b/app/helpers/react_component_helper.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module ReactComponentHelper + def react_component(name, props = {}, &block) + data = { component: name.to_s.camelcase, props: Oj.dump(props) } + if block.nil? + div_tag_with_data(data) + else + content_tag(:div, data: data, &block) + end + end + + def react_admin_component(name, props = {}) + data = { 'admin-component': name.to_s.camelcase, props: Oj.dump({ locale: I18n.locale }.merge(props)) } + div_tag_with_data(data) + end + + private + + def div_tag_with_data(data) + content_tag(:div, nil, data: data) + end +end diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index d1e3fddafe..9f87593674 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -105,94 +105,10 @@ module StatusesHelper end end - def sensitized?(status, account) - if !account.nil? && account.id == status.account_id - status.sensitive - else - status.account.sensitized? || status.sensitive - end - end - def embedded_view? params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION end - def render_video_component(status, **options) - video = status.ordered_media_attachments.first - - meta = video.file.meta || {} - - component_params = { - sensitive: sensitized?(status, current_account), - src: full_asset_url(video.file.url(:original)), - preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), - alt: video.description, - blurhash: video.blurhash, - frameRate: meta.dig('original', 'frame_rate'), - inline: true, - media: [ - ActiveModelSerializers::SerializableResource.new(video, serializer: REST::MediaAttachmentSerializer), - ].as_json, - }.merge(**options) - - react_component :video, component_params do - render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } - end - end - - def render_audio_component(status, **options) - audio = status.ordered_media_attachments.first - - meta = audio.file.meta || {} - - component_params = { - src: full_asset_url(audio.file.url(:original)), - poster: full_asset_url(audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url), - alt: audio.description, - backgroundColor: meta.dig('colors', 'background'), - foregroundColor: meta.dig('colors', 'foreground'), - accentColor: meta.dig('colors', 'accent'), - duration: meta.dig('original', 'duration'), - }.merge(**options) - - react_component :audio, component_params do - render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } - end - end - - def render_media_gallery_component(status, **options) - component_params = { - sensitive: sensitized?(status, current_account), - autoplay: prefers_autoplay?, - media: status.ordered_media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }, - }.merge(**options) - - react_component :media_gallery, component_params do - render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } - end - end - - def render_card_component(status, **options) - component_params = { - sensitive: sensitized?(status, current_account), - maxDescription: 160, - card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json, - }.merge(**options) - - react_component :card, component_params - end - - def render_poll_component(status, **options) - component_params = { - disabled: true, - poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json, - }.merge(**options) - - react_component :poll, component_params do - render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: prefers_autoplay? } - end - end - def prefers_autoplay? ActiveModel::Type::Boolean.new.cast(params[:autoplay]) || current_user&.setting_auto_play_gif end diff --git a/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx b/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx index 6d73fa68c8..93dfed2f06 100644 --- a/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx +++ b/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx @@ -32,7 +32,7 @@ class EditedTimestamp extends React.PureComponent { renderHeader = items => { return ( - + ); }; diff --git a/app/javascript/flavours/glitch/components/hashtag.jsx b/app/javascript/flavours/glitch/components/hashtag.jsx index bc4d7c7bdc..c4701055c7 100644 --- a/app/javascript/flavours/glitch/components/hashtag.jsx +++ b/app/javascript/flavours/glitch/components/hashtag.jsx @@ -41,7 +41,7 @@ class SilentErrorBoundary extends React.Component { export const accountsCountRenderer = (displayNumber, pluralReady) => ( {displayNumber}, diff --git a/app/javascript/flavours/glitch/components/logo.jsx b/app/javascript/flavours/glitch/components/logo.jsx index ee5c22496c..60e8f40b23 100644 --- a/app/javascript/flavours/glitch/components/logo.jsx +++ b/app/javascript/flavours/glitch/components/logo.jsx @@ -1,10 +1,15 @@ import React from 'react'; +import logo from 'mastodon/../images/logo.svg'; -const Logo = () => ( - +export const WordmarkLogo = () => ( + Mastodon ); -export default Logo; +export const SymbolLogo = () => ( + Mastodon +); + +export default WordmarkLogo; diff --git a/app/javascript/flavours/glitch/components/short_number.jsx b/app/javascript/flavours/glitch/components/short_number.jsx index 535c17727d..861d0bc589 100644 --- a/app/javascript/flavours/glitch/components/short_number.jsx +++ b/app/javascript/flavours/glitch/components/short_number.jsx @@ -32,17 +32,14 @@ function ShortNumber({ value, renderer, children }) { const shortNumber = toShortNumber(value); const [, division] = shortNumber; - // eslint-disable-next-line eqeqeq if (children != null && renderer != null) { console.warn('Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.'); } - // eslint-disable-next-line eqeqeq const customRenderer = children != null ? children : renderer; const displayNumber = ; - // eslint-disable-next-line eqeqeq return customRenderer != null ? customRenderer(displayNumber, pluralReady(value, division)) : displayNumber; diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index f5730b8241..cde952d11d 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -69,6 +69,9 @@ class Status extends ImmutablePureComponent { id: PropTypes.string, status: ImmutablePropTypes.map, account: ImmutablePropTypes.map, + previousId: PropTypes.string, + nextInReplyToId: PropTypes.string, + rootId: PropTypes.string, onReply: PropTypes.func, onFavourite: PropTypes.func, onReblog: PropTypes.func, @@ -522,6 +525,9 @@ class Status extends ImmutablePureComponent { unread, featured, pictureInPicture, + previousId, + nextInReplyToId, + rootId, ...other } = this.props; const { isCollapsed, forceFilter } = this.state; @@ -565,6 +571,8 @@ class Status extends ImmutablePureComponent { openMedia: this.handleHotkeyOpenMedia, }; + let prepend, rebloggedByText; + if (hidden) { return ( @@ -576,7 +584,11 @@ class Status extends ImmutablePureComponent { ); } + const connectUp = previousId && previousId === status.get('in_reply_to_id'); + const connectToRoot = rootId && rootId === status.get('in_reply_to_id'); + const connectReply = nextInReplyToId && nextInReplyToId === status.get('id'); const matchedFilters = status.get('matched_filters'); + if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) { const minHandlers = this.props.muted ? {} : { moveUp: this.handleHotkeyMoveUp, @@ -667,7 +679,7 @@ class Status extends ImmutablePureComponent { inline sensitive={status.get('sensitive')} letterbox={settings.getIn(['media', 'letterbox'])} - fullwidth={settings.getIn(['media', 'fullwidth'])} + fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])} preventPlayback={isCollapsed || !isExpanded} onOpenVideo={this.handleOpenVideo} width={this.props.cachedMediaWidth} @@ -688,7 +700,7 @@ class Status extends ImmutablePureComponent { lang={status.get('language')} sensitive={status.get('sensitive')} letterbox={settings.getIn(['media', 'letterbox'])} - fullwidth={settings.getIn(['media', 'fullwidth'])} + fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])} hidden={isCollapsed || !isExpanded} onOpenMedia={this.handleOpenMedia} cacheWidth={this.props.cacheMediaWidth} @@ -730,8 +742,6 @@ class Status extends ImmutablePureComponent { 'data-status-by': `@${status.getIn(['account', 'acct'])}`, }; - let prepend; - if (this.props.prepend && account) { const notifKind = { favourite: 'favourited', @@ -753,8 +763,6 @@ class Status extends ImmutablePureComponent { ); } - let rebloggedByText; - if (this.props.prepend === 'reblog') { rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: account.get('acct') }); } @@ -763,6 +771,8 @@ class Status extends ImmutablePureComponent { collapsed: isCollapsed, 'has-background': isCollapsed && background, 'status__wrapper-reply': !!status.get('in_reply_to_id'), + 'status--in-thread': !!rootId, + 'status--first-in-thread': previousId && (!connectUp || connectToRoot), unread, muted, }, 'focusable'); @@ -779,6 +789,9 @@ class Status extends ImmutablePureComponent { aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))} > {!muted && prepend} + + {(connectReply || connectUp || connectToRoot) &&
} +
{muted && prepend} diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 7faed925c5..f152e9a324 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -85,6 +85,7 @@ const makeMapStateToProps = () => { return { containerId: props.containerId || props.id, // Should match reblogStatus's id for reblogs status: status, + nextInReplyToId: props.nextId ? state.getIn(['statuses', props.nextId, 'in_reply_to_id']) : null, account: account || props.account, settings: state.get('local_settings'), prepend: prepend || props.prepend, diff --git a/app/javascript/flavours/glitch/features/compose/components/search_results.jsx b/app/javascript/flavours/glitch/features/compose/components/search_results.jsx index bf009d13a8..6c1dee454c 100644 --- a/app/javascript/flavours/glitch/features/compose/components/search_results.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/search_results.jsx @@ -127,7 +127,7 @@ class SearchResults extends ImmutablePureComponent {
- +
{accounts} diff --git a/app/javascript/flavours/glitch/features/emoji/emoji.js b/app/javascript/flavours/glitch/features/emoji/emoji.js index 24c5814c41..b058db522a 100644 --- a/app/javascript/flavours/glitch/features/emoji/emoji.js +++ b/app/javascript/flavours/glitch/features/emoji/emoji.js @@ -20,68 +20,88 @@ const emojiFilename = (filename) => { }; const emojifyTextNode = (node, customEmojis) => { + const VS15 = 0xFE0E; + const VS16 = 0xFE0F; + let str = node.textContent; const fragment = new DocumentFragment(); + let i = 0; for (;;) { - let match, i = 0; + let unicode_emoji; + // Skip to the next potential emoji to replace (either custom emoji or custom emoji :shortcode:) if (customEmojis === null) { - while (i < str.length && (useSystemEmojiFont || !(match = trie.search(str.slice(i))))) { + while (i < str.length && (useSystemEmojiFont || !(unicode_emoji = trie.search(str.slice(i))))) { i += str.codePointAt(i) < 65536 ? 1 : 2; } } else { - while (i < str.length && str[i] !== ':' && (useSystemEmojiFont || !(match = trie.search(str.slice(i))))) { + while (i < str.length && str[i] !== ':' && (useSystemEmojiFont || !(unicode_emoji = trie.search(str.slice(i))))) { i += str.codePointAt(i) < 65536 ? 1 : 2; } } - let rend, replacement = null; + // We reached the end of the string, nothing to replace if (i === str.length) { break; - } else if (str[i] === ':') { - if (!(() => { - rend = str.indexOf(':', i + 1) + 1; - if (!rend) return false; // no pair of ':' - const shortname = str.slice(i, rend); - // now got a replacee as ':shortname:' - // if you want additional emoji handler, add statements below which set replacement and return true. - if (shortname in customEmojis) { - const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url; - replacement = document.createElement('img'); - replacement.setAttribute('draggable', 'false'); - replacement.setAttribute('class', 'emojione custom-emoji'); - replacement.setAttribute('alt', shortname); - replacement.setAttribute('title', shortname); - replacement.setAttribute('src', filename); - replacement.setAttribute('data-original', customEmojis[shortname].url); - replacement.setAttribute('data-static', customEmojis[shortname].static_url); - return true; - } - return false; - })()) rend = ++i; - } else if (!useSystemEmojiFont) { // matched to unicode emoji - const { filename, shortCode } = unicodeMapping[match]; + } + + let rend, replacement = null; + if (str[i] === ':') { // Potentially the start of a custom emoji :shortcode: + rend = str.indexOf(':', i + 1) + 1; + + // no matching ending ':', skip + if (!rend) { + i++; + continue; + } + + const shortcode = str.slice(i, rend); + const custom_emoji = customEmojis[shortcode]; + + // not a recognized shortcode, skip + if (!custom_emoji) { + i++; + continue; + } + + // now got a replacee as ':shortcode:' + // if you want additional emoji handler, add statements below which set replacement and return true. + const filename = autoPlayGif ? custom_emoji.url : custom_emoji.static_url; + replacement = document.createElement('img'); + replacement.setAttribute('draggable', 'false'); + replacement.setAttribute('class', 'emojione custom-emoji'); + replacement.setAttribute('alt', shortcode); + replacement.setAttribute('title', shortcode); + replacement.setAttribute('src', filename); + replacement.setAttribute('data-original', custom_emoji.url); + replacement.setAttribute('data-static', custom_emoji.static_url); + } else { // start of an unicode emoji + rend = i + unicode_emoji.length; + + // If the matched character was followed by VS15 (for selecting text presentation), skip it. + if (str.codePointAt(rend - 1) !== VS16 && str.codePointAt(rend) === VS15) { + i = rend + 1; + continue; + } + + const { filename, shortCode } = unicodeMapping[unicode_emoji]; const title = shortCode ? `:${shortCode}:` : ''; + replacement = document.createElement('img'); replacement.setAttribute('draggable', 'false'); replacement.setAttribute('class', 'emojione'); - replacement.setAttribute('alt', match); + replacement.setAttribute('alt', unicode_emoji); replacement.setAttribute('title', title); replacement.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename)}.svg`); - rend = i + match.length; - // If the matched character was followed by VS15 (for selecting text presentation), skip it. - if (str.codePointAt(rend) === 65038) { - rend += 1; - } } + // Add the processed-up-to-now string and the emoji replacement fragment.append(document.createTextNode(str.slice(0, i))); - if (replacement) { - fragment.append(replacement); - } + fragment.append(replacement); str = str.slice(rend); + i = 0; } fragment.append(document.createTextNode(str)); diff --git a/app/javascript/flavours/glitch/features/explore/index.jsx b/app/javascript/flavours/glitch/features/explore/index.jsx index 3587de1dba..a64c8183c4 100644 --- a/app/javascript/flavours/glitch/features/explore/index.jsx +++ b/app/javascript/flavours/glitch/features/explore/index.jsx @@ -71,17 +71,20 @@ class Explore extends React.PureComponent { + + + {signedIn && ( + + + + )} + - {signedIn && ( - - - - )}
diff --git a/app/javascript/flavours/glitch/features/notifications/components/report.jsx b/app/javascript/flavours/glitch/features/notifications/components/report.jsx index 9110735a14..5cff01dacd 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/report.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/report.jsx @@ -45,7 +45,7 @@ class Report extends ImmutablePureComponent {
- · + ·
{intl.formatMessage(messages[report.get('category')])}
diff --git a/app/javascript/flavours/glitch/features/status/components/card.jsx b/app/javascript/flavours/glitch/features/status/components/card.jsx index 359dbbc207..7e834b2dc1 100644 --- a/app/javascript/flavours/glitch/features/status/components/card.jsx +++ b/app/javascript/flavours/glitch/features/status/components/card.jsx @@ -17,16 +17,6 @@ const getHostname = url => { return parser.hostname; }; -const trim = (text, len) => { - const cut = text.indexOf(' ', len); - - if (cut === -1) { - return text; - } - - return text.slice(0, cut) + (text.length > len ? '…' : ''); -}; - const domParser = new DOMParser(); const addAutoPlay = html => { @@ -54,7 +44,6 @@ export default class Card extends React.PureComponent { static propTypes = { card: ImmutablePropTypes.map, - maxDescription: PropTypes.number, onOpenMedia: PropTypes.func.isRequired, compact: PropTypes.bool, defaultWidth: PropTypes.number, @@ -63,7 +52,6 @@ export default class Card extends React.PureComponent { }; static defaultProps = { - maxDescription: 50, compact: false, }; @@ -176,7 +164,7 @@ export default class Card extends React.PureComponent { } render () { - const { card, maxDescription, compact, defaultWidth } = this.props; + const { card, compact, defaultWidth } = this.props; const { width, embedded, revealed } = this.state; if (card === null) { @@ -195,7 +183,7 @@ export default class Card extends React.PureComponent { const description = (
{title} - {!(horizontal || compact) &&

{trim(card.get('description') || '', maxDescription)}

} + {!(horizontal || compact) &&

{card.get('description')}

} {provider}
); diff --git a/app/javascript/flavours/glitch/features/status/index.jsx b/app/javascript/flavours/glitch/features/status/index.jsx index 36928735f6..9bf84e0a11 100644 --- a/app/javascript/flavours/glitch/features/status/index.jsx +++ b/app/javascript/flavours/glitch/features/status/index.jsx @@ -65,7 +65,7 @@ const messages = defineMessages({ redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' }, revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' }, hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' }, - statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {{attachmentCount} attachments}}' }, + statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}' }, detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, @@ -574,8 +574,10 @@ class Status extends ImmutablePureComponent { this.column.scrollTop(); }; - renderChildren (list) { - return list.map(id => ( + renderChildren (list, ancestors) { + const { params: { statusId } } = this.props; + + return list.map((id, i) => ( 0 && list.get(i - 1)} + nextId={list.get(i + 1) || (ancestors && statusId)} + rootId={statusId} /> )); } @@ -643,7 +648,7 @@ class Status extends ImmutablePureComponent { const isExpanded = settings.getIn(['content_warnings', 'shared_state']) ? !status.get('hidden') : this.state.isExpanded; if (ancestorsIds && ancestorsIds.size > 0) { - ancestors = <>{this.renderChildren(ancestorsIds)}; + ancestors = <>{this.renderChildren(ancestorsIds, true)}; } if (descendantsIds && descendantsIds.size > 0) { diff --git a/app/javascript/flavours/glitch/features/ui/components/header.jsx b/app/javascript/flavours/glitch/features/ui/components/header.jsx index f7bab24871..42ed4d9013 100644 --- a/app/javascript/flavours/glitch/features/ui/components/header.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/header.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import Logo from 'flavours/glitch/components/logo'; +import { WordmarkLogo, SymbolLogo } from 'flavours/glitch/components/logo'; import { Link, withRouter } from 'react-router-dom'; import { FormattedMessage } from 'react-intl'; import { registrationsOpen, me } from 'flavours/glitch/initial_state'; @@ -74,7 +74,10 @@ class Header extends React.PureComponent { return (
- + + + +
{content} diff --git a/app/javascript/flavours/glitch/locales/af.json b/app/javascript/flavours/glitch/locales/af.json index 4d243f94c2..f41d051683 100644 --- a/app/javascript/flavours/glitch/locales/af.json +++ b/app/javascript/flavours/glitch/locales/af.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Done", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Plasings van mense wat jy volg, kom chronologies in jou tuisvoer verby. Moenie huiwer nie. Volg na hartelus. As daar mense is wie se plasings jy nie meer wil sien nie, ontvolg hulle net!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/an.json b/app/javascript/flavours/glitch/locales/an.json index 4d243f94c2..e5ebed35c8 100644 --- a/app/javascript/flavours/glitch/locales/an.json +++ b/app/javascript/flavours/glitch/locales/an.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Pareixe que no s'ha puesto chenerar garra sucherencia pa tu. Puetz prebar a buscar a chent que talment conoixcas u explorar los hashtags que son en tendencia.", + "follow_recommendations.done": "Feito", + "follow_recommendations.heading": "Sigue a chent que publique cosetas que te faigan goyo! Aquí tiens qualques sucherencias.", + "follow_recommendations.lead": "Las publicacions d'a chent a la quala sigas amaneixerán ordenadas cronolochicament en Inicio. No tiengas miedo de cometer errors, puetz deixar-les de seguir en qualsequier momento con a mesma facilidat!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/ar.json b/app/javascript/flavours/glitch/locales/ar.json index 4d243f94c2..e65f4f7bd8 100644 --- a/app/javascript/flavours/glitch/locales/ar.json +++ b/app/javascript/flavours/glitch/locales/ar.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "يبدو أنه لا يمكن إنشاء أي اقتراحات لك. يمكنك البحث عن أشخاص قد تعرفهم أو استكشاف الوسوم الرائجة.", + "follow_recommendations.done": "تم", + "follow_recommendations.heading": "تابع الأشخاص الذين ترغب في رؤية منشوراتهم! إليك بعض الاقتراحات.", + "follow_recommendations.lead": "ستظهر منشورات الأشخاص الذين تُتابعتهم بترتيب تسلسلي زمني على صفحتك الرئيسية. لا تخف إذا ارتكبت أي أخطاء، تستطيع إلغاء متابعة أي شخص في أي وقت تريد!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/ast.json b/app/javascript/flavours/glitch/locales/ast.json index 4d243f94c2..87050f965b 100644 --- a/app/javascript/flavours/glitch/locales/ast.json +++ b/app/javascript/flavours/glitch/locales/ast.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Paez que nun se puen xenerar suxerencies pa ti. Pues tentar d'usar la busca p'atopar perfiles que pues conocer o esplorar les etiquetes en tendencia.", + "follow_recommendations.done": "Fecho", + "follow_recommendations.heading": "¡Sigui a perfiles que te prestaría ver nel feed personal! Equí tienes dalgunes suxerencies.", + "follow_recommendations.lead": "Los artículos de los perfiles que sigas van apaecer n'orde cronolóxicu nel to feed d'aniciu. ¡Nun tengas mieu d'enquivocate, pues dexar de siguilos con facilidá en cualesquier momentu!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/be.json b/app/javascript/flavours/glitch/locales/be.json index 4d243f94c2..073980d52c 100644 --- a/app/javascript/flavours/glitch/locales/be.json +++ b/app/javascript/flavours/glitch/locales/be.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Здаецца, прапаноў для вас няма. Вы можаце паспрабаваць выкарыстаць пошук, каб знайсці людзей, якіх вы можаце ведаць, ці даследаваць папулярныя хэштэгі.", + "follow_recommendations.done": "Гатова", + "follow_recommendations.heading": "Падпісвайцеся на людзей, допісы якіх вам будуць цікавы! Вось некаторыя рэкамендацыі.", + "follow_recommendations.lead": "Допісы людзей, на якіх вы падпісаны, будуць паказаны ў храналагічным парадку на вашай хатняй старонцы. Не бойцеся памыляцца, вы лёгка зможаце адпісацца ў любы момант!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/bg.json b/app/javascript/flavours/glitch/locales/bg.json index 4d243f94c2..024fc0bb93 100644 --- a/app/javascript/flavours/glitch/locales/bg.json +++ b/app/javascript/flavours/glitch/locales/bg.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Изглежда, че няма предложения, които може да се породят за вас. Може да опитате да потърсите хора, които познавате или да разгледате налагащи се хаштагове.", + "follow_recommendations.done": "Готово", + "follow_recommendations.heading": "Следвайте хора, от които харесвате да виждате публикации! Ето някои предложения.", + "follow_recommendations.lead": "Публикациите от последваните, ще се показват в хронологичен ред в началния ви инфоканал. Не се страхувайте, че ще сгрешите, по всяко време много лесно може да спрете да ги следвате!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/bn.json b/app/javascript/flavours/glitch/locales/bn.json index 4d243f94c2..ce9603d72b 100644 --- a/app/javascript/flavours/glitch/locales/bn.json +++ b/app/javascript/flavours/glitch/locales/bn.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "সম্পন্ন", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/br.json b/app/javascript/flavours/glitch/locales/br.json index 4d243f94c2..b597f32fe3 100644 --- a/app/javascript/flavours/glitch/locales/br.json +++ b/app/javascript/flavours/glitch/locales/br.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "War a seblant ne c'hall ket bezañ savet erbedadenn ebet evidoc'h. Gallout a rit implijout un enklask evit kavout tud a anavezfec'h pe furchal ar gerioù-klik diouzh ar c'hiz.", + "follow_recommendations.done": "Graet", + "follow_recommendations.heading": "Heuilhit tud a blijfe deoc'h lenn o zoudoù ! Setu un nebeud erbedadennoù.", + "follow_recommendations.lead": "Toudoù gant tud a vez heuliet ganeoc'h a zeuio war wel en urzh kronologel war ho red degemer. Arabat kaout aon ober fazioù, diheuliañ tud a c'hellit ober aes ha forzh pegoulz !", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/bs.json b/app/javascript/flavours/glitch/locales/bs.json index 4d243f94c2..f9ed6d5adc 100644 --- a/app/javascript/flavours/glitch/locales/bs.json +++ b/app/javascript/flavours/glitch/locales/bs.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Done", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/ca.json b/app/javascript/flavours/glitch/locales/ca.json index 4d243f94c2..a6acfa0cbf 100644 --- a/app/javascript/flavours/glitch/locales/ca.json +++ b/app/javascript/flavours/glitch/locales/ca.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Sembla que no s'han pogut generar suggeriments per a tu. Pots provar d'usar la cerca per a trobar persones que vulguis conèixer o explorar les etiquetes en tendència.", + "follow_recommendations.done": "Fet", + "follow_recommendations.heading": "Segueix a la gent de la que t'agradaria veure els seus tuts! Aquí hi ha algunes recomanacions.", + "follow_recommendations.lead": "Els tuts dels usuaris que segueixes es mostraran en ordre cronològic en la teva línia de temps Inici. No tinguis por en cometre errors, pots fàcilment deixar de seguir-los en qualsevol moment!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/ckb.json b/app/javascript/flavours/glitch/locales/ckb.json index 4d243f94c2..9d761e3d1e 100644 --- a/app/javascript/flavours/glitch/locales/ckb.json +++ b/app/javascript/flavours/glitch/locales/ckb.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "پێدەچێت هیچ پێشنیارێک بۆ تۆ دروست نەکرێت. دەتوانیت هەوڵبدەیت گەڕان بەکاربهێنیت بۆ گەڕان بەدوای ئەو کەسانەی کە ڕەنگە بیانناسیت یان بەدوای هاشتاگە ڕەوتەکاندا بگەڕێیت.", + "follow_recommendations.done": "تەواو", + "follow_recommendations.heading": "شوێن ئەو کەسانە بکەون کە دەتەوێت پۆستەکان ببینیت لە! لێرەدا چەند پێشنیارێک هەیە.", + "follow_recommendations.lead": "بابەتەکانی ئەو کەسانەی کە بەدوایدا دەگەڕێیت بە فەرمانی کرۆنۆلۆجی لە خواردنەکانی ماڵەکەت دەردەکەون. مەترسە لە هەڵەکردن، دەتوانیت بە ئاسانی خەڵک هەڵبکەیت هەر کاتێک!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/co.json b/app/javascript/flavours/glitch/locales/co.json index 4d243f94c2..ba37bce814 100644 --- a/app/javascript/flavours/glitch/locales/co.json +++ b/app/javascript/flavours/glitch/locales/co.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Si pare ch'ùn s'hè micca pussutu generà e ricumandazione per voi. Pudete sempre pruvà d'utilizà a ricerca per truvà ghjente chì cunnuscete, o splurà l'hashtag in tindenza.", + "follow_recommendations.done": "Fatta", + "follow_recommendations.heading": "Siguitate a ghjente da quelli vulete vede i missaghji! Eccu qualchì ricumandazione.", + "follow_recommendations.lead": "I missaghji da a ghjente che voi siguitate figureranu in ordine crunulogicu nant'a vostra pagina d'accolta. Ùn timite micca di fà un sbagliu, pudete sempre disabbunavvi d'un contu à ogni mumentu!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/cs.json b/app/javascript/flavours/glitch/locales/cs.json index a3d1c3b9cc..6283d02107 100644 --- a/app/javascript/flavours/glitch/locales/cs.json +++ b/app/javascript/flavours/glitch/locales/cs.json @@ -19,8 +19,12 @@ "confirmation_modal.do_not_ask_again": "Příště se už neptat", "content-type.change": "Formát příspěvku", "direct.group_by_conversations": "Seskupit do konverzací", + "empty_column.follow_recommendations": "Zdá se, že pro vás nelze vygenerovat žádné návrhy. Můžete zkusit přes vyhledávání nalézt lidi, které znáte, nebo prozkoumat populární hashtagy.", "endorsed_accounts_editor.endorsed_accounts": "Vybrané účty", "favourite_modal.combo": "Příště můžete pro přeskočení stisknout {combo}", + "follow_recommendations.done": "Hotovo", + "follow_recommendations.heading": "Sledujte lidi, jejichž příspěvky chcete vidět! Tady jsou nějaké návrhy.", + "follow_recommendations.lead": "Příspěvky od lidí, které sledujete, se budou objevovat v chronologickém pořadí ve vašem domovském kanálu. Nebojte se, že uděláte chybu, stejně snadno můžete lidi kdykoliv přestat sledovat!", "getting_started.onboarding": "Ukaž mi to tu", "home.column_settings.advanced": "Pokročilé", "home.column_settings.filter_regex": "Filtrovat podle regulárních výrazů", diff --git a/app/javascript/flavours/glitch/locales/cy.json b/app/javascript/flavours/glitch/locales/cy.json index 4d243f94c2..f7b668e4e6 100644 --- a/app/javascript/flavours/glitch/locales/cy.json +++ b/app/javascript/flavours/glitch/locales/cy.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Does dim awgrymiadau yma i chi. Gallwch geisio chwilio am bobl rydych yn eu hadnabod neu edrych drwy hashnodau sy'n trendio.", + "follow_recommendations.done": "Wedi gorffen", + "follow_recommendations.heading": "Dilynwch y bobl yr hoffech chi weld eu postiadau! Dyma ambell i awgrymiad.", + "follow_recommendations.lead": "Bydd postiadau gan bobl rydych chi'n eu dilyn yn ymddangos mewn trefn amser ar eich ffrwd cartref. Peidiwch â bod ofn gwneud camgymeriadau, gallwch chi ddad-ddilyn pobl yr un mor hawdd unrhyw bryd!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/da.json b/app/javascript/flavours/glitch/locales/da.json index 4d243f94c2..7652c983ed 100644 --- a/app/javascript/flavours/glitch/locales/da.json +++ b/app/javascript/flavours/glitch/locales/da.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Det ser ud til, at der ikke kunne genereres forslag til dig. Du kan prøve med Søg for at lede efter personer, du måske kender, eller udforske hashtags.", + "follow_recommendations.done": "Udført", + "follow_recommendations.heading": "Følg personer du gerne vil se indlæg fra! Her er nogle forslag.", + "follow_recommendations.lead": "Indlæg, fra personer du følger, vil fremgå kronologisk ordnet i dit hjemmefeed. Vær ikke bange for at begå fejl, da du altid og meget nemt kan ændre dit valg!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/de.json b/app/javascript/flavours/glitch/locales/de.json index f14e14d986..03e6e3b98a 100644 --- a/app/javascript/flavours/glitch/locales/de.json +++ b/app/javascript/flavours/glitch/locales/de.json @@ -47,8 +47,12 @@ "confirmations.unfilter.filters": "Passende{count, plural, one {r} other {}} Filter", "content-type.change": "Inhaltstyp", "direct.group_by_conversations": "Nach Unterhaltung gruppieren", + "empty_column.follow_recommendations": "Es sieht so aus, als könnten keine Vorschläge für dich generiert werden. Du kannst versuchen, nach Leuten zu suchen, die du vielleicht kennst, oder du kannst angesagte Hashtags erkunden.", "endorsed_accounts_editor.endorsed_accounts": "Empfohlene Konten", "favourite_modal.combo": "Mit {combo} wird dieses Fenster beim nächsten Mal nicht mehr angezeigt", + "follow_recommendations.done": "Fertig", + "follow_recommendations.heading": "Folge Leuten, deren Beiträge du sehen möchtest! Hier sind einige Vorschläge.", + "follow_recommendations.lead": "Beiträge von Profilen, denen du folgst, werden in chronologischer Reihenfolge auf deiner Startseite angezeigt. Sei unbesorgt, mal Fehler zu begehen. Du kannst diesen Konten ganz einfach und jederzeit wieder entfolgen.", "getting_started.onboarding": "Führe mich herum", "home.column_settings.advanced": "Erweitert", "home.column_settings.filter_regex": "Mit regulären Ausdrücken herausfiltern", diff --git a/app/javascript/flavours/glitch/locales/defaultMessages.json b/app/javascript/flavours/glitch/locales/defaultMessages.json index fe943f97f9..bf21fdef2d 100644 --- a/app/javascript/flavours/glitch/locales/defaultMessages.json +++ b/app/javascript/flavours/glitch/locales/defaultMessages.json @@ -424,6 +424,27 @@ ], "path": "app/javascript/flavours/glitch/features/favourites/index.json" }, + { + "descriptors": [ + { + "defaultMessage": "Follow people you'd like to see posts from! Here are some suggestions.", + "id": "follow_recommendations.heading" + }, + { + "defaultMessage": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", + "id": "follow_recommendations.lead" + }, + { + "defaultMessage": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "id": "empty_column.follow_recommendations" + }, + { + "defaultMessage": "Done", + "id": "follow_recommendations.done" + } + ], + "path": "app/javascript/flavours/glitch/features/follow_recommendations/index.json" + }, { "descriptors": [ { diff --git a/app/javascript/flavours/glitch/locales/el.json b/app/javascript/flavours/glitch/locales/el.json index 4d243f94c2..f9021e7096 100644 --- a/app/javascript/flavours/glitch/locales/el.json +++ b/app/javascript/flavours/glitch/locales/el.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Φαίνεται ότι δεν υπάρχει καμία πρόταση για σένα. Μπορείς να κάνεις μια αναζήτηση για άτομα που μπορεί να γνωρίζεις ή να εξερευνήσεις ετικέτες σε τάση.", + "follow_recommendations.done": "Έγινε", + "follow_recommendations.heading": "Ακολουθήστε άτομα από τα οποία θα θέλατε να βλέπετε δημοσιεύσεις! Ορίστε μερικές προτάσεις.", + "follow_recommendations.lead": "Οι αναρτήσεις των ατόμων που ακολουθείτε θα εμφανίζονται με χρονολογική σειρά στη ροή σας. Μη φοβάστε να κάνετε λάθη, καθώς μπορείτε πολύ εύκολα να σταματήσετε να ακολουθείτε άλλα άτομα οποιαδήποτε στιγμή!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/en-GB.json b/app/javascript/flavours/glitch/locales/en-GB.json index 4d243f94c2..f9ed6d5adc 100644 --- a/app/javascript/flavours/glitch/locales/en-GB.json +++ b/app/javascript/flavours/glitch/locales/en-GB.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Done", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/en.json b/app/javascript/flavours/glitch/locales/en.json index 3ed4ca91af..dfdf061968 100644 --- a/app/javascript/flavours/glitch/locales/en.json +++ b/app/javascript/flavours/glitch/locales/en.json @@ -47,8 +47,12 @@ "confirmations.unfilter.filters": "Matching {count, plural, one {filter} other {filters}}", "content-type.change": "Content type", "direct.group_by_conversations": "Group by conversation", + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", "endorsed_accounts_editor.endorsed_accounts": "Featured accounts", "favourite_modal.combo": "You can press {combo} to skip this next time", + "follow_recommendations.done": "Done", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "getting_started.onboarding": "Show me around", "home.column_settings.advanced": "Advanced", "home.column_settings.filter_regex": "Filter out by regular expressions", diff --git a/app/javascript/flavours/glitch/locales/eo.json b/app/javascript/flavours/glitch/locales/eo.json index 5905ad5485..bb59888a36 100644 --- a/app/javascript/flavours/glitch/locales/eo.json +++ b/app/javascript/flavours/glitch/locales/eo.json @@ -42,6 +42,10 @@ "confirmations.unfilter.author": "Aŭtoro", "confirmations.unfilter.confirm": "Montri", "confirmations.unfilter.edit_filter": "Redakti filtrilon", + "empty_column.follow_recommendations": "Ŝajnas, ke neniuj sugestoj povis esti generitaj por vi. Vi povas provi uzi serĉon por serĉi homojn, kiujn vi eble konas, aŭ esplori tendencajn kradvortojn.", + "follow_recommendations.done": "Farita", + "follow_recommendations.heading": "Sekvi la personojn kies mesaĝojn vi volas vidi! Jen iom da sugestoj.", + "follow_recommendations.lead": "La mesaĝoj de personoj kiujn vi sekvas, aperos laŭ kronologia ordo en via hejma templinio. Ne timu erari, vi povas ĉesi sekvi facile iam ajn!", "navigation_bar.keyboard_shortcuts": "Fulmoklavoj", "notification_purge.btn_all": "Selekti ĉiujn", "notification_purge.btn_apply": "Forigi selektajn", diff --git a/app/javascript/flavours/glitch/locales/es-AR.json b/app/javascript/flavours/glitch/locales/es-AR.json index fabab9ccb5..fd65f835ea 100644 --- a/app/javascript/flavours/glitch/locales/es-AR.json +++ b/app/javascript/flavours/glitch/locales/es-AR.json @@ -47,8 +47,12 @@ "confirmations.unfilter.filters": "Coincidencia con {count, plural, one {filtro} other {filtros}}", "content-type.change": "Tipo de contenido", "direct.group_by_conversations": "Agrupar por conversación", + "empty_column.follow_recommendations": "Parece que no se pudieron generar sugerencias para vos. Podés intentar buscar gente que conozcas o explorar las tendencias de las etiquetas.", "endorsed_accounts_editor.endorsed_accounts": "Cuentas destacadas", "favourite_modal.combo": "Puedes presionar {combo} para omitir esto la próxima vez", + "follow_recommendations.done": "Listo", + "follow_recommendations.heading": "¡Seguí cuentas cuyos mensajes te gustaría ver! Acá tenés algunas sugerencias.", + "follow_recommendations.lead": "Los mensajes de las cuentas que seguís aparecerán en orden cronológico en la columna \"Inicio\". No tengás miedo de meter la pata, ¡podés dejar de seguir cuentas fácilmente en cualquier momento!", "getting_started.onboarding": "Paseo inicial", "home.column_settings.advanced": "Avanzado", "home.column_settings.filter_regex": "Filtrar por expresiones regulares", diff --git a/app/javascript/flavours/glitch/locales/es-MX.json b/app/javascript/flavours/glitch/locales/es-MX.json index b1b1db72ff..a02270305d 100644 --- a/app/javascript/flavours/glitch/locales/es-MX.json +++ b/app/javascript/flavours/glitch/locales/es-MX.json @@ -47,8 +47,12 @@ "confirmations.unfilter.filters": "Coincidencia con {count, plural, one {filtro} other {filtros}}", "content-type.change": "Tipo de contenido", "direct.group_by_conversations": "Agrupar por conversación", + "empty_column.follow_recommendations": "Parece que no se ha podido generar ninguna sugerencia para ti. Puedes probar a buscar a gente que quizá conozcas o explorar los hashtags que están en tendencia.", "endorsed_accounts_editor.endorsed_accounts": "Cuentas destacadas", "favourite_modal.combo": "Puedes presionar {combo} para omitir esto la próxima vez", + "follow_recommendations.done": "Hecho", + "follow_recommendations.heading": "¡Sigue a gente que publique cosas que te gusten! Aquí tienes algunas sugerencias.", + "follow_recommendations.lead": "Las publicaciones de la gente a la que sigas aparecerán ordenadas cronológicamente en Inicio. No tengas miedo de cometer errores, ¡puedes dejarles de seguir en cualquier momento con la misma facilidad!", "getting_started.onboarding": "Paseo inicial", "home.column_settings.advanced": "Avanzado", "home.column_settings.filter_regex": "Filtrar por expresiones regulares", diff --git a/app/javascript/flavours/glitch/locales/es.json b/app/javascript/flavours/glitch/locales/es.json index db53f585f2..5dc50fd991 100644 --- a/app/javascript/flavours/glitch/locales/es.json +++ b/app/javascript/flavours/glitch/locales/es.json @@ -47,8 +47,12 @@ "confirmations.unfilter.filters": "Coincidiendo {count, plural, one {filtro} other {filtros}}", "content-type.change": "Tipo de contenido", "direct.group_by_conversations": "Agrupar por conversación", + "empty_column.follow_recommendations": "Parece que no se ha podido generar ninguna sugerencia para ti. Puedes probar a buscar a gente que quizá conozcas o explorar los hashtags que están en tendencia.", "endorsed_accounts_editor.endorsed_accounts": "Cuentas destacadas", "favourite_modal.combo": "Puedes presionar {combo} para omitir esto la próxima vez", + "follow_recommendations.done": "Hecho", + "follow_recommendations.heading": "¡Sigue a gente que publique cosas que te gusten! Aquí tienes algunas sugerencias.", + "follow_recommendations.lead": "Las publicaciones de la gente a la que sigas aparecerán ordenadas cronológicamente en Inicio. No tengas miedo de cometer errores, ¡puedes dejarles de seguir en cualquier momento con la misma facilidad!", "getting_started.onboarding": "Paseo inicial", "home.column_settings.advanced": "Avanzado", "home.column_settings.filter_regex": "Filtrar por expresiones regulares", diff --git a/app/javascript/flavours/glitch/locales/et.json b/app/javascript/flavours/glitch/locales/et.json index 4d243f94c2..03824f782b 100644 --- a/app/javascript/flavours/glitch/locales/et.json +++ b/app/javascript/flavours/glitch/locales/et.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Tundub, et sinu jaoks ei ole võimalik soovitusi luua. Proovi kasutada otsingut, et leida tuttavaid inimesi, või sirvi populaarseid silte.", + "follow_recommendations.done": "Valmis", + "follow_recommendations.heading": "Jälgi inimesi, kelle postitusi tahaksid näha! Mõned soovitused on siin.", + "follow_recommendations.lead": "Postitused inimestelt, keda jälgid, ilmuvad ajalises järjestuses kodu ajajoonel. Ära karda eksida, alati saab inimeste jälgimist ka lõpetada!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/eu.json b/app/javascript/flavours/glitch/locales/eu.json index 4d243f94c2..cb3c985d7d 100644 --- a/app/javascript/flavours/glitch/locales/eu.json +++ b/app/javascript/flavours/glitch/locales/eu.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Dirudienez ezin izan da zuretzako proposamenik sortu. Bilaketa erabili dezakezu ezagutzen duzun jendea aurkitzeko edo traolen joerak arakatu.", + "follow_recommendations.done": "Egina", + "follow_recommendations.heading": "Jarraitu jendea beren bidalketak ikusteko! Hemen dituzu iradokizun batzuk.", + "follow_recommendations.lead": "Jarraitzen duzun jendearen bidalketak ordena kronologikoan agertuko dira zure hasierako jarioan. Ez izan akatsak egiteko beldurrik, jendea jarraitzeari uztea erraza da!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/fa.json b/app/javascript/flavours/glitch/locales/fa.json index 4d243f94c2..f3c236c1f7 100644 --- a/app/javascript/flavours/glitch/locales/fa.json +++ b/app/javascript/flavours/glitch/locales/fa.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "به نظر نمی‌توان هیچ پیشنهادی برایتان ایجاد کرد. می‌توانید برای یافتن افرادی که ممکن است بشناسید از جست‌وجو یا کاوش برچسب‌های داغ استفاده کنید.", + "follow_recommendations.done": "انجام شد", + "follow_recommendations.heading": "افرادی را که می‌خواهید فرسته‌هایشان را ببینید پی‌گیری کنید! این‌ها تعدادی پیشنهاد هستند.", + "follow_recommendations.lead": "فرسته‌های افرادی که دنبال می‌کنید به ترتیب زمانی در خوراک خانه‌تان نشان داده خواهد شد. از اشتباه کردن نترسید. می‌توانید به همین سادگی در هر زمانی از دنبال کردن افراد دست بکشید!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/fi.json b/app/javascript/flavours/glitch/locales/fi.json index 4d243f94c2..0adee60ee5 100644 --- a/app/javascript/flavours/glitch/locales/fi.json +++ b/app/javascript/flavours/glitch/locales/fi.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Näyttää siltä, että sinulle ei voi luoda ehdotuksia. Voit yrittää etsiä ihmisiä, jotka saatat tuntea tai tutkia trendaavia aihetunnisteita.", + "follow_recommendations.done": "Valmis", + "follow_recommendations.heading": "Seuraa ihmisiä, joilta haluat nähdä viestejä! Tässä on muutamia ehdotuksia.", + "follow_recommendations.lead": "Seuraamiesi julkaisut näkyvät aikajärjestyksessä kotisyötteessä. Älä pelkää seurata vahingossa, voit lopettaa seuraamisen yhtä helposti!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/fo.json b/app/javascript/flavours/glitch/locales/fo.json index 4d243f94c2..af87ec4db8 100644 --- a/app/javascript/flavours/glitch/locales/fo.json +++ b/app/javascript/flavours/glitch/locales/fo.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Tað sær út sum, at eingi uppskot kundu fáast fram til tín. Tú kanst royna at brúka leiting fyri at finna fólk, sum tú kanska kennir, ella at kanna frámerki, sum eru vælumtókt í løtuni.", + "follow_recommendations.done": "Liðugt", + "follow_recommendations.heading": "Fylg fólki, sum tú hevur hug at síggja postar frá! Her eru nøkur boð.", + "follow_recommendations.lead": "Postar frá fólki, sum tú fylgir, verða vístir í tíðarrøð á heimarásini hjá tær. Ver ikki bangin fyri at gera feilir; tú kanst gevast at fylgja fólki eins lættliga nær sum helst!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/fr-QC.json b/app/javascript/flavours/glitch/locales/fr-QC.json index ec42f666d4..2a0c808a42 100644 --- a/app/javascript/flavours/glitch/locales/fr-QC.json +++ b/app/javascript/flavours/glitch/locales/fr-QC.json @@ -47,8 +47,12 @@ "confirmations.unfilter.filters": "Correspondance avec {count, plural, one {un filtre} other {plusieurs filtres}}", "content-type.change": "Type de contenu", "direct.group_by_conversations": "Grouper par conversation", + "empty_column.follow_recommendations": "Il semble qu’aucune suggestion n’ait pu être générée pour vous. Vous pouvez essayer d’utiliser la recherche pour découvrir des personnes que vous pourriez connaître ou explorer les hashtags populaires.", "endorsed_accounts_editor.endorsed_accounts": "Comptes mis en avant", "favourite_modal.combo": "Vous pouvez appuyer sur {combo} pour passer ceci la prochaine fois", + "follow_recommendations.done": "Terminé", + "follow_recommendations.heading": "Suivez les personnes dont vous aimeriez voir les publications! Voici quelques suggestions.", + "follow_recommendations.lead": "Les publication de personnes que vous suivez apparaîtront par ordre chronologique sur votre fil d'accueil. N'ayez pas peur de faire des erreurs, vous pouvez arrêter de suivre les gens aussi facilement n'importe quand!", "getting_started.onboarding": "Montre-moi les alentours", "home.column_settings.advanced": "Avancé", "home.column_settings.filter_regex": "Filtrer par expression régulière", diff --git a/app/javascript/flavours/glitch/locales/fr.json b/app/javascript/flavours/glitch/locales/fr.json index ec42f666d4..b0ccce7692 100644 --- a/app/javascript/flavours/glitch/locales/fr.json +++ b/app/javascript/flavours/glitch/locales/fr.json @@ -47,8 +47,12 @@ "confirmations.unfilter.filters": "Correspondance avec {count, plural, one {un filtre} other {plusieurs filtres}}", "content-type.change": "Type de contenu", "direct.group_by_conversations": "Grouper par conversation", + "empty_column.follow_recommendations": "Il semble qu’aucune suggestion n’ait pu être générée pour vous. Vous pouvez essayer d’utiliser la recherche pour découvrir des personnes que vous pourriez connaître ou explorer les hashtags tendance.", "endorsed_accounts_editor.endorsed_accounts": "Comptes mis en avant", "favourite_modal.combo": "Vous pouvez appuyer sur {combo} pour passer ceci la prochaine fois", + "follow_recommendations.done": "Terminé", + "follow_recommendations.heading": "Suivez les personnes dont vous aimeriez voir les messages ! Voici quelques suggestions.", + "follow_recommendations.lead": "Les messages des personnes que vous suivez apparaîtront par ordre chronologique sur votre fil d'accueil. Ne craignez pas de faire des erreurs, vous pouvez arrêter de suivre les gens aussi facilement à tout moment !", "getting_started.onboarding": "Montre-moi les alentours", "home.column_settings.advanced": "Avancé", "home.column_settings.filter_regex": "Filtrer par expression régulière", diff --git a/app/javascript/flavours/glitch/locales/fy.json b/app/javascript/flavours/glitch/locales/fy.json index 4d243f94c2..5741fa7703 100644 --- a/app/javascript/flavours/glitch/locales/fy.json +++ b/app/javascript/flavours/glitch/locales/fy.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "It liket der op dat der gjin oanrekommandaasjes foar jo oanmakke wurde kinne. Jo kinne probearje te sykjen nei minsken dy’t jo miskien kinne, sykje op hashtags, de lokale en globale tiidlinen besjen of de brûkersgids trochblêdzje.", + "follow_recommendations.done": "Klear", + "follow_recommendations.heading": "Folgje minsken dêr’t jo graach berjochten fan sjen wolle! Hjir binne wat suggestjes.", + "follow_recommendations.lead": "Berjochten fan minsken dy’t jo folgje sille yn gronologyske folchoarder op jo starttiidline ferskine. Wês net bang om hjiryn flaters te meitsjen, want jo kinne minsken op elk momint krekt sa ienfâldich ûntfolgje!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/ga.json b/app/javascript/flavours/glitch/locales/ga.json index 4d243f94c2..9298ac3ccd 100644 --- a/app/javascript/flavours/glitch/locales/ga.json +++ b/app/javascript/flavours/glitch/locales/ga.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Is cosúil nár fhéadfaí moltaí a ghineadh. D'fhéadfá cuardach a úsáid le teacht ar dhaoine a bhfuil aithne agat orthu, nó iniúchadh ar haischlibeanna atá ag treochtáil a dhéanamh.", + "follow_recommendations.done": "Déanta", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/gd.json b/app/javascript/flavours/glitch/locales/gd.json index 4d243f94c2..874d3308b0 100644 --- a/app/javascript/flavours/glitch/locales/gd.json +++ b/app/javascript/flavours/glitch/locales/gd.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Chan urrainn dhuinn dad a mholadh dhut. Cleachd gleus an luirg feuch an lorg thu daoine air a bheil thu eòlach no rùraich na tagaichean-hais a tha a’ treandadh.", + "follow_recommendations.done": "Deiseil", + "follow_recommendations.heading": "Lean daoine ma tha thu airson na postaichean aca fhaicinn! Seo moladh no dà dhut.", + "follow_recommendations.lead": "Nochdaidh na postaichean aig na daoine a leanas tu a-rèir an ama nad dhachaigh. Bi dàna on as urrainn dhut sgur de dhaoine a leantainn cuideachd uair sam bith!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/gl.json b/app/javascript/flavours/glitch/locales/gl.json index 4d243f94c2..27c1dc8fc5 100644 --- a/app/javascript/flavours/glitch/locales/gl.json +++ b/app/javascript/flavours/glitch/locales/gl.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Semella que non temos suxestións para ti. Podes utilizar a busca para atopar persoas que coñezas ou explorar os cancelos en voga.", + "follow_recommendations.done": "Feito", + "follow_recommendations.heading": "Segue a persoas das que queiras ler publicacións! Aqui tes unhas suxestións.", + "follow_recommendations.lead": "As publicacións das persoas que segues aparecerán na túa cronoloxía de inicio ordenadas temporalmente. Non teñas medo a equivocarte, podes deixar de seguirlas igual de fácil en calquera momento!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/he.json b/app/javascript/flavours/glitch/locales/he.json index 4d243f94c2..e3ecf8f71f 100644 --- a/app/javascript/flavours/glitch/locales/he.json +++ b/app/javascript/flavours/glitch/locales/he.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "נראה שלא ניתן לייצר המלצות עבורך. נסה/י להשתמש בחיפוש כדי למצוא אנשים מוכרים או לבדוק את הנושאים החמים.", + "follow_recommendations.done": "בוצע", + "follow_recommendations.heading": "עקב/י אחרי אנשים שתרצה/י לראות את הודעותיהם! הנה כמה הצעות.", + "follow_recommendations.lead": "הודעות מאנשים במעקב יופיעו בסדר כרונולוגי בפיד הבית. אל תחששו מטעויות, אפשר להסיר מעקב באותה הקלות ובכל זמן!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/hi.json b/app/javascript/flavours/glitch/locales/hi.json index f6eb75f841..bc7ea8e582 100644 --- a/app/javascript/flavours/glitch/locales/hi.json +++ b/app/javascript/flavours/glitch/locales/hi.json @@ -11,6 +11,10 @@ "account_note.save": "सेव", "advanced_options.icon_title": "एडवांस्ड ऑप्शन्स", "advanced_options.local-only.long": "दूसरे इंस्टेंसों में पोस्ट ना करें", + "empty_column.follow_recommendations": "ऐसा लगता है कि आपके लिए कोई सुझाव जेनरेट नहीं किया जा सका. आप उन लोगों को खोजने के लिए सर्च का उपयोग करने का प्रयास कर सकते हैं जिन्हें आप जानते हैं या ट्रेंडिंग हैशटैग का पता लगा सकते हैं।", + "follow_recommendations.done": "पूर्ण", + "follow_recommendations.heading": "उन लोगों का अनुसरण करें जिनकी पोस्ट आप देखना चाहते हैं! यहाँ कुछ सुझाव हैं।", + "follow_recommendations.lead": "आपके द्वारा फ़ॉलो किए जाने वाले लोगों के पोस्ट आपके होम फ़ीड पर कालानुक्रमिक क्रम में दिखाई देंगे। गलतियाँ करने से न डरें, आप किसी भी समय लोगों को उतनी ही आसानी से अनफ़ॉलो कर सकते हैं!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/hr.json b/app/javascript/flavours/glitch/locales/hr.json index 4d243f94c2..e2dcae22ea 100644 --- a/app/javascript/flavours/glitch/locales/hr.json +++ b/app/javascript/flavours/glitch/locales/hr.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Čini se da se ne postoje sugestije generirane za tebe. Možeš pokušati koristiti pretragu kako bi pronašao osobe koje poznaš ili istraži popularne hashtagove.", + "follow_recommendations.done": "Učinjeno", + "follow_recommendations.heading": "Zaprati osobe čije objave želiš vidjeti! Evo nekoliko prijedloga.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/hu.json b/app/javascript/flavours/glitch/locales/hu.json index 4d243f94c2..7b0eb8dd12 100644 --- a/app/javascript/flavours/glitch/locales/hu.json +++ b/app/javascript/flavours/glitch/locales/hu.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Úgy tűnik, neked nem tudunk javaslatokat adni. Próbáld a keresést használni olyanok megtalálására, akiket ismerhetsz, vagy fedezd fel a felkapott hastageket.", + "follow_recommendations.done": "Kész", + "follow_recommendations.heading": "Kövesd azokat, akiknek a bejegyzéseit látni szeretnéd! Itt van néhány javaslat.", + "follow_recommendations.lead": "Az általad követettek bejegyzései a saját idővonaladon fognak megjelenni időrendi sorrendben. Ne félj attól, hogy hibázol! A követést bármikor, ugyanilyen könnyen visszavonhatod!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/hy.json b/app/javascript/flavours/glitch/locales/hy.json index 4d243f94c2..b892918f37 100644 --- a/app/javascript/flavours/glitch/locales/hy.json +++ b/app/javascript/flavours/glitch/locales/hy.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Կարծես քեզ համար ոչ մի առաջարկ չի գեներացուել։ Օգտագործիր որոնման դաշտը մարդկանց փնտրելու համար կամ բացայայտիր յայտնի պիտակներով։", + "follow_recommendations.done": "Աւարտուած է", + "follow_recommendations.heading": "Հետեւիր այն մարդկանց, որոնց գրառումները կը ցանկանաս տեսնել։ Ահա մի քանի առաջարկ։", + "follow_recommendations.lead": "Քո հոսքում, ժամանակագրական դասաւորութեամբ կը տեսնես այն մարդկանց գրառումները, որոնց հետեւում ես։ Մի վախեցիր սխալուել, դու միշտ կարող ես հեշտութեամբ ապահետեւել մարդկանց։", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/id.json b/app/javascript/flavours/glitch/locales/id.json index 4d243f94c2..94103fd8a5 100644 --- a/app/javascript/flavours/glitch/locales/id.json +++ b/app/javascript/flavours/glitch/locales/id.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Sepertinya tak ada saran yang dibuat untuk Anda. Anda dapat mencoba menggunakan pencarian untuk menemukan orang yang Anda ketahui atau menjelajahi tagar yang sedang tren.", + "follow_recommendations.done": "Selesai", + "follow_recommendations.heading": "Ikuti orang yang ingin Anda lihat kirimannya! Ini ada beberapa saran.", + "follow_recommendations.lead": "Kiriman dari orang yang Anda ikuti akan tampil berdasar waktu di beranda Anda. Jangan takut membuat kesalahan, Anda dapat berhenti mengikuti mereka dengan mudah kapan saja!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/ig.json b/app/javascript/flavours/glitch/locales/ig.json index 4d243f94c2..f9ed6d5adc 100644 --- a/app/javascript/flavours/glitch/locales/ig.json +++ b/app/javascript/flavours/glitch/locales/ig.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Done", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/io.json b/app/javascript/flavours/glitch/locales/io.json index 4d243f94c2..d774822516 100644 --- a/app/javascript/flavours/glitch/locales/io.json +++ b/app/javascript/flavours/glitch/locales/io.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Semblas tale nula sugestato povas facesar por vu. Vu povas probar trovar personi quon vu forsan konocas o exploras tendenca hashtagi.", + "follow_recommendations.done": "Fina", + "follow_recommendations.heading": "Sequez personi quo igas posti quon vu volas vidar! Hike esas ula sugestati.", + "follow_recommendations.lead": "Posti de personi quon vu sequas kronologiale montresos en vua hemniuzeto. Ne timas igar erori, vu povas desequar personi tam same facila irgatempe!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/is.json b/app/javascript/flavours/glitch/locales/is.json index 4d243f94c2..70d62a461b 100644 --- a/app/javascript/flavours/glitch/locales/is.json +++ b/app/javascript/flavours/glitch/locales/is.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Það lítur út fyrir að ekki hafi verið hægt að útbúa neinar tillögur fyrir þig. Þú getur reynt að leita að fólki sem þú gætir þekkt eða skoðað myllumerki sem eru í umræðunni.", + "follow_recommendations.done": "Lokið", + "follow_recommendations.heading": "Fylgstu með fólki sem þú vilt sjá færslur frá! Hér eru nokkrar tillögur.", + "follow_recommendations.lead": "Færslur frá fólki sem þú fylgist með eru birtar í tímaröð á heimastreyminu þínu. Þú þarft ekki að hræðast mistök, það er jafn auðvelt að hætta að fylgjast með fólki hvenær sem er!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/it.json b/app/javascript/flavours/glitch/locales/it.json index 4d243f94c2..578b4a5956 100644 --- a/app/javascript/flavours/glitch/locales/it.json +++ b/app/javascript/flavours/glitch/locales/it.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Sembra che non sia stato possibile generare alcun suggerimento per te. Puoi provare a utilizzare la ricerca per cercare persone che potresti conoscere, o a esplorare gli hashtag in tendenza.", + "follow_recommendations.done": "Fatto", + "follow_recommendations.heading": "Segui le persone di cui vorresti vedere i post! Ecco alcuni suggerimenti.", + "follow_recommendations.lead": "I post dalle persone che segui appariranno in ordine cronologico sul feed della tua home. Non preoccuparti di fare errori, è altrettanto facile smettere di seguire le persone, in qualsiasi momento!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/ja.json b/app/javascript/flavours/glitch/locales/ja.json index d10cf3addf..d7fd888d9d 100644 --- a/app/javascript/flavours/glitch/locales/ja.json +++ b/app/javascript/flavours/glitch/locales/ja.json @@ -38,8 +38,12 @@ "confirmations.unfilter.edit_filter": "フィルターを編集", "confirmations.unfilter.filters": "適用されたフィルター", "content-type.change": "コンテンツ形式を変更", + "empty_column.follow_recommendations": "おすすめを生成できませんでした。検索を使って知り合いを探したり、トレンドハッシュタグを見てみましょう。", "endorsed_accounts_editor.endorsed_accounts": "紹介しているユーザー", "favourite_modal.combo": "次からは {combo} を押せば、これをスキップできます。", + "follow_recommendations.done": "完了", + "follow_recommendations.heading": "投稿を見たい人をフォローしてください!ここにおすすめがあります。", + "follow_recommendations.lead": "あなたがフォローしている人の投稿は、ホームフィードに時系列で表示されます。いつでも簡単に解除できるので、気軽にフォローしてみてください!", "getting_started.onboarding": "解説を表示", "home.column_settings.advanced": "高度", "home.column_settings.filter_regex": "正規表現でフィルター", diff --git a/app/javascript/flavours/glitch/locales/ka.json b/app/javascript/flavours/glitch/locales/ka.json index 4d243f94c2..f9ed6d5adc 100644 --- a/app/javascript/flavours/glitch/locales/ka.json +++ b/app/javascript/flavours/glitch/locales/ka.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Done", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/kab.json b/app/javascript/flavours/glitch/locales/kab.json index 4d243f94c2..b71a9336df 100644 --- a/app/javascript/flavours/glitch/locales/kab.json +++ b/app/javascript/flavours/glitch/locales/kab.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Immed", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/kk.json b/app/javascript/flavours/glitch/locales/kk.json index 4d243f94c2..f9ed6d5adc 100644 --- a/app/javascript/flavours/glitch/locales/kk.json +++ b/app/javascript/flavours/glitch/locales/kk.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Done", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/kn.json b/app/javascript/flavours/glitch/locales/kn.json index 4d243f94c2..f9ed6d5adc 100644 --- a/app/javascript/flavours/glitch/locales/kn.json +++ b/app/javascript/flavours/glitch/locales/kn.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Done", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/ko.json b/app/javascript/flavours/glitch/locales/ko.json index 30b663b554..23c59b51ba 100644 --- a/app/javascript/flavours/glitch/locales/ko.json +++ b/app/javascript/flavours/glitch/locales/ko.json @@ -47,8 +47,12 @@ "confirmations.unfilter.filters": "적용된 {count, plural, one {필터} other {필터들}}", "content-type.change": "콘텐트 타입", "direct.group_by_conversations": "대화별로 묶기", + "empty_column.follow_recommendations": "당신을 위한 제안이 생성될 수 없는 것 같습니다. 알 수도 있는 사람을 검색하거나 유행하는 해시태그를 둘러볼 수 있습니다.", "endorsed_accounts_editor.endorsed_accounts": "추천하는 계정들", "favourite_modal.combo": "다음엔 {combo}를 눌러 건너뛸 수 있습니다", + "follow_recommendations.done": "완료", + "follow_recommendations.heading": "게시물을 받아 볼 사람을 팔로우하세요! 여기 몇몇 추천이 있습니다.", + "follow_recommendations.lead": "당신이 팔로우 하는 사람들의 게시물이 시간순으로 정렬되어 당신의 홈 피드에 표시될 것입니다. 실수를 두려워 하지 마세요, 언제든지 쉽게 팔로우 취소를 할 수 있습니다!", "getting_started.onboarding": "둘러보기", "home.column_settings.advanced": "고급", "home.column_settings.filter_regex": "정규표현식으로 필터", diff --git a/app/javascript/flavours/glitch/locales/ku.json b/app/javascript/flavours/glitch/locales/ku.json index 4d243f94c2..1dd2b8415e 100644 --- a/app/javascript/flavours/glitch/locales/ku.json +++ b/app/javascript/flavours/glitch/locales/ku.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Wusa dixuye ku ji bo we tu pêşniyar nehatine çêkirin. Hûn dikarin lêgerînê bikarbînin da ku li kesên ku hûn nas dikin bigerin an hashtagên trendî bigerin.", + "follow_recommendations.done": "Qediya", + "follow_recommendations.heading": "Mirovên ku tu dixwazî ji wan peyaman bibînî bişopîne! Hin pêşnîyar li vir in.", + "follow_recommendations.lead": "Li gorî rêza kronolojîkî peyamên mirovên ku tu dişopînî dê demnameya te de xûya bike. Ji xeletiyan netirse, bi awayekî hêsan her wextî tu dikarî dev ji şopandinê berdî!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/kw.json b/app/javascript/flavours/glitch/locales/kw.json index 4d243f94c2..2e772d5f0c 100644 --- a/app/javascript/flavours/glitch/locales/kw.json +++ b/app/javascript/flavours/glitch/locales/kw.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Gwrys", + "follow_recommendations.heading": "Holyewgh tus a vynnowgh gweles postow anedha! Ottomma nebes profyansow.", + "follow_recommendations.lead": "Postow a dus a holyewgh a wra omdhiskwedhes omma yn aray termynel yn agas lin dre. Na borthewgh own a gammwul, hwi a yll p'eurpynag anholya tus mar es poran!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/la.json b/app/javascript/flavours/glitch/locales/la.json index 4d243f94c2..e5de322755 100644 --- a/app/javascript/flavours/glitch/locales/la.json +++ b/app/javascript/flavours/glitch/locales/la.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Confectum", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/lt.json b/app/javascript/flavours/glitch/locales/lt.json index 4d243f94c2..f9ed6d5adc 100644 --- a/app/javascript/flavours/glitch/locales/lt.json +++ b/app/javascript/flavours/glitch/locales/lt.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Done", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/lv.json b/app/javascript/flavours/glitch/locales/lv.json index 4d243f94c2..eea1872fe7 100644 --- a/app/javascript/flavours/glitch/locales/lv.json +++ b/app/javascript/flavours/glitch/locales/lv.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Neizdevās ģenerēt tev pielāgotus ieteikumus. Vari mēģināt izmantot meklēšanu, lai meklētu cilvēkus, kurus tu varētu pazīt, vai izpētīt populārākos tēmturus.", + "follow_recommendations.done": "Izpildīts", + "follow_recommendations.heading": "Seko cilvēkiem, no kuriem vēlies redzēt ziņas! Šeit ir daži ieteikumi.", + "follow_recommendations.lead": "Ziņas no cilvēkiem, kuriem seko, mājas plūsmā tiks parādītas hronoloģiskā secībā. Nebaidies kļūdīties, tu tikpat viegli vari pārtraukt sekot cilvēkiem jebkurā laikā!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/mk.json b/app/javascript/flavours/glitch/locales/mk.json index 4d243f94c2..f9ed6d5adc 100644 --- a/app/javascript/flavours/glitch/locales/mk.json +++ b/app/javascript/flavours/glitch/locales/mk.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Done", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/ml.json b/app/javascript/flavours/glitch/locales/ml.json index 4d243f94c2..7e6c479d70 100644 --- a/app/javascript/flavours/glitch/locales/ml.json +++ b/app/javascript/flavours/glitch/locales/ml.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "പൂര്‍ത്തിയായീ", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/mr.json b/app/javascript/flavours/glitch/locales/mr.json index 4d243f94c2..f9ed6d5adc 100644 --- a/app/javascript/flavours/glitch/locales/mr.json +++ b/app/javascript/flavours/glitch/locales/mr.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Done", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/ms.json b/app/javascript/flavours/glitch/locales/ms.json index 4d243f94c2..2a2fe3e585 100644 --- a/app/javascript/flavours/glitch/locales/ms.json +++ b/app/javascript/flavours/glitch/locales/ms.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Nampaknya tiada cadangan yang boleh dijana untuk anda. Anda boleh cuba gunakan gelintar untuk mencari orang yang anda mungkin kenal atau jelajahi tanda pagar sohor kini.", + "follow_recommendations.done": "Selesai", + "follow_recommendations.heading": "Ikuti orang yang anda ingin lihat hantarannya! Di sini ada beberapa cadangan.", + "follow_recommendations.lead": "Hantaran daripada orang yang anda ikuti akan muncul dalam susunan kronologi di suapan rumah anda. Jangan takut melakukan kesilapan, anda boleh nyahikuti orang dengan mudah pada bila-bila masa!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/my.json b/app/javascript/flavours/glitch/locales/my.json index 4d243f94c2..d5a71c9d96 100644 --- a/app/javascript/flavours/glitch/locales/my.json +++ b/app/javascript/flavours/glitch/locales/my.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "သင့်အတွက် အကြံဉာဏ်များ မရရှိနိုင်ပါ။ သင်သိနိုင်သည့်လူများ သို့မဟုတ် ခေတ်စားနေသည့် hashtags များ ရှာဖွေရန်တို့အတွက် ရှာဖွေမှုကို အသုံးပြုနိုင်သည်။", + "follow_recommendations.done": "ပြီးပါပြီ", + "follow_recommendations.heading": "သင်မြင်လိုသော သူများထံမှ ပို့စ်များကို စောင့်ကြည့်ပါ။ ဤတွင် အကြံပြုချက်အချို့ရှိပါသည်။", + "follow_recommendations.lead": "သင်စောင့်ကြည့်ထားသူများ၏ ပို့စ်များမှာ သင့်ပင်မစာမျက်နှာတွင် အချိန်နှင့်တပြေးညီ ပေါ်လာပါမည်။ မကြောက်ပါနှင့်။ အချိန်မရွေး စောင့်ကြည့်ခြင်းအား ရပ်တန့်နိုင်ပါသည်။", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/nl.json b/app/javascript/flavours/glitch/locales/nl.json index 4d243f94c2..51a96814da 100644 --- a/app/javascript/flavours/glitch/locales/nl.json +++ b/app/javascript/flavours/glitch/locales/nl.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Het lijkt er op dat er geen aanbevelingen voor jou aangemaakt kunnen worden. Je kunt proberen te zoeken naar mensen die je wellicht kent, zoeken op hashtags, de lokale en globale tijdlijnen bekijken of de gebruikersgids doorbladeren.", + "follow_recommendations.done": "Klaar", + "follow_recommendations.heading": "Volg mensen waarvan je graag berichten wil zien! Hier zijn enkele aanbevelingen.", + "follow_recommendations.lead": "Berichten van mensen die je volgt zullen in chronologische volgorde op jouw starttijdlijn verschijnen. Wees niet bang om hierin fouten te maken, want je kunt mensen op elk moment net zo eenvoudig ontvolgen!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/nn.json b/app/javascript/flavours/glitch/locales/nn.json index 4d243f94c2..228b27512d 100644 --- a/app/javascript/flavours/glitch/locales/nn.json +++ b/app/javascript/flavours/glitch/locales/nn.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Det ser ikkje ut til at noko forslag kunne genererast til deg. Prøv søkjefunksjonen for å finna folk du kjenner, eller utforsk populære emneknaggar.", + "follow_recommendations.done": "Ferdig", + "follow_recommendations.heading": "Fylg folk du ønsker å sjå innlegg frå! Her er nokre forslag.", + "follow_recommendations.lead": "Innlegg frå folk du fylgjer, kjem kronologisk i heimestraumen din. Ikkje ver redd for å gjera feil, du kan enkelt avfylgja folk når som helst!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/no.json b/app/javascript/flavours/glitch/locales/no.json index 4d243f94c2..ca0ee6213c 100644 --- a/app/javascript/flavours/glitch/locales/no.json +++ b/app/javascript/flavours/glitch/locales/no.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Ser ut som at det ikke finnes noen forslag for deg. Du kan prøve å bruke søk for å se etter folk du kan vite eller utforske trendende hashtags.", + "follow_recommendations.done": "Utført", + "follow_recommendations.heading": "Følg folk du ønsker å se innlegg fra! Her er noen forslag.", + "follow_recommendations.lead": "Innlegg fra mennesker du følger vil vises i kronologisk rekkefølge på hjemmefeed. Ikke vær redd for å gjøre feil, du kan slutte å følge folk like enkelt som alt!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/oc.json b/app/javascript/flavours/glitch/locales/oc.json index 4d243f94c2..441ad8ded9 100644 --- a/app/javascript/flavours/glitch/locales/oc.json +++ b/app/javascript/flavours/glitch/locales/oc.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Acabat", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/pa.json b/app/javascript/flavours/glitch/locales/pa.json index 4d243f94c2..f9ed6d5adc 100644 --- a/app/javascript/flavours/glitch/locales/pa.json +++ b/app/javascript/flavours/glitch/locales/pa.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Done", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/pl.json b/app/javascript/flavours/glitch/locales/pl.json index 11279c4232..107ad31ca4 100644 --- a/app/javascript/flavours/glitch/locales/pl.json +++ b/app/javascript/flavours/glitch/locales/pl.json @@ -45,8 +45,12 @@ "confirmations.unfilter.edit_filter": "Edytuj filtr", "content-type.change": "Typ zawartości", "direct.group_by_conversations": "Grupuj rozmowami", + "empty_column.follow_recommendations": "Wygląda na to, że nie można wygenerować dla Ciebie żadnych sugestii. Możesz spróbować wyszukać osoby, które znasz, lub przeglądać popularne hasztagi.", "endorsed_accounts_editor.endorsed_accounts": "Wybrane konta", "favourite_modal.combo": "Możesz nacisnąć {combo}, aby pominąć to następnym razem", + "follow_recommendations.done": "Gotowe", + "follow_recommendations.heading": "Obserwuj ludzi, których wpisy chcesz czytać. Oto kilka propozycji.", + "follow_recommendations.lead": "Wpisy osób, które obserwujesz będą pojawiać się w porządku chronologicznym na stronie głównej. Nie bój się popełniać błędów, możesz bez problemu przestać obserwować każdego w każdej chwili!", "getting_started.onboarding": "Rozejrzyj się", "home.column_settings.advanced": "Zaawansowane", "home.column_settings.filter_regex": "Filtruj, używając wyrażeń regularnych", diff --git a/app/javascript/flavours/glitch/locales/pt-BR.json b/app/javascript/flavours/glitch/locales/pt-BR.json index 80e102a4e7..4387f4b0a9 100644 --- a/app/javascript/flavours/glitch/locales/pt-BR.json +++ b/app/javascript/flavours/glitch/locales/pt-BR.json @@ -47,8 +47,12 @@ "confirmations.unfilter.filters": "Correspondência de {count, plural, one {filtro} other {filtros}}", "content-type.change": "Tipo de conteúdo", "direct.group_by_conversations": "Agrupar por conversa", + "empty_column.follow_recommendations": "Parece que não há sugestões para você. Tente usar a pesquisa para encontrar pessoas que você possa conhecer ou explorar hashtags.", "endorsed_accounts_editor.endorsed_accounts": "Contas em destaque", "favourite_modal.combo": "Você pode pressionar {combo} para pular isso da próxima vez", + "follow_recommendations.done": "Salvar", + "follow_recommendations.heading": "Siga pessoas que você gostaria de acompanhar! Aqui estão algumas sugestões.", + "follow_recommendations.lead": "Toots de pessoas que você segue aparecerão em ordem cronológica na página inicial. Não tenha medo de cometer erros, você pode facilmente deixar de seguir a qualquer momento!", "getting_started.onboarding": "Mostre-me ao redor", "home.column_settings.advanced": "Avançado", "home.column_settings.filter_regex": "Filtrar com uma expressão regular", diff --git a/app/javascript/flavours/glitch/locales/pt-PT.json b/app/javascript/flavours/glitch/locales/pt-PT.json index fc3cdc6218..65ccd22fba 100644 --- a/app/javascript/flavours/glitch/locales/pt-PT.json +++ b/app/javascript/flavours/glitch/locales/pt-PT.json @@ -18,6 +18,10 @@ "advanced_options.threaded_mode.short": "Modo de fio", "advanced_options.threaded_mode.tooltip": "Modo de fio ativado", "boost_modal.missing_description": "Este post contém alguns media sem descrição", + "empty_column.follow_recommendations": "Parece que não foi possível gerar nenhuma sugestão para si. Pode tentar utilizar a pesquisa para procurar pessoas que conheça ou explorar as #etiquetas em destaque.", + "follow_recommendations.done": "Concluído", + "follow_recommendations.heading": "Siga pessoas das quais gostaria de ver publicações! Aqui estão algumas sugestões.", + "follow_recommendations.lead": "As publicações das pessoas que segue serão exibidos em ordem cronológica na sua página inicial. Não tenha medo de cometer erros, você pode deixar de seguir as pessoas tão facilmente a qualquer momento!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/ro.json b/app/javascript/flavours/glitch/locales/ro.json index 4d243f94c2..43705c0438 100644 --- a/app/javascript/flavours/glitch/locales/ro.json +++ b/app/javascript/flavours/glitch/locales/ro.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Se pare că nu am putut genera nicio sugestie pentru tine. Poți încerca funcția de căutare pentru a căuta persoane pe care le cunoști, sau poți explora tendințele.", + "follow_recommendations.done": "Terminat", + "follow_recommendations.heading": "Urmărește persoanele ale căror postări te-ar interesa! Iată câteva sugestii.", + "follow_recommendations.lead": "Postările de la persoanele la care te-ai abonat vor apărea în ordine cronologică în cronologia principală. Nu-ți fie teamă să faci greșeli, poți să te dezabonezi oricând de la ei la fel de ușor!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/ru.json b/app/javascript/flavours/glitch/locales/ru.json index 4d243f94c2..030d0e6c51 100644 --- a/app/javascript/flavours/glitch/locales/ru.json +++ b/app/javascript/flavours/glitch/locales/ru.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Похоже, у нас нет предложений для вас. Вы можете попробовать поискать людей, которых уже знаете, или изучить актуальные хэштеги.", + "follow_recommendations.done": "Готово", + "follow_recommendations.heading": "Подпишитесь на людей, чьи посты вы бы хотели видеть. Вот несколько предложений.", + "follow_recommendations.lead": "Посты от людей, на которых вы подписаны, будут отображаться в вашей домашней ленте в хронологическом порядке. Не бойтесь ошибиться — вы так же легко сможете отписаться от них в любое время!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/sa.json b/app/javascript/flavours/glitch/locales/sa.json index 4d243f94c2..494676062d 100644 --- a/app/javascript/flavours/glitch/locales/sa.json +++ b/app/javascript/flavours/glitch/locales/sa.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "तुभ्यं कोऽपि प्रबोधो न जनयितुं न शक्यतेऽति प्रतीयते। अन्वेष्यप्रणालिं प्रयोक्तुं जनानवलोकयितुं शक्नोषि उत प्रचलितवस्तूनि अन्वेषितुं शक्नोषि।", + "follow_recommendations.done": "कृतम्", + "follow_recommendations.heading": "जनमनुसर यस्मात्पत्राणि द्रष्टुमिच्छसि! सन्ति इह काश्चित्सूचनाः।", + "follow_recommendations.lead": "जनेभ्यः पत्राणि याननुसरसि कालिकक्रमेण दर्शिष्यन्ते तव गृहनिरासे। दोषान्मा बिभीहि, कदापि सरलरूपेण ते जनाननुसरणं वारयितुं शक्नोषि!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/sc.json b/app/javascript/flavours/glitch/locales/sc.json index 4d243f94c2..36c7315d9f 100644 --- a/app/javascript/flavours/glitch/locales/sc.json +++ b/app/javascript/flavours/glitch/locales/sc.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Fatu", + "follow_recommendations.heading": "Sighi gente de chie boles bìdere is publicatziones! Càstia custos cussìgios.", + "follow_recommendations.lead": "Is messàgios de gente a sa chi ses sighende ant a èssere ammustrados in òrdine cronològicu in sa lìnia de tempus printzipale tua. Non timas de fàghere errores, acabbare de sighire gente est fàtzile in cale si siat momentu!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/sco.json b/app/javascript/flavours/glitch/locales/sco.json index 4d243f94c2..f8a3d59e98 100644 --- a/app/javascript/flavours/glitch/locales/sco.json +++ b/app/javascript/flavours/glitch/locales/sco.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "It luiks lik nae suggestions cuid be generatit fir ye. Ye kin try uisin seirch fir tae luik fir fowk ye wad mibbie ken, or splore uisin trendin hashtags.", + "follow_recommendations.done": "Duin", + "follow_recommendations.heading": "Follae fowk thit ye'd want tae see posts fae! Here some suggestions.", + "follow_recommendations.lead": "Posts fae thaim thit ye follae wull shaw up in chronological order on yer hame feed. Dinnae be feart tae mak mistaks, ye kin unfollae fowk juist as easy onie tim!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/si.json b/app/javascript/flavours/glitch/locales/si.json index 4d243f94c2..2a11d24301 100644 --- a/app/javascript/flavours/glitch/locales/si.json +++ b/app/javascript/flavours/glitch/locales/si.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "ඔබ වෙනුවෙන් යෝජනා ජනනය කළ නොහැකි බව පෙනේ. ඔබ දන්නා හඳුනන පුද්ගලයින් සෙවීමට හෝ ප්‍රවණතා හැෂ් ටැග් ගවේෂණය කිරීමට ඔබට සෙවීම භාවිත කිරීමට උත්සාහ කළ හැක.", + "follow_recommendations.done": "අහවරයි", + "follow_recommendations.heading": "ඔබ පළ කිරීම් බැලීමට කැමති පුද්ගලයින් අනුගමනය කරන්න! මෙන්න යෝජනා කිහිපයක්.", + "follow_recommendations.lead": "ඔබ අනුගමන කරන පුද්ගලයින්ගේ පළ කිරීම් ඔබගේ නිවසේ සංග්‍රහයේ කාලානුක්‍රමික අනුපිළිවෙලට පෙන්වනු ඇත. වැරදි කිරීමට බිය නොවන්න, ඔබට ඕනෑම වේලාවක පහසුවෙන් මිනිසුන් අනුගමනය කළ නොහැක!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/sk.json b/app/javascript/flavours/glitch/locales/sk.json index 4d243f94c2..92ec64b9d1 100644 --- a/app/javascript/flavours/glitch/locales/sk.json +++ b/app/javascript/flavours/glitch/locales/sk.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Zdá sa že pre Vás nemohli byť vygenerované žiadne návrhy. Môžete skúsiť použiť vyhľadávanie aby ste našli ľudi ktorých poznáte, alebo preskúmať trendujúce heštegy.", + "follow_recommendations.done": "Hotovo", + "follow_recommendations.heading": "Následuj ľudí od ktorých by si chcel/a vidieť príspevky! Tu sú nejaké návrhy.", + "follow_recommendations.lead": "Príspevky od ľudi ktorých sledujete sa zobrazia v chronologickom poradí na Vašej nástenke. Nebojte sa spraviť chyby, vždy môžete zrušiť sledovanie konkrétnych ľudí!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/sl.json b/app/javascript/flavours/glitch/locales/sl.json index 4d243f94c2..a5306b5bc3 100644 --- a/app/javascript/flavours/glitch/locales/sl.json +++ b/app/javascript/flavours/glitch/locales/sl.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Kaže, da za vas ni mogoče pripraviti nobenih predlogov. Poskusite uporabiti iskanje, da poiščete osebe, ki jih poznate, ali raziščete ključnike, ki so v trendu.", + "follow_recommendations.done": "Opravljeno", + "follow_recommendations.heading": "Sledite osebam, katerih objave želite videti! Tukaj je nekaj predlogov.", + "follow_recommendations.lead": "Objave oseb, ki jim sledite, se bodo prikazale v kronološkem zaporedju v vašem domačem viru. Ne bojte se storiti napake, osebam enako enostavno nehate slediti kadar koli!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/sq.json b/app/javascript/flavours/glitch/locales/sq.json index 4d243f94c2..f7705ebdbc 100644 --- a/app/javascript/flavours/glitch/locales/sq.json +++ b/app/javascript/flavours/glitch/locales/sq.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Duket se s’u prodhuan dot sugjerime për ju. Mund të provoni të kërkoni për persona që mund të njihni, ose të eksploroni hashtag-ë që janë në modë.", + "follow_recommendations.done": "U bë", + "follow_recommendations.heading": "Ndiqni persona prej të cilëve doni të shihni postime! Ja ca sugjerime.", + "follow_recommendations.lead": "Postimet prej personash që ndiqni do të shfaqen në rend kohor te prurja juaj kryesore. Mos kini frikë të bëni gabime, mund të ndalni po aq kollaj ndjekjen e dikujt, në çfarëdo kohe!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/sr-Latn.json b/app/javascript/flavours/glitch/locales/sr-Latn.json index 4d243f94c2..d24fc17c2e 100644 --- a/app/javascript/flavours/glitch/locales/sr-Latn.json +++ b/app/javascript/flavours/glitch/locales/sr-Latn.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Izgleda da ne mogu da se generišu bilo kakvi predlozi za vas. Možete pokušati da koristite pretragu kako biste potražili ljude koje možda poznajete ili istražili popularne heš oznake.", + "follow_recommendations.done": "Gotovo", + "follow_recommendations.heading": "Pratite ljude čije objave želite da vidite! Evo nekih predloga.", + "follow_recommendations.lead": "Objave korisnika koje pratite će se pojavljivati hronološkim redosledom na početnoj stranici. Ne plašite se grešaka, možete otpratiti korisnike u bilo kom trenutku!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/sr.json b/app/javascript/flavours/glitch/locales/sr.json index 4d243f94c2..ab58c2e62f 100644 --- a/app/javascript/flavours/glitch/locales/sr.json +++ b/app/javascript/flavours/glitch/locales/sr.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Изгледа да не могу да се генеришу било какви предлози за вас. Можете покушати да користите претрагу како бисте потражили људе које можда познајете или истражили популарне хеш ознаке.", + "follow_recommendations.done": "Готово", + "follow_recommendations.heading": "Пратите људе чије објаве желите да видите! Ево неких предлога.", + "follow_recommendations.lead": "Објаве корисника које пратите ће се појављивати хронолошким редоследом на почетној страници. Не плашите се грешака, можете отпратити кориснике у било ком тренутку!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/sv.json b/app/javascript/flavours/glitch/locales/sv.json index 4d243f94c2..e73215e610 100644 --- a/app/javascript/flavours/glitch/locales/sv.json +++ b/app/javascript/flavours/glitch/locales/sv.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Det ser ut som om inga förslag kan genereras till dig. Du kan prova att använda sök för att leta efter personer som du kanske känner eller utforska trendande hash-taggar.", + "follow_recommendations.done": "Klar", + "follow_recommendations.heading": "Följ personer du skulle vilja se inlägg från! Här kommer några förslag.", + "follow_recommendations.lead": "Inlägg från personer du följer kommer att dyka upp i kronologisk ordning i ditt hemflöde. Var inte rädd för att göra misstag, du kan sluta följa folk när som helst!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/szl.json b/app/javascript/flavours/glitch/locales/szl.json index 6fd7dc2693..bc033e4928 100644 --- a/app/javascript/flavours/glitch/locales/szl.json +++ b/app/javascript/flavours/glitch/locales/szl.json @@ -47,8 +47,12 @@ "confirmations.unfilter.filters": "Matching {count, plural, one {filter} other {filters}}", "content-type.change": "Content type", "direct.group_by_conversations": "Group by conversation", + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", "endorsed_accounts_editor.endorsed_accounts": "Featured accounts", "favourite_modal.combo": "You can press {combo} to skip this next time", + "follow_recommendations.done": "Done", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "getting_started.onboarding": "Show me around", "home.column_settings.advanced": "Advanced", "home.column_settings.filter_regex": "Filter out by regular expressions", diff --git a/app/javascript/flavours/glitch/locales/ta.json b/app/javascript/flavours/glitch/locales/ta.json index 4d243f94c2..f9ed6d5adc 100644 --- a/app/javascript/flavours/glitch/locales/ta.json +++ b/app/javascript/flavours/glitch/locales/ta.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Done", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/tai.json b/app/javascript/flavours/glitch/locales/tai.json index 6fd7dc2693..bc033e4928 100644 --- a/app/javascript/flavours/glitch/locales/tai.json +++ b/app/javascript/flavours/glitch/locales/tai.json @@ -47,8 +47,12 @@ "confirmations.unfilter.filters": "Matching {count, plural, one {filter} other {filters}}", "content-type.change": "Content type", "direct.group_by_conversations": "Group by conversation", + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", "endorsed_accounts_editor.endorsed_accounts": "Featured accounts", "favourite_modal.combo": "You can press {combo} to skip this next time", + "follow_recommendations.done": "Done", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "getting_started.onboarding": "Show me around", "home.column_settings.advanced": "Advanced", "home.column_settings.filter_regex": "Filter out by regular expressions", diff --git a/app/javascript/flavours/glitch/locales/te.json b/app/javascript/flavours/glitch/locales/te.json index 4d243f94c2..f9ed6d5adc 100644 --- a/app/javascript/flavours/glitch/locales/te.json +++ b/app/javascript/flavours/glitch/locales/te.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Done", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/th.json b/app/javascript/flavours/glitch/locales/th.json index 4d243f94c2..a5f452a9a7 100644 --- a/app/javascript/flavours/glitch/locales/th.json +++ b/app/javascript/flavours/glitch/locales/th.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "ดูเหมือนว่าจะไม่สามารถสร้างข้อเสนอแนะสำหรับคุณ คุณสามารถลองใช้การค้นหาเพื่อมองหาผู้คนที่คุณอาจรู้จักหรือสำรวจแฮชแท็กที่กำลังนิยม", + "follow_recommendations.done": "เสร็จสิ้น", + "follow_recommendations.heading": "ติดตามผู้คนที่คุณต้องการเห็นโพสต์! นี่คือข้อเสนอแนะบางส่วน", + "follow_recommendations.lead": "โพสต์จากผู้คนที่คุณติดตามจะแสดงตามลำดับเวลาในฟีดหน้าแรกของคุณ อย่ากลัวที่จะทำผิดพลาด คุณสามารถเลิกติดตามผู้คนได้อย่างง่ายดายเมื่อใดก็ตาม!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/tr.json b/app/javascript/flavours/glitch/locales/tr.json index 4d243f94c2..b8b440c99c 100644 --- a/app/javascript/flavours/glitch/locales/tr.json +++ b/app/javascript/flavours/glitch/locales/tr.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Öyle görünüyor ki sizin için hiçbir öneri oluşturulamıyor. Tanıdığınız kişileri aramak için aramayı kullanabilir veya öne çıkanlara bakabilirsiniz.", + "follow_recommendations.done": "Tamam", + "follow_recommendations.heading": "Gönderilerini görmek isteyeceğiniz kişileri takip edin! Burada bazı öneriler bulabilirsiniz.", + "follow_recommendations.lead": "Takip ettiğiniz kişilerin gönderileri anasayfa akışınızda kronolojik sırada görünmeye devam edecek. Hata yapmaktan çekinmeyin, kişileri istediğiniz anda kolayca takipten çıkabilirsiniz!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/tt.json b/app/javascript/flavours/glitch/locales/tt.json index 4d243f94c2..96c7a7f559 100644 --- a/app/javascript/flavours/glitch/locales/tt.json +++ b/app/javascript/flavours/glitch/locales/tt.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Булды", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/ug.json b/app/javascript/flavours/glitch/locales/ug.json index 4d243f94c2..f9ed6d5adc 100644 --- a/app/javascript/flavours/glitch/locales/ug.json +++ b/app/javascript/flavours/glitch/locales/ug.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Done", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/uk.json b/app/javascript/flavours/glitch/locales/uk.json index b215846591..793a96fc9b 100644 --- a/app/javascript/flavours/glitch/locales/uk.json +++ b/app/javascript/flavours/glitch/locales/uk.json @@ -5,7 +5,11 @@ "compose.attach": "Вкласти...", "compose.attach.doodle": "Помалювати", "compose.attach.upload": "Завантажити сюди файл", + "empty_column.follow_recommendations": "Схоже, для вас не було створено жодної пропозиції. Ви можете спробувати скористатися пошуком людей, яких ви можете знати, або переглянути популярні гештеґи.", "favourite_modal.combo": "Ви можете натиснути {combo}, щоб пропустити це наступного разу", + "follow_recommendations.done": "Готово", + "follow_recommendations.heading": "Підпишіться на людей, чиї дописи ви хочете бачити! Ось деякі пропозиції.", + "follow_recommendations.lead": "Дописи від людей, за якими ви стежите, з'являться в хронологічному порядку у вашій домашній стрічці. Не бійся помилятися, ви можете відписатися від людей так само легко в будь-який час!", "getting_started.onboarding": "Шо тут", "home.column_settings.show_direct": "Показати прямі повідомлення", "layout.auto": "Автоматичний", diff --git a/app/javascript/flavours/glitch/locales/ur.json b/app/javascript/flavours/glitch/locales/ur.json index 4d243f94c2..f9ed6d5adc 100644 --- a/app/javascript/flavours/glitch/locales/ur.json +++ b/app/javascript/flavours/glitch/locales/ur.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", + "follow_recommendations.done": "Done", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/vi.json b/app/javascript/flavours/glitch/locales/vi.json index 4d243f94c2..1ad8182dda 100644 --- a/app/javascript/flavours/glitch/locales/vi.json +++ b/app/javascript/flavours/glitch/locales/vi.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "Bạn chưa có gợi ý theo dõi nào. Hãy thử tìm kiếm những người thú vị hoặc khám phá những hashtag nổi bật.", + "follow_recommendations.done": "Xong", + "follow_recommendations.heading": "Theo dõi những người bạn muốn đọc tút của họ! Dưới đây là vài gợi ý.", + "follow_recommendations.lead": "Tút từ những người bạn theo dõi sẽ hiện theo thứ tự thời gian trên bảng tin. Đừng ngại, bạn có thể dễ dàng ngưng theo dõi họ bất cứ lúc nào!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/zgh.json b/app/javascript/flavours/glitch/locales/zgh.json index 6fd7dc2693..bc033e4928 100644 --- a/app/javascript/flavours/glitch/locales/zgh.json +++ b/app/javascript/flavours/glitch/locales/zgh.json @@ -47,8 +47,12 @@ "confirmations.unfilter.filters": "Matching {count, plural, one {filter} other {filters}}", "content-type.change": "Content type", "direct.group_by_conversations": "Group by conversation", + "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", "endorsed_accounts_editor.endorsed_accounts": "Featured accounts", "favourite_modal.combo": "You can press {combo} to skip this next time", + "follow_recommendations.done": "Done", + "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", + "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", "getting_started.onboarding": "Show me around", "home.column_settings.advanced": "Advanced", "home.column_settings.filter_regex": "Filter out by regular expressions", diff --git a/app/javascript/flavours/glitch/locales/zh-CN.json b/app/javascript/flavours/glitch/locales/zh-CN.json index 5c17580592..dcb0b8b115 100644 --- a/app/javascript/flavours/glitch/locales/zh-CN.json +++ b/app/javascript/flavours/glitch/locales/zh-CN.json @@ -47,8 +47,12 @@ "confirmations.unfilter.filters": "应用 {count, plural, one {过滤器} other {过滤器}}", "content-type.change": "内容类型 ", "direct.group_by_conversations": "以对话分组", + "empty_column.follow_recommendations": "似乎无法为你生成任何建议。你可以尝试使用搜索寻找你可能知道的人或探索热门标签。", "endorsed_accounts_editor.endorsed_accounts": "推荐用户", "favourite_modal.combo": "下次你可以按 {combo} 跳过这个", + "follow_recommendations.done": "完成", + "follow_recommendations.heading": "关注你感兴趣的用户!这里有一些推荐。", + "follow_recommendations.lead": "你关注的人的嘟文将按时间顺序显示在你的主页上。别担心,你可以在任何时候取消对别人的关注!", "getting_started.onboarding": "参观一下", "home.column_settings.advanced": "高级", "home.column_settings.filter_regex": "按正则表达式过滤", diff --git a/app/javascript/flavours/glitch/locales/zh-HK.json b/app/javascript/flavours/glitch/locales/zh-HK.json index 4d243f94c2..9aeefd3f14 100644 --- a/app/javascript/flavours/glitch/locales/zh-HK.json +++ b/app/javascript/flavours/glitch/locales/zh-HK.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "似乎未能替您產生任何建議。您可以試著搜尋您知道的帳戶或者探索熱門主題標籤", + "follow_recommendations.done": "完成", + "follow_recommendations.heading": "追蹤人們以看到他們的帖文!這裡有些建議。", + "follow_recommendations.lead": "您所追蹤的對象之帖文將會以時間順序顯示於您的首頁時間軸上。別擔心犯下錯誤,您隨時可以取消追蹤任何人!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/locales/zh-TW.json b/app/javascript/flavours/glitch/locales/zh-TW.json index 4d243f94c2..abb400b711 100644 --- a/app/javascript/flavours/glitch/locales/zh-TW.json +++ b/app/javascript/flavours/glitch/locales/zh-TW.json @@ -1,4 +1,8 @@ { + "empty_column.follow_recommendations": "似乎未能為您產生任何建議。您可以嘗試使用搜尋來尋找您可能認識的人,或是探索熱門主題標籤。", + "follow_recommendations.done": "完成", + "follow_recommendations.heading": "跟隨您想檢視其嘟文的人!這裡有一些建議。", + "follow_recommendations.lead": "來自您跟隨的人之嘟文將會按時間順序顯示在您的首頁時間軸上。不要害怕犯錯,您隨時都可以取消跟隨其他人!", "onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "settings.content_warnings": "Content warnings", diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index ceaf5bfa6d..a82f68b51f 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -20,9 +20,11 @@ } &__note { - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; color: $ui-secondary-color; } diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss index a72afe7261..f9a5720e51 100644 --- a/app/javascript/flavours/glitch/styles/components/columns.scss +++ b/app/javascript/flavours/glitch/styles/components/columns.scss @@ -62,10 +62,9 @@ $ui-header-height: 55px; height: $ui-header-height; position: sticky; top: 0; - z-index: 2; + z-index: 3; justify-content: space-between; align-items: center; - overflow: hidden; &__logo { display: inline-flex; @@ -75,6 +74,20 @@ $ui-header-height: 55px; height: $ui-header-height - 30px; width: auto; } + + .logo--wordmark { + display: none; + } + + @media screen and (min-width: 320px) { + .logo--wordmark { + display: block; + } + + .logo--icon { + display: none; + } + } } &__links { diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss index e1a6ae3099..4cfd9007b4 100644 --- a/app/javascript/flavours/glitch/styles/components/media.scss +++ b/app/javascript/flavours/glitch/styles/components/media.scss @@ -381,7 +381,6 @@ background: darken($ui-base-color, 8%); border-radius: 4px; padding-bottom: 44px; - direction: ltr; &.editable { border-radius: 0; @@ -448,7 +447,6 @@ max-width: 100%; border-radius: 4px; box-sizing: border-box; - direction: ltr; color: $white; &.editable { @@ -500,6 +498,7 @@ &__controls { position: absolute; + direction: ltr; z-index: 2; bottom: 0; inset-inline-start: 0; diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index c29b0a9b4a..cca50fd8a2 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -566,6 +566,7 @@ .detailed-status { background: lighten($ui-base-color, 4%); padding: 14px 10px; + border-top: 1px solid lighten($ui-base-color, 8%); &--flex { display: flex; @@ -706,6 +707,7 @@ a.status__display-name, margin-inline-end: 10px; height: 48px; width: 48px; + box-shadow: 0 0 0 2px $ui-base-color; } .muted { @@ -849,6 +851,10 @@ a.status-card { .status-card__description { color: $darker-text-color; + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; } .status-card__host { @@ -1041,6 +1047,54 @@ a.status-card.compact:hover { pointer-events: none; } } + + &--in-thread { + border-bottom: 0; + + .status__content, + .status__action-bar { + margin-inline-start: 46px + 10px; + width: calc(100% - (46px + 10px)); + } + } + + &--first-in-thread { + border-top: 1px solid lighten($ui-base-color, 8%); + } + + &__line { + height: 16px - 4px; + border-inline-start: 2px solid lighten($ui-base-color, 8%); + width: 0; + position: absolute; + top: 0; + inset-inline-start: 16px + ((46px - 2px) * 0.5); + + &--full { + top: 0; + height: 100%; + + &::before { + content: ''; + display: block; + position: absolute; + top: 16px - 4px; + height: 46px + 4px + 4px; + width: 2px; + background: $ui-base-color; + inset-inline-start: -2px; + } + } + + &--first { + top: 16px + 46px + 4px; + height: calc(100% - (16px + 46px + 4px)); + + &::before { + display: none; + } + } + } } .picture-in-picture { diff --git a/app/javascript/flavours/glitch/utils/numbers.js b/app/javascript/flavours/glitch/utils/numbers.js index 6ef563ad8f..fa3d58fad1 100644 --- a/app/javascript/flavours/glitch/utils/numbers.js +++ b/app/javascript/flavours/glitch/utils/numbers.js @@ -60,7 +60,6 @@ export function toShortNumber(sourceNumber) { * // => 1790 */ export function pluralReady(sourceNumber, division) { - // eslint-disable-next-line eqeqeq if (division == null || division < DECIMAL_UNITS.HUNDRED) { return sourceNumber; } diff --git a/app/javascript/images/elephant_ui_conversation.svg b/app/javascript/images/elephant_ui_conversation.svg new file mode 100644 index 0000000000..f849b59592 --- /dev/null +++ b/app/javascript/images/elephant_ui_conversation.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index e1db44359c..7e36bd22aa 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -74,6 +74,7 @@ export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTIO export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS'; export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS'; +export const COMPOSE_FOCUS = 'COMPOSE_FOCUS'; const messages = defineMessages({ uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, @@ -125,6 +126,15 @@ export function resetCompose() { }; } +export const focusCompose = (routerHistory, defaultText) => dispatch => { + dispatch({ + type: COMPOSE_FOCUS, + defaultText, + }); + + ensureComposeIsVisible(routerHistory); +}; + export function mentionCompose(account, routerHistory) { return (dispatch, getState) => { dispatch({ diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js index 56608f28ba..605a457a2e 100644 --- a/app/javascript/mastodon/actions/search.js +++ b/app/javascript/mastodon/actions/search.js @@ -135,8 +135,7 @@ export const showSearch = () => ({ type: SEARCH_SHOW, }); -export const openURL = routerHistory => (dispatch, getState) => { - const value = getState().getIn(['search', 'value']); +export const openURL = (value, history, onFailure) => (dispatch, getState) => { const signedIn = !!getState().getIn(['meta', 'me']); if (!signedIn) { @@ -148,15 +147,21 @@ export const openURL = routerHistory => (dispatch, getState) => { api(getState).get('/api/v2/search', { params: { q: value, resolve: true } }).then(response => { if (response.data.accounts?.length > 0) { dispatch(importFetchedAccounts(response.data.accounts)); - routerHistory.push(`/@${response.data.accounts[0].acct}`); + history.push(`/@${response.data.accounts[0].acct}`); } else if (response.data.statuses?.length > 0) { dispatch(importFetchedStatuses(response.data.statuses)); - routerHistory.push(`/@${response.data.statuses[0].account.acct}/${response.data.statuses[0].id}`); + history.push(`/@${response.data.statuses[0].account.acct}/${response.data.statuses[0].id}`); + } else if (onFailure) { + onFailure(); } dispatch(fetchSearchSuccess(response.data, value)); }).catch(err => { dispatch(fetchSearchFail(err)); + + if (onFailure) { + onFailure(); + } }); }; diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx index a8a47ecacb..0e2295e3a9 100644 --- a/app/javascript/mastodon/components/account.jsx +++ b/app/javascript/mastodon/components/account.jsx @@ -12,8 +12,8 @@ import Skeleton from 'mastodon/components/skeleton'; import { Link } from 'react-router-dom'; import { counterRenderer } from 'mastodon/components/common_counter'; import ShortNumber from 'mastodon/components/short_number'; -import Icon from 'mastodon/components/icon'; import classNames from 'classnames'; +import VerifiedBadge from 'mastodon/components/verified_badge'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, @@ -27,26 +27,6 @@ const messages = defineMessages({ block: { id: 'account.block', defaultMessage: 'Block @{name}' }, }); -class VerifiedBadge extends React.PureComponent { - - static propTypes = { - link: PropTypes.string.isRequired, - verifiedAt: PropTypes.string.isRequired, - }; - - render () { - const { link } = this.props; - - return ( - - - - - ); - } - -} - class Account extends ImmutablePureComponent { static propTypes = { diff --git a/app/javascript/mastodon/components/check.jsx b/app/javascript/mastodon/components/check.jsx index ee2ef1595a..2fd0af7401 100644 --- a/app/javascript/mastodon/components/check.jsx +++ b/app/javascript/mastodon/components/check.jsx @@ -1,8 +1,8 @@ import React from 'react'; const Check = () => ( - - + + ); diff --git a/app/javascript/mastodon/components/column_back_button.jsx b/app/javascript/mastodon/components/column_back_button.jsx index 5c5226b7ea..12926bb253 100644 --- a/app/javascript/mastodon/components/column_back_button.jsx +++ b/app/javascript/mastodon/components/column_back_button.jsx @@ -12,13 +12,19 @@ export default class ColumnBackButton extends React.PureComponent { static propTypes = { multiColumn: PropTypes.bool, + onClick: PropTypes.func, }; handleClick = () => { - if (window.history && window.history.state) { - this.context.router.history.goBack(); + const { router } = this.context; + const { onClick } = this.props; + + if (onClick) { + onClick(); + } else if (window.history && window.history.state) { + router.history.goBack(); } else { - this.context.router.history.push('/'); + router.history.push('/'); } }; diff --git a/app/javascript/mastodon/components/edited_timestamp/index.jsx b/app/javascript/mastodon/components/edited_timestamp/index.jsx index 1513f93619..8a5417a7d9 100644 --- a/app/javascript/mastodon/components/edited_timestamp/index.jsx +++ b/app/javascript/mastodon/components/edited_timestamp/index.jsx @@ -32,7 +32,7 @@ class EditedTimestamp extends React.PureComponent { renderHeader = items => { return ( - + ); }; diff --git a/app/javascript/mastodon/components/hashtag.jsx b/app/javascript/mastodon/components/hashtag.jsx index 94c61b6546..8821c66f02 100644 --- a/app/javascript/mastodon/components/hashtag.jsx +++ b/app/javascript/mastodon/components/hashtag.jsx @@ -43,7 +43,7 @@ class SilentErrorBoundary extends React.Component { export const accountsCountRenderer = (displayNumber, pluralReady) => ( {displayNumber}, diff --git a/app/javascript/mastodon/components/logo.jsx b/app/javascript/mastodon/components/logo.jsx index ee5c22496c..60e8f40b23 100644 --- a/app/javascript/mastodon/components/logo.jsx +++ b/app/javascript/mastodon/components/logo.jsx @@ -1,10 +1,15 @@ import React from 'react'; +import logo from 'mastodon/../images/logo.svg'; -const Logo = () => ( - +export const WordmarkLogo = () => ( + Mastodon ); -export default Logo; +export const SymbolLogo = () => ( + Mastodon +); + +export default WordmarkLogo; diff --git a/app/javascript/mastodon/components/short_number.jsx b/app/javascript/mastodon/components/short_number.jsx index 535c17727d..861d0bc589 100644 --- a/app/javascript/mastodon/components/short_number.jsx +++ b/app/javascript/mastodon/components/short_number.jsx @@ -32,17 +32,14 @@ function ShortNumber({ value, renderer, children }) { const shortNumber = toShortNumber(value); const [, division] = shortNumber; - // eslint-disable-next-line eqeqeq if (children != null && renderer != null) { console.warn('Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.'); } - // eslint-disable-next-line eqeqeq const customRenderer = children != null ? children : renderer; const displayNumber = ; - // eslint-disable-next-line eqeqeq return customRenderer != null ? customRenderer(displayNumber, pluralReady(value, division)) : displayNumber; diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 261ed15b2e..d442df44b8 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -70,6 +70,9 @@ class Status extends ImmutablePureComponent { static propTypes = { status: ImmutablePropTypes.map, account: ImmutablePropTypes.map, + previousId: PropTypes.string, + nextInReplyToId: PropTypes.string, + rootId: PropTypes.string, onClick: PropTypes.func, onReply: PropTypes.func, onFavourite: PropTypes.func, @@ -314,10 +317,7 @@ class Status extends ImmutablePureComponent { }; render () { - let media = null; - let statusAvatar, prepend, rebloggedByText; - - const { intl, hidden, featured, unread, showThread, scrollKey, pictureInPicture } = this.props; + const { intl, hidden, featured, unread, showThread, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId } = this.props; let { status, account, ...other } = this.props; @@ -339,6 +339,8 @@ class Status extends ImmutablePureComponent { openMedia: this.handleHotkeyOpenMedia, }; + let media, statusAvatar, prepend, rebloggedByText; + if (hidden) { return ( @@ -350,7 +352,11 @@ class Status extends ImmutablePureComponent { ); } + const connectUp = previousId && previousId === status.get('in_reply_to_id'); + const connectToRoot = rootId && rootId === status.get('in_reply_to_id'); + const connectReply = nextInReplyToId && nextInReplyToId === status.get('id'); const matchedFilters = status.get('matched_filters'); + if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) { const minHandlers = this.props.muted ? {} : { moveUp: this.handleHotkeyMoveUp, @@ -524,7 +530,10 @@ class Status extends ImmutablePureComponent {
{prepend} -
+
+ {(connectReply || connectUp || connectToRoot) &&
} + + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index 60f820bc52..dd589dac6d 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -104,7 +104,7 @@ class StatusContent extends React.PureComponent { link.setAttribute('href', `/@${mention.get('acct')}`); } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); - link.setAttribute('href', `/tags/${link.text.slice(1)}`); + link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`); } else { link.setAttribute('title', link.href); link.classList.add('unhandled-link'); diff --git a/app/javascript/mastodon/components/verified_badge.jsx b/app/javascript/mastodon/components/verified_badge.jsx new file mode 100644 index 0000000000..3d878d5dd1 --- /dev/null +++ b/app/javascript/mastodon/components/verified_badge.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Icon from 'mastodon/components/icon'; + +class VerifiedBadge extends React.PureComponent { + + static propTypes = { + link: PropTypes.string.isRequired, + verifiedAt: PropTypes.string.isRequired, + }; + + render () { + const { link } = this.props; + + return ( + + + + + ); + } + +} + +export default VerifiedBadge; \ No newline at end of file diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index 643cf5704a..5128352611 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -69,6 +69,7 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, props) => ({ status: getStatus(state, props), + nextInReplyToId: props.nextId ? state.getIn(['statuses', props.nextId, 'in_reply_to_id']) : null, pictureInPicture: getPictureInPicture(state, props), emojiMap: makeCustomEmojiMap(state), }); diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx index 72eb7e6b65..ff8dc5fa9f 100644 --- a/app/javascript/mastodon/features/account/components/header.jsx +++ b/app/javascript/mastodon/features/account/components/header.jsx @@ -80,6 +80,7 @@ class Header extends ImmutablePureComponent { static contextTypes = { identity: PropTypes.object, + router: PropTypes.object, }; static propTypes = { @@ -101,11 +102,16 @@ class Header extends ImmutablePureComponent { onChangeLanguages: PropTypes.func.isRequired, onInteractionModal: PropTypes.func.isRequired, onOpenAvatar: PropTypes.func.isRequired, + onOpenURL: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, domain: PropTypes.string.isRequired, hidden: PropTypes.bool, }; + setRef = c => { + this.node = c; + }; + openEditProfile = () => { window.open('/settings/profile', '_blank'); }; @@ -162,6 +168,61 @@ class Header extends ImmutablePureComponent { }); }; + handleHashtagClick = e => { + const { router } = this.context; + const value = e.currentTarget.textContent.replace(/^#/, ''); + + if (router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + router.history.push(`/tags/${value}`); + } + }; + + handleMentionClick = e => { + const { router } = this.context; + const { onOpenURL } = this.props; + + if (router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + + const link = e.currentTarget; + + onOpenURL(link.href, router.history, () => { + window.location = link.href; + }); + } + }; + + _attachLinkEvents () { + const node = this.node; + + if (!node) { + return; + } + + const links = node.querySelectorAll('a'); + + let link; + + for (var i = 0; i < links.length; ++i) { + link = links[i]; + + if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { + link.addEventListener('click', this.handleHashtagClick, false); + } else if (link.classList.contains('mention')) { + link.addEventListener('click', this.handleMentionClick, false); + } + } + } + + componentDidMount () { + this._attachLinkEvents(); + } + + componentDidUpdate () { + this._attachLinkEvents(); + } + render () { const { account, hidden, intl, domain } = this.props; const { signedIn, permissions } = this.context.identity; @@ -360,7 +421,7 @@ class Header extends ImmutablePureComponent { {!(suspended || hidden) && (
-
+
{(account.get('id') !== me && signedIn) && } {account.get('note').length > 0 && account.get('note') !== '

' &&
} diff --git a/app/javascript/mastodon/features/account_timeline/components/header.jsx b/app/javascript/mastodon/features/account_timeline/components/header.jsx index c008f0342d..e64deaefa6 100644 --- a/app/javascript/mastodon/features/account_timeline/components/header.jsx +++ b/app/javascript/mastodon/features/account_timeline/components/header.jsx @@ -26,6 +26,7 @@ export default class Header extends ImmutablePureComponent { onChangeLanguages: PropTypes.func.isRequired, onInteractionModal: PropTypes.func.isRequired, onOpenAvatar: PropTypes.func.isRequired, + onOpenURL: PropTypes.func.isRequired, hideTabs: PropTypes.bool, domain: PropTypes.string.isRequired, hidden: PropTypes.bool, @@ -137,6 +138,7 @@ export default class Header extends ImmutablePureComponent { onChangeLanguages={this.handleChangeLanguages} onInteractionModal={this.handleInteractionModal} onOpenAvatar={this.handleOpenAvatar} + onOpenURL={this.props.onOpenURL} domain={this.props.domain} hidden={hidden} /> diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx b/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx index f53cd24807..419a9fa566 100644 --- a/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx +++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx @@ -10,6 +10,7 @@ import { pinAccount, unpinAccount, } from '../../../actions/accounts'; +import { openURL } from 'mastodon/actions/search'; import { mentionCompose, directCompose, @@ -159,6 +160,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ })); }, + onOpenURL (url, routerHistory, onFailure) { + dispatch(openURL(url, routerHistory, onFailure)); + }, + }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header)); diff --git a/app/javascript/mastodon/features/compose/components/compose_form.jsx b/app/javascript/mastodon/features/compose/components/compose_form.jsx index a40eb87e3c..df11d2c97e 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.jsx +++ b/app/javascript/mastodon/features/compose/components/compose_form.jsx @@ -20,6 +20,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { length } from 'stringz'; import { countableText } from '../util/counter'; import Icon from 'mastodon/components/icon'; +import classNames from 'classnames'; import { maxChars } from '../../../initial_state'; const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d'; @@ -71,6 +72,10 @@ class ComposeForm extends ImmutablePureComponent { autoFocus: false, }; + state = { + highlighted: false, + }; + handleChange = (e) => { this.props.onChange(e.target.value); }; @@ -144,6 +149,10 @@ class ComposeForm extends ImmutablePureComponent { this._updateFocusAndSelection({ }); } + componentWillUnmount () { + if (this.timeout) clearTimeout(this.timeout); + } + componentDidUpdate (prevProps) { this._updateFocusAndSelection(prevProps); } @@ -174,6 +183,8 @@ class ComposeForm extends ImmutablePureComponent { Promise.resolve().then(() => { this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd); this.autosuggestTextarea.textarea.focus(); + this.setState({ highlighted: true }); + this.timeout = setTimeout(() => this.setState({ highlighted: false }), 700); }).catch(console.error); } else if(prevProps.isSubmitting && !this.props.isSubmitting) { this.autosuggestTextarea.textarea.focus(); @@ -208,6 +219,7 @@ class ComposeForm extends ImmutablePureComponent { render () { const { intl, onPaste, autoFocus } = this.props; + const { highlighted } = this.state; const disabled = this.props.isSubmitting; let publishText = ''; @@ -246,41 +258,43 @@ class ComposeForm extends ImmutablePureComponent { />
- - +
+ + -
- - -
-
+
+ + +
+ -
-
- - - - - -
+
+
+ + + + + +
-
- +
+ +
diff --git a/app/javascript/mastodon/features/compose/components/search.jsx b/app/javascript/mastodon/features/compose/components/search.jsx index 46723f5cce..7fff587d68 100644 --- a/app/javascript/mastodon/features/compose/components/search.jsx +++ b/app/javascript/mastodon/features/compose/components/search.jsx @@ -161,9 +161,9 @@ class Search extends React.PureComponent { handleURLClick = () => { const { router } = this.context; - const { onOpenURL } = this.props; + const { value, onOpenURL } = this.props; - onOpenURL(router.history); + onOpenURL(value, router.history); }; handleStatusSearch = () => { diff --git a/app/javascript/mastodon/features/compose/components/search_results.jsx b/app/javascript/mastodon/features/compose/components/search_results.jsx index 1dccd950cb..8fafd88b07 100644 --- a/app/javascript/mastodon/features/compose/components/search_results.jsx +++ b/app/javascript/mastodon/features/compose/components/search_results.jsx @@ -126,7 +126,7 @@ class SearchResults extends ImmutablePureComponent {
- +
{accounts} diff --git a/app/javascript/mastodon/features/compose/containers/search_container.js b/app/javascript/mastodon/features/compose/containers/search_container.js index 3ee55fae59..3d2d728c8d 100644 --- a/app/javascript/mastodon/features/compose/containers/search_container.js +++ b/app/javascript/mastodon/features/compose/containers/search_container.js @@ -34,8 +34,8 @@ const mapDispatchToProps = dispatch => ({ dispatch(showSearch()); }, - onOpenURL (routerHistory) { - dispatch(openURL(routerHistory)); + onOpenURL (q, routerHistory) { + dispatch(openURL(q, routerHistory)); }, onClickSearchResult (q, type) { diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js index 72a732e3bc..7b917ac43b 100644 --- a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js +++ b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js @@ -74,9 +74,9 @@ describe('emoji', () => { .toEqual('😇'); }); - it('skips the textual presentation VS15 character', () => { + it('does not emojify emojis with textual presentation VS15 character', () => { expect(emojify('✴︎')) // This is U+2734 EIGHT POINTED BLACK STAR then U+FE0E VARIATION SELECTOR-15 - .toEqual('✴'); + .toEqual('✴︎'); }); it('does an simple emoji properly', () => { diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js index 6ae4066242..06800d4329 100644 --- a/app/javascript/mastodon/features/emoji/emoji.js +++ b/app/javascript/mastodon/features/emoji/emoji.js @@ -20,68 +20,88 @@ const emojiFilename = (filename) => { }; const emojifyTextNode = (node, customEmojis) => { + const VS15 = 0xFE0E; + const VS16 = 0xFE0F; + let str = node.textContent; const fragment = new DocumentFragment(); + let i = 0; for (;;) { - let match, i = 0; + let unicode_emoji; + // Skip to the next potential emoji to replace (either custom emoji or custom emoji :shortcode: if (customEmojis === null) { - while (i < str.length && !(match = trie.search(str.slice(i)))) { + while (i < str.length && !(unicode_emoji = trie.search(str.slice(i)))) { i += str.codePointAt(i) < 65536 ? 1 : 2; } } else { - while (i < str.length && str[i] !== ':' && !(match = trie.search(str.slice(i)))) { + while (i < str.length && str[i] !== ':' && !(unicode_emoji = trie.search(str.slice(i)))) { i += str.codePointAt(i) < 65536 ? 1 : 2; } } - let rend, replacement = null; + // We reached the end of the string, nothing to replace if (i === str.length) { break; - } else if (str[i] === ':') { - if (!(() => { - rend = str.indexOf(':', i + 1) + 1; - if (!rend) return false; // no pair of ':' - const shortname = str.slice(i, rend); - // now got a replacee as ':shortname:' - // if you want additional emoji handler, add statements below which set replacement and return true. - if (shortname in customEmojis) { - const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url; - replacement = document.createElement('img'); - replacement.setAttribute('draggable', 'false'); - replacement.setAttribute('class', 'emojione custom-emoji'); - replacement.setAttribute('alt', shortname); - replacement.setAttribute('title', shortname); - replacement.setAttribute('src', filename); - replacement.setAttribute('data-original', customEmojis[shortname].url); - replacement.setAttribute('data-static', customEmojis[shortname].static_url); - return true; - } - return false; - })()) rend = ++i; - } else { // matched to unicode emoji - const { filename, shortCode } = unicodeMapping[match]; + } + + let rend, replacement = null; + if (str[i] === ':') { // Potentially the start of a custom emoji :shortcode: + rend = str.indexOf(':', i + 1) + 1; + + // no matching ending ':', skip + if (!rend) { + i++; + continue; + } + + const shortcode = str.slice(i, rend); + const custom_emoji = customEmojis[shortcode]; + + // not a recognized shortcode, skip + if (!custom_emoji) { + i++; + continue; + } + + // now got a replacee as ':shortcode:' + // if you want additional emoji handler, add statements below which set replacement and return true. + const filename = autoPlayGif ? custom_emoji.url : custom_emoji.static_url; + replacement = document.createElement('img'); + replacement.setAttribute('draggable', 'false'); + replacement.setAttribute('class', 'emojione custom-emoji'); + replacement.setAttribute('alt', shortcode); + replacement.setAttribute('title', shortcode); + replacement.setAttribute('src', filename); + replacement.setAttribute('data-original', custom_emoji.url); + replacement.setAttribute('data-static', custom_emoji.static_url); + } else { // start of an unicode emoji + rend = i + unicode_emoji.length; + + // If the matched character was followed by VS15 (for selecting text presentation), skip it. + if (str.codePointAt(rend - 1) !== VS16 && str.codePointAt(rend) === VS15) { + i = rend + 1; + continue; + } + + const { filename, shortCode } = unicodeMapping[unicode_emoji]; const title = shortCode ? `:${shortCode}:` : ''; + replacement = document.createElement('img'); replacement.setAttribute('draggable', 'false'); replacement.setAttribute('class', 'emojione'); - replacement.setAttribute('alt', match); + replacement.setAttribute('alt', unicode_emoji); replacement.setAttribute('title', title); replacement.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename)}.svg`); - rend = i + match.length; - // If the matched character was followed by VS15 (for selecting text presentation), skip it. - if (str.codePointAt(rend) === 65038) { - rend += 1; - } } + // Add the processed-up-to-now string and the emoji replacement fragment.append(document.createTextNode(str.slice(0, i))); - if (replacement) { - fragment.append(replacement); - } + fragment.append(replacement); str = str.slice(rend); + i = 0; } fragment.append(document.createTextNode(str)); diff --git a/app/javascript/mastodon/features/explore/index.jsx b/app/javascript/mastodon/features/explore/index.jsx index 939550d833..35626226ec 100644 --- a/app/javascript/mastodon/features/explore/index.jsx +++ b/app/javascript/mastodon/features/explore/index.jsx @@ -71,17 +71,20 @@ class Explore extends React.PureComponent { + + + {signedIn && ( + + + + )} + - {signedIn && ( - - - - )}
diff --git a/app/javascript/mastodon/features/follow_recommendations/components/account.jsx b/app/javascript/mastodon/features/follow_recommendations/components/account.jsx deleted file mode 100644 index 9cb26fe645..0000000000 --- a/app/javascript/mastodon/features/follow_recommendations/components/account.jsx +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { connect } from 'react-redux'; -import { makeGetAccount } from 'mastodon/selectors'; -import Avatar from 'mastodon/components/avatar'; -import DisplayName from 'mastodon/components/display_name'; -import { Link } from 'react-router-dom'; -import IconButton from 'mastodon/components/icon_button'; -import { injectIntl, defineMessages } from 'react-intl'; -import { followAccount, unfollowAccount } from 'mastodon/actions/accounts'; - -const messages = defineMessages({ - follow: { id: 'account.follow', defaultMessage: 'Follow' }, - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, -}); - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = (state, props) => ({ - account: getAccount(state, props.id), - }); - - return mapStateToProps; -}; - -const getFirstSentence = str => { - const arr = str.split(/(([.?!]+\s)|[.。?!\n•])/); - - return arr[0]; -}; - -class Account extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - intl: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - }; - - handleFollow = () => { - const { account, dispatch } = this.props; - - if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { - dispatch(unfollowAccount(account.get('id'))); - } else { - dispatch(followAccount(account.get('id'))); - } - }; - - render () { - const { account, intl } = this.props; - - let button; - - if (account.getIn(['relationship', 'following'])) { - button = ; - } else { - button = ; - } - - return ( -
-
- -
- - - -
{getFirstSentence(account.get('note_plain'))}
- - -
- {button} -
-
-
- ); - } - -} - -export default connect(makeMapStateToProps)(injectIntl(Account)); diff --git a/app/javascript/mastodon/features/follow_recommendations/index.jsx b/app/javascript/mastodon/features/follow_recommendations/index.jsx deleted file mode 100644 index 7ba34b51f4..0000000000 --- a/app/javascript/mastodon/features/follow_recommendations/index.jsx +++ /dev/null @@ -1,117 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { connect } from 'react-redux'; -import { FormattedMessage } from 'react-intl'; -import { fetchSuggestions } from 'mastodon/actions/suggestions'; -import { changeSetting, saveSettings } from 'mastodon/actions/settings'; -import { requestBrowserPermission } from 'mastodon/actions/notifications'; -import { markAsPartial } from 'mastodon/actions/timelines'; -import Column from 'mastodon/features/ui/components/column'; -import Account from './components/account'; -import imageGreeting from 'mastodon/../images/elephant_ui_greeting.svg'; -import Button from 'mastodon/components/button'; -import { Helmet } from 'react-helmet'; - -const mapStateToProps = state => ({ - suggestions: state.getIn(['suggestions', 'items']), - isLoading: state.getIn(['suggestions', 'isLoading']), -}); - -class FollowRecommendations extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object.isRequired, - }; - - static propTypes = { - dispatch: PropTypes.func.isRequired, - suggestions: ImmutablePropTypes.list, - isLoading: PropTypes.bool, - }; - - componentDidMount () { - const { dispatch, suggestions } = this.props; - - // Don't re-fetch if we're e.g. navigating backwards to this page, - // since we don't want followed accounts to disappear from the list - - if (suggestions.size === 0) { - dispatch(fetchSuggestions(true)); - } - } - - componentWillUnmount () { - const { dispatch } = this.props; - - // Force the home timeline to be reloaded when the user navigates - // to it; if the user is new, it would've been empty before - - dispatch(markAsPartial('home')); - } - - handleDone = () => { - const { dispatch } = this.props; - const { router } = this.context; - - dispatch(requestBrowserPermission((permission) => { - if (permission === 'granted') { - dispatch(changeSetting(['notifications', 'alerts', 'follow'], true)); - dispatch(changeSetting(['notifications', 'alerts', 'favourite'], true)); - dispatch(changeSetting(['notifications', 'alerts', 'reblog'], true)); - dispatch(changeSetting(['notifications', 'alerts', 'mention'], true)); - dispatch(changeSetting(['notifications', 'alerts', 'poll'], true)); - dispatch(changeSetting(['notifications', 'alerts', 'status'], true)); - dispatch(saveSettings()); - } - })); - - router.history.push('/home'); - }; - - render () { - const { suggestions, isLoading } = this.props; - - return ( - -
-
- - - - -

-

-
- - {!isLoading && ( - -
- {suggestions.size > 0 ? suggestions.map(suggestion => ( - - )) : ( -
- -
- )} -
- -
- - -
-
- )} -
- - - - -
- ); - } - -} - -export default connect(mapStateToProps)(FollowRecommendations); diff --git a/app/javascript/mastodon/features/notifications/components/report.jsx b/app/javascript/mastodon/features/notifications/components/report.jsx index 4663b23597..a9decdc8e9 100644 --- a/app/javascript/mastodon/features/notifications/components/report.jsx +++ b/app/javascript/mastodon/features/notifications/components/report.jsx @@ -45,7 +45,7 @@ class Report extends ImmutablePureComponent {
- · + ·
{intl.formatMessage(messages[report.get('category')])}
diff --git a/app/javascript/mastodon/features/onboarding/components/arrow_small_right.jsx b/app/javascript/mastodon/features/onboarding/components/arrow_small_right.jsx new file mode 100644 index 0000000000..40e166f6dc --- /dev/null +++ b/app/javascript/mastodon/features/onboarding/components/arrow_small_right.jsx @@ -0,0 +1,9 @@ +import React from 'react'; + +const ArrowSmallRight = () => ( + + + +); + +export default ArrowSmallRight; \ No newline at end of file diff --git a/app/javascript/mastodon/features/onboarding/components/progress_indicator.jsx b/app/javascript/mastodon/features/onboarding/components/progress_indicator.jsx new file mode 100644 index 0000000000..97134c0c9c --- /dev/null +++ b/app/javascript/mastodon/features/onboarding/components/progress_indicator.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Check from 'mastodon/components/check'; +import classNames from 'classnames'; + +const ProgressIndicator = ({ steps, completed }) => ( +
+ {(new Array(steps)).fill().map((_, i) => ( + + {i > 0 &&
i })} />} + +
i })}> + {completed > i && } +
+ + ))} +
+); + +ProgressIndicator.propTypes = { + steps: PropTypes.number.isRequired, + completed: PropTypes.number, +}; + +export default ProgressIndicator; \ No newline at end of file diff --git a/app/javascript/mastodon/features/onboarding/components/step.jsx b/app/javascript/mastodon/features/onboarding/components/step.jsx new file mode 100644 index 0000000000..6f376e5d55 --- /dev/null +++ b/app/javascript/mastodon/features/onboarding/components/step.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Icon from 'mastodon/components/icon'; +import Check from 'mastodon/components/check'; + +const Step = ({ label, description, icon, completed, onClick, href }) => { + const content = ( + <> +
+ +
+ +
+
{label}
+

{description}

+
+ + {completed && ( +
+ +
+ )} + + ); + + if (href) { + return ( +
+ {content} + + ); + } + + return ( + + ); +}; + +Step.propTypes = { + label: PropTypes.node, + description: PropTypes.node, + icon: PropTypes.string, + completed: PropTypes.bool, + href: PropTypes.string, + onClick: PropTypes.func, +}; + +export default Step; \ No newline at end of file diff --git a/app/javascript/mastodon/features/onboarding/follows.jsx b/app/javascript/mastodon/features/onboarding/follows.jsx new file mode 100644 index 0000000000..7cccdefb3d --- /dev/null +++ b/app/javascript/mastodon/features/onboarding/follows.jsx @@ -0,0 +1,87 @@ +import React from 'react'; +import Column from 'mastodon/components/column'; +import ColumnBackButton from 'mastodon/components/column_back_button'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { fetchSuggestions } from 'mastodon/actions/suggestions'; +import { markAsPartial } from 'mastodon/actions/timelines'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Account from 'mastodon/containers/account_container'; +import EmptyAccount from 'mastodon/components/account'; +import { FormattedMessage, FormattedHTMLMessage } from 'react-intl'; +import { makeGetAccount } from 'mastodon/selectors'; +import { me } from 'mastodon/initial_state'; +import ProgressIndicator from './components/progress_indicator'; + +const mapStateToProps = () => { + const getAccount = makeGetAccount(); + + return state => ({ + account: getAccount(state, me), + suggestions: state.getIn(['suggestions', 'items']), + isLoading: state.getIn(['suggestions', 'isLoading']), + }); +}; + +class Follows extends React.PureComponent { + + static propTypes = { + onBack: PropTypes.func, + dispatch: PropTypes.func.isRequired, + suggestions: ImmutablePropTypes.list, + account: ImmutablePropTypes.map, + isLoading: PropTypes.bool, + }; + + componentDidMount () { + const { dispatch } = this.props; + dispatch(fetchSuggestions(true)); + } + + componentWillUnmount () { + const { dispatch } = this.props; + dispatch(markAsPartial('home')); + } + + render () { + const { onBack, isLoading, suggestions, account } = this.props; + + let loadedContent; + + if (isLoading) { + loadedContent = (new Array(8)).fill().map((_, i) => ); + } else if (suggestions.isEmpty()) { + loadedContent =
; + } else { + loadedContent = suggestions.map(suggestion => ); + } + + return ( + + + +
+
+

+

+
+ + + +
+ {loadedContent} +
+ +

+ +
+ +
+
+
+ ); + } + +} + +export default connect(mapStateToProps)(Follows); \ No newline at end of file diff --git a/app/javascript/mastodon/features/onboarding/index.jsx b/app/javascript/mastodon/features/onboarding/index.jsx new file mode 100644 index 0000000000..388734055e --- /dev/null +++ b/app/javascript/mastodon/features/onboarding/index.jsx @@ -0,0 +1,145 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; +import { focusCompose } from 'mastodon/actions/compose'; +import Column from 'mastodon/features/ui/components/column'; +import { Helmet } from 'react-helmet'; +import illustration from 'mastodon/../images/elephant_ui_conversation.svg'; +import { Link } from 'react-router-dom'; +import { me } from 'mastodon/initial_state'; +import { makeGetAccount } from 'mastodon/selectors'; +import { closeOnboarding } from 'mastodon/actions/onboarding'; +import { fetchAccount } from 'mastodon/actions/accounts'; +import Follows from './follows'; +import Share from './share'; +import Step from './components/step'; +import ArrowSmallRight from './components/arrow_small_right'; +import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; +import { debounce } from 'lodash'; + +const messages = defineMessages({ + template: { id: 'onboarding.compose.template', defaultMessage: 'Hello #Mastodon!' }, +}); + +const mapStateToProps = () => { + const getAccount = makeGetAccount(); + + return state => ({ + account: getAccount(state, me), + }); +}; + +class Onboarding extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object.isRequired, + }; + + static propTypes = { + dispatch: PropTypes.func.isRequired, + account: ImmutablePropTypes.map, + }; + + state = { + step: null, + profileClicked: false, + shareClicked: false, + }; + + handleClose = () => { + const { dispatch } = this.props; + const { router } = this.context; + + dispatch(closeOnboarding()); + router.history.push('/home'); + }; + + handleProfileClick = () => { + this.setState({ profileClicked: true }); + }; + + handleFollowClick = () => { + this.setState({ step: 'follows' }); + }; + + handleComposeClick = () => { + const { dispatch, intl } = this.props; + const { router } = this.context; + + dispatch(focusCompose(router.history, intl.formatMessage(messages.template))); + }; + + handleShareClick = () => { + this.setState({ step: 'share', shareClicked: true }); + }; + + handleBackClick = () => { + this.setState({ step: null }); + }; + + handleWindowFocus = debounce(() => { + const { dispatch, account } = this.props; + dispatch(fetchAccount(account.get('id'))); + }, 1000, { trailing: true }); + + componentDidMount () { + window.addEventListener('focus', this.handleWindowFocus, false); + } + + componentWillUnmount () { + window.removeEventListener('focus', this.handleWindowFocus); + } + + render () { + const { account } = this.props; + const { step, shareClicked } = this.state; + + switch(step) { + case 'follows': + return ; + case 'share': + return ; + } + + return ( + +
+
+ +

+

+
+ +
+ 0 && account.get('note').length > 0)} icon='address-book-o' label={} description={} /> + = 7} icon='user-plus' label={} description={} /> + = 1} icon='pencil-square-o' label={} description={} /> + } description={} /> +
+ +

+ +
+ + + + +
+ +
+ +
+
+ + + + +
+ ); + } + +} + +export default connect(mapStateToProps)(injectIntl(Onboarding)); diff --git a/app/javascript/mastodon/features/onboarding/share.jsx b/app/javascript/mastodon/features/onboarding/share.jsx new file mode 100644 index 0000000000..9555a3a43a --- /dev/null +++ b/app/javascript/mastodon/features/onboarding/share.jsx @@ -0,0 +1,193 @@ +import React from 'react'; +import Column from 'mastodon/components/column'; +import ColumnBackButton from 'mastodon/components/column_back_button'; +import PropTypes from 'prop-types'; +import { me, domain } from 'mastodon/initial_state'; +import { connect } from 'react-redux'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl, FormattedMessage, FormattedHTMLMessage } from 'react-intl'; +import classNames from 'classnames'; +import Icon from 'mastodon/components/icon'; +import ArrowSmallRight from './components/arrow_small_right'; +import { Link } from 'react-router-dom'; +import SwipeableViews from 'react-swipeable-views'; + +const messages = defineMessages({ + shareableMessage: { id: 'onboarding.share.message', defaultMessage: 'I\'m {username} on #Mastodon! Come follow me at {url}' }, +}); + +const mapStateToProps = state => ({ + account: state.getIn(['accounts', me]), +}); + +class CopyPasteText extends React.PureComponent { + + static propTypes = { + value: PropTypes.string, + }; + + state = { + copied: false, + focused: false, + }; + + setRef = c => { + this.input = c; + }; + + handleInputClick = () => { + this.setState({ copied: false }); + this.input.focus(); + this.input.select(); + this.input.setSelectionRange(0, this.props.value.length); + }; + + handleButtonClick = e => { + e.stopPropagation(); + + const { value } = this.props; + navigator.clipboard.writeText(value); + this.input.blur(); + this.setState({ copied: true }); + this.timeout = setTimeout(() => this.setState({ copied: false }), 700); + }; + + handleFocus = () => { + this.setState({ focused: true }); + }; + + handleBlur = () => { + this.setState({ focused: false }); + }; + + componentWillUnmount () { + if (this.timeout) clearTimeout(this.timeout); + } + + render () { + const { value } = this.props; + const { copied, focused } = this.state; + + return ( +
+