Merge remote-tracking branch 'parent/main' into upstream-20241216

This commit is contained in:
KMY 2024-12-16 10:14:31 +09:00
commit 3784ad273c
555 changed files with 7564 additions and 3363 deletions

View file

@ -109,7 +109,7 @@ module.exports = defineConfig({
'react/jsx-equals-spacing': 'error', 'react/jsx-equals-spacing': 'error',
'react/jsx-no-bind': 'error', 'react/jsx-no-bind': 'error',
'react/jsx-no-useless-fragment': 'error', 'react/jsx-no-useless-fragment': 'error',
'react/jsx-no-target-blank': 'off', 'react/jsx-no-target-blank': ['error', { allowReferrer: true }],
'react/jsx-tag-spacing': 'error', 'react/jsx-tag-spacing': 'error',
'react/jsx-uses-react': 'off', // not needed with new JSX transform 'react/jsx-uses-react': 'off', // not needed with new JSX transform
'react/jsx-wrap-multilines': 'error', 'react/jsx-wrap-multilines': 'error',

View file

@ -43,4 +43,4 @@ jobs:
uses: ./.github/actions/setup-javascript uses: ./.github/actions/setup-javascript
- name: Stylelint - name: Stylelint
run: yarn lint:css -f github run: yarn lint:css --custom-formatter @csstools/stylelint-formatter-github

View file

@ -1,4 +1,7 @@
--- ---
Style/ArrayIntersect:
Enabled: false
Style/ClassAndModuleChildren: Style/ClassAndModuleChildren:
Enabled: false Enabled: false
@ -19,6 +22,16 @@ Style/HashSyntax:
EnforcedShorthandSyntax: either EnforcedShorthandSyntax: either
EnforcedStyle: ruby19_no_mixed_keys EnforcedStyle: ruby19_no_mixed_keys
Style/IfUnlessModifier:
Exclude:
- '**/*.haml'
Style/KeywordArgumentsMerging:
Enabled: false
Style/MultipleComparison:
Enabled: false
Style/NumericLiterals: Style/NumericLiterals:
AllowedPatterns: AllowedPatterns:
- \d{4}_\d{2}_\d{2}_\d{6} - \d{4}_\d{2}_\d{2}_\d{6}
@ -37,6 +50,9 @@ Style/RedundantFetchBlock:
Style/RescueStandardError: Style/RescueStandardError:
EnforcedStyle: implicit EnforcedStyle: implicit
Style/SafeNavigationChainLength:
Enabled: false
Style/SymbolArray: Style/SymbolArray:
Enabled: false Enabled: false

View file

@ -1,6 +1,6 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp` # `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.66.1. # using RuboCop version 1.69.1.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
@ -39,7 +39,6 @@ Rails/OutputSafety:
# Configuration parameters: AllowedVars. # Configuration parameters: AllowedVars.
Style/FetchEnvVar: Style/FetchEnvVar:
Exclude: Exclude:
- 'app/lib/translation_service.rb'
- 'config/environments/production.rb' - 'config/environments/production.rb'
- 'config/initializers/2_limited_federation_mode.rb' - 'config/initializers/2_limited_federation_mode.rb'
- 'config/initializers/3_omniauth.rb' - 'config/initializers/3_omniauth.rb'

View file

@ -2,6 +2,48 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [4.3.2] - 2024-12-03
### Added
- Add `tootctl feeds vacuum` (#33065 by @ClearlyClaire)
- Add error message when user tries to follow their own account (#31910 by @lenikadali)
- Add client_secret_expires_at to OAuth Applications (#30317 by @ThisIsMissEm)
### Changed
- Change design of Content Warnings and filters (#32543 by @ClearlyClaire)
### Fixed
- Fix processing incoming post edits with mentions to unresolvable accounts (#33129 by @ClearlyClaire)
- Fix error when including multiple instances of `embed.js` (#33107 by @YKWeyer)
- Fix inactive users' timelines being backfilled on follow and unsuspend (#33094 by @ClearlyClaire)
- Fix direct inbox delivery pushing posts into inactive followers' timelines (#33067 by @ClearlyClaire)
- Fix `TagFollow` records not being correctly handled in account operations (#33063 by @ClearlyClaire)
- Fix pushing hashtag-followed posts to feeds of inactive users (#33018 by @Gargron)
- Fix duplicate notifications in notification groups when using slow mode (#33014 by @ClearlyClaire)
- Fix posts made in the future being allowed to trend (#32996 by @ClearlyClaire)
- Fix uploading higher-than-wide GIF profile picture with libvips enabled (#32911 by @ClearlyClaire)
- Fix domain attribution field having autocorrect and autocapitalize enabled (#32903 by @ClearlyClaire)
- Fix titles being escaped twice (#32889 by @ClearlyClaire)
- Fix list creation limit check (#32869 by @ClearlyClaire)
- Fix error in `tootctl email_domain_blocks` when supplying `--with-dns-records` (#32863 by @mjankowski)
- Fix `min_id` and `max_id` causing error in search API (#32857 by @Gargron)
- Fix inefficiencies when processing removal of posts that use featured tags (#32787 by @ClearlyClaire)
- Fix alt-text pop-in not using the translated description (#32766 by @ClearlyClaire)
- Fix preview cards with long titles erroneously causing layout changes (#32678 by @ClearlyClaire)
- Fix embed modal layout on mobile (#32641 by @DismalShadowX)
- Fix and improve batch attachment deletion handling when using OpenStack Swift (#32637 by @hugogameiro)
- Fix blocks not being applied on link timeline (#32625 by @tribela)
- Fix follow counters being incorrectly changed (#32622 by @oneiros)
- Fix 'unknown' media attachment type rendering (#32613 and #32713 by @ThisIsMissEm and @renatolond)
- Fix tl language native name (#32606 by @seav)
### Security
- Update dependencies
## [4.3.1] - 2024-10-21 ## [4.3.1] - 2024-10-21
### Added ### Added

View file

@ -10,29 +10,29 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.2.2) actioncable (7.2.2.1)
actionpack (= 7.2.2) actionpack (= 7.2.2.1)
activesupport (= 7.2.2) activesupport (= 7.2.2.1)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (7.2.2) actionmailbox (7.2.2.1)
actionpack (= 7.2.2) actionpack (= 7.2.2.1)
activejob (= 7.2.2) activejob (= 7.2.2.1)
activerecord (= 7.2.2) activerecord (= 7.2.2.1)
activestorage (= 7.2.2) activestorage (= 7.2.2.1)
activesupport (= 7.2.2) activesupport (= 7.2.2.1)
mail (>= 2.8.0) mail (>= 2.8.0)
actionmailer (7.2.2) actionmailer (7.2.2.1)
actionpack (= 7.2.2) actionpack (= 7.2.2.1)
actionview (= 7.2.2) actionview (= 7.2.2.1)
activejob (= 7.2.2) activejob (= 7.2.2.1)
activesupport (= 7.2.2) activesupport (= 7.2.2.1)
mail (>= 2.8.0) mail (>= 2.8.0)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (7.2.2) actionpack (7.2.2.1)
actionview (= 7.2.2) actionview (= 7.2.2.1)
activesupport (= 7.2.2) activesupport (= 7.2.2.1)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
racc racc
rack (>= 2.2.4, < 3.2) rack (>= 2.2.4, < 3.2)
@ -41,15 +41,15 @@ GEM
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
useragent (~> 0.16) useragent (~> 0.16)
actiontext (7.2.2) actiontext (7.2.2.1)
actionpack (= 7.2.2) actionpack (= 7.2.2.1)
activerecord (= 7.2.2) activerecord (= 7.2.2.1)
activestorage (= 7.2.2) activestorage (= 7.2.2.1)
activesupport (= 7.2.2) activesupport (= 7.2.2.1)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.2.2) actionview (7.2.2.1)
activesupport (= 7.2.2) activesupport (= 7.2.2.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
@ -59,22 +59,22 @@ GEM
activemodel (>= 4.1) activemodel (>= 4.1)
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (7.2.2) activejob (7.2.2.1)
activesupport (= 7.2.2) activesupport (= 7.2.2.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.2.2) activemodel (7.2.2.1)
activesupport (= 7.2.2) activesupport (= 7.2.2.1)
activerecord (7.2.2) activerecord (7.2.2.1)
activemodel (= 7.2.2) activemodel (= 7.2.2.1)
activesupport (= 7.2.2) activesupport (= 7.2.2.1)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (7.2.2) activestorage (7.2.2.1)
actionpack (= 7.2.2) actionpack (= 7.2.2.1)
activejob (= 7.2.2) activejob (= 7.2.2.1)
activerecord (= 7.2.2) activerecord (= 7.2.2.1)
activesupport (= 7.2.2) activesupport (= 7.2.2.1)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (7.2.2) activesupport (7.2.2.1)
base64 base64
benchmark (>= 0.3) benchmark (>= 0.3)
bigdecimal bigdecimal
@ -103,7 +103,7 @@ GEM
aws-sdk-kms (1.96.0) aws-sdk-kms (1.96.0)
aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.176.0) aws-sdk-s3 (1.176.1)
aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
@ -175,7 +175,7 @@ GEM
activerecord (>= 5.a) activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0) database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1) database_cleaner-core (2.0.1)
date (3.4.0) date (3.4.1)
debug (1.9.2) debug (1.9.2)
irb (~> 1.10) irb (~> 1.10)
reline (>= 0.3.8) reline (>= 0.3.8)
@ -199,9 +199,9 @@ GEM
activerecord (>= 4.2, < 9.0) activerecord (>= 4.2, < 9.0)
docile (1.4.1) docile (1.4.1)
domain_name (0.6.20240107) domain_name (0.6.20240107)
doorkeeper (5.8.0) doorkeeper (5.8.1)
railties (>= 5) railties (>= 5)
dotenv (3.1.4) dotenv (3.1.5)
drb (2.2.1) drb (2.2.1)
elasticsearch (7.17.11) elasticsearch (7.17.11)
elasticsearch-api (= 7.17.11) elasticsearch-api (= 7.17.11)
@ -319,7 +319,7 @@ GEM
activesupport (>= 3.0) activesupport (>= 3.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
io-console (0.7.2) io-console (0.7.2)
irb (1.14.1) irb (1.14.2)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jd-paperclip-azure (3.0.0) jd-paperclip-azure (3.0.0)
@ -327,7 +327,7 @@ GEM
azure-blob (~> 0.5.2) azure-blob (~> 0.5.2)
hashie (~> 5.0) hashie (~> 5.0)
jmespath (1.6.2) jmespath (1.6.2)
json (2.8.1) json (2.9.0)
json-canonicalization (1.0.0) json-canonicalization (1.0.0)
json-jwt (1.15.3.1) json-jwt (1.15.3.1)
activesupport (>= 4.2) activesupport (>= 4.2)
@ -384,7 +384,7 @@ GEM
llhttp-ffi (0.5.0) llhttp-ffi (0.5.0)
ffi-compiler (~> 1.0) ffi-compiler (~> 1.0)
rake (~> 13.0) rake (~> 13.0)
logger (1.6.1) logger (1.6.2)
lograge (0.14.0) lograge (0.14.0)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
@ -409,7 +409,7 @@ GEM
mime-types-data (3.2024.1105) mime-types-data (3.2024.1105)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.8) mini_portile2 (2.8.8)
minitest (5.25.2) minitest (5.25.4)
msgpack (1.7.5) msgpack (1.7.5)
multi_json (1.15.0) multi_json (1.15.0)
mutex_m (0.3.0) mutex_m (0.3.0)
@ -426,7 +426,7 @@ GEM
net-smtp (0.5.0) net-smtp (0.5.0)
net-protocol net-protocol
nio4r (2.7.4) nio4r (2.7.4)
nokogiri (1.16.8) nokogiri (1.17.2)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
oj (3.16.7) oj (3.16.7)
@ -579,7 +579,8 @@ GEM
activesupport (>= 7.0.0) activesupport (>= 7.0.0)
rack rack
railties (>= 7.0.0) railties (>= 7.0.0)
psych (5.2.0) psych (5.2.1)
date
stringio stringio
public_suffix (6.0.1) public_suffix (6.0.1)
puma (6.5.0) puma (6.5.0)
@ -608,23 +609,23 @@ GEM
rack (< 3) rack (< 3)
rack-test (2.1.0) rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
rackup (1.0.0) rackup (1.0.1)
rack (< 3) rack (< 3)
webrick webrick
rails (7.2.2) rails (7.2.2.1)
actioncable (= 7.2.2) actioncable (= 7.2.2.1)
actionmailbox (= 7.2.2) actionmailbox (= 7.2.2.1)
actionmailer (= 7.2.2) actionmailer (= 7.2.2.1)
actionpack (= 7.2.2) actionpack (= 7.2.2.1)
actiontext (= 7.2.2) actiontext (= 7.2.2.1)
actionview (= 7.2.2) actionview (= 7.2.2.1)
activejob (= 7.2.2) activejob (= 7.2.2.1)
activemodel (= 7.2.2) activemodel (= 7.2.2.1)
activerecord (= 7.2.2) activerecord (= 7.2.2.1)
activestorage (= 7.2.2) activestorage (= 7.2.2.1)
activesupport (= 7.2.2) activesupport (= 7.2.2.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.2.2) railties (= 7.2.2.1)
rails-controller-testing (1.0.5) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1) actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1)
@ -639,9 +640,9 @@ GEM
rails-i18n (7.0.10) rails-i18n (7.0.10)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8) railties (>= 6.0.0, < 8)
railties (7.2.2) railties (7.2.2.1)
actionpack (= 7.2.2) actionpack (= 7.2.2.1)
activesupport (= 7.2.2) activesupport (= 7.2.2.1)
irb (~> 1.13) irb (~> 1.13)
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
@ -663,8 +664,8 @@ GEM
redis (>= 4) redis (>= 4)
redlock (1.3.2) redlock (1.3.2)
redis (>= 3.0.0, < 6.0) redis (>= 3.0.0, < 6.0)
regexp_parser (2.9.2) regexp_parser (2.9.3)
reline (0.5.11) reline (0.5.12)
io-console (~> 0.5) io-console (~> 0.5)
request_store (1.6.0) request_store (1.6.0)
rack (>= 1.4) rack (>= 1.4)
@ -707,21 +708,21 @@ GEM
rspec-mocks (~> 3.0) rspec-mocks (~> 3.0)
sidekiq (>= 5, < 8) sidekiq (>= 5, < 8)
rspec-support (3.13.1) rspec-support (3.13.1)
rubocop (1.66.1) rubocop (1.69.2)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.4, < 3.0) regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.32.2, < 2.0) rubocop-ast (>= 1.36.2, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.32.3) rubocop-ast (1.36.2)
parser (>= 3.3.1.0) parser (>= 3.3.1.0)
rubocop-capybara (2.21.0) rubocop-capybara (2.21.0)
rubocop (~> 1.41) rubocop (~> 1.41)
rubocop-performance (1.22.1) rubocop-performance (1.23.0)
rubocop (>= 1.48.1, < 2.0) rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.27.0) rubocop-rails (2.27.0)
@ -729,7 +730,7 @@ GEM
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.52.0, < 2.0) rubocop (>= 1.52.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (3.2.0) rubocop-rspec (3.3.0)
rubocop (~> 1.61) rubocop (~> 1.61)
rubocop-rspec_rails (2.30.0) rubocop-rspec_rails (2.30.0)
rubocop (~> 1.61) rubocop (~> 1.61)
@ -837,7 +838,7 @@ GEM
unf_ext (0.0.9.1) unf_ext (0.0.9.1)
unicode-display_width (2.6.0) unicode-display_width (2.6.0)
uri (0.13.1) uri (0.13.1)
useragent (0.16.10) useragent (0.16.11)
validate_email (0.1.6) validate_email (0.1.6)
activemodel (>= 3.0) activemodel (>= 3.0)
mail (>= 2.2.5) mail (>= 2.2.5)
@ -866,7 +867,7 @@ GEM
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
semantic_range (>= 2.3.0) semantic_range (>= 2.3.0)
webrick (1.9.0) webrick (1.9.1)
websocket (1.2.11) websocket (1.2.11)
websocket-driver (0.7.6) websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)

2
Vagrantfile vendored
View file

@ -174,7 +174,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
if config.vm.networks.any? { |type, options| type == :private_network } if config.vm.networks.any? { |type, options| type == :private_network }
config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'actimeo=1'] config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'actimeo=1']
else else
config.vm.synced_folder ".", "/vagrant" config.vm.synced_folder ".", "/vagrant", type: "rsync", create: true, rsync__args: ["--verbose", "--archive", "--delete", "-z"]
end end
# Otherwise, you can access the site at http://localhost:3000 and http://localhost:4000 , http://localhost:8080 # Otherwise, you can access the site at http://localhost:3000 and http://localhost:4000 , http://localhost:8080

View file

@ -8,6 +8,7 @@ module Admin
layout 'admin' layout 'admin'
before_action :set_cache_headers before_action :set_cache_headers
before_action :set_referrer_policy_header
after_action :verify_authorized after_action :verify_authorized
@ -17,6 +18,10 @@ module Admin
response.cache_control.replace(private: true, no_store: true) response.cache_control.replace(private: true, no_store: true)
end end
def set_referrer_policy_header
response.headers['Referrer-Policy'] = 'same-origin'
end
def set_user def set_user
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
class Admin::TermsOfService::DistributionsController < Admin::BaseController
before_action :set_terms_of_service
def create
authorize @terms_of_service, :distribute?
@terms_of_service.touch(:notification_sent_at)
Admin::DistributeTermsOfServiceNotificationWorker.perform_async(@terms_of_service.id)
redirect_to admin_terms_of_service_index_path
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.find(params[:terms_of_service_id])
end
end

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
class Admin::TermsOfService::DraftsController < Admin::BaseController
before_action :set_terms_of_service
def show
authorize :terms_of_service, :create?
end
def update
authorize @terms_of_service, :update?
@terms_of_service.published_at = Time.now.utc if params[:action_type] == 'publish'
if @terms_of_service.update(resource_params)
log_action(:publish, @terms_of_service) if @terms_of_service.published?
redirect_to @terms_of_service.published? ? admin_terms_of_service_index_path : admin_terms_of_service_draft_path
else
render :show
end
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text)
end
def current_terms_of_service
TermsOfService.live.first
end
def resource_params
params.require(:terms_of_service).permit(:text, :changelog)
end
end

View file

@ -0,0 +1,37 @@
# frozen_string_literal: true
class Admin::TermsOfService::GeneratesController < Admin::BaseController
before_action :set_instance_presenter
def show
authorize :terms_of_service, :create?
@generator = TermsOfService::Generator.new(
domain: @instance_presenter.domain,
admin_email: @instance_presenter.contact.email
)
end
def create
authorize :terms_of_service, :create?
@generator = TermsOfService::Generator.new(resource_params)
if @generator.valid?
TermsOfService.create!(text: @generator.render)
redirect_to admin_terms_of_service_draft_path
else
render :show
end
end
private
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def resource_params
params.require(:terms_of_service_generator).permit(*TermsOfService::Generator::VARIABLES)
end
end

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
class Admin::TermsOfService::HistoriesController < Admin::BaseController
def show
authorize :terms_of_service, :index?
@terms_of_service = TermsOfService.published.all
end
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
class Admin::TermsOfService::PreviewsController < Admin::BaseController
before_action :set_terms_of_service
def show
authorize @terms_of_service, :distribute?
@user_count = @terms_of_service.scope_for_notification.count
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.find(params[:terms_of_service_id])
end
end

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
class Admin::TermsOfService::TestsController < Admin::BaseController
before_action :set_terms_of_service
def create
authorize @terms_of_service, :distribute?
UserMailer.terms_of_service_changed(current_user, @terms_of_service).deliver_later!
redirect_to admin_terms_of_service_preview_path(@terms_of_service)
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.find(params[:terms_of_service_id])
end
end

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
class Admin::TermsOfServiceController < Admin::BaseController
def index
authorize :terms_of_service, :index?
@terms_of_service = TermsOfService.live.first
end
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
class Api::V1::Instances::TermsOfServicesController < Api::V1::Instances::BaseController
before_action :set_terms_of_service
def show
cache_even_if_authenticated!
render json: @terms_of_service, serializer: REST::PrivacyPolicySerializer
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.live.first!
end
end

View file

@ -70,7 +70,13 @@ class ApplicationController < ActionController::Base
end end
def require_functional! def require_functional!
redirect_to edit_user_registration_path unless current_user.functional? return if current_user.functional?
if current_user.confirmed?
redirect_to edit_user_registration_path
else
redirect_to auth_setup_path
end
end end
def skip_csrf_meta_tags? def skip_csrf_meta_tags?

View file

@ -142,4 +142,12 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def set_cache_headers def set_cache_headers
response.cache_control.replace(private: true, no_store: true) response.cache_control.replace(private: true, no_store: true)
end end
def is_flashing_format? # rubocop:disable Naming/PredicateName
if params[:action] == 'create'
false # Disable flash messages for sign-up
else
super
end
end
end end

View file

@ -7,6 +7,7 @@ module WebAppControllerConcern
vary_by 'Accept, Accept-Language, Cookie' vary_by 'Accept, Accept-Language, Cookie'
before_action :redirect_unauthenticated_to_permalinks! before_action :redirect_unauthenticated_to_permalinks!
before_action :set_referer_header
content_security_policy do |p| content_security_policy do |p|
policy = ContentSecurityPolicy.new policy = ContentSecurityPolicy.new
@ -41,4 +42,10 @@ module WebAppControllerConcern
end end
end end
end end
protected
def set_referer_header
response.set_header('Referrer-Policy', Setting.allow_referrer_origin ? 'origin' : 'same-origin')
end
end end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
class TermsOfServiceController < ApplicationController
include WebAppControllerConcern
skip_before_action :require_functional!
def show
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
end
end

View file

@ -68,6 +68,10 @@ module FormattingHelper
end end
end end
def markdown(text)
Redcarpet::Markdown.new(Redcarpet::Render::HTML, escape_html: true, no_images: true).render(text).html_safe # rubocop:disable Rails/OutputSafety
end
private private
def wrapped_status_content_format(status) def wrapped_status_content_format(status)

View file

@ -60,6 +60,10 @@ window.addEventListener('message', (e) => {
const data = e.data; const data = e.data;
// Only set overflow to `hidden` once we got the expected `message` so the post can still be scrolled if
// embedded without parent Javascript support
document.body.style.overflow = 'hidden';
// We use a timeout to allow for the React page to render before calculating the height // We use a timeout to allow for the React page to render before calculating the height
afterInitialRender(() => { afterInitialRender(() => {
window.parent.postMessage( window.parent.postMessage(

View file

@ -2,6 +2,8 @@ import { useCallback } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { isFulfilled, isRejected } from '@reduxjs/toolkit';
import { openURL } from 'mastodon/actions/search'; import { openURL } from 'mastodon/actions/search';
import { useAppDispatch } from 'mastodon/store'; import { useAppDispatch } from 'mastodon/store';
@ -28,12 +30,22 @@ export const useLinks = () => {
); );
const handleMentionClick = useCallback( const handleMentionClick = useCallback(
(element: HTMLAnchorElement) => { async (element: HTMLAnchorElement) => {
dispatch( const result = await dispatch(openURL({ url: element.href }));
openURL(element.href, history, () => {
if (isFulfilled(result)) {
if (result.payload.accounts[0]) {
history.push(`/@${result.payload.accounts[0].acct}`);
} else if (result.payload.statuses[0]) {
history.push(
`/@${result.payload.statuses[0].account.acct}/${result.payload.statuses[0].id}`,
);
} else {
window.location.href = element.href; window.location.href = element.href;
}), }
); } else if (isRejected(result)) {
window.location.href = element.href;
}
}, },
[dispatch, history], [dispatch, history],
); );
@ -48,7 +60,7 @@ export const useLinks = () => {
if (isMentionClick(target)) { if (isMentionClick(target)) {
e.preventDefault(); e.preventDefault();
handleMentionClick(target); void handleMentionClick(target);
} else if (isHashtagClick(target)) { } else if (isHashtagClick(target)) {
e.preventDefault(); e.preventDefault();
handleHashtagClick(target); handleHashtagClick(target);

View file

@ -1,10 +1,12 @@
import { createPollFromServerJSON } from 'mastodon/models/poll';
import { importAccounts } from '../accounts_typed'; import { importAccounts } from '../accounts_typed';
import { normalizeStatus, normalizePoll } from './normalizer'; import { normalizeStatus } from './normalizer';
import { importPolls } from './polls';
export const STATUS_IMPORT = 'STATUS_IMPORT'; export const STATUS_IMPORT = 'STATUS_IMPORT';
export const STATUSES_IMPORT = 'STATUSES_IMPORT'; export const STATUSES_IMPORT = 'STATUSES_IMPORT';
export const POLLS_IMPORT = 'POLLS_IMPORT';
export const FILTERS_IMPORT = 'FILTERS_IMPORT'; export const FILTERS_IMPORT = 'FILTERS_IMPORT';
function pushUnique(array, object) { function pushUnique(array, object) {
@ -25,10 +27,6 @@ export function importFilters(filters) {
return { type: FILTERS_IMPORT, filters }; return { type: FILTERS_IMPORT, filters };
} }
export function importPolls(polls) {
return { type: POLLS_IMPORT, polls };
}
export function importFetchedAccount(account) { export function importFetchedAccount(account) {
return importFetchedAccounts([account]); return importFetchedAccounts([account]);
} }
@ -77,7 +75,7 @@ export function importFetchedStatuses(statuses) {
} }
if (status.poll?.id) { if (status.poll?.id) {
pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id]))); pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls.get(status.poll.id)));
} }
if (status.card) { if (status.card) {
@ -87,15 +85,9 @@ export function importFetchedStatuses(statuses) {
statuses.forEach(processStatus); statuses.forEach(processStatus);
dispatch(importPolls(polls)); dispatch(importPolls({ polls }));
dispatch(importFetchedAccounts(accounts)); dispatch(importFetchedAccounts(accounts));
dispatch(importStatuses(normalStatuses)); dispatch(importStatuses(normalStatuses));
dispatch(importFilters(filters)); dispatch(importFilters(filters));
}; };
} }
export function importFetchedPoll(poll) {
return (dispatch, getState) => {
dispatch(importPolls([normalizePoll(poll, getState().getIn(['polls', poll.id]))]));
};
}

View file

@ -1,15 +1,12 @@
import escapeTextContentForBrowser from 'escape-html'; import escapeTextContentForBrowser from 'escape-html';
import { makeEmojiMap } from 'mastodon/models/custom_emoji';
import emojify from '../../features/emoji/emoji'; import emojify from '../../features/emoji/emoji';
import { expandSpoilers, me } from '../../initial_state'; import { expandSpoilers, me } from '../../initial_state';
const domParser = new DOMParser(); const domParser = new DOMParser();
const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => {
obj[`:${emoji.shortcode}:`] = emoji;
return obj;
}, {});
export function searchTextFromRawStatus (status) { export function searchTextFromRawStatus (status) {
const spoilerText = status.spoiler_text || ''; const spoilerText = status.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n'); const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
@ -140,38 +137,6 @@ export function normalizeStatusTranslation(translation, status) {
return normalTranslation; return normalTranslation;
} }
export function normalizePoll(poll, normalOldPoll) {
const normalPoll = { ...poll };
const emojiMap = makeEmojiMap(poll.emojis);
normalPoll.options = poll.options.map((option, index) => {
const normalOption = {
...option,
voted: poll.own_votes && poll.own_votes.includes(index),
titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
};
if (normalOldPoll && normalOldPoll.getIn(['options', index, 'title']) === option.title) {
normalOption.translation = normalOldPoll.getIn(['options', index, 'translation']);
}
return normalOption;
});
return normalPoll;
}
export function normalizePollOptionTranslation(translation, poll) {
const emojiMap = makeEmojiMap(poll.get('emojis').toJS());
const normalTranslation = {
...translation,
titleHtml: emojify(escapeTextContentForBrowser(translation.title), emojiMap),
};
return normalTranslation;
}
export function normalizeAnnouncement(announcement) { export function normalizeAnnouncement(announcement) {
const normalAnnouncement = { ...announcement }; const normalAnnouncement = { ...announcement };
const emojiMap = makeEmojiMap(normalAnnouncement.emojis); const emojiMap = makeEmojiMap(normalAnnouncement.emojis);

View file

@ -0,0 +1,7 @@
import { createAction } from '@reduxjs/toolkit';
import type { Poll } from 'mastodon/models/poll';
export const importPolls = createAction<{ polls: Poll[] }>(
'poll/importMultiple',
);

View file

@ -1,61 +0,0 @@
import api from '../api';
import { importFetchedPoll } from './importer';
export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST';
export const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS';
export const POLL_VOTE_FAIL = 'POLL_VOTE_FAIL';
export const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST';
export const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS';
export const POLL_FETCH_FAIL = 'POLL_FETCH_FAIL';
export const vote = (pollId, choices) => (dispatch) => {
dispatch(voteRequest());
api().post(`/api/v1/polls/${pollId}/votes`, { choices })
.then(({ data }) => {
dispatch(importFetchedPoll(data));
dispatch(voteSuccess(data));
})
.catch(err => dispatch(voteFail(err)));
};
export const fetchPoll = pollId => (dispatch) => {
dispatch(fetchPollRequest());
api().get(`/api/v1/polls/${pollId}`)
.then(({ data }) => {
dispatch(importFetchedPoll(data));
dispatch(fetchPollSuccess(data));
})
.catch(err => dispatch(fetchPollFail(err)));
};
export const voteRequest = () => ({
type: POLL_VOTE_REQUEST,
});
export const voteSuccess = poll => ({
type: POLL_VOTE_SUCCESS,
poll,
});
export const voteFail = error => ({
type: POLL_VOTE_FAIL,
error,
});
export const fetchPollRequest = () => ({
type: POLL_FETCH_REQUEST,
});
export const fetchPollSuccess = poll => ({
type: POLL_FETCH_SUCCESS,
poll,
});
export const fetchPollFail = error => ({
type: POLL_FETCH_FAIL,
error,
});

View file

@ -0,0 +1,40 @@
import { apiGetPoll, apiPollVote } from 'mastodon/api/polls';
import type { ApiPollJSON } from 'mastodon/api_types/polls';
import { createPollFromServerJSON } from 'mastodon/models/poll';
import {
createAppAsyncThunk,
createDataLoadingThunk,
} from 'mastodon/store/typed_functions';
import { importPolls } from './importer/polls';
export const importFetchedPoll = createAppAsyncThunk(
'poll/importFetched',
(args: { poll: ApiPollJSON }, { dispatch, getState }) => {
const { poll } = args;
dispatch(
importPolls({
polls: [createPollFromServerJSON(poll, getState().polls.get(poll.id))],
}),
);
},
);
export const vote = createDataLoadingThunk(
'poll/vote',
({ pollId, choices }: { pollId: string; choices: string[] }) =>
apiPollVote(pollId, choices),
async (poll, { dispatch, discardLoadData }) => {
await dispatch(importFetchedPoll({ poll }));
return discardLoadData;
},
);
export const fetchPoll = createDataLoadingThunk(
'poll/fetch',
({ pollId }: { pollId: string }) => apiGetPoll(pollId),
async (poll, { dispatch }) => {
await dispatch(importFetchedPoll({ poll }));
},
);

View file

@ -1,215 +0,0 @@
import { fromJS } from 'immutable';
import { searchHistory } from 'mastodon/settings';
import api from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts, importFetchedStatuses } from './importer';
export const SEARCH_CHANGE = 'SEARCH_CHANGE';
export const SEARCH_CLEAR = 'SEARCH_CLEAR';
export const SEARCH_SHOW = 'SEARCH_SHOW';
export const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST';
export const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS';
export const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL';
export const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST';
export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS';
export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL';
export const SEARCH_HISTORY_UPDATE = 'SEARCH_HISTORY_UPDATE';
export function changeSearch(value) {
return {
type: SEARCH_CHANGE,
value,
};
}
export function clearSearch() {
return {
type: SEARCH_CLEAR,
};
}
export function submitSearch(type) {
return (dispatch, getState) => {
const value = getState().getIn(['search', 'value']);
const signedIn = !!getState().getIn(['meta', 'me']);
if (value.length === 0) {
dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, '', type));
return;
}
dispatch(fetchSearchRequest(type));
api().get('/api/v2/search', {
params: {
q: value,
resolve: signedIn,
limit: 11,
type,
},
}).then(response => {
if (response.data.accounts) {
dispatch(importFetchedAccounts(response.data.accounts));
}
if (response.data.statuses) {
dispatch(importFetchedStatuses(response.data.statuses));
}
dispatch(fetchSearchSuccess(response.data, value, type));
dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
}).catch(error => {
dispatch(fetchSearchFail(error));
});
};
}
export function fetchSearchRequest(searchType) {
return {
type: SEARCH_FETCH_REQUEST,
searchType,
};
}
export function fetchSearchSuccess(results, searchTerm, searchType) {
return {
type: SEARCH_FETCH_SUCCESS,
results,
searchType,
searchTerm,
};
}
export function fetchSearchFail(error) {
return {
type: SEARCH_FETCH_FAIL,
error,
};
}
export const expandSearch = type => (dispatch, getState) => {
const value = getState().getIn(['search', 'value']);
const offset = getState().getIn(['search', 'results', type]).size - 1;
dispatch(expandSearchRequest(type));
api().get('/api/v2/search', {
params: {
q: value,
type,
offset,
limit: 11,
},
}).then(({ data }) => {
if (data.accounts) {
dispatch(importFetchedAccounts(data.accounts));
}
if (data.statuses) {
dispatch(importFetchedStatuses(data.statuses));
}
dispatch(expandSearchSuccess(data, value, type));
dispatch(fetchRelationships(data.accounts.map(item => item.id)));
}).catch(error => {
dispatch(expandSearchFail(error));
});
};
export const expandSearchRequest = (searchType) => ({
type: SEARCH_EXPAND_REQUEST,
searchType,
});
export const expandSearchSuccess = (results, searchTerm, searchType) => ({
type: SEARCH_EXPAND_SUCCESS,
results,
searchTerm,
searchType,
});
export const expandSearchFail = error => ({
type: SEARCH_EXPAND_FAIL,
error,
});
export const showSearch = () => ({
type: SEARCH_SHOW,
});
export const openURL = (value, history, onFailure) => (dispatch, getState) => {
const signedIn = !!getState().getIn(['meta', 'me']);
if (!signedIn) {
if (onFailure) {
onFailure();
}
return;
}
dispatch(fetchSearchRequest());
api().get('/api/v2/search', { params: { q: value, resolve: true } }).then(response => {
if (response.data.accounts?.length > 0) {
dispatch(importFetchedAccounts(response.data.accounts));
history.push(`/@${response.data.accounts[0].acct}`);
} else if (response.data.statuses?.length > 0) {
dispatch(importFetchedStatuses(response.data.statuses));
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();
}
});
};
export const clickSearchResult = (q, type) => (dispatch, getState) => {
const previous = getState().getIn(['search', 'recent']);
if (previous.some(x => x.get('q') === q && x.get('type') === type)) {
return;
}
const me = getState().getIn(['meta', 'me']);
const current = previous.add(fromJS({ type, q })).takeLast(4);
searchHistory.set(me, current.toJS());
dispatch(updateSearchHistory(current));
};
export const forgetSearchResult = q => (dispatch, getState) => {
const previous = getState().getIn(['search', 'recent']);
const me = getState().getIn(['meta', 'me']);
const current = previous.filterNot(result => result.get('q') === q);
searchHistory.set(me, current.toJS());
dispatch(updateSearchHistory(current));
};
export const updateSearchHistory = recent => ({
type: SEARCH_HISTORY_UPDATE,
recent,
});
export const hydrateSearch = () => (dispatch, getState) => {
const me = getState().getIn(['meta', 'me']);
const history = searchHistory.get(me);
if (history !== null) {
dispatch(updateSearchHistory(history));
}
};

View file

@ -0,0 +1,151 @@
import { createAction } from '@reduxjs/toolkit';
import { apiGetSearch } from 'mastodon/api/search';
import type { ApiSearchType } from 'mastodon/api_types/search';
import type {
RecentSearch,
SearchType as RecentSearchType,
} from 'mastodon/models/search';
import { searchHistory } from 'mastodon/settings';
import {
createDataLoadingThunk,
createAppAsyncThunk,
} from 'mastodon/store/typed_functions';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts, importFetchedStatuses } from './importer';
export const SEARCH_HISTORY_UPDATE = 'SEARCH_HISTORY_UPDATE';
export const submitSearch = createDataLoadingThunk(
'search/submit',
async ({ q, type }: { q: string; type?: ApiSearchType }, { getState }) => {
const signedIn = !!getState().meta.get('me');
return apiGetSearch({
q,
type,
resolve: signedIn,
limit: 11,
});
},
(data, { dispatch }) => {
if (data.accounts.length > 0) {
dispatch(importFetchedAccounts(data.accounts));
dispatch(fetchRelationships(data.accounts.map((account) => account.id)));
}
if (data.statuses.length > 0) {
dispatch(importFetchedStatuses(data.statuses));
}
return data;
},
{
useLoadingBar: false,
},
);
export const expandSearch = createDataLoadingThunk(
'search/expand',
async ({ type }: { type: ApiSearchType }, { getState }) => {
const q = getState().search.q;
const results = getState().search.results;
const offset = results?.[type].length;
return apiGetSearch({
q,
type,
limit: 11,
offset,
});
},
(data, { dispatch }) => {
if (data.accounts.length > 0) {
dispatch(importFetchedAccounts(data.accounts));
dispatch(fetchRelationships(data.accounts.map((account) => account.id)));
}
if (data.statuses.length > 0) {
dispatch(importFetchedStatuses(data.statuses));
}
return data;
},
{
useLoadingBar: true,
},
);
export const openURL = createDataLoadingThunk(
'search/openURL',
({ url }: { url: string }, { getState }) => {
const signedIn = !!getState().meta.get('me');
return apiGetSearch({
q: url,
resolve: signedIn,
limit: 1,
});
},
(data, { dispatch }) => {
if (data.accounts.length > 0) {
dispatch(importFetchedAccounts(data.accounts));
} else if (data.statuses.length > 0) {
dispatch(importFetchedStatuses(data.statuses));
}
return data;
},
{
useLoadingBar: true,
},
);
export const clickSearchResult = createAppAsyncThunk(
'search/clickResult',
(
{ q, type }: { q: string; type?: RecentSearchType },
{ dispatch, getState },
) => {
const previous = getState().search.recent;
if (previous.some((x) => x.q === q && x.type === type)) {
return;
}
const me = getState().meta.get('me') as string;
const current = [{ type, q }, ...previous].slice(0, 4);
searchHistory.set(me, current);
dispatch(updateSearchHistory(current));
},
);
export const forgetSearchResult = createAppAsyncThunk(
'search/forgetResult',
(q: string, { dispatch, getState }) => {
const previous = getState().search.recent;
const me = getState().meta.get('me') as string;
const current = previous.filter((result) => result.q !== q);
searchHistory.set(me, current);
dispatch(updateSearchHistory(current));
},
);
export const updateSearchHistory = createAction<RecentSearch[]>(
'search/updateHistory',
);
export const hydrateSearch = createAppAsyncThunk(
'search/hydrate',
(_args, { dispatch, getState }) => {
const me = getState().meta.get('me') as string;
const history = searchHistory.get(me) as RecentSearch[] | null;
if (history !== null) {
dispatch(updateSearchHistory(history));
}
},
);

View file

@ -0,0 +1,11 @@
import { apiRequestGet } from 'mastodon/api';
import type {
ApiTermsOfServiceJSON,
ApiPrivacyPolicyJSON,
} from 'mastodon/api_types/instance';
export const apiGetTermsOfService = () =>
apiRequestGet<ApiTermsOfServiceJSON>('v1/instance/terms_of_service');
export const apiGetPrivacyPolicy = () =>
apiRequestGet<ApiPrivacyPolicyJSON>('v1/instance/privacy_policy');

View file

@ -0,0 +1,10 @@
import { apiRequestGet, apiRequestPost } from 'mastodon/api';
import type { ApiPollJSON } from 'mastodon/api_types/polls';
export const apiGetPoll = (pollId: string) =>
apiRequestGet<ApiPollJSON>(`/v1/polls/${pollId}`);
export const apiPollVote = (pollId: string, choices: string[]) =>
apiRequestPost<ApiPollJSON>(`/v1/polls/${pollId}/votes`, {
choices,
});

View file

@ -0,0 +1,16 @@
import { apiRequestGet } from 'mastodon/api';
import type {
ApiSearchType,
ApiSearchResultsJSON,
} from 'mastodon/api_types/search';
export const apiGetSearch = (params: {
q: string;
resolve?: boolean;
type?: ApiSearchType;
limit?: number;
offset?: number;
}) =>
apiRequestGet<ApiSearchResultsJSON>('v2/search', {
...params,
});

View file

@ -0,0 +1,9 @@
export interface ApiTermsOfServiceJSON {
updated_at: string;
content: string;
}
export interface ApiPrivacyPolicyJSON {
updated_at: string;
content: string;
}

View file

@ -18,6 +18,6 @@ export interface ApiPollJSON {
options: ApiPollOptionJSON[]; options: ApiPollOptionJSON[];
emojis: ApiCustomEmojiJSON[]; emojis: ApiCustomEmojiJSON[];
voted: boolean; voted?: boolean;
own_votes: number[]; own_votes?: number[];
} }

View file

@ -0,0 +1,11 @@
import type { ApiAccountJSON } from './accounts';
import type { ApiStatusJSON } from './statuses';
import type { ApiHashtagJSON } from './tags';
export type ApiSearchType = 'accounts' | 'statuses' | 'hashtags';
export interface ApiSearchResultsJSON {
accounts: ApiAccountJSON[];
statuses: ApiStatusJSON[];
hashtags: ApiHashtagJSON[];
}

View file

@ -36,7 +36,7 @@ export default class AttachmentList extends ImmutablePureComponent {
return ( return (
<li key={attachment.get('id')}> <li key={attachment.get('id')}>
<a href={displayUrl} target='_blank' rel='noopener noreferrer'> <a href={displayUrl} target='_blank' rel='noopener'>
{compact && <Icon id='link' icon={LinkIcon} />} {compact && <Icon id='link' icon={LinkIcon} />}
{compact && ' ' } {compact && ' ' }
{displayUrl ? filename(displayUrl) : <FormattedMessage id='attachments_list.unprocessed' defaultMessage='(unprocessed)' />} {displayUrl ? filename(displayUrl) : <FormattedMessage id='attachments_list.unprocessed' defaultMessage='(unprocessed)' />}

View file

@ -124,7 +124,7 @@ class DropdownMenu extends PureComponent {
return ( return (
<li className={classNames('dropdown-menu__item', { 'dropdown-menu__item--dangerous': dangerous })} key={`${text}-${i}`}> <li className={classNames('dropdown-menu__item', { 'dropdown-menu__item--dangerous': dangerous })} key={`${text}-${i}`}>
<a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex={0} ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}> <a href={href} target={target} data-method={method} rel='noopener' role='button' tabIndex={0} ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
{text} {text}
</a> </a>
</li> </li>

View file

@ -98,7 +98,7 @@ export default class ErrorBoundary extends PureComponent {
)} )}
</p> </p>
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied ? 'copied' : ''}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p> <p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied ? 'copied' : ''}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
</div> </div>
<Helmet> <Helmet>

View file

@ -92,7 +92,7 @@ export const FollowButton: React.FC<{
<a <a
href='/settings/profile' href='/settings/profile'
target='_blank' target='_blank'
rel='noreferrer noopener' rel='noopener'
className='button button-secondary' className='button button-secondary'
> >
{label} {label}

View file

@ -12,6 +12,7 @@ import { Sparklines, SparklinesCurve } from 'react-sparklines';
import { ShortNumber } from 'mastodon/components/short_number'; import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton'; import { Skeleton } from 'mastodon/components/skeleton';
import type { Hashtag as HashtagType } from 'mastodon/models/tags';
interface SilentErrorBoundaryProps { interface SilentErrorBoundaryProps {
children: React.ReactNode; children: React.ReactNode;
@ -80,6 +81,22 @@ export const ImmutableHashtag = ({ hashtag }: ImmutableHashtagProps) => (
/> />
); );
export const CompatibilityHashtag: React.FC<{
hashtag: HashtagType;
}> = ({ hashtag }) => (
<Hashtag
name={hashtag.name}
to={`/tags/${hashtag.name}`}
people={
(hashtag.history[0].accounts as unknown as number) * 1 +
((hashtag.history[1]?.accounts ?? 0) as unknown as number) * 1
}
history={hashtag.history
.map((day) => (day.uses as unknown as number) * 1)
.reverse()}
/>
);
export interface HashtagProps { export interface HashtagProps {
className?: string; className?: string;
description?: React.ReactNode; description?: React.ReactNode;

View file

@ -122,7 +122,7 @@ class Item extends PureComponent {
if (attachment.get('type') === 'unknown') { if (attachment.get('type') === 'unknown') {
return ( return (
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}> <div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={description} lang={lang} target='_blank' rel='noopener noreferrer'> <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={description} lang={lang} target='_blank' rel='noopener'>
<Blurhash <Blurhash
hash={attachment.get('blurhash')} hash={attachment.get('blurhash')}
className='media-gallery__preview' className='media-gallery__preview'
@ -154,7 +154,7 @@ class Item extends PureComponent {
href={attachment.get('remote_url') || originalUrl} href={attachment.get('remote_url') || originalUrl}
onClick={this.handleClick} onClick={this.handleClick}
target='_blank' target='_blank'
rel='noopener noreferrer' rel='noopener'
> >
<img <img
src={previewUrl} src={previewUrl}

View file

@ -33,15 +33,10 @@ const messages = defineMessages({
}, },
}); });
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
return obj;
}, {});
class Poll extends ImmutablePureComponent { class Poll extends ImmutablePureComponent {
static propTypes = { static propTypes = {
identity: identityContextPropShape, identity: identityContextPropShape,
poll: ImmutablePropTypes.map.isRequired, poll: ImmutablePropTypes.record.isRequired,
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
lang: PropTypes.string, lang: PropTypes.string,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
@ -150,7 +145,7 @@ class Poll extends ImmutablePureComponent {
let titleHtml = option.getIn(['translation', 'titleHtml']) || option.get('titleHtml'); let titleHtml = option.getIn(['translation', 'titleHtml']) || option.get('titleHtml');
if (!titleHtml) { if (!titleHtml) {
const emojiMap = makeEmojiMap(poll); const emojiMap = emojiMap(poll);
titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap); titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
} }

View file

@ -42,7 +42,7 @@ class ServerBanner extends PureComponent {
return ( return (
<div className='server-banner'> <div className='server-banner'>
<div className='server-banner__introduction'> <div className='server-banner__introduction'>
<FormattedMessage id='server_banner.is_one_of_many' defaultMessage='{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} /> <FormattedMessage id='server_banner.is_one_of_many' defaultMessage='{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank' rel='noopener'>Mastodon</a> }} />
</div> </div>
<Link to='/about'> <Link to='/about'>

View file

@ -302,7 +302,7 @@ class Status extends ImmutablePureComponent {
if (e?.button === 0 && !(e?.ctrlKey || e?.metaKey)) { if (e?.button === 0 && !(e?.ctrlKey || e?.metaKey)) {
history.push(path); history.push(path);
} else if (e?.button === 1 || (e?.button === 0 && (e?.ctrlKey || e?.metaKey))) { } else if (e?.button === 1 || (e?.button === 0 && (e?.ctrlKey || e?.metaKey))) {
window.open(path, '_blank', 'noreferrer noopener'); window.open(path, '_blank', 'noopener');
} }
}; };

View file

@ -9,14 +9,14 @@ import Poll from 'mastodon/components/poll';
const mapDispatchToProps = (dispatch, { pollId }) => ({ const mapDispatchToProps = (dispatch, { pollId }) => ({
refresh: debounce( refresh: debounce(
() => { () => {
dispatch(fetchPoll(pollId)); dispatch(fetchPoll({ pollId }));
}, },
1000, 1000,
{ leading: true }, { leading: true },
), ),
onVote (choices) { onVote (choices) {
dispatch(vote(pollId, choices)); dispatch(vote({ pollId, choices }));
}, },
onInteractionModal (type, status) { onInteractionModal (type, status) {
@ -32,7 +32,7 @@ const mapDispatchToProps = (dispatch, { pollId }) => ({
}); });
const mapStateToProps = (state, { pollId }) => ({ const mapStateToProps = (state, { pollId }) => ({
poll: state.getIn(['polls', pollId]), poll: state.polls.get(pollId),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(Poll); export default connect(mapStateToProps, mapDispatchToProps)(Poll);

View file

@ -20,7 +20,7 @@ import Column from 'mastodon/components/column';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { ServerHeroImage } from 'mastodon/components/server_hero_image'; import { ServerHeroImage } from 'mastodon/components/server_hero_image';
import { Skeleton } from 'mastodon/components/skeleton'; import { Skeleton } from 'mastodon/components/skeleton';
import LinkFooter from 'mastodon/features/ui/components/link_footer'; import { LinkFooter} from 'mastodon/features/ui/components/link_footer';
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'column.about', defaultMessage: 'About' }, title: { id: 'column.about', defaultMessage: 'About' },
@ -173,7 +173,7 @@ class About extends PureComponent {
<div className='about__header'> <div className='about__header'>
<ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' /> <ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />
<h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1> <h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1>
<p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank'>Mastodon</a> }} /></p> <p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank' rel='noopener'>Mastodon</a> }} /></p>
</div> </div>
<div className='about__meta'> <div className='about__meta'>

View file

@ -6,6 +6,7 @@ import classNames from 'classnames';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { NavLink, withRouter } from 'react-router-dom'; import { NavLink, withRouter } from 'react-router-dom';
import { isFulfilled, isRejected } from '@reduxjs/toolkit';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
@ -233,8 +234,20 @@ class Header extends ImmutablePureComponent {
const link = e.currentTarget; const link = e.currentTarget;
onOpenURL(link.href, history, () => { onOpenURL(link.href).then((result) => {
window.location = link.href; if (isFulfilled(result)) {
if (result.payload.accounts[0]) {
history.push(`/@${result.payload.accounts[0].acct}`);
} else if (result.payload.statuses[0]) {
history.push(`/@${result.payload.statuses[0].account.acct}/${result.payload.statuses[0].id}`);
} else {
window.location = link.href;
}
} else if (isRejected(result)) {
window.location = link.href;
}
}).catch(() => {
// Nothing
}); });
} }
}; };
@ -450,7 +463,7 @@ class Header extends ImmutablePureComponent {
<div className='account__header__bar'> <div className='account__header__bar'>
<div className='account__header__tabs'> <div className='account__header__tabs'>
<a className='avatar' href={account.get('avatar')} rel='noopener noreferrer' target='_blank' onClick={this.handleAvatarClick}> <a className='avatar' href={account.get('avatar')} rel='noopener' target='_blank' onClick={this.handleAvatarClick}>
<Avatar account={suspended || hidden ? undefined : account} size={90} /> <Avatar account={suspended || hidden ? undefined : account} size={90} />
</a> </a>

View file

@ -174,8 +174,8 @@ const mapDispatchToProps = (dispatch) => ({
})); }));
}, },
onOpenURL (url, routerHistory, onFailure) { onOpenURL (url) {
dispatch(openURL(url, routerHistory, onFailure)); return dispatch(openURL({ url }));
}, },
}); });

View file

@ -1,405 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { defineMessages, injectIntl, FormattedMessage, FormattedList } from 'react-intl';
import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import CancelIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import { Icon } from 'mastodon/components/icon';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { domain, searchEnabled } from 'mastodon/initial_state';
import { HASHTAG_REGEX } from 'mastodon/utils/hashtags';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
const messages = defineMessages({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
placeholderSignedIn: { id: 'search.search_or_paste', defaultMessage: 'Search or paste URL' },
});
const labelForRecentSearch = search => {
switch(search.get('type')) {
case 'account':
return `@${search.get('q')}`;
case 'hashtag':
return `#${search.get('q')}`;
default:
return search.get('q');
}
};
class Search extends PureComponent {
static propTypes = {
identity: identityContextPropShape,
value: PropTypes.string.isRequired,
recent: ImmutablePropTypes.orderedSet,
submitted: PropTypes.bool,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onOpenURL: PropTypes.func.isRequired,
onClickSearchResult: PropTypes.func.isRequired,
onForgetSearchResult: PropTypes.func.isRequired,
onClear: PropTypes.func.isRequired,
onShow: PropTypes.func.isRequired,
openInRoute: PropTypes.bool,
intl: PropTypes.object.isRequired,
singleColumn: PropTypes.bool,
...WithRouterPropTypes,
};
state = {
expanded: false,
selectedOption: -1,
options: [],
};
defaultOptions = [
{ key: 'prompt-has', label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:'); } },
{ key: 'prompt-is', label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:'); } },
{ key: 'prompt-my', label: <><mark>my:</mark> <FormattedList type='disjunction' value={['favourited', 'bookmarked', 'boosted']} /></>, action: e => { e.preventDefault(); this._insertText('my:'); } },
{ key: 'prompt-language', label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:'); } },
{ key: 'prompt-from', label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:'); } },
{ key: 'prompt-domain', label: <><mark>domain:</mark> <FormattedMessage id='search_popout.domain' defaultMessage='domain' /></>, action: e => { e.preventDefault(); this._insertText('domain:'); } },
{ key: 'prompt-before', label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:'); } },
{ key: 'prompt-during', label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:'); } },
{ key: 'prompt-after', label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:'); } },
{ key: 'prompt-in', label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library', 'public']} /></>, action: e => { e.preventDefault(); this._insertText('in:'); } },
{ key: 'prompt-order', label: <><mark>order:</mark> <FormattedList type='disjunction' value={['desc', 'asc']} /></>, action: e => { e.preventDefault(); this._insertText('order:'); } },
];
setRef = c => {
this.searchForm = c;
};
handleChange = ({ target }) => {
const { onChange } = this.props;
onChange(target.value);
this._calculateOptions(target.value);
};
handleClear = e => {
const { value, submitted, onClear } = this.props;
e.preventDefault();
if (value.length > 0 || submitted) {
onClear();
this.setState({ options: [], selectedOption: -1 });
}
};
handleKeyDown = (e) => {
const { selectedOption } = this.state;
const options = searchEnabled ? this._getOptions().concat(this.defaultOptions) : this._getOptions();
switch(e.key) {
case 'Escape':
e.preventDefault();
this._unfocus();
break;
case 'ArrowDown':
e.preventDefault();
if (options.length > 0) {
this.setState({ selectedOption: Math.min(selectedOption + 1, options.length - 1) });
}
break;
case 'ArrowUp':
e.preventDefault();
if (options.length > 0) {
this.setState({ selectedOption: Math.max(selectedOption - 1, -1) });
}
break;
case 'Enter':
e.preventDefault();
if (selectedOption === -1) {
this._submit();
} else if (options.length > 0) {
options[selectedOption].action(e);
}
break;
case 'Delete':
if (selectedOption > -1 && options.length > 0) {
const search = options[selectedOption];
if (typeof search.forget === 'function') {
e.preventDefault();
search.forget(e);
}
}
break;
}
};
handleFocus = () => {
const { onShow, singleColumn } = this.props;
this.setState({ expanded: true, selectedOption: -1 });
onShow();
if (this.searchForm && !singleColumn) {
const { left, right } = this.searchForm.getBoundingClientRect();
if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) {
this.searchForm.scrollIntoView();
}
}
};
handleBlur = () => {
this.setState({ expanded: false, selectedOption: -1 });
};
handleHashtagClick = () => {
const { value, onClickSearchResult, history } = this.props;
const query = value.trim().replace(/^#/, '');
history.push(`/tags/${query}`);
onClickSearchResult(query, 'hashtag');
this._unfocus();
};
handleAccountClick = () => {
const { value, onClickSearchResult, history } = this.props;
const query = value.trim().replace(/^@/, '');
history.push(`/@${query}`);
onClickSearchResult(query, 'account');
this._unfocus();
};
handleURLClick = () => {
const { value, onOpenURL, history } = this.props;
onOpenURL(value, history);
this._unfocus();
};
handleStatusSearch = () => {
this._submit('statuses');
};
handleAccountSearch = () => {
this._submit('accounts');
};
handleRecentSearchClick = search => {
const { onChange, history } = this.props;
if (search.get('type') === 'account') {
history.push(`/@${search.get('q')}`);
} else if (search.get('type') === 'hashtag') {
history.push(`/tags/${search.get('q')}`);
} else {
onChange(search.get('q'));
this._submit(search.get('type'));
}
this._unfocus();
};
handleForgetRecentSearchClick = search => {
const { onForgetSearchResult } = this.props;
onForgetSearchResult(search.get('q'));
};
_unfocus () {
document.querySelector('.ui').parentElement.focus();
}
_insertText (text) {
const { value, onChange } = this.props;
if (value === '') {
onChange(text);
} else if (value[value.length - 1] === ' ') {
onChange(`${value}${text}`);
} else {
onChange(`${value} ${text}`);
}
}
_submit (type) {
const { onSubmit, openInRoute, value, onClickSearchResult, history } = this.props;
onSubmit(type);
if (value) {
onClickSearchResult(value, type);
}
if (openInRoute) {
history.push('/search');
}
this._unfocus();
}
_getOptions () {
const { options } = this.state;
if (options.length > 0) {
return options;
}
const { recent } = this.props;
return recent.toArray().map(search => ({
key: `${search.get('type')}/${search.get('q')}`,
label: labelForRecentSearch(search),
action: () => this.handleRecentSearchClick(search),
forget: e => {
e.stopPropagation();
this.handleForgetRecentSearchClick(search);
},
}));
}
_calculateOptions (value) {
const { signedIn } = this.props.identity;
const trimmedValue = value.trim();
const options = [];
if (trimmedValue.length > 0) {
const couldBeURL = trimmedValue.startsWith('https://') && !trimmedValue.includes(' ');
if (couldBeURL) {
options.push({ key: 'open-url', label: <FormattedMessage id='search.quick_action.open_url' defaultMessage='Open URL in Mastodon' />, action: this.handleURLClick });
}
const couldBeHashtag = (trimmedValue.startsWith('#') && trimmedValue.length > 1) || trimmedValue.match(HASHTAG_REGEX);
if (couldBeHashtag) {
options.push({ key: 'go-to-hashtag', label: <FormattedMessage id='search.quick_action.go_to_hashtag' defaultMessage='Go to hashtag {x}' values={{ x: <mark>#{trimmedValue.replace(/^#/, '')}</mark> }} />, action: this.handleHashtagClick });
}
const couldBeUsername = trimmedValue.match(/^@?[a-z0-9_-]+(@[^\s]+)?$/i);
if (couldBeUsername) {
options.push({ key: 'go-to-account', label: <FormattedMessage id='search.quick_action.go_to_account' defaultMessage='Go to profile {x}' values={{ x: <mark>@{trimmedValue.replace(/^@/, '')}</mark> }} />, action: this.handleAccountClick });
}
const couldBeStatusSearch = searchEnabled;
if (couldBeStatusSearch && signedIn) {
options.push({ key: 'status-search', label: <FormattedMessage id='search.quick_action.status_search' defaultMessage='Posts matching {x}' values={{ x: <mark>{trimmedValue}</mark> }} />, action: this.handleStatusSearch });
}
const couldBeUserSearch = true;
if (couldBeUserSearch) {
options.push({ key: 'account-search', label: <FormattedMessage id='search.quick_action.account_search' defaultMessage='Profiles matching {x}' values={{ x: <mark>{trimmedValue}</mark> }} />, action: this.handleAccountSearch });
}
}
this.setState({ options });
}
render () {
const { intl, value, submitted, recent } = this.props;
const { expanded, options, selectedOption } = this.state;
const { signedIn } = this.props.identity;
const hasValue = value.length > 0 || submitted;
return (
<div className={classNames('search', { active: expanded })}>
<input
ref={this.setRef}
className='search__input'
type='text'
placeholder={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)}
aria-label={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)}
value={value}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
/>
<div role='button' tabIndex={0} className='search__icon' onClick={this.handleClear}>
<Icon id='search' icon={SearchIcon} className={hasValue ? '' : 'active'} />
<Icon id='times-circle' icon={CancelIcon} className={hasValue ? 'active' : ''} aria-label={intl.formatMessage(messages.placeholder)} />
</div>
<div className='search__popout'>
{options.length === 0 && (
<>
<h4><FormattedMessage id='search_popout.recent' defaultMessage='Recent searches' /></h4>
<div className='search__popout__menu'>
{recent.size > 0 ? this._getOptions().map(({ label, key, action, forget }, i) => (
<button key={key} onMouseDown={action} className={classNames('search__popout__menu__item search__popout__menu__item--flex', { selected: selectedOption === i })}>
<span>{label}</span>
<button className='icon-button' onMouseDown={forget}><Icon id='times' icon={CloseIcon} /></button>
</button>
)) : (
<div className='search__popout__menu__message'>
<FormattedMessage id='search.no_recent_searches' defaultMessage='No recent searches' />
</div>
)}
</div>
</>
)}
{options.length > 0 && (
<>
<h4><FormattedMessage id='search_popout.quick_actions' defaultMessage='Quick actions' /></h4>
<div className='search__popout__menu'>
{options.map(({ key, label, action }, i) => (
<button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === i })}>
{label}
</button>
))}
</div>
</>
)}
<h4><FormattedMessage id='search_popout.options' defaultMessage='Search options' /></h4>
{searchEnabled && signedIn ? (
<div className='search__popout__menu'>
{this.defaultOptions.map(({ key, label, action }, i) => (
<button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === ((options.length || recent.size) + i) })}>
{label}
</button>
))}
</div>
) : (
<div className='search__popout__menu__message'>
{searchEnabled ? (
<FormattedMessage id='search_popout.full_text_search_logged_out_message' defaultMessage='Only available when logged in.' />
) : (
<FormattedMessage id='search_popout.full_text_search_disabled_message' defaultMessage='Not available on {domain}.' values={{ domain }} />
)}
</div>
)}
</div>
</div>
);
}
}
export default withRouter(withIdentity(injectIntl(Search)));

View file

@ -0,0 +1,635 @@
import { useCallback, useState, useRef } from 'react';
import {
defineMessages,
useIntl,
FormattedMessage,
FormattedList,
} from 'react-intl';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { isFulfilled } from '@reduxjs/toolkit';
import CancelIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import {
clickSearchResult,
forgetSearchResult,
openURL,
} from 'mastodon/actions/search';
import { Icon } from 'mastodon/components/icon';
import { useIdentity } from 'mastodon/identity_context';
import { domain, searchEnabled } from 'mastodon/initial_state';
import type { RecentSearch, SearchType } from 'mastodon/models/search';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { HASHTAG_REGEX } from 'mastodon/utils/hashtags';
const messages = defineMessages({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
placeholderSignedIn: {
id: 'search.search_or_paste',
defaultMessage: 'Search or paste URL',
},
});
const labelForRecentSearch = (search: RecentSearch) => {
switch (search.type) {
case 'account':
return `@${search.q}`;
case 'hashtag':
return `#${search.q}`;
default:
return search.q;
}
};
const unfocus = () => {
document.querySelector('.ui')?.parentElement?.focus();
};
interface SearchOption {
key: string;
label: React.ReactNode;
action: (e: React.MouseEvent | React.KeyboardEvent) => void;
forget?: (e: React.MouseEvent | React.KeyboardEvent) => void;
}
export const Search: React.FC<{
singleColumn: boolean;
initialValue?: string;
}> = ({ singleColumn, initialValue }) => {
const intl = useIntl();
const recent = useAppSelector((state) => state.search.recent);
const { signedIn } = useIdentity();
const dispatch = useAppDispatch();
const history = useHistory();
const searchInputRef = useRef<HTMLInputElement>(null);
const [value, setValue] = useState(initialValue ?? '');
const hasValue = value.length > 0;
const [expanded, setExpanded] = useState(false);
const [selectedOption, setSelectedOption] = useState(-1);
const [quickActions, setQuickActions] = useState<SearchOption[]>([]);
const searchOptions: SearchOption[] = [];
if (searchEnabled) {
searchOptions.push(
{
key: 'prompt-has',
label: (
<>
<mark>has:</mark>{' '}
<FormattedList
type='disjunction'
value={['media', 'poll', 'embed']}
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('has:');
},
},
{
key: 'prompt-is',
label: (
<>
<mark>is:</mark>{' '}
<FormattedList type='disjunction' value={['reply', 'sensitive']} />
</>
),
action: (e) => {
e.preventDefault();
insertText('is:');
},
},
{
key: 'prompt-my',
label: (
<>
<mark>my:</mark>{' '}
<FormattedList type='disjunction' value={['fav', 'bm']} />
</>
),
action: (e) => {
e.preventDefault();
insertText('my:');
},
},
{
key: 'prompt-language',
label: (
<>
<mark>language:</mark>{' '}
<FormattedMessage
id='search_popout.language_code'
defaultMessage='ISO language code'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('language:');
},
},
{
key: 'prompt-from',
label: (
<>
<mark>from:</mark>{' '}
<FormattedMessage id='search_popout.user' defaultMessage='user' />
</>
),
action: (e) => {
e.preventDefault();
insertText('from:');
},
},
{
key: 'prompt-domain',
label: (
<>
<mark>language:</mark>{' '}
<FormattedMessage
id='search_popout.domain'
defaultMessage='user domain'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('domain:');
},
},
{
key: 'prompt-before',
label: (
<>
<mark>before:</mark>{' '}
<FormattedMessage
id='search_popout.specific_date'
defaultMessage='specific date'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('before:');
},
},
{
key: 'prompt-during',
label: (
<>
<mark>during:</mark>{' '}
<FormattedMessage
id='search_popout.specific_date'
defaultMessage='specific date'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('during:');
},
},
{
key: 'prompt-after',
label: (
<>
<mark>after:</mark>{' '}
<FormattedMessage
id='search_popout.specific_date'
defaultMessage='specific date'
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('after:');
},
},
{
key: 'prompt-in',
label: (
<>
<mark>in:</mark>{' '}
<FormattedList
type='disjunction'
value={['all', 'library', 'public']}
/>
</>
),
action: (e) => {
e.preventDefault();
insertText('in:');
},
},
{
key: 'prompt-order',
label: (
<>
<mark>order:</mark>{' '}
<FormattedList type='disjunction' value={['desc', 'asc']} />
</>
),
action: (e) => {
e.preventDefault();
insertText('order:');
},
},
);
}
const recentOptions: SearchOption[] = recent.map((search) => ({
key: `${search.type}/${search.q}`,
label: labelForRecentSearch(search),
action: () => {
setValue(search.q);
if (search.type === 'account') {
history.push(`/@${search.q}`);
} else if (search.type === 'hashtag') {
history.push(`/tags/${search.q}`);
} else {
const queryParams = new URLSearchParams({ q: search.q });
if (search.type) queryParams.set('type', search.type);
history.push({ pathname: '/search', search: queryParams.toString() });
}
unfocus();
},
forget: (e) => {
e.stopPropagation();
void dispatch(forgetSearchResult(search.q));
},
}));
const navigableOptions = hasValue
? quickActions.concat(searchOptions)
: recentOptions.concat(quickActions, searchOptions);
const insertText = (text: string) => {
setValue((currentValue) => {
if (currentValue === '') {
return text;
} else if (currentValue.endsWith(' ')) {
return `${currentValue}${text}`;
} else {
return `${currentValue} ${text}`;
}
});
};
const submit = useCallback(
(q: string, type?: SearchType) => {
void dispatch(clickSearchResult({ q, type }));
const queryParams = new URLSearchParams({ q });
if (type) queryParams.set('type', type);
history.push({ pathname: '/search', search: queryParams.toString() });
unfocus();
},
[dispatch, history],
);
const handleChange = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
setValue(value);
const trimmedValue = value.trim();
const newQuickActions = [];
if (trimmedValue.length > 0) {
const couldBeURL =
trimmedValue.startsWith('https://') && !trimmedValue.includes(' ');
if (couldBeURL) {
newQuickActions.push({
key: 'open-url',
label: (
<FormattedMessage
id='search.quick_action.open_url'
defaultMessage='Open URL in Mastodon'
/>
),
action: async () => {
const result = await dispatch(openURL({ url: trimmedValue }));
if (isFulfilled(result)) {
if (result.payload.accounts[0]) {
history.push(`/@${result.payload.accounts[0].acct}`);
} else if (result.payload.statuses[0]) {
history.push(
`/@${result.payload.statuses[0].account.acct}/${result.payload.statuses[0].id}`,
);
}
}
unfocus();
},
});
}
const couldBeHashtag =
(trimmedValue.startsWith('#') && trimmedValue.length > 1) ||
trimmedValue.match(HASHTAG_REGEX);
if (couldBeHashtag) {
newQuickActions.push({
key: 'go-to-hashtag',
label: (
<FormattedMessage
id='search.quick_action.go_to_hashtag'
defaultMessage='Go to hashtag {x}'
values={{ x: <mark>#{trimmedValue.replace(/^#/, '')}</mark> }}
/>
),
action: () => {
const query = trimmedValue.replace(/^#/, '');
history.push(`/tags/${query}`);
void dispatch(clickSearchResult({ q: query, type: 'hashtag' }));
unfocus();
},
});
}
const couldBeUsername = /^@?[a-z0-9_-]+(@[^\s]+)?$/i.exec(trimmedValue);
if (couldBeUsername) {
newQuickActions.push({
key: 'go-to-account',
label: (
<FormattedMessage
id='search.quick_action.go_to_account'
defaultMessage='Go to profile {x}'
values={{ x: <mark>@{trimmedValue.replace(/^@/, '')}</mark> }}
/>
),
action: () => {
const query = trimmedValue.replace(/^@/, '');
history.push(`/@${query}`);
void dispatch(clickSearchResult({ q: query, type: 'account' }));
unfocus();
},
});
}
const couldBeStatusSearch = searchEnabled;
if (couldBeStatusSearch && signedIn) {
newQuickActions.push({
key: 'status-search',
label: (
<FormattedMessage
id='search.quick_action.status_search'
defaultMessage='Posts matching {x}'
values={{ x: <mark>{trimmedValue}</mark> }}
/>
),
action: () => {
submit(trimmedValue, 'statuses');
},
});
}
newQuickActions.push({
key: 'account-search',
label: (
<FormattedMessage
id='search.quick_action.account_search'
defaultMessage='Profiles matching {x}'
values={{ x: <mark>{trimmedValue}</mark> }}
/>
),
action: () => {
submit(trimmedValue, 'accounts');
},
});
}
setQuickActions(newQuickActions);
},
[dispatch, history, signedIn, setValue, setQuickActions, submit],
);
const handleClear = useCallback(() => {
setValue('');
setQuickActions([]);
setSelectedOption(-1);
}, [setValue, setQuickActions, setSelectedOption]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
switch (e.key) {
case 'Escape':
e.preventDefault();
unfocus();
break;
case 'ArrowDown':
e.preventDefault();
if (navigableOptions.length > 0) {
setSelectedOption(
Math.min(selectedOption + 1, navigableOptions.length - 1),
);
}
break;
case 'ArrowUp':
e.preventDefault();
if (navigableOptions.length > 0) {
setSelectedOption(Math.max(selectedOption - 1, -1));
}
break;
case 'Enter':
e.preventDefault();
if (selectedOption === -1) {
submit(value);
} else if (navigableOptions.length > 0) {
navigableOptions[selectedOption]?.action(e);
}
break;
case 'Delete':
if (selectedOption > -1 && navigableOptions.length > 0) {
const search = navigableOptions[selectedOption];
if (typeof search?.forget === 'function') {
e.preventDefault();
search.forget(e);
}
}
break;
}
},
[navigableOptions, value, selectedOption, setSelectedOption, submit],
);
const handleFocus = useCallback(() => {
setExpanded(true);
setSelectedOption(-1);
if (searchInputRef.current && !singleColumn) {
const { left, right } = searchInputRef.current.getBoundingClientRect();
if (
left < 0 ||
right > (window.innerWidth || document.documentElement.clientWidth)
) {
searchInputRef.current.scrollIntoView();
}
}
}, [setExpanded, setSelectedOption, singleColumn]);
const handleBlur = useCallback(() => {
setExpanded(false);
setSelectedOption(-1);
}, [setExpanded, setSelectedOption]);
return (
<form className={classNames('search', { active: expanded })}>
<input
ref={searchInputRef}
className='search__input'
type='text'
placeholder={intl.formatMessage(
signedIn ? messages.placeholderSignedIn : messages.placeholder,
)}
aria-label={intl.formatMessage(
signedIn ? messages.placeholderSignedIn : messages.placeholder,
)}
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
onBlur={handleBlur}
/>
<button type='button' className='search__icon' onClick={handleClear}>
<Icon
id='search'
icon={SearchIcon}
className={hasValue ? '' : 'active'}
/>
<Icon
id='times-circle'
icon={CancelIcon}
className={hasValue ? 'active' : ''}
aria-label={intl.formatMessage(messages.placeholder)}
/>
</button>
<div className='search__popout'>
{!hasValue && (
<>
<h4>
<FormattedMessage
id='search_popout.recent'
defaultMessage='Recent searches'
/>
</h4>
<div className='search__popout__menu'>
{recentOptions.length > 0 ? (
recentOptions.map(({ label, key, action, forget }, i) => (
<button
key={key}
onMouseDown={action}
className={classNames(
'search__popout__menu__item search__popout__menu__item--flex',
{ selected: selectedOption === i },
)}
>
<span>{label}</span>
<button className='icon-button' onMouseDown={forget}>
<Icon id='times' icon={CloseIcon} />
</button>
</button>
))
) : (
<div className='search__popout__menu__message'>
<FormattedMessage
id='search.no_recent_searches'
defaultMessage='No recent searches'
/>
</div>
)}
</div>
</>
)}
{quickActions.length > 0 && (
<>
<h4>
<FormattedMessage
id='search_popout.quick_actions'
defaultMessage='Quick actions'
/>
</h4>
<div className='search__popout__menu'>
{quickActions.map(({ key, label, action }, i) => (
<button
key={key}
onMouseDown={action}
className={classNames('search__popout__menu__item', {
selected: selectedOption === i,
})}
>
{label}
</button>
))}
</div>
</>
)}
<h4>
<FormattedMessage
id='search_popout.options'
defaultMessage='Search options'
/>
</h4>
{searchEnabled && signedIn ? (
<div className='search__popout__menu'>
{searchOptions.map(({ key, label, action }, i) => (
<button
key={key}
onMouseDown={action}
className={classNames('search__popout__menu__item', {
selected:
selectedOption ===
(quickActions.length || recent.length) + i,
})}
>
{label}
</button>
))}
</div>
) : (
<div className='search__popout__menu__message'>
{searchEnabled ? (
<FormattedMessage
id='search_popout.full_text_search_logged_out_message'
defaultMessage='Only available when logged in.'
/>
) : (
<FormattedMessage
id='search_popout.full_text_search_disabled_message'
defaultMessage='Not available on {domain}.'
values={{ domain }}
/>
)}
</div>
)}
</div>
</form>
);
};

View file

@ -1,93 +0,0 @@
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { expandSearch } from 'mastodon/actions/search';
import { Account } from 'mastodon/components/account';
import { Icon } from 'mastodon/components/icon';
import { LoadMore } from 'mastodon/components/load_more';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { SearchSection } from 'mastodon/features/explore/components/search_section';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { ImmutableHashtag as Hashtag } from '../../../components/hashtag';
import StatusContainer from '../../../containers/status_container';
const INITIAL_PAGE_LIMIT = 10;
const withoutLastResult = list => {
if (list.size > INITIAL_PAGE_LIMIT && list.size % INITIAL_PAGE_LIMIT === 1) {
return list.skipLast(1);
} else {
return list;
}
};
export const SearchResults = () => {
const results = useAppSelector((state) => state.getIn(['search', 'results']));
const isLoading = useAppSelector((state) => state.getIn(['search', 'isLoading']));
const dispatch = useAppDispatch();
const handleLoadMoreAccounts = useCallback(() => {
dispatch(expandSearch('accounts'));
}, [dispatch]);
const handleLoadMoreStatuses = useCallback(() => {
dispatch(expandSearch('statuses'));
}, [dispatch]);
const handleLoadMoreHashtags = useCallback(() => {
dispatch(expandSearch('hashtags'));
}, [dispatch]);
let accounts, statuses, hashtags;
if (results.get('accounts') && results.get('accounts').size > 0) {
accounts = (
<SearchSection title={<><Icon id='users' icon={PeopleIcon} /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>}>
{withoutLastResult(results.get('accounts')).map(accountId => <Account key={accountId} id={accountId} />)}
{(results.get('accounts').size > INITIAL_PAGE_LIMIT && results.get('accounts').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={handleLoadMoreAccounts} />}
</SearchSection>
);
}
if (results.get('hashtags') && results.get('hashtags').size > 0) {
hashtags = (
<SearchSection title={<><Icon id='hashtag' icon={TagIcon} /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></>}>
{withoutLastResult(results.get('hashtags')).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
{(results.get('hashtags').size > INITIAL_PAGE_LIMIT && results.get('hashtags').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={handleLoadMoreHashtags} />}
</SearchSection>
);
}
if (results.get('statuses') && results.get('statuses').size > 0) {
statuses = (
<SearchSection title={<><Icon id='quote-right' icon={FindInPageIcon} /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></>}>
{withoutLastResult(results.get('statuses')).map(statusId => <StatusContainer key={statusId} id={statusId} />)}
{(results.get('statuses').size > INITIAL_PAGE_LIMIT && results.get('statuses').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={handleLoadMoreStatuses} />}
</SearchSection>
);
}
return (
<div className='search-results'>
{!accounts && !hashtags && !statuses && (
isLoading ? (
<LoadingIndicator />
) : (
<div className='empty-column-indicator'>
<FormattedMessage id='search_results.nothing_found' defaultMessage='Could not find anything for these search terms' />
</div>
)
)}
{accounts}
{hashtags}
{statuses}
</div>
);
};

View file

@ -1,59 +0,0 @@
import { createSelector } from '@reduxjs/toolkit';
import { connect } from 'react-redux';
import {
changeSearch,
clearSearch,
submitSearch,
showSearch,
openURL,
clickSearchResult,
forgetSearchResult,
} from 'mastodon/actions/search';
import Search from '../components/search';
const getRecentSearches = createSelector(
state => state.getIn(['search', 'recent']),
recent => recent.reverse(),
);
const mapStateToProps = state => ({
value: state.getIn(['search', 'value']),
submitted: state.getIn(['search', 'submitted']),
recent: getRecentSearches(state),
});
const mapDispatchToProps = dispatch => ({
onChange (value) {
dispatch(changeSearch(value));
},
onClear () {
dispatch(clearSearch());
},
onSubmit (type) {
dispatch(submitSearch(type));
},
onShow () {
dispatch(showSearch());
},
onOpenURL (q, routerHistory) {
dispatch(openURL(q, routerHistory));
},
onClickSearchResult (q, type) {
dispatch(clickSearchResult(q, type));
},
onForgetSearchResult (q) {
dispatch(forgetSearchResult(q));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(Search);

View file

@ -9,8 +9,6 @@ import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import spring from 'react-motion/lib/spring';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react'; import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import LogoutIcon from '@/material-icons/400-24px/logout.svg?react'; import LogoutIcon from '@/material-icons/400-24px/logout.svg?react';
@ -26,11 +24,9 @@ import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
import { changeComposing, mountCompose, unmountCompose } from '../../actions/compose'; import { changeComposing, mountCompose, unmountCompose } from '../../actions/compose';
import { mascot } from '../../initial_state'; import { mascot } from '../../initial_state';
import { isMobile } from '../../is_mobile'; import { isMobile } from '../../is_mobile';
import Motion from '../ui/util/optional_motion';
import { SearchResults } from './components/search_results'; import { Search } from './components/search';
import ComposeFormContainer from './containers/compose_form_container'; import ComposeFormContainer from './containers/compose_form_container';
import SearchContainer from './containers/search_container';
const messages = defineMessages({ const messages = defineMessages({
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
@ -43,9 +39,8 @@ const messages = defineMessages({
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' }, compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' },
}); });
const mapStateToProps = (state, ownProps) => ({ const mapStateToProps = (state) => ({
columns: state.getIn(['settings', 'columns']), columns: state.getIn(['settings', 'columns']),
showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : false,
}); });
class Compose extends PureComponent { class Compose extends PureComponent {
@ -54,7 +49,6 @@ class Compose extends PureComponent {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
columns: ImmutablePropTypes.list.isRequired, columns: ImmutablePropTypes.list.isRequired,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
showSearch: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -88,7 +82,7 @@ class Compose extends PureComponent {
}; };
render () { render () {
const { multiColumn, showSearch, intl } = this.props; const { multiColumn, intl } = this.props;
if (multiColumn) { if (multiColumn) {
const { columns } = this.props; const { columns } = this.props;
@ -113,7 +107,7 @@ class Compose extends PureComponent {
<a href='/auth/sign_out' className='drawer__tab' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}><Icon id='sign-out' icon={LogoutIcon} /></a> <a href='/auth/sign_out' className='drawer__tab' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}><Icon id='sign-out' icon={LogoutIcon} /></a>
</nav> </nav>
{multiColumn && <SearchContainer /> } {multiColumn && <Search /> }
<div className='drawer__pager'> <div className='drawer__pager'>
<div className='drawer__inner' onFocus={this.onFocus}> <div className='drawer__inner' onFocus={this.onFocus}>
@ -123,14 +117,6 @@ class Compose extends PureComponent {
<img alt='' draggable='false' src={mascot || elephantUIPlane} /> <img alt='' draggable='false' src={mascot || elephantUIPlane} />
</div> </div>
</div> </div>
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
{({ x }) => (
<div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
<SearchResults />
</div>
)}
</Motion>
</div> </div>
</div> </div>
); );

View file

@ -90,8 +90,8 @@ describe('emoji', () => {
}); });
it('keeps ordering as expected (issue fixed by PR 20677)', () => { it('keeps ordering as expected (issue fixed by PR 20677)', () => {
expect(emojify('<p>💕 <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener noreferrer" target="_blank">#<span>foo</span></a> test: foo.</p>')) expect(emojify('<p>💕 <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener" target="_blank">#<span>foo</span></a> test: foo.</p>'))
.toEqual('<p><picture><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"></picture> <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener noreferrer" target="_blank">#<span>foo</span></a> test: foo.</p>'); .toEqual('<p><picture><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"></picture> <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener" target="_blank">#<span>foo</span></a> test: foo.</p>');
}); });
}); });
}); });

View file

@ -1,20 +0,0 @@
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
export const SearchSection = ({ title, onClickMore, children }) => (
<div className='search-results__section'>
<div className='search-results__section__header'>
<h3>{title}</h3>
{onClickMore && <button onClick={onClickMore}><FormattedMessage id='search_results.see_all' defaultMessage='See all' /></button>}
</div>
{children}
</div>
);
SearchSection.propTypes = {
title: PropTypes.node.isRequired,
onClickMore: PropTypes.func,
children: PropTypes.children,
};

View file

@ -1,114 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import { NavLink, Switch, Route } from 'react-router-dom';
import { connect } from 'react-redux';
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header';
import Search from 'mastodon/features/compose/containers/search_container';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { trendsEnabled } from 'mastodon/initial_state';
import Links from './links';
import SearchResults from './results';
import Statuses from './statuses';
import Suggestions from './suggestions';
import Tags from './tags';
const messages = defineMessages({
title: { id: 'explore.title', defaultMessage: 'Explore' },
searchResults: { id: 'explore.search_results', defaultMessage: 'Search results' },
});
const mapStateToProps = state => ({
layout: state.getIn(['meta', 'layout']),
isSearching: state.getIn(['search', 'submitted']) || !trendsEnabled,
});
class Explore extends PureComponent {
static propTypes = {
identity: identityContextPropShape,
intl: PropTypes.object.isRequired,
multiColumn: PropTypes.bool,
isSearching: PropTypes.bool,
};
handleHeaderClick = () => {
this.column.scrollTop();
};
setRef = c => {
this.column = c;
};
render() {
const { intl, multiColumn, isSearching } = this.props;
const { signedIn } = this.props.identity;
return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon={isSearching ? 'search' : 'explore'}
iconComponent={isSearching ? SearchIcon : ExploreIcon}
title={intl.formatMessage(isSearching ? messages.searchResults : messages.title)}
onClick={this.handleHeaderClick}
multiColumn={multiColumn}
/>
<div className='explore__search-header'>
<Search />
</div>
{isSearching ? (
<SearchResults />
) : (
<>
<div className='account__section-headline'>
<NavLink exact to='/explore'>
<FormattedMessage tagName='div' id='explore.trending_statuses' defaultMessage='Posts' />
</NavLink>
<NavLink exact to='/explore/tags'>
<FormattedMessage tagName='div' id='explore.trending_tags' defaultMessage='Hashtags' />
</NavLink>
{signedIn && (
<NavLink exact to='/explore/suggestions'>
<FormattedMessage tagName='div' id='explore.suggested_follows' defaultMessage='People' />
</NavLink>
)}
<NavLink exact to='/explore/links'>
<FormattedMessage tagName='div' id='explore.trending_links' defaultMessage='News' />
</NavLink>
</div>
<Switch>
<Route path='/explore/tags' component={Tags} />
<Route path='/explore/links' component={Links} />
<Route path='/explore/suggestions' component={Suggestions} />
<Route exact path={['/explore', '/explore/posts', '/search']}>
<Statuses multiColumn={multiColumn} />
</Route>
</Switch>
<Helmet>
<title>{intl.formatMessage(messages.title)}</title>
<meta name='robots' content={isSearching ? 'noindex' : 'all'} />
</Helmet>
</>
)}
</Column>
);
}
}
export default withIdentity(connect(mapStateToProps)(injectIntl(Explore)));

View file

@ -0,0 +1,105 @@
import { useCallback, useRef } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import { NavLink, Switch, Route } from 'react-router-dom';
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
import { Column } from 'mastodon/components/column';
import type { ColumnRef } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { Search } from 'mastodon/features/compose/components/search';
import { useIdentity } from 'mastodon/identity_context';
import Links from './links';
import Statuses from './statuses';
import Suggestions from './suggestions';
import Tags from './tags';
const messages = defineMessages({
title: { id: 'explore.title', defaultMessage: 'Explore' },
});
const Explore: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
const { signedIn } = useIdentity();
const intl = useIntl();
const columnRef = useRef<ColumnRef>(null);
const handleHeaderClick = useCallback(() => {
columnRef.current?.scrollTop();
}, []);
return (
<Column
bindToDocument={!multiColumn}
ref={columnRef}
label={intl.formatMessage(messages.title)}
>
<ColumnHeader
icon={'explore'}
iconComponent={ExploreIcon}
title={intl.formatMessage(messages.title)}
onClick={handleHeaderClick}
multiColumn={multiColumn}
/>
<div className='explore__search-header'>
<Search singleColumn />
</div>
<div className='account__section-headline'>
<NavLink exact to='/explore'>
<FormattedMessage
tagName='div'
id='explore.trending_statuses'
defaultMessage='Posts'
/>
</NavLink>
<NavLink exact to='/explore/tags'>
<FormattedMessage
tagName='div'
id='explore.trending_tags'
defaultMessage='Hashtags'
/>
</NavLink>
{signedIn && (
<NavLink exact to='/explore/suggestions'>
<FormattedMessage
tagName='div'
id='explore.suggested_follows'
defaultMessage='People'
/>
</NavLink>
)}
<NavLink exact to='/explore/links'>
<FormattedMessage
tagName='div'
id='explore.trending_links'
defaultMessage='News'
/>
</NavLink>
</div>
<Switch>
<Route path='/explore/tags' component={Tags} />
<Route path='/explore/links' component={Links} />
<Route path='/explore/suggestions' component={Suggestions} />
<Route exact path={['/explore', '/explore/posts']}>
<Statuses multiColumn={multiColumn} />
</Route>
</Switch>
<Helmet>
<title>{intl.formatMessage(messages.title)}</title>
<meta name='robots' content='all' />
</Helmet>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default Explore;

View file

@ -1,232 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { submitSearch, expandSearch } from 'mastodon/actions/search';
import { Account } from 'mastodon/components/account';
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
import { Icon } from 'mastodon/components/icon';
import ScrollableList from 'mastodon/components/scrollable_list';
import Status from 'mastodon/containers/status_container';
import { SearchSection } from './components/search_section';
const messages = defineMessages({
title: { id: 'search_results.title', defaultMessage: 'Search for {q}' },
});
const mapStateToProps = state => ({
isLoading: state.getIn(['search', 'isLoading']),
results: state.getIn(['search', 'results']),
q: state.getIn(['search', 'searchTerm']),
submittedType: state.getIn(['search', 'type']),
});
const INITIAL_PAGE_LIMIT = 10;
const INITIAL_DISPLAY = 4;
const hidePeek = list => {
if (list.size > INITIAL_PAGE_LIMIT && list.size % INITIAL_PAGE_LIMIT === 1) {
return list.skipLast(1);
} else {
return list;
}
};
const renderAccounts = accounts => hidePeek(accounts).map(id => (
<Account key={id} id={id} />
));
const renderHashtags = hashtags => hidePeek(hashtags).map(hashtag => (
<Hashtag key={hashtag.get('name')} hashtag={hashtag} />
));
const renderStatuses = statuses => hidePeek(statuses).map(id => (
<Status key={id} id={id} contextType='explore' />
));
class Results extends PureComponent {
static propTypes = {
results: ImmutablePropTypes.contains({
accounts: ImmutablePropTypes.orderedSet,
statuses: ImmutablePropTypes.orderedSet,
hashtags: ImmutablePropTypes.orderedSet,
}),
isLoading: PropTypes.bool,
multiColumn: PropTypes.bool,
dispatch: PropTypes.func.isRequired,
q: PropTypes.string,
intl: PropTypes.object,
submittedType: PropTypes.oneOf(['accounts', 'statuses', 'hashtags']),
};
state = {
type: this.props.submittedType || 'all',
};
static getDerivedStateFromProps(props, state) {
if (props.submittedType !== state.type) {
return {
type: props.submittedType || 'all',
};
}
return null;
}
handleSelectAll = () => {
const { submittedType, dispatch } = this.props;
// If we originally searched for a specific type, we need to resubmit
// the query to get all types of results
if (submittedType) {
dispatch(submitSearch());
}
this.setState({ type: 'all' });
};
handleSelectAccounts = () => {
const { submittedType, dispatch } = this.props;
// If we originally searched for something else (but not everything),
// we need to resubmit the query for this specific type
if (submittedType !== 'accounts') {
dispatch(submitSearch('accounts'));
}
this.setState({ type: 'accounts' });
};
handleSelectHashtags = () => {
const { submittedType, dispatch } = this.props;
// If we originally searched for something else (but not everything),
// we need to resubmit the query for this specific type
if (submittedType !== 'hashtags') {
dispatch(submitSearch('hashtags'));
}
this.setState({ type: 'hashtags' });
};
handleSelectStatuses = () => {
const { submittedType, dispatch } = this.props;
// If we originally searched for something else (but not everything),
// we need to resubmit the query for this specific type
if (submittedType !== 'statuses') {
dispatch(submitSearch('statuses'));
}
this.setState({ type: 'statuses' });
};
handleLoadMoreAccounts = () => this._loadMore('accounts');
handleLoadMoreStatuses = () => this._loadMore('statuses');
handleLoadMoreHashtags = () => this._loadMore('hashtags');
_loadMore (type) {
const { dispatch } = this.props;
dispatch(expandSearch(type));
}
handleLoadMore = () => {
const { type } = this.state;
if (type !== 'all') {
this._loadMore(type);
}
};
render () {
const { intl, isLoading, q, results } = this.props;
const { type } = this.state;
// We request 1 more result than we display so we can tell if there'd be a next page
const hasMore = type !== 'all' ? results.get(type, ImmutableList()).size > INITIAL_PAGE_LIMIT && results.get(type).size % INITIAL_PAGE_LIMIT === 1 : false;
let filteredResults;
const accounts = results.get('accounts', ImmutableList());
const hashtags = results.get('hashtags', ImmutableList());
const statuses = results.get('statuses', ImmutableList());
switch(type) {
case 'all':
filteredResults = (accounts.size + hashtags.size + statuses.size) > 0 ? (
<>
{accounts.size > 0 && (
<SearchSection key='accounts' title={<><Icon id='users' icon={PeopleIcon} /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>} onClickMore={this.handleLoadMoreAccounts}>
{accounts.take(INITIAL_DISPLAY).map(id => <Account key={id} id={id} />)}
</SearchSection>
)}
{hashtags.size > 0 && (
<SearchSection key='hashtags' title={<><Icon id='hashtag' icon={TagIcon} /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></>} onClickMore={this.handleLoadMoreHashtags}>
{hashtags.take(INITIAL_DISPLAY).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
</SearchSection>
)}
{statuses.size > 0 && (
<SearchSection key='statuses' title={<><Icon id='quote-right' icon={FindInPageIcon} /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></>} onClickMore={this.handleLoadMoreStatuses}>
{statuses.take(INITIAL_DISPLAY).map(id => <Status key={id} id={id} contextType='explore' />)}
</SearchSection>
)}
</>
) : [];
break;
case 'accounts':
filteredResults = renderAccounts(accounts);
break;
case 'hashtags':
filteredResults = renderHashtags(hashtags);
break;
case 'statuses':
filteredResults = renderStatuses(statuses);
break;
}
return (
<>
<div className='account__section-headline'>
<button onClick={this.handleSelectAll} className={type === 'all' ? 'active' : undefined}><FormattedMessage id='search_results.all' defaultMessage='All' /></button>
<button onClick={this.handleSelectAccounts} className={type === 'accounts' ? 'active' : undefined}><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></button>
<button onClick={this.handleSelectHashtags} className={type === 'hashtags' ? 'active' : undefined}><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></button>
<button onClick={this.handleSelectStatuses} className={type === 'statuses' ? 'active' : undefined}><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></button>
</div>
<div className='explore__search-results' data-nosnippet>
<ScrollableList
scrollKey='search-results'
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
hasMore={hasMore}
emptyMessage={<FormattedMessage id='search_results.nothing_found' defaultMessage='Could not find anything for these search terms' />}
bindToDocument
>
{filteredResults}
</ScrollableList>
</div>
<Helmet>
<title>{intl.formatMessage(messages.title, { q })}</title>
</Helmet>
</>
);
}
}
export default connect(mapStateToProps)(injectIntl(Results));

View file

@ -85,7 +85,7 @@ class ContentWithRouter extends ImmutablePureComponent {
} }
link.setAttribute('target', '_blank'); link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer'); link.setAttribute('rel', 'noopener');
} }
} }

View file

@ -27,7 +27,7 @@ import AntennaIcon from '@/material-icons/400-24px/wifi.svg?react';
import { fetchFollowRequests } from 'mastodon/actions/accounts'; import { fetchFollowRequests } from 'mastodon/actions/accounts';
import Column from 'mastodon/components/column'; import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header'; import ColumnHeader from 'mastodon/components/column_header';
import LinkFooter from 'mastodon/features/ui/components/link_footer'; import { LinkFooter } from 'mastodon/features/ui/components/link_footer';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { canManageReports, canViewAdminDashboard } from 'mastodon/permissions'; import { canManageReports, canViewAdminDashboard } from 'mastodon/permissions';

View file

@ -28,7 +28,7 @@ export const RelationshipsSeveranceEvent = ({ type, target, followingCount, foll
<div className='notification-group__main'> <div className='notification-group__main'>
<p>{intl.formatMessage(messages[type], { from: <strong>{domain}</strong>, target: <strong>{target}</strong>, followingCount, followersCount })}</p> <p>{intl.formatMessage(messages[type], { from: <strong>{domain}</strong>, target: <strong>{target}</strong>, followingCount, followersCount })}</p>
<a href='/severed_relationships' target='_blank' rel='noopener noreferrer' className='link-button'><FormattedMessage id='notification.relationships_severance_event.learn_more' defaultMessage='Learn more' /></a> <a href='/severed_relationships' target='_blank' rel='noopener' className='link-button'><FormattedMessage id='notification.relationships_severance_event.learn_more' defaultMessage='Learn more' /></a>
</div> </div>
</div> </div>
); );

View file

@ -55,7 +55,7 @@ class Report extends ImmutablePureComponent {
</div> </div>
<div className='notification__report__actions'> <div className='notification__report__actions'>
<a href={`/admin/reports/${report.get('id')}`} className='button' target='_blank' rel='noopener noreferrer'>{intl.formatMessage(messages.openReport)}</a> <a href={`/admin/reports/${report.get('id')}`} className='button' target='_blank' rel='noopener'>{intl.formatMessage(messages.openReport)}</a>
</div> </div>
</div> </div>
</div> </div>

View file

@ -70,7 +70,7 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
if (button === 0 && !(ctrlKey || metaKey)) { if (button === 0 && !(ctrlKey || metaKey)) {
history.push(path); history.push(path);
} else if (button === 1 || (button === 0 && (ctrlKey || metaKey))) { } else if (button === 1 || (button === 0 && (ctrlKey || metaKey))) {
window.open(path, '_blank', 'noreferrer noopener'); window.open(path, '_blank', 'noopener');
} }
} }

View file

@ -33,6 +33,34 @@ const labelRenderer: LabelRenderer = (displayedName, total, seeMoreHref) => {
); );
}; };
const privateLabelRenderer: LabelRenderer = (
displayedName,
total,
seeMoreHref,
) => {
if (total === 1)
return (
<FormattedMessage
id='notification.favourite_pm'
defaultMessage='{name} favorited your private mention'
values={{ name: displayedName }}
/>
);
return (
<FormattedMessage
id='notification.favourite_pm.name_and_others_with_link'
defaultMessage='{name} and <a>{count, plural, one {# other} other {# others}}</a> favorited your private mention'
values={{
name: displayedName,
count: total - 1,
a: (chunks) =>
seeMoreHref ? <Link to={seeMoreHref}>{chunks}</Link> : chunks,
}}
/>
);
};
export const NotificationFavourite: React.FC<{ export const NotificationFavourite: React.FC<{
notification: NotificationGroupFavourite; notification: NotificationGroupFavourite;
unread: boolean; unread: boolean;
@ -44,6 +72,10 @@ export const NotificationFavourite: React.FC<{
?.acct, ?.acct,
); );
const isPrivateMention = useAppSelector(
(state) => state.statuses.getIn([statusId, 'visibility']) === 'direct',
);
return ( return (
<NotificationGroupWithStatus <NotificationGroupWithStatus
type='favourite' type='favourite'
@ -53,7 +85,7 @@ export const NotificationFavourite: React.FC<{
statusId={notification.statusId} statusId={notification.statusId}
timestamp={notification.latest_page_notification_at} timestamp={notification.latest_page_notification_at}
count={notification.notifications_count} count={notification.notifications_count}
labelRenderer={labelRenderer} labelRenderer={isPrivateMention ? privateLabelRenderer : labelRenderer}
labelSeeMoreHref={ labelSeeMoreHref={
statusAccount ? `/@${statusAccount}/${statusId}/favourites` : undefined statusAccount ? `/@${statusAccount}/${statusId}/favourites` : undefined
} }

View file

@ -1,65 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { FormattedMessage, FormattedDate, injectIntl, defineMessages } from 'react-intl';
import { Helmet } from 'react-helmet';
import api from 'mastodon/api';
import Column from 'mastodon/components/column';
import { Skeleton } from 'mastodon/components/skeleton';
const messages = defineMessages({
title: { id: 'privacy_policy.title', defaultMessage: 'Privacy Policy' },
});
class PrivacyPolicy extends PureComponent {
static propTypes = {
intl: PropTypes.object,
multiColumn: PropTypes.bool,
};
state = {
content: null,
lastUpdated: null,
isLoading: true,
};
componentDidMount () {
api().get('/api/v1/instance/privacy_policy').then(({ data }) => {
this.setState({ content: data.content, lastUpdated: data.updated_at, isLoading: false });
}).catch(() => {
this.setState({ isLoading: false });
});
}
render () {
const { intl, multiColumn } = this.props;
const { isLoading, content, lastUpdated } = this.state;
return (
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
<div className='scrollable privacy-policy'>
<div className='column-title'>
<h3><FormattedMessage id='privacy_policy.title' defaultMessage='Privacy Policy' /></h3>
<p><FormattedMessage id='privacy_policy.last_updated' defaultMessage='Last updated {date}' values={{ date: isLoading ? <Skeleton width='10ch' /> : <FormattedDate value={lastUpdated} year='numeric' month='short' day='2-digit' /> }} /></p>
</div>
<div
className='privacy-policy__body prose'
dangerouslySetInnerHTML={{ __html: content }}
/>
</div>
<Helmet>
<title>{intl.formatMessage(messages.title)}</title>
<meta name='robots' content='all' />
</Helmet>
</Column>
);
}
}
export default injectIntl(PrivacyPolicy);

View file

@ -0,0 +1,90 @@
import { useState, useEffect } from 'react';
import {
FormattedMessage,
FormattedDate,
useIntl,
defineMessages,
} from 'react-intl';
import { Helmet } from 'react-helmet';
import { apiGetPrivacyPolicy } from 'mastodon/api/instance';
import type { ApiPrivacyPolicyJSON } from 'mastodon/api_types/instance';
import { Column } from 'mastodon/components/column';
import { Skeleton } from 'mastodon/components/skeleton';
const messages = defineMessages({
title: { id: 'privacy_policy.title', defaultMessage: 'Privacy Policy' },
});
const PrivacyPolicy: React.FC<{
multiColumn: boolean;
}> = ({ multiColumn }) => {
const intl = useIntl();
const [response, setResponse] = useState<ApiPrivacyPolicyJSON>();
const [loading, setLoading] = useState(true);
useEffect(() => {
apiGetPrivacyPolicy()
.then((data) => {
setResponse(data);
setLoading(false);
return '';
})
.catch(() => {
setLoading(false);
});
}, []);
return (
<Column
bindToDocument={!multiColumn}
label={intl.formatMessage(messages.title)}
>
<div className='scrollable privacy-policy'>
<div className='column-title'>
<h3>
<FormattedMessage
id='privacy_policy.title'
defaultMessage='Privacy Policy'
/>
</h3>
<p>
<FormattedMessage
id='privacy_policy.last_updated'
defaultMessage='Last updated {date}'
values={{
date: loading ? (
<Skeleton width='10ch' />
) : (
<FormattedDate
value={response?.updated_at}
year='numeric'
month='short'
day='2-digit'
/>
),
}}
/>
</p>
</div>
{response && (
<div
className='privacy-policy__body prose'
dangerouslySetInnerHTML={{ __html: response.content }}
/>
)}
</div>
<Helmet>
<title>{intl.formatMessage(messages.title)}</title>
<meta name='robots' content='all' />
</Helmet>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default PrivacyPolicy;

View file

@ -0,0 +1,23 @@
import { FormattedMessage } from 'react-intl';
export const SearchSection: React.FC<{
title: React.ReactNode;
onClickMore?: () => void;
children: React.ReactNode;
}> = ({ title, onClickMore, children }) => (
<div className='search-results__section'>
<div className='search-results__section__header'>
<h3>{title}</h3>
{onClickMore && (
<button onClick={onClickMore}>
<FormattedMessage
id='search_results.see_all'
defaultMessage='See all'
/>
</button>
)}
</div>
{children}
</div>
);

View file

@ -0,0 +1,304 @@
import { useCallback, useEffect, useRef } from 'react';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import { useSearchParam } from '@/hooks/useSearchParam';
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { submitSearch, expandSearch } from 'mastodon/actions/search';
import type { ApiSearchType } from 'mastodon/api_types/search';
import { Account } from 'mastodon/components/account';
import { Column } from 'mastodon/components/column';
import type { ColumnRef } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { CompatibilityHashtag as Hashtag } from 'mastodon/components/hashtag';
import { Icon } from 'mastodon/components/icon';
import ScrollableList from 'mastodon/components/scrollable_list';
import Status from 'mastodon/containers/status_container';
import { Search } from 'mastodon/features/compose/components/search';
import type { Hashtag as HashtagType } from 'mastodon/models/tags';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { SearchSection } from './components/search_section';
const messages = defineMessages({
title: { id: 'search_results.title', defaultMessage: 'Search for "{q}"' },
});
const INITIAL_PAGE_LIMIT = 10;
const INITIAL_DISPLAY = 4;
const hidePeek = <T,>(list: T[]) => {
if (
list.length > INITIAL_PAGE_LIMIT &&
list.length % INITIAL_PAGE_LIMIT === 1
) {
return list.slice(0, -2);
} else {
return list;
}
};
const renderAccounts = (accountIds: string[]) =>
hidePeek<string>(accountIds).map((id) => <Account key={id} id={id} />);
const renderHashtags = (hashtags: HashtagType[]) =>
hidePeek<HashtagType>(hashtags).map((hashtag) => (
<Hashtag key={hashtag.name} hashtag={hashtag} />
));
const renderStatuses = (statusIds: string[]) =>
hidePeek<string>(statusIds).map((id) => (
// @ts-expect-error inferred props are wrong
<Status key={id} id={id} contextType='explore' />
));
type SearchType = 'all' | ApiSearchType;
const typeFromParam = (param?: string): SearchType => {
if (param && ['all', 'accounts', 'statuses', 'hashtags'].includes(param)) {
return param as SearchType;
} else {
return 'all';
}
};
export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
multiColumn,
}) => {
const columnRef = useRef<ColumnRef>(null);
const intl = useIntl();
const [q] = useSearchParam('q');
const [type, setType] = useSearchParam('type');
const isLoading = useAppSelector((state) => state.search.loading);
const results = useAppSelector((state) => state.search.results);
const dispatch = useAppDispatch();
const mappedType = typeFromParam(type);
const trimmedValue = q?.trim() ?? '';
useEffect(() => {
if (trimmedValue.length > 0) {
void dispatch(
submitSearch({
q: trimmedValue,
type: mappedType === 'all' ? undefined : mappedType,
}),
);
}
}, [dispatch, trimmedValue, mappedType]);
const handleHeaderClick = useCallback(() => {
columnRef.current?.scrollTop();
}, []);
const handleSelectAll = useCallback(() => {
setType(null);
}, [setType]);
const handleSelectAccounts = useCallback(() => {
setType('accounts');
}, [setType]);
const handleSelectHashtags = useCallback(() => {
setType('hashtags');
}, [setType]);
const handleSelectStatuses = useCallback(() => {
setType('statuses');
}, [setType]);
const handleLoadMore = useCallback(() => {
if (mappedType !== 'all') {
void dispatch(expandSearch({ type: mappedType }));
}
}, [dispatch, mappedType]);
// We request 1 more result than we display so we can tell if there'd be a next page
const hasMore =
mappedType !== 'all' && results
? results[mappedType].length > INITIAL_PAGE_LIMIT &&
results[mappedType].length % INITIAL_PAGE_LIMIT === 1
: false;
let filteredResults;
if (results) {
switch (mappedType) {
case 'all':
filteredResults =
results.accounts.length +
results.hashtags.length +
results.statuses.length >
0 ? (
<>
{results.accounts.length > 0 && (
<SearchSection
key='accounts'
title={
<>
<Icon id='users' icon={PeopleIcon} />
<FormattedMessage
id='search_results.accounts'
defaultMessage='Profiles'
/>
</>
}
onClickMore={handleSelectAccounts}
>
{results.accounts.slice(0, INITIAL_DISPLAY).map((id) => (
<Account key={id} id={id} />
))}
</SearchSection>
)}
{results.hashtags.length > 0 && (
<SearchSection
key='hashtags'
title={
<>
<Icon id='hashtag' icon={TagIcon} />
<FormattedMessage
id='search_results.hashtags'
defaultMessage='Hashtags'
/>
</>
}
onClickMore={handleSelectHashtags}
>
{results.hashtags.slice(0, INITIAL_DISPLAY).map((hashtag) => (
<Hashtag key={hashtag.name} hashtag={hashtag} />
))}
</SearchSection>
)}
{results.statuses.length > 0 && (
<SearchSection
key='statuses'
title={
<>
<Icon id='quote-right' icon={FindInPageIcon} />
<FormattedMessage
id='search_results.statuses'
defaultMessage='Posts'
/>
</>
}
onClickMore={handleSelectStatuses}
>
{results.statuses.slice(0, INITIAL_DISPLAY).map((id) => (
// @ts-expect-error inferred props are wrong
<Status key={id} id={id} contextType='explore' />
))}
</SearchSection>
)}
</>
) : (
[]
);
break;
case 'accounts':
filteredResults = renderAccounts(results.accounts);
break;
case 'hashtags':
filteredResults = renderHashtags(results.hashtags);
break;
case 'statuses':
filteredResults = renderStatuses(results.statuses);
break;
}
}
return (
<Column
bindToDocument={!multiColumn}
ref={columnRef}
label={intl.formatMessage(messages.title, { q })}
>
<ColumnHeader
icon={'search'}
iconComponent={SearchIcon}
title={intl.formatMessage(messages.title, { q })}
onClick={handleHeaderClick}
multiColumn={multiColumn}
/>
<div className='explore__search-header'>
<Search singleColumn initialValue={trimmedValue} />
</div>
<div className='account__section-headline'>
<button
onClick={handleSelectAll}
className={mappedType === 'all' ? 'active' : undefined}
>
<FormattedMessage id='search_results.all' defaultMessage='All' />
</button>
<button
onClick={handleSelectAccounts}
className={mappedType === 'accounts' ? 'active' : undefined}
>
<FormattedMessage
id='search_results.accounts'
defaultMessage='Profiles'
/>
</button>
<button
onClick={handleSelectHashtags}
className={mappedType === 'hashtags' ? 'active' : undefined}
>
<FormattedMessage
id='search_results.hashtags'
defaultMessage='Hashtags'
/>
</button>
<button
onClick={handleSelectStatuses}
className={mappedType === 'statuses' ? 'active' : undefined}
>
<FormattedMessage
id='search_results.statuses'
defaultMessage='Posts'
/>
</button>
</div>
<div className='explore__search-results' data-nosnippet>
<ScrollableList
scrollKey='search-results'
isLoading={isLoading}
showLoading={isLoading && !results}
onLoadMore={handleLoadMore}
hasMore={hasMore}
emptyMessage={
trimmedValue.length > 0 ? (
<FormattedMessage
id='search_results.no_results'
defaultMessage='No results.'
/>
) : (
<FormattedMessage
id='search_results.no_search_yet'
defaultMessage='Try searching for posts, profiles or hashtags.'
/>
)
}
bindToDocument
>
{filteredResults}
</ScrollableList>
</div>
<Helmet>
<title>{intl.formatMessage(messages.title, { q })}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default SearchResults;

View file

@ -61,7 +61,7 @@ const Embed: React.FC<{ id: string }> = ({ id }) => {
className='embed__overlay' className='embed__overlay'
href={permalink} href={permalink}
target='_blank' target='_blank'
rel='noreferrer noopener' rel='noopener'
aria-label='' aria-label=''
/> />
</div> </div>

View file

@ -208,7 +208,7 @@ export default class Card extends PureComponent {
<div className='status-card__actions' onClick={this.handleEmbedClick} role='none'> <div className='status-card__actions' onClick={this.handleEmbedClick} role='none'>
<div> <div>
<button type='button' onClick={this.handleEmbedClick}><Icon id='play' icon={PlayArrowIcon} /></button> <button type='button' onClick={this.handleEmbedClick}><Icon id='play' icon={PlayArrowIcon} /></button>
<a href={card.get('url')} onClick={this.handleExternalLinkClick} target='_blank' rel='noopener noreferrer'><Icon id='external-link' icon={OpenInNewIcon} /></a> <a href={card.get('url')} onClick={this.handleExternalLinkClick} target='_blank' rel='noopener'><Icon id='external-link' icon={OpenInNewIcon} /></a>
</div> </div>
</div> </div>
) : spoilerButton} ) : spoilerButton}
@ -219,7 +219,7 @@ export default class Card extends PureComponent {
return ( return (
<div className={classNames('status-card', { expanded: largeImage })} ref={this.setRef} onClick={revealed ? null : this.handleReveal} role={revealed ? 'button' : null}> <div className={classNames('status-card', { expanded: largeImage })} ref={this.setRef} onClick={revealed ? null : this.handleReveal} role={revealed ? 'button' : null}>
{embed} {embed}
<a href={card.get('url')} target='_blank' rel='noopener noreferrer'>{description}</a> <a href={card.get('url')} target='_blank' rel='noopener'>{description}</a>
</div> </div>
); );
} else if (card.get('image')) { } else if (card.get('image')) {
@ -239,7 +239,7 @@ export default class Card extends PureComponent {
return ( return (
<> <>
<a href={card.get('url')} className={classNames('status-card', { expanded: largeImage, bottomless: showAuthor })} target='_blank' rel='noopener noreferrer' ref={this.setRef}> <a href={card.get('url')} className={classNames('status-card', { expanded: largeImage, bottomless: showAuthor })} target='_blank' rel='noopener' ref={this.setRef}>
{embed} {embed}
{description} {description}
</a> </a>

View file

@ -0,0 +1,95 @@
import { useState, useEffect } from 'react';
import {
FormattedMessage,
FormattedDate,
useIntl,
defineMessages,
} from 'react-intl';
import { Helmet } from 'react-helmet';
import { apiGetTermsOfService } from 'mastodon/api/instance';
import type { ApiTermsOfServiceJSON } from 'mastodon/api_types/instance';
import { Column } from 'mastodon/components/column';
import { Skeleton } from 'mastodon/components/skeleton';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
const messages = defineMessages({
title: { id: 'terms_of_service.title', defaultMessage: 'Terms of Service' },
});
const TermsOfService: React.FC<{
multiColumn: boolean;
}> = ({ multiColumn }) => {
const intl = useIntl();
const [response, setResponse] = useState<ApiTermsOfServiceJSON>();
const [loading, setLoading] = useState(true);
useEffect(() => {
apiGetTermsOfService()
.then((data) => {
setResponse(data);
setLoading(false);
return '';
})
.catch(() => {
setLoading(false);
});
}, []);
if (!loading && !response) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
}
return (
<Column
bindToDocument={!multiColumn}
label={intl.formatMessage(messages.title)}
>
<div className='scrollable privacy-policy'>
<div className='column-title'>
<h3>
<FormattedMessage
id='terms_of_service.title'
defaultMessage='Terms of Service'
/>
</h3>
<p>
<FormattedMessage
id='privacy_policy.last_updated'
defaultMessage='Last updated {date}'
values={{
date: loading ? (
<Skeleton width='10ch' />
) : (
<FormattedDate
value={response?.updated_at}
year='numeric'
month='short'
day='2-digit'
/>
),
}}
/>
</p>
</div>
{response && (
<div
className='privacy-policy__body prose'
dangerouslySetInnerHTML={{ __html: response.content }}
/>
)}
</div>
<Helmet>
<title>{intl.formatMessage(messages.title)}</title>
<meta name='robots' content='all' />
</Helmet>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default TermsOfService;

View file

@ -24,7 +24,7 @@ export default class ActionsModal extends ImmutablePureComponent {
return ( return (
<li key={`${text}-${i}`}> <li key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener noreferrer' onClick={this.props.onClick} data-index={i} className={classNames({ active })}> <a href={href} target='_blank' rel='noopener' onClick={this.props.onClick} data-index={i} className={classNames({ active })}>
{icon && <IconButton title={text} icon={icon} iconComponent={iconComponent} role='presentation' tabIndex={-1} inverted />} {icon && <IconButton title={text} icon={icon} iconComponent={iconComponent} role='presentation' tabIndex={-1} inverted />}
<div> <div>
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div> <div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>

View file

@ -5,12 +5,11 @@ import { connect } from 'react-redux';
import { changeComposing, mountCompose, unmountCompose } from 'mastodon/actions/compose'; import { changeComposing, mountCompose, unmountCompose } from 'mastodon/actions/compose';
import ServerBanner from 'mastodon/components/server_banner'; import ServerBanner from 'mastodon/components/server_banner';
import { Search } from 'mastodon/features/compose/components/search';
import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container'; import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container';
import SearchContainer from 'mastodon/features/compose/containers/search_container'; import { LinkFooter } from 'mastodon/features/ui/components/link_footer';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import LinkFooter from './link_footer';
class ComposePanel extends PureComponent { class ComposePanel extends PureComponent {
static propTypes = { static propTypes = {
identity: identityContextPropShape, identity: identityContextPropShape,
@ -42,7 +41,7 @@ class ComposePanel extends PureComponent {
return ( return (
<div className='compose-panel' onFocus={this.onFocus}> <div className='compose-panel' onFocus={this.onFocus}>
<SearchContainer openInRoute /> <Search openInRoute />
{!signedIn && ( {!signedIn && (
<> <>

View file

@ -1,95 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { FormattedMessage, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { openModal } from 'mastodon/actions/modal';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { domain, version, source_url, statusPageUrl, profile_directory as profileDirectory } from 'mastodon/initial_state';
import { PERMISSION_INVITE_USERS } from 'mastodon/permissions';
const mapDispatchToProps = (dispatch) => ({
onLogout () {
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
},
});
class LinkFooter extends PureComponent {
static propTypes = {
identity: identityContextPropShape,
multiColumn: PropTypes.bool,
onLogout: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleLogoutClick = e => {
e.preventDefault();
e.stopPropagation();
this.props.onLogout();
return false;
};
render () {
const { signedIn, permissions } = this.props.identity;
const { multiColumn } = this.props;
const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS);
const canProfileDirectory = profileDirectory;
const DividingCircle = <span aria-hidden>{' · '}</span>;
return (
<div className='link-footer'>
<p>
<strong>{domain}</strong>:
{' '}
<Link to='/about' target={multiColumn ? '_blank' : undefined}><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
{statusPageUrl && (
<>
{DividingCircle}
<a href={statusPageUrl} target='_blank' rel='noopener'><FormattedMessage id='footer.status' defaultMessage='Status' /></a>
</>
)}
{canInvite && (
<>
{DividingCircle}
<a href='/invites' target='_blank'><FormattedMessage id='footer.invite' defaultMessage='Invite people' /></a>
</>
)}
{canProfileDirectory && (
<>
{DividingCircle}
<Link to='/directory'><FormattedMessage id='footer.directory' defaultMessage='Profiles directory' /></Link>
</>
)}
{DividingCircle}
<Link to='/privacy-policy' target={multiColumn ? '_blank' : undefined}><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
</p>
<p>
<strong>Mastodon</strong>:
{' '}
<a href='https://joinmastodon.org' target='_blank'><FormattedMessage id='footer.about' defaultMessage='About' /></a>
{DividingCircle}
<a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='footer.get_app' defaultMessage='Get the app' /></a>
{DividingCircle}
<Link to='/keyboard-shortcuts'><FormattedMessage id='footer.keyboard_shortcuts' defaultMessage='Keyboard shortcuts' /></Link>
{DividingCircle}
<a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a>
{DividingCircle}
<span className='version'>v{version}</span>
</p>
</div>
);
}
}
export default injectIntl(withIdentity(connect(null, mapDispatchToProps)(LinkFooter)));

View file

@ -0,0 +1,101 @@
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import {
domain,
version,
source_url,
statusPageUrl,
profile_directory as canProfileDirectory,
termsOfServiceEnabled,
} from 'mastodon/initial_state';
const DividingCircle: React.FC = () => <span aria-hidden>{' · '}</span>;
export const LinkFooter: React.FC<{
multiColumn: boolean;
}> = ({ multiColumn }) => {
return (
<div className='link-footer'>
<p>
<strong>{domain}</strong>:{' '}
<Link to='/about' target={multiColumn ? '_blank' : undefined}>
<FormattedMessage id='footer.about' defaultMessage='About' />
</Link>
{statusPageUrl && (
<>
<DividingCircle />
<a href={statusPageUrl} target='_blank' rel='noopener'>
<FormattedMessage id='footer.status' defaultMessage='Status' />
</a>
</>
)}
{canProfileDirectory && (
<>
<DividingCircle />
<Link to='/directory'>
<FormattedMessage
id='footer.directory'
defaultMessage='Profiles directory'
/>
</Link>
</>
)}
<DividingCircle />
<Link
to='/privacy-policy'
target={multiColumn ? '_blank' : undefined}
rel='privacy-policy'
>
<FormattedMessage
id='footer.privacy_policy'
defaultMessage='Privacy policy'
/>
</Link>
{termsOfServiceEnabled && (
<>
<DividingCircle />
<Link
to='/terms-of-service'
target={multiColumn ? '_blank' : undefined}
rel='terms-of-service'
>
<FormattedMessage
id='footer.terms_of_service'
defaultMessage='Terms of service'
/>
</Link>
</>
)}
</p>
<p>
<strong>Mastodon</strong>:{' '}
<a href='https://joinmastodon.org' target='_blank' rel='noopener'>
<FormattedMessage id='footer.about' defaultMessage='About' />
</a>
<DividingCircle />
<a href='https://joinmastodon.org/apps' target='_blank' rel='noopener'>
<FormattedMessage id='footer.get_app' defaultMessage='Get the app' />
</a>
<DividingCircle />
<Link to='/keyboard-shortcuts'>
<FormattedMessage
id='footer.keyboard_shortcuts'
defaultMessage='Keyboard shortcuts'
/>
</Link>
<DividingCircle />
<a href={source_url} rel='noopener' target='_blank'>
<FormattedMessage
id='footer.source_code'
defaultMessage='View source code'
/>
</a>
<DividingCircle />
<span className='version'>v{version}</span>
</p>
</div>
);
};

View file

@ -80,6 +80,7 @@ import {
OnboardingProfile, OnboardingProfile,
OnboardingFollows, OnboardingFollows,
Explore, Explore,
Search,
About, About,
PrivacyPolicy, PrivacyPolicy,
CommunityTimeline, CommunityTimeline,
@ -89,6 +90,7 @@ import {
CircleMembers, CircleMembers,
BookmarkCategoryEdit, BookmarkCategoryEdit,
ReactionDeck, ReactionDeck,
TermsOfService,
} from './util/async-components'; } from './util/async-components';
import { ColumnsContextProvider } from './util/columns_context'; import { ColumnsContextProvider } from './util/columns_context';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
@ -217,6 +219,7 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} /> <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
<WrappedRoute path='/about' component={About} content={children} /> <WrappedRoute path='/about' component={About} content={children} />
<WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} /> <WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
<WrappedRoute path='/terms-of-service' component={TermsOfService} content={children} />
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} /> <WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
<Redirect from='/timelines/public' to='/public' exact /> <Redirect from='/timelines/public' to='/public' exact />
@ -259,7 +262,8 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path={['/start', '/start/profile']} exact component={OnboardingProfile} content={children} /> <WrappedRoute path={['/start', '/start/profile']} exact component={OnboardingProfile} content={children} />
<WrappedRoute path='/start/follows' component={OnboardingFollows} content={children} /> <WrappedRoute path='/start/follows' component={OnboardingFollows} content={children} />
<WrappedRoute path='/directory' component={Directory} content={children} /> <WrappedRoute path='/directory' component={Directory} content={children} />
<WrappedRoute path={['/explore', '/search']} component={Explore} content={children} /> <WrappedRoute path='/explore' component={Explore} content={children} />
<WrappedRoute path='/search' component={Search} content={children} />
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} /> <WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} /> <WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />

View file

@ -230,6 +230,10 @@ export function Explore () {
return import(/* webpackChunkName: "features/explore" */'../../explore'); return import(/* webpackChunkName: "features/explore" */'../../explore');
} }
export function Search () {
return import(/* webpackChunkName: "features/explore" */'../../search');
}
export function FilterModal () { export function FilterModal () {
return import(/*webpackChunkName: "modals/filter_modal" */'../components/filter_modal'); return import(/*webpackChunkName: "modals/filter_modal" */'../components/filter_modal');
} }
@ -254,6 +258,10 @@ export function PrivacyPolicy () {
return import(/*webpackChunkName: "features/privacy_policy" */'../../privacy_policy'); return import(/*webpackChunkName: "features/privacy_policy" */'../../privacy_policy');
} }
export function TermsOfService () {
return import(/*webpackChunkName: "features/terms_of_service" */'../../terms_of_service');
}
export function NotificationRequests () { export function NotificationRequests () {
return import(/*webpackChunkName: "features/notifications/requests" */'../../notifications/requests'); return import(/*webpackChunkName: "features/notifications/requests" */'../../notifications/requests');
} }

View file

@ -69,6 +69,8 @@
* @property {boolean=} use_pending_items * @property {boolean=} use_pending_items
* @property {string} version * @property {string} version
* @property {string} sso_redirect * @property {string} sso_redirect
* @property {string} status_page_url
* @property {boolean} terms_of_service_enabled
*/ */
/** /**
@ -165,10 +167,9 @@ export const usePendingItems = getMeta('use_pending_items');
export const version = getMeta('version'); export const version = getMeta('version');
export const languages = initialState?.languages; export const languages = initialState?.languages;
export const criticalUpdatesPending = initialState?.critical_updates_pending; export const criticalUpdatesPending = initialState?.critical_updates_pending;
// @ts-expect-error
export const statusPageUrl = getMeta('status_page_url'); export const statusPageUrl = getMeta('status_page_url');
export const sso_redirect = getMeta('sso_redirect'); export const sso_redirect = getMeta('sso_redirect');
export const termsOfServiceEnabled = getMeta('terms_of_service_enabled');
/** /**
* @returns {string | undefined} * @returns {string | undefined}
*/ */

View file

@ -152,7 +152,6 @@
"empty_column.home": "Your home timeline is empty! Follow more people to fill it up. {suggestions}", "empty_column.home": "Your home timeline is empty! Follow more people to fill it up. {suggestions}",
"empty_column.list": "Hierdie lys is nog leeg. Nuwe plasings deur lyslede sal voortaan hier verskyn.", "empty_column.list": "Hierdie lys is nog leeg. Nuwe plasings deur lyslede sal voortaan hier verskyn.",
"empty_column.notifications": "Jy het nog geen kennisgewings nie. Interaksie van ander mense met jou, sal hier vertoon.", "empty_column.notifications": "Jy het nog geen kennisgewings nie. Interaksie van ander mense met jou, sal hier vertoon.",
"explore.search_results": "Soekresultate",
"explore.suggested_follows": "Mense", "explore.suggested_follows": "Mense",
"explore.trending_links": "Nuus", "explore.trending_links": "Nuus",
"filter_modal.added.settings_link": "instellings bladsy", "filter_modal.added.settings_link": "instellings bladsy",
@ -162,7 +161,6 @@
"footer.about": "Oor", "footer.about": "Oor",
"footer.directory": "Profielgids", "footer.directory": "Profielgids",
"footer.get_app": "Kry die app", "footer.get_app": "Kry die app",
"footer.invite": "Nooi ander",
"footer.keyboard_shortcuts": "Kortpadsleutels", "footer.keyboard_shortcuts": "Kortpadsleutels",
"footer.privacy_policy": "Privaatheidsbeleid", "footer.privacy_policy": "Privaatheidsbeleid",
"footer.source_code": "Wys bronkode", "footer.source_code": "Wys bronkode",
@ -259,9 +257,7 @@
"search.search_or_paste": "Soek of plak URL", "search.search_or_paste": "Soek of plak URL",
"search_results.all": "Alles", "search_results.all": "Alles",
"search_results.hashtags": "Hutsetiket", "search_results.hashtags": "Hutsetiket",
"search_results.nothing_found": "Hierdie soekwoorde lewer niks op nie",
"search_results.statuses": "Plasings", "search_results.statuses": "Plasings",
"search_results.title": "Soek {q}",
"server_banner.administered_by": "Administrasie deur:", "server_banner.administered_by": "Administrasie deur:",
"sign_in_banner.sign_in": "Sign in", "sign_in_banner.sign_in": "Sign in",
"status.admin_status": "Open hierdie plasing as moderator", "status.admin_status": "Open hierdie plasing as moderator",

View file

@ -192,7 +192,6 @@
"error.unexpected_crash.next_steps_addons": "Intenta deshabilitar-los y recarga la pachina. Si ixo no aduya, podrías usar Mastodon a traviés d'un navegador web diferent u aplicación nativa.", "error.unexpected_crash.next_steps_addons": "Intenta deshabilitar-los y recarga la pachina. Si ixo no aduya, podrías usar Mastodon a traviés d'un navegador web diferent u aplicación nativa.",
"errors.unexpected_crash.copy_stacktrace": "Copiar lo seguimiento de pila en o portafuellas", "errors.unexpected_crash.copy_stacktrace": "Copiar lo seguimiento de pila en o portafuellas",
"errors.unexpected_crash.report_issue": "Informar d'un problema/error", "errors.unexpected_crash.report_issue": "Informar d'un problema/error",
"explore.search_results": "Resultaus de busqueda",
"explore.title": "Explorar", "explore.title": "Explorar",
"explore.trending_links": "Noticias", "explore.trending_links": "Noticias",
"explore.trending_statuses": "Publicacions", "explore.trending_statuses": "Publicacions",
@ -219,7 +218,6 @@
"footer.about": "Sobre", "footer.about": "Sobre",
"footer.directory": "Directorio de perfils", "footer.directory": "Directorio de perfils",
"footer.get_app": "Obtener l'aplicación", "footer.get_app": "Obtener l'aplicación",
"footer.invite": "Convidar chent",
"footer.keyboard_shortcuts": "Alcorces de teclau", "footer.keyboard_shortcuts": "Alcorces de teclau",
"footer.privacy_policy": "Politica de privacidat", "footer.privacy_policy": "Politica de privacidat",
"footer.source_code": "Veyer codigo fuent", "footer.source_code": "Veyer codigo fuent",
@ -432,9 +430,7 @@
"search_popout.full_text_search_logged_out_message": "Nomás disponible iniciando la sesión.", "search_popout.full_text_search_logged_out_message": "Nomás disponible iniciando la sesión.",
"search_results.all": "Totz", "search_results.all": "Totz",
"search_results.hashtags": "Etiquetas", "search_results.hashtags": "Etiquetas",
"search_results.nothing_found": "No se podió trobar cosa pa estes termins de busqueda",
"search_results.statuses": "Publicacions", "search_results.statuses": "Publicacions",
"search_results.title": "Buscar {q}",
"server_banner.about_active_users": "Usuarios activos en o servidor entre los zaguers 30 días (Usuarios Activos Mensuals)", "server_banner.about_active_users": "Usuarios activos en o servidor entre los zaguers 30 días (Usuarios Activos Mensuals)",
"server_banner.active_users": "usuarios activos", "server_banner.active_users": "usuarios activos",
"server_banner.administered_by": "Administrau per:", "server_banner.administered_by": "Administrau per:",

View file

@ -274,7 +274,6 @@
"error.unexpected_crash.next_steps_addons": "حاول تعطيلهم وإنعاش الصفحة. إن لم ينجح ذلك، يمكنك دائمًا استخدام ماستدون عبر متصفح آخر أو تطبيق أصلي.", "error.unexpected_crash.next_steps_addons": "حاول تعطيلهم وإنعاش الصفحة. إن لم ينجح ذلك، يمكنك دائمًا استخدام ماستدون عبر متصفح آخر أو تطبيق أصلي.",
"errors.unexpected_crash.copy_stacktrace": "انسخ تتبع الارتباطات إلى الحافظة", "errors.unexpected_crash.copy_stacktrace": "انسخ تتبع الارتباطات إلى الحافظة",
"errors.unexpected_crash.report_issue": "الإبلاغ عن خلل", "errors.unexpected_crash.report_issue": "الإبلاغ عن خلل",
"explore.search_results": "نتائج البحث",
"explore.suggested_follows": "أشخاص", "explore.suggested_follows": "أشخاص",
"explore.title": "استكشف", "explore.title": "استكشف",
"explore.trending_links": "المُستجدّات", "explore.trending_links": "المُستجدّات",
@ -322,7 +321,6 @@
"footer.about": "عن", "footer.about": "عن",
"footer.directory": "دليل الصفحات التعريفية", "footer.directory": "دليل الصفحات التعريفية",
"footer.get_app": "احصل على التطبيق", "footer.get_app": "احصل على التطبيق",
"footer.invite": "دعوة أشخاص",
"footer.keyboard_shortcuts": "اختصارات لوحة المفاتيح", "footer.keyboard_shortcuts": "اختصارات لوحة المفاتيح",
"footer.privacy_policy": "سياسة الخصوصية", "footer.privacy_policy": "سياسة الخصوصية",
"footer.source_code": "الاطلاع على الشفرة المصدرية", "footer.source_code": "الاطلاع على الشفرة المصدرية",
@ -681,10 +679,8 @@
"search_results.accounts": "الصفحات التعريفية", "search_results.accounts": "الصفحات التعريفية",
"search_results.all": "الكل", "search_results.all": "الكل",
"search_results.hashtags": "الوُسوم", "search_results.hashtags": "الوُسوم",
"search_results.nothing_found": "تعذر العثور على نتائج تتضمن هذه المصطلحات",
"search_results.see_all": "رؤية الكل", "search_results.see_all": "رؤية الكل",
"search_results.statuses": "المنشورات", "search_results.statuses": "المنشورات",
"search_results.title": "البحث عن {q}",
"server_banner.about_active_users": "الأشخاص الذين يستخدمون هذا الخادم خلال الأيام الثلاثين الأخيرة (المستخدمون النشطون شهريًا)", "server_banner.about_active_users": "الأشخاص الذين يستخدمون هذا الخادم خلال الأيام الثلاثين الأخيرة (المستخدمون النشطون شهريًا)",
"server_banner.active_users": "مستخدم نشط", "server_banner.active_users": "مستخدم نشط",
"server_banner.administered_by": "يُديره:", "server_banner.administered_by": "يُديره:",

View file

@ -159,7 +159,6 @@
"error.unexpected_crash.explanation_addons": "Esta páxina nun se pudo amosar correutamente. Ye probable que dalgún complementu del restolador o dalguna ferramienta de traducción automática produxere esti error.", "error.unexpected_crash.explanation_addons": "Esta páxina nun se pudo amosar correutamente. Ye probable que dalgún complementu del restolador o dalguna ferramienta de traducción automática produxere esti error.",
"error.unexpected_crash.next_steps": "Prueba a anovar la páxina. Si nun sirve, ye posible que tovía seyas a usar Mastodon pente otru restolador o una aplicación nativa.", "error.unexpected_crash.next_steps": "Prueba a anovar la páxina. Si nun sirve, ye posible que tovía seyas a usar Mastodon pente otru restolador o una aplicación nativa.",
"error.unexpected_crash.next_steps_addons": "Prueba a desactivalos y a anovar la páxina. Si nun sirve, ye posible que tovía seyas a usar Mastodon pente otru restolador o una aplicación nativa.", "error.unexpected_crash.next_steps_addons": "Prueba a desactivalos y a anovar la páxina. Si nun sirve, ye posible que tovía seyas a usar Mastodon pente otru restolador o una aplicación nativa.",
"explore.search_results": "Resultaos de la busca",
"explore.suggested_follows": "Perfiles", "explore.suggested_follows": "Perfiles",
"explore.title": "Esploración", "explore.title": "Esploración",
"explore.trending_links": "Noticies", "explore.trending_links": "Noticies",
@ -196,7 +195,6 @@
"footer.about": "Tocante a", "footer.about": "Tocante a",
"footer.directory": "Direutoriu de perfiles", "footer.directory": "Direutoriu de perfiles",
"footer.get_app": "Consiguir l'aplicación", "footer.get_app": "Consiguir l'aplicación",
"footer.invite": "Convidar a persones",
"footer.keyboard_shortcuts": "Atayos del tecláu", "footer.keyboard_shortcuts": "Atayos del tecláu",
"footer.privacy_policy": "Política de privacidá", "footer.privacy_policy": "Política de privacidá",
"footer.source_code": "Ver el códigu fonte", "footer.source_code": "Ver el códigu fonte",
@ -389,10 +387,8 @@
"search_results.accounts": "Perfiles", "search_results.accounts": "Perfiles",
"search_results.all": "Too", "search_results.all": "Too",
"search_results.hashtags": "Etiquetes", "search_results.hashtags": "Etiquetes",
"search_results.nothing_found": "Nun se pudo atopar nada con esos términos de busca",
"search_results.see_all": "Ver too", "search_results.see_all": "Ver too",
"search_results.statuses": "Artículos", "search_results.statuses": "Artículos",
"search_results.title": "Busca de: {q}",
"server_banner.server_stats": "Estadístiques del sirvidor:", "server_banner.server_stats": "Estadístiques del sirvidor:",
"sign_in_banner.create_account": "Crear una cuenta", "sign_in_banner.create_account": "Crear una cuenta",
"sign_in_banner.sso_redirect": "Aniciar la sesión o rexistrase", "sign_in_banner.sso_redirect": "Aniciar la sesión o rexistrase",

View file

@ -0,0 +1 @@
{}

View file

@ -278,7 +278,6 @@
"error.unexpected_crash.next_steps_addons": "Паспрабуйце выключыць іх і аднавіць старонку. Калі гэта не дапаможа, вы можаце карыстацца Мастадонт праз другі браўзер ці аплікацыю.", "error.unexpected_crash.next_steps_addons": "Паспрабуйце выключыць іх і аднавіць старонку. Калі гэта не дапаможа, вы можаце карыстацца Мастадонт праз другі браўзер ці аплікацыю.",
"errors.unexpected_crash.copy_stacktrace": "Дадаць дыягнастычны стэк у буфер абмену", "errors.unexpected_crash.copy_stacktrace": "Дадаць дыягнастычны стэк у буфер абмену",
"errors.unexpected_crash.report_issue": "Паведаміць аб праблеме", "errors.unexpected_crash.report_issue": "Паведаміць аб праблеме",
"explore.search_results": "Вынікі пошуку",
"explore.suggested_follows": "Людзі", "explore.suggested_follows": "Людзі",
"explore.title": "Агляд", "explore.title": "Агляд",
"explore.trending_links": "Навіны", "explore.trending_links": "Навіны",
@ -328,7 +327,6 @@
"footer.about": "Пра нас", "footer.about": "Пра нас",
"footer.directory": "Дырэкторыя профіляў", "footer.directory": "Дырэкторыя профіляў",
"footer.get_app": "Спампаваць праграму", "footer.get_app": "Спампаваць праграму",
"footer.invite": "Запрасіць людзей",
"footer.keyboard_shortcuts": "Спалучэнні клавіш", "footer.keyboard_shortcuts": "Спалучэнні клавіш",
"footer.privacy_policy": "Палітыка прыватнасці", "footer.privacy_policy": "Палітыка прыватнасці",
"footer.source_code": "Прагледзець зыходны код", "footer.source_code": "Прагледзець зыходны код",
@ -686,10 +684,8 @@
"search_results.accounts": "Профілі", "search_results.accounts": "Профілі",
"search_results.all": "Усё", "search_results.all": "Усё",
"search_results.hashtags": "Хэштэгі", "search_results.hashtags": "Хэштэгі",
"search_results.nothing_found": "Па дадзенаму запыту нічога не знойдзена",
"search_results.see_all": "Праглядзець усе", "search_results.see_all": "Праглядзець усе",
"search_results.statuses": "Допісы", "search_results.statuses": "Допісы",
"search_results.title": "Пошук {q}",
"server_banner.about_active_users": "Людзі, якія карыстаюцца гэтым сервера на працягу апошніх 30 дзён (Штомесячна Актыўныя Карыстальнікі)", "server_banner.about_active_users": "Людзі, якія карыстаюцца гэтым сервера на працягу апошніх 30 дзён (Штомесячна Актыўныя Карыстальнікі)",
"server_banner.active_users": "актыўныя карыстальнікі", "server_banner.active_users": "актыўныя карыстальнікі",
"server_banner.administered_by": "Адміністратар:", "server_banner.administered_by": "Адміністратар:",

View file

@ -108,7 +108,7 @@
"annual_report.summary.thanks": "Благодарим, че сте част от Mastodon!", "annual_report.summary.thanks": "Благодарим, че сте част от Mastodon!",
"attachments_list.unprocessed": "(необработено)", "attachments_list.unprocessed": "(необработено)",
"audio.hide": "Скриване на звука", "audio.hide": "Скриване на звука",
"block_modal.remote_users_caveat": "Ще поискаме сървърът {domain} да почита решението ви. Съгласието обаче не се гарантира откак някои сървъри могат да боравят с блоковете по различен начин. Обществените публикации още може да се виждат от невлезли в системата потребители.", "block_modal.remote_users_caveat": "Ще приканим сървъра {domain} да уважава решението ви. За съжаление не можем да гарантираме това защото някои сървъри могат да третират блокиранията по различен начин. Публичните постове може да продължат да бъдат видими за потребители, които не са се регистрирали.",
"block_modal.show_less": "Повече на показ", "block_modal.show_less": "Повече на показ",
"block_modal.show_more": "По-малко на показ", "block_modal.show_more": "По-малко на показ",
"block_modal.they_cant_mention": "Те не могат да ви споменават или последват.", "block_modal.they_cant_mention": "Те не могат да ви споменават или последват.",
@ -255,14 +255,14 @@
"domain_pill.activitypub_lets_connect": "Позволява ви да се свързвате и взаимодействате с хора не само в Mastodon, но и през различни социални приложения.", "domain_pill.activitypub_lets_connect": "Позволява ви да се свързвате и взаимодействате с хора не само в Mastodon, но и през различни социални приложения.",
"domain_pill.activitypub_like_language": "ActivityPub е като език на Mastodon, говорещ с други социални мрежи.", "domain_pill.activitypub_like_language": "ActivityPub е като език на Mastodon, говорещ с други социални мрежи.",
"domain_pill.server": "Сървър", "domain_pill.server": "Сървър",
"domain_pill.their_handle": "Тяхната ръчка:", "domain_pill.their_handle": "Техният адрес:",
"domain_pill.their_server": "Цифровият им дом, където живеят всичките им публикации.", "domain_pill.their_server": "Цифровият им дом, където живеят всичките им публикации.",
"domain_pill.their_username": "Неповторимият им идентификатор на сървъра им. Възможно е да се намерят потребители със същото потребителско име на други сървъри.", "domain_pill.their_username": "Неповторимият им идентификатор на сървъра им. Възможно е да се намерят потребители със същото потребителско име на други сървъри.",
"domain_pill.username": "Потребителско име", "domain_pill.username": "Потребителско име",
"domain_pill.whats_in_a_handle": "Какво е в ръчката?", "domain_pill.whats_in_a_handle": "Как се съставя адресът?",
"domain_pill.who_they_are": "Откак ръчките казват кой кой е и къде е, то може да взаимодействате с хора през социаното уебпространство на <button>захранваните платформи от ActivityPub</button>.", "domain_pill.who_they_are": "Адресът показва за някой кой е той и къде се намира. Това ви позволява да общувате с всички в социалната мрежа от <button>платформите поддържащи ActivityPub</button>.",
"domain_pill.who_you_are": "Тъй като вашата ръчка казва кои сте и къде сте, то може да взаимодействате с хора през социаното уебпространство на <button>захранваните платформи от ActivityPub</button>.", "domain_pill.who_you_are": "Адресът ви показва кой сте и къде се намирате. Това ви позволява да общувате с всички в социалната мрежа от <button>платформите поддържащи ActivityPub</button>.",
"domain_pill.your_handle": "Вашата ръчка:", "domain_pill.your_handle": "Вашият адрес:",
"domain_pill.your_server": "Цифровият ви дом, където живеят всичките ви публикации. Не харесвате ли този? Прехвърляте се на сървъри по всяко време и докарвате последователите си също.", "domain_pill.your_server": "Цифровият ви дом, където живеят всичките ви публикации. Не харесвате ли този? Прехвърляте се на сървъри по всяко време и докарвате последователите си също.",
"domain_pill.your_username": "Неповторимият ви идентификатор на този сървър. Възможно е да се намерят потребители със същото потребителско име на други сървъри.", "domain_pill.your_username": "Неповторимият ви идентификатор на този сървър. Възможно е да се намерят потребители със същото потребителско име на други сървъри.",
"embed.instructions": "Вградете публикацията в уебсайта си, копирайки кода долу.", "embed.instructions": "Вградете публикацията в уебсайта си, копирайки кода долу.",
@ -309,7 +309,6 @@
"error.unexpected_crash.next_steps_addons": "Опитайте се да ги изключите и да опресните страницата. Ако това не помогне, то още може да използвате Mastodon чрез различен браузър или приложение.", "error.unexpected_crash.next_steps_addons": "Опитайте се да ги изключите и да опресните страницата. Ако това не помогне, то още може да използвате Mastodon чрез различен браузър или приложение.",
"errors.unexpected_crash.copy_stacktrace": "Копиране на трасето на стека в буферната памет", "errors.unexpected_crash.copy_stacktrace": "Копиране на трасето на стека в буферната памет",
"errors.unexpected_crash.report_issue": "Сигнал за проблем", "errors.unexpected_crash.report_issue": "Сигнал за проблем",
"explore.search_results": "Резултати от търсенето",
"explore.suggested_follows": "Хора", "explore.suggested_follows": "Хора",
"explore.title": "Разглеждане", "explore.title": "Разглеждане",
"explore.trending_links": "Новини", "explore.trending_links": "Новини",
@ -359,11 +358,11 @@
"footer.about": "Относно", "footer.about": "Относно",
"footer.directory": "Директория на профилите", "footer.directory": "Директория на профилите",
"footer.get_app": "Вземане на приложението", "footer.get_app": "Вземане на приложението",
"footer.invite": "Поканване на хора",
"footer.keyboard_shortcuts": "Клавишни комбинации", "footer.keyboard_shortcuts": "Клавишни комбинации",
"footer.privacy_policy": "Политика за поверителност", "footer.privacy_policy": "Политика за поверителност",
"footer.source_code": "Преглед на изходния код", "footer.source_code": "Преглед на изходния код",
"footer.status": "Състояние", "footer.status": "Състояние",
"footer.terms_of_service": "Условия на услугата",
"generic.saved": "Запазено", "generic.saved": "Запазено",
"getting_started.heading": "Първи стъпки", "getting_started.heading": "Първи стъпки",
"hashtag.admin_moderation": "Отваряне на модериращия интерфейс за #{name}", "hashtag.admin_moderation": "Отваряне на модериращия интерфейс за #{name}",
@ -550,6 +549,8 @@
"notification.annual_report.view": "Преглед на #Wrapstodon", "notification.annual_report.view": "Преглед на #Wrapstodon",
"notification.favourite": "{name} направи любима публикацията ви", "notification.favourite": "{name} направи любима публикацията ви",
"notification.favourite.name_and_others_with_link": "{name} и <a>{count, plural, one {# друг} other {# други}}</a> направиха любима ваша публикация", "notification.favourite.name_and_others_with_link": "{name} и <a>{count, plural, one {# друг} other {# други}}</a> направиха любима ваша публикация",
"notification.favourite_pm": "{name} хареса вашето лично споменаване",
"notification.favourite_pm.name_and_others_with_link": "{name} и <a>{count, plural, one {# друг} other {# други}}</a> харесаха вашето частно споменаване",
"notification.follow": "{name} ви последва", "notification.follow": "{name} ви последва",
"notification.follow.name_and_others": "{name} и <a>{count, plural, one {# друг} other {# други}}</a> ви последваха", "notification.follow.name_and_others": "{name} и <a>{count, plural, one {# друг} other {# други}}</a> ви последваха",
"notification.follow_request": "{name} поиска да ви последва", "notification.follow_request": "{name} поиска да ви последва",
@ -781,10 +782,11 @@
"search_results.accounts": "Профили", "search_results.accounts": "Профили",
"search_results.all": "Всичко", "search_results.all": "Всичко",
"search_results.hashtags": "Хаштагове", "search_results.hashtags": "Хаштагове",
"search_results.nothing_found": "Не може да се намери каквото и да било за тези термини при търсене", "search_results.no_results": "Няма намерени резултати.",
"search_results.no_search_yet": "Опитайте да потърсите постове, профили или хаштагове.",
"search_results.see_all": "Поглед на всички", "search_results.see_all": "Поглед на всички",
"search_results.statuses": "Публикации", "search_results.statuses": "Публикации",
"search_results.title": "Търсене за {q}", "search_results.title": "Търсене на \"{q}\"",
"server_banner.about_active_users": "Ползващите сървъра през последните 30 дни (дейните месечно потребители)", "server_banner.about_active_users": "Ползващите сървъра през последните 30 дни (дейните месечно потребители)",
"server_banner.active_users": "дейни потребители", "server_banner.active_users": "дейни потребители",
"server_banner.administered_by": "Администрира се от:", "server_banner.administered_by": "Администрира се от:",
@ -857,6 +859,7 @@
"subscribed_languages.target": "Промяна на абонираните езици за {target}", "subscribed_languages.target": "Промяна на абонираните езици за {target}",
"tabs_bar.home": "Начало", "tabs_bar.home": "Начало",
"tabs_bar.notifications": "Известия", "tabs_bar.notifications": "Известия",
"terms_of_service.title": "Условия на услугата",
"time_remaining.days": "{number, plural, one {остава # ден} other {остават # дни}}", "time_remaining.days": "{number, plural, one {остава # ден} other {остават # дни}}",
"time_remaining.hours": "{number, plural, one {остава # час} other {остават # часа}}", "time_remaining.hours": "{number, plural, one {остава # час} other {остават # часа}}",
"time_remaining.minutes": "{number, plural, one {остава # минута} other {остават # минути}}", "time_remaining.minutes": "{number, plural, one {остава # минута} other {остават # минути}}",

View file

@ -230,7 +230,6 @@
"error.unexpected_crash.next_steps_addons": "Klaskit azbevaat ar bajenn. Ma n'ez a ket en-dro e c'hallit klask ober gant Mastodon dre ur merdeer disheñvel pe dre an arload genidik.", "error.unexpected_crash.next_steps_addons": "Klaskit azbevaat ar bajenn. Ma n'ez a ket en-dro e c'hallit klask ober gant Mastodon dre ur merdeer disheñvel pe dre an arload genidik.",
"errors.unexpected_crash.copy_stacktrace": "Eilañ ar roudoù diveugañ er golver", "errors.unexpected_crash.copy_stacktrace": "Eilañ ar roudoù diveugañ er golver",
"errors.unexpected_crash.report_issue": "Danevellañ ur fazi", "errors.unexpected_crash.report_issue": "Danevellañ ur fazi",
"explore.search_results": "Disoc'hoù an enklask",
"explore.suggested_follows": "Tud", "explore.suggested_follows": "Tud",
"explore.title": "Furchal", "explore.title": "Furchal",
"explore.trending_links": "Keleier", "explore.trending_links": "Keleier",
@ -263,7 +262,6 @@
"footer.about": "Diwar-benn", "footer.about": "Diwar-benn",
"footer.directory": "Kavlec'h ar profiloù", "footer.directory": "Kavlec'h ar profiloù",
"footer.get_app": "Pellgargañ an arload", "footer.get_app": "Pellgargañ an arload",
"footer.invite": "Pediñ tud",
"footer.keyboard_shortcuts": "Berradennoù klavier", "footer.keyboard_shortcuts": "Berradennoù klavier",
"footer.privacy_policy": "Reolennoù prevezded", "footer.privacy_policy": "Reolennoù prevezded",
"footer.source_code": "Gwelet ar c'hod mammenn", "footer.source_code": "Gwelet ar c'hod mammenn",
@ -524,10 +522,8 @@
"search_results.accounts": "Profiloù", "search_results.accounts": "Profiloù",
"search_results.all": "Pep tra", "search_results.all": "Pep tra",
"search_results.hashtags": "Hashtagoù", "search_results.hashtags": "Hashtagoù",
"search_results.nothing_found": "Disoc'h ebet gant ar gerioù-se",
"search_results.see_all": "Gwelet pep tra", "search_results.see_all": "Gwelet pep tra",
"search_results.statuses": "Toudoù", "search_results.statuses": "Toudoù",
"search_results.title": "Klask {q}",
"server_banner.active_users": "implijerien·ezed oberiant", "server_banner.active_users": "implijerien·ezed oberiant",
"server_banner.administered_by": "Meret gant :", "server_banner.administered_by": "Meret gant :",
"server_banner.server_stats": "Stadegoù ar servijer :", "server_banner.server_stats": "Stadegoù ar servijer :",

View file

@ -309,7 +309,6 @@
"error.unexpected_crash.next_steps_addons": "Prova de desactivar-los i actualitza la pàgina. Si això no serveix, és possible que encara puguis fer servir Mastodon amb un altre navegador o una aplicació nativa.", "error.unexpected_crash.next_steps_addons": "Prova de desactivar-los i actualitza la pàgina. Si això no serveix, és possible que encara puguis fer servir Mastodon amb un altre navegador o una aplicació nativa.",
"errors.unexpected_crash.copy_stacktrace": "Copia stacktrace al porta-retalls", "errors.unexpected_crash.copy_stacktrace": "Copia stacktrace al porta-retalls",
"errors.unexpected_crash.report_issue": "Informa d'un problema", "errors.unexpected_crash.report_issue": "Informa d'un problema",
"explore.search_results": "Resultats de la cerca",
"explore.suggested_follows": "Persones", "explore.suggested_follows": "Persones",
"explore.title": "Explora", "explore.title": "Explora",
"explore.trending_links": "Notícies", "explore.trending_links": "Notícies",
@ -359,11 +358,11 @@
"footer.about": "Quant a", "footer.about": "Quant a",
"footer.directory": "Directori de perfils", "footer.directory": "Directori de perfils",
"footer.get_app": "Aconsegueix l'app", "footer.get_app": "Aconsegueix l'app",
"footer.invite": "Convida persones",
"footer.keyboard_shortcuts": "Dreceres de teclat", "footer.keyboard_shortcuts": "Dreceres de teclat",
"footer.privacy_policy": "Política de privadesa", "footer.privacy_policy": "Política de privadesa",
"footer.source_code": "Mostra el codi font", "footer.source_code": "Mostra el codi font",
"footer.status": "Estat", "footer.status": "Estat",
"footer.terms_of_service": "Condicions de servei",
"generic.saved": "Desat", "generic.saved": "Desat",
"getting_started.heading": "Primeres passes", "getting_started.heading": "Primeres passes",
"hashtag.admin_moderation": "Obre la interfície de moderació per a #{name}", "hashtag.admin_moderation": "Obre la interfície de moderació per a #{name}",
@ -550,6 +549,8 @@
"notification.annual_report.view": "Visualitzeu #Wrapstodon", "notification.annual_report.view": "Visualitzeu #Wrapstodon",
"notification.favourite": "{name} ha afavorit el teu tut", "notification.favourite": "{name} ha afavorit el teu tut",
"notification.favourite.name_and_others_with_link": "{name} i <a>{count, plural, one {# altre} other {# altres}}</a> han afavorit la vostra publicació", "notification.favourite.name_and_others_with_link": "{name} i <a>{count, plural, one {# altre} other {# altres}}</a> han afavorit la vostra publicació",
"notification.favourite_pm": "{name} ha afavorit la vostra menció privada",
"notification.favourite_pm.name_and_others_with_link": "{name} i <a>{count, plural, one {un altre} other {# altres}}</a> han afavorit la vostra menció",
"notification.follow": "{name} et segueix", "notification.follow": "{name} et segueix",
"notification.follow.name_and_others": "{name} i <a>{count, plural, one {# altre} other {# altres}}</a> us han seguit", "notification.follow.name_and_others": "{name} i <a>{count, plural, one {# altre} other {# altres}}</a> us han seguit",
"notification.follow_request": "{name} ha sol·licitat de seguir-te", "notification.follow_request": "{name} ha sol·licitat de seguir-te",
@ -781,10 +782,11 @@
"search_results.accounts": "Perfils", "search_results.accounts": "Perfils",
"search_results.all": "Tots", "search_results.all": "Tots",
"search_results.hashtags": "Etiquetes", "search_results.hashtags": "Etiquetes",
"search_results.nothing_found": "No s'ha pogut trobar res per a aquests termes de cerca", "search_results.no_results": "Cap resultat.",
"search_results.no_search_yet": "Proveu de cercar publicacions, perfils o etiquetes.",
"search_results.see_all": "Veure'ls tots", "search_results.see_all": "Veure'ls tots",
"search_results.statuses": "Tuts", "search_results.statuses": "Tuts",
"search_results.title": "Cerca de {q}", "search_results.title": "Cerca de {q}",
"server_banner.about_active_users": "Gent que ha fet servir aquest servidor en els darrers 30 dies (Usuaris Actius Mensuals)", "server_banner.about_active_users": "Gent que ha fet servir aquest servidor en els darrers 30 dies (Usuaris Actius Mensuals)",
"server_banner.active_users": "usuaris actius", "server_banner.active_users": "usuaris actius",
"server_banner.administered_by": "Administrat per:", "server_banner.administered_by": "Administrat per:",
@ -857,6 +859,7 @@
"subscribed_languages.target": "Canvia les llengües subscrites per a {target}", "subscribed_languages.target": "Canvia les llengües subscrites per a {target}",
"tabs_bar.home": "Inici", "tabs_bar.home": "Inici",
"tabs_bar.notifications": "Notificacions", "tabs_bar.notifications": "Notificacions",
"terms_of_service.title": "Condicions de servei",
"time_remaining.days": "{number, plural, one {# dia restant} other {# dies restants}}", "time_remaining.days": "{number, plural, one {# dia restant} other {# dies restants}}",
"time_remaining.hours": "{number, plural, one {# hora restant} other {# hores restants}}", "time_remaining.hours": "{number, plural, one {# hora restant} other {# hores restants}}",
"time_remaining.minutes": "{number, plural, one {# minut restant} other {# minuts restants}}", "time_remaining.minutes": "{number, plural, one {# minut restant} other {# minuts restants}}",

View file

@ -226,7 +226,6 @@
"error.unexpected_crash.next_steps_addons": "هەوڵدە لەکاریان بخەیت و لاپەڕەکە تازە بکەوە. ئەگەر ئەمە یارمەتیدەر نەبوو، لەوانەیە هێشتا بتوانیت ماستۆدۆن بەکاربێنیت لە ڕێگەی وێبگەڕەکانی دیکە یان نەرمەکالاکانی ئەسڵی.", "error.unexpected_crash.next_steps_addons": "هەوڵدە لەکاریان بخەیت و لاپەڕەکە تازە بکەوە. ئەگەر ئەمە یارمەتیدەر نەبوو، لەوانەیە هێشتا بتوانیت ماستۆدۆن بەکاربێنیت لە ڕێگەی وێبگەڕەکانی دیکە یان نەرمەکالاکانی ئەسڵی.",
"errors.unexpected_crash.copy_stacktrace": "کۆپیکردنی ستێکتراسی بۆ کلیپ بۆرد", "errors.unexpected_crash.copy_stacktrace": "کۆپیکردنی ستێکتراسی بۆ کلیپ بۆرد",
"errors.unexpected_crash.report_issue": "کێشەی گوزارشت", "errors.unexpected_crash.report_issue": "کێشەی گوزارشت",
"explore.search_results": "ئەنجامەکانی گەڕان",
"explore.suggested_follows": "خەڵک", "explore.suggested_follows": "خەڵک",
"explore.title": "گەڕان", "explore.title": "گەڕان",
"explore.trending_links": "هەواڵەکان", "explore.trending_links": "هەواڵەکان",
@ -262,7 +261,6 @@
"footer.about": "دەربارە", "footer.about": "دەربارە",
"footer.directory": "ڕابەری پەڕەی ناساندن", "footer.directory": "ڕابەری پەڕەی ناساندن",
"footer.get_app": "بەرنامەکە بەدەست بێنە", "footer.get_app": "بەرنامەکە بەدەست بێنە",
"footer.invite": "بانگهێشتکردنی خەڵک",
"footer.keyboard_shortcuts": "کورتەڕێکانی تەختەکلیک", "footer.keyboard_shortcuts": "کورتەڕێکانی تەختەکلیک",
"footer.privacy_policy": "سیاسەتی تایبەتمەندێتی", "footer.privacy_policy": "سیاسەتی تایبەتمەندێتی",
"footer.source_code": "پیشاندانی کۆدی سەرچاوە", "footer.source_code": "پیشاندانی کۆدی سەرچاوە",
@ -487,9 +485,7 @@
"search_results.accounts": "پرۆفایلەکان", "search_results.accounts": "پرۆفایلەکان",
"search_results.all": "هەموو", "search_results.all": "هەموو",
"search_results.hashtags": "هەشتاگ", "search_results.hashtags": "هەشتاگ",
"search_results.nothing_found": "هیچ بۆ ئەم زاراوە گەڕانانە نەدۆزراوەتەوە",
"search_results.statuses": "توتەکان", "search_results.statuses": "توتەکان",
"search_results.title": "گەڕان بەدوای {q}",
"server_banner.about_active_users": "ئەو کەسانەی لە ماوەی ٣٠ ڕۆژی ڕابردوودا ئەم سێرڤەرە بەکاردەهێنن (بەکارهێنەرانی چالاک مانگانە)", "server_banner.about_active_users": "ئەو کەسانەی لە ماوەی ٣٠ ڕۆژی ڕابردوودا ئەم سێرڤەرە بەکاردەهێنن (بەکارهێنەرانی چالاک مانگانە)",
"server_banner.active_users": "بەکارهێنەرانی چالاک", "server_banner.active_users": "بەکارهێنەرانی چالاک",
"server_banner.administered_by": "بەڕێوەبردن لەلایەن:", "server_banner.administered_by": "بەڕێوەبردن لەلایەن:",

View file

@ -272,7 +272,6 @@
"error.unexpected_crash.next_steps_addons": "Zkuste je vypnout a stránku obnovit. Pokud to nepomůže, zkuste otevřít Mastodon v jiném prohlížeči nebo nativní aplikaci.", "error.unexpected_crash.next_steps_addons": "Zkuste je vypnout a stránku obnovit. Pokud to nepomůže, zkuste otevřít Mastodon v jiném prohlížeči nebo nativní aplikaci.",
"errors.unexpected_crash.copy_stacktrace": "Zkopírovat stacktrace do schránky", "errors.unexpected_crash.copy_stacktrace": "Zkopírovat stacktrace do schránky",
"errors.unexpected_crash.report_issue": "Nahlásit problém", "errors.unexpected_crash.report_issue": "Nahlásit problém",
"explore.search_results": "Výsledky hledání",
"explore.suggested_follows": "Lidé", "explore.suggested_follows": "Lidé",
"explore.title": "Objevit", "explore.title": "Objevit",
"explore.trending_links": "Zprávy", "explore.trending_links": "Zprávy",
@ -320,7 +319,6 @@
"footer.about": "O aplikaci", "footer.about": "O aplikaci",
"footer.directory": "Adresář profilů", "footer.directory": "Adresář profilů",
"footer.get_app": "Získat aplikaci", "footer.get_app": "Získat aplikaci",
"footer.invite": "Pozvat lidi",
"footer.keyboard_shortcuts": "Klávesové zkratky", "footer.keyboard_shortcuts": "Klávesové zkratky",
"footer.privacy_policy": "Zásady ochrany osobních údajů", "footer.privacy_policy": "Zásady ochrany osobních údajů",
"footer.source_code": "Zobrazit zdrojový kód", "footer.source_code": "Zobrazit zdrojový kód",
@ -652,10 +650,8 @@
"search_results.accounts": "Profily", "search_results.accounts": "Profily",
"search_results.all": "Vše", "search_results.all": "Vše",
"search_results.hashtags": "Hashtagy", "search_results.hashtags": "Hashtagy",
"search_results.nothing_found": "Pro tyto hledané výrazy nebylo nic nenalezeno",
"search_results.see_all": "Zobrazit vše", "search_results.see_all": "Zobrazit vše",
"search_results.statuses": "Příspěvky", "search_results.statuses": "Příspěvky",
"search_results.title": "Hledat {q}",
"server_banner.about_active_users": "Lidé používající tento server během posledních 30 dní (měsíční aktivní uživatelé)", "server_banner.about_active_users": "Lidé používající tento server během posledních 30 dní (měsíční aktivní uživatelé)",
"server_banner.active_users": "aktivní uživatelé", "server_banner.active_users": "aktivní uživatelé",
"server_banner.administered_by": "Spravováno:", "server_banner.administered_by": "Spravováno:",

View file

@ -103,6 +103,7 @@
"annual_report.summary.most_used_hashtag.most_used_hashtag": "hashnod a ddefnyddiwyd fwyaf", "annual_report.summary.most_used_hashtag.most_used_hashtag": "hashnod a ddefnyddiwyd fwyaf",
"annual_report.summary.most_used_hashtag.none": "Dim", "annual_report.summary.most_used_hashtag.none": "Dim",
"annual_report.summary.new_posts.new_posts": "postiadau newydd", "annual_report.summary.new_posts.new_posts": "postiadau newydd",
"annual_report.summary.percentile.text": "<topLabel>Mae hynny'n eich rhoi chi ar y brig</topLabel><percentage></percentage><bottomLabel> o ddefnyddiwr {domain}.</bottomLabel>",
"annual_report.summary.percentile.we_wont_tell_bernie": "Ni fyddwn yn dweud wrth Bernie.", "annual_report.summary.percentile.we_wont_tell_bernie": "Ni fyddwn yn dweud wrth Bernie.",
"annual_report.summary.thanks": "Diolch am fod yn rhan o Mastodon!", "annual_report.summary.thanks": "Diolch am fod yn rhan o Mastodon!",
"attachments_list.unprocessed": "(heb eu prosesu)", "attachments_list.unprocessed": "(heb eu prosesu)",
@ -128,6 +129,7 @@
"bundle_column_error.routing.body": "Nid oedd modd canfod y dudalen honno. Ydych chi'n siŵr fod yr URL yn y bar cyfeiriad yn gywir?", "bundle_column_error.routing.body": "Nid oedd modd canfod y dudalen honno. Ydych chi'n siŵr fod yr URL yn y bar cyfeiriad yn gywir?",
"bundle_column_error.routing.title": "404", "bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Cau", "bundle_modal_error.close": "Cau",
"bundle_modal_error.message": "Aeth rhywbeth o'i le wrth lwytho'r sgrin hon.",
"bundle_modal_error.retry": "Ceisiwch eto", "bundle_modal_error.retry": "Ceisiwch eto",
"closed_registrations.other_server_instructions": "Gan fod Mastodon yn ddatganoledig, gallwch greu cyfrif ar weinydd arall a dal i ryngweithio gyda hwn.", "closed_registrations.other_server_instructions": "Gan fod Mastodon yn ddatganoledig, gallwch greu cyfrif ar weinydd arall a dal i ryngweithio gyda hwn.",
"closed_registrations_modal.description": "Ar hyn o bryd nid yw'n bosib creu cyfrif ar {domain}, ond cadwch mewn cof nad oes raid i chi gael cyfrif yn benodol ar {domain} i ddefnyddio Mastodon.", "closed_registrations_modal.description": "Ar hyn o bryd nid yw'n bosib creu cyfrif ar {domain}, ond cadwch mewn cof nad oes raid i chi gael cyfrif yn benodol ar {domain} i ddefnyddio Mastodon.",
@ -203,6 +205,9 @@
"confirmations.edit.confirm": "Golygu", "confirmations.edit.confirm": "Golygu",
"confirmations.edit.message": "Bydd golygu nawr yn trosysgrifennu'r neges rydych yn ei ysgrifennu ar hyn o bryd. Ydych chi'n siŵr eich bod eisiau gwneud hyn?", "confirmations.edit.message": "Bydd golygu nawr yn trosysgrifennu'r neges rydych yn ei ysgrifennu ar hyn o bryd. Ydych chi'n siŵr eich bod eisiau gwneud hyn?",
"confirmations.edit.title": "Trosysgrifo'r postiad?", "confirmations.edit.title": "Trosysgrifo'r postiad?",
"confirmations.follow_to_list.confirm": "Dilyn ac ychwanegu at y rhestr",
"confirmations.follow_to_list.message": "Mae angen i chi fod yn dilyn {name} i'w ychwanegu at restr.",
"confirmations.follow_to_list.title": "Dilyn defnyddiwr?",
"confirmations.logout.confirm": "Allgofnodi", "confirmations.logout.confirm": "Allgofnodi",
"confirmations.logout.message": "Ydych chi'n siŵr eich bod am allgofnodi?", "confirmations.logout.message": "Ydych chi'n siŵr eich bod am allgofnodi?",
"confirmations.logout.title": "Allgofnodi?", "confirmations.logout.title": "Allgofnodi?",
@ -234,6 +239,10 @@
"disabled_account_banner.text": "Mae eich cyfrif {disabledAccount} wedi ei analluogi ar hyn o bryd.", "disabled_account_banner.text": "Mae eich cyfrif {disabledAccount} wedi ei analluogi ar hyn o bryd.",
"dismissable_banner.community_timeline": "Dyma'r postiadau cyhoeddus diweddaraf gan bobl sydd â chyfrifon ar {domain}.", "dismissable_banner.community_timeline": "Dyma'r postiadau cyhoeddus diweddaraf gan bobl sydd â chyfrifon ar {domain}.",
"dismissable_banner.dismiss": "Cau", "dismissable_banner.dismiss": "Cau",
"dismissable_banner.explore_links": "Y straeon newyddion hyn yw'r rhai sy'n cael eu rhannu fwyaf ar y ffederasiwn heddiw. Mae straeon newyddion mwy diweddar sy'n cael eu postio gan fwy o amrywiaeth o bobl yn cael eu graddio'n uwch.",
"dismissable_banner.explore_statuses": "Mae'r postiadau hyn o bob rhan o'r ffedysawd yn cael mwy o sylw heddiw. Mae postiadau mwy diweddar sydd â mwy o hybu a ffefrynnu'n cael eu graddio'n uwch.",
"dismissable_banner.explore_tags": "Mae'r hashnodau hyn ar gynnydd y ffedysawd heddiw. Mae hashnodau sy'n cael eu defnyddio gan fwy o bobl amrywiol yn cael eu graddio'n uwch.",
"dismissable_banner.public_timeline": "Dyma'r postiadau cyhoeddus diweddaraf gan bobl ar y ffedysawd y mae pobl ar {domain} yn eu dilyn.",
"domain_block_modal.block": "Blocio gweinydd", "domain_block_modal.block": "Blocio gweinydd",
"domain_block_modal.block_account_instead": "Blocio @{name} yn ei le", "domain_block_modal.block_account_instead": "Blocio @{name} yn ei le",
"domain_block_modal.they_can_interact_with_old_posts": "Gall pobl o'r gweinydd hwn ryngweithio â'ch hen bostiadau.", "domain_block_modal.they_can_interact_with_old_posts": "Gall pobl o'r gweinydd hwn ryngweithio â'ch hen bostiadau.",
@ -300,7 +309,6 @@
"error.unexpected_crash.next_steps_addons": "Ceisiwch eu hanalluogi ac adnewyddu'r dudalen. Os nad yw hynny'n helpu, efallai y byddwch yn dal i allu defnyddio Mastodon trwy borwr neu ap cynhenid arall.", "error.unexpected_crash.next_steps_addons": "Ceisiwch eu hanalluogi ac adnewyddu'r dudalen. Os nad yw hynny'n helpu, efallai y byddwch yn dal i allu defnyddio Mastodon trwy borwr neu ap cynhenid arall.",
"errors.unexpected_crash.copy_stacktrace": "Copïo'r olrhain stac i'r clipfwrdd", "errors.unexpected_crash.copy_stacktrace": "Copïo'r olrhain stac i'r clipfwrdd",
"errors.unexpected_crash.report_issue": "Rhoi gwybod am broblem", "errors.unexpected_crash.report_issue": "Rhoi gwybod am broblem",
"explore.search_results": "Canlyniadau chwilio",
"explore.suggested_follows": "Pobl", "explore.suggested_follows": "Pobl",
"explore.title": "Darganfod", "explore.title": "Darganfod",
"explore.trending_links": "Newyddion", "explore.trending_links": "Newyddion",
@ -350,13 +358,14 @@
"footer.about": "Ynghylch", "footer.about": "Ynghylch",
"footer.directory": "Cyfeiriadur proffiliau", "footer.directory": "Cyfeiriadur proffiliau",
"footer.get_app": "Lawrlwytho'r ap", "footer.get_app": "Lawrlwytho'r ap",
"footer.invite": "Gwahodd pobl",
"footer.keyboard_shortcuts": "Bysellau brys", "footer.keyboard_shortcuts": "Bysellau brys",
"footer.privacy_policy": "Polisi preifatrwydd", "footer.privacy_policy": "Polisi preifatrwydd",
"footer.source_code": "Gweld y cod ffynhonnell", "footer.source_code": "Gweld y cod ffynhonnell",
"footer.status": "Statws", "footer.status": "Statws",
"footer.terms_of_service": "Telerau gwasanaeth",
"generic.saved": "Wedi'i Gadw", "generic.saved": "Wedi'i Gadw",
"getting_started.heading": "Dechrau", "getting_started.heading": "Dechrau",
"hashtag.admin_moderation": "Agor rhyngwyneb cymedroli #{name}",
"hashtag.column_header.tag_mode.all": "a {additional}", "hashtag.column_header.tag_mode.all": "a {additional}",
"hashtag.column_header.tag_mode.any": "neu {additional}", "hashtag.column_header.tag_mode.any": "neu {additional}",
"hashtag.column_header.tag_mode.none": "heb {additional}", "hashtag.column_header.tag_mode.none": "heb {additional}",
@ -486,6 +495,7 @@
"lists.replies_policy.list": "Aelodau'r rhestr", "lists.replies_policy.list": "Aelodau'r rhestr",
"lists.replies_policy.none": "Neb", "lists.replies_policy.none": "Neb",
"lists.save": "Cadw", "lists.save": "Cadw",
"lists.search": "Chwilio",
"lists.show_replies_to": "Cynhwyswch atebion gan aelodau'r rhestr i", "lists.show_replies_to": "Cynhwyswch atebion gan aelodau'r rhestr i",
"load_pending": "{count, plural, one {# eitem newydd} other {# eitem newydd}}", "load_pending": "{count, plural, one {# eitem newydd} other {# eitem newydd}}",
"loading_indicator.label": "Yn llwytho…", "loading_indicator.label": "Yn llwytho…",
@ -539,6 +549,7 @@
"notification.annual_report.view": "Gweld #Wrapstodon", "notification.annual_report.view": "Gweld #Wrapstodon",
"notification.favourite": "Ffafriodd {name} eich postiad", "notification.favourite": "Ffafriodd {name} eich postiad",
"notification.favourite.name_and_others_with_link": "Ffafriodd {name} a <a>{count, plural, one {# arall} other {# arall}}</a> eich postiad", "notification.favourite.name_and_others_with_link": "Ffafriodd {name} a <a>{count, plural, one {# arall} other {# arall}}</a> eich postiad",
"notification.favourite_pm": "Mae {name} wedi ffefrynnu eich cyfeiriad preifat",
"notification.follow": "Dilynodd {name} chi", "notification.follow": "Dilynodd {name} chi",
"notification.follow.name_and_others": "Mae {name} a <a>{count, plural, zero {}one {# arall} two {# arall} few {# arall} many {# others} other {# arall}}</a> nawr yn eich dilyn chi", "notification.follow.name_and_others": "Mae {name} a <a>{count, plural, zero {}one {# arall} two {# arall} few {# arall} many {# others} other {# arall}}</a> nawr yn eich dilyn chi",
"notification.follow_request": "Mae {name} wedi gwneud cais i'ch dilyn", "notification.follow_request": "Mae {name} wedi gwneud cais i'ch dilyn",
@ -770,10 +781,8 @@
"search_results.accounts": "Proffilau", "search_results.accounts": "Proffilau",
"search_results.all": "Popeth", "search_results.all": "Popeth",
"search_results.hashtags": "Hashnodau", "search_results.hashtags": "Hashnodau",
"search_results.nothing_found": "Methu dod o hyd i unrhyw beth ar gyfer y termau chwilio hyn",
"search_results.see_all": "Gweld y cyfan", "search_results.see_all": "Gweld y cyfan",
"search_results.statuses": "Postiadau", "search_results.statuses": "Postiadau",
"search_results.title": "Chwilio am {q}",
"server_banner.about_active_users": "Pobl sy'n defnyddio'r gweinydd hwn yn ystod y 30 diwrnod diwethaf (Defnyddwyr Gweithredol Misol)", "server_banner.about_active_users": "Pobl sy'n defnyddio'r gweinydd hwn yn ystod y 30 diwrnod diwethaf (Defnyddwyr Gweithredol Misol)",
"server_banner.active_users": "defnyddwyr gweithredol", "server_banner.active_users": "defnyddwyr gweithredol",
"server_banner.administered_by": "Gweinyddir gan:", "server_banner.administered_by": "Gweinyddir gan:",
@ -846,6 +855,7 @@
"subscribed_languages.target": "Newid ieithoedd tanysgrifio {target}", "subscribed_languages.target": "Newid ieithoedd tanysgrifio {target}",
"tabs_bar.home": "Cartref", "tabs_bar.home": "Cartref",
"tabs_bar.notifications": "Hysbysiadau", "tabs_bar.notifications": "Hysbysiadau",
"terms_of_service.title": "Telerau Gwasanaeth",
"time_remaining.days": "{number, plural, one {# diwrnod} other {# diwrnod}} ar ôl", "time_remaining.days": "{number, plural, one {# diwrnod} other {# diwrnod}} ar ôl",
"time_remaining.hours": "{number, plural, one {# awr} other {# awr}} ar ôl", "time_remaining.hours": "{number, plural, one {# awr} other {# awr}} ar ôl",
"time_remaining.minutes": "{number, plural, one {# munud} other {# munud}} ar ôl", "time_remaining.minutes": "{number, plural, one {# munud} other {# munud}} ar ôl",

View file

@ -309,7 +309,6 @@
"error.unexpected_crash.next_steps_addons": "Prøv at deaktivere dem og genindlæse siden. Hvis det ikke hjælper, kan Mastodon muligvis stadig bruges via en anden browser eller app.", "error.unexpected_crash.next_steps_addons": "Prøv at deaktivere dem og genindlæse siden. Hvis det ikke hjælper, kan Mastodon muligvis stadig bruges via en anden browser eller app.",
"errors.unexpected_crash.copy_stacktrace": "Kopiér stacktrace til udklipsholderen", "errors.unexpected_crash.copy_stacktrace": "Kopiér stacktrace til udklipsholderen",
"errors.unexpected_crash.report_issue": "Anmeld problem", "errors.unexpected_crash.report_issue": "Anmeld problem",
"explore.search_results": "Søgeresultater",
"explore.suggested_follows": "Personer", "explore.suggested_follows": "Personer",
"explore.title": "Udforsk", "explore.title": "Udforsk",
"explore.trending_links": "Nyheder", "explore.trending_links": "Nyheder",
@ -359,11 +358,11 @@
"footer.about": "Om", "footer.about": "Om",
"footer.directory": "Profiloversigt", "footer.directory": "Profiloversigt",
"footer.get_app": "Hent appen", "footer.get_app": "Hent appen",
"footer.invite": "Invitér personer",
"footer.keyboard_shortcuts": "Tastaturgenveje", "footer.keyboard_shortcuts": "Tastaturgenveje",
"footer.privacy_policy": "Privatlivspolitik", "footer.privacy_policy": "Privatlivspolitik",
"footer.source_code": "Vis kildekode", "footer.source_code": "Vis kildekode",
"footer.status": "Status", "footer.status": "Status",
"footer.terms_of_service": "Tjenestevilkår",
"generic.saved": "Gemt", "generic.saved": "Gemt",
"getting_started.heading": "Startmenu", "getting_started.heading": "Startmenu",
"hashtag.admin_moderation": "Åbn modereringsbrugerflade for #{name}", "hashtag.admin_moderation": "Åbn modereringsbrugerflade for #{name}",
@ -549,6 +548,8 @@
"notification.annual_report.view": "Vis #Wrapstodon", "notification.annual_report.view": "Vis #Wrapstodon",
"notification.favourite": "{name} favoritmarkerede dit indlæg", "notification.favourite": "{name} favoritmarkerede dit indlæg",
"notification.favourite.name_and_others_with_link": "{name} og <a>{count, plural, one {# anden} other {# andre}}</a> gjorde dit indlæg til favorit", "notification.favourite.name_and_others_with_link": "{name} og <a>{count, plural, one {# anden} other {# andre}}</a> gjorde dit indlæg til favorit",
"notification.favourite_pm": "{name} favoritmarkerede din private omtale",
"notification.favourite_pm.name_and_others_with_link": "{name} og <a>{count, plural, one {# anden} other {# andre}}</a> favoritmarkerede dit indlæg",
"notification.follow": "{name} begyndte at følge dig", "notification.follow": "{name} begyndte at følge dig",
"notification.follow.name_and_others": "{name} og <a>{count, plural, one {# andre} other {# andre}}</a> begyndte at følge dig", "notification.follow.name_and_others": "{name} og <a>{count, plural, one {# andre} other {# andre}}</a> begyndte at følge dig",
"notification.follow_request": "{name} har anmodet om at følge dig", "notification.follow_request": "{name} har anmodet om at følge dig",
@ -780,10 +781,11 @@
"search_results.accounts": "Profiler", "search_results.accounts": "Profiler",
"search_results.all": "Alle", "search_results.all": "Alle",
"search_results.hashtags": "Hashtags", "search_results.hashtags": "Hashtags",
"search_results.nothing_found": "Ingen resultater for disse søgeord", "search_results.no_results": "Ingen resultater.",
"search_results.no_search_yet": "Prøv at søge efter indlæg, profiler eller hashtags.",
"search_results.see_all": "Vis alle", "search_results.see_all": "Vis alle",
"search_results.statuses": "Indlæg", "search_results.statuses": "Indlæg",
"search_results.title": "Søg efter {q}", "search_results.title": "Søg efter \"{q}\"",
"server_banner.about_active_users": "Folk, som brugte denne server de seneste 30 dage (månedlige aktive brugere)", "server_banner.about_active_users": "Folk, som brugte denne server de seneste 30 dage (månedlige aktive brugere)",
"server_banner.active_users": "aktive brugere", "server_banner.active_users": "aktive brugere",
"server_banner.administered_by": "Håndteres af:", "server_banner.administered_by": "Håndteres af:",
@ -856,6 +858,7 @@
"subscribed_languages.target": "Skift abonnementssprog for {target}", "subscribed_languages.target": "Skift abonnementssprog for {target}",
"tabs_bar.home": "Hjem", "tabs_bar.home": "Hjem",
"tabs_bar.notifications": "Notifikationer", "tabs_bar.notifications": "Notifikationer",
"terms_of_service.title": "Tjenestevilkår",
"time_remaining.days": "{number, plural, one {# dag} other {# dage}} tilbage", "time_remaining.days": "{number, plural, one {# dag} other {# dage}} tilbage",
"time_remaining.hours": "{number, plural, one {# time} other {# timer}} tilbage", "time_remaining.hours": "{number, plural, one {# time} other {# timer}} tilbage",
"time_remaining.minutes": "{number, plural, one {# minut} other {# minutter}} tilbage", "time_remaining.minutes": "{number, plural, one {# minut} other {# minutter}} tilbage",

View file

@ -71,7 +71,7 @@
"account.unmute": "Stummschaltung von @{name} aufheben", "account.unmute": "Stummschaltung von @{name} aufheben",
"account.unmute_notifications_short": "Stummschaltung der Benachrichtigungen aufheben", "account.unmute_notifications_short": "Stummschaltung der Benachrichtigungen aufheben",
"account.unmute_short": "Stummschaltung aufheben", "account.unmute_short": "Stummschaltung aufheben",
"account_note.placeholder": "Notiz durch Klicken hinzufügen", "account_note.placeholder": "Klicken, um Notiz hinzuzufügen",
"admin.dashboard.daily_retention": "Verweildauer der Benutzer*innen pro Tag nach der Registrierung", "admin.dashboard.daily_retention": "Verweildauer der Benutzer*innen pro Tag nach der Registrierung",
"admin.dashboard.monthly_retention": "Verweildauer der Benutzer*innen pro Monat nach der Registrierung", "admin.dashboard.monthly_retention": "Verweildauer der Benutzer*innen pro Monat nach der Registrierung",
"admin.dashboard.retention.average": "Durchschnitt", "admin.dashboard.retention.average": "Durchschnitt",
@ -309,7 +309,6 @@
"error.unexpected_crash.next_steps_addons": "Versuche, das Add-on oder Übersetzungswerkzeug zu deaktivieren und lade die Seite anschließend neu. Sollte das Problem weiter bestehen, kannst du das Webinterface von Mastodon vermutlich über einen anderen Browser erreichen oder du verwendest eine mobile (native) App.", "error.unexpected_crash.next_steps_addons": "Versuche, das Add-on oder Übersetzungswerkzeug zu deaktivieren und lade die Seite anschließend neu. Sollte das Problem weiter bestehen, kannst du das Webinterface von Mastodon vermutlich über einen anderen Browser erreichen oder du verwendest eine mobile (native) App.",
"errors.unexpected_crash.copy_stacktrace": "Fehlerdiagnose in die Zwischenablage kopieren", "errors.unexpected_crash.copy_stacktrace": "Fehlerdiagnose in die Zwischenablage kopieren",
"errors.unexpected_crash.report_issue": "Fehler melden", "errors.unexpected_crash.report_issue": "Fehler melden",
"explore.search_results": "Suchergebnisse",
"explore.suggested_follows": "Profile", "explore.suggested_follows": "Profile",
"explore.title": "Entdecken", "explore.title": "Entdecken",
"explore.trending_links": "Neuigkeiten", "explore.trending_links": "Neuigkeiten",
@ -359,11 +358,11 @@
"footer.about": "Über", "footer.about": "Über",
"footer.directory": "Profilverzeichnis", "footer.directory": "Profilverzeichnis",
"footer.get_app": "App herunterladen", "footer.get_app": "App herunterladen",
"footer.invite": "Leute einladen",
"footer.keyboard_shortcuts": "Tastenkombinationen", "footer.keyboard_shortcuts": "Tastenkombinationen",
"footer.privacy_policy": "Datenschutzerklärung", "footer.privacy_policy": "Datenschutzerklärung",
"footer.source_code": "Quellcode anzeigen", "footer.source_code": "Quellcode anzeigen",
"footer.status": "Status", "footer.status": "Status",
"footer.terms_of_service": "Nutzungsbedingungen",
"generic.saved": "Gespeichert", "generic.saved": "Gespeichert",
"getting_started.heading": "Auf gehts!", "getting_started.heading": "Auf gehts!",
"hashtag.admin_moderation": "#{name} moderieren", "hashtag.admin_moderation": "#{name} moderieren",
@ -550,6 +549,8 @@
"notification.annual_report.view": "#Wrapstodon ansehen", "notification.annual_report.view": "#Wrapstodon ansehen",
"notification.favourite": "{name} favorisierte deinen Beitrag", "notification.favourite": "{name} favorisierte deinen Beitrag",
"notification.favourite.name_and_others_with_link": "{name} und <a>{count, plural, one {# weiteres Profil} other {# weitere Profile}}</a> favorisierten deinen Beitrag", "notification.favourite.name_and_others_with_link": "{name} und <a>{count, plural, one {# weiteres Profil} other {# weitere Profile}}</a> favorisierten deinen Beitrag",
"notification.favourite_pm": "{name} favorisierte deine private Erwähnung",
"notification.favourite_pm.name_and_others_with_link": "{name} und <a>{count, plural, one {# weiteres Profil} other {# weitere Profile}}</a> favorisierten deine private Erwähnung",
"notification.follow": "{name} folgt dir", "notification.follow": "{name} folgt dir",
"notification.follow.name_and_others": "{name} und <a>{count, plural, one {# weiteres Profil} other {# weitere Profile}}</a> folgen dir", "notification.follow.name_and_others": "{name} und <a>{count, plural, one {# weiteres Profil} other {# weitere Profile}}</a> folgen dir",
"notification.follow_request": "{name} möchte dir folgen", "notification.follow_request": "{name} möchte dir folgen",
@ -781,10 +782,11 @@
"search_results.accounts": "Profile", "search_results.accounts": "Profile",
"search_results.all": "Alles", "search_results.all": "Alles",
"search_results.hashtags": "Hashtags", "search_results.hashtags": "Hashtags",
"search_results.nothing_found": "Nichts zu diesen Suchbegriffen gefunden", "search_results.no_results": "Keine Ergebnisse.",
"search_results.no_search_yet": "Suche nach Beiträgen, Profilen oder Hashtags.",
"search_results.see_all": "Alle ansehen", "search_results.see_all": "Alle ansehen",
"search_results.statuses": "Beiträge", "search_results.statuses": "Beiträge",
"search_results.title": "Suchergebnisse für {q}", "search_results.title": "Nach „{q}“ suchen",
"server_banner.about_active_users": "Personen, die diesen Server in den vergangenen 30 Tagen verwendet haben (monatlich aktive Nutzer*innen)", "server_banner.about_active_users": "Personen, die diesen Server in den vergangenen 30 Tagen verwendet haben (monatlich aktive Nutzer*innen)",
"server_banner.active_users": "aktive Profile", "server_banner.active_users": "aktive Profile",
"server_banner.administered_by": "Verwaltet von:", "server_banner.administered_by": "Verwaltet von:",
@ -857,6 +859,7 @@
"subscribed_languages.target": "Abonnierte Sprachen für {target} ändern", "subscribed_languages.target": "Abonnierte Sprachen für {target} ändern",
"tabs_bar.home": "Startseite", "tabs_bar.home": "Startseite",
"tabs_bar.notifications": "Benachrichtigungen", "tabs_bar.notifications": "Benachrichtigungen",
"terms_of_service.title": "Nutzungsbedingungen",
"time_remaining.days": "noch {number, plural, one {# Tag} other {# Tage}}", "time_remaining.days": "noch {number, plural, one {# Tag} other {# Tage}}",
"time_remaining.hours": "noch {number, plural, one {# Stunde} other {# Stunden}}", "time_remaining.hours": "noch {number, plural, one {# Stunde} other {# Stunden}}",
"time_remaining.minutes": "noch {number, plural, one {# Minute} other {# Minuten}}", "time_remaining.minutes": "noch {number, plural, one {# Minute} other {# Minuten}}",

View file

@ -309,7 +309,6 @@
"error.unexpected_crash.next_steps_addons": "Δοκίμασε να τα απενεργοποιήσεις και ανανέωσε τη σελίδα. Αν αυτό δεν βοηθήσει, ίσως να μπορέσεις να χρησιμοποιήσεις το Mastodon μέσω διαφορετικού φυλλομετρητή ή κάποιας εφαρμογής.", "error.unexpected_crash.next_steps_addons": "Δοκίμασε να τα απενεργοποιήσεις και ανανέωσε τη σελίδα. Αν αυτό δεν βοηθήσει, ίσως να μπορέσεις να χρησιμοποιήσεις το Mastodon μέσω διαφορετικού φυλλομετρητή ή κάποιας εφαρμογής.",
"errors.unexpected_crash.copy_stacktrace": "Αντιγραφή μηνυμάτων κώδικα στο πρόχειρο", "errors.unexpected_crash.copy_stacktrace": "Αντιγραφή μηνυμάτων κώδικα στο πρόχειρο",
"errors.unexpected_crash.report_issue": "Αναφορά προβλήματος", "errors.unexpected_crash.report_issue": "Αναφορά προβλήματος",
"explore.search_results": "Αποτελέσματα αναζήτησης",
"explore.suggested_follows": "Άτομα", "explore.suggested_follows": "Άτομα",
"explore.title": "Εξερεύνηση", "explore.title": "Εξερεύνηση",
"explore.trending_links": "Νέα", "explore.trending_links": "Νέα",
@ -359,7 +358,6 @@
"footer.about": "Σχετικά με", "footer.about": "Σχετικά με",
"footer.directory": "Κατάλογος προφίλ", "footer.directory": "Κατάλογος προφίλ",
"footer.get_app": "Αποκτήστε την εφαρμογή", "footer.get_app": "Αποκτήστε την εφαρμογή",
"footer.invite": "Προσκάλεσε άτομα",
"footer.keyboard_shortcuts": "Συντομεύσεις πληκτρολογίου", "footer.keyboard_shortcuts": "Συντομεύσεις πληκτρολογίου",
"footer.privacy_policy": "Πολιτική απορρήτου", "footer.privacy_policy": "Πολιτική απορρήτου",
"footer.source_code": "Προβολή πηγαίου κώδικα", "footer.source_code": "Προβολή πηγαίου κώδικα",
@ -781,10 +779,8 @@
"search_results.accounts": "Προφίλ", "search_results.accounts": "Προφίλ",
"search_results.all": "Όλα", "search_results.all": "Όλα",
"search_results.hashtags": "Ετικέτες", "search_results.hashtags": "Ετικέτες",
"search_results.nothing_found": "Δεν βρέθηκε τίποτα με αυτούς τους όρους αναζήτησης",
"search_results.see_all": "Δες τα όλα", "search_results.see_all": "Δες τα όλα",
"search_results.statuses": "Αναρτήσεις", "search_results.statuses": "Αναρτήσεις",
"search_results.title": "Αναζήτηση για {q}",
"server_banner.about_active_users": "Άτομα που χρησιμοποιούν αυτόν τον διακομιστή κατά τις τελευταίες 30 ημέρες (Μηνιαία Ενεργοί Χρήστες)", "server_banner.about_active_users": "Άτομα που χρησιμοποιούν αυτόν τον διακομιστή κατά τις τελευταίες 30 ημέρες (Μηνιαία Ενεργοί Χρήστες)",
"server_banner.active_users": "ενεργοί χρήστες", "server_banner.active_users": "ενεργοί χρήστες",
"server_banner.administered_by": "Διαχειριστής:", "server_banner.administered_by": "Διαχειριστής:",

View file

@ -300,7 +300,6 @@
"error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.", "error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard",
"errors.unexpected_crash.report_issue": "Report issue", "errors.unexpected_crash.report_issue": "Report issue",
"explore.search_results": "Search results",
"explore.suggested_follows": "People", "explore.suggested_follows": "People",
"explore.title": "Explore", "explore.title": "Explore",
"explore.trending_links": "News", "explore.trending_links": "News",
@ -350,7 +349,6 @@
"footer.about": "About", "footer.about": "About",
"footer.directory": "Profiles directory", "footer.directory": "Profiles directory",
"footer.get_app": "Get the app", "footer.get_app": "Get the app",
"footer.invite": "Invite people",
"footer.keyboard_shortcuts": "Keyboard shortcuts", "footer.keyboard_shortcuts": "Keyboard shortcuts",
"footer.privacy_policy": "Privacy policy", "footer.privacy_policy": "Privacy policy",
"footer.source_code": "View source code", "footer.source_code": "View source code",
@ -770,10 +768,8 @@
"search_results.accounts": "Profiles", "search_results.accounts": "Profiles",
"search_results.all": "All", "search_results.all": "All",
"search_results.hashtags": "Hashtags", "search_results.hashtags": "Hashtags",
"search_results.nothing_found": "Could not find anything for these search terms",
"search_results.see_all": "See all", "search_results.see_all": "See all",
"search_results.statuses": "Posts", "search_results.statuses": "Posts",
"search_results.title": "Search for {q}",
"server_banner.about_active_users": "People using this server during the last 30 days (Monthly Active Users)", "server_banner.about_active_users": "People using this server during the last 30 days (Monthly Active Users)",
"server_banner.active_users": "active users", "server_banner.active_users": "active users",
"server_banner.administered_by": "Administered by:", "server_banner.administered_by": "Administered by:",

View file

@ -454,7 +454,6 @@
"error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.", "error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard",
"errors.unexpected_crash.report_issue": "Report issue", "errors.unexpected_crash.report_issue": "Report issue",
"explore.search_results": "Search results",
"explore.suggested_follows": "People", "explore.suggested_follows": "People",
"explore.title": "Explore", "explore.title": "Explore",
"explore.trending_links": "News", "explore.trending_links": "News",
@ -504,11 +503,11 @@
"footer.about": "About", "footer.about": "About",
"footer.directory": "Profiles directory", "footer.directory": "Profiles directory",
"footer.get_app": "Get the app", "footer.get_app": "Get the app",
"footer.invite": "Invite people",
"footer.keyboard_shortcuts": "Keyboard shortcuts", "footer.keyboard_shortcuts": "Keyboard shortcuts",
"footer.privacy_policy": "Privacy policy", "footer.privacy_policy": "Privacy policy",
"footer.source_code": "View source code", "footer.source_code": "View source code",
"footer.status": "Status", "footer.status": "Status",
"footer.terms_of_service": "Terms of service",
"generic.saved": "Saved", "generic.saved": "Saved",
"getting_started.heading": "Getting started", "getting_started.heading": "Getting started",
"hashtag.admin_moderation": "Open moderation interface for #{name}", "hashtag.admin_moderation": "Open moderation interface for #{name}",
@ -713,6 +712,8 @@
"notification.emoji_reaction.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> reacted your post with emoji", "notification.emoji_reaction.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> reacted your post with emoji",
"notification.favourite": "{name} favorited your post", "notification.favourite": "{name} favorited your post",
"notification.favourite.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> favorited your post", "notification.favourite.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> favorited your post",
"notification.favourite_pm": "{name} favorited your private mention",
"notification.favourite_pm.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> favorited your private mention",
"notification.follow": "{name} followed you", "notification.follow": "{name} followed you",
"notification.follow.name_and_others": "{name} and <a>{count, plural, one {# other} other {# others}}</a> followed you", "notification.follow.name_and_others": "{name} and <a>{count, plural, one {# other} other {# others}}</a> followed you",
"notification.follow_request": "{name} has requested to follow you", "notification.follow_request": "{name} has requested to follow you",
@ -969,10 +970,11 @@
"search_results.accounts": "Profiles", "search_results.accounts": "Profiles",
"search_results.all": "All", "search_results.all": "All",
"search_results.hashtags": "Hashtags", "search_results.hashtags": "Hashtags",
"search_results.nothing_found": "Could not find anything for these search terms", "search_results.no_results": "No results.",
"search_results.no_search_yet": "Try searching for posts, profiles or hashtags.",
"search_results.see_all": "See all", "search_results.see_all": "See all",
"search_results.statuses": "Posts", "search_results.statuses": "Posts",
"search_results.title": "Search for {q}", "search_results.title": "Search for \"{q}\"",
"searchability.change": "Change post searchability", "searchability.change": "Change post searchability",
"searchability.direct.long": "Nobody can find, but you can", "searchability.direct.long": "Nobody can find, but you can",
"searchability.direct.short": "Self only", "searchability.direct.short": "Self only",
@ -1075,6 +1077,7 @@
"subscribed_languages.target": "Change subscribed languages for {target}", "subscribed_languages.target": "Change subscribed languages for {target}",
"tabs_bar.home": "Home", "tabs_bar.home": "Home",
"tabs_bar.notifications": "Notifications", "tabs_bar.notifications": "Notifications",
"terms_of_service.title": "Terms of Service",
"time_remaining.days": "{number, plural, one {# day} other {# days}} left", "time_remaining.days": "{number, plural, one {# day} other {# days}} left",
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",

View file

@ -10,7 +10,7 @@
"about.domain_blocks.suspended.title": "Suspendita", "about.domain_blocks.suspended.title": "Suspendita",
"about.not_available": "Ĉi tiu informo ne estas disponebla ĉe ĉi tiu servilo.", "about.not_available": "Ĉi tiu informo ne estas disponebla ĉe ĉi tiu servilo.",
"about.powered_by": "Malcentrigita socia retejo pere de {mastodon}", "about.powered_by": "Malcentrigita socia retejo pere de {mastodon}",
"about.rules": "Regularo de la servilo", "about.rules": "Reguloj de la servilo",
"account.account_note_header": "Personaj notoj", "account.account_note_header": "Personaj notoj",
"account.add_or_remove_from_list": "Aldoni al aŭ forigi el listoj", "account.add_or_remove_from_list": "Aldoni al aŭ forigi el listoj",
"account.badges.bot": "Aŭtomata", "account.badges.bot": "Aŭtomata",
@ -87,9 +87,14 @@
"alert.unexpected.title": "Aj!", "alert.unexpected.title": "Aj!",
"alt_text_badge.title": "Alt-teksto", "alt_text_badge.title": "Alt-teksto",
"announcement.announcement": "Anonco", "announcement.announcement": "Anonco",
"annual_report.summary.archetype.booster": "La Ĉasanto de Mojoso",
"annual_report.summary.archetype.lurker": "La vidanto",
"annual_report.summary.archetype.oracle": "La Orakolo",
"annual_report.summary.archetype.pollster": "La balotenketisto", "annual_report.summary.archetype.pollster": "La balotenketisto",
"annual_report.summary.archetype.replier": "La plej societema", "annual_report.summary.archetype.replier": "La plej societema",
"annual_report.summary.followers.followers": "sekvantoj", "annual_report.summary.followers.followers": "sekvantoj",
"annual_report.summary.followers.total": "{count} tute",
"annual_report.summary.here_it_is": "Jen via resumo de {year}:",
"annual_report.summary.highlighted_post.by_favourites": "plej ŝatata afiŝo", "annual_report.summary.highlighted_post.by_favourites": "plej ŝatata afiŝo",
"annual_report.summary.highlighted_post.by_reblogs": "plej diskonigita afiŝo", "annual_report.summary.highlighted_post.by_reblogs": "plej diskonigita afiŝo",
"annual_report.summary.highlighted_post.by_replies": "afiŝo kun la plej multaj respondoj", "annual_report.summary.highlighted_post.by_replies": "afiŝo kun la plej multaj respondoj",
@ -98,6 +103,7 @@
"annual_report.summary.most_used_hashtag.most_used_hashtag": "plej uzata kradvorto", "annual_report.summary.most_used_hashtag.most_used_hashtag": "plej uzata kradvorto",
"annual_report.summary.most_used_hashtag.none": "Nenio", "annual_report.summary.most_used_hashtag.none": "Nenio",
"annual_report.summary.new_posts.new_posts": "novaj afiŝoj", "annual_report.summary.new_posts.new_posts": "novaj afiŝoj",
"annual_report.summary.percentile.text": "<topLabel>Tio metas vin en la plej</topLabel><percentage></percentage><bottomLabel>de {domain} uzantoj.</bottomLabel>",
"annual_report.summary.percentile.we_wont_tell_bernie": "Ni ne diros al Zamenhof.", "annual_report.summary.percentile.we_wont_tell_bernie": "Ni ne diros al Zamenhof.",
"annual_report.summary.thanks": "Dankon pro esti parto de Mastodon!", "annual_report.summary.thanks": "Dankon pro esti parto de Mastodon!",
"attachments_list.unprocessed": "(neprilaborita)", "attachments_list.unprocessed": "(neprilaborita)",
@ -233,8 +239,10 @@
"disabled_account_banner.text": "Via konto {disabledAccount} estas nune malvalidigita.", "disabled_account_banner.text": "Via konto {disabledAccount} estas nune malvalidigita.",
"dismissable_banner.community_timeline": "Jen la plej novaj publikaj afiŝoj de uzantoj, kies kontojn gastigas {domain}.", "dismissable_banner.community_timeline": "Jen la plej novaj publikaj afiŝoj de uzantoj, kies kontojn gastigas {domain}.",
"dismissable_banner.dismiss": "Eksigi", "dismissable_banner.dismiss": "Eksigi",
"dismissable_banner.explore_links": "Ĉi tiuj revuaĵoj plejkunhaviĝas en la fediverso hodiaŭ. Pli novaj revuaĵoj afiŝis de pli da homoj metis pli alte.",
"dismissable_banner.explore_statuses": "Ĉi tiuj afiŝoj populariĝas sur la fediverso hodiaŭ. Pli novaj afiŝoj kun pli da diskonigoj kaj stemuloj estas rangigitaj pli alte.", "dismissable_banner.explore_statuses": "Ĉi tiuj afiŝoj populariĝas sur la fediverso hodiaŭ. Pli novaj afiŝoj kun pli da diskonigoj kaj stemuloj estas rangigitaj pli alte.",
"dismissable_banner.explore_tags": "Ĉi tiuj kradvortoj populariĝas sur la fediverso hodiaŭ. Kradvortoj, kiuj estas uzataj de pli malsamaj homoj, estas rangigitaj pli alte.", "dismissable_banner.explore_tags": "Ĉi tiuj kradvortoj populariĝas sur la fediverso hodiaŭ. Kradvortoj, kiuj estas uzataj de pli malsamaj homoj, estas rangigitaj pli alte.",
"dismissable_banner.public_timeline": "Ĉi tiuj estas la plej ĵusaj publikaj afiŝoj de homoj en la fediverso, kiujn la homoj en {domain} sekvas.",
"domain_block_modal.block": "Bloki servilon", "domain_block_modal.block": "Bloki servilon",
"domain_block_modal.block_account_instead": "Bloki @{name} anstataŭe", "domain_block_modal.block_account_instead": "Bloki @{name} anstataŭe",
"domain_block_modal.they_can_interact_with_old_posts": "Homoj de ĉi tiu servilo povas interagi kun viaj malnovaj afiŝoj.", "domain_block_modal.they_can_interact_with_old_posts": "Homoj de ĉi tiu servilo povas interagi kun viaj malnovaj afiŝoj.",
@ -301,7 +309,6 @@
"error.unexpected_crash.next_steps_addons": "Provu malaktivigi ilin kaj tiam refreŝigi la paĝon. Se tio ne helpas, vi ankoraŭ povus uzi Mastodon per malsama retumilo aŭ operaciuma aplikajo.", "error.unexpected_crash.next_steps_addons": "Provu malaktivigi ilin kaj tiam refreŝigi la paĝon. Se tio ne helpas, vi ankoraŭ povus uzi Mastodon per malsama retumilo aŭ operaciuma aplikajo.",
"errors.unexpected_crash.copy_stacktrace": "Kopii stakspuron en tondujo", "errors.unexpected_crash.copy_stacktrace": "Kopii stakspuron en tondujo",
"errors.unexpected_crash.report_issue": "Raporti problemon", "errors.unexpected_crash.report_issue": "Raporti problemon",
"explore.search_results": "Serĉaj rezultoj",
"explore.suggested_follows": "Homoj", "explore.suggested_follows": "Homoj",
"explore.title": "Esplori", "explore.title": "Esplori",
"explore.trending_links": "Novaĵoj", "explore.trending_links": "Novaĵoj",
@ -351,13 +358,14 @@
"footer.about": "Pri", "footer.about": "Pri",
"footer.directory": "Profilujo", "footer.directory": "Profilujo",
"footer.get_app": "Akiri la apon", "footer.get_app": "Akiri la apon",
"footer.invite": "Inviti homojn",
"footer.keyboard_shortcuts": "Fulmoklavoj", "footer.keyboard_shortcuts": "Fulmoklavoj",
"footer.privacy_policy": "Politiko de privateco", "footer.privacy_policy": "Politiko de privateco",
"footer.source_code": "Montri fontkodon", "footer.source_code": "Montri fontkodon",
"footer.status": "Stato", "footer.status": "Stato",
"footer.terms_of_service": "Kondiĉoj de uzado",
"generic.saved": "Konservita", "generic.saved": "Konservita",
"getting_started.heading": "Por komenci", "getting_started.heading": "Por komenci",
"hashtag.admin_moderation": "Malfermi fasadon de moderigado por #{name}",
"hashtag.column_header.tag_mode.all": "kaj {additional}", "hashtag.column_header.tag_mode.all": "kaj {additional}",
"hashtag.column_header.tag_mode.any": "aŭ {additional}", "hashtag.column_header.tag_mode.any": "aŭ {additional}",
"hashtag.column_header.tag_mode.none": "sen {additional}", "hashtag.column_header.tag_mode.none": "sen {additional}",
@ -442,7 +450,7 @@
"keyboard_shortcuts.notifications": "Malfermu la sciigajn kolumnon", "keyboard_shortcuts.notifications": "Malfermu la sciigajn kolumnon",
"keyboard_shortcuts.open_media": "Malfermu plurmedion", "keyboard_shortcuts.open_media": "Malfermu plurmedion",
"keyboard_shortcuts.pinned": "Malfermu alpinglitajn afiŝojn-liston", "keyboard_shortcuts.pinned": "Malfermu alpinglitajn afiŝojn-liston",
"keyboard_shortcuts.profile": "Malfermu la profilon de aŭtoro", "keyboard_shortcuts.profile": "Malfermu la profilon de aŭtoroprofilo",
"keyboard_shortcuts.reply": "Respondu al afiŝo", "keyboard_shortcuts.reply": "Respondu al afiŝo",
"keyboard_shortcuts.requests": "Malfermi la liston de petoj por sekvado", "keyboard_shortcuts.requests": "Malfermi la liston de petoj por sekvado",
"keyboard_shortcuts.search": "Enfokusigi la serĉbreton", "keyboard_shortcuts.search": "Enfokusigi la serĉbreton",
@ -488,6 +496,7 @@
"lists.replies_policy.none": "Neniu", "lists.replies_policy.none": "Neniu",
"lists.save": "Konservi", "lists.save": "Konservi",
"lists.search": "Ŝerci", "lists.search": "Ŝerci",
"lists.show_replies_to": "Inkludi respondojn de listomembroj al",
"load_pending": "{count,plural, one {# nova elemento} other {# novaj elementoj}}", "load_pending": "{count,plural, one {# nova elemento} other {# novaj elementoj}}",
"loading_indicator.label": "Ŝargado…", "loading_indicator.label": "Ŝargado…",
"media_gallery.hide": "Kaŝi", "media_gallery.hide": "Kaŝi",
@ -536,9 +545,12 @@
"notification.admin.report_statuses_other": "{name} raportis {target}", "notification.admin.report_statuses_other": "{name} raportis {target}",
"notification.admin.sign_up": "{name} kreis konton", "notification.admin.sign_up": "{name} kreis konton",
"notification.admin.sign_up.name_and_others": "{name} kaj {count, plural, one {# alia} other {# aliaj}} kreis konton", "notification.admin.sign_up.name_and_others": "{name} kaj {count, plural, one {# alia} other {# aliaj}} kreis konton",
"notification.annual_report.message": "Via {year} #Wrapstodon atendas! Malkovru viajn bonaĵojn kaj memorindajn momentojn en Mastodon!",
"notification.annual_report.view": "Vidu #Wrapstodon", "notification.annual_report.view": "Vidu #Wrapstodon",
"notification.favourite": "{name} stelumis vian afiŝon", "notification.favourite": "{name} ŝatis vian afiŝon",
"notification.favourite.name_and_others_with_link": "{name} kaj <a>{count, plural, one {# alia} other {# aliaj}}</a> ŝatis vian afiŝon", "notification.favourite.name_and_others_with_link": "{name} kaj <a>{count, plural, one {# alia} other {# aliaj}}</a> ŝatis vian afiŝon",
"notification.favourite_pm": "{name} ŝatis vian privatan mencion",
"notification.favourite_pm.name_and_others_with_link": "{name} kaj <a>{count, plural, one {# alia} other {# aliaj}}</a> ŝatis vian privatan mencion",
"notification.follow": "{name} eksekvis vin", "notification.follow": "{name} eksekvis vin",
"notification.follow.name_and_others": "{name} kaj <a>{count, plural, one {# alia} other {# aliaj}}</a> sekvis vin", "notification.follow.name_and_others": "{name} kaj <a>{count, plural, one {# alia} other {# aliaj}}</a> sekvis vin",
"notification.follow_request": "{name} petis sekvi vin", "notification.follow_request": "{name} petis sekvi vin",
@ -770,10 +782,8 @@
"search_results.accounts": "Profiloj", "search_results.accounts": "Profiloj",
"search_results.all": "Ĉiuj", "search_results.all": "Ĉiuj",
"search_results.hashtags": "Kradvortoj", "search_results.hashtags": "Kradvortoj",
"search_results.nothing_found": "Povis trovi nenion por ĉi tiuj serĉaj terminoj",
"search_results.see_all": "Vidu ĉiujn", "search_results.see_all": "Vidu ĉiujn",
"search_results.statuses": "Afiŝoj", "search_results.statuses": "Afiŝoj",
"search_results.title": "Serĉ-rezultoj por {q}",
"server_banner.about_active_users": "Personoj uzantaj ĉi tiun servilon dum la lastaj 30 tagoj (Aktivaj Uzantoj Monate)", "server_banner.about_active_users": "Personoj uzantaj ĉi tiun servilon dum la lastaj 30 tagoj (Aktivaj Uzantoj Monate)",
"server_banner.active_users": "aktivaj uzantoj", "server_banner.active_users": "aktivaj uzantoj",
"server_banner.administered_by": "Administrata de:", "server_banner.administered_by": "Administrata de:",
@ -846,6 +856,7 @@
"subscribed_languages.target": "Ŝanĝu abonitajn lingvojn por {target}", "subscribed_languages.target": "Ŝanĝu abonitajn lingvojn por {target}",
"tabs_bar.home": "Hejmo", "tabs_bar.home": "Hejmo",
"tabs_bar.notifications": "Sciigoj", "tabs_bar.notifications": "Sciigoj",
"terms_of_service.title": "Kondiĉoj de uzado",
"time_remaining.days": "{number, plural, one {# tago} other {# tagoj}} restas", "time_remaining.days": "{number, plural, one {# tago} other {# tagoj}} restas",
"time_remaining.hours": "{number, plural, one {# horo} other {# horoj}} restas", "time_remaining.hours": "{number, plural, one {# horo} other {# horoj}} restas",
"time_remaining.minutes": "{number, plural, one {# minuto} other {# minutoj}} restas", "time_remaining.minutes": "{number, plural, one {# minuto} other {# minutoj}} restas",

View file

@ -309,7 +309,6 @@
"error.unexpected_crash.next_steps_addons": "Intentá deshabilitarlos y recargá la página. Si eso no ayuda, podés usar Mastodon a través de un navegador web diferente o aplicación nativa.", "error.unexpected_crash.next_steps_addons": "Intentá deshabilitarlos y recargá la página. Si eso no ayuda, podés usar Mastodon a través de un navegador web diferente o aplicación nativa.",
"errors.unexpected_crash.copy_stacktrace": "Copiar stacktrace al portapapeles", "errors.unexpected_crash.copy_stacktrace": "Copiar stacktrace al portapapeles",
"errors.unexpected_crash.report_issue": "Informar problema", "errors.unexpected_crash.report_issue": "Informar problema",
"explore.search_results": "Resultados de búsqueda",
"explore.suggested_follows": "Cuentas", "explore.suggested_follows": "Cuentas",
"explore.title": "Explorá", "explore.title": "Explorá",
"explore.trending_links": "Noticias", "explore.trending_links": "Noticias",
@ -359,11 +358,11 @@
"footer.about": "Información", "footer.about": "Información",
"footer.directory": "Directorio de perfiles", "footer.directory": "Directorio de perfiles",
"footer.get_app": "Conseguí la aplicación", "footer.get_app": "Conseguí la aplicación",
"footer.invite": "Invitá a gente",
"footer.keyboard_shortcuts": "Atajos de teclado", "footer.keyboard_shortcuts": "Atajos de teclado",
"footer.privacy_policy": "Política de privacidad", "footer.privacy_policy": "Política de privacidad",
"footer.source_code": "Ver código fuente", "footer.source_code": "Ver código fuente",
"footer.status": "Estado", "footer.status": "Estado",
"footer.terms_of_service": "Términos del servicio",
"generic.saved": "Guardado", "generic.saved": "Guardado",
"getting_started.heading": "Inicio de Mastodon", "getting_started.heading": "Inicio de Mastodon",
"hashtag.admin_moderation": "Abrir interfaz de moderación para #{name}", "hashtag.admin_moderation": "Abrir interfaz de moderación para #{name}",
@ -550,6 +549,8 @@
"notification.annual_report.view": "Ver #Wrapstodon", "notification.annual_report.view": "Ver #Wrapstodon",
"notification.favourite": "{name} marcó tu mensaje como favorito", "notification.favourite": "{name} marcó tu mensaje como favorito",
"notification.favourite.name_and_others_with_link": "{name} y <a>{count, plural, one {# cuenta más} other {# cuentas más}}</a> marcaron tu mensaje como favorito", "notification.favourite.name_and_others_with_link": "{name} y <a>{count, plural, one {# cuenta más} other {# cuentas más}}</a> marcaron tu mensaje como favorito",
"notification.favourite_pm": "{name} ha marcado como favorita tu mención privada",
"notification.favourite_pm.name_and_others_with_link": "{name} y <a>{count, plural, one {# más} other {# más}}</a> han marcado como favorita tu mención privada",
"notification.follow": "{name} te empezó a seguir", "notification.follow": "{name} te empezó a seguir",
"notification.follow.name_and_others": "{name} y <a>{count, plural, one {# cuenta más} other {# cuentas más}}</a> te están siguiendo", "notification.follow.name_and_others": "{name} y <a>{count, plural, one {# cuenta más} other {# cuentas más}}</a> te están siguiendo",
"notification.follow_request": "{name} solicitó seguirte", "notification.follow_request": "{name} solicitó seguirte",
@ -781,10 +782,11 @@
"search_results.accounts": "Perfiles", "search_results.accounts": "Perfiles",
"search_results.all": "Todos", "search_results.all": "Todos",
"search_results.hashtags": "Etiquetas", "search_results.hashtags": "Etiquetas",
"search_results.nothing_found": "No se pudo encontrar nada para estos términos de búsqueda", "search_results.no_results": "Sin resultados.",
"search_results.no_search_yet": "Intenta buscar publicaciones, perfiles o etiquetas.",
"search_results.see_all": "Ver todo", "search_results.see_all": "Ver todo",
"search_results.statuses": "Mensajes", "search_results.statuses": "Mensajes",
"search_results.title": "Buscar {q}", "search_results.title": "Búsqueda de \"{q}\"",
"server_banner.about_active_users": "Personas usando este servidor durante los últimos 30 días (Usuarios Activos Mensuales)", "server_banner.about_active_users": "Personas usando este servidor durante los últimos 30 días (Usuarios Activos Mensuales)",
"server_banner.active_users": "usuarios activos", "server_banner.active_users": "usuarios activos",
"server_banner.administered_by": "Administrado por:", "server_banner.administered_by": "Administrado por:",
@ -857,6 +859,7 @@
"subscribed_languages.target": "Cambiar idiomas suscritos para {target}", "subscribed_languages.target": "Cambiar idiomas suscritos para {target}",
"tabs_bar.home": "Principal", "tabs_bar.home": "Principal",
"tabs_bar.notifications": "Notificaciones", "tabs_bar.notifications": "Notificaciones",
"terms_of_service.title": "Términos del servicio",
"time_remaining.days": "{number, plural,one {queda # día} other {quedan # días}}", "time_remaining.days": "{number, plural,one {queda # día} other {quedan # días}}",
"time_remaining.hours": "{number, plural,one {queda # hora} other {quedan # horas}}", "time_remaining.hours": "{number, plural,one {queda # hora} other {quedan # horas}}",
"time_remaining.minutes": "{number, plural,one {queda # minuto} other {quedan # minutos}}", "time_remaining.minutes": "{number, plural,one {queda # minuto} other {quedan # minutos}}",

View file

@ -309,7 +309,6 @@
"error.unexpected_crash.next_steps_addons": "Intenta deshabilitarlos y recarga la página. Si eso no ayuda, podrías usar Mastodon a través de un navegador web diferente o aplicación nativa.", "error.unexpected_crash.next_steps_addons": "Intenta deshabilitarlos y recarga la página. Si eso no ayuda, podrías usar Mastodon a través de un navegador web diferente o aplicación nativa.",
"errors.unexpected_crash.copy_stacktrace": "Copiar el seguimiento de pila en el portapapeles", "errors.unexpected_crash.copy_stacktrace": "Copiar el seguimiento de pila en el portapapeles",
"errors.unexpected_crash.report_issue": "Informar problema", "errors.unexpected_crash.report_issue": "Informar problema",
"explore.search_results": "Resultados de búsqueda",
"explore.suggested_follows": "Personas", "explore.suggested_follows": "Personas",
"explore.title": "Descubrir", "explore.title": "Descubrir",
"explore.trending_links": "Noticias", "explore.trending_links": "Noticias",
@ -359,11 +358,11 @@
"footer.about": "Acerca de", "footer.about": "Acerca de",
"footer.directory": "Directorio de perfiles", "footer.directory": "Directorio de perfiles",
"footer.get_app": "Obtener la aplicación", "footer.get_app": "Obtener la aplicación",
"footer.invite": "Invitar personas",
"footer.keyboard_shortcuts": "Atajos de teclado", "footer.keyboard_shortcuts": "Atajos de teclado",
"footer.privacy_policy": "Política de privacidad", "footer.privacy_policy": "Política de privacidad",
"footer.source_code": "Ver código fuente", "footer.source_code": "Ver código fuente",
"footer.status": "Estado", "footer.status": "Estado",
"footer.terms_of_service": "Condiciones del servicio",
"generic.saved": "Guardado", "generic.saved": "Guardado",
"getting_started.heading": "Primeros pasos", "getting_started.heading": "Primeros pasos",
"hashtag.admin_moderation": "Abrir interfaz de moderación para #{name}", "hashtag.admin_moderation": "Abrir interfaz de moderación para #{name}",
@ -550,6 +549,8 @@
"notification.annual_report.view": "Ver #Wrapstodon", "notification.annual_report.view": "Ver #Wrapstodon",
"notification.favourite": "{name} marcó como favorita tu publicación", "notification.favourite": "{name} marcó como favorita tu publicación",
"notification.favourite.name_and_others_with_link": "{name} y <a>{count, plural, one {# otro} other {# otros}}</a> marcaron tu publicación como favorita", "notification.favourite.name_and_others_with_link": "{name} y <a>{count, plural, one {# otro} other {# otros}}</a> marcaron tu publicación como favorita",
"notification.favourite_pm": "{name} marcó como favorito tu mención privada",
"notification.favourite_pm.name_and_others_with_link": "{name} y <a>{count, plural, one {# otro} other {# otros}}</a> marcaron como favorito tu mención privada",
"notification.follow": "{name} te empezó a seguir", "notification.follow": "{name} te empezó a seguir",
"notification.follow.name_and_others": "{name} y <a>{count, plural, one {# otro} other {# otros}}</a> te han seguido", "notification.follow.name_and_others": "{name} y <a>{count, plural, one {# otro} other {# otros}}</a> te han seguido",
"notification.follow_request": "{name} ha solicitado seguirte", "notification.follow_request": "{name} ha solicitado seguirte",
@ -781,10 +782,11 @@
"search_results.accounts": "Perfiles", "search_results.accounts": "Perfiles",
"search_results.all": "Todos", "search_results.all": "Todos",
"search_results.hashtags": "Etiquetas", "search_results.hashtags": "Etiquetas",
"search_results.nothing_found": "No se pudo encontrar nada para estos términos de búsqueda", "search_results.no_results": "Sin resultados.",
"search_results.no_search_yet": "Intenta buscar publicaciones, perfiles o etiquetas.",
"search_results.see_all": "Ver todos", "search_results.see_all": "Ver todos",
"search_results.statuses": "Publicaciones", "search_results.statuses": "Publicaciones",
"search_results.title": "Buscar {q}", "search_results.title": "Búsqueda de \"{q}\"",
"server_banner.about_active_users": "Personas utilizando este servidor durante los últimos 30 días (Usuarios Activos Mensuales)", "server_banner.about_active_users": "Personas utilizando este servidor durante los últimos 30 días (Usuarios Activos Mensuales)",
"server_banner.active_users": "usuarios activos", "server_banner.active_users": "usuarios activos",
"server_banner.administered_by": "Administrado por:", "server_banner.administered_by": "Administrado por:",
@ -857,6 +859,7 @@
"subscribed_languages.target": "Cambiar idiomas suscritos para {target}", "subscribed_languages.target": "Cambiar idiomas suscritos para {target}",
"tabs_bar.home": "Inicio", "tabs_bar.home": "Inicio",
"tabs_bar.notifications": "Notificaciones", "tabs_bar.notifications": "Notificaciones",
"terms_of_service.title": "Condiciones del servicio",
"time_remaining.days": "{number, plural, one {# día restante} other {# días restantes}}", "time_remaining.days": "{number, plural, one {# día restante} other {# días restantes}}",
"time_remaining.hours": "{number, plural, one {# hora restante} other {# horas restantes}}", "time_remaining.hours": "{number, plural, one {# hora restante} other {# horas restantes}}",
"time_remaining.minutes": "{number, plural, one {# minuto restante} other {# minutos restantes}}", "time_remaining.minutes": "{number, plural, one {# minuto restante} other {# minutos restantes}}",

View file

@ -309,7 +309,6 @@
"error.unexpected_crash.next_steps_addons": "Intenta deshabilitarlos y recarga la página. Si eso no ayuda, podrías usar Mastodon a través de un navegador web diferente o aplicación nativa.", "error.unexpected_crash.next_steps_addons": "Intenta deshabilitarlos y recarga la página. Si eso no ayuda, podrías usar Mastodon a través de un navegador web diferente o aplicación nativa.",
"errors.unexpected_crash.copy_stacktrace": "Copiar el seguimiento de pila en el portapapeles", "errors.unexpected_crash.copy_stacktrace": "Copiar el seguimiento de pila en el portapapeles",
"errors.unexpected_crash.report_issue": "Informar de un problema/error", "errors.unexpected_crash.report_issue": "Informar de un problema/error",
"explore.search_results": "Resultados de búsqueda",
"explore.suggested_follows": "Personas", "explore.suggested_follows": "Personas",
"explore.title": "Explorar", "explore.title": "Explorar",
"explore.trending_links": "Noticias", "explore.trending_links": "Noticias",
@ -359,11 +358,11 @@
"footer.about": "Acerca de", "footer.about": "Acerca de",
"footer.directory": "Directorio de perfiles", "footer.directory": "Directorio de perfiles",
"footer.get_app": "Obtener la aplicación", "footer.get_app": "Obtener la aplicación",
"footer.invite": "Invitar personas",
"footer.keyboard_shortcuts": "Atajos de teclado", "footer.keyboard_shortcuts": "Atajos de teclado",
"footer.privacy_policy": "Política de privacidad", "footer.privacy_policy": "Política de privacidad",
"footer.source_code": "Ver código fuente", "footer.source_code": "Ver código fuente",
"footer.status": "Estado", "footer.status": "Estado",
"footer.terms_of_service": "Términos del servicio",
"generic.saved": "Guardado", "generic.saved": "Guardado",
"getting_started.heading": "Primeros pasos", "getting_started.heading": "Primeros pasos",
"hashtag.admin_moderation": "Abrir interfaz de moderación para #{name}", "hashtag.admin_moderation": "Abrir interfaz de moderación para #{name}",
@ -550,6 +549,8 @@
"notification.annual_report.view": "Ver #Wrapstodon", "notification.annual_report.view": "Ver #Wrapstodon",
"notification.favourite": "{name} marcó como favorita tu publicación", "notification.favourite": "{name} marcó como favorita tu publicación",
"notification.favourite.name_and_others_with_link": "{name} y <a>{count, plural, one {# más} other {# más}}</a> marcaron tu publicación como favorita", "notification.favourite.name_and_others_with_link": "{name} y <a>{count, plural, one {# más} other {# más}}</a> marcaron tu publicación como favorita",
"notification.favourite_pm": "{name} ha marcado como favorita tu mención privada",
"notification.favourite_pm.name_and_others_with_link": "{name} y <a>{count, plural, one {# más} other {# más}}</a> han marcado como favorita tu mención privada",
"notification.follow": "{name} te empezó a seguir", "notification.follow": "{name} te empezó a seguir",
"notification.follow.name_and_others": "{name} y <a>{count, plural, one {# otro} other {# otros}}</a> te siguieron", "notification.follow.name_and_others": "{name} y <a>{count, plural, one {# otro} other {# otros}}</a> te siguieron",
"notification.follow_request": "{name} ha solicitado seguirte", "notification.follow_request": "{name} ha solicitado seguirte",
@ -781,10 +782,11 @@
"search_results.accounts": "Perfiles", "search_results.accounts": "Perfiles",
"search_results.all": "Todos", "search_results.all": "Todos",
"search_results.hashtags": "Etiquetas", "search_results.hashtags": "Etiquetas",
"search_results.nothing_found": "No se pudo encontrar nada para estos términos de búsqueda", "search_results.no_results": "Sin resultados.",
"search_results.no_search_yet": "Intenta buscar publicaciones, perfiles o etiquetas.",
"search_results.see_all": "Ver todos", "search_results.see_all": "Ver todos",
"search_results.statuses": "Publicaciones", "search_results.statuses": "Publicaciones",
"search_results.title": "Buscar {q}", "search_results.title": "Búsqueda de \"{q}\"",
"server_banner.about_active_users": "Usuarios activos en el servidor durante los últimos 30 días (Usuarios Activos Mensuales)", "server_banner.about_active_users": "Usuarios activos en el servidor durante los últimos 30 días (Usuarios Activos Mensuales)",
"server_banner.active_users": "usuarios activos", "server_banner.active_users": "usuarios activos",
"server_banner.administered_by": "Administrado por:", "server_banner.administered_by": "Administrado por:",
@ -857,6 +859,7 @@
"subscribed_languages.target": "Cambiar idiomas suscritos para {target}", "subscribed_languages.target": "Cambiar idiomas suscritos para {target}",
"tabs_bar.home": "Inicio", "tabs_bar.home": "Inicio",
"tabs_bar.notifications": "Notificaciones", "tabs_bar.notifications": "Notificaciones",
"terms_of_service.title": "Términos del servicio",
"time_remaining.days": "{number, plural, one {# día restante} other {# días restantes}}", "time_remaining.days": "{number, plural, one {# día restante} other {# días restantes}}",
"time_remaining.hours": "{number, plural, one {# hora restante} other {# horas restantes}}", "time_remaining.hours": "{number, plural, one {# hora restante} other {# horas restantes}}",
"time_remaining.minutes": "{number, plural, one {# minuto restante} other {# minutos restantes}}", "time_remaining.minutes": "{number, plural, one {# minuto restante} other {# minutos restantes}}",

Some files were not shown because too many files have changed in this diff Show more