Merge remote-tracking branch 'parent/main' into upstream-20240705
This commit is contained in:
commit
9a07550fa6
75 changed files with 1046 additions and 543 deletions
31
CHANGELOG.md
31
CHANGELOG.md
|
@ -2,6 +2,37 @@
|
||||||
|
|
||||||
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.2.10] - 2024-07-04
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix incorrect permission checking on multiple API endpoints ([GHSA-58x8-3qxw-6hm7](https://github.com/mastodon/mastodon/security/advisories/GHSA-58x8-3qxw-6hm7))
|
||||||
|
- Fix incorrect authorship checking when processing some activities (CVE-2024-37903, [GHSA-xjvf-fm67-4qc3](https://github.com/mastodon/mastodon/security/advisories/GHSA-xjvf-fm67-4qc3))
|
||||||
|
- Fix ongoing streaming sessions not being invalidated when application tokens get revoked ([GHSA-vp5r-5pgw-jwqx](https://github.com/mastodon/mastodon/security/advisories/GHSA-vp5r-5pgw-jwqx))
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add yarn version specification to avoid confusion with Yarn 3 and Yarn 4
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change preview cards generation to skip unusually long URLs ([oneiros](https://github.com/mastodon/mastodon/pull/30854))
|
||||||
|
- Change search modifiers to be case-insensitive ([Gargron](https://github.com/mastodon/mastodon/pull/30865))
|
||||||
|
- Change `STATSD_ADDR` handling to emit a warning rather than crashing if the address is unreachable ([timothyjrogers](https://github.com/mastodon/mastodon/pull/30691))
|
||||||
|
- Change PWA start URL from `/home` to `/` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27377))
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Removed dependency on `posix-spawn` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18559))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix scheduled statuses scheduled in less than 5 minutes being immediately published ([danielmbrasil](https://github.com/mastodon/mastodon/pull/30584))
|
||||||
|
- Fix encoding detection for link cards ([oneiros](https://github.com/mastodon/mastodon/pull/30780))
|
||||||
|
- Fix `/admin/accounts/:account_id/statuses/:id` for edited posts with media attachments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30819))
|
||||||
|
- Fix duplicate `@context` attribute in user archive export ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30653))
|
||||||
|
|
||||||
## [4.2.9] - 2024-05-30
|
## [4.2.9] - 2024-05-30
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
4
Gemfile
4
Gemfile
|
@ -25,7 +25,7 @@ gem 'ruby-vips', '~> 2.2', require: false
|
||||||
gem 'active_model_serializers', '~> 0.10'
|
gem 'active_model_serializers', '~> 0.10'
|
||||||
gem 'addressable', '~> 2.8'
|
gem 'addressable', '~> 2.8'
|
||||||
gem 'bootsnap', '~> 1.18.0', require: false
|
gem 'bootsnap', '~> 1.18.0', require: false
|
||||||
gem 'browser'
|
gem 'browser', '< 6' # https://github.com/fnando/browser/issues/543
|
||||||
gem 'charlock_holmes', '~> 0.7.7'
|
gem 'charlock_holmes', '~> 0.7.7'
|
||||||
gem 'chewy', '~> 7.3'
|
gem 'chewy', '~> 7.3'
|
||||||
gem 'devise', '~> 4.9'
|
gem 'devise', '~> 4.9'
|
||||||
|
@ -114,7 +114,7 @@ group :opentelemetry do
|
||||||
gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false
|
gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false
|
||||||
gem 'opentelemetry-instrumentation-pg', '~> 0.27.1', require: false
|
gem 'opentelemetry-instrumentation-pg', '~> 0.27.1', require: false
|
||||||
gem 'opentelemetry-instrumentation-rack', '~> 0.24.1', require: false
|
gem 'opentelemetry-instrumentation-rack', '~> 0.24.1', require: false
|
||||||
gem 'opentelemetry-instrumentation-rails', '~> 0.30.0', require: false
|
gem 'opentelemetry-instrumentation-rails', '~> 0.31.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false
|
gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false
|
||||||
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false
|
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false
|
||||||
gem 'opentelemetry-sdk', '~> 1.4', require: false
|
gem 'opentelemetry-sdk', '~> 1.4', require: false
|
||||||
|
|
44
Gemfile.lock
44
Gemfile.lock
|
@ -100,19 +100,19 @@ GEM
|
||||||
attr_required (1.0.2)
|
attr_required (1.0.2)
|
||||||
awrence (1.2.1)
|
awrence (1.2.1)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.947.0)
|
aws-partitions (1.950.0)
|
||||||
aws-sdk-core (3.199.0)
|
aws-sdk-core (3.201.0)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.8)
|
aws-sigv4 (~> 1.8)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.87.0)
|
aws-sdk-kms (1.88.0)
|
||||||
aws-sdk-core (~> 3, >= 3.199.0)
|
aws-sdk-core (~> 3, >= 3.201.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.154.0)
|
aws-sdk-s3 (1.156.0)
|
||||||
aws-sdk-core (~> 3, >= 3.199.0)
|
aws-sdk-core (~> 3, >= 3.201.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.8)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.8.0)
|
aws-sigv4 (1.8.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
azure-storage-blob (2.0.3)
|
azure-storage-blob (2.0.3)
|
||||||
|
@ -159,7 +159,7 @@ GEM
|
||||||
case_transform (0.2)
|
case_transform (0.2)
|
||||||
activesupport
|
activesupport
|
||||||
cbor (0.5.9.8)
|
cbor (0.5.9.8)
|
||||||
charlock_holmes (0.7.7)
|
charlock_holmes (0.7.8)
|
||||||
chewy (7.6.0)
|
chewy (7.6.0)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
elasticsearch (>= 7.14.0, < 8)
|
elasticsearch (>= 7.14.0, < 8)
|
||||||
|
@ -208,7 +208,7 @@ GEM
|
||||||
activerecord (>= 4.2, < 8)
|
activerecord (>= 4.2, < 8)
|
||||||
docile (1.4.0)
|
docile (1.4.0)
|
||||||
domain_name (0.6.20240107)
|
domain_name (0.6.20240107)
|
||||||
doorkeeper (5.6.9)
|
doorkeeper (5.7.1)
|
||||||
railties (>= 5)
|
railties (>= 5)
|
||||||
dotenv (3.1.2)
|
dotenv (3.1.2)
|
||||||
drb (2.2.1)
|
drb (2.2.1)
|
||||||
|
@ -431,7 +431,7 @@ GEM
|
||||||
mime-types-data (3.2024.0604)
|
mime-types-data (3.2024.0604)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.7)
|
mini_portile2 (2.8.7)
|
||||||
minitest (5.23.1)
|
minitest (5.24.1)
|
||||||
msgpack (1.7.2)
|
msgpack (1.7.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.4.0)
|
multipart-post (2.4.0)
|
||||||
|
@ -516,7 +516,7 @@ GEM
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-active_support (~> 0.1)
|
opentelemetry-instrumentation-active_support (~> 0.1)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-active_job (0.7.1)
|
opentelemetry-instrumentation-active_job (0.7.2)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-active_model_serializers (0.20.1)
|
opentelemetry-instrumentation-active_model_serializers (0.20.1)
|
||||||
|
@ -525,7 +525,7 @@ GEM
|
||||||
opentelemetry-instrumentation-active_record (0.7.2)
|
opentelemetry-instrumentation-active_record (0.7.2)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-active_support (0.5.1)
|
opentelemetry-instrumentation-active_support (0.6.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-base (0.22.3)
|
opentelemetry-instrumentation-base (0.22.3)
|
||||||
|
@ -556,19 +556,19 @@ GEM
|
||||||
opentelemetry-instrumentation-rack (0.24.5)
|
opentelemetry-instrumentation-rack (0.24.5)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-rails (0.30.2)
|
opentelemetry-instrumentation-rails (0.31.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-action_mailer (~> 0.1.0)
|
opentelemetry-instrumentation-action_mailer (~> 0.1.0)
|
||||||
opentelemetry-instrumentation-action_pack (~> 0.9.0)
|
opentelemetry-instrumentation-action_pack (~> 0.9.0)
|
||||||
opentelemetry-instrumentation-action_view (~> 0.7.0)
|
opentelemetry-instrumentation-action_view (~> 0.7.0)
|
||||||
opentelemetry-instrumentation-active_job (~> 0.7.0)
|
opentelemetry-instrumentation-active_job (~> 0.7.0)
|
||||||
opentelemetry-instrumentation-active_record (~> 0.7.0)
|
opentelemetry-instrumentation-active_record (~> 0.7.0)
|
||||||
opentelemetry-instrumentation-active_support (~> 0.5.0)
|
opentelemetry-instrumentation-active_support (~> 0.6.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-redis (0.25.6)
|
opentelemetry-instrumentation-redis (0.25.6)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-sidekiq (0.25.5)
|
opentelemetry-instrumentation-sidekiq (0.25.6)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-registry (0.3.1)
|
opentelemetry-registry (0.3.1)
|
||||||
|
@ -696,7 +696,7 @@ GEM
|
||||||
responders (3.1.1)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml (3.3.0)
|
rexml (3.3.1)
|
||||||
strscan
|
strscan
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rouge (4.2.1)
|
rouge (4.2.1)
|
||||||
|
@ -751,12 +751,12 @@ GEM
|
||||||
rubocop-performance (1.21.1)
|
rubocop-performance (1.21.1)
|
||||||
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.25.0)
|
rubocop-rails (2.25.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.33.0, < 2.0)
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rspec (3.0.1)
|
rubocop-rspec (3.0.2)
|
||||||
rubocop (~> 1.61)
|
rubocop (~> 1.61)
|
||||||
rubocop-rspec_rails (2.30.0)
|
rubocop-rspec_rails (2.30.0)
|
||||||
rubocop (~> 1.61)
|
rubocop (~> 1.61)
|
||||||
|
@ -833,7 +833,7 @@ GEM
|
||||||
unicode-display_width (>= 1.1.1, < 3)
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
terrapin (1.0.1)
|
terrapin (1.0.1)
|
||||||
climate_control
|
climate_control
|
||||||
test-prof (1.3.3)
|
test-prof (1.3.3.1)
|
||||||
thor (1.3.1)
|
thor (1.3.1)
|
||||||
tilt (2.3.0)
|
tilt (2.3.0)
|
||||||
timeout (0.4.1)
|
timeout (0.4.1)
|
||||||
|
@ -916,7 +916,7 @@ DEPENDENCIES
|
||||||
blurhash (~> 0.1)
|
blurhash (~> 0.1)
|
||||||
bootsnap (~> 1.18.0)
|
bootsnap (~> 1.18.0)
|
||||||
brakeman (~> 6.0)
|
brakeman (~> 6.0)
|
||||||
browser
|
browser (< 6)
|
||||||
bundler-audit (~> 0.9)
|
bundler-audit (~> 0.9)
|
||||||
capybara (~> 3.39)
|
capybara (~> 3.39)
|
||||||
charlock_holmes (~> 0.7.7)
|
charlock_holmes (~> 0.7.7)
|
||||||
|
@ -994,7 +994,7 @@ DEPENDENCIES
|
||||||
opentelemetry-instrumentation-net_http (~> 0.22.4)
|
opentelemetry-instrumentation-net_http (~> 0.22.4)
|
||||||
opentelemetry-instrumentation-pg (~> 0.27.1)
|
opentelemetry-instrumentation-pg (~> 0.27.1)
|
||||||
opentelemetry-instrumentation-rack (~> 0.24.1)
|
opentelemetry-instrumentation-rack (~> 0.24.1)
|
||||||
opentelemetry-instrumentation-rails (~> 0.30.0)
|
opentelemetry-instrumentation-rails (~> 0.31.0)
|
||||||
opentelemetry-instrumentation-redis (~> 0.25.3)
|
opentelemetry-instrumentation-redis (~> 0.25.3)
|
||||||
opentelemetry-instrumentation-sidekiq (~> 0.25.2)
|
opentelemetry-instrumentation-sidekiq (~> 0.25.2)
|
||||||
opentelemetry-sdk (~> 1.4)
|
opentelemetry-sdk (~> 1.4)
|
||||||
|
|
|
@ -6,6 +6,7 @@ class Api::V1::ScheduledStatusesController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, except: [:update, :destroy]
|
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, except: [:update, :destroy]
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:update, :destroy]
|
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:update, :destroy]
|
||||||
|
|
||||||
|
before_action :require_user!
|
||||||
before_action :set_statuses, only: :index
|
before_action :set_statuses, only: :index
|
||||||
before_action :set_status, except: :index
|
before_action :set_status, except: :index
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class Api::V1::Statuses::TranslationsController < Api::V1::Statuses::BaseController
|
class Api::V1::Statuses::TranslationsController < Api::V1::Statuses::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
|
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
|
||||||
|
before_action :require_user!
|
||||||
before_action :set_translation
|
before_action :set_translation
|
||||||
|
|
||||||
rescue_from TranslationService::NotConfiguredError, with: :not_found
|
rescue_from TranslationService::NotConfiguredError, with: :not_found
|
||||||
|
|
|
@ -3,8 +3,14 @@
|
||||||
class Api::V1::Timelines::BaseController < Api::BaseController
|
class Api::V1::Timelines::BaseController < Api::BaseController
|
||||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||||
|
|
||||||
|
before_action :require_user!, if: :require_auth?
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def require_auth?
|
||||||
|
!Setting.timeline_preview
|
||||||
|
end
|
||||||
|
|
||||||
def pagination_collection
|
def pagination_collection
|
||||||
@statuses
|
@statuses
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Timelines::LinkController < Api::V1::Timelines::BaseController
|
class Api::V1::Timelines::LinkController < Api::V1::Timelines::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :show, if: :require_auth?
|
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
|
||||||
before_action :set_preview_card
|
before_action :set_preview_card
|
||||||
before_action :set_statuses
|
before_action :set_statuses
|
||||||
|
|
||||||
|
@ -17,10 +17,6 @@ class Api::V1::Timelines::LinkController < Api::V1::Timelines::BaseController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def require_auth?
|
|
||||||
!Setting.timeline_preview
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_preview_card
|
def set_preview_card
|
||||||
@preview_card = PreviewCard.joins(:trend).merge(PreviewCardTrend.allowed).find_by!(url: params[:url])
|
@preview_card = PreviewCard.joins(:trend).merge(PreviewCardTrend.allowed).find_by!(url: params[:url])
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
|
class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
|
||||||
before_action :require_user!, only: [:show], if: :require_auth?
|
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
|
||||||
|
|
||||||
PERMITTED_PARAMS = %i(local remote limit only_media).freeze
|
PERMITTED_PARAMS = %i(local remote limit only_media).freeze
|
||||||
|
|
||||||
|
@ -15,10 +15,6 @@ class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def require_auth?
|
|
||||||
!Setting.timeline_preview
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_statuses
|
def load_statuses
|
||||||
preloaded_public_statuses_page
|
preloaded_public_statuses_page
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Timelines::TagController < Api::V1::Timelines::BaseController
|
class Api::V1::Timelines::TagController < Api::V1::Timelines::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :show, if: :require_auth?
|
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
|
||||||
before_action :load_tag
|
before_action :load_tag
|
||||||
|
|
||||||
PERMITTED_PARAMS = %i(local limit only_media).freeze
|
PERMITTED_PARAMS = %i(local limit only_media).freeze
|
||||||
|
|
|
@ -17,6 +17,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
Web::PushSubscription.unsubscribe_for(params[:id], current_resource_owner)
|
Web::PushSubscription.unsubscribe_for(params[:id], current_resource_owner)
|
||||||
|
Doorkeeper::Application.find_by(id: params[:id])&.close_streaming_sessions(current_resource_owner)
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,19 +2,34 @@ import { useRef, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
export const useTimeout = () => {
|
export const useTimeout = () => {
|
||||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
const callbackRef = useRef<() => void>();
|
||||||
|
|
||||||
const set = useCallback((callback: () => void, delay: number) => {
|
const set = useCallback((callback: () => void, delay: number) => {
|
||||||
if (timeoutRef.current) {
|
if (timeoutRef.current) {
|
||||||
clearTimeout(timeoutRef.current);
|
clearTimeout(timeoutRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
callbackRef.current = callback;
|
||||||
timeoutRef.current = setTimeout(callback, delay);
|
timeoutRef.current = setTimeout(callback, delay);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const delay = useCallback((delay: number) => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!callbackRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutRef.current = setTimeout(callbackRef.current, delay);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const cancel = useCallback(() => {
|
const cancel = useCallback(() => {
|
||||||
if (timeoutRef.current) {
|
if (timeoutRef.current) {
|
||||||
clearTimeout(timeoutRef.current);
|
clearTimeout(timeoutRef.current);
|
||||||
timeoutRef.current = undefined;
|
timeoutRef.current = undefined;
|
||||||
|
callbackRef.current = undefined;
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -25,5 +40,5 @@ export const useTimeout = () => {
|
||||||
[cancel],
|
[cancel],
|
||||||
);
|
);
|
||||||
|
|
||||||
return [set, cancel] as const;
|
return [set, cancel, delay] as const;
|
||||||
};
|
};
|
||||||
|
|
|
@ -159,6 +159,7 @@ export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => exp
|
||||||
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
|
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
|
||||||
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
||||||
export const expandAntennaTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`antenna:${id}`, `/api/v1/timelines/antenna/${id}`, { max_id: maxId }, done);
|
export const expandAntennaTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`antenna:${id}`, `/api/v1/timelines/antenna/${id}`, { max_id: maxId }, done);
|
||||||
|
export const expandLinkTimeline = (url, { maxId } = {}, done = noOp) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId }, done);
|
||||||
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
|
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
|
||||||
return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
|
return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||||
max_id: maxId,
|
max_id: maxId,
|
||||||
|
|
|
@ -50,6 +50,7 @@ export interface ApiPreviewCardJSON {
|
||||||
type: string;
|
type: string;
|
||||||
author_name: string;
|
author_name: string;
|
||||||
author_url: string;
|
author_url: string;
|
||||||
|
author_account?: ApiAccountJSON;
|
||||||
provider_name: string;
|
provider_name: string;
|
||||||
provider_url: string;
|
provider_url: string;
|
||||||
html: string;
|
html: string;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import { useIntl, defineMessages } from 'react-intl';
|
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { useIdentity } from '@/mastodon/identity_context';
|
import { useIdentity } from '@/mastodon/identity_context';
|
||||||
import {
|
import {
|
||||||
|
@ -19,15 +19,11 @@ const messages = defineMessages({
|
||||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' },
|
followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' },
|
||||||
mutual: { id: 'account.mutual', defaultMessage: 'Mutual' },
|
mutual: { id: 'account.mutual', defaultMessage: 'Mutual' },
|
||||||
cancel_follow_request: {
|
|
||||||
id: 'account.cancel_follow_request',
|
|
||||||
defaultMessage: 'Withdraw follow request',
|
|
||||||
},
|
|
||||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const FollowButton: React.FC<{
|
export const FollowButton: React.FC<{
|
||||||
accountId: string;
|
accountId?: string;
|
||||||
}> = ({ accountId }) => {
|
}> = ({ accountId }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
@ -36,7 +32,7 @@ export const FollowButton: React.FC<{
|
||||||
accountId ? state.accounts.get(accountId) : undefined,
|
accountId ? state.accounts.get(accountId) : undefined,
|
||||||
);
|
);
|
||||||
const relationship = useAppSelector((state) =>
|
const relationship = useAppSelector((state) =>
|
||||||
state.relationships.get(accountId),
|
accountId ? state.relationships.get(accountId) : undefined,
|
||||||
);
|
);
|
||||||
const following = relationship?.following || relationship?.requested;
|
const following = relationship?.following || relationship?.requested;
|
||||||
|
|
||||||
|
@ -65,11 +61,28 @@ export const FollowButton: React.FC<{
|
||||||
if (accountId === me) {
|
if (accountId === me) {
|
||||||
return;
|
return;
|
||||||
} else if (relationship.following || relationship.requested) {
|
} else if (relationship.following || relationship.requested) {
|
||||||
dispatch(unfollowAccount(accountId));
|
dispatch(
|
||||||
|
openModal({
|
||||||
|
modalType: 'CONFIRM',
|
||||||
|
modalProps: {
|
||||||
|
message: (
|
||||||
|
<FormattedMessage
|
||||||
|
id='confirmations.unfollow.message'
|
||||||
|
defaultMessage='Are you sure you want to unfollow {name}?'
|
||||||
|
values={{ name: <strong>@{account?.acct}</strong> }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
confirm: intl.formatMessage(messages.unfollow),
|
||||||
|
onConfirm: () => {
|
||||||
|
dispatch(unfollowAccount(accountId));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
dispatch(followAccount(accountId));
|
dispatch(followAccount(accountId));
|
||||||
}
|
}
|
||||||
}, [dispatch, accountId, relationship, account, signedIn]);
|
}, [dispatch, intl, accountId, relationship, account, signedIn]);
|
||||||
|
|
||||||
let label;
|
let label;
|
||||||
|
|
||||||
|
@ -79,8 +92,6 @@ export const FollowButton: React.FC<{
|
||||||
label = intl.formatMessage(messages.edit_profile);
|
label = intl.formatMessage(messages.edit_profile);
|
||||||
} else if (!relationship) {
|
} else if (!relationship) {
|
||||||
label = <LoadingIndicator />;
|
label = <LoadingIndicator />;
|
||||||
} else if (relationship.requested) {
|
|
||||||
label = intl.formatMessage(messages.cancel_follow_request);
|
|
||||||
} else if (
|
} else if (
|
||||||
relationship.following &&
|
relationship.following &&
|
||||||
isShowItem('relationships') &&
|
isShowItem('relationships') &&
|
||||||
|
@ -93,7 +104,7 @@ export const FollowButton: React.FC<{
|
||||||
relationship.followed_by
|
relationship.followed_by
|
||||||
) {
|
) {
|
||||||
label = intl.formatMessage(messages.followBack);
|
label = intl.formatMessage(messages.followBack);
|
||||||
} else if (relationship.following) {
|
} else if (relationship.following || relationship.requested) {
|
||||||
label = intl.formatMessage(messages.unfollow);
|
label = intl.formatMessage(messages.unfollow);
|
||||||
} else {
|
} else {
|
||||||
label = intl.formatMessage(messages.follow);
|
label = intl.formatMessage(messages.follow);
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
export const HoverCardAccount = forwardRef<
|
export const HoverCardAccount = forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
{ accountId: string }
|
{ accountId?: string }
|
||||||
>(({ accountId }, ref) => {
|
>(({ accountId }, ref) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,8 @@ import { useTimeout } from 'mastodon/../hooks/useTimeout';
|
||||||
import { HoverCardAccount } from 'mastodon/components/hover_card_account';
|
import { HoverCardAccount } from 'mastodon/components/hover_card_account';
|
||||||
|
|
||||||
const offset = [-12, 4] as OffsetValue;
|
const offset = [-12, 4] as OffsetValue;
|
||||||
const enterDelay = 650;
|
const enterDelay = 750;
|
||||||
const leaveDelay = 250;
|
const leaveDelay = 150;
|
||||||
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
||||||
|
|
||||||
const isHoverCardAnchor = (/*element: HTMLElement*/) => false; // set false until original is fixed some problem about this hover
|
const isHoverCardAnchor = (/*element: HTMLElement*/) => false; // set false until original is fixed some problem about this hover
|
||||||
|
@ -23,50 +23,12 @@ export const HoverCardController: React.FC = () => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [accountId, setAccountId] = useState<string | undefined>();
|
const [accountId, setAccountId] = useState<string | undefined>();
|
||||||
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
|
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
|
||||||
const cardRef = useRef<HTMLDivElement>(null);
|
const cardRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
|
const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
|
||||||
const [setEnterTimeout, cancelEnterTimeout] = useTimeout();
|
const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout();
|
||||||
|
const [setScrollTimeout] = useTimeout();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const handleAnchorMouseEnter = useCallback(
|
|
||||||
(e: MouseEvent) => {
|
|
||||||
const { target } = e;
|
|
||||||
|
|
||||||
if (target instanceof HTMLElement && isHoverCardAnchor(/*target*/)) {
|
|
||||||
cancelLeaveTimeout();
|
|
||||||
|
|
||||||
setEnterTimeout(() => {
|
|
||||||
target.setAttribute('aria-describedby', 'hover-card');
|
|
||||||
setAnchor(target);
|
|
||||||
setOpen(true);
|
|
||||||
setAccountId(
|
|
||||||
target.getAttribute('data-hover-card-account') ?? undefined,
|
|
||||||
);
|
|
||||||
}, enterDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target === cardRef.current?.parentNode) {
|
|
||||||
cancelLeaveTimeout();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[cancelLeaveTimeout, setEnterTimeout, setOpen, setAccountId, setAnchor],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleAnchorMouseLeave = useCallback(
|
|
||||||
(e: MouseEvent) => {
|
|
||||||
if (e.target === anchor || e.target === cardRef.current?.parentNode) {
|
|
||||||
cancelEnterTimeout();
|
|
||||||
|
|
||||||
setLeaveTimeout(() => {
|
|
||||||
anchor?.removeAttribute('aria-describedby');
|
|
||||||
setOpen(false);
|
|
||||||
setAnchor(null);
|
|
||||||
}, leaveDelay);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[cancelEnterTimeout, setLeaveTimeout, setOpen, setAnchor, anchor],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
cancelEnterTimeout();
|
cancelEnterTimeout();
|
||||||
cancelLeaveTimeout();
|
cancelLeaveTimeout();
|
||||||
|
@ -79,22 +41,119 @@ export const HoverCardController: React.FC = () => {
|
||||||
}, [handleClose, location]);
|
}, [handleClose, location]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.addEventListener('mouseenter', handleAnchorMouseEnter, {
|
let isScrolling = false;
|
||||||
|
let currentAnchor: HTMLElement | null = null;
|
||||||
|
|
||||||
|
const open = (target: HTMLElement) => {
|
||||||
|
target.setAttribute('aria-describedby', 'hover-card');
|
||||||
|
setOpen(true);
|
||||||
|
setAnchor(target);
|
||||||
|
setAccountId(target.getAttribute('data-hover-card-account') ?? undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
currentAnchor?.removeAttribute('aria-describedby');
|
||||||
|
currentAnchor = null;
|
||||||
|
setOpen(false);
|
||||||
|
setAnchor(null);
|
||||||
|
setAccountId(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseEnter = (e: MouseEvent) => {
|
||||||
|
const { target } = e;
|
||||||
|
|
||||||
|
// We've exited the window
|
||||||
|
if (!(target instanceof HTMLElement)) {
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We've entered an anchor
|
||||||
|
if (!isScrolling && isHoverCardAnchor(/*target*/)) {
|
||||||
|
cancelLeaveTimeout();
|
||||||
|
|
||||||
|
currentAnchor?.removeAttribute('aria-describedby');
|
||||||
|
currentAnchor = target;
|
||||||
|
|
||||||
|
setEnterTimeout(() => {
|
||||||
|
open(target);
|
||||||
|
}, enterDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We've entered the hover card
|
||||||
|
if (
|
||||||
|
!isScrolling &&
|
||||||
|
(target === currentAnchor || target === cardRef.current)
|
||||||
|
) {
|
||||||
|
cancelLeaveTimeout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = (e: MouseEvent) => {
|
||||||
|
if (!currentAnchor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.target === currentAnchor || e.target === cardRef.current) {
|
||||||
|
cancelEnterTimeout();
|
||||||
|
|
||||||
|
setLeaveTimeout(() => {
|
||||||
|
close();
|
||||||
|
}, leaveDelay);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScrollEnd = () => {
|
||||||
|
isScrolling = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
isScrolling = true;
|
||||||
|
cancelEnterTimeout();
|
||||||
|
setScrollTimeout(handleScrollEnd, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = () => {
|
||||||
|
delayEnterTimeout(enterDelay);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.addEventListener('mouseenter', handleMouseEnter, {
|
||||||
passive: true,
|
passive: true,
|
||||||
capture: true,
|
capture: true,
|
||||||
});
|
});
|
||||||
document.body.addEventListener('mouseleave', handleAnchorMouseLeave, {
|
|
||||||
|
document.body.addEventListener('mousemove', handleMouseMove, {
|
||||||
|
passive: true,
|
||||||
|
capture: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.addEventListener('mouseleave', handleMouseLeave, {
|
||||||
|
passive: true,
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('scroll', handleScroll, {
|
||||||
passive: true,
|
passive: true,
|
||||||
capture: true,
|
capture: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.body.removeEventListener('mouseenter', handleAnchorMouseEnter);
|
document.body.removeEventListener('mouseenter', handleMouseEnter);
|
||||||
document.body.removeEventListener('mouseleave', handleAnchorMouseLeave);
|
document.body.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.body.removeEventListener('mouseleave', handleMouseLeave);
|
||||||
|
document.removeEventListener('scroll', handleScroll);
|
||||||
};
|
};
|
||||||
}, [handleAnchorMouseEnter, handleAnchorMouseLeave]);
|
}, [
|
||||||
|
setEnterTimeout,
|
||||||
if (!accountId) return null;
|
setLeaveTimeout,
|
||||||
|
setScrollTimeout,
|
||||||
|
cancelEnterTimeout,
|
||||||
|
cancelLeaveTimeout,
|
||||||
|
delayEnterTimeout,
|
||||||
|
setOpen,
|
||||||
|
setAccountId,
|
||||||
|
setAnchor,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay
|
<Overlay
|
||||||
|
|
|
@ -33,6 +33,7 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
withCounters: PropTypes.bool,
|
withCounters: PropTypes.bool,
|
||||||
timelineId: PropTypes.string,
|
timelineId: PropTypes.string,
|
||||||
lastId: PropTypes.string,
|
lastId: PropTypes.string,
|
||||||
|
bindToDocument: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
|
|
@ -98,7 +98,7 @@ const messageForFollowButton = relationship => {
|
||||||
return messages.mutual;
|
return messages.mutual;
|
||||||
} else if (!relationship.get('following') && relationship.get('followed_by') && isShowItem('relationships')) {
|
} else if (!relationship.get('following') && relationship.get('followed_by') && isShowItem('relationships')) {
|
||||||
return messages.followBack;
|
return messages.followBack;
|
||||||
} else if (relationship.get('following')) {
|
} else if (relationship.get('following') || relationship.get('requested')) {
|
||||||
return messages.unfollow;
|
return messages.unfollow;
|
||||||
} else {
|
} else {
|
||||||
return messages.follow;
|
return messages.follow;
|
||||||
|
@ -313,10 +313,8 @@ class Header extends ImmutablePureComponent {
|
||||||
if (me !== account.get('id')) {
|
if (me !== account.get('id')) {
|
||||||
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
|
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
|
||||||
actionBtn = <Button disabled><LoadingIndicator /></Button>;
|
actionBtn = <Button disabled><LoadingIndicator /></Button>;
|
||||||
} else if (account.getIn(['relationship', 'requested'])) {
|
|
||||||
actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
|
|
||||||
} else if (!account.getIn(['relationship', 'blocking'])) {
|
} else if (!account.getIn(['relationship', 'blocking'])) {
|
||||||
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(messageForFollowButton(account.get('relationship')))} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
|
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) })} text={intl.formatMessage(messageForFollowButton(account.get('relationship')))} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
|
||||||
} else if (account.getIn(['relationship', 'blocking'])) {
|
} else if (account.getIn(['relationship', 'blocking'])) {
|
||||||
actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
|
actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,6 @@ import { makeGetAccount, getAccountHidden } from '../../../selectors';
|
||||||
import Header from '../components/header';
|
import Header from '../components/header';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
cancelFollowRequestConfirm: { id: 'confirmations.cancel_follow_request.confirm', defaultMessage: 'Withdraw request' },
|
|
||||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
|
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
|
||||||
});
|
});
|
||||||
|
@ -46,7 +45,7 @@ const makeMapStateToProps = () => {
|
||||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
|
|
||||||
onFollow (account) {
|
onFollow (account) {
|
||||||
if (account.getIn(['relationship', 'following'])) {
|
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
||||||
dispatch(openModal({
|
dispatch(openModal({
|
||||||
modalType: 'CONFIRM',
|
modalType: 'CONFIRM',
|
||||||
modalProps: {
|
modalProps: {
|
||||||
|
@ -55,15 +54,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} else if (account.getIn(['relationship', 'requested'])) {
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'CONFIRM',
|
|
||||||
modalProps: {
|
|
||||||
message: <FormattedMessage id='confirmations.cancel_follow_request.message' defaultMessage='Are you sure you want to withdraw your request to follow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
|
||||||
confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
|
|
||||||
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
dispatch(followAccount(account.get('id')));
|
dispatch(followAccount(account.get('id')));
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,7 +163,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete });
|
menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete });
|
||||||
|
|
||||||
const names = accounts.map(a => (
|
const names = accounts.map(a => (
|
||||||
<Link to={`/@${a.get('acct')}`} key={a.get('id')} title={a.get('acct')}>
|
<Link to={`/@${a.get('acct')}`} key={a.get('id')} data-hover-card-account={a.get('id')}>
|
||||||
<bdi>
|
<bdi>
|
||||||
<strong
|
<strong
|
||||||
className='display-name__html'
|
className='display-name__html'
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { useState, useCallback } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import { Blurhash } from 'mastodon/components/blurhash';
|
import { Blurhash } from 'mastodon/components/blurhash';
|
||||||
|
@ -57,7 +59,7 @@ export const Story = ({
|
||||||
|
|
||||||
<div className='story__details__shared'>
|
<div className='story__details__shared'>
|
||||||
{author ? <FormattedMessage id='link_preview.author' className='story__details__shared__author' defaultMessage='By {name}' values={{ name: authorAccount ? <AuthorLink accountId={authorAccount} /> : <strong>{author}</strong> }} /> : <span />}
|
{author ? <FormattedMessage id='link_preview.author' className='story__details__shared__author' defaultMessage='By {name}' values={{ name: authorAccount ? <AuthorLink accountId={authorAccount} /> : <strong>{author}</strong> }} /> : <span />}
|
||||||
{typeof sharedTimes === 'number' ? <span className='story__details__shared__pill'><ShortNumber value={sharedTimes} renderer={sharesCountRenderer} /></span> : <Skeleton width='10ch' />}
|
{typeof sharedTimes === 'number' ? <Link className='story__details__shared__pill' to={`/links/${encodeURIComponent(url)}`}><ShortNumber value={sharedTimes} renderer={sharesCountRenderer} /></Link> : <Skeleton width='10ch' />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
76
app/javascript/mastodon/features/link_timeline/index.tsx
Normal file
76
app/javascript/mastodon/features/link_timeline/index.tsx
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { useRef, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
|
||||||
|
import { expandLinkTimeline } from 'mastodon/actions/timelines';
|
||||||
|
import Column from 'mastodon/components/column';
|
||||||
|
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||||
|
import StatusListContainer from 'mastodon/features/ui/containers/status_list_container';
|
||||||
|
import type { Card } from 'mastodon/models/status';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
export const LinkTimeline: React.FC<{
|
||||||
|
multiColumn: boolean;
|
||||||
|
}> = ({ multiColumn }) => {
|
||||||
|
const { url } = useParams<{ url: string }>();
|
||||||
|
const decodedUrl = url ? decodeURIComponent(url) : undefined;
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const columnRef = useRef<Column>(null);
|
||||||
|
const firstStatusId = useAppSelector((state) =>
|
||||||
|
decodedUrl
|
||||||
|
? // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||||
|
(state.timelines.getIn([`link:${decodedUrl}`, 'items', 0]) as string)
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
const story = useAppSelector((state) =>
|
||||||
|
firstStatusId
|
||||||
|
? (state.statuses.getIn([firstStatusId, 'card']) as Card)
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleHeaderClick = useCallback(() => {
|
||||||
|
columnRef.current?.scrollTop();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLoadMore = useCallback(
|
||||||
|
(maxId: string) => {
|
||||||
|
dispatch(expandLinkTimeline(decodedUrl, { maxId }));
|
||||||
|
},
|
||||||
|
[dispatch, decodedUrl],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(expandLinkTimeline(decodedUrl));
|
||||||
|
}, [dispatch, decodedUrl]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column bindToDocument={!multiColumn} ref={columnRef} label={story?.title}>
|
||||||
|
<ColumnHeader
|
||||||
|
icon='explore'
|
||||||
|
iconComponent={ExploreIcon}
|
||||||
|
title={story?.title}
|
||||||
|
onClick={handleHeaderClick}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
showBackButton
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatusListContainer
|
||||||
|
timelineId={`link:${decodedUrl}`}
|
||||||
|
onLoadMore={handleLoadMore}
|
||||||
|
trackScroll
|
||||||
|
scrollKey={`link_timeline-${decodedUrl}`}
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{story?.title}</title>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default LinkTimeline;
|
|
@ -63,6 +63,7 @@ import {
|
||||||
BookmarkCategories,
|
BookmarkCategories,
|
||||||
BookmarkCategoryStatuses,
|
BookmarkCategoryStatuses,
|
||||||
FollowedTags,
|
FollowedTags,
|
||||||
|
LinkTimeline,
|
||||||
ListTimeline,
|
ListTimeline,
|
||||||
Blocks,
|
Blocks,
|
||||||
DomainBlocks,
|
DomainBlocks,
|
||||||
|
@ -217,6 +218,7 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
<WrappedRoute path='/public/local/fixed' exact component={CommunityTimeline} content={children} />
|
<WrappedRoute path='/public/local/fixed' exact component={CommunityTimeline} content={children} />
|
||||||
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
||||||
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
||||||
|
<WrappedRoute path='/links/:url' component={LinkTimeline} content={children} />
|
||||||
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
||||||
<WrappedRoute path='/antennasw/:id' component={AntennaSetting} content={children} />
|
<WrappedRoute path='/antennasw/:id' component={AntennaSetting} content={children} />
|
||||||
<WrappedRoute path='/antennast/:id' component={AntennaTimeline} content={children} />
|
<WrappedRoute path='/antennast/:id' component={AntennaTimeline} content={children} />
|
||||||
|
|
|
@ -269,3 +269,7 @@ export function NotificationRequests () {
|
||||||
export function NotificationRequest () {
|
export function NotificationRequest () {
|
||||||
return import(/*webpackChunkName: "features/notifications/request" */'../../notifications/request');
|
return import(/*webpackChunkName: "features/notifications/request" */'../../notifications/request');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function LinkTimeline () {
|
||||||
|
return import(/*webpackChunkName: "features/link_timeline" */'../../link_timeline');
|
||||||
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
"account.follow_back": "Heuliañ d'ho tro",
|
"account.follow_back": "Heuliañ d'ho tro",
|
||||||
"account.followers": "Tud koumanantet",
|
"account.followers": "Tud koumanantet",
|
||||||
"account.followers.empty": "Den na heul an implijer·ez-mañ c'hoazh.",
|
"account.followers.empty": "Den na heul an implijer·ez-mañ c'hoazh.",
|
||||||
|
"account.followers_counter": "{count, plural, one {{counter} heulier} two {{counter} heulier} few {{counter} heulier} many {{counter} heulier} other {{counter} heulier}}",
|
||||||
"account.following": "Koumanantoù",
|
"account.following": "Koumanantoù",
|
||||||
"account.follows.empty": "An implijer·ez-mañ na heul den ebet.",
|
"account.follows.empty": "An implijer·ez-mañ na heul den ebet.",
|
||||||
"account.go_to_profile": "Gwelet ar profil",
|
"account.go_to_profile": "Gwelet ar profil",
|
||||||
|
@ -60,6 +61,7 @@
|
||||||
"account.requested_follow": "Gant {name} eo bet goulennet ho heuliañ",
|
"account.requested_follow": "Gant {name} eo bet goulennet ho heuliañ",
|
||||||
"account.share": "Skignañ profil @{name}",
|
"account.share": "Skignañ profil @{name}",
|
||||||
"account.show_reblogs": "Diskouez skignadennoù @{name}",
|
"account.show_reblogs": "Diskouez skignadennoù @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, one {{counter} embannadur} two {{counter} embannadur} few {{counter} embannadur} many {{counter} embannadur} other {{counter} embannadur}}",
|
||||||
"account.unblock": "Diverzañ @{name}",
|
"account.unblock": "Diverzañ @{name}",
|
||||||
"account.unblock_domain": "Diverzañ an domani {domain}",
|
"account.unblock_domain": "Diverzañ an domani {domain}",
|
||||||
"account.unblock_short": "Distankañ",
|
"account.unblock_short": "Distankañ",
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
"account.block_short": "Zablokovat",
|
"account.block_short": "Zablokovat",
|
||||||
"account.blocked": "Blokovaný",
|
"account.blocked": "Blokovaný",
|
||||||
"account.browse_more_on_origin_server": "Více na původním profilu",
|
"account.browse_more_on_origin_server": "Více na původním profilu",
|
||||||
"account.cancel_follow_request": "Zrušit žádost o sledování",
|
"account.cancel_follow_request": "Zrušit sledování",
|
||||||
"account.copy": "Kopírovat odkaz na profil",
|
"account.copy": "Kopírovat odkaz na profil",
|
||||||
"account.direct": "Soukromě zmínit @{name}",
|
"account.direct": "Soukromě zmínit @{name}",
|
||||||
"account.disable_notifications": "Přestat mě upozorňovat, když @{name} zveřejní příspěvek",
|
"account.disable_notifications": "Přestat mě upozorňovat, když @{name} zveřejní příspěvek",
|
||||||
|
@ -35,7 +35,9 @@
|
||||||
"account.follow_back": "Také sledovat",
|
"account.follow_back": "Také sledovat",
|
||||||
"account.followers": "Sledující",
|
"account.followers": "Sledující",
|
||||||
"account.followers.empty": "Tohoto uživatele zatím nikdo nesleduje.",
|
"account.followers.empty": "Tohoto uživatele zatím nikdo nesleduje.",
|
||||||
|
"account.followers_counter": "{count, plural, one {{counter} sledující} few {{counter} sledující} many {{counter} sledujících} other {{counter} sledujících}}",
|
||||||
"account.following": "Sledujete",
|
"account.following": "Sledujete",
|
||||||
|
"account.following_counter": "{count, plural, one {{counter} sledovaný} few {{counter} sledovaní} many {{counter} sledovaných} other {{counter} sledovaných}}",
|
||||||
"account.follows.empty": "Tento uživatel zatím nikoho nesleduje.",
|
"account.follows.empty": "Tento uživatel zatím nikoho nesleduje.",
|
||||||
"account.go_to_profile": "Přejít na profil",
|
"account.go_to_profile": "Přejít na profil",
|
||||||
"account.hide_reblogs": "Skrýt boosty od @{name}",
|
"account.hide_reblogs": "Skrýt boosty od @{name}",
|
||||||
|
@ -61,6 +63,7 @@
|
||||||
"account.requested_follow": "{name} tě požádal o sledování",
|
"account.requested_follow": "{name} tě požádal o sledování",
|
||||||
"account.share": "Sdílet profil @{name}",
|
"account.share": "Sdílet profil @{name}",
|
||||||
"account.show_reblogs": "Zobrazit boosty od @{name}",
|
"account.show_reblogs": "Zobrazit boosty od @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, one {{counter} příspěvek} few {{counter} příspěvky} many {{counter} příspěvků} other {{counter} příspěvků}}",
|
||||||
"account.unblock": "Odblokovat @{name}",
|
"account.unblock": "Odblokovat @{name}",
|
||||||
"account.unblock_domain": "Odblokovat doménu {domain}",
|
"account.unblock_domain": "Odblokovat doménu {domain}",
|
||||||
"account.unblock_short": "Odblokovat",
|
"account.unblock_short": "Odblokovat",
|
||||||
|
@ -75,9 +78,9 @@
|
||||||
"admin.dashboard.retention.average": "Průměr",
|
"admin.dashboard.retention.average": "Průměr",
|
||||||
"admin.dashboard.retention.cohort": "Měsíc registrace",
|
"admin.dashboard.retention.cohort": "Měsíc registrace",
|
||||||
"admin.dashboard.retention.cohort_size": "Noví uživatelé",
|
"admin.dashboard.retention.cohort_size": "Noví uživatelé",
|
||||||
"admin.impact_report.instance_accounts": "Profily účtů, které by odstranily",
|
"admin.impact_report.instance_accounts": "Profily účtů, které by byli odstaněny",
|
||||||
"admin.impact_report.instance_followers": "Sledovatelé, o které by naši uživatelé přišli",
|
"admin.impact_report.instance_followers": "Sledující, o které by naši uživatelé přišli",
|
||||||
"admin.impact_report.instance_follows": "Následovníci jejich uživatelé by ztratili",
|
"admin.impact_report.instance_follows": "Sledující, o které by naši uživatelé přišli",
|
||||||
"admin.impact_report.title": "Shrnutí dopadu",
|
"admin.impact_report.title": "Shrnutí dopadu",
|
||||||
"alert.rate_limited.message": "Zkuste to prosím znovu po {retry_time, time, medium}.",
|
"alert.rate_limited.message": "Zkuste to prosím znovu po {retry_time, time, medium}.",
|
||||||
"alert.rate_limited.title": "Spojení omezena",
|
"alert.rate_limited.title": "Spojení omezena",
|
||||||
|
@ -86,7 +89,7 @@
|
||||||
"announcement.announcement": "Oznámení",
|
"announcement.announcement": "Oznámení",
|
||||||
"attachments_list.unprocessed": "(nezpracováno)",
|
"attachments_list.unprocessed": "(nezpracováno)",
|
||||||
"audio.hide": "Skrýt zvuk",
|
"audio.hide": "Skrýt zvuk",
|
||||||
"block_modal.remote_users_caveat": "Požádáme server {domain}, aby respektoval vaše rozhodnutí. Úplné dodržování nastavení však není zaručeno, protože některé servery mohou řešit blokování různě. Veřejné příspěvky mohou být stále viditelné pro nepřihlášené uživatele.",
|
"block_modal.remote_users_caveat": "Požádáme server {domain}, aby respektoval vaše rozhodnutí. Úplné dodržování nastavení však není zaručeno, protože některé servery mohou řešit blokování různě. Veřejné příspěvky mohou stále být viditelné pro nepřihlášené uživatele.",
|
||||||
"block_modal.show_less": "Zobrazit méně",
|
"block_modal.show_less": "Zobrazit méně",
|
||||||
"block_modal.show_more": "Zobrazit více",
|
"block_modal.show_more": "Zobrazit více",
|
||||||
"block_modal.they_cant_mention": "Nemůže vás zmiňovat ani sledovat.",
|
"block_modal.they_cant_mention": "Nemůže vás zmiňovat ani sledovat.",
|
||||||
|
@ -411,6 +414,8 @@
|
||||||
"limited_account_hint.action": "Přesto profil zobrazit",
|
"limited_account_hint.action": "Přesto profil zobrazit",
|
||||||
"limited_account_hint.title": "Tento profil byl skryt moderátory {domain}.",
|
"limited_account_hint.title": "Tento profil byl skryt moderátory {domain}.",
|
||||||
"link_preview.author": "Podle {name}",
|
"link_preview.author": "Podle {name}",
|
||||||
|
"link_preview.more_from_author": "Více od {name}",
|
||||||
|
"link_preview.shares": "{count, plural, one {{counter} příspěvek} few {{counter} příspěvky} many {{counter} příspěvků} other {{counter} příspěvků}}",
|
||||||
"lists.account.add": "Přidat do seznamu",
|
"lists.account.add": "Přidat do seznamu",
|
||||||
"lists.account.remove": "Odebrat ze seznamu",
|
"lists.account.remove": "Odebrat ze seznamu",
|
||||||
"lists.delete": "Smazat seznam",
|
"lists.delete": "Smazat seznam",
|
||||||
|
@ -691,8 +696,11 @@
|
||||||
"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:",
|
||||||
|
"server_banner.is_one_of_many": "{domain} je jedním z mnoha Mastodon serverů, které můžete použít k účasti na fediversu.",
|
||||||
"server_banner.server_stats": "Statistiky serveru:",
|
"server_banner.server_stats": "Statistiky serveru:",
|
||||||
"sign_in_banner.create_account": "Vytvořit účet",
|
"sign_in_banner.create_account": "Vytvořit účet",
|
||||||
|
"sign_in_banner.follow_anyone": "Sledujte kohokoli napříč fediversem a uvidíte vše v chronologickém pořadí. Bez algoritmů, reklam a clickbaitu.",
|
||||||
|
"sign_in_banner.mastodon_is": "Mastodon je ten nejlepší způsob, jak udržet krok s tím, co se právě děje.",
|
||||||
"sign_in_banner.sign_in": "Přihlásit se",
|
"sign_in_banner.sign_in": "Přihlásit se",
|
||||||
"sign_in_banner.sso_redirect": "Přihlášení nebo Registrace",
|
"sign_in_banner.sso_redirect": "Přihlášení nebo Registrace",
|
||||||
"status.admin_account": "Otevřít moderátorské rozhraní pro @{name}",
|
"status.admin_account": "Otevřít moderátorské rozhraní pro @{name}",
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
"account.follow_back": "Follow back",
|
"account.follow_back": "Follow back",
|
||||||
"account.followers": "Followers",
|
"account.followers": "Followers",
|
||||||
"account.followers.empty": "No one follows this user yet.",
|
"account.followers.empty": "No one follows this user yet.",
|
||||||
|
"account.followers_counter": "{count, plural, one {{counter} follower} other {{counter} followers}}",
|
||||||
"account.following": "Following",
|
"account.following": "Following",
|
||||||
|
"account.following_counter": "{count, plural, one {{counter} following} other {{counter} following}}",
|
||||||
"account.follows.empty": "This user doesn't follow anyone yet.",
|
"account.follows.empty": "This user doesn't follow anyone yet.",
|
||||||
"account.go_to_profile": "Go to profile",
|
"account.go_to_profile": "Go to profile",
|
||||||
"account.hide_reblogs": "Hide boosts from @{name}",
|
"account.hide_reblogs": "Hide boosts from @{name}",
|
||||||
|
@ -61,6 +63,7 @@
|
||||||
"account.requested_follow": "{name} has requested to follow you",
|
"account.requested_follow": "{name} has requested to follow you",
|
||||||
"account.share": "Share @{name}'s profile",
|
"account.share": "Share @{name}'s profile",
|
||||||
"account.show_reblogs": "Show boosts from @{name}",
|
"account.show_reblogs": "Show boosts from @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, one {{counter} post} other {{counter} posts}}",
|
||||||
"account.unblock": "Unblock @{name}",
|
"account.unblock": "Unblock @{name}",
|
||||||
"account.unblock_domain": "Unblock domain {domain}",
|
"account.unblock_domain": "Unblock domain {domain}",
|
||||||
"account.unblock_short": "Unblock",
|
"account.unblock_short": "Unblock",
|
||||||
|
@ -411,6 +414,8 @@
|
||||||
"limited_account_hint.action": "Show profile anyway",
|
"limited_account_hint.action": "Show profile anyway",
|
||||||
"limited_account_hint.title": "This profile has been hidden by the moderators of {domain}.",
|
"limited_account_hint.title": "This profile has been hidden by the moderators of {domain}.",
|
||||||
"link_preview.author": "By {name}",
|
"link_preview.author": "By {name}",
|
||||||
|
"link_preview.more_from_author": "More from {name}",
|
||||||
|
"link_preview.shares": "{count, plural, one {{counter} post} other {{counter} posts}}",
|
||||||
"lists.account.add": "Add to list",
|
"lists.account.add": "Add to list",
|
||||||
"lists.account.remove": "Remove from list",
|
"lists.account.remove": "Remove from list",
|
||||||
"lists.delete": "Delete list",
|
"lists.delete": "Delete list",
|
||||||
|
@ -691,8 +696,11 @@
|
||||||
"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:",
|
||||||
|
"server_banner.is_one_of_many": "{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.",
|
||||||
"server_banner.server_stats": "Server stats:",
|
"server_banner.server_stats": "Server stats:",
|
||||||
"sign_in_banner.create_account": "Create account",
|
"sign_in_banner.create_account": "Create account",
|
||||||
|
"sign_in_banner.follow_anyone": "Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.",
|
||||||
|
"sign_in_banner.mastodon_is": "Mastodon is the best way to keep up with what's happening.",
|
||||||
"sign_in_banner.sign_in": "Sign in",
|
"sign_in_banner.sign_in": "Sign in",
|
||||||
"sign_in_banner.sso_redirect": "Login or Register",
|
"sign_in_banner.sso_redirect": "Login or Register",
|
||||||
"status.admin_account": "Open moderation interface for @{name}",
|
"status.admin_account": "Open moderation interface for @{name}",
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
"account.follow_back": "Fylg aftur",
|
"account.follow_back": "Fylg aftur",
|
||||||
"account.followers": "Fylgjarar",
|
"account.followers": "Fylgjarar",
|
||||||
"account.followers.empty": "Ongar fylgjarar enn.",
|
"account.followers.empty": "Ongar fylgjarar enn.",
|
||||||
|
"account.followers_counter": "{count, plural, one {{counter} fylgjari} other {{counter} fylgjarar}}",
|
||||||
"account.following": "Fylgir",
|
"account.following": "Fylgir",
|
||||||
|
"account.following_counter": "{count, plural, one {{counter} fylgir} other {{counter} fylgja}}",
|
||||||
"account.follows.empty": "Hesin brúkari fylgir ongum enn.",
|
"account.follows.empty": "Hesin brúkari fylgir ongum enn.",
|
||||||
"account.go_to_profile": "Far til vanga",
|
"account.go_to_profile": "Far til vanga",
|
||||||
"account.hide_reblogs": "Fjal lyft frá @{name}",
|
"account.hide_reblogs": "Fjal lyft frá @{name}",
|
||||||
|
@ -61,6 +63,7 @@
|
||||||
"account.requested_follow": "{name} hevur biðið um at fylgja tær",
|
"account.requested_follow": "{name} hevur biðið um at fylgja tær",
|
||||||
"account.share": "Deil vanga @{name}'s",
|
"account.share": "Deil vanga @{name}'s",
|
||||||
"account.show_reblogs": "Vís lyft frá @{name}",
|
"account.show_reblogs": "Vís lyft frá @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, one {{counter} postur} other {{counter} postar}}",
|
||||||
"account.unblock": "Banna ikki @{name}",
|
"account.unblock": "Banna ikki @{name}",
|
||||||
"account.unblock_domain": "Banna ikki økisnavnið {domain}",
|
"account.unblock_domain": "Banna ikki økisnavnið {domain}",
|
||||||
"account.unblock_short": "Banna ikki",
|
"account.unblock_short": "Banna ikki",
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
"account.follow_back": "Seguir tamén",
|
"account.follow_back": "Seguir tamén",
|
||||||
"account.followers": "Seguidoras",
|
"account.followers": "Seguidoras",
|
||||||
"account.followers.empty": "Aínda ninguén segue esta usuaria.",
|
"account.followers.empty": "Aínda ninguén segue esta usuaria.",
|
||||||
|
"account.followers_counter": "{count, plural, one {{counter} seguidora} other {{counter} seguidoras}}",
|
||||||
"account.following": "Seguindo",
|
"account.following": "Seguindo",
|
||||||
|
"account.following_counter": "{count, plural, one {{counter} seguimento} other {{counter} seguimentos}}",
|
||||||
"account.follows.empty": "Esta usuaria aínda non segue a ninguén.",
|
"account.follows.empty": "Esta usuaria aínda non segue a ninguén.",
|
||||||
"account.go_to_profile": "Ir ao perfil",
|
"account.go_to_profile": "Ir ao perfil",
|
||||||
"account.hide_reblogs": "Agochar promocións de @{name}",
|
"account.hide_reblogs": "Agochar promocións de @{name}",
|
||||||
|
@ -61,6 +63,7 @@
|
||||||
"account.requested_follow": "{name} solicitou seguirte",
|
"account.requested_follow": "{name} solicitou seguirte",
|
||||||
"account.share": "Compartir o perfil de @{name}",
|
"account.share": "Compartir o perfil de @{name}",
|
||||||
"account.show_reblogs": "Amosar compartidos de @{name}",
|
"account.show_reblogs": "Amosar compartidos de @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, one {{counter} publicación} other {{counter} publicacións}}",
|
||||||
"account.unblock": "Desbloquear @{name}",
|
"account.unblock": "Desbloquear @{name}",
|
||||||
"account.unblock_domain": "Amosar {domain}",
|
"account.unblock_domain": "Amosar {domain}",
|
||||||
"account.unblock_short": "Desbloquear",
|
"account.unblock_short": "Desbloquear",
|
||||||
|
@ -221,7 +224,7 @@
|
||||||
"domain_pill.their_server": "O seu fogar dixital, onde están as súas publicacións.",
|
"domain_pill.their_server": "O seu fogar dixital, onde están as súas publicacións.",
|
||||||
"domain_pill.their_username": "O seu identificador único no seu servidor. É posible atopar usuarias co mesmo nome de usuaria en diferentes servidores.",
|
"domain_pill.their_username": "O seu identificador único no seu servidor. É posible atopar usuarias co mesmo nome de usuaria en diferentes servidores.",
|
||||||
"domain_pill.username": "Nome de usuaria",
|
"domain_pill.username": "Nome de usuaria",
|
||||||
"domain_pill.whats_in_a_handle": "Que é o alcume?",
|
"domain_pill.whats_in_a_handle": "As partes do alcume?",
|
||||||
"domain_pill.who_they_are": "O alcume dinos quen é esa persoa e onde está, para que poidas interactuar con ela en toda a web social de <button>plataformas ActivityPub</button>.",
|
"domain_pill.who_they_are": "O alcume dinos quen é esa persoa e onde está, para que poidas interactuar con ela en toda a web social de <button>plataformas ActivityPub</button>.",
|
||||||
"domain_pill.who_you_are": "Como o teu alcume informa de quen es e onde estás, as persoas poden interactuar contigo desde toda a web social de <button>plataformas ActivityPub</button>.",
|
"domain_pill.who_you_are": "Como o teu alcume informa de quen es e onde estás, as persoas poden interactuar contigo desde toda a web social de <button>plataformas ActivityPub</button>.",
|
||||||
"domain_pill.your_handle": "O teu alcume:",
|
"domain_pill.your_handle": "O teu alcume:",
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
"account.follow_back": "לעקוב בחזרה",
|
"account.follow_back": "לעקוב בחזרה",
|
||||||
"account.followers": "עוקבים",
|
"account.followers": "עוקבים",
|
||||||
"account.followers.empty": "אף אחד לא עוקב אחר המשתמש הזה עדיין.",
|
"account.followers.empty": "אף אחד לא עוקב אחר המשתמש הזה עדיין.",
|
||||||
|
"account.followers_counter": "{count, plural,one {עוקב אחד} other {{count} עוקבים}}",
|
||||||
"account.following": "נעקבים",
|
"account.following": "נעקבים",
|
||||||
|
"account.following_counter": "{count, plural,one {עוקב אחרי {count}}other {עוקב אחרי {count}}}",
|
||||||
"account.follows.empty": "משתמש זה עדיין לא עוקב אחרי אף אחד.",
|
"account.follows.empty": "משתמש זה עדיין לא עוקב אחרי אף אחד.",
|
||||||
"account.go_to_profile": "מעבר לפרופיל",
|
"account.go_to_profile": "מעבר לפרופיל",
|
||||||
"account.hide_reblogs": "להסתיר הידהודים מאת @{name}",
|
"account.hide_reblogs": "להסתיר הידהודים מאת @{name}",
|
||||||
|
@ -61,6 +63,7 @@
|
||||||
"account.requested_follow": "{name} ביקשו לעקוב אחריך",
|
"account.requested_follow": "{name} ביקשו לעקוב אחריך",
|
||||||
"account.share": "שתף את הפרופיל של @{name}",
|
"account.share": "שתף את הפרופיל של @{name}",
|
||||||
"account.show_reblogs": "הצג הדהודים מאת @{name}",
|
"account.show_reblogs": "הצג הדהודים מאת @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, one {הודעה אחת} two {הודעותיים} many {{count} הודעות} other {{count} הודעות}}",
|
||||||
"account.unblock": "להסיר חסימה ל- @{name}",
|
"account.unblock": "להסיר חסימה ל- @{name}",
|
||||||
"account.unblock_domain": "הסירי את החסימה של קהילת {domain}",
|
"account.unblock_domain": "הסירי את החסימה של קהילת {domain}",
|
||||||
"account.unblock_short": "הסר חסימה",
|
"account.unblock_short": "הסר חסימה",
|
||||||
|
@ -693,8 +696,11 @@
|
||||||
"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": "מנוהל ע\"י:",
|
||||||
|
"server_banner.is_one_of_many": "{domain} הוא שרת אחד משרתי מסטודון עצמאיים רבים שדרגם תוכלו להשתתף בפדיוורס (רשת חברתית מבוזרת).",
|
||||||
"server_banner.server_stats": "סטטיסטיקות שרת:",
|
"server_banner.server_stats": "סטטיסטיקות שרת:",
|
||||||
"sign_in_banner.create_account": "יצירת חשבון",
|
"sign_in_banner.create_account": "יצירת חשבון",
|
||||||
|
"sign_in_banner.follow_anyone": "תוכלו לעקוב אחרי כל משמתמש בפדיוורס ולקרוא הכל לפי סדר הפרסום בציר הזמן. אין אלגוריתמים, פרסומות, או קליקבייט מטעם בעלי הרשת.",
|
||||||
|
"sign_in_banner.mastodon_is": "מסטודון הוא הדרך הטובה ביותר לעקוב אחרי מה שקורה.",
|
||||||
"sign_in_banner.sign_in": "התחברות",
|
"sign_in_banner.sign_in": "התחברות",
|
||||||
"sign_in_banner.sso_redirect": "התחברות/הרשמה",
|
"sign_in_banner.sso_redirect": "התחברות/הרשמה",
|
||||||
"status.admin_account": "פתח/י ממשק ניהול עבור @{name}",
|
"status.admin_account": "פתח/י ממשק ניהול עבור @{name}",
|
||||||
|
@ -771,7 +777,7 @@
|
||||||
"timeline_hint.resources.followers": "עוקבים",
|
"timeline_hint.resources.followers": "עוקבים",
|
||||||
"timeline_hint.resources.follows": "נעקבים",
|
"timeline_hint.resources.follows": "נעקבים",
|
||||||
"timeline_hint.resources.statuses": "הודעות ישנות יותר",
|
"timeline_hint.resources.statuses": "הודעות ישנות יותר",
|
||||||
"trends.counter_by_accounts": "{count, plural, one {אדם {count}} other {{count} א.נשים}} {days, plural, one {מאז אתמול} two {ביומיים האחרונים} other {במשך {days} הימים האחרונים}}",
|
"trends.counter_by_accounts": "{count, plural, one {אדם אחד} other {{count} א.נשים}} {days, plural, one {מאז אתמול} two {ביומיים האחרונים} other {במשך {days} הימים האחרונים}}",
|
||||||
"trends.trending_now": "נושאים חמים",
|
"trends.trending_now": "נושאים חמים",
|
||||||
"ui.beforeunload": "הטיוטא תאבד אם תעזבו את מסטודון.",
|
"ui.beforeunload": "הטיוטא תאבד אם תעזבו את מסטודון.",
|
||||||
"units.short.billion": "{count} מליארד",
|
"units.short.billion": "{count} מליארד",
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
"account.follow_back": "Viszontkövetés",
|
"account.follow_back": "Viszontkövetés",
|
||||||
"account.followers": "Követő",
|
"account.followers": "Követő",
|
||||||
"account.followers.empty": "Ezt a felhasználót még senki sem követi.",
|
"account.followers.empty": "Ezt a felhasználót még senki sem követi.",
|
||||||
|
"account.followers_counter": "{count, plural, one {{counter} követő} other {{counter} követő}}",
|
||||||
"account.following": "Követve",
|
"account.following": "Követve",
|
||||||
|
"account.following_counter": "{count, plural, one {{counter} követett} other {{counter} követett}}",
|
||||||
"account.follows.empty": "Ez a felhasználó még senkit sem követ.",
|
"account.follows.empty": "Ez a felhasználó még senkit sem követ.",
|
||||||
"account.go_to_profile": "Ugrás a profilhoz",
|
"account.go_to_profile": "Ugrás a profilhoz",
|
||||||
"account.hide_reblogs": "@{name} megtolásainak elrejtése",
|
"account.hide_reblogs": "@{name} megtolásainak elrejtése",
|
||||||
|
@ -61,6 +63,7 @@
|
||||||
"account.requested_follow": "{name} kérte, hogy követhessen",
|
"account.requested_follow": "{name} kérte, hogy követhessen",
|
||||||
"account.share": "@{name} profiljának megosztása",
|
"account.share": "@{name} profiljának megosztása",
|
||||||
"account.show_reblogs": "@{name} megtolásainak mutatása",
|
"account.show_reblogs": "@{name} megtolásainak mutatása",
|
||||||
|
"account.statuses_counter": "{count, plural, one {{counter} bejegyzés} other {{counter} bejegyzés}}",
|
||||||
"account.unblock": "@{name} letiltásának feloldása",
|
"account.unblock": "@{name} letiltásának feloldása",
|
||||||
"account.unblock_domain": "{domain} domain tiltásának feloldása",
|
"account.unblock_domain": "{domain} domain tiltásának feloldása",
|
||||||
"account.unblock_short": "Tiltás feloldása",
|
"account.unblock_short": "Tiltás feloldása",
|
||||||
|
|
|
@ -354,7 +354,7 @@
|
||||||
"home.pending_critical_update.link": "Vider actualisationes",
|
"home.pending_critical_update.link": "Vider actualisationes",
|
||||||
"home.pending_critical_update.title": "Actualisation de securitate critic disponibile!",
|
"home.pending_critical_update.title": "Actualisation de securitate critic disponibile!",
|
||||||
"home.show_announcements": "Monstrar annuncios",
|
"home.show_announcements": "Monstrar annuncios",
|
||||||
"interaction_modal.description.favourite": "Con un conto sur Mastodon, tu pote marcar iste message como favorite pro informar le autor que tu lo apprecia e salveguarda pro plus tarde.",
|
"interaction_modal.description.favourite": "Con un conto sur Mastodon, tu pote marcar iste message como favorite pro informar le autor que tu lo apprecia e lo salva pro plus tarde.",
|
||||||
"interaction_modal.description.follow": "Con un conto sur Mastodon, tu pote sequer {name} e reciper su messages in tu fluxo de initio.",
|
"interaction_modal.description.follow": "Con un conto sur Mastodon, tu pote sequer {name} e reciper su messages in tu fluxo de initio.",
|
||||||
"interaction_modal.description.reblog": "Con un conto sur Mastodon, tu pote impulsar iste message pro condivider lo con tu proprie sequitores.",
|
"interaction_modal.description.reblog": "Con un conto sur Mastodon, tu pote impulsar iste message pro condivider lo con tu proprie sequitores.",
|
||||||
"interaction_modal.description.reply": "Con un conto sur Mastodon, tu pote responder a iste message.",
|
"interaction_modal.description.reply": "Con un conto sur Mastodon, tu pote responder a iste message.",
|
||||||
|
@ -764,7 +764,7 @@
|
||||||
"status.unmute_conversation": "Non plus silentiar conversation",
|
"status.unmute_conversation": "Non plus silentiar conversation",
|
||||||
"status.unpin": "Disfixar del profilo",
|
"status.unpin": "Disfixar del profilo",
|
||||||
"subscribed_languages.lead": "Solmente le messages in le linguas seligite apparera in tu chronologias de initio e de listas post le cambiamento. Selige necun pro reciper messages in tote le linguas.",
|
"subscribed_languages.lead": "Solmente le messages in le linguas seligite apparera in tu chronologias de initio e de listas post le cambiamento. Selige necun pro reciper messages in tote le linguas.",
|
||||||
"subscribed_languages.save": "Salveguardar le cambiamentos",
|
"subscribed_languages.save": "Salvar le cambiamentos",
|
||||||
"subscribed_languages.target": "Cambiar le linguas subscribite pro {target}",
|
"subscribed_languages.target": "Cambiar le linguas subscribite pro {target}",
|
||||||
"tabs_bar.home": "Initio",
|
"tabs_bar.home": "Initio",
|
||||||
"tabs_bar.notifications": "Notificationes",
|
"tabs_bar.notifications": "Notificationes",
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
"account.follow_back": "맞팔로우 하기",
|
"account.follow_back": "맞팔로우 하기",
|
||||||
"account.followers": "팔로워",
|
"account.followers": "팔로워",
|
||||||
"account.followers.empty": "아직 아무도 이 사용자를 팔로우하고 있지 않습니다.",
|
"account.followers.empty": "아직 아무도 이 사용자를 팔로우하고 있지 않습니다.",
|
||||||
|
"account.followers_counter": "{count, plural, other {{counter} 팔로워}}",
|
||||||
"account.following": "팔로잉",
|
"account.following": "팔로잉",
|
||||||
|
"account.following_counter": "{count, plural, other {{counter} 팔로잉}}",
|
||||||
"account.follows.empty": "이 사용자는 아직 아무도 팔로우하고 있지 않습니다.",
|
"account.follows.empty": "이 사용자는 아직 아무도 팔로우하고 있지 않습니다.",
|
||||||
"account.go_to_profile": "프로필로 이동",
|
"account.go_to_profile": "프로필로 이동",
|
||||||
"account.hide_reblogs": "@{name}의 부스트를 숨기기",
|
"account.hide_reblogs": "@{name}의 부스트를 숨기기",
|
||||||
|
@ -61,6 +63,7 @@
|
||||||
"account.requested_follow": "{name} 님이 팔로우 요청을 보냈습니다",
|
"account.requested_follow": "{name} 님이 팔로우 요청을 보냈습니다",
|
||||||
"account.share": "@{name}의 프로필 공유",
|
"account.share": "@{name}의 프로필 공유",
|
||||||
"account.show_reblogs": "@{name}의 부스트 보기",
|
"account.show_reblogs": "@{name}의 부스트 보기",
|
||||||
|
"account.statuses_counter": "{count, plural, other {{counter} 게시물}}",
|
||||||
"account.unblock": "차단 해제",
|
"account.unblock": "차단 해제",
|
||||||
"account.unblock_domain": "도메인 {domain} 차단 해제",
|
"account.unblock_domain": "도메인 {domain} 차단 해제",
|
||||||
"account.unblock_short": "차단 해제",
|
"account.unblock_short": "차단 해제",
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
"account.follow_back": "Sekti atgal",
|
"account.follow_back": "Sekti atgal",
|
||||||
"account.followers": "Sekėjai",
|
"account.followers": "Sekėjai",
|
||||||
"account.followers.empty": "Šio naudotojo dar niekas neseka.",
|
"account.followers.empty": "Šio naudotojo dar niekas neseka.",
|
||||||
|
"account.followers_counter": "{count, plural, one {{counter} sekėjas} few {{counter} sekėjai} many {{counter} sekėjo} other {{counter} sekėjų}}",
|
||||||
"account.following": "Sekama",
|
"account.following": "Sekama",
|
||||||
|
"account.following_counter": "{count, plural, one {{counter} sekimas} few {{counter} sekimai} many {{counter} sekimo} other {{counter} sekimų}}",
|
||||||
"account.follows.empty": "Šis naudotojas dar nieko neseka.",
|
"account.follows.empty": "Šis naudotojas dar nieko neseka.",
|
||||||
"account.go_to_profile": "Eiti į profilį",
|
"account.go_to_profile": "Eiti į profilį",
|
||||||
"account.hide_reblogs": "Slėpti pakėlimus iš @{name}",
|
"account.hide_reblogs": "Slėpti pakėlimus iš @{name}",
|
||||||
|
@ -61,6 +63,7 @@
|
||||||
"account.requested_follow": "{name} paprašė tave sekti",
|
"account.requested_follow": "{name} paprašė tave sekti",
|
||||||
"account.share": "Bendrinti @{name} profilį",
|
"account.share": "Bendrinti @{name} profilį",
|
||||||
"account.show_reblogs": "Rodyti pakėlimus iš @{name}",
|
"account.show_reblogs": "Rodyti pakėlimus iš @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, one {{counter} įrašas} few {{counter} įrašai} many {{counter} įrašo} other {{counter} įrašų}}",
|
||||||
"account.unblock": "Atblokuoti @{name}",
|
"account.unblock": "Atblokuoti @{name}",
|
||||||
"account.unblock_domain": "Atblokuoti domeną {domain}",
|
"account.unblock_domain": "Atblokuoti domeną {domain}",
|
||||||
"account.unblock_short": "Atblokuoti",
|
"account.unblock_short": "Atblokuoti",
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
"account.follow_back": "Fylg tilbake",
|
"account.follow_back": "Fylg tilbake",
|
||||||
"account.followers": "Fylgjarar",
|
"account.followers": "Fylgjarar",
|
||||||
"account.followers.empty": "Ingen fylgjer denne brukaren enno.",
|
"account.followers.empty": "Ingen fylgjer denne brukaren enno.",
|
||||||
|
"account.followers_counter": "{count, plural, one {{counter} følgjar} other {{counter} følgjarar}}",
|
||||||
"account.following": "Fylgjer",
|
"account.following": "Fylgjer",
|
||||||
|
"account.following_counter": "{count, plural, one {{counter} følgjer} other {{counter} følgjer}}",
|
||||||
"account.follows.empty": "Denne brukaren fylgjer ikkje nokon enno.",
|
"account.follows.empty": "Denne brukaren fylgjer ikkje nokon enno.",
|
||||||
"account.go_to_profile": "Gå til profil",
|
"account.go_to_profile": "Gå til profil",
|
||||||
"account.hide_reblogs": "Gøym framhevingar frå @{name}",
|
"account.hide_reblogs": "Gøym framhevingar frå @{name}",
|
||||||
|
@ -61,6 +63,7 @@
|
||||||
"account.requested_follow": "{name} har bedt om å få fylgja deg",
|
"account.requested_follow": "{name} har bedt om å få fylgja deg",
|
||||||
"account.share": "Del @{name} sin profil",
|
"account.share": "Del @{name} sin profil",
|
||||||
"account.show_reblogs": "Vis framhevingar frå @{name}",
|
"account.show_reblogs": "Vis framhevingar frå @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, one {{counter} innlegg} other {{counter} innlegg}}",
|
||||||
"account.unblock": "Stopp blokkering av @{name}",
|
"account.unblock": "Stopp blokkering av @{name}",
|
||||||
"account.unblock_domain": "Stopp blokkering av domenet {domain}",
|
"account.unblock_domain": "Stopp blokkering av domenet {domain}",
|
||||||
"account.unblock_short": "Stopp blokkering",
|
"account.unblock_short": "Stopp blokkering",
|
||||||
|
@ -693,8 +696,11 @@
|
||||||
"server_banner.about_active_users": "Personar som har brukt denne tenaren dei siste 30 dagane (Månadlege Aktive Brukarar)",
|
"server_banner.about_active_users": "Personar som har brukt denne tenaren dei siste 30 dagane (Månadlege Aktive Brukarar)",
|
||||||
"server_banner.active_users": "aktive brukarar",
|
"server_banner.active_users": "aktive brukarar",
|
||||||
"server_banner.administered_by": "Administrert av:",
|
"server_banner.administered_by": "Administrert av:",
|
||||||
|
"server_banner.is_one_of_many": "{domain} er ein av dei mange uavhengige Mastodon-serverane du kan bruke til å delta i Fødiverset.",
|
||||||
"server_banner.server_stats": "Tenarstatistikk:",
|
"server_banner.server_stats": "Tenarstatistikk:",
|
||||||
"sign_in_banner.create_account": "Opprett konto",
|
"sign_in_banner.create_account": "Opprett konto",
|
||||||
|
"sign_in_banner.follow_anyone": "Følg kven som helst på tvers av Fødiverset og sjå alt i kronologisk rekkjefølgje. Ingen algoritmar, reklamar eller clickbait i sikte.",
|
||||||
|
"sign_in_banner.mastodon_is": "Mastodon er den beste måten å følgje med på det som skjer.",
|
||||||
"sign_in_banner.sign_in": "Logg inn",
|
"sign_in_banner.sign_in": "Logg inn",
|
||||||
"sign_in_banner.sso_redirect": "Logg inn eller registrer deg",
|
"sign_in_banner.sso_redirect": "Logg inn eller registrer deg",
|
||||||
"status.admin_account": "Opne moderasjonsgrensesnitt for @{name}",
|
"status.admin_account": "Opne moderasjonsgrensesnitt for @{name}",
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
"account.follow_back": "Obserwuj wzajemnie",
|
"account.follow_back": "Obserwuj wzajemnie",
|
||||||
"account.followers": "Obserwujący",
|
"account.followers": "Obserwujący",
|
||||||
"account.followers.empty": "Nikt jeszcze nie obserwuje tego użytkownika.",
|
"account.followers.empty": "Nikt jeszcze nie obserwuje tego użytkownika.",
|
||||||
|
"account.followers_counter": "{count, plural, one {{counter} obserwujący} few {{counter} obserwujących} many {{counter} obserwujących} other {{counter} obserwujących}}",
|
||||||
"account.following": "Obserwowani",
|
"account.following": "Obserwowani",
|
||||||
|
"account.following_counter": "{count, plural, one {{counter} obserwowany} few {{counter} obserwowanych} many {{counter} obserwowanych} other {{counter} obserwowanych}}",
|
||||||
"account.follows.empty": "Ten użytkownik nie obserwuje jeszcze nikogo.",
|
"account.follows.empty": "Ten użytkownik nie obserwuje jeszcze nikogo.",
|
||||||
"account.go_to_profile": "Przejdź do profilu",
|
"account.go_to_profile": "Przejdź do profilu",
|
||||||
"account.hide_reblogs": "Ukryj podbicia od @{name}",
|
"account.hide_reblogs": "Ukryj podbicia od @{name}",
|
||||||
|
@ -61,6 +63,7 @@
|
||||||
"account.requested_follow": "{name} chce zaobserwować twój profil",
|
"account.requested_follow": "{name} chce zaobserwować twój profil",
|
||||||
"account.share": "Udostępnij profil @{name}",
|
"account.share": "Udostępnij profil @{name}",
|
||||||
"account.show_reblogs": "Pokazuj podbicia od @{name}",
|
"account.show_reblogs": "Pokazuj podbicia od @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, one {{counter} wpis} few {{counter} wpisy} many {{counter} wpisów} other {{counter} wpisów}}",
|
||||||
"account.unblock": "Odblokuj @{name}",
|
"account.unblock": "Odblokuj @{name}",
|
||||||
"account.unblock_domain": "Odblokuj domenę {domain}",
|
"account.unblock_domain": "Odblokuj domenę {domain}",
|
||||||
"account.unblock_short": "Odblokuj",
|
"account.unblock_short": "Odblokuj",
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
"account.follow_back": "Seguir de volta",
|
"account.follow_back": "Seguir de volta",
|
||||||
"account.followers": "Seguidores",
|
"account.followers": "Seguidores",
|
||||||
"account.followers.empty": "Ainda ninguém segue este utilizador.",
|
"account.followers.empty": "Ainda ninguém segue este utilizador.",
|
||||||
|
"account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}",
|
||||||
"account.following": "A seguir",
|
"account.following": "A seguir",
|
||||||
|
"account.following_counter": "{count, plural, one {A seguir {counter}} other {A seguir {counter}}}",
|
||||||
"account.follows.empty": "Este utilizador ainda não segue ninguém.",
|
"account.follows.empty": "Este utilizador ainda não segue ninguém.",
|
||||||
"account.go_to_profile": "Ir para o perfil",
|
"account.go_to_profile": "Ir para o perfil",
|
||||||
"account.hide_reblogs": "Esconder partilhas de @{name}",
|
"account.hide_reblogs": "Esconder partilhas de @{name}",
|
||||||
|
@ -61,6 +63,7 @@
|
||||||
"account.requested_follow": "{name} pediu para segui-lo",
|
"account.requested_follow": "{name} pediu para segui-lo",
|
||||||
"account.share": "Partilhar o perfil @{name}",
|
"account.share": "Partilhar o perfil @{name}",
|
||||||
"account.show_reblogs": "Mostrar partilhas de @{name}",
|
"account.show_reblogs": "Mostrar partilhas de @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, one {{counter} publicação} other {{counter} publicações}}",
|
||||||
"account.unblock": "Desbloquear @{name}",
|
"account.unblock": "Desbloquear @{name}",
|
||||||
"account.unblock_domain": "Desbloquear o domínio {domain}",
|
"account.unblock_domain": "Desbloquear o domínio {domain}",
|
||||||
"account.unblock_short": "Desbloquear",
|
"account.unblock_short": "Desbloquear",
|
||||||
|
|
|
@ -88,7 +88,10 @@
|
||||||
"audio.hide": "Skryť zvuk",
|
"audio.hide": "Skryť zvuk",
|
||||||
"block_modal.show_less": "Zobraziť menej",
|
"block_modal.show_less": "Zobraziť menej",
|
||||||
"block_modal.show_more": "Zobraziť viac",
|
"block_modal.show_more": "Zobraziť viac",
|
||||||
|
"block_modal.they_cant_mention": "Nemôžu ťa spomenúť, alebo nasledovať.",
|
||||||
|
"block_modal.they_will_know": "Môžu vidieť, že sú zablokovaní/ý.",
|
||||||
"block_modal.title": "Blokovať užívateľa?",
|
"block_modal.title": "Blokovať užívateľa?",
|
||||||
|
"block_modal.you_wont_see_mentions": "Neuvidíš príspevky, ktoré ich spomínajú.",
|
||||||
"boost_modal.combo": "Nabudúce môžete preskočiť stlačením {combo}",
|
"boost_modal.combo": "Nabudúce môžete preskočiť stlačením {combo}",
|
||||||
"bundle_column_error.copy_stacktrace": "Kopírovať chybovú hlášku",
|
"bundle_column_error.copy_stacktrace": "Kopírovať chybovú hlášku",
|
||||||
"bundle_column_error.error.body": "Požadovanú stránku nebolo možné vykresliť. Môže to byť spôsobené chybou v našom kóde alebo problémom s kompatibilitou prehliadača.",
|
"bundle_column_error.error.body": "Požadovanú stránku nebolo možné vykresliť. Môže to byť spôsobené chybou v našom kóde alebo problémom s kompatibilitou prehliadača.",
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
"account.follow_back": "Ndiqe gjithashtu",
|
"account.follow_back": "Ndiqe gjithashtu",
|
||||||
"account.followers": "Ndjekës",
|
"account.followers": "Ndjekës",
|
||||||
"account.followers.empty": "Këtë përdorues ende s’e ndjek kush.",
|
"account.followers.empty": "Këtë përdorues ende s’e ndjek kush.",
|
||||||
|
"account.followers_counter": "{count, plural, one {{counter} ndjekës} other {{counter} ndjekës}}",
|
||||||
"account.following": "Ndjekje",
|
"account.following": "Ndjekje",
|
||||||
|
"account.following_counter": "{count, plural, one {{counter} i ndjekur} other {{counter} të ndjekur}}",
|
||||||
"account.follows.empty": "Ky përdorues ende s’ndjek kënd.",
|
"account.follows.empty": "Ky përdorues ende s’ndjek kënd.",
|
||||||
"account.go_to_profile": "Kalo te profili",
|
"account.go_to_profile": "Kalo te profili",
|
||||||
"account.hide_reblogs": "Fshih përforcime nga @{name}",
|
"account.hide_reblogs": "Fshih përforcime nga @{name}",
|
||||||
|
@ -61,6 +63,7 @@
|
||||||
"account.requested_follow": "{name} ka kërkuar t’ju ndjekë",
|
"account.requested_follow": "{name} ka kërkuar t’ju ndjekë",
|
||||||
"account.share": "Ndajeni profilin e @{name} me të tjerët",
|
"account.share": "Ndajeni profilin e @{name} me të tjerët",
|
||||||
"account.show_reblogs": "Shfaq përforcime nga @{name}",
|
"account.show_reblogs": "Shfaq përforcime nga @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, one {{counter} postim} other {{counter} postime}}",
|
||||||
"account.unblock": "Zhbllokoje @{name}",
|
"account.unblock": "Zhbllokoje @{name}",
|
||||||
"account.unblock_domain": "Zhblloko përkatësinë {domain}",
|
"account.unblock_domain": "Zhblloko përkatësinë {domain}",
|
||||||
"account.unblock_short": "Zhbllokoje",
|
"account.unblock_short": "Zhbllokoje",
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
"account.follow_back": "Uzvrati praćenje",
|
"account.follow_back": "Uzvrati praćenje",
|
||||||
"account.followers": "Pratioci",
|
"account.followers": "Pratioci",
|
||||||
"account.followers.empty": "Još uvek niko ne prati ovog korisnika.",
|
"account.followers.empty": "Još uvek niko ne prati ovog korisnika.",
|
||||||
|
"account.followers_counter": "{count, plural, one {{counter} pratilac} few {{counter} pratioca} other {{counter} pratilaca}}",
|
||||||
"account.following": "Prati",
|
"account.following": "Prati",
|
||||||
|
"account.following_counter": "{count, plural, one {{counter} prati} few {{counter} prati} other {{counter} prati}}",
|
||||||
"account.follows.empty": "Ovaj korisnik još uvek nikog ne prati.",
|
"account.follows.empty": "Ovaj korisnik još uvek nikog ne prati.",
|
||||||
"account.go_to_profile": "Idi na profil",
|
"account.go_to_profile": "Idi na profil",
|
||||||
"account.hide_reblogs": "Sakrij podržavanja @{name}",
|
"account.hide_reblogs": "Sakrij podržavanja @{name}",
|
||||||
|
@ -61,6 +63,7 @@
|
||||||
"account.requested_follow": "{name} je zatražio da vas prati",
|
"account.requested_follow": "{name} je zatražio da vas prati",
|
||||||
"account.share": "Podeli profil korisnika @{name}",
|
"account.share": "Podeli profil korisnika @{name}",
|
||||||
"account.show_reblogs": "Prikaži podržavanja od korisnika @{name}",
|
"account.show_reblogs": "Prikaži podržavanja od korisnika @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, one {{counter} objava} few {{counter} objave} other {{counter} objava}}",
|
||||||
"account.unblock": "Odblokiraj korisnika @{name}",
|
"account.unblock": "Odblokiraj korisnika @{name}",
|
||||||
"account.unblock_domain": "Odblokiraj domen {domain}",
|
"account.unblock_domain": "Odblokiraj domen {domain}",
|
||||||
"account.unblock_short": "Odblokiraj",
|
"account.unblock_short": "Odblokiraj",
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
"account.follow_back": "Узврати праћење",
|
"account.follow_back": "Узврати праћење",
|
||||||
"account.followers": "Пратиоци",
|
"account.followers": "Пратиоци",
|
||||||
"account.followers.empty": "Још увек нико не прати овог корисника.",
|
"account.followers.empty": "Још увек нико не прати овог корисника.",
|
||||||
|
"account.followers_counter": "{count, plural, one {{counter} пратилац} few {{counter} пратиоца} other {{counter} пратилаца}}",
|
||||||
"account.following": "Прати",
|
"account.following": "Прати",
|
||||||
|
"account.following_counter": "{count, plural, one {{counter} прати} few {{counter} прати} other {{counter} прати}}",
|
||||||
"account.follows.empty": "Овај корисник још увек никог не прати.",
|
"account.follows.empty": "Овај корисник још увек никог не прати.",
|
||||||
"account.go_to_profile": "Иди на профил",
|
"account.go_to_profile": "Иди на профил",
|
||||||
"account.hide_reblogs": "Сакриј подржавања од @{name}",
|
"account.hide_reblogs": "Сакриј подржавања од @{name}",
|
||||||
|
@ -61,6 +63,7 @@
|
||||||
"account.requested_follow": "{name} је затражио да вас прати",
|
"account.requested_follow": "{name} је затражио да вас прати",
|
||||||
"account.share": "Подели профил корисника @{name}",
|
"account.share": "Подели профил корисника @{name}",
|
||||||
"account.show_reblogs": "Прикажи подржавања од корисника @{name}",
|
"account.show_reblogs": "Прикажи подржавања од корисника @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, one {{counter} објава} few {{counter} објаве} other {{counter} објава}}",
|
||||||
"account.unblock": "Одблокирај корисника @{name}",
|
"account.unblock": "Одблокирај корисника @{name}",
|
||||||
"account.unblock_domain": "Одблокирај домен {domain}",
|
"account.unblock_domain": "Одблокирај домен {domain}",
|
||||||
"account.unblock_short": "Одблокирај",
|
"account.unblock_short": "Одблокирај",
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
"account.follow_back": "Följ tillbaka",
|
"account.follow_back": "Följ tillbaka",
|
||||||
"account.followers": "Följare",
|
"account.followers": "Följare",
|
||||||
"account.followers.empty": "Ingen följer denna användare än.",
|
"account.followers.empty": "Ingen följer denna användare än.",
|
||||||
|
"account.followers_counter": "{count, plural, one {{counter} följare} other {{counter} följare}}",
|
||||||
"account.following": "Följer",
|
"account.following": "Följer",
|
||||||
"account.follows.empty": "Denna användare följer inte någon än.",
|
"account.follows.empty": "Denna användare följer inte någon än.",
|
||||||
"account.go_to_profile": "Gå till profilen",
|
"account.go_to_profile": "Gå till profilen",
|
||||||
|
@ -61,6 +62,7 @@
|
||||||
"account.requested_follow": "{name} har begärt att följa dig",
|
"account.requested_follow": "{name} har begärt att följa dig",
|
||||||
"account.share": "Dela @{name}s profil",
|
"account.share": "Dela @{name}s profil",
|
||||||
"account.show_reblogs": "Visa boostar från @{name}",
|
"account.show_reblogs": "Visa boostar från @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, one {{counter} inlägg} other {{counter} inlägg}}",
|
||||||
"account.unblock": "Avblockera @{name}",
|
"account.unblock": "Avblockera @{name}",
|
||||||
"account.unblock_domain": "Avblockera {domain}",
|
"account.unblock_domain": "Avblockera {domain}",
|
||||||
"account.unblock_short": "Avblockera",
|
"account.unblock_short": "Avblockera",
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
"account.follow_back": "ติดตามกลับ",
|
"account.follow_back": "ติดตามกลับ",
|
||||||
"account.followers": "ผู้ติดตาม",
|
"account.followers": "ผู้ติดตาม",
|
||||||
"account.followers.empty": "ยังไม่มีใครติดตามผู้ใช้นี้",
|
"account.followers.empty": "ยังไม่มีใครติดตามผู้ใช้นี้",
|
||||||
|
"account.followers_counter": "{count, plural, other {{counter} ผู้ติดตาม}}",
|
||||||
"account.following": "กำลังติดตาม",
|
"account.following": "กำลังติดตาม",
|
||||||
|
"account.following_counter": "{count, plural, other {{counter} กำลังติดตาม}}",
|
||||||
"account.follows.empty": "ผู้ใช้นี้ยังไม่ได้ติดตามใคร",
|
"account.follows.empty": "ผู้ใช้นี้ยังไม่ได้ติดตามใคร",
|
||||||
"account.go_to_profile": "ไปยังโปรไฟล์",
|
"account.go_to_profile": "ไปยังโปรไฟล์",
|
||||||
"account.hide_reblogs": "ซ่อนการดันจาก @{name}",
|
"account.hide_reblogs": "ซ่อนการดันจาก @{name}",
|
||||||
|
@ -61,6 +63,7 @@
|
||||||
"account.requested_follow": "{name} ได้ขอติดตามคุณ",
|
"account.requested_follow": "{name} ได้ขอติดตามคุณ",
|
||||||
"account.share": "แชร์โปรไฟล์ของ @{name}",
|
"account.share": "แชร์โปรไฟล์ของ @{name}",
|
||||||
"account.show_reblogs": "แสดงการดันจาก @{name}",
|
"account.show_reblogs": "แสดงการดันจาก @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, other {{counter} โพสต์}}",
|
||||||
"account.unblock": "เลิกปิดกั้น @{name}",
|
"account.unblock": "เลิกปิดกั้น @{name}",
|
||||||
"account.unblock_domain": "เลิกปิดกั้นโดเมน {domain}",
|
"account.unblock_domain": "เลิกปิดกั้นโดเมน {domain}",
|
||||||
"account.unblock_short": "เลิกปิดกั้น",
|
"account.unblock_short": "เลิกปิดกั้น",
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
"account.follow_back": "Підписатися взаємно",
|
"account.follow_back": "Підписатися взаємно",
|
||||||
"account.followers": "Підписники",
|
"account.followers": "Підписники",
|
||||||
"account.followers.empty": "Ніхто ще не підписаний на цього користувача.",
|
"account.followers.empty": "Ніхто ще не підписаний на цього користувача.",
|
||||||
|
"account.followers_counter": "{count, plural, one {{counter} підписник} few {{counter} підписники} many {{counter} підписників} other {{counter} підписники}}",
|
||||||
"account.following": "Ви стежите",
|
"account.following": "Ви стежите",
|
||||||
|
"account.following_counter": "{count, plural, one {{counter} підписка} few {{counter} підписки} many {{counter} підписок} other {{counter} підписки}}",
|
||||||
"account.follows.empty": "Цей користувач ще ні на кого не підписався.",
|
"account.follows.empty": "Цей користувач ще ні на кого не підписався.",
|
||||||
"account.go_to_profile": "Перейти до профілю",
|
"account.go_to_profile": "Перейти до профілю",
|
||||||
"account.hide_reblogs": "Сховати поширення від @{name}",
|
"account.hide_reblogs": "Сховати поширення від @{name}",
|
||||||
|
@ -61,6 +63,7 @@
|
||||||
"account.requested_follow": "{name} надсилає запит на стеження",
|
"account.requested_follow": "{name} надсилає запит на стеження",
|
||||||
"account.share": "Поділитися профілем @{name}",
|
"account.share": "Поділитися профілем @{name}",
|
||||||
"account.show_reblogs": "Показати поширення від @{name}",
|
"account.show_reblogs": "Показати поширення від @{name}",
|
||||||
|
"account.statuses_counter": "{count, plural, one {{counter} допис} few {{counter} дописи} many {{counter} дописів} other {{counter} допис}}",
|
||||||
"account.unblock": "Розблокувати @{name}",
|
"account.unblock": "Розблокувати @{name}",
|
||||||
"account.unblock_domain": "Розблокувати {domain}",
|
"account.unblock_domain": "Розблокувати {domain}",
|
||||||
"account.unblock_short": "Розблокувати",
|
"account.unblock_short": "Розблокувати",
|
||||||
|
@ -412,6 +415,7 @@
|
||||||
"limited_account_hint.title": "Цей профіль сховали модератори {domain}.",
|
"limited_account_hint.title": "Цей профіль сховали модератори {domain}.",
|
||||||
"link_preview.author": "Від {name}",
|
"link_preview.author": "Від {name}",
|
||||||
"link_preview.more_from_author": "Більше від {name}",
|
"link_preview.more_from_author": "Більше від {name}",
|
||||||
|
"link_preview.shares": "{count, plural, one {{counter} допис} few {{counter} дописи} many {{counter} дописів} other {{counter} допис}}",
|
||||||
"lists.account.add": "Додати до списку",
|
"lists.account.add": "Додати до списку",
|
||||||
"lists.account.remove": "Вилучити зі списку",
|
"lists.account.remove": "Вилучити зі списку",
|
||||||
"lists.delete": "Видалити список",
|
"lists.delete": "Видалити список",
|
||||||
|
@ -695,6 +699,7 @@
|
||||||
"server_banner.is_one_of_many": "{domain} - один з багатьох незалежних серверів Mastodon, які ви можете використати, щоб брати участь у федівері.",
|
"server_banner.is_one_of_many": "{domain} - один з багатьох незалежних серверів Mastodon, які ви можете використати, щоб брати участь у федівері.",
|
||||||
"server_banner.server_stats": "Статистика сервера:",
|
"server_banner.server_stats": "Статистика сервера:",
|
||||||
"sign_in_banner.create_account": "Створити обліковий запис",
|
"sign_in_banner.create_account": "Створити обліковий запис",
|
||||||
|
"sign_in_banner.follow_anyone": "Слідкуйте за ким завгодно у всьому fediverse і дивіться все це в хронологічному порядку. Немає алгоритмів, реклами чи наживок для натискань при перегляді.",
|
||||||
"sign_in_banner.mastodon_is": "Мастодон - найкращий спосіб продовжувати свою справу.",
|
"sign_in_banner.mastodon_is": "Мастодон - найкращий спосіб продовжувати свою справу.",
|
||||||
"sign_in_banner.sign_in": "Увійти",
|
"sign_in_banner.sign_in": "Увійти",
|
||||||
"sign_in_banner.sso_redirect": "Увійдіть або зареєструйтесь",
|
"sign_in_banner.sso_redirect": "Увійдіть або зареєструйтесь",
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
"account.follow_back": "Theo dõi lại",
|
"account.follow_back": "Theo dõi lại",
|
||||||
"account.followers": "Người theo dõi",
|
"account.followers": "Người theo dõi",
|
||||||
"account.followers.empty": "Chưa có người theo dõi nào.",
|
"account.followers.empty": "Chưa có người theo dõi nào.",
|
||||||
|
"account.followers_counter": "{count, plural, other {{counter} người theo dõi}}",
|
||||||
"account.following": "Đang theo dõi",
|
"account.following": "Đang theo dõi",
|
||||||
|
"account.following_counter": "{count, plural, other {{counter} đang theo dõi}}",
|
||||||
"account.follows.empty": "Người này chưa theo dõi ai.",
|
"account.follows.empty": "Người này chưa theo dõi ai.",
|
||||||
"account.go_to_profile": "Xem hồ sơ",
|
"account.go_to_profile": "Xem hồ sơ",
|
||||||
"account.hide_reblogs": "Ẩn tút @{name} đăng lại",
|
"account.hide_reblogs": "Ẩn tút @{name} đăng lại",
|
||||||
|
@ -61,6 +63,7 @@
|
||||||
"account.requested_follow": "{name} yêu cầu theo dõi bạn",
|
"account.requested_follow": "{name} yêu cầu theo dõi bạn",
|
||||||
"account.share": "Chia sẻ @{name}",
|
"account.share": "Chia sẻ @{name}",
|
||||||
"account.show_reblogs": "Hiện tút do @{name} đăng lại",
|
"account.show_reblogs": "Hiện tút do @{name} đăng lại",
|
||||||
|
"account.statuses_counter": "{count, plural, other {{counter} tút}}",
|
||||||
"account.unblock": "Bỏ chặn @{name}",
|
"account.unblock": "Bỏ chặn @{name}",
|
||||||
"account.unblock_domain": "Bỏ ẩn {domain}",
|
"account.unblock_domain": "Bỏ ẩn {domain}",
|
||||||
"account.unblock_short": "Bỏ chặn",
|
"account.unblock_short": "Bỏ chặn",
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
|
import type { RecordOf } from 'immutable';
|
||||||
|
|
||||||
|
import type { ApiPreviewCardJSON } from 'mastodon/api_types/statuses';
|
||||||
|
|
||||||
export type { StatusVisibility } from 'mastodon/api_types/statuses';
|
export type { StatusVisibility } from 'mastodon/api_types/statuses';
|
||||||
|
|
||||||
// Temporary until we type it correctly
|
// Temporary until we type it correctly
|
||||||
export type Status = Immutable.Map<string, unknown>;
|
export type Status = Immutable.Map<string, unknown>;
|
||||||
|
|
||||||
|
type CardShape = Required<ApiPreviewCardJSON>;
|
||||||
|
|
||||||
|
export type Card = RecordOf<CardShape>;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { IntlProvider } from 'react-intl';
|
||||||
|
|
||||||
import { MemoryRouter } from 'react-router';
|
import { MemoryRouter } from 'react-router';
|
||||||
|
|
||||||
|
import type { RenderOptions } from '@testing-library/react';
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import { render as rtlRender } from '@testing-library/react';
|
import { render as rtlRender } from '@testing-library/react';
|
||||||
|
|
||||||
|
@ -9,7 +10,11 @@ import { IdentityContext } from './identity_context';
|
||||||
|
|
||||||
function render(
|
function render(
|
||||||
ui: React.ReactElement,
|
ui: React.ReactElement,
|
||||||
{ locale = 'en', signedIn = true, ...renderOptions } = {},
|
{
|
||||||
|
locale = 'en',
|
||||||
|
signedIn = true,
|
||||||
|
...renderOptions
|
||||||
|
}: RenderOptions & { locale?: string; signedIn?: boolean } = {},
|
||||||
) {
|
) {
|
||||||
const fakeIdentity = {
|
const fakeIdentity = {
|
||||||
signedIn: signedIn,
|
signedIn: signedIn,
|
||||||
|
|
|
@ -10899,12 +10899,14 @@ noscript {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
text-align: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.verified {
|
&.verified {
|
||||||
dd {
|
dd {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
|
@ -118,7 +118,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
def find_existing_status
|
def find_existing_status
|
||||||
status = status_from_uri(object_uri)
|
status = status_from_uri(object_uri)
|
||||||
status ||= Status.find_by(uri: @object['atomUri']) if @object['atomUri'].present?
|
status ||= Status.find_by(uri: @object['atomUri']) if @object['atomUri'].present?
|
||||||
status
|
status if status&.account_id == @account.id
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_status_params
|
def process_status_params
|
||||||
|
|
|
@ -16,7 +16,7 @@ module ApplicationExtension
|
||||||
# dependent: delete_all, which means the ActiveRecord callback in
|
# dependent: delete_all, which means the ActiveRecord callback in
|
||||||
# AccessTokenExtension is not run, so instead we manually announce to
|
# AccessTokenExtension is not run, so instead we manually announce to
|
||||||
# streaming that these tokens are being deleted.
|
# streaming that these tokens are being deleted.
|
||||||
before_destroy :push_to_streaming_api, prepend: true
|
before_destroy :close_streaming_sessions, prepend: true
|
||||||
end
|
end
|
||||||
|
|
||||||
def confirmation_redirect_uri
|
def confirmation_redirect_uri
|
||||||
|
@ -29,10 +29,12 @@ module ApplicationExtension
|
||||||
redirect_uri.split
|
redirect_uri.split
|
||||||
end
|
end
|
||||||
|
|
||||||
def push_to_streaming_api
|
def close_streaming_sessions(resource_owner = nil)
|
||||||
# TODO: #28793 Combine into a single topic
|
# TODO: #28793 Combine into a single topic
|
||||||
payload = Oj.dump(event: :kill)
|
payload = Oj.dump(event: :kill)
|
||||||
access_tokens.in_batches do |tokens|
|
scope = access_tokens
|
||||||
|
scope = scope.where(resource_owner_id: resource_owner.id) unless resource_owner.nil?
|
||||||
|
scope.in_batches do |tokens|
|
||||||
redis.pipelined do |pipeline|
|
redis.pipelined do |pipeline|
|
||||||
tokens.ids.each do |id|
|
tokens.ids.each do |id|
|
||||||
pipeline.publish("timeline:access_token:#{id}", payload)
|
pipeline.publish("timeline:access_token:#{id}", payload)
|
||||||
|
|
|
@ -81,7 +81,7 @@ cs:
|
||||||
invite_request_text: Důvody založení
|
invite_request_text: Důvody založení
|
||||||
invited_by: Pozván uživatelem
|
invited_by: Pozván uživatelem
|
||||||
ip: IP adresa
|
ip: IP adresa
|
||||||
joined: Uživatel založen
|
joined: Uživatelem od
|
||||||
location:
|
location:
|
||||||
all: Všechny
|
all: Všechny
|
||||||
local: Místní
|
local: Místní
|
||||||
|
@ -291,6 +291,7 @@ cs:
|
||||||
update_custom_emoji_html: Uživatel %{name} aktualizoval emoji %{target}
|
update_custom_emoji_html: Uživatel %{name} aktualizoval emoji %{target}
|
||||||
update_domain_block_html: "%{name} aktualizoval blokaci domény %{target}"
|
update_domain_block_html: "%{name} aktualizoval blokaci domény %{target}"
|
||||||
update_ip_block_html: "%{name} změnil pravidlo pro IP %{target}"
|
update_ip_block_html: "%{name} změnil pravidlo pro IP %{target}"
|
||||||
|
update_report_html: "%{name} aktualizoval hlášení %{target}"
|
||||||
update_status_html: Uživatel %{name} aktualizoval příspěvek uživatele %{target}
|
update_status_html: Uživatel %{name} aktualizoval příspěvek uživatele %{target}
|
||||||
update_user_role_html: "%{name} změnil %{target} roli"
|
update_user_role_html: "%{name} změnil %{target} roli"
|
||||||
deleted_account: smazaný účet
|
deleted_account: smazaný účet
|
||||||
|
@ -298,6 +299,7 @@ cs:
|
||||||
filter_by_action: Filtrovat podle akce
|
filter_by_action: Filtrovat podle akce
|
||||||
filter_by_user: Filtrovat podle uživatele
|
filter_by_user: Filtrovat podle uživatele
|
||||||
title: Protokol auditu
|
title: Protokol auditu
|
||||||
|
unavailable_instance: "(doména není k dispozici)"
|
||||||
announcements:
|
announcements:
|
||||||
destroyed_msg: Oznámení bylo úspěšně odstraněno!
|
destroyed_msg: Oznámení bylo úspěšně odstraněno!
|
||||||
edit:
|
edit:
|
||||||
|
@ -984,6 +986,7 @@ cs:
|
||||||
delete: Smazat
|
delete: Smazat
|
||||||
edit_preset: Upravit předlohu pro varování
|
edit_preset: Upravit předlohu pro varování
|
||||||
empty: Zatím jste nedefinovali žádné předlohy varování.
|
empty: Zatím jste nedefinovali žádné předlohy varování.
|
||||||
|
title: Předvolby varování
|
||||||
webhooks:
|
webhooks:
|
||||||
add_new: Přidat koncový bod
|
add_new: Přidat koncový bod
|
||||||
delete: Smazat
|
delete: Smazat
|
||||||
|
|
|
@ -166,6 +166,7 @@ cs:
|
||||||
admin:write:reports: provádět moderátorské akce s hlášeními
|
admin:write:reports: provádět moderátorské akce s hlášeními
|
||||||
crypto: používat end-to-end šifrování
|
crypto: používat end-to-end šifrování
|
||||||
follow: upravovat vztahy mezi profily
|
follow: upravovat vztahy mezi profily
|
||||||
|
profile: číst pouze základní informace o vašem účtu
|
||||||
push: přijímat vaše push oznámení
|
push: přijímat vaše push oznámení
|
||||||
read: vidět všechna data vašeho účtu
|
read: vidět všechna data vašeho účtu
|
||||||
read:accounts: vidět informace o účtech
|
read:accounts: vidět informace o účtech
|
||||||
|
|
|
@ -135,6 +135,7 @@ en-GB:
|
||||||
media: Media attachments
|
media: Media attachments
|
||||||
mutes: Mutes
|
mutes: Mutes
|
||||||
notifications: Notifications
|
notifications: Notifications
|
||||||
|
profile: Your Mastodon profile
|
||||||
push: Push notifications
|
push: Push notifications
|
||||||
reports: Reports
|
reports: Reports
|
||||||
search: Search
|
search: Search
|
||||||
|
@ -165,6 +166,7 @@ en-GB:
|
||||||
admin:write:reports: perform moderation actions on reports
|
admin:write:reports: perform moderation actions on reports
|
||||||
crypto: use end-to-end encryption
|
crypto: use end-to-end encryption
|
||||||
follow: modify account relationships
|
follow: modify account relationships
|
||||||
|
profile: read only your account's profile information
|
||||||
push: receive your push notifications
|
push: receive your push notifications
|
||||||
read: read all your account's data
|
read: read all your account's data
|
||||||
read:accounts: see accounts information
|
read:accounts: see accounts information
|
||||||
|
|
|
@ -285,6 +285,7 @@ en-GB:
|
||||||
update_custom_emoji_html: "%{name} updated emoji %{target}"
|
update_custom_emoji_html: "%{name} updated emoji %{target}"
|
||||||
update_domain_block_html: "%{name} updated domain block for %{target}"
|
update_domain_block_html: "%{name} updated domain block for %{target}"
|
||||||
update_ip_block_html: "%{name} changed rule for IP %{target}"
|
update_ip_block_html: "%{name} changed rule for IP %{target}"
|
||||||
|
update_report_html: "%{name} updated report %{target}"
|
||||||
update_status_html: "%{name} updated post by %{target}"
|
update_status_html: "%{name} updated post by %{target}"
|
||||||
update_user_role_html: "%{name} changed %{target} role"
|
update_user_role_html: "%{name} changed %{target} role"
|
||||||
deleted_account: deleted account
|
deleted_account: deleted account
|
||||||
|
@ -292,6 +293,7 @@ en-GB:
|
||||||
filter_by_action: Filter by action
|
filter_by_action: Filter by action
|
||||||
filter_by_user: Filter by user
|
filter_by_user: Filter by user
|
||||||
title: Audit log
|
title: Audit log
|
||||||
|
unavailable_instance: "(domain name unavailable)"
|
||||||
announcements:
|
announcements:
|
||||||
destroyed_msg: Announcement successfully deleted!
|
destroyed_msg: Announcement successfully deleted!
|
||||||
edit:
|
edit:
|
||||||
|
@ -950,6 +952,7 @@ en-GB:
|
||||||
delete: Delete
|
delete: Delete
|
||||||
edit_preset: Edit warning preset
|
edit_preset: Edit warning preset
|
||||||
empty: You haven't defined any warning presets yet.
|
empty: You haven't defined any warning presets yet.
|
||||||
|
title: Warning presets
|
||||||
webhooks:
|
webhooks:
|
||||||
add_new: Add endpoint
|
add_new: Add endpoint
|
||||||
delete: Delete
|
delete: Delete
|
||||||
|
|
|
@ -574,7 +574,7 @@ ia:
|
||||||
enabled: Activate
|
enabled: Activate
|
||||||
inbox_url: URL del repetitor
|
inbox_url: URL del repetitor
|
||||||
pending: Attende le approbation del repetitor
|
pending: Attende le approbation del repetitor
|
||||||
save_and_enable: Salveguardar e activar
|
save_and_enable: Salvar e activar
|
||||||
setup: Crear un connexion con un repetitor
|
setup: Crear un connexion con un repetitor
|
||||||
signatures_not_enabled: Le repetitores pote non functionar correctemente durante que le modo secur o le modo de federation limitate es activate
|
signatures_not_enabled: Le repetitores pote non functionar correctemente durante que le modo secur o le modo de federation limitate es activate
|
||||||
status: Stato
|
status: Stato
|
||||||
|
@ -1276,7 +1276,7 @@ ia:
|
||||||
other: "%{count} messages individual celate"
|
other: "%{count} messages individual celate"
|
||||||
title: Filtros
|
title: Filtros
|
||||||
new:
|
new:
|
||||||
save: Salveguardar nove filtro
|
save: Salvar nove filtro
|
||||||
title: Adder nove filtro
|
title: Adder nove filtro
|
||||||
statuses:
|
statuses:
|
||||||
back_to_filter: Retro al filtro
|
back_to_filter: Retro al filtro
|
||||||
|
@ -1294,14 +1294,14 @@ ia:
|
||||||
one: "<strong>%{count}</strong> elemento correspondente al recerca es seligite."
|
one: "<strong>%{count}</strong> elemento correspondente al recerca es seligite."
|
||||||
other: Tote le <strong>%{count}</strong> elementos correspondente al recerca es seligite.
|
other: Tote le <strong>%{count}</strong> elementos correspondente al recerca es seligite.
|
||||||
cancel: Cancellar
|
cancel: Cancellar
|
||||||
changes_saved_msg: Cambios salveguardate con successo!
|
changes_saved_msg: Le cambiamentos ha essite salvate!
|
||||||
confirm: Confirmar
|
confirm: Confirmar
|
||||||
copy: Copiar
|
copy: Copiar
|
||||||
delete: Deler
|
delete: Deler
|
||||||
deselect: Deseliger toto
|
deselect: Deseliger toto
|
||||||
none: Necun
|
none: Necun
|
||||||
order_by: Ordinar per
|
order_by: Ordinar per
|
||||||
save_changes: Salvar le cambios
|
save_changes: Salvar le cambiamentos
|
||||||
select_all_matching_items:
|
select_all_matching_items:
|
||||||
one: Selige %{count} elemento correspondente a tu recerca.
|
one: Selige %{count} elemento correspondente a tu recerca.
|
||||||
other: Selige %{count} elementos correspondente a tu recerca.
|
other: Selige %{count} elementos correspondente a tu recerca.
|
||||||
|
|
|
@ -293,6 +293,7 @@ nn:
|
||||||
filter_by_action: Sorter etter handling
|
filter_by_action: Sorter etter handling
|
||||||
filter_by_user: Sorter etter brukar
|
filter_by_user: Sorter etter brukar
|
||||||
title: Revisionslogg
|
title: Revisionslogg
|
||||||
|
unavailable_instance: "(domenenamn er utilgjengeleg)"
|
||||||
announcements:
|
announcements:
|
||||||
destroyed_msg: Kunngjøringen er slettet!
|
destroyed_msg: Kunngjøringen er slettet!
|
||||||
edit:
|
edit:
|
||||||
|
|
|
@ -59,7 +59,7 @@ cs:
|
||||||
setting_display_media_default: Skrývat média označená jako citlivá
|
setting_display_media_default: Skrývat média označená jako citlivá
|
||||||
setting_display_media_hide_all: Vždy skrývat média
|
setting_display_media_hide_all: Vždy skrývat média
|
||||||
setting_display_media_show_all: Vždy zobrazovat média
|
setting_display_media_show_all: Vždy zobrazovat média
|
||||||
setting_use_blurhash: Gradienty jsou založeny na barvách skryté grafiky, ale zakrývají jakékoliv detaily
|
setting_use_blurhash: Gradienty jsou vytvořeny na základě barvev skrytých médií, ale zakrývají veškeré detaily
|
||||||
setting_use_pending_items: Aktualizovat časovou osu až po kliknutí namísto automatického rolování kanálu
|
setting_use_pending_items: Aktualizovat časovou osu až po kliknutí namísto automatického rolování kanálu
|
||||||
username: Pouze písmena, číslice a podtržítka
|
username: Pouze písmena, číslice a podtržítka
|
||||||
whole_word: Je-li klíčové slovo či fráze pouze alfanumerická, bude aplikován pouze, pokud se shoduje s celým slovem
|
whole_word: Je-li klíčové slovo či fráze pouze alfanumerická, bude aplikován pouze, pokud se shoduje s celým slovem
|
||||||
|
|
|
@ -107,7 +107,7 @@ ja:
|
||||||
backups_retention_period: ユーザーには、後でダウンロードするために投稿のアーカイブを生成する機能があります。正の値に設定すると、これらのアーカイブは指定された日数後に自動的にストレージから削除されます。
|
backups_retention_period: ユーザーには、後でダウンロードするために投稿のアーカイブを生成する機能があります。正の値に設定すると、これらのアーカイブは指定された日数後に自動的にストレージから削除されます。
|
||||||
bootstrap_timeline_accounts: これらのアカウントは、新しいユーザー向けのおすすめユーザーの一番上にピン留めされます。
|
bootstrap_timeline_accounts: これらのアカウントは、新しいユーザー向けのおすすめユーザーの一番上にピン留めされます。
|
||||||
closed_registrations_message: アカウント作成を停止している時に表示されます
|
closed_registrations_message: アカウント作成を停止している時に表示されます
|
||||||
content_cache_retention_period: 他のサーバーからのすべての投稿(ブーストや返信を含む)は、指定された日数が経過すると、ローカルユーザーとのやりとりに関係なく削除されます。これには、ローカルユーザーがブックマークやお気に入りとして登録した投稿も含まれます。異なるサーバーのユーザー間の非公開な変身も失われ、復元することは不可能です。この設定の使用は特別な目的のインスタンスのためのものであり、一般的な目的のサーバーで使用するした場合、多くのユーザーの期待を裏切ることになります。
|
content_cache_retention_period: 他のサーバーからのすべての投稿(ブーストや返信を含む)は、指定された日数が経過すると、ローカルユーザーとのやりとりに関係なく削除されます。これには、ローカルユーザーがブックマークやお気に入りとして登録した投稿も含まれます。異なるサーバーのユーザー間の非公開な返信も失われ、復元することは不可能です。この設定の使用は特別な目的のインスタンスのためのものであり、一般的な目的のサーバーで使用した場合、多くのユーザーの期待を裏切ることになります。
|
||||||
custom_css: ウェブ版のMastodonでカスタムスタイルを適用できます。
|
custom_css: ウェブ版のMastodonでカスタムスタイルを適用できます。
|
||||||
delete_content_cache_without_reaction: この機能は現在、負荷に関する懸念があります。すべての投稿をブックマークするユーザーbotの出現も想定されます。特に大規模サーバーでは慎重に検討してください。
|
delete_content_cache_without_reaction: この機能は現在、負荷に関する懸念があります。すべての投稿をブックマークするユーザーbotの出現も想定されます。特に大規模サーバーでは慎重に検討してください。
|
||||||
enable_local_timeline: 有効にすると気の合ったユーザー同士の交流が捗る反面、内輪の雰囲気が強くなるかもしれません。Mastodonはローカルタイムラインがあるものだと思われているので、無効にする場合はサーバー紹介での注記をおすすめします。
|
enable_local_timeline: 有効にすると気の合ったユーザー同士の交流が捗る反面、内輪の雰囲気が強くなるかもしれません。Mastodonはローカルタイムラインがあるものだと思われているので、無効にする場合はサーバー紹介での注記をおすすめします。
|
||||||
|
|
|
@ -31,6 +31,7 @@ Rails.application.routes.draw do
|
||||||
/antennasw/(*any)
|
/antennasw/(*any)
|
||||||
/antennast/(*any)
|
/antennast/(*any)
|
||||||
/circles/(*any)
|
/circles/(*any)
|
||||||
|
/links/(*any)
|
||||||
/notifications/(*any)
|
/notifications/(*any)
|
||||||
/favourites
|
/favourites
|
||||||
/emoji_reactions
|
/emoji_reactions
|
||||||
|
|
|
@ -58,7 +58,7 @@ services:
|
||||||
|
|
||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v4.2.9
|
image: ghcr.io/mastodon/mastodon:v4.2.10
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec puma -C config/puma.rb
|
command: bundle exec puma -C config/puma.rb
|
||||||
|
@ -79,7 +79,7 @@ services:
|
||||||
|
|
||||||
streaming:
|
streaming:
|
||||||
build: .
|
build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v4.2.9
|
image: ghcr.io/mastodon/mastodon:v4.2.10
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: node ./streaming
|
command: node ./streaming
|
||||||
|
@ -97,7 +97,7 @@ services:
|
||||||
|
|
||||||
sidekiq:
|
sidekiq:
|
||||||
build: .
|
build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v4.2.9
|
image: ghcr.io/mastodon/mastodon:v4.2.10
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec sidekiq
|
command: bundle exec sidekiq
|
||||||
|
|
|
@ -31,7 +31,7 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_prerelease
|
def default_prerelease
|
||||||
'alpha.4'
|
'alpha.5'
|
||||||
end
|
end
|
||||||
|
|
||||||
def prerelease
|
def prerelease
|
||||||
|
|
|
@ -116,34 +116,23 @@ module Paperclip
|
||||||
# The number of occurrences of a color (r, g, b) is thus encoded in band `b` at pixel position `(r, g)`
|
# The number of occurrences of a color (r, g, b) is thus encoded in band `b` at pixel position `(r, g)`
|
||||||
histogram = image.hist_find_ndim(bins: BINS)
|
histogram = image.hist_find_ndim(bins: BINS)
|
||||||
|
|
||||||
# `histogram.max` returns an array of maxima with their pixel positions, but we don't know in which
|
# With `bandunfold`, we get back to a (BINS*BINS)×BINS 2D image with a single band.
|
||||||
# band they are
|
# The number of occurrences of a color (r, g, b) is thus encoded at pixel position `(r * BINS + b, g)`
|
||||||
|
histogram = histogram.bandunfold
|
||||||
|
|
||||||
_, colors = histogram.max(size: 10, out_array: true, x_array: true, y_array: true)
|
_, colors = histogram.max(size: 10, out_array: true, x_array: true, y_array: true)
|
||||||
|
|
||||||
colors['out_array'].zip(colors['x_array'], colors['y_array']).map do |v, x, y|
|
colors['x_array'].zip(colors['y_array']).map do |x, y|
|
||||||
rgb_from_xyv(histogram, x, y, v)
|
rgb_from_hist_xy(x, y)
|
||||||
end.flatten.reverse.uniq
|
end.flatten.reverse
|
||||||
end
|
end
|
||||||
|
|
||||||
# rubocop:disable Naming/MethodParameterName
|
# rubocop:disable Naming/MethodParameterName
|
||||||
def rgb_from_xyv(image, x, y, v)
|
def rgb_from_hist_xy(x, y)
|
||||||
pixel = image.getpoint(x, y)
|
r = ((x / BINS) + 0.5) * 256 / BINS
|
||||||
|
g = (y + 0.5) * 256 / BINS
|
||||||
# As we only have the first 2 dimensions for this maximum, we
|
b = ((x % BINS) + 0.5) * 256 / BINS
|
||||||
# can't distinguish with different maxima with the same `r` and `g`
|
ColorDiff::Color::RGB.new(r, g, b)
|
||||||
# values but different `b` values.
|
|
||||||
#
|
|
||||||
# Therefore, we return an array of maxima, which is always non-empty,
|
|
||||||
# but may contain multiple colors with the same values.
|
|
||||||
|
|
||||||
pixel.filter_map.with_index do |pv, z|
|
|
||||||
next if pv != v
|
|
||||||
|
|
||||||
r = (x + 0.5) * 256 / BINS
|
|
||||||
g = (y + 0.5) * 256 / BINS
|
|
||||||
b = (z + 0.5) * 256 / BINS
|
|
||||||
ColorDiff::Color::RGB.new(r, g, b)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def w3c_contrast(color1, color2)
|
def w3c_contrast(color1, color2)
|
||||||
|
|
|
@ -88,7 +88,7 @@ class Sanitize
|
||||||
end
|
end
|
||||||
|
|
||||||
MASTODON_STRICT = freeze_config(
|
MASTODON_STRICT = freeze_config(
|
||||||
elements: %w(p br span a del pre blockquote code b strong u i em ul ol li),
|
elements: %w(p br span a del pre blockquote code b strong u i em ul ol li ruby rt rp),
|
||||||
|
|
||||||
attributes: {
|
attributes: {
|
||||||
'a' => %w(href rel class translate),
|
'a' => %w(href rel class translate),
|
||||||
|
|
|
@ -132,7 +132,7 @@
|
||||||
"webpack-assets-manifest": "^4.0.6",
|
"webpack-assets-manifest": "^4.0.6",
|
||||||
"webpack-bundle-analyzer": "^4.8.0",
|
"webpack-bundle-analyzer": "^4.8.0",
|
||||||
"webpack-cli": "^3.3.12",
|
"webpack-cli": "^3.3.12",
|
||||||
"webpack-merge": "^5.9.0",
|
"webpack-merge": "^6.0.0",
|
||||||
"wicg-inert": "^3.1.2",
|
"wicg-inert": "^3.1.2",
|
||||||
"workbox-expiration": "^7.0.0",
|
"workbox-expiration": "^7.0.0",
|
||||||
"workbox-precaching": "^7.0.0",
|
"workbox-precaching": "^7.0.0",
|
||||||
|
@ -143,8 +143,9 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@formatjs/cli": "^6.1.1",
|
"@formatjs/cli": "^6.1.1",
|
||||||
|
"@testing-library/dom": "^10.2.0",
|
||||||
"@testing-library/jest-dom": "^6.0.0",
|
"@testing-library/jest-dom": "^6.0.0",
|
||||||
"@testing-library/react": "^15.0.0",
|
"@testing-library/react": "^16.0.0",
|
||||||
"@types/babel__core": "^7.20.1",
|
"@types/babel__core": "^7.20.1",
|
||||||
"@types/emoji-mart": "^3.0.9",
|
"@types/emoji-mart": "^3.0.9",
|
||||||
"@types/escape-html": "^1.0.2",
|
"@types/escape-html": "^1.0.2",
|
||||||
|
@ -182,8 +183,8 @@
|
||||||
"eslint-plugin-formatjs": "^4.10.1",
|
"eslint-plugin-formatjs": "^4.10.1",
|
||||||
"eslint-plugin-import": "~2.29.0",
|
"eslint-plugin-import": "~2.29.0",
|
||||||
"eslint-plugin-jsdoc": "^48.0.0",
|
"eslint-plugin-jsdoc": "^48.0.0",
|
||||||
"eslint-plugin-jsx-a11y": "~6.8.0",
|
"eslint-plugin-jsx-a11y": "~6.9.0",
|
||||||
"eslint-plugin-promise": "~6.2.0",
|
"eslint-plugin-promise": "~6.4.0",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
|
|
|
@ -50,9 +50,11 @@ describe Oauth::AuthorizedApplicationsController do
|
||||||
let!(:application) { Fabricate(:application) }
|
let!(:application) { Fabricate(:application) }
|
||||||
let!(:access_token) { Fabricate(:accessible_access_token, application: application, resource_owner_id: user.id) }
|
let!(:access_token) { Fabricate(:accessible_access_token, application: application, resource_owner_id: user.id) }
|
||||||
let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) }
|
let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) }
|
||||||
|
let(:redis_pipeline_stub) { instance_double(Redis::Namespace, publish: nil) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
sign_in user, scope: :user
|
sign_in user, scope: :user
|
||||||
|
allow(redis).to receive(:pipelined).and_yield(redis_pipeline_stub)
|
||||||
post :destroy, params: { id: application.id }
|
post :destroy, params: { id: application.id }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -67,5 +69,9 @@ describe Oauth::AuthorizedApplicationsController do
|
||||||
it 'removes the web_push_subscription' do
|
it 'removes the web_push_subscription' do
|
||||||
expect { web_push_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
expect { web_push_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'sends a session kill payload to the streaming server' do
|
||||||
|
expect(redis_pipeline_stub).to have_received(:publish).with("timeline:access_token:#{access_token.id}", '{"event":"kill"}')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -147,14 +147,22 @@ describe Settings::ApplicationsController do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'destroy' do
|
describe 'destroy' do
|
||||||
|
let(:redis_pipeline_stub) { instance_double(Redis::Namespace, publish: nil) }
|
||||||
|
let!(:access_token) { Fabricate(:accessible_access_token, application: app) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
allow(redis).to receive(:pipelined).and_yield(redis_pipeline_stub)
|
||||||
post :destroy, params: { id: app.id }
|
post :destroy, params: { id: app.id }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'redirects back to applications page and removes the app' do
|
it 'redirects back to applications page removes the app' do
|
||||||
expect(response).to redirect_to(settings_applications_path)
|
expect(response).to redirect_to(settings_applications_path)
|
||||||
expect(Doorkeeper::Application.find_by(id: app.id)).to be_nil
|
expect(Doorkeeper::Application.find_by(id: app.id)).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'sends a session kill payload to the streaming server' do
|
||||||
|
expect(redis_pipeline_stub).to have_received(:publish).with("timeline:access_token:#{access_token.id}", '{"event":"kill"}')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'regenerate' do
|
describe 'regenerate' do
|
||||||
|
|
|
@ -41,6 +41,14 @@ RSpec.describe HtmlAwareFormatter do
|
||||||
expect(subject).to_not include 'status__content__spoiler-link'
|
expect(subject).to_not include 'status__content__spoiler-link'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when given text containing ruby tags for east-asian languages' do
|
||||||
|
let(:text) { '<ruby>明日 <rp>(</rp><rt>Ashita</rt><rp>)</rp></ruby>' }
|
||||||
|
|
||||||
|
it 'keeps the ruby tags' do
|
||||||
|
expect(subject).to eq '<ruby>明日 <rp>(</rp><rt>Ashita</rt><rp>)</rp></ruby>'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -72,6 +72,14 @@ RSpec.describe PlainTextFormatter do
|
||||||
expect(subject).to eq 'Lorem ipsum'
|
expect(subject).to eq 'Lorem ipsum'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when text contains HTML ruby tags' do
|
||||||
|
let(:status) { Fabricate(:status, account: remote_account, text: '<p>Lorem <ruby>明日 <rp>(</rp><rt>Ashita</rt><rp>)</rp></ruby> ipsum</p>') }
|
||||||
|
|
||||||
|
it 'strips the comment' do
|
||||||
|
expect(subject).to eq 'Lorem 明日 (Ashita) ipsum'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,6 +18,10 @@ describe Sanitize::Config do
|
||||||
expect(Sanitize.fragment('<p>Check out:</p><ol start="3" reversed=""><li>Foo</li><li>Bar</li></ol>', subject)).to eq '<p>Check out:</p><ol start="3" reversed=""><li>Foo</li><li>Bar</li></ol>'
|
expect(Sanitize.fragment('<p>Check out:</p><ol start="3" reversed=""><li>Foo</li><li>Bar</li></ol>', subject)).to eq '<p>Check out:</p><ol start="3" reversed=""><li>Foo</li><li>Bar</li></ol>'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'keeps ruby tags' do
|
||||||
|
expect(Sanitize.fragment('<p><ruby>明日 <rp>(</rp><rt>Ashita</rt><rp>)</rp></ruby></p>', subject)).to eq '<p><ruby>明日 <rp>(</rp><rt>Ashita</rt><rp>)</rp></ruby></p>'
|
||||||
|
end
|
||||||
|
|
||||||
it 'removes a without href' do
|
it 'removes a without href' do
|
||||||
expect(Sanitize.fragment('<a>Test</a>', subject)).to eq 'Test'
|
expect(Sanitize.fragment('<a>Test</a>', subject)).to eq 'Test'
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,6 +25,17 @@ describe 'Scheduled Statuses' do
|
||||||
it_behaves_like 'forbidden for wrong scope', 'write write:statuses'
|
it_behaves_like 'forbidden for wrong scope', 'write write:statuses'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with an application token' do
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: 'read:statuses') }
|
||||||
|
|
||||||
|
it 'returns http unprocessable entity' do
|
||||||
|
get api_v1_scheduled_statuses_path, headers: headers
|
||||||
|
|
||||||
|
expect(response)
|
||||||
|
.to have_http_status(422)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with correct scope' do
|
context 'with correct scope' do
|
||||||
let(:scopes) { 'read:statuses' }
|
let(:scopes) { 'read:statuses' }
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,22 @@ describe 'API V1 Statuses Translations' do
|
||||||
let(:scopes) { 'read:statuses' }
|
let(:scopes) { 'read:statuses' }
|
||||||
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
|
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
|
||||||
|
|
||||||
|
context 'with an application token' do
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: scopes) }
|
||||||
|
|
||||||
|
describe 'POST /api/v1/statuses/:status_id/translate' do
|
||||||
|
let(:status) { Fabricate(:status, account: user.account, text: 'Hola', language: 'es') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
post "/api/v1/statuses/#{status.id}/translate", headers: headers
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http unprocessable entity' do
|
||||||
|
expect(response).to have_http_status(422)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with an oauth token' do
|
context 'with an oauth token' do
|
||||||
describe 'POST /api/v1/statuses/:status_id/translate' do
|
describe 'POST /api/v1/statuses/:status_id/translate' do
|
||||||
let(:status) { Fabricate(:status, account: user.account, text: 'Hola', language: 'es') }
|
let(:status) { Fabricate(:status, account: user.account, text: 'Hola', language: 'es') }
|
||||||
|
|
|
@ -41,6 +41,8 @@ describe 'Link' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'profile'
|
||||||
|
|
||||||
context 'when there is no preview card' do
|
context 'when there is no preview card' do
|
||||||
let(:preview_card) { nil }
|
let(:preview_card) { nil }
|
||||||
|
|
||||||
|
@ -80,13 +82,25 @@ describe 'Link' do
|
||||||
Form::AdminSettings.new(timeline_preview: false).save
|
Form::AdminSettings.new(timeline_preview: false).save
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the user is not authenticated' do
|
it_behaves_like 'forbidden for wrong scope', 'profile'
|
||||||
|
|
||||||
|
context 'without an authentication token' do
|
||||||
let(:headers) { {} }
|
let(:headers) { {} }
|
||||||
|
|
||||||
it 'returns http unauthorized' do
|
it 'returns http unprocessable entity' do
|
||||||
subject
|
subject
|
||||||
|
|
||||||
expect(response).to have_http_status(401)
|
expect(response).to have_http_status(422)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an application access token, not bound to a user' do
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: scopes) }
|
||||||
|
|
||||||
|
it 'returns http unprocessable entity' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(422)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,8 @@ describe 'Public' do
|
||||||
context 'when the instance allows public preview' do
|
context 'when the instance allows public preview' do
|
||||||
let(:expected_statuses) { [local_status, remote_status, media_status] }
|
let(:expected_statuses) { [local_status, remote_status, media_status] }
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'profile'
|
||||||
|
|
||||||
context 'with an authorized user' do
|
context 'with an authorized user' do
|
||||||
it_behaves_like 'a successful request to the public timeline'
|
it_behaves_like 'a successful request to the public timeline'
|
||||||
end
|
end
|
||||||
|
@ -122,13 +124,9 @@ describe 'Public' do
|
||||||
Form::AdminSettings.new(timeline_preview: false).save
|
Form::AdminSettings.new(timeline_preview: false).save
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with an authenticated user' do
|
it_behaves_like 'forbidden for wrong scope', 'profile'
|
||||||
let(:expected_statuses) { [local_status, remote_status, media_status] }
|
|
||||||
|
|
||||||
it_behaves_like 'a successful request to the public timeline'
|
context 'without an authentication token' do
|
||||||
end
|
|
||||||
|
|
||||||
context 'with an unauthenticated user' do
|
|
||||||
let(:headers) { {} }
|
let(:headers) { {} }
|
||||||
|
|
||||||
it 'returns http unprocessable entity' do
|
it 'returns http unprocessable entity' do
|
||||||
|
@ -137,6 +135,22 @@ describe 'Public' do
|
||||||
expect(response).to have_http_status(422)
|
expect(response).to have_http_status(422)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with an application access token, not bound to a user' do
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: scopes) }
|
||||||
|
|
||||||
|
it 'returns http unprocessable entity' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(422)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an authenticated user' do
|
||||||
|
let(:expected_statuses) { [local_status, remote_status, media_status] }
|
||||||
|
|
||||||
|
it_behaves_like 'a successful request to the public timeline'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user is setting filters' do
|
context 'when user is setting filters' do
|
||||||
|
|
|
@ -30,6 +30,8 @@ RSpec.describe 'Tag' do
|
||||||
let(:params) { {} }
|
let(:params) { {} }
|
||||||
let(:hashtag) { 'life' }
|
let(:hashtag) { 'life' }
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'profile'
|
||||||
|
|
||||||
context 'when given only one hashtag' do
|
context 'when given only one hashtag' do
|
||||||
let(:expected_statuses) { [life_status] }
|
let(:expected_statuses) { [life_status] }
|
||||||
|
|
||||||
|
@ -93,13 +95,15 @@ RSpec.describe 'Tag' do
|
||||||
Form::AdminSettings.new(timeline_preview: false).save
|
Form::AdminSettings.new(timeline_preview: false).save
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the user is not authenticated' do
|
it_behaves_like 'forbidden for wrong scope', 'profile'
|
||||||
|
|
||||||
|
context 'without an authentication token' do
|
||||||
let(:headers) { {} }
|
let(:headers) { {} }
|
||||||
|
|
||||||
it 'returns http unauthorized' do
|
it 'returns http unprocessable entity' do
|
||||||
subject
|
subject
|
||||||
|
|
||||||
expect(response).to have_http_status(401)
|
expect(response).to have_http_status(422)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue