Gearheads: v4.2.7-gh24050
This commit is contained in:
commit
41e76b7902
729 changed files with 19416 additions and 7622 deletions
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AccountsIndex < Chewy::Index
|
||||
include DatetimeClampingConcern
|
||||
|
||||
settings index: index_preset(refresh_interval: '30s'), analysis: {
|
||||
filter: {
|
||||
english_stop: {
|
||||
|
|
@ -21,12 +23,13 @@ class AccountsIndex < Chewy::Index
|
|||
|
||||
analyzer: {
|
||||
natural: {
|
||||
tokenizer: 'uax_url_email',
|
||||
tokenizer: 'standard',
|
||||
filter: %w(
|
||||
english_possessive_stemmer
|
||||
lowercase
|
||||
asciifolding
|
||||
cjk_width
|
||||
elision
|
||||
english_possessive_stemmer
|
||||
english_stop
|
||||
english_stemmer
|
||||
),
|
||||
|
|
@ -59,9 +62,9 @@ class AccountsIndex < Chewy::Index
|
|||
field(:following_count, type: 'long')
|
||||
field(:followers_count, type: 'long')
|
||||
field(:properties, type: 'keyword', value: ->(account) { account.searchable_properties })
|
||||
field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at })
|
||||
field(:last_status_at, type: 'date', value: ->(account) { clamp_date(account.last_status_at || account.created_at) })
|
||||
field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
|
||||
field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
|
||||
field(:text, type: 'text', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
|
||||
field(:text, type: 'text', analyzer: 'verbatim', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
14
app/chewy/concerns/datetime_clamping_concern.rb
Normal file
14
app/chewy/concerns/datetime_clamping_concern.rb
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module DatetimeClampingConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
MIN_ISO8601_DATETIME = '0000-01-01T00:00:00Z'.to_datetime.freeze
|
||||
MAX_ISO8601_DATETIME = '9999-12-31T23:59:59Z'.to_datetime.freeze
|
||||
|
||||
class_methods do
|
||||
def clamp_date(datetime)
|
||||
datetime.clamp(MIN_ISO8601_DATETIME, MAX_ISO8601_DATETIME)
|
||||
end
|
||||
end
|
||||
end
|
||||
69
app/chewy/public_statuses_index.rb
Normal file
69
app/chewy/public_statuses_index.rb
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PublicStatusesIndex < Chewy::Index
|
||||
include DatetimeClampingConcern
|
||||
|
||||
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
|
||||
filter: {
|
||||
english_stop: {
|
||||
type: 'stop',
|
||||
stopwords: '_english_',
|
||||
},
|
||||
|
||||
english_stemmer: {
|
||||
type: 'stemmer',
|
||||
language: 'english',
|
||||
},
|
||||
|
||||
english_possessive_stemmer: {
|
||||
type: 'stemmer',
|
||||
language: 'possessive_english',
|
||||
},
|
||||
},
|
||||
|
||||
analyzer: {
|
||||
verbatim: {
|
||||
tokenizer: 'uax_url_email',
|
||||
filter: %w(lowercase),
|
||||
},
|
||||
|
||||
content: {
|
||||
tokenizer: 'standard',
|
||||
filter: %w(
|
||||
lowercase
|
||||
asciifolding
|
||||
cjk_width
|
||||
elision
|
||||
english_possessive_stemmer
|
||||
english_stop
|
||||
english_stemmer
|
||||
),
|
||||
},
|
||||
|
||||
hashtag: {
|
||||
tokenizer: 'keyword',
|
||||
filter: %w(
|
||||
word_delimiter_graph
|
||||
lowercase
|
||||
asciifolding
|
||||
cjk_width
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
index_scope ::Status.unscoped
|
||||
.kept
|
||||
.indexable
|
||||
.includes(:media_attachments, :preloadable_poll, :preview_cards, :tags)
|
||||
|
||||
root date_detection: false do
|
||||
field(:id, type: 'long')
|
||||
field(:account_id, type: 'long')
|
||||
field(:text, type: 'text', analyzer: 'verbatim', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'content') }
|
||||
field(:tags, type: 'text', analyzer: 'hashtag', value: ->(status) { status.tags.map(&:display_name) })
|
||||
field(:language, type: 'keyword')
|
||||
field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
|
||||
field(:created_at, type: 'date', value: ->(status) { clamp_date(status.created_at) })
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class StatusesIndex < Chewy::Index
|
||||
include FormattingHelper
|
||||
include DatetimeClampingConcern
|
||||
|
||||
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
|
||||
filter: {
|
||||
|
|
@ -9,67 +9,59 @@ class StatusesIndex < Chewy::Index
|
|||
type: 'stop',
|
||||
stopwords: '_english_',
|
||||
},
|
||||
|
||||
english_stemmer: {
|
||||
type: 'stemmer',
|
||||
language: 'english',
|
||||
},
|
||||
|
||||
english_possessive_stemmer: {
|
||||
type: 'stemmer',
|
||||
language: 'possessive_english',
|
||||
},
|
||||
},
|
||||
|
||||
analyzer: {
|
||||
content: {
|
||||
verbatim: {
|
||||
tokenizer: 'uax_url_email',
|
||||
filter: %w(lowercase),
|
||||
},
|
||||
|
||||
content: {
|
||||
tokenizer: 'standard',
|
||||
filter: %w(
|
||||
english_possessive_stemmer
|
||||
lowercase
|
||||
asciifolding
|
||||
cjk_width
|
||||
elision
|
||||
english_possessive_stemmer
|
||||
english_stop
|
||||
english_stemmer
|
||||
),
|
||||
},
|
||||
|
||||
hashtag: {
|
||||
tokenizer: 'keyword',
|
||||
filter: %w(
|
||||
word_delimiter_graph
|
||||
lowercase
|
||||
asciifolding
|
||||
cjk_width
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# We do not use delete_if option here because it would call a method that we
|
||||
# expect to be called with crutches without crutches, causing n+1 queries
|
||||
index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll)
|
||||
|
||||
crutch :mentions do |collection|
|
||||
data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local, silent: false).pluck(:status_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
|
||||
crutch :favourites do |collection|
|
||||
data = ::Favourite.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
|
||||
crutch :reblogs do |collection|
|
||||
data = ::Status.where(reblog_of_id: collection.map(&:id)).where(account: Account.local).pluck(:reblog_of_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
|
||||
crutch :bookmarks do |collection|
|
||||
data = ::Bookmark.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
|
||||
crutch :votes do |collection|
|
||||
data = ::PollVote.joins(:poll).where(poll: { status_id: collection.map(&:id) }).where(account: Account.local).pluck(:status_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preview_cards, :local_mentioned, :local_favorited, :local_reblogged, :local_bookmarked, :tags, preloadable_poll: :local_voters), delete_if: ->(status) { status.searchable_by.empty? }
|
||||
|
||||
root date_detection: false do
|
||||
field :id, type: 'long'
|
||||
field :account_id, type: 'long'
|
||||
|
||||
field :text, type: 'text', value: ->(status) { status.searchable_text } do
|
||||
field :stemmed, type: 'text', analyzer: 'content'
|
||||
end
|
||||
|
||||
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
|
||||
field(:id, type: 'long')
|
||||
field(:account_id, type: 'long')
|
||||
field(:text, type: 'text', analyzer: 'verbatim', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'content') }
|
||||
field(:tags, type: 'text', analyzer: 'hashtag', value: ->(status) { status.tags.map(&:display_name) })
|
||||
field(:searchable_by, type: 'long', value: ->(status) { status.searchable_by })
|
||||
field(:language, type: 'keyword')
|
||||
field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
|
||||
field(:created_at, type: 'date', value: ->(status) { clamp_date(status.created_at) })
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,16 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class TagsIndex < Chewy::Index
|
||||
include DatetimeClampingConcern
|
||||
|
||||
settings index: index_preset(refresh_interval: '30s'), analysis: {
|
||||
analyzer: {
|
||||
content: {
|
||||
tokenizer: 'keyword',
|
||||
filter: %w(lowercase asciifolding cjk_width),
|
||||
filter: %w(
|
||||
word_delimiter_graph
|
||||
lowercase
|
||||
asciifolding
|
||||
cjk_width
|
||||
),
|
||||
},
|
||||
|
||||
edge_ngram: {
|
||||
tokenizer: 'edge_ngram',
|
||||
filter: %w(lowercase asciifolding cjk_width),
|
||||
filter: %w(
|
||||
lowercase
|
||||
asciifolding
|
||||
cjk_width
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -30,12 +41,9 @@ class TagsIndex < Chewy::Index
|
|||
end
|
||||
|
||||
root date_detection: false do
|
||||
field :name, type: 'text', analyzer: 'content' do
|
||||
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
||||
end
|
||||
|
||||
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
|
||||
field :usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts }
|
||||
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
|
||||
field(:name, type: 'text', analyzer: 'content', value: :display_name) { field(:edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content') }
|
||||
field(:reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? })
|
||||
field(:usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts })
|
||||
field(:last_status_at, type: 'date', value: ->(tag) { clamp_date(tag.last_status_at || tag.created_at) })
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ module Admin
|
|||
account_action.save!
|
||||
|
||||
if account_action.with_report?
|
||||
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: params[:report_id])
|
||||
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: resource_params[:report_id])
|
||||
else
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
|
|
|
|||
18
app/controllers/admin/software_updates_controller.rb
Normal file
18
app/controllers/admin/software_updates_controller.rb
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class SoftwareUpdatesController < BaseController
|
||||
before_action :check_enabled!
|
||||
|
||||
def index
|
||||
authorize :software_update, :index?
|
||||
@software_updates = SoftwareUpdate.all.sort_by(&:gem_version)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_enabled!
|
||||
not_found unless SoftwareUpdate.check_enabled?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -30,6 +30,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
|
|||
:bot,
|
||||
:discoverable,
|
||||
:hide_collections,
|
||||
:indexable,
|
||||
fields_attributes: [:name, :value]
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,6 +25,6 @@ class Api::V1::Accounts::NotesController < Api::BaseController
|
|||
end
|
||||
|
||||
def relationships_presenter
|
||||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id)
|
||||
AccountRelationshipsPresenter.new([@account], current_user.account_id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,6 +25,6 @@ class Api::V1::Accounts::PinsController < Api::BaseController
|
|||
end
|
||||
|
||||
def relationships_presenter
|
||||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id)
|
||||
AccountRelationshipsPresenter.new([@account], current_user.account_id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,11 +5,10 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
|
|||
before_action :require_user!
|
||||
|
||||
def index
|
||||
accounts = Account.without_suspended.where(id: account_ids).select('id')
|
||||
@accounts = Account.without_suspended.where(id: account_ids).select(:id, :domain).to_a
|
||||
# .where doesn't guarantee that our results are in the same order
|
||||
# we requested them, so return the "right" order to the requestor.
|
||||
@accounts = accounts.index_by(&:id).values_at(*account_ids).compact
|
||||
render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
render json: @accounts.index_by(&:id).values_at(*account_ids).compact, each_serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
end
|
||||
|
||||
def relationships(**options)
|
||||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, **options)
|
||||
AccountRelationshipsPresenter.new([@account], current_user.account_id, **options)
|
||||
end
|
||||
|
||||
def account_params
|
||||
|
|
|
|||
74
app/controllers/api/v1/admin/tags_controller.rb
Normal file
74
app/controllers/api/v1/admin/tags_controller.rb
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Admin::TagsController < Api::BaseController
|
||||
include Authorization
|
||||
before_action -> { authorize_if_got_token! :'admin:read' }, only: [:index, :show]
|
||||
before_action -> { authorize_if_got_token! :'admin:write' }, only: :update
|
||||
|
||||
before_action :set_tags, only: :index
|
||||
before_action :set_tag, except: :index
|
||||
|
||||
after_action :insert_pagination_headers, only: :index
|
||||
after_action :verify_authorized
|
||||
|
||||
LIMIT = 100
|
||||
PAGINATION_PARAMS = %i(limit).freeze
|
||||
|
||||
def index
|
||||
authorize :tag, :index?
|
||||
render json: @tags, each_serializer: REST::Admin::TagSerializer
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @tag, :show?
|
||||
render json: @tag, serializer: REST::Admin::TagSerializer
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @tag, :update?
|
||||
@tag.update!(tag_params.merge(reviewed_at: Time.now.utc))
|
||||
render json: @tag, serializer: REST::Admin::TagSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tag
|
||||
@tag = Tag.find(params[:id])
|
||||
end
|
||||
|
||||
def set_tags
|
||||
@tags = Tag.all.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||
end
|
||||
|
||||
def tag_params
|
||||
params.permit(:display_name, :trendable, :usable, :listable)
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
set_pagination_headers(next_path, prev_path)
|
||||
end
|
||||
|
||||
def next_path
|
||||
api_v1_admin_tags_url(pagination_params(max_id: pagination_max_id)) if records_continue?
|
||||
end
|
||||
|
||||
def prev_path
|
||||
api_v1_admin_tags_url(pagination_params(min_id: pagination_since_id)) unless @tags.empty?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@tags.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@tags.first.id
|
||||
end
|
||||
|
||||
def records_continue?
|
||||
@tags.size == limit_param(LIMIT)
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
|
||||
end
|
||||
end
|
||||
|
|
@ -16,7 +16,9 @@ class Api::V1::DirectoriesController < Api::BaseController
|
|||
end
|
||||
|
||||
def set_accounts
|
||||
@accounts = accounts_scope.offset(params[:offset]).limit(limit_param(DEFAULT_ACCOUNTS_LIMIT))
|
||||
with_read_replica do
|
||||
@accounts = accounts_scope.offset(params[:offset]).limit(limit_param(DEFAULT_ACCOUNTS_LIMIT))
|
||||
end
|
||||
end
|
||||
|
||||
def accounts_scope
|
||||
|
|
|
|||
|
|
@ -25,11 +25,11 @@ class Api::V1::FollowRequestsController < Api::BaseController
|
|||
private
|
||||
|
||||
def account
|
||||
Account.find(params[:id])
|
||||
@account ||= Account.find(params[:id])
|
||||
end
|
||||
|
||||
def relationships(**options)
|
||||
AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, **options)
|
||||
AccountRelationshipsPresenter.new([account], current_user.account_id, **options)
|
||||
end
|
||||
|
||||
def load_accounts
|
||||
|
|
|
|||
|
|
@ -41,5 +41,7 @@ class Api::V1::Peers::SearchController < Api::BaseController
|
|||
domain = TagManager.instance.normalize_domain(domain)
|
||||
@domains = Instance.searchable.where(Instance.arel_table[:domain].matches("#{Instance.sanitize_sql_like(domain)}%", false, true)).limit(10).pluck(:domain)
|
||||
end
|
||||
rescue Addressable::URI::InvalidURIError
|
||||
@domains = []
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,7 +8,15 @@ class Api::V1::Statuses::TranslationsController < Api::BaseController
|
|||
before_action :set_translation
|
||||
|
||||
rescue_from TranslationService::NotConfiguredError, with: :not_found
|
||||
rescue_from TranslationService::UnexpectedResponseError, TranslationService::QuotaExceededError, TranslationService::TooManyRequestsError, with: :service_unavailable
|
||||
rescue_from TranslationService::UnexpectedResponseError, with: :service_unavailable
|
||||
|
||||
rescue_from TranslationService::QuotaExceededError do
|
||||
render json: { error: I18n.t('translation.errors.quota_exceeded') }, status: 503
|
||||
end
|
||||
|
||||
rescue_from TranslationService::TooManyRequestsError do
|
||||
render json: { error: I18n.t('translation.errors.too_many_requests') }, status: 503
|
||||
end
|
||||
|
||||
def create
|
||||
render json: @translation, serializer: REST::TranslationSerializer
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
class Api::V1::StreamingController < Api::BaseController
|
||||
def index
|
||||
if Rails.configuration.x.streaming_api_base_url == request.host
|
||||
if same_host?
|
||||
not_found
|
||||
else
|
||||
redirect_to streaming_api_url, status: 301, allow_other_host: true
|
||||
|
|
@ -11,9 +11,16 @@ class Api::V1::StreamingController < Api::BaseController
|
|||
|
||||
private
|
||||
|
||||
def same_host?
|
||||
base_url = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url)
|
||||
request.host == base_url.host && request.port == (base_url.port || 80)
|
||||
end
|
||||
|
||||
def streaming_api_url
|
||||
Addressable::URI.parse(request.url).tap do |uri|
|
||||
uri.host = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url).host
|
||||
base_url = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url)
|
||||
uri.host = base_url.host
|
||||
uri.port = base_url.port
|
||||
end.to_s
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Timelines::TagController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :show, if: :require_auth?
|
||||
before_action :load_tag
|
||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||
|
||||
|
|
@ -12,6 +13,10 @@ class Api::V1::Timelines::TagController < Api::BaseController
|
|||
|
||||
private
|
||||
|
||||
def require_auth?
|
||||
!Setting.timeline_preview
|
||||
end
|
||||
|
||||
def load_tag
|
||||
@tag = Tag.find_normalized(params[:id])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base
|
|||
include CacheConcern
|
||||
include DomainControlHelper
|
||||
include DatabaseHelper
|
||||
include AuthorizedFetchHelper
|
||||
|
||||
helper_method :current_account
|
||||
helper_method :current_session
|
||||
|
|
@ -51,10 +52,6 @@ class ApplicationController < ActionController::Base
|
|||
|
||||
private
|
||||
|
||||
def authorized_fetch_mode?
|
||||
ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.limited_federation_mode
|
||||
end
|
||||
|
||||
def public_fetch_mode?
|
||||
!authorized_fetch_mode?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
def self.provides_callback_for(provider)
|
||||
define_method provider do
|
||||
@provider = provider
|
||||
@user = User.find_for_oauth(request.env['omniauth.auth'], current_user)
|
||||
@user = User.find_for_omniauth(request.env['omniauth.auth'], current_user)
|
||||
|
||||
if @user.persisted?
|
||||
record_login_activity
|
||||
|
|
@ -16,6 +16,9 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
session["devise.#{provider}_data"] = request.env['omniauth.auth']
|
||||
redirect_to new_user_registration_url
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
flash[:alert] = I18n.t('devise.failure.omniauth_user_creation_failure') if is_navigational_format?
|
||||
redirect_to new_user_session_url
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Auth::SessionsController < Devise::SessionsController
|
||||
include Redisable
|
||||
|
||||
MAX_2FA_ATTEMPTS_PER_HOUR = 10
|
||||
|
||||
layout 'auth'
|
||||
|
||||
skip_before_action :require_no_authentication, only: [:create]
|
||||
|
|
@ -134,9 +138,23 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
session.delete(:attempt_user_updated_at)
|
||||
end
|
||||
|
||||
def clear_2fa_attempt_from_user(user)
|
||||
redis.del(second_factor_attempts_key(user))
|
||||
end
|
||||
|
||||
def check_second_factor_rate_limits(user)
|
||||
attempts, = redis.multi do |multi|
|
||||
multi.incr(second_factor_attempts_key(user))
|
||||
multi.expire(second_factor_attempts_key(user), 1.hour)
|
||||
end
|
||||
|
||||
attempts >= MAX_2FA_ATTEMPTS_PER_HOUR
|
||||
end
|
||||
|
||||
def on_authentication_success(user, security_measure)
|
||||
@on_authentication_success_called = true
|
||||
|
||||
clear_2fa_attempt_from_user(user)
|
||||
clear_attempt_from_session
|
||||
|
||||
user.update_sign_in!(new_sign_in: true)
|
||||
|
|
@ -168,4 +186,8 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
user_agent: request.user_agent
|
||||
)
|
||||
end
|
||||
|
||||
def second_factor_attempts_key(user)
|
||||
"2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -91,14 +91,23 @@ module SignatureVerification
|
|||
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
|
||||
|
||||
signature = Base64.decode64(signature_params['signature'])
|
||||
compare_signed_string = build_signed_string
|
||||
compare_signed_string = build_signed_string(include_query_string: true)
|
||||
|
||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
||||
|
||||
# Compatibility quirk with older Mastodon versions
|
||||
compare_signed_string = build_signed_string(include_query_string: false)
|
||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
||||
|
||||
actor = stoplight_wrap_request { actor_refresh_key!(actor) }
|
||||
|
||||
raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil?
|
||||
|
||||
compare_signed_string = build_signed_string(include_query_string: true)
|
||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
||||
|
||||
# Compatibility quirk with older Mastodon versions
|
||||
compare_signed_string = build_signed_string(include_query_string: false)
|
||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
||||
|
||||
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)", signed_string: compare_signed_string, signature: signature_params['signature']
|
||||
|
|
@ -119,6 +128,8 @@ module SignatureVerification
|
|||
private
|
||||
|
||||
def fail_with!(message, **options)
|
||||
Rails.logger.debug { "Signature verification failed: #{message}" }
|
||||
|
||||
@signature_verification_failure_reason = { error: message }.merge(options)
|
||||
@signed_request_actor = nil
|
||||
end
|
||||
|
|
@ -178,11 +189,18 @@ module SignatureVerification
|
|||
nil
|
||||
end
|
||||
|
||||
def build_signed_string
|
||||
def build_signed_string(include_query_string: true)
|
||||
signed_headers.map do |signed_header|
|
||||
case signed_header
|
||||
when Request::REQUEST_TARGET
|
||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
||||
if include_query_string
|
||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}"
|
||||
else
|
||||
# Current versions of Mastodon incorrectly omit the query string from the (request-target) pseudo-header.
|
||||
# Therefore, temporarily support such incorrect signatures for compatibility.
|
||||
# TODO: remove eventually some time after release of the fixed version
|
||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
||||
end
|
||||
when '(created)'
|
||||
raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
|
||||
raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank?
|
||||
|
|
@ -248,7 +266,7 @@ module SignatureVerification
|
|||
stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) }
|
||||
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
|
||||
account = ActivityPub::TagManager.instance.uri_to_actor(key_id)
|
||||
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) }
|
||||
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) }
|
||||
account
|
||||
end
|
||||
rescue Mastodon::PrivateNetworkAddressError => e
|
||||
|
|
|
|||
|
|
@ -65,6 +65,11 @@ module TwoFactorAuthenticationConcern
|
|||
end
|
||||
|
||||
def authenticate_with_two_factor_via_otp(user)
|
||||
if check_second_factor_rate_limits(user)
|
||||
flash.now[:alert] = I18n.t('users.rate_limited')
|
||||
return prompt_for_two_factor(user)
|
||||
end
|
||||
|
||||
if valid_otp_attempt?(user)
|
||||
on_authentication_success(user, :otp)
|
||||
else
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ module WebAppControllerConcern
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
prepend_before_action :redirect_unauthenticated_to_permalinks!
|
||||
before_action :set_app_body_class
|
||||
|
||||
vary_by 'Accept, Accept-Language, Cookie'
|
||||
|
||||
before_action :redirect_unauthenticated_to_permalinks!
|
||||
before_action :set_app_body_class
|
||||
end
|
||||
|
||||
def skip_csrf_meta_tags?
|
||||
!(ENV['OMNIAUTH_ONLY'] == 'true' && Devise.omniauth_providers.length == 1) && current_user.nil?
|
||||
!(ENV['ONE_CLICK_SSO_LOGIN'] == 'true' && ENV['OMNIAUTH_ONLY'] == 'true' && Devise.omniauth_providers.length == 1) && current_user.nil?
|
||||
end
|
||||
|
||||
def set_app_body_class
|
||||
|
|
@ -22,7 +22,9 @@ module WebAppControllerConcern
|
|||
return if user_signed_in? && current_account.moved_to_account_id.nil?
|
||||
|
||||
redirect_path = PermalinkRedirector.new(request.path).redirect_path
|
||||
return if redirect_path.blank?
|
||||
|
||||
redirect_to(redirect_path) if redirect_path.present?
|
||||
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
|
||||
redirect_to(redirect_path)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
class FollowerAccountsController < ApplicationController
|
||||
include AccountControllerConcern
|
||||
include SignatureVerification
|
||||
include WebAppControllerConcern
|
||||
|
||||
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
class FollowingAccountsController < ApplicationController
|
||||
include AccountControllerConcern
|
||||
include SignatureVerification
|
||||
include WebAppControllerConcern
|
||||
|
||||
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class RelationshipsController < ApplicationController
|
|||
end
|
||||
|
||||
def set_relationships
|
||||
@relationships = AccountRelationshipsPresenter.new(@accounts.pluck(:id), current_user.account_id)
|
||||
@relationships = AccountRelationshipsPresenter.new(@accounts, current_user.account_id)
|
||||
end
|
||||
|
||||
def form_account_batch_params
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class Settings::PrivacyController < Settings::BaseController
|
|||
private
|
||||
|
||||
def account_params
|
||||
params.require(:account).permit(:discoverable, :unlocked, :show_collections, settings: UserSettings.keys)
|
||||
params.require(:account).permit(:discoverable, :unlocked, :indexable, :show_collections, settings: UserSettings.keys)
|
||||
end
|
||||
|
||||
def set_account
|
||||
|
|
|
|||
11
app/helpers/authorized_fetch_helper.rb
Normal file
11
app/helpers/authorized_fetch_helper.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AuthorizedFetchHelper
|
||||
def authorized_fetch_mode?
|
||||
ENV.fetch('AUTHORIZED_FETCH') { Setting.authorized_fetch && 'true' } == 'true' || Rails.configuration.x.limited_federation_mode
|
||||
end
|
||||
|
||||
def authorized_fetch_overridden?
|
||||
ENV.key?('AUTHORIZED_FETCH') || Rails.configuration.x.limited_federation_mode
|
||||
end
|
||||
end
|
||||
|
|
@ -21,6 +21,7 @@ module ContextHelper
|
|||
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
|
||||
discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
|
||||
indexable: { 'toot' => 'http://joinmastodon.org/ns#', 'indexable' => 'toot:indexable' },
|
||||
memorial: { 'toot' => 'http://joinmastodon.org/ns#', 'memorial' => 'toot:memorial' },
|
||||
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
|
||||
olm: {
|
||||
'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId',
|
||||
|
|
|
|||
|
|
@ -1,11 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module DatabaseHelper
|
||||
def replica_enabled?
|
||||
ENV['REPLICA_DB_NAME'] || ENV.fetch('REPLICA_DATABASE_URL', nil)
|
||||
end
|
||||
module_function :replica_enabled?
|
||||
|
||||
def with_read_replica(&block)
|
||||
ApplicationRecord.connected_to(role: :reading, prevent_writes: true, &block)
|
||||
if replica_enabled?
|
||||
ApplicationRecord.connected_to(role: :reading, prevent_writes: true, &block)
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def with_primary(&block)
|
||||
ApplicationRecord.connected_to(role: :writing, &block)
|
||||
if replica_enabled?
|
||||
ApplicationRecord.connected_to(role: :writing, &block)
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -155,8 +155,8 @@ module JsonLdHelper
|
|||
end
|
||||
end
|
||||
|
||||
def fetch_resource(uri, id, on_behalf_of = nil)
|
||||
unless id
|
||||
def fetch_resource(uri, id_is_known, on_behalf_of = nil, request_options: {})
|
||||
unless id_is_known
|
||||
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
||||
|
||||
return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id'])
|
||||
|
|
@ -164,17 +164,29 @@ module JsonLdHelper
|
|||
uri = json['id']
|
||||
end
|
||||
|
||||
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
||||
json = fetch_resource_without_id_validation(uri, on_behalf_of, request_options: request_options)
|
||||
json.present? && json['id'] == uri ? json : nil
|
||||
end
|
||||
|
||||
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false)
|
||||
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {})
|
||||
on_behalf_of ||= Account.representative
|
||||
|
||||
build_request(uri, on_behalf_of).perform do |response|
|
||||
build_request(uri, on_behalf_of, options: request_options).perform do |response|
|
||||
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
|
||||
|
||||
body_to_json(response.body_with_limit) if response.code == 200
|
||||
body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response)
|
||||
end
|
||||
end
|
||||
|
||||
def valid_activitypub_content_type?(response)
|
||||
return true if response.mime_type == 'application/activity+json'
|
||||
|
||||
# When the mime type is `application/ld+json`, we need to check the profile,
|
||||
# but `http.rb` does not parse it for us.
|
||||
return false unless response.mime_type == 'application/ld+json'
|
||||
|
||||
response.headers[HTTP::Headers::CONTENT_TYPE]&.split(';')&.map(&:strip)&.any? do |str|
|
||||
str.start_with?('profile="') && str[9...-1].split.include?('https://www.w3.org/ns/activitystreams')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -204,8 +216,8 @@ module JsonLdHelper
|
|||
response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code))
|
||||
end
|
||||
|
||||
def build_request(uri, on_behalf_of = nil)
|
||||
Request.new(:get, uri).tap do |request|
|
||||
def build_request(uri, on_behalf_of = nil, options: {})
|
||||
Request.new(:get, uri, **options).tap do |request|
|
||||
request.on_behalf_of(on_behalf_of) if on_behalf_of
|
||||
request.add_headers('Accept' => 'application/activity+json, application/ld+json')
|
||||
end
|
||||
|
|
|
|||
|
|
@ -188,11 +188,11 @@ module LanguagesHelper
|
|||
|
||||
ISO_639_3 = {
|
||||
ast: ['Asturian', 'Asturianu'].freeze,
|
||||
chr: ['Cherokee', 'ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ'].freeze,
|
||||
ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze,
|
||||
cnr: ['Montenegrin', 'crnogorski'].freeze,
|
||||
jbo: ['Lojban', 'la .lojban.'].freeze,
|
||||
kab: ['Kabyle', 'Taqbaylit'].freeze,
|
||||
kmr: ['Kurmanji (Kurdish)', 'Kurmancî'].freeze,
|
||||
ldn: ['Láadan', 'Láadan'].freeze,
|
||||
lfn: ['Lingua Franca Nova', 'lingua franca nova'].freeze,
|
||||
sco: ['Scots', 'Scots'].freeze,
|
||||
|
|
@ -200,6 +200,7 @@ module LanguagesHelper
|
|||
smj: ['Lule Sami', 'Julevsámegiella'].freeze,
|
||||
szl: ['Silesian', 'ślůnsko godka'].freeze,
|
||||
tok: ['Toki Pona', 'toki pona'].freeze,
|
||||
xal: ['Kalmyk', 'Хальмг келн'].freeze,
|
||||
zba: ['Balaibalan', 'باليبلن'].freeze,
|
||||
zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze,
|
||||
}.freeze
|
||||
|
|
@ -253,6 +254,7 @@ module LanguagesHelper
|
|||
|
||||
def valid_locale_or_nil(str)
|
||||
return if str.blank?
|
||||
return str if valid_locale?(str)
|
||||
|
||||
code, = str.to_s.split(/[_-]/) # Strip out the region from e.g. en_US or ja-JP
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ module MediaComponentHelper
|
|||
blurhash: video.blurhash,
|
||||
frameRate: meta.dig('original', 'frame_rate'),
|
||||
inline: true,
|
||||
aspectRatio: "#{meta.dig('original', 'width')} / #{meta.dig('original', 'height')}",
|
||||
media: [
|
||||
serialize_media_attachment(video),
|
||||
].as_json,
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
import api from '../api';
|
||||
|
||||
export const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
|
||||
export const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS';
|
||||
export const ACCOUNT_NOTE_SUBMIT_FAIL = 'ACCOUNT_NOTE_SUBMIT_FAIL';
|
||||
|
||||
export function submitAccountNote(id, value) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(submitAccountNoteRequest());
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/note`, {
|
||||
comment: value,
|
||||
}).then(response => {
|
||||
dispatch(submitAccountNoteSuccess(response.data));
|
||||
}).catch(error => dispatch(submitAccountNoteFail(error)));
|
||||
};
|
||||
}
|
||||
|
||||
export function submitAccountNoteRequest() {
|
||||
return {
|
||||
type: ACCOUNT_NOTE_SUBMIT_REQUEST,
|
||||
};
|
||||
}
|
||||
|
||||
export function submitAccountNoteSuccess(relationship) {
|
||||
return {
|
||||
type: ACCOUNT_NOTE_SUBMIT_SUCCESS,
|
||||
relationship,
|
||||
};
|
||||
}
|
||||
|
||||
export function submitAccountNoteFail(error) {
|
||||
return {
|
||||
type: ACCOUNT_NOTE_SUBMIT_FAIL,
|
||||
error,
|
||||
};
|
||||
}
|
||||
18
app/javascript/mastodon/actions/account_notes.ts
Normal file
18
app/javascript/mastodon/actions/account_notes.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
import api from '../api';
|
||||
|
||||
export const submitAccountNote = createAppAsyncThunk(
|
||||
'account_note/submit',
|
||||
async (args: { id: string; value: string }, { getState }) => {
|
||||
// TODO: replace `unknown` with `ApiRelationshipJSON` when it is merged
|
||||
const response = await api(getState).post<unknown>(
|
||||
`/api/v1/accounts/${args.id}/note`,
|
||||
{
|
||||
comment: args.value,
|
||||
},
|
||||
);
|
||||
|
||||
return { relationship: response.data };
|
||||
},
|
||||
);
|
||||
|
|
@ -84,6 +84,7 @@ const messages = defineMessages({
|
|||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||
open: { id: 'compose.published.open', defaultMessage: 'Open' },
|
||||
published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
|
||||
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
|
||||
});
|
||||
|
||||
export const ensureComposeIsVisible = (getState, routerHistory) => {
|
||||
|
|
@ -244,7 +245,7 @@ export function submitCompose(routerHistory) {
|
|||
}
|
||||
|
||||
dispatch(showAlert({
|
||||
message: messages.published,
|
||||
message: statusId === null ? messages.published : messages.saved,
|
||||
action: messages.open,
|
||||
dismissAfter: 10000,
|
||||
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
import api from '../api';
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||
|
||||
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
|
||||
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
|
||||
export const REBLOG_FAIL = 'REBLOG_FAIL';
|
||||
|
||||
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
|
||||
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
|
||||
export const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL';
|
||||
|
||||
export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
|
||||
export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
|
||||
export const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
|
||||
|
|
@ -26,6 +31,10 @@ export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
|
|||
export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
|
||||
export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
|
||||
|
||||
export const FAVOURITES_EXPAND_REQUEST = 'FAVOURITES_EXPAND_REQUEST';
|
||||
export const FAVOURITES_EXPAND_SUCCESS = 'FAVOURITES_EXPAND_SUCCESS';
|
||||
export const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL';
|
||||
|
||||
export const PIN_REQUEST = 'PIN_REQUEST';
|
||||
export const PIN_SUCCESS = 'PIN_SUCCESS';
|
||||
export const PIN_FAIL = 'PIN_FAIL';
|
||||
|
|
@ -273,8 +282,10 @@ export function fetchReblogs(id) {
|
|||
dispatch(fetchReblogsRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchReblogsSuccess(id, response.data));
|
||||
dispatch(fetchReblogsSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(fetchReblogsFail(id, error));
|
||||
});
|
||||
|
|
@ -288,17 +299,62 @@ export function fetchReblogsRequest(id) {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchReblogsSuccess(id, accounts) {
|
||||
export function fetchReblogsSuccess(id, accounts, next) {
|
||||
return {
|
||||
type: REBLOGS_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchReblogsFail(id, error) {
|
||||
return {
|
||||
type: REBLOGS_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandReblogs(id) {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['user_lists', 'reblogged_by', id, 'next']);
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandReblogsRequest(id));
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(expandReblogsSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
}).catch(error => dispatch(expandReblogsFail(id, error)));
|
||||
};
|
||||
}
|
||||
|
||||
export function expandReblogsRequest(id) {
|
||||
return {
|
||||
type: REBLOGS_EXPAND_REQUEST,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandReblogsSuccess(id, accounts, next) {
|
||||
return {
|
||||
type: REBLOGS_EXPAND_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandReblogsFail(id, error) {
|
||||
return {
|
||||
type: REBLOGS_EXPAND_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
|
@ -308,8 +364,10 @@ export function fetchFavourites(id) {
|
|||
dispatch(fetchFavouritesRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchFavouritesSuccess(id, response.data));
|
||||
dispatch(fetchFavouritesSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(fetchFavouritesFail(id, error));
|
||||
});
|
||||
|
|
@ -323,17 +381,62 @@ export function fetchFavouritesRequest(id) {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchFavouritesSuccess(id, accounts) {
|
||||
export function fetchFavouritesSuccess(id, accounts, next) {
|
||||
return {
|
||||
type: FAVOURITES_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchFavouritesFail(id, error) {
|
||||
return {
|
||||
type: FAVOURITES_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandFavourites(id) {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['user_lists', 'favourited_by', id, 'next']);
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandFavouritesRequest(id));
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(expandFavouritesSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
}).catch(error => dispatch(expandFavouritesFail(id, error)));
|
||||
};
|
||||
}
|
||||
|
||||
export function expandFavouritesRequest(id) {
|
||||
return {
|
||||
type: FAVOURITES_EXPAND_REQUEST,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandFavouritesSuccess(id, accounts, next) {
|
||||
return {
|
||||
type: FAVOURITES_EXPAND_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandFavouritesFail(id, error) {
|
||||
return {
|
||||
type: FAVOURITES_EXPAND_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
importFetchedStatuses,
|
||||
} from './importer';
|
||||
import { submitMarkers } from './markers';
|
||||
import { register as registerPushNotifications } from './push_notifications';
|
||||
import { saveSettings } from './settings';
|
||||
|
||||
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
||||
|
|
@ -293,6 +294,10 @@ export function requestBrowserPermission(callback = noOp) {
|
|||
requestNotificationPermission((permission) => {
|
||||
dispatch(setBrowserPermission(permission));
|
||||
callback(permission);
|
||||
|
||||
if (permission === 'granted') {
|
||||
dispatch(registerPushNotifications());
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
import { fromJS } from 'immutable';
|
||||
|
||||
import { searchHistory } from 'mastodon/settings';
|
||||
|
||||
import api from '../api';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
|
|
@ -15,8 +19,7 @@ export const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST';
|
|||
export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS';
|
||||
export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL';
|
||||
|
||||
export const SEARCH_RESULT_CLICK = 'SEARCH_RESULT_CLICK';
|
||||
export const SEARCH_RESULT_FORGET = 'SEARCH_RESULT_FORGET';
|
||||
export const SEARCH_HISTORY_UPDATE = 'SEARCH_HISTORY_UPDATE';
|
||||
|
||||
export function changeSearch(value) {
|
||||
return {
|
||||
|
|
@ -37,17 +40,17 @@ export function submitSearch(type) {
|
|||
const signedIn = !!getState().getIn(['meta', 'me']);
|
||||
|
||||
if (value.length === 0) {
|
||||
dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, ''));
|
||||
dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, '', type));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchSearchRequest());
|
||||
dispatch(fetchSearchRequest(type));
|
||||
|
||||
api(getState).get('/api/v2/search', {
|
||||
params: {
|
||||
q: value,
|
||||
resolve: signedIn,
|
||||
limit: 5,
|
||||
limit: 11,
|
||||
type,
|
||||
},
|
||||
}).then(response => {
|
||||
|
|
@ -59,7 +62,7 @@ export function submitSearch(type) {
|
|||
dispatch(importFetchedStatuses(response.data.statuses));
|
||||
}
|
||||
|
||||
dispatch(fetchSearchSuccess(response.data, value));
|
||||
dispatch(fetchSearchSuccess(response.data, value, type));
|
||||
dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(fetchSearchFail(error));
|
||||
|
|
@ -67,16 +70,18 @@ export function submitSearch(type) {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchSearchRequest() {
|
||||
export function fetchSearchRequest(searchType) {
|
||||
return {
|
||||
type: SEARCH_FETCH_REQUEST,
|
||||
searchType,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchSearchSuccess(results, searchTerm) {
|
||||
export function fetchSearchSuccess(results, searchTerm, searchType) {
|
||||
return {
|
||||
type: SEARCH_FETCH_SUCCESS,
|
||||
results,
|
||||
searchType,
|
||||
searchTerm,
|
||||
};
|
||||
}
|
||||
|
|
@ -90,15 +95,16 @@ export function fetchSearchFail(error) {
|
|||
|
||||
export const expandSearch = type => (dispatch, getState) => {
|
||||
const value = getState().getIn(['search', 'value']);
|
||||
const offset = getState().getIn(['search', 'results', type]).size;
|
||||
const offset = getState().getIn(['search', 'results', type]).size - 1;
|
||||
|
||||
dispatch(expandSearchRequest());
|
||||
dispatch(expandSearchRequest(type));
|
||||
|
||||
api(getState).get('/api/v2/search', {
|
||||
params: {
|
||||
q: value,
|
||||
type,
|
||||
offset,
|
||||
limit: 11,
|
||||
},
|
||||
}).then(({ data }) => {
|
||||
if (data.accounts) {
|
||||
|
|
@ -116,8 +122,9 @@ export const expandSearch = type => (dispatch, getState) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const expandSearchRequest = () => ({
|
||||
export const expandSearchRequest = (searchType) => ({
|
||||
type: SEARCH_EXPAND_REQUEST,
|
||||
searchType,
|
||||
});
|
||||
|
||||
export const expandSearchSuccess = (results, searchTerm, searchType) => ({
|
||||
|
|
@ -140,6 +147,10 @@ export const openURL = (value, history, onFailure) => (dispatch, getState) => {
|
|||
const signedIn = !!getState().getIn(['meta', 'me']);
|
||||
|
||||
if (!signedIn) {
|
||||
if (onFailure) {
|
||||
onFailure();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -166,16 +177,34 @@ export const openURL = (value, history, onFailure) => (dispatch, getState) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const clickSearchResult = (q, type) => ({
|
||||
type: SEARCH_RESULT_CLICK,
|
||||
export const clickSearchResult = (q, type) => (dispatch, getState) => {
|
||||
const previous = getState().getIn(['search', 'recent']);
|
||||
const me = getState().getIn(['meta', 'me']);
|
||||
const current = previous.add(fromJS({ type, q })).takeLast(4);
|
||||
|
||||
result: {
|
||||
type,
|
||||
q,
|
||||
},
|
||||
searchHistory.set(me, current.toJS());
|
||||
dispatch(updateSearchHistory(current));
|
||||
};
|
||||
|
||||
export const forgetSearchResult = q => (dispatch, getState) => {
|
||||
const previous = getState().getIn(['search', 'recent']);
|
||||
const me = getState().getIn(['meta', 'me']);
|
||||
const current = previous.filterNot(result => result.get('q') === q);
|
||||
|
||||
searchHistory.set(me, current.toJS());
|
||||
dispatch(updateSearchHistory(current));
|
||||
};
|
||||
|
||||
export const updateSearchHistory = recent => ({
|
||||
type: SEARCH_HISTORY_UPDATE,
|
||||
recent,
|
||||
});
|
||||
|
||||
export const forgetSearchResult = q => ({
|
||||
type: SEARCH_RESULT_FORGET,
|
||||
q,
|
||||
});
|
||||
export const hydrateSearch = () => (dispatch, getState) => {
|
||||
const me = getState().getIn(['meta', 'me']);
|
||||
const history = searchHistory.get(me);
|
||||
|
||||
if (history !== null) {
|
||||
dispatch(updateSearchHistory(history));
|
||||
}
|
||||
};
|
||||
|
|
@ -2,6 +2,7 @@ import { Iterable, fromJS } from 'immutable';
|
|||
|
||||
import { hydrateCompose } from './compose';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
import { hydrateSearch } from './search';
|
||||
|
||||
export const STORE_HYDRATE = 'STORE_HYDRATE';
|
||||
export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
|
||||
|
|
@ -20,6 +21,7 @@ export function hydrateStore(rawState) {
|
|||
});
|
||||
|
||||
dispatch(hydrateCompose());
|
||||
dispatch(hydrateSearch());
|
||||
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
import axios from 'axios';
|
||||
import LinkHeader from 'http-link-header';
|
||||
|
||||
import ready from './ready';
|
||||
|
||||
/**
|
||||
* @param {import('axios').AxiosResponse} response
|
||||
* @returns {LinkHeader}
|
||||
*/
|
||||
export const getLinks = response => {
|
||||
const value = response.headers.link;
|
||||
|
||||
if (!value) {
|
||||
return new LinkHeader();
|
||||
}
|
||||
|
||||
return LinkHeader.parse(value);
|
||||
};
|
||||
|
||||
/** @type {import('axios').RawAxiosRequestHeaders} */
|
||||
const csrfHeader = {};
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
const setCSRFHeader = () => {
|
||||
/** @type {HTMLMetaElement | null} */
|
||||
const csrfToken = document.querySelector('meta[name=csrf-token]');
|
||||
|
||||
if (csrfToken) {
|
||||
csrfHeader['X-CSRF-Token'] = csrfToken.content;
|
||||
}
|
||||
};
|
||||
|
||||
ready(setCSRFHeader);
|
||||
|
||||
/**
|
||||
* @param {() => import('immutable').Map<string,any>} getState
|
||||
* @returns {import('axios').RawAxiosRequestHeaders}
|
||||
*/
|
||||
const authorizationHeaderFromState = getState => {
|
||||
const accessToken = getState && getState().getIn(['meta', 'access_token'], '');
|
||||
|
||||
if (!accessToken) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {() => import('immutable').Map<string,any>} getState
|
||||
* @returns {import('axios').AxiosInstance}
|
||||
*/
|
||||
export default function api(getState) {
|
||||
return axios.create({
|
||||
headers: {
|
||||
...csrfHeader,
|
||||
...authorizationHeaderFromState(getState),
|
||||
},
|
||||
|
||||
transformResponse: [
|
||||
function (data) {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return data;
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
63
app/javascript/mastodon/api.ts
Normal file
63
app/javascript/mastodon/api.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import type { AxiosResponse, RawAxiosRequestHeaders } from 'axios';
|
||||
import axios from 'axios';
|
||||
import LinkHeader from 'http-link-header';
|
||||
|
||||
import ready from './ready';
|
||||
import type { GetState } from './store';
|
||||
|
||||
export const getLinks = (response: AxiosResponse) => {
|
||||
const value = response.headers.link as string | undefined;
|
||||
|
||||
if (!value) {
|
||||
return new LinkHeader();
|
||||
}
|
||||
|
||||
return LinkHeader.parse(value);
|
||||
};
|
||||
|
||||
const csrfHeader: RawAxiosRequestHeaders = {};
|
||||
|
||||
const setCSRFHeader = () => {
|
||||
const csrfToken = document.querySelector<HTMLMetaElement>(
|
||||
'meta[name=csrf-token]',
|
||||
);
|
||||
|
||||
if (csrfToken) {
|
||||
csrfHeader['X-CSRF-Token'] = csrfToken.content;
|
||||
}
|
||||
};
|
||||
|
||||
void ready(setCSRFHeader);
|
||||
|
||||
const authorizationHeaderFromState = (getState?: GetState) => {
|
||||
const accessToken =
|
||||
getState && (getState().meta.get('access_token', '') as string);
|
||||
|
||||
if (!accessToken) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
} as RawAxiosRequestHeaders;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function api(getState: GetState) {
|
||||
return axios.create({
|
||||
headers: {
|
||||
...csrfHeader,
|
||||
...authorizationHeaderFromState(getState),
|
||||
},
|
||||
|
||||
transformResponse: [
|
||||
function (data: unknown) {
|
||||
try {
|
||||
return JSON.parse(data as string) as unknown;
|
||||
} catch {
|
||||
return data;
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
|
@ -45,6 +45,21 @@ describe('computeHashtagBarForStatus', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('does not truncate the contents when the last child is a text node', () => {
|
||||
const status = createStatus(
|
||||
'this is a #<a class="zrl" href="https://example.com/search?tag=test">test</a>. Some more text',
|
||||
['test'],
|
||||
);
|
||||
|
||||
const { hashtagsInBar, statusContentProps } =
|
||||
computeHashtagBarForStatus(status);
|
||||
|
||||
expect(hashtagsInBar).toEqual([]);
|
||||
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||
`"this is a #<a class="zrl" href="https://example.com/search?tag=test">test</a>. Some more text"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('extract tags from the last line', () => {
|
||||
const status = createStatus(
|
||||
'<p>Simple text</p><p><a href="test">#hashtag</a></p>',
|
||||
|
|
@ -105,6 +120,21 @@ describe('computeHashtagBarForStatus', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('handles server-side normalized tags with accentuated characters', () => {
|
||||
const status = createStatus(
|
||||
'<p>Text</p><p><a href="test">#éaa</a> <a href="test">#Éaa</a></p>',
|
||||
['eaa'], // The server may normalize the hashtags in the `tags` attribute
|
||||
);
|
||||
|
||||
const { hashtagsInBar, statusContentProps } =
|
||||
computeHashtagBarForStatus(status);
|
||||
|
||||
expect(hashtagsInBar).toEqual(['Éaa']);
|
||||
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||
`"<p>Text</p>"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not display in bar a hashtag in content with a case difference', () => {
|
||||
const status = createStatus(
|
||||
'<p>Text <a href="test">#Éaa</a></p><p><a href="test">#éaa</a></p>',
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@ import api from 'mastodon/api';
|
|||
import { roundTo10 } from 'mastodon/utils/numbers';
|
||||
|
||||
const dateForCohort = cohort => {
|
||||
const timeZone = 'UTC';
|
||||
switch(cohort.frequency) {
|
||||
case 'day':
|
||||
return <FormattedDate value={cohort.period} month='long' day='2-digit' />;
|
||||
return <FormattedDate value={cohort.period} month='long' day='2-digit' timeZone={timeZone} />;
|
||||
default:
|
||||
return <FormattedDate value={cohort.period} month='long' year='numeric' />;
|
||||
return <FormattedDate value={cohort.period} month='long' year='numeric' timeZone={timeZone} />;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -6,21 +6,10 @@ import { reduceMotion } from '../initial_state';
|
|||
|
||||
import { ShortNumber } from './short_number';
|
||||
|
||||
const obfuscatedCount = (count: number) => {
|
||||
if (count < 0) {
|
||||
return 0;
|
||||
} else if (count <= 1) {
|
||||
return count;
|
||||
} else {
|
||||
return '1+';
|
||||
}
|
||||
};
|
||||
|
||||
interface Props {
|
||||
value: number;
|
||||
obfuscate?: boolean;
|
||||
}
|
||||
export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => {
|
||||
export const AnimatedNumber: React.FC<Props> = ({ value }) => {
|
||||
const [previousValue, setPreviousValue] = useState(value);
|
||||
const [direction, setDirection] = useState<1 | -1>(1);
|
||||
|
||||
|
|
@ -36,11 +25,7 @@ export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => {
|
|||
);
|
||||
|
||||
if (reduceMotion) {
|
||||
return obfuscate ? (
|
||||
<>{obfuscatedCount(value)}</>
|
||||
) : (
|
||||
<ShortNumber value={value} />
|
||||
);
|
||||
return <ShortNumber value={value} />;
|
||||
}
|
||||
|
||||
const styles = [
|
||||
|
|
@ -67,11 +52,7 @@ export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => {
|
|||
transform: `translateY(${style.y * 100}%)`,
|
||||
}}
|
||||
>
|
||||
{obfuscate ? (
|
||||
obfuscatedCount(data as number)
|
||||
) : (
|
||||
<ShortNumber value={data as number} />
|
||||
)}
|
||||
<ShortNumber value={data as number} />
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,13 @@ export default class Column extends PureComponent {
|
|||
};
|
||||
|
||||
scrollTop () {
|
||||
const scrollable = this.props.bindToDocument ? document.scrollingElement : this.node.querySelector('.scrollable');
|
||||
let scrollable = null;
|
||||
|
||||
if (this.props.bindToDocument) {
|
||||
scrollable = document.scrollingElement;
|
||||
} else {
|
||||
scrollable = this.node.querySelector('.scrollable');
|
||||
}
|
||||
|
||||
if (!scrollable) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-call,
|
||||
@typescript-eslint/no-unsafe-return,
|
||||
@typescript-eslint/no-unsafe-assignment,
|
||||
@typescript-eslint/no-unsafe-member-access
|
||||
-- the settings store is not yet typed */
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useState, useEffect } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { changeSetting } from 'mastodon/actions/settings';
|
||||
import { bannerSettings } from 'mastodon/settings';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import { IconButton } from './icon_button';
|
||||
|
||||
|
|
@ -19,13 +26,25 @@ export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
|
|||
id,
|
||||
children,
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(!bannerSettings.get(id));
|
||||
const dismissed = useAppSelector((state) =>
|
||||
state.settings.getIn(['dismissed_banners', id], false),
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [visible, setVisible] = useState(!bannerSettings.get(id) && !dismissed);
|
||||
const intl = useIntl();
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
setVisible(false);
|
||||
bannerSettings.set(id, true);
|
||||
}, [id]);
|
||||
dispatch(changeSetting(['dismissed_banners', id], true));
|
||||
}, [id, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible && !dismissed) {
|
||||
dispatch(changeSetting(['dismissed_banners', id], true));
|
||||
}
|
||||
}, [id, dispatch, visible, dismissed]);
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
|
|
@ -33,8 +52,6 @@ export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
|
|||
|
||||
return (
|
||||
<div className='dismissable-banner'>
|
||||
<div className='dismissable-banner__message'>{children}</div>
|
||||
|
||||
<div className='dismissable-banner__action'>
|
||||
<IconButton
|
||||
icon='times'
|
||||
|
|
@ -42,6 +59,8 @@ export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
|
|||
onClick={handleDismiss}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='dismissable-banner__message'>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import { groupBy, minBy } from 'lodash';
|
|||
|
||||
import { getStatusContent } from './status_content';
|
||||
|
||||
// About two lines on desktop
|
||||
const VISIBLE_HASHTAGS = 7;
|
||||
// Fit on a single line on desktop
|
||||
const VISIBLE_HASHTAGS = 3;
|
||||
|
||||
// Those types are not correct, they need to be replaced once this part of the state is typed
|
||||
export type TagLike = Record<{ name: string }>;
|
||||
|
|
@ -23,8 +23,9 @@ export type StatusLike = Record<{
|
|||
}>;
|
||||
|
||||
function normalizeHashtag(hashtag: string) {
|
||||
if (hashtag && hashtag.startsWith('#')) return hashtag.slice(1);
|
||||
else return hashtag;
|
||||
return (
|
||||
hashtag && hashtag.startsWith('#') ? hashtag.slice(1) : hashtag
|
||||
).normalize('NFKC');
|
||||
}
|
||||
|
||||
function isNodeLinkHashtag(element: Node): element is HTMLLinkElement {
|
||||
|
|
@ -70,9 +71,16 @@ function uniqueHashtagsWithCaseHandling(hashtags: string[]) {
|
|||
}
|
||||
|
||||
// Create the collator once, this is much more efficient
|
||||
const collator = new Intl.Collator(undefined, { sensitivity: 'accent' });
|
||||
const collator = new Intl.Collator(undefined, {
|
||||
sensitivity: 'base', // we use this to emulate the ASCII folding done on the server-side, hopefuly more efficiently
|
||||
});
|
||||
|
||||
function localeAwareInclude(collection: string[], value: string) {
|
||||
return collection.find((item) => collator.compare(item, value) === 0);
|
||||
const normalizedValue = value.normalize('NFKC');
|
||||
|
||||
return !!collection.find(
|
||||
(item) => collator.compare(item.normalize('NFKC'), normalizedValue) === 0,
|
||||
);
|
||||
}
|
||||
|
||||
// We use an intermediate function here to make it easier to test
|
||||
|
|
@ -101,7 +109,7 @@ export function computeHashtagBarForStatus(status: StatusLike): {
|
|||
|
||||
const lastChild = template.content.lastChild;
|
||||
|
||||
if (!lastChild) return defaultResult;
|
||||
if (!lastChild || lastChild.nodeType === Node.TEXT_NODE) return defaultResult;
|
||||
|
||||
template.content.removeChild(lastChild);
|
||||
const contentWithoutLastLine = template;
|
||||
|
|
@ -121,11 +129,13 @@ export function computeHashtagBarForStatus(status: StatusLike): {
|
|||
// try to see if the last line is only hashtags
|
||||
let onlyHashtags = true;
|
||||
|
||||
const normalizedTagNames = tagNames.map((tag) => tag.normalize('NFKC'));
|
||||
|
||||
Array.from(lastChild.childNodes).forEach((node) => {
|
||||
if (isNodeLinkHashtag(node) && node.textContent) {
|
||||
const normalized = normalizeHashtag(node.textContent);
|
||||
|
||||
if (!localeAwareInclude(tagNames, normalized)) {
|
||||
if (!localeAwareInclude(normalizedTagNames, normalized)) {
|
||||
// stop here, this is not a real hashtag, so consider it as text
|
||||
onlyHashtags = false;
|
||||
return;
|
||||
|
|
@ -140,12 +150,14 @@ export function computeHashtagBarForStatus(status: StatusLike): {
|
|||
}
|
||||
});
|
||||
|
||||
const hashtagsInBar = tagNames.filter(
|
||||
(tag) =>
|
||||
// the tag does not appear at all in the status content, it is an out-of-band tag
|
||||
!localeAwareInclude(contentHashtags, tag) &&
|
||||
!localeAwareInclude(lastLineHashtags, tag),
|
||||
);
|
||||
const hashtagsInBar = tagNames.filter((tag) => {
|
||||
const normalizedTag = tag.normalize('NFKC');
|
||||
// the tag does not appear at all in the status content, it is an out-of-band tag
|
||||
return (
|
||||
!localeAwareInclude(contentHashtags, normalizedTag) &&
|
||||
!localeAwareInclude(lastLineHashtags, normalizedTag)
|
||||
);
|
||||
});
|
||||
|
||||
const isOnlyOneLine = contentWithoutLastLine.content.childElementCount === 0;
|
||||
const hasMedia = status.get('media_attachments').size > 0;
|
||||
|
|
@ -198,13 +210,13 @@ const HashtagBar: React.FC<{
|
|||
|
||||
const revealedHashtags = expanded
|
||||
? hashtags
|
||||
: hashtags.slice(0, VISIBLE_HASHTAGS - 1);
|
||||
: hashtags.slice(0, VISIBLE_HASHTAGS);
|
||||
|
||||
return (
|
||||
<div className='hashtag-bar'>
|
||||
{revealedHashtags.map((hashtag) => (
|
||||
<Link key={hashtag} to={`/tags/${hashtag}`}>
|
||||
#{hashtag}
|
||||
#<span>{hashtag}</span>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ interface Props {
|
|||
overlay: boolean;
|
||||
tabIndex: number;
|
||||
counter?: number;
|
||||
obfuscateCount?: boolean;
|
||||
href?: string;
|
||||
ariaHidden: boolean;
|
||||
}
|
||||
|
|
@ -105,7 +104,6 @@ export class IconButton extends PureComponent<Props, States> {
|
|||
tabIndex,
|
||||
title,
|
||||
counter,
|
||||
obfuscateCount,
|
||||
href,
|
||||
ariaHidden,
|
||||
} = this.props;
|
||||
|
|
@ -131,7 +129,7 @@ export class IconButton extends PureComponent<Props, States> {
|
|||
<Icon id={icon} fixedWidth aria-hidden='true' />{' '}
|
||||
{typeof counter !== 'undefined' && (
|
||||
<span className='icon-button__counter'>
|
||||
<AnimatedNumber value={counter} obfuscate={obfuscateCount} />
|
||||
<AnimatedNumber value={counter} />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class ScrollableList extends PureComponent {
|
|||
const clientHeight = this.getClientHeight();
|
||||
const offset = scrollHeight - scrollTop - clientHeight;
|
||||
|
||||
if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
|
||||
if (scrollTop > 0 && offset < 400 && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
|
||||
this.props.onLoadMore();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -546,10 +546,11 @@ class Status extends ImmutablePureComponent {
|
|||
const visibilityIcon = visibilityIconInfo[status.get('visibility')];
|
||||
|
||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
|
||||
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
|
||||
{prepend}
|
||||
|
||||
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted })} data-id={status.get('id')}>
|
||||
|
|
@ -574,7 +575,7 @@ class Status extends ImmutablePureComponent {
|
|||
<StatusContent
|
||||
status={status}
|
||||
onClick={this.handleClick}
|
||||
expanded={!status.get('hidden')}
|
||||
expanded={expanded}
|
||||
onExpandedToggle={this.handleExpandedToggle}
|
||||
onTranslate={this.handleTranslate}
|
||||
collapsible
|
||||
|
|
@ -584,7 +585,7 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
{media}
|
||||
|
||||
{hashtagBar}
|
||||
{expanded && hashtagBar}
|
||||
|
||||
<StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters ? this.handleFilterClick : null} {...other} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -362,7 +362,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<div className='status__action-bar'>
|
||||
<IconButton className='status__action-bar__button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
||||
<IconButton className='status__action-bar__button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
|
||||
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
|
||||
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
|
||||
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|||
import classNames from 'classnames';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
|
@ -161,7 +162,7 @@ class About extends PureComponent {
|
|||
</Section>
|
||||
|
||||
<Section title={intl.formatMessage(messages.rules)}>
|
||||
{!isLoading && (server.get('rules', []).isEmpty() ? (
|
||||
{!isLoading && (server.get('rules', ImmutableList()).isEmpty() ? (
|
||||
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
||||
) : (
|
||||
<ol className='rules-list'>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const mapStateToProps = (state, { account }) => ({
|
|||
const mapDispatchToProps = (dispatch, { account }) => ({
|
||||
|
||||
onSave (value) {
|
||||
dispatch(submitAccountNote(account.get('id'), value));
|
||||
dispatch(submitAccountNote({ id: account.get('id'), value}));
|
||||
},
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -205,11 +205,11 @@ class Audio extends PureComponent {
|
|||
};
|
||||
|
||||
toggleMute = () => {
|
||||
const muted = !this.state.muted;
|
||||
const muted = !(this.state.muted || this.state.volume === 0);
|
||||
|
||||
this.setState({ muted }, () => {
|
||||
this.setState((state) => ({ muted, volume: Math.max(state.volume || 0.5, 0.05) }), () => {
|
||||
if (this.gainNode) {
|
||||
this.gainNode.gain.value = muted ? 0 : this.state.volume;
|
||||
this.gainNode.gain.value = this.state.muted ? 0 : this.state.volume;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -287,7 +287,7 @@ class Audio extends PureComponent {
|
|||
const { x } = getPointerPosition(this.volume, e);
|
||||
|
||||
if(!isNaN(x)) {
|
||||
this.setState({ volume: x }, () => {
|
||||
this.setState((state) => ({ volume: x, muted: state.muted && x === 0 }), () => {
|
||||
if (this.gainNode) {
|
||||
this.gainNode.gain.value = this.state.muted ? 0 : x;
|
||||
}
|
||||
|
|
@ -466,8 +466,9 @@ class Audio extends PureComponent {
|
|||
|
||||
render () {
|
||||
const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash } = this.props;
|
||||
const { paused, muted, volume, currentTime, duration, buffer, dragging, revealed } = this.state;
|
||||
const { paused, volume, currentTime, duration, buffer, dragging, revealed } = this.state;
|
||||
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||
const muted = this.state.muted || volume === 0;
|
||||
|
||||
let warning;
|
||||
|
||||
|
|
@ -557,12 +558,12 @@ class Audio extends PureComponent {
|
|||
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
|
||||
|
||||
<div className={classNames('video-player__volume', { active: this.state.hovered })} ref={this.setVolumeRef} onMouseDown={this.handleVolumeMouseDown}>
|
||||
<div className='video-player__volume__current' style={{ width: `${volume * 100}%`, backgroundColor: this._getAccentColor() }} />
|
||||
<div className='video-player__volume__current' style={{ width: `${muted ? 0 : volume * 100}%`, backgroundColor: this._getAccentColor() }} />
|
||||
|
||||
<span
|
||||
className='video-player__volume__handle'
|
||||
tabIndex={0}
|
||||
style={{ left: `${volume * 100}%`, backgroundColor: this._getAccentColor() }}
|
||||
style={{ left: `${muted ? 0 : volume * 100}%`, backgroundColor: this._getAccentColor() }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, injectIntl, FormattedMessage, FormattedList } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { searchEnabled } from 'mastodon/initial_state';
|
||||
import { domain, searchEnabled } from 'mastodon/initial_state';
|
||||
import { HASHTAG_REGEX } from 'mastodon/utils/hashtags';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
|
@ -16,6 +16,17 @@ const messages = defineMessages({
|
|||
placeholderSignedIn: { id: 'search.search_or_paste', defaultMessage: 'Search or paste URL' },
|
||||
});
|
||||
|
||||
const labelForRecentSearch = search => {
|
||||
switch(search.get('type')) {
|
||||
case 'account':
|
||||
return `@${search.get('q')}`;
|
||||
case 'hashtag':
|
||||
return `#${search.get('q')}`;
|
||||
default:
|
||||
return search.get('q');
|
||||
}
|
||||
};
|
||||
|
||||
class Search extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
|
|
@ -45,6 +56,17 @@ class Search extends PureComponent {
|
|||
options: [],
|
||||
};
|
||||
|
||||
defaultOptions = [
|
||||
{ label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:') } },
|
||||
{ label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:') } },
|
||||
{ label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:') } },
|
||||
{ label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:') } },
|
||||
{ label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:') } },
|
||||
{ label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:') } },
|
||||
{ label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:') } },
|
||||
{ label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library']} /></>, action: e => { e.preventDefault(); this._insertText('in:') } }
|
||||
];
|
||||
|
||||
setRef = c => {
|
||||
this.searchForm = c;
|
||||
};
|
||||
|
|
@ -70,7 +92,7 @@ class Search extends PureComponent {
|
|||
|
||||
handleKeyDown = (e) => {
|
||||
const { selectedOption } = this.state;
|
||||
const options = this._getOptions();
|
||||
const options = searchEnabled ? this._getOptions().concat(this.defaultOptions) : this._getOptions();
|
||||
|
||||
switch(e.key) {
|
||||
case 'Escape':
|
||||
|
|
@ -100,11 +122,9 @@ class Search extends PureComponent {
|
|||
if (selectedOption === -1) {
|
||||
this._submit();
|
||||
} else if (options.length > 0) {
|
||||
options[selectedOption].action();
|
||||
options[selectedOption].action(e);
|
||||
}
|
||||
|
||||
this._unfocus();
|
||||
|
||||
break;
|
||||
case 'Delete':
|
||||
if (selectedOption > -1 && options.length > 0) {
|
||||
|
|
@ -147,6 +167,7 @@ class Search extends PureComponent {
|
|||
|
||||
router.history.push(`/tags/${query}`);
|
||||
onClickSearchResult(query, 'hashtag');
|
||||
this._unfocus();
|
||||
};
|
||||
|
||||
handleAccountClick = () => {
|
||||
|
|
@ -157,6 +178,7 @@ class Search extends PureComponent {
|
|||
|
||||
router.history.push(`/@${query}`);
|
||||
onClickSearchResult(query, 'account');
|
||||
this._unfocus();
|
||||
};
|
||||
|
||||
handleURLClick = () => {
|
||||
|
|
@ -164,6 +186,7 @@ class Search extends PureComponent {
|
|||
const { value, onOpenURL } = this.props;
|
||||
|
||||
onOpenURL(value, router.history);
|
||||
this._unfocus();
|
||||
};
|
||||
|
||||
handleStatusSearch = () => {
|
||||
|
|
@ -175,13 +198,19 @@ class Search extends PureComponent {
|
|||
};
|
||||
|
||||
handleRecentSearchClick = search => {
|
||||
const { onChange } = this.props;
|
||||
const { router } = this.context;
|
||||
|
||||
if (search.get('type') === 'account') {
|
||||
router.history.push(`/@${search.get('q')}`);
|
||||
} else if (search.get('type') === 'hashtag') {
|
||||
router.history.push(`/tags/${search.get('q')}`);
|
||||
} else {
|
||||
onChange(search.get('q'));
|
||||
this._submit(search.get('type'));
|
||||
}
|
||||
|
||||
this._unfocus();
|
||||
};
|
||||
|
||||
handleForgetRecentSearchClick = search => {
|
||||
|
|
@ -194,15 +223,33 @@ class Search extends PureComponent {
|
|||
document.querySelector('.ui').parentElement.focus();
|
||||
}
|
||||
|
||||
_insertText (text) {
|
||||
const { value, onChange } = this.props;
|
||||
|
||||
if (value === '') {
|
||||
onChange(text);
|
||||
} else if (value[value.length - 1] === ' ') {
|
||||
onChange(`${value}${text}`);
|
||||
} else {
|
||||
onChange(`${value} ${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
_submit (type) {
|
||||
const { onSubmit, openInRoute } = this.props;
|
||||
const { onSubmit, openInRoute, value, onClickSearchResult } = this.props;
|
||||
const { router } = this.context;
|
||||
|
||||
onSubmit(type);
|
||||
|
||||
if (value) {
|
||||
onClickSearchResult(value, type);
|
||||
}
|
||||
|
||||
if (openInRoute) {
|
||||
router.history.push('/search');
|
||||
}
|
||||
|
||||
this._unfocus();
|
||||
}
|
||||
|
||||
_getOptions () {
|
||||
|
|
@ -215,7 +262,7 @@ class Search extends PureComponent {
|
|||
const { recent } = this.props;
|
||||
|
||||
return recent.toArray().map(search => ({
|
||||
label: search.get('type') === 'account' ? `@${search.get('q')}` : `#${search.get('q')}`,
|
||||
label: labelForRecentSearch(search),
|
||||
|
||||
action: () => this.handleRecentSearchClick(search),
|
||||
|
||||
|
|
@ -325,6 +372,22 @@ class Search extends PureComponent {
|
|||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h4><FormattedMessage id='search_popout.options' defaultMessage='Search options' /></h4>
|
||||
|
||||
{searchEnabled ? (
|
||||
<div className='search__popout__menu'>
|
||||
{this.defaultOptions.map(({ key, label, action }, i) => (
|
||||
<button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === ((options.length || recent.size) + i) })}>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className='search__popout__menu__message'>
|
||||
<FormattedMessage id='search_popout.full_text_search_disabled_message' defaultMessage='Not available on {domain}.' values={{ domain }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,46 +1,36 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { LoadMore } from 'mastodon/components/load_more';
|
||||
import { SearchSection } from 'mastodon/features/explore/components/search_section';
|
||||
|
||||
import { ImmutableHashtag as Hashtag } from '../../../components/hashtag';
|
||||
import AccountContainer from '../../../containers/account_container';
|
||||
import StatusContainer from '../../../containers/status_container';
|
||||
import { searchEnabled } from '../../../initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
|
||||
});
|
||||
const INITIAL_PAGE_LIMIT = 10;
|
||||
|
||||
const withoutLastResult = list => {
|
||||
if (list.size > INITIAL_PAGE_LIMIT && list.size % INITIAL_PAGE_LIMIT === 1) {
|
||||
return list.skipLast(1);
|
||||
} else {
|
||||
return list;
|
||||
}
|
||||
};
|
||||
|
||||
class SearchResults extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
results: ImmutablePropTypes.map.isRequired,
|
||||
suggestions: ImmutablePropTypes.list.isRequired,
|
||||
fetchSuggestions: PropTypes.func.isRequired,
|
||||
expandSearch: PropTypes.func.isRequired,
|
||||
dismissSuggestion: PropTypes.func.isRequired,
|
||||
searchTerm: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
if (this.props.searchTerm === '') {
|
||||
this.props.fetchSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
if (this.props.searchTerm === '') {
|
||||
this.props.fetchSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMoreAccounts = () => this.props.expandSearch('accounts');
|
||||
|
||||
handleLoadMoreStatuses = () => this.props.expandSearch('statuses');
|
||||
|
|
@ -48,97 +38,52 @@ class SearchResults extends ImmutablePureComponent {
|
|||
handleLoadMoreHashtags = () => this.props.expandSearch('hashtags');
|
||||
|
||||
render () {
|
||||
const { intl, results, suggestions, dismissSuggestion, searchTerm } = this.props;
|
||||
|
||||
if (searchTerm === '' && !suggestions.isEmpty()) {
|
||||
return (
|
||||
<div className='search-results'>
|
||||
<div className='trends'>
|
||||
<div className='trends__header'>
|
||||
<Icon id='user-plus' fixedWidth />
|
||||
<FormattedMessage id='suggestions.header' defaultMessage='You might be interested in…' />
|
||||
</div>
|
||||
|
||||
{suggestions && suggestions.map(suggestion => (
|
||||
<AccountContainer
|
||||
key={suggestion.get('account')}
|
||||
id={suggestion.get('account')}
|
||||
actionIcon={suggestion.get('source') === 'past_interactions' ? 'times' : null}
|
||||
actionTitle={suggestion.get('source') === 'past_interactions' ? intl.formatMessage(messages.dismissSuggestion) : null}
|
||||
onActionClick={dismissSuggestion}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const { results } = this.props;
|
||||
|
||||
let accounts, statuses, hashtags;
|
||||
let count = 0;
|
||||
|
||||
if (results.get('accounts') && results.get('accounts').size > 0) {
|
||||
count += results.get('accounts').size;
|
||||
accounts = (
|
||||
<div className='search-results__section'>
|
||||
<h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></h5>
|
||||
|
||||
{results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)}
|
||||
|
||||
{results.get('accounts').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreAccounts} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (results.get('statuses') && results.get('statuses').size > 0) {
|
||||
count += results.get('statuses').size;
|
||||
statuses = (
|
||||
<div className='search-results__section'>
|
||||
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></h5>
|
||||
|
||||
{results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)}
|
||||
|
||||
{results.get('statuses').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreStatuses} />}
|
||||
</div>
|
||||
);
|
||||
} else if(results.get('statuses') && results.get('statuses').size === 0 && !searchEnabled && !(searchTerm.startsWith('@') || searchTerm.startsWith('#') || searchTerm.includes(' '))) {
|
||||
statuses = (
|
||||
<div className='search-results__section'>
|
||||
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></h5>
|
||||
|
||||
<div className='search-results__info'>
|
||||
<FormattedMessage id='search_results.statuses_fts_disabled' defaultMessage='Searching posts by their content is not enabled on this Mastodon server.' />
|
||||
</div>
|
||||
</div>
|
||||
<SearchSection title={<><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>}>
|
||||
{withoutLastResult(results.get('accounts')).map(accountId => <AccountContainer key={accountId} id={accountId} />)}
|
||||
{(results.get('accounts').size > INITIAL_PAGE_LIMIT && results.get('accounts').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={this.handleLoadMoreAccounts} />}
|
||||
</SearchSection>
|
||||
);
|
||||
}
|
||||
|
||||
if (results.get('hashtags') && results.get('hashtags').size > 0) {
|
||||
count += results.get('hashtags').size;
|
||||
hashtags = (
|
||||
<div className='search-results__section'>
|
||||
<h5><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
|
||||
|
||||
{results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
||||
|
||||
{results.get('hashtags').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreHashtags} />}
|
||||
</div>
|
||||
<SearchSection title={<><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></>}>
|
||||
{withoutLastResult(results.get('hashtags')).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
||||
{(results.get('hashtags').size > INITIAL_PAGE_LIMIT && results.get('hashtags').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={this.handleLoadMoreHashtags} />}
|
||||
</SearchSection>
|
||||
);
|
||||
}
|
||||
|
||||
if (results.get('statuses') && results.get('statuses').size > 0) {
|
||||
statuses = (
|
||||
<SearchSection title={<><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></>}>
|
||||
{withoutLastResult(results.get('statuses')).map(statusId => <StatusContainer key={statusId} id={statusId} />)}
|
||||
{(results.get('statuses').size > INITIAL_PAGE_LIMIT && results.get('statuses').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={this.handleLoadMoreStatuses} />}
|
||||
</SearchSection>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className='search-results'>
|
||||
<div className='search-results__header'>
|
||||
<Icon id='search' fixedWidth />
|
||||
<FormattedMessage id='search_results.total' defaultMessage='{count, plural, one {# result} other {# results}}' values={{ count }} />
|
||||
<FormattedMessage id='explore.search_results' defaultMessage='Search results' />
|
||||
</div>
|
||||
|
||||
{accounts}
|
||||
{statuses}
|
||||
{hashtags}
|
||||
{statuses}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(SearchResults);
|
||||
export default SearchResults;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { PureComponent } from 'react';
|
|||
const iconStyle = {
|
||||
height: null,
|
||||
lineHeight: '27px',
|
||||
width: `${18 * 1.28571429}px`,
|
||||
minWidth: `${18 * 1.28571429}px`,
|
||||
};
|
||||
|
||||
export default class TextIconButton extends PureComponent {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import Search from '../components/search';
|
|||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['search', 'value']),
|
||||
submitted: state.getIn(['search', 'submitted']),
|
||||
recent: state.getIn(['search', 'recent']),
|
||||
recent: state.getIn(['search', 'recent']).reverse(),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export const SearchSection = ({ title, onClickMore, children }) => (
|
||||
<div className='search-results__section'>
|
||||
<div className='search-results__section__header'>
|
||||
<h3>{title}</h3>
|
||||
{onClickMore && <button onClick={onClickMore}><FormattedMessage id='search_results.see_all' defaultMessage='See all' /></button>}
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
SearchSection.propTypes = {
|
||||
title: PropTypes.node.isRequired,
|
||||
onClickMore: PropTypes.func,
|
||||
children: PropTypes.children,
|
||||
};
|
||||
|
|
@ -67,47 +67,45 @@ class Explore extends PureComponent {
|
|||
<Search />
|
||||
</div>
|
||||
|
||||
<div className='scrollable scrollable--flex' data-nosnippet>
|
||||
{isSearching ? (
|
||||
<SearchResults />
|
||||
) : (
|
||||
<>
|
||||
<div className='account__section-headline'>
|
||||
<NavLink exact to='/explore'>
|
||||
<FormattedMessage tagName='div' id='explore.trending_statuses' defaultMessage='Posts' />
|
||||
{isSearching ? (
|
||||
<SearchResults />
|
||||
) : (
|
||||
<>
|
||||
<div className='account__section-headline'>
|
||||
<NavLink exact to='/explore'>
|
||||
<FormattedMessage tagName='div' id='explore.trending_statuses' defaultMessage='Posts' />
|
||||
</NavLink>
|
||||
|
||||
<NavLink exact to='/explore/tags'>
|
||||
<FormattedMessage tagName='div' id='explore.trending_tags' defaultMessage='Hashtags' />
|
||||
</NavLink>
|
||||
|
||||
{signedIn && (
|
||||
<NavLink exact to='/explore/suggestions'>
|
||||
<FormattedMessage tagName='div' id='explore.suggested_follows' defaultMessage='People' />
|
||||
</NavLink>
|
||||
)}
|
||||
|
||||
<NavLink exact to='/explore/tags'>
|
||||
<FormattedMessage tagName='div' id='explore.trending_tags' defaultMessage='Hashtags' />
|
||||
</NavLink>
|
||||
<NavLink exact to='/explore/links'>
|
||||
<FormattedMessage tagName='div' id='explore.trending_links' defaultMessage='News' />
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
{signedIn && (
|
||||
<NavLink exact to='/explore/suggestions'>
|
||||
<FormattedMessage tagName='div' id='explore.suggested_follows' defaultMessage='People' />
|
||||
</NavLink>
|
||||
)}
|
||||
<Switch>
|
||||
<Route path='/explore/tags' component={Tags} />
|
||||
<Route path='/explore/links' component={Links} />
|
||||
<Route path='/explore/suggestions' component={Suggestions} />
|
||||
<Route exact path={['/explore', '/explore/posts', '/search']}>
|
||||
<Statuses multiColumn={multiColumn} />
|
||||
</Route>
|
||||
</Switch>
|
||||
|
||||
<NavLink exact to='/explore/links'>
|
||||
<FormattedMessage tagName='div' id='explore.trending_links' defaultMessage='News' />
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<Switch>
|
||||
<Route path='/explore/tags' component={Tags} />
|
||||
<Route path='/explore/links' component={Links} />
|
||||
<Route path='/explore/suggestions' component={Suggestions} />
|
||||
<Route exact path={['/explore', '/explore/posts', '/search']}>
|
||||
<Statuses multiColumn={multiColumn} />
|
||||
</Route>
|
||||
</Switch>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content={isSearching ? 'noindex' : 'all'} />
|
||||
</Helmet>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content={isSearching ? 'noindex' : 'all'} />
|
||||
</Helmet>
|
||||
</>
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class Links extends PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='explore__links'>
|
||||
<div className='explore__links scrollable' data-nosnippet>
|
||||
{banner}
|
||||
|
||||
{isLoading ? (<LoadingIndicator />) : links.map((link, i) => (
|
||||
|
|
|
|||
|
|
@ -9,13 +9,15 @@ import { List as ImmutableList } from 'immutable';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { expandSearch } from 'mastodon/actions/search';
|
||||
import { submitSearch, expandSearch } from 'mastodon/actions/search';
|
||||
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
|
||||
import { LoadMore } from 'mastodon/components/load_more';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import Account from 'mastodon/containers/account_container';
|
||||
import Status from 'mastodon/containers/status_container';
|
||||
|
||||
import { SearchSection } from './components/search_section';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'search_results.title', defaultMessage: 'Search for {q}' },
|
||||
});
|
||||
|
|
@ -24,98 +26,195 @@ const mapStateToProps = state => ({
|
|||
isLoading: state.getIn(['search', 'isLoading']),
|
||||
results: state.getIn(['search', 'results']),
|
||||
q: state.getIn(['search', 'searchTerm']),
|
||||
submittedType: state.getIn(['search', 'type']),
|
||||
});
|
||||
|
||||
const appendLoadMore = (id, list, onLoadMore) => {
|
||||
if (list.size >= 5) {
|
||||
return list.push(<LoadMore key={`${id}-load-more`} visible onClick={onLoadMore} />);
|
||||
const INITIAL_PAGE_LIMIT = 10;
|
||||
const INITIAL_DISPLAY = 4;
|
||||
|
||||
const hidePeek = list => {
|
||||
if (list.size > INITIAL_PAGE_LIMIT && list.size % INITIAL_PAGE_LIMIT === 1) {
|
||||
return list.skipLast(1);
|
||||
} else {
|
||||
return list;
|
||||
}
|
||||
};
|
||||
|
||||
const renderAccounts = (results, onLoadMore) => appendLoadMore('accounts', results.get('accounts', ImmutableList()).map(item => (
|
||||
<Account key={`account-${item}`} id={item} />
|
||||
)), onLoadMore);
|
||||
const renderAccounts = accounts => hidePeek(accounts).map(id => (
|
||||
<Account key={id} id={id} />
|
||||
));
|
||||
|
||||
const renderHashtags = (results, onLoadMore) => appendLoadMore('hashtags', results.get('hashtags', ImmutableList()).map(item => (
|
||||
<Hashtag key={`tag-${item.get('name')}`} hashtag={item} />
|
||||
)), onLoadMore);
|
||||
const renderHashtags = hashtags => hidePeek(hashtags).map(hashtag => (
|
||||
<Hashtag key={hashtag.get('name')} hashtag={hashtag} />
|
||||
));
|
||||
|
||||
const renderStatuses = (results, onLoadMore) => appendLoadMore('statuses', results.get('statuses', ImmutableList()).map(item => (
|
||||
<Status key={`status-${item}`} id={item} />
|
||||
)), onLoadMore);
|
||||
const renderStatuses = statuses => hidePeek(statuses).map(id => (
|
||||
<Status key={id} id={id} />
|
||||
));
|
||||
|
||||
class Results extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
results: ImmutablePropTypes.map,
|
||||
results: ImmutablePropTypes.contains({
|
||||
accounts: ImmutablePropTypes.orderedSet,
|
||||
statuses: ImmutablePropTypes.orderedSet,
|
||||
hashtags: ImmutablePropTypes.orderedSet,
|
||||
}),
|
||||
isLoading: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
q: PropTypes.string,
|
||||
intl: PropTypes.object,
|
||||
submittedType: PropTypes.oneOf(['accounts', 'statuses', 'hashtags']),
|
||||
};
|
||||
|
||||
state = {
|
||||
type: 'all',
|
||||
type: this.props.submittedType || 'all',
|
||||
};
|
||||
|
||||
handleSelectAll = () => this.setState({ type: 'all' });
|
||||
handleSelectAccounts = () => this.setState({ type: 'accounts' });
|
||||
handleSelectHashtags = () => this.setState({ type: 'hashtags' });
|
||||
handleSelectStatuses = () => this.setState({ type: 'statuses' });
|
||||
handleLoadMoreAccounts = () => this.loadMore('accounts');
|
||||
handleLoadMoreStatuses = () => this.loadMore('statuses');
|
||||
handleLoadMoreHashtags = () => this.loadMore('hashtags');
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
if (props.submittedType !== state.type) {
|
||||
return {
|
||||
type: props.submittedType || 'all',
|
||||
};
|
||||
}
|
||||
|
||||
loadMore (type) {
|
||||
return null;
|
||||
};
|
||||
|
||||
handleSelectAll = () => {
|
||||
const { submittedType, dispatch } = this.props;
|
||||
|
||||
// If we originally searched for a specific type, we need to resubmit
|
||||
// the query to get all types of results
|
||||
if (submittedType) {
|
||||
dispatch(submitSearch());
|
||||
}
|
||||
|
||||
this.setState({ type: 'all' });
|
||||
};
|
||||
|
||||
handleSelectAccounts = () => {
|
||||
const { submittedType, dispatch } = this.props;
|
||||
|
||||
// If we originally searched for something else (but not everything),
|
||||
// we need to resubmit the query for this specific type
|
||||
if (submittedType !== 'accounts') {
|
||||
dispatch(submitSearch('accounts'));
|
||||
}
|
||||
|
||||
this.setState({ type: 'accounts' });
|
||||
};
|
||||
|
||||
handleSelectHashtags = () => {
|
||||
const { submittedType, dispatch } = this.props;
|
||||
|
||||
// If we originally searched for something else (but not everything),
|
||||
// we need to resubmit the query for this specific type
|
||||
if (submittedType !== 'hashtags') {
|
||||
dispatch(submitSearch('hashtags'));
|
||||
}
|
||||
|
||||
this.setState({ type: 'hashtags' });
|
||||
}
|
||||
|
||||
handleSelectStatuses = () => {
|
||||
const { submittedType, dispatch } = this.props;
|
||||
|
||||
// If we originally searched for something else (but not everything),
|
||||
// we need to resubmit the query for this specific type
|
||||
if (submittedType !== 'statuses') {
|
||||
dispatch(submitSearch('statuses'));
|
||||
}
|
||||
|
||||
this.setState({ type: 'statuses' });
|
||||
}
|
||||
|
||||
handleLoadMoreAccounts = () => this._loadMore('accounts');
|
||||
handleLoadMoreStatuses = () => this._loadMore('statuses');
|
||||
handleLoadMoreHashtags = () => this._loadMore('hashtags');
|
||||
|
||||
_loadMore (type) {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(expandSearch(type));
|
||||
}
|
||||
|
||||
handleLoadMore = () => {
|
||||
const { type } = this.state;
|
||||
|
||||
if (type !== 'all') {
|
||||
this._loadMore(type);
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, isLoading, q, results } = this.props;
|
||||
const { type } = this.state;
|
||||
|
||||
let filteredResults = ImmutableList();
|
||||
// We request 1 more result than we display so we can tell if there'd be a next page
|
||||
const hasMore = type !== 'all' ? results.get(type, ImmutableList()).size > INITIAL_PAGE_LIMIT && results.get(type).size % INITIAL_PAGE_LIMIT === 1 : false;
|
||||
|
||||
if (!isLoading) {
|
||||
switch(type) {
|
||||
case 'all':
|
||||
filteredResults = filteredResults.concat(renderAccounts(results, this.handleLoadMoreAccounts), renderHashtags(results, this.handleLoadMoreHashtags), renderStatuses(results, this.handleLoadMoreStatuses));
|
||||
break;
|
||||
case 'accounts':
|
||||
filteredResults = filteredResults.concat(renderAccounts(results, this.handleLoadMoreAccounts));
|
||||
break;
|
||||
case 'hashtags':
|
||||
filteredResults = filteredResults.concat(renderHashtags(results, this.handleLoadMoreHashtags));
|
||||
break;
|
||||
case 'statuses':
|
||||
filteredResults = filteredResults.concat(renderStatuses(results, this.handleLoadMoreStatuses));
|
||||
break;
|
||||
}
|
||||
let filteredResults;
|
||||
|
||||
if (filteredResults.size === 0) {
|
||||
filteredResults = (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='search_results.nothing_found' defaultMessage='Could not find anything for these search terms' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const accounts = results.get('accounts', ImmutableList());
|
||||
const hashtags = results.get('hashtags', ImmutableList());
|
||||
const statuses = results.get('statuses', ImmutableList());
|
||||
|
||||
switch(type) {
|
||||
case 'all':
|
||||
filteredResults = (accounts.size + hashtags.size + statuses.size) > 0 ? (
|
||||
<>
|
||||
{accounts.size > 0 && (
|
||||
<SearchSection key='accounts' title={<><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>} onClickMore={this.handleLoadMoreAccounts}>
|
||||
{accounts.take(INITIAL_DISPLAY).map(id => <Account key={id} id={id} />)}
|
||||
</SearchSection>
|
||||
)}
|
||||
|
||||
{hashtags.size > 0 && (
|
||||
<SearchSection key='hashtags' title={<><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></>} onClickMore={this.handleLoadMoreHashtags}>
|
||||
{hashtags.take(INITIAL_DISPLAY).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
||||
</SearchSection>
|
||||
)}
|
||||
|
||||
{statuses.size > 0 && (
|
||||
<SearchSection key='statuses' title={<><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></>} onClickMore={this.handleLoadMoreStatuses}>
|
||||
{statuses.take(INITIAL_DISPLAY).map(id => <Status key={id} id={id} />)}
|
||||
</SearchSection>
|
||||
)}
|
||||
</>
|
||||
) : [];
|
||||
break;
|
||||
case 'accounts':
|
||||
filteredResults = renderAccounts(accounts);
|
||||
break;
|
||||
case 'hashtags':
|
||||
filteredResults = renderHashtags(hashtags);
|
||||
break;
|
||||
case 'statuses':
|
||||
filteredResults = renderStatuses(statuses);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='account__section-headline'>
|
||||
<button onClick={this.handleSelectAll} className={type === 'all' && 'active'}><FormattedMessage id='search_results.all' defaultMessage='All' /></button>
|
||||
<button onClick={this.handleSelectAccounts} className={type === 'accounts' && 'active'}><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></button>
|
||||
<button onClick={this.handleSelectHashtags} className={type === 'hashtags' && 'active'}><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></button>
|
||||
<button onClick={this.handleSelectStatuses} className={type === 'statuses' && 'active'}><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></button>
|
||||
<button onClick={this.handleSelectAll} className={type === 'all' ? 'active' : undefined}><FormattedMessage id='search_results.all' defaultMessage='All' /></button>
|
||||
<button onClick={this.handleSelectAccounts} className={type === 'accounts' ? 'active' : undefined}><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></button>
|
||||
<button onClick={this.handleSelectHashtags} className={type === 'hashtags' ? 'active' : undefined}><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></button>
|
||||
<button onClick={this.handleSelectStatuses} className={type === 'statuses' ? 'active' : undefined}><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></button>
|
||||
</div>
|
||||
|
||||
<div className='explore__search-results'>
|
||||
{isLoading ? <LoadingIndicator /> : filteredResults}
|
||||
<div className='explore__search-results' data-nosnippet>
|
||||
<ScrollableList
|
||||
scrollKey='search-results'
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
emptyMessage={<FormattedMessage id='search_results.nothing_found' defaultMessage='Could not find anything for these search terms' />}
|
||||
bindToDocument
|
||||
>
|
||||
{filteredResults}
|
||||
</ScrollableList>
|
||||
</div>
|
||||
|
||||
<Helmet>
|
||||
|
|
|
|||
|
|
@ -45,24 +45,20 @@ class Statuses extends PureComponent {
|
|||
const emptyMessage = <FormattedMessage id='empty_column.explore_statuses' defaultMessage='Nothing is trending right now. Check back later!' />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DismissableBanner id='explore/statuses'>
|
||||
<FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favorites are ranked higher.' />
|
||||
</DismissableBanner>
|
||||
|
||||
<StatusList
|
||||
trackScroll
|
||||
timelineId='explore'
|
||||
statusIds={statusIds}
|
||||
scrollKey='explore-statuses'
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
withCounters
|
||||
/>
|
||||
</>
|
||||
<StatusList
|
||||
trackScroll
|
||||
prepend={<DismissableBanner id='explore/statuses'><FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favorites are ranked higher.' /></DismissableBanner>}
|
||||
alwaysPrepend
|
||||
timelineId='explore'
|
||||
statusIds={statusIds}
|
||||
scrollKey='explore-statuses'
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
withCounters
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class Suggestions extends PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='explore__suggestions'>
|
||||
<div className='explore__suggestions scrollable' data-nosnippet>
|
||||
{isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => (
|
||||
<AccountCard key={suggestion.get('account')} id={suggestion.get('account')} />
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class Tags extends PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='explore__links'>
|
||||
<div className='scrollable explore__links' data-nosnippet>
|
||||
{banner}
|
||||
|
||||
{isLoading ? (<LoadingIndicator />) : hashtags.map(hashtag => (
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { fetchFavourites } from 'mastodon/actions/interactions';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { fetchFavourites, expandFavourites } from 'mastodon/actions/interactions';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
|
|
@ -21,7 +23,9 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]),
|
||||
accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'isLoading'], true),
|
||||
});
|
||||
|
||||
class Favourites extends ImmutablePureComponent {
|
||||
|
|
@ -30,6 +34,8 @@ class Favourites extends ImmutablePureComponent {
|
|||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
|
@ -40,18 +46,16 @@ class Favourites extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
|
||||
this.props.dispatch(fetchFavourites(nextProps.params.statusId));
|
||||
}
|
||||
}
|
||||
|
||||
handleRefresh = () => {
|
||||
this.props.dispatch(fetchFavourites(this.props.params.statusId));
|
||||
};
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandFavourites(this.props.params.statusId));
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, accountIds, multiColumn } = this.props;
|
||||
const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
|
|
@ -75,6 +79,9 @@ class Favourites extends ImmutablePureComponent {
|
|||
|
||||
<ScrollableList
|
||||
scrollKey='favourites'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -169,32 +169,30 @@ const Firehose = ({ feedType, multiColumn }) => {
|
|||
<ColumnSettings />
|
||||
</ColumnHeader>
|
||||
|
||||
<div className='scrollable scrollable--flex'>
|
||||
<div className='account__section-headline'>
|
||||
<NavLink exact to='/public/local'>
|
||||
<FormattedMessage tagName='div' id='firehose.local' defaultMessage='This server' />
|
||||
</NavLink>
|
||||
<div className='account__section-headline'>
|
||||
<NavLink exact to='/public/local'>
|
||||
<FormattedMessage tagName='div' id='firehose.local' defaultMessage='This server' />
|
||||
</NavLink>
|
||||
|
||||
<NavLink exact to='/public/remote'>
|
||||
<FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Other servers' />
|
||||
</NavLink>
|
||||
<NavLink exact to='/public/remote'>
|
||||
<FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Other servers' />
|
||||
</NavLink>
|
||||
|
||||
<NavLink exact to='/public'>
|
||||
<FormattedMessage tagName='div' id='firehose.all' defaultMessage='All' />
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<StatusListContainer
|
||||
prepend={prependBanner}
|
||||
timelineId={`${feedType}${onlyMedia ? ':media' : ''}`}
|
||||
onLoadMore={handleLoadMore}
|
||||
trackScroll
|
||||
scrollKey='firehose'
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
/>
|
||||
<NavLink exact to='/public'>
|
||||
<FormattedMessage tagName='div' id='firehose.all' defaultMessage='All' />
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<StatusListContainer
|
||||
prepend={prependBanner}
|
||||
timelineId={`${feedType}${onlyMedia ? ':media' : ''}`}
|
||||
onLoadMore={handleLoadMore}
|
||||
trackScroll
|
||||
scrollKey='firehose'
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
/>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export const CriticalUpdateBanner = () => (
|
||||
<div className='warning-banner'>
|
||||
<div className='warning-banner__message'>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id='home.pending_critical_update.title'
|
||||
defaultMessage='Critical security update available!'
|
||||
/>
|
||||
</h1>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='home.pending_critical_update.body'
|
||||
defaultMessage='Please update your Mastodon server as soon as possible!'
|
||||
/>{' '}
|
||||
<a href='/admin/software_updates'>
|
||||
<FormattedMessage
|
||||
id='home.pending_critical_update.link'
|
||||
defaultMessage='See updates'
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -14,7 +14,7 @@ import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/an
|
|||
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
|
||||
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
||||
import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import { me, criticalUpdatesPending } from 'mastodon/initial_state';
|
||||
|
||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||
import { expandHomeTimeline } from '../../actions/timelines';
|
||||
|
|
@ -23,6 +23,7 @@ import ColumnHeader from '../../components/column_header';
|
|||
import StatusListContainer from '../ui/containers/status_list_container';
|
||||
|
||||
import { ColumnSettings } from './components/column_settings';
|
||||
import { CriticalUpdateBanner } from './components/critical_update_banner';
|
||||
import { ExplorePrompt } from './components/explore_prompt';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
|
@ -38,8 +39,17 @@ const getHomeFeedSpeed = createSelector([
|
|||
], (statusIds, pendingStatusIds, statusMap) => {
|
||||
const recentStatusIds = pendingStatusIds.size > 0 ? pendingStatusIds : statusIds;
|
||||
const statuses = recentStatusIds.filter(id => id !== null).map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20);
|
||||
const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0));
|
||||
const newest = new Date(statuses.getIn([0, 'created_at'], 0));
|
||||
|
||||
if (statuses.isEmpty()) {
|
||||
return {
|
||||
gap: 0,
|
||||
newest: new Date(0),
|
||||
};
|
||||
}
|
||||
|
||||
const datetimes = statuses.map(status => status.get('created_at', 0));
|
||||
const oldest = new Date(datetimes.min());
|
||||
const newest = new Date(datetimes.max());
|
||||
const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds
|
||||
|
||||
return {
|
||||
|
|
@ -54,8 +64,10 @@ const homeTooSlow = createSelector([
|
|||
getHomeFeedSpeed,
|
||||
], (isLoading, isPartial, speed) =>
|
||||
!isLoading && !isPartial // Only if the home feed has finished loading
|
||||
&& (speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes
|
||||
|| (Date.now() - speed.newest) > (1000 * 3600)) // If the most recent post is from over an hour ago
|
||||
&& (
|
||||
(speed.gap > (30 * 60) // If the average gap between posts is more than 30 minutes
|
||||
|| (Date.now() - speed.newest) > (1000 * 3600)) // If the most recent post is from over an hour ago
|
||||
)
|
||||
);
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
|
|
@ -156,8 +168,9 @@ class HomeTimeline extends PureComponent {
|
|||
const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
|
||||
const pinned = !!columnId;
|
||||
const { signedIn } = this.context.identity;
|
||||
const banners = [];
|
||||
|
||||
let announcementsButton, banner;
|
||||
let announcementsButton;
|
||||
|
||||
if (hasAnnouncements) {
|
||||
announcementsButton = (
|
||||
|
|
@ -173,8 +186,12 @@ class HomeTimeline extends PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
if (criticalUpdatesPending) {
|
||||
banners.push(<CriticalUpdateBanner key='critical-update-banner' />);
|
||||
}
|
||||
|
||||
if (tooSlow) {
|
||||
banner = <ExplorePrompt />;
|
||||
banners.push(<ExplorePrompt key='explore-prompt' />);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -196,7 +213,7 @@ class HomeTimeline extends PureComponent {
|
|||
|
||||
{signedIn ? (
|
||||
<StatusListContainer
|
||||
prepend={banner}
|
||||
prepend={banners}
|
||||
alwaysPrepend
|
||||
trackScroll={!pinned}
|
||||
scrollKey={`home_timeline-${columnId}`}
|
||||
|
|
|
|||
|
|
@ -100,8 +100,41 @@ class LoginForm extends React.PureComponent {
|
|||
this.input = c;
|
||||
};
|
||||
|
||||
isValueValid = (value) => {
|
||||
let likelyAcct = false;
|
||||
let url = null;
|
||||
|
||||
if (value.startsWith('/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value.startsWith('@')) {
|
||||
value = value.slice(1);
|
||||
likelyAcct = true;
|
||||
}
|
||||
|
||||
// The user is in the middle of typing something, do not error out
|
||||
if (value === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/^https?:\/\//.test(value) && !likelyAcct) {
|
||||
url = value;
|
||||
} else {
|
||||
url = `https://${value}`;
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch(_) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
handleChange = ({ target }) => {
|
||||
this.setState(state => ({ value: target.value, isLoading: true, error: false, options: addInputToOptions(target.value, state.networkOptions) }), () => this._loadOptions());
|
||||
const error = !this.isValueValid(target.value);
|
||||
this.setState(state => ({ error, value: target.value, isLoading: true, options: addInputToOptions(target.value, state.networkOptions) }), () => this._loadOptions());
|
||||
};
|
||||
|
||||
handleMessage = (event) => {
|
||||
|
|
@ -115,11 +148,18 @@ class LoginForm extends React.PureComponent {
|
|||
this.setState({ isSubmitting: false, error: true });
|
||||
} else if (event.data?.type === 'fetchInteractionURL-success') {
|
||||
if (/^https?:\/\//.test(event.data.template)) {
|
||||
if (localStorage) {
|
||||
localStorage.setItem(PERSISTENCE_KEY, event.data.uri_or_domain);
|
||||
}
|
||||
try {
|
||||
const url = new URL(event.data.template.replace('{uri}', encodeURIComponent(resourceUrl)));
|
||||
|
||||
window.location.href = event.data.template.replace('{uri}', encodeURIComponent(resourceUrl));
|
||||
if (localStorage) {
|
||||
localStorage.setItem(PERSISTENCE_KEY, event.data.uri_or_domain);
|
||||
}
|
||||
|
||||
window.location.href = url;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.setState({ isSubmitting: false, error: true });
|
||||
}
|
||||
} else {
|
||||
this.setState({ isSubmitting: false, error: true });
|
||||
}
|
||||
|
|
@ -259,7 +299,7 @@ class LoginForm extends React.PureComponent {
|
|||
spellcheck='false'
|
||||
/>
|
||||
|
||||
<Button onClick={this.handleSubmit} disabled={isSubmitting}><FormattedMessage id='interaction_modal.login.action' defaultMessage='Take me home' /></Button>
|
||||
<Button onClick={this.handleSubmit} disabled={isSubmitting || error}><FormattedMessage id='interaction_modal.login.action' defaultMessage='Take me home' /></Button>
|
||||
</div>
|
||||
|
||||
{hasPopOut && (
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ class ListTimeline extends PureComponent {
|
|||
</div>
|
||||
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`list-${id}-exclusive`} defaultChecked={isExclusive} onChange={this.onExclusiveToggle} />
|
||||
<Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
|
||||
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ class Footer extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<div className='picture-in-picture__footer'>
|
||||
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
||||
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
|
||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
|
||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
|
||||
{withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' onClick={this.handleOpenClick} href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} />}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,11 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import { fetchReblogs } from '../../actions/interactions';
|
||||
import { fetchReblogs, expandReblogs } from '../../actions/interactions';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
|
|
@ -22,7 +24,9 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]),
|
||||
accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'isLoading'], true),
|
||||
});
|
||||
|
||||
class Reblogs extends ImmutablePureComponent {
|
||||
|
|
@ -31,6 +35,8 @@ class Reblogs extends ImmutablePureComponent {
|
|||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
|
@ -39,20 +45,18 @@ class Reblogs extends ImmutablePureComponent {
|
|||
if (!this.props.accountIds) {
|
||||
this.props.dispatch(fetchReblogs(this.props.params.statusId));
|
||||
}
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
|
||||
this.props.dispatch(fetchReblogs(nextProps.params.statusId));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleRefresh = () => {
|
||||
this.props.dispatch(fetchReblogs(this.props.params.statusId));
|
||||
};
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandReblogs(this.props.params.statusId));
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, accountIds, multiColumn } = this.props;
|
||||
const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
|
|
@ -76,6 +80,9 @@ class Reblogs extends ImmutablePureComponent {
|
|||
|
||||
<ScrollableList
|
||||
scrollKey='reblogs'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -293,6 +293,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
|
||||
|
||||
return (
|
||||
<div style={outerStyle}>
|
||||
|
|
@ -318,7 +319,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
|
||||
{media}
|
||||
|
||||
{hashtagBar}
|
||||
{expanded && hashtagBar}
|
||||
|
||||
<div className='detailed-status__meta'>
|
||||
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
|
||||
|
|
|
|||
|
|
@ -220,6 +220,8 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
componentDidMount () {
|
||||
attachFullscreenListener(this.onFullScreenChange);
|
||||
|
||||
this._scrollStatusIntoView();
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
||||
|
|
@ -568,7 +570,7 @@ class Status extends ImmutablePureComponent {
|
|||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
contextType='thread'
|
||||
previousId={i > 0 && list.get(i - 1)}
|
||||
previousId={i > 0 ? list.get(i - 1) : undefined}
|
||||
nextId={list.get(i + 1) || (ancestors && statusId)}
|
||||
rootId={statusId}
|
||||
/>
|
||||
|
|
@ -579,10 +581,10 @@ class Status extends ImmutablePureComponent {
|
|||
this.node = c;
|
||||
};
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { status, ancestorsIds, multiColumn } = this.props;
|
||||
_scrollStatusIntoView () {
|
||||
const { status, multiColumn } = this.props;
|
||||
|
||||
if (status && (ancestorsIds.size > prevProps.ancestorsIds.size || prevProps.status?.get('id') !== status.get('id'))) {
|
||||
if (status) {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.node?.querySelector('.detailed-status__wrapper')?.scrollIntoView(true);
|
||||
|
||||
|
|
@ -599,6 +601,14 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { status, ancestorsIds } = this.props;
|
||||
|
||||
if (status && (ancestorsIds.size > prevProps.ancestorsIds.size || prevProps.status?.get('id') !== status.get('id'))) {
|
||||
this._scrollStatusIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
detachFullscreenListener(this.onFullScreenChange);
|
||||
}
|
||||
|
|
@ -607,6 +617,22 @@ class Status extends ImmutablePureComponent {
|
|||
this.setState({ fullscreen: isFullscreen() });
|
||||
};
|
||||
|
||||
shouldUpdateScroll = (prevRouterProps, { location }) => {
|
||||
// Do not change scroll when opening a modal
|
||||
if (location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Scroll to focused post if it is loaded
|
||||
const child = this.node?.querySelector('.detailed-status__wrapper');
|
||||
if (child) {
|
||||
return [0, child.offsetTop];
|
||||
}
|
||||
|
||||
// Do not scroll otherwise, `componentDidUpdate` will take care of that
|
||||
return false;
|
||||
};
|
||||
|
||||
render () {
|
||||
let ancestors, descendants;
|
||||
const { isLoading, status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
|
||||
|
|
@ -660,7 +686,7 @@ class Status extends ImmutablePureComponent {
|
|||
)}
|
||||
/>
|
||||
|
||||
<ScrollContainer scrollKey='thread'>
|
||||
<ScrollContainer scrollKey='thread' shouldUpdateScroll={this.shouldUpdateScroll}>
|
||||
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
|
||||
{ancestors}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class LinkFooter extends PureComponent {
|
|||
{DividingCircle}
|
||||
<a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a>
|
||||
{DividingCircle}
|
||||
v{version}
|
||||
<span className='version'>v{version}</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -115,7 +115,10 @@ export default class ModalRoot extends PureComponent {
|
|||
{visible && (
|
||||
<>
|
||||
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
|
||||
{(SpecificComponent) => <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={this.setModalRef} />}
|
||||
{(SpecificComponent) => {
|
||||
const ref = typeof SpecificComponent !== 'function' ? this.setModalRef : undefined;
|
||||
return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={ref} />
|
||||
}}
|
||||
</BundleContainer>
|
||||
|
||||
<Helmet>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ const messages = defineMessages({
|
|||
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
|
||||
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
|
||||
advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' },
|
||||
openedInClassicInterface: { id: 'navigation_bar.opened_in_classic_interface', defaultMessage: 'Posts, accounts, and other specific pages are opened by default in the classic web interface.' },
|
||||
});
|
||||
|
||||
class NavigationPanel extends Component {
|
||||
|
|
@ -52,19 +53,30 @@ class NavigationPanel extends Component {
|
|||
const { intl } = this.props;
|
||||
const { signedIn, disabledAccountId } = this.context.identity;
|
||||
|
||||
let banner = undefined;
|
||||
|
||||
if(transientSingleColumn)
|
||||
banner = (<div className='switch-to-advanced'>
|
||||
{intl.formatMessage(messages.openedInClassicInterface)}
|
||||
{" "}
|
||||
<a href={`/deck${location.pathname}`} className='switch-to-advanced__toggle'>
|
||||
{intl.formatMessage(messages.advancedInterface)}
|
||||
</a>
|
||||
</div>);
|
||||
|
||||
return (
|
||||
<div className='navigation-panel'>
|
||||
<div className='navigation-panel__logo'>
|
||||
<Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
|
||||
|
||||
{transientSingleColumn && (
|
||||
<a href={`/deck${location.pathname}`} className='button button--block'>
|
||||
{intl.formatMessage(messages.advancedInterface)}
|
||||
</a>
|
||||
)}
|
||||
<hr />
|
||||
{!banner && <hr />}
|
||||
</div>
|
||||
|
||||
{banner &&
|
||||
<div class='navigation-panel__banner'>
|
||||
{banner}
|
||||
</div>
|
||||
}
|
||||
|
||||
{signedIn && (
|
||||
<>
|
||||
<ColumnLink transparent to='/home' icon='home' text={intl.formatMessage(messages.home)} />
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class ReportModal extends ImmutablePureComponent {
|
|||
dispatch(submitReport({
|
||||
account_id: accountId,
|
||||
status_ids: selectedStatusIds.toArray(),
|
||||
selected_domains: selectedDomains.toArray(),
|
||||
forward_to_domains: selectedDomains.toArray(),
|
||||
comment,
|
||||
forward: selectedDomains.size > 0,
|
||||
category,
|
||||
|
|
|
|||
|
|
@ -217,8 +217,9 @@ class Video extends PureComponent {
|
|||
const { x } = getPointerPosition(this.volume, e);
|
||||
|
||||
if(!isNaN(x)) {
|
||||
this.setState({ volume: x }, () => {
|
||||
this.setState((state) => ({ volume: x, muted: state.muted && x === 0 }), () => {
|
||||
this.video.volume = x;
|
||||
this.video.muted = this.state.muted;
|
||||
});
|
||||
}
|
||||
}, 15);
|
||||
|
|
@ -425,10 +426,11 @@ class Video extends PureComponent {
|
|||
};
|
||||
|
||||
toggleMute = () => {
|
||||
const muted = !this.video.muted;
|
||||
const muted = !(this.video.muted || this.state.volume === 0);
|
||||
|
||||
this.setState({ muted }, () => {
|
||||
this.video.muted = muted;
|
||||
this.setState((state) => ({ muted, volume: Math.max(state.volume || 0.5, 0.05) }), () => {
|
||||
this.video.volume = this.state.volume;
|
||||
this.video.muted = this.state.muted;
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -501,8 +503,9 @@ class Video extends PureComponent {
|
|||
|
||||
render () {
|
||||
const { preview, src, aspectRatio, onOpenVideo, onCloseVideo, intl, alt, lang, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
|
||||
const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
|
||||
const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, revealed } = this.state;
|
||||
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||
const muted = this.state.muted || volume === 0;
|
||||
|
||||
let preload;
|
||||
|
||||
|
|
@ -593,12 +596,12 @@ class Video extends PureComponent {
|
|||
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
|
||||
|
||||
<div className={classNames('video-player__volume', { active: this.state.hovered })} onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
|
||||
<div className='video-player__volume__current' style={{ width: `${volume * 100}%` }} />
|
||||
<div className='video-player__volume__current' style={{ width: `${muted ? 0 : volume * 100}%` }} />
|
||||
|
||||
<span
|
||||
className={classNames('video-player__volume__handle')}
|
||||
tabIndex={0}
|
||||
style={{ left: `${volume * 100}%` }}
|
||||
style={{ left: `${muted ? 0 : volume * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@
|
|||
* @typedef InitialState
|
||||
* @property {Record<string, Account>} accounts
|
||||
* @property {InitialStateLanguage[]} languages
|
||||
* @property {boolean=} critical_updates_pending
|
||||
* @property {InitialStateMeta} meta
|
||||
*/
|
||||
|
||||
|
|
@ -140,6 +141,7 @@ export const useBlurhash = getMeta('use_blurhash');
|
|||
export const usePendingItems = getMeta('use_pending_items');
|
||||
export const version = getMeta('version');
|
||||
export const languages = initialState?.languages;
|
||||
export const criticalUpdatesPending = initialState?.critical_updates_pending;
|
||||
// @ts-expect-error
|
||||
export const statusPageUrl = getMeta('status_page_url');
|
||||
export const sso_redirect = getMeta('sso_redirect');
|
||||
|
|
|
|||
|
|
@ -282,9 +282,7 @@
|
|||
"search_results.hashtags": "Hutsetiket",
|
||||
"search_results.nothing_found": "Hierdie soekwoorde lewer niks op nie",
|
||||
"search_results.statuses": "Plasings",
|
||||
"search_results.statuses_fts_disabled": "Hierdie Mastodonbediener is nie opgestel om soekwoorde in plasings te kan vind nie.",
|
||||
"search_results.title": "Soek {q}",
|
||||
"search_results.total": "{count, number} {count, plural, one {resultaat} other {resultate}}",
|
||||
"server_banner.administered_by": "Administrasie deur:",
|
||||
"sign_in_banner.sign_in": "Sign in",
|
||||
"status.admin_status": "Open hierdie plasing as moderator",
|
||||
|
|
|
|||
|
|
@ -504,9 +504,7 @@
|
|||
"search_results.hashtags": "Etiquetas",
|
||||
"search_results.nothing_found": "No se podió trobar cosa pa estes termins de busqueda",
|
||||
"search_results.statuses": "Publicacions",
|
||||
"search_results.statuses_fts_disabled": "Buscar publicacions per lo suyo conteniu no ye disponible en este servidor de Mastodon.",
|
||||
"search_results.title": "Buscar {q}",
|
||||
"search_results.total": "{count, number} {count, plural, one {resultau} other {resultaus}}",
|
||||
"server_banner.about_active_users": "Usuarios activos en o servidor entre los zaguers 30 días (Usuarios Activos Mensuals)",
|
||||
"server_banner.active_users": "usuarios activos",
|
||||
"server_banner.administered_by": "Administrau per:",
|
||||
|
|
@ -570,8 +568,6 @@
|
|||
"subscribed_languages.lead": "Nomás los mensaches en os idiomas triaus amaneixerán en o suyo inicio y atras linias de tiempo dimpués d'o cambio. Tríe garra pa recibir mensaches en totz los idiomas.",
|
||||
"subscribed_languages.save": "Alzar cambios",
|
||||
"subscribed_languages.target": "Cambiar idiomas suscritos pa {target}",
|
||||
"suggestions.dismiss": "Descartar sucherencia",
|
||||
"suggestions.header": "Ye posible que t'intrese…",
|
||||
"tabs_bar.home": "Inicio",
|
||||
"tabs_bar.notifications": "Notificacions",
|
||||
"time_remaining.days": "{number, plural, one {# día restante} other {# días restantes}}",
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
"account.edit_profile": "تعديل الملف الشخصي",
|
||||
"account.enable_notifications": "أشعرني عندما ينشر @{name}",
|
||||
"account.endorse": "أوصِ به على صفحتك الشخصية",
|
||||
"account.featured_tags.last_status_at": "آخر مشاركة في {date}",
|
||||
"account.featured_tags.last_status_at": "آخر منشور في {date}",
|
||||
"account.featured_tags.last_status_never": "لا توجد رسائل",
|
||||
"account.featured_tags.title": "وسوم {name} المميَّزة",
|
||||
"account.follow": "متابعة",
|
||||
|
|
@ -35,11 +35,11 @@
|
|||
"account.followers.empty": "لا أحدَ يُتابع هذا المُستخدم إلى حد الآن.",
|
||||
"account.followers_counter": "{count, plural, zero{لا مُتابع} one {مُتابعٌ واحِد} two {مُتابعانِ اِثنان} few {{counter} مُتابِعين} many {{counter} مُتابِعًا} other {{counter} مُتابع}}",
|
||||
"account.following": "الاشتراكات",
|
||||
"account.following_counter": "{count, plural, zero{لا يُتابِع} one {يُتابِعُ واحد} two{يُتابِعُ اِثنان} few{يُتابِعُ {counter}} many{يُتابِعُ {counter}} other {يُتابِعُ {counter}}}",
|
||||
"account.following_counter": "{count, plural, zero{لا يُتابِع أحدًا} one {يُتابِعُ واحد} two{يُتابِعُ اِثنان} few{يُتابِعُ {counter}} many{يُتابِعُ {counter}} other {يُتابِعُ {counter}}}",
|
||||
"account.follows.empty": "لا يُتابع هذا المُستخدمُ أيَّ أحدٍ حتى الآن.",
|
||||
"account.follows_you": "يُتابِعُك",
|
||||
"account.go_to_profile": "اذهب إلى الملف الشخصي",
|
||||
"account.hide_reblogs": "إخفاء مشاركات @{name}",
|
||||
"account.hide_reblogs": "إخفاء المعاد نشرها مِن @{name}",
|
||||
"account.in_memoriam": "في الذكرى.",
|
||||
"account.joined_short": "انضم في",
|
||||
"account.languages": "تغيير اللغات المشترَك فيها",
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
"account.requested": "في انتظار القبول. اضغط لإلغاء طلب المُتابعة",
|
||||
"account.requested_follow": "لقد طلب {name} متابعتك",
|
||||
"account.share": "شارِك الملف التعريفي لـ @{name}",
|
||||
"account.show_reblogs": "عرض مشاركات @{name}",
|
||||
"account.show_reblogs": "اعرض إعادات نشر @{name}",
|
||||
"account.statuses_counter": "{count, plural, zero {لَا منشورات} one {منشور واحد} two {منشوران إثنان} few {{counter} منشورات} many {{counter} منشورًا} other {{counter} منشور}}",
|
||||
"account.unblock": "إلغاء الحَظر عن @{name}",
|
||||
"account.unblock_domain": "إلغاء الحَظر عن النِّطاق {domain}",
|
||||
|
|
@ -77,11 +77,11 @@
|
|||
"admin.dashboard.retention.cohort": "شهر التسجيل",
|
||||
"admin.dashboard.retention.cohort_size": "المستخدمون الجدد",
|
||||
"admin.impact_report.instance_accounts": "ملفات حسابات سوف يتم حذفها",
|
||||
"admin.impact_report.instance_followers": "المتابعون الذين سوف يخسرهم مستخدمونا",
|
||||
"admin.impact_report.instance_follows": "المتابعون الذين سوف يخسرهم مستخدموهم",
|
||||
"admin.impact_report.instance_followers": "المتابِعون الذين سوف يخسرهم مستخدمونا",
|
||||
"admin.impact_report.instance_follows": "المتابِعون الذين سوف يخسرهم مستخدموهم",
|
||||
"admin.impact_report.title": "موجز التأثير",
|
||||
"alert.rate_limited.message": "يُرجى إعادة المحاولة بعد {retry_time, time, medium}.",
|
||||
"alert.rate_limited.title": "المُعَدَّل مَحدود",
|
||||
"alert.rate_limited.title": "معدل الطلبات محدود",
|
||||
"alert.unexpected.message": "لقد طرأ خطأ غير متوقّع.",
|
||||
"alert.unexpected.title": "المعذرة!",
|
||||
"announcement.announcement": "إعلان",
|
||||
|
|
@ -96,7 +96,7 @@
|
|||
"bundle_column_error.network.title": "خطأ في الشبكة",
|
||||
"bundle_column_error.retry": "إعادة المُحاولة",
|
||||
"bundle_column_error.return": "العودة إلى الرئيسية",
|
||||
"bundle_column_error.routing.body": "تعذر العثور على الصفحة المطلوبة. هل أنت متأكد من أنّ عنوان URL في شريط العناوين صحيح؟",
|
||||
"bundle_column_error.routing.body": "تعذر العثور على الصفحة المطلوبة. هل أنت متأكد من أنّ الرابط التشعبي URL في شريط العناوين صحيح؟",
|
||||
"bundle_column_error.routing.title": "404",
|
||||
"bundle_modal_error.close": "إغلاق",
|
||||
"bundle_modal_error.message": "لقد حدث خطأ ما أثناء تحميل هذا العنصر.",
|
||||
|
|
@ -113,8 +113,8 @@
|
|||
"column.direct": "الإشارات الخاصة",
|
||||
"column.directory": "تَصَفُّحُ المَلفات الشخصية",
|
||||
"column.domain_blocks": "النطاقات المحظورة",
|
||||
"column.favourites": "Favorites",
|
||||
"column.firehose": "التغذيات المباشرة",
|
||||
"column.favourites": "المفضلة",
|
||||
"column.firehose": "الموجزات الحية",
|
||||
"column.follow_requests": "طلبات المتابعة",
|
||||
"column.home": "الرئيسية",
|
||||
"column.lists": "القوائم",
|
||||
|
|
@ -135,10 +135,11 @@
|
|||
"community.column_settings.remote_only": "عن بُعد فقط",
|
||||
"compose.language.change": "تغيير اللغة",
|
||||
"compose.language.search": "البحث عن لغة…",
|
||||
"compose.published.body": "تم نشر المنشور.",
|
||||
"compose.published.open": "فتح",
|
||||
"compose.published.body": "نُشِرَ المنشور.",
|
||||
"compose.published.open": "افتحه",
|
||||
"compose.saved.body": "تم حفظ المنشور.",
|
||||
"compose_form.direct_message_warning_learn_more": "تَعَلَّم المَزيد",
|
||||
"compose_form.encryption_warning": "إنّ المنشورات على ماستدون ليست مشفرة من النهاية إلى النهاية. لا تشارك أي معلومات حساسة عبر ماستدون.",
|
||||
"compose_form.encryption_warning": "إنّ المنشورات على ماستدون ليست مشفرة من الطرف إلى نهاية الطرف. لذا، لا تشارك أي معلومات حساسة عبر ماستدون.",
|
||||
"compose_form.hashtag_warning": "لن يُدرَج هذا المنشور تحت أي وسم بما أنَّه غير منشور للعامة. إلّا الرسائل المنشورة للعامة يُمكن البحث عنها بواسطة وسم.",
|
||||
"compose_form.lock_disclaimer": "حسابُك غير {locked}. يُمكن لأي شخص مُتابعتك لرؤية (منشورات المتابعين فقط).",
|
||||
"compose_form.lock_disclaimer.lock": "مُقفَل",
|
||||
|
|
@ -150,27 +151,27 @@
|
|||
"compose_form.poll.switch_to_multiple": "تغيِير الاستطلاع للسماح باِخيارات مُتعدِّدة",
|
||||
"compose_form.poll.switch_to_single": "تغيِير الاستطلاع للسماح باِخيار واحد فقط",
|
||||
"compose_form.publish": "نشر",
|
||||
"compose_form.publish_form": "انشر",
|
||||
"compose_form.publish_form": "منشور جديد",
|
||||
"compose_form.publish_loud": "{publish}!",
|
||||
"compose_form.save_changes": "احفظ التعديلات",
|
||||
"compose_form.sensitive.hide": "{count, plural, one {الإشارة إلى الوَسط كمُحتوى حسّاس} two{الإشارة إلى الوسطان كمُحتويان حسّاسان} other {الإشارة إلى الوسائط كمُحتويات حسّاسة}}",
|
||||
"compose_form.sensitive.marked": "{count, plural, one {تمَّ الإشارة إلى الوسط كمُحتوى حسّاس} two{تمَّ الإشارة إلى الوسطان كمُحتويان حسّاسان} other {تمَّ الإشارة إلى الوسائط كمُحتويات حسّاسة}}",
|
||||
"compose_form.sensitive.unmarked": "{count, plural, one {لم تَتِمّ الإشارة إلى الوسط كمُحتوى حسّاس} two{لم تَتِمّ الإشارة إلى الوسطان كمُحتويان حسّاسان} other {لم تَتِمّ الإشارة إلى الوسائط كمُحتويات حسّاسة}}",
|
||||
"compose_form.spoiler.marked": "إزالة تحذير المحتوى",
|
||||
"compose_form.spoiler.unmarked": "إنَّ النص غير مخفي",
|
||||
"compose_form.spoiler.unmarked": "إضافة تحذير للمحتوى",
|
||||
"compose_form.spoiler_placeholder": "اُكتُب تحذيركَ هُنا",
|
||||
"confirmation_modal.cancel": "إلغاء",
|
||||
"confirmations.block.block_and_report": "حظره والإبلاغ عنه",
|
||||
"confirmations.block.confirm": "حظر",
|
||||
"confirmations.block.message": "هل أنتَ مُتأكدٌ أنكَ تُريدُ حَظرَ {name}؟",
|
||||
"confirmations.cancel_follow_request.confirm": "إلغاء الطلب",
|
||||
"confirmations.cancel_follow_request.message": "متأكد من إلغاء طلب متابعة {name}؟",
|
||||
"confirmations.cancel_follow_request.message": "متأكد من أنك تريد إلغاء طلب متابعتك لـ {name}؟",
|
||||
"confirmations.delete.confirm": "حذف",
|
||||
"confirmations.delete.message": "هل أنتَ مُتأكدٌ أنك تُريدُ حَذفَ هذا المنشور؟",
|
||||
"confirmations.delete_list.confirm": "حذف",
|
||||
"confirmations.delete_list.message": "هل أنتَ مُتأكدٌ أنكَ تُريدُ حَذفَ هذِهِ القائمة بشكلٍ دائم؟",
|
||||
"confirmations.discard_edit_media.confirm": "تجاهل",
|
||||
"confirmations.discard_edit_media.message": "لديك تغييرات غير محفوظة لوصف الوسائط أو معاينتها، تجاهلها على أي حال؟",
|
||||
"confirmations.discard_edit_media.message": "لديك تغييرات غير محفوظة لوصف الوسائط أو معاينتها، أتريد تجاهلها على أي حال؟",
|
||||
"confirmations.domain_block.confirm": "حظر اِسم النِّطاق بشكلٍ كامل",
|
||||
"confirmations.domain_block.message": "متأكد من أنك تود حظر اسم النطاق {domain} بالكامل ؟ في غالب الأحيان يُستَحسَن كتم أو حظر بعض الحسابات بدلا من حظر نطاق بالكامل.\nلن تتمكن مِن رؤية محتوى هذا النطاق لا على خيوطك العمومية و لا في إشعاراتك. سوف يتم كذلك إزالة كافة متابعيك المنتمين إلى هذا النطاق.",
|
||||
"confirmations.edit.confirm": "تعديل",
|
||||
|
|
@ -198,11 +199,11 @@
|
|||
"directory.recently_active": "نشط مؤخرا",
|
||||
"disabled_account_banner.account_settings": "إعدادات الحساب",
|
||||
"disabled_account_banner.text": "حسابك {disabledAccount} معطل حاليا.",
|
||||
"dismissable_banner.community_timeline": "هذه هي أحدث المشاركات العامة من الأشخاص الذين تُستضاف حساباتهم على {domain}.",
|
||||
"dismissable_banner.community_timeline": "هذه هي أحدث المنشورات العامة من أشخاص تُستضاف حساباتهم على {domain}.",
|
||||
"dismissable_banner.dismiss": "رفض",
|
||||
"dismissable_banner.explore_links": "هذه القصص الإخبارية يتحدث عنها حاليًا أشخاص على هذا الخادم وكذا على الخوادم الأخرى للشبكة اللامركزية.",
|
||||
"dismissable_banner.explore_statuses": "هذه هي المنشورات الرائجة على الشبكات الاجتماعيّة اليوم. تظهر المنشورات التي أعيد مشاركتها وحازت على مفضّلات أكثر في مرتبة عليا.",
|
||||
"dismissable_banner.explore_tags": "هذه الوسوم تكتسب جذب اهتمام الناس حاليًا على هذا الخادم وكذا على الخوادم الأخرى للشبكة اللامركزية.",
|
||||
"dismissable_banner.explore_links": "هذه هي القصص الإخبارية الأكثر مشاركة على الشبكة الاجتماعية اليوم. القصص الإخبارية الأحدث التي تنشرها أشخاص مختلفة هي مصنفة في الأعلى.",
|
||||
"dismissable_banner.explore_statuses": "هذه هي المنشورات الرائجة على الشبكات الاجتماعيّة اليوم. تظهر المنشورات المعاد نشرها والحائزة على مفضّلات أكثر في مرتبة عليا.",
|
||||
"dismissable_banner.explore_tags": "هذه هي الوسوم تكتسب جذب الاهتمام حاليًا على الويب الاجتماعي. الوسوم التي يستخدمها مختلف الناس تحتل مرتبة عليا.",
|
||||
"dismissable_banner.public_timeline": "هذه هي أحدث المنشورات العامة من الناس على الشبكة الاجتماعية التي يتبعها الناس على {domain}.",
|
||||
"embed.instructions": "يمكنكم إدماج هذا المنشور على موقعكم الإلكتروني عن طريق نسخ الشفرة أدناه.",
|
||||
"embed.preview": "إليك ما سيبدو عليه:",
|
||||
|
|
@ -226,7 +227,7 @@
|
|||
"empty_column.account_unavailable": "الملف التعريفي غير متوفر",
|
||||
"empty_column.blocks": "لم تقم بحظر أي مستخدِم بعد.",
|
||||
"empty_column.bookmarked_statuses": "ليس لديك أية منشورات في الفواصل المرجعية بعد. عندما ستقوم بإضافة البعض منها، ستظهر هنا.",
|
||||
"empty_column.community": "الخط العام المحلي فارغ. أكتب شيئا ما للعامة كبداية!",
|
||||
"empty_column.community": "الخيط العام المحلي فارغ. أكتب شيئا ما للعامة كبداية!",
|
||||
"empty_column.direct": "لم يتم الإشارة إليك بشكل خاص بعد. عندما تتلقى أو ترسل إشارة، سيتم عرضها هنا.",
|
||||
"empty_column.domain_blocks": "ليس هناك نطاقات تم حجبها بعد.",
|
||||
"empty_column.explore_statuses": "ليس هناك ما هو متداوَل الآن. عد في وقت لاحق!",
|
||||
|
|
@ -235,9 +236,9 @@
|
|||
"empty_column.follow_requests": "ليس عندك أي طلب للمتابعة بعد. سوف تظهر طلباتك هنا إن قمت بتلقي البعض منها.",
|
||||
"empty_column.followed_tags": "لم تُتابع أي وسم بعدُ. ستظهر الوسوم هنا حينما تفعل ذلك.",
|
||||
"empty_column.hashtag": "ليس هناك بعدُ أي محتوى ذو علاقة بهذا الوسم.",
|
||||
"empty_column.home": "إنّ الخيط الزمني لصفحتك الرئيسية فارغ. قم بزيارة {public} أو استخدم حقل البحث لكي تكتشف مستخدمين آخرين.",
|
||||
"empty_column.home": "إنّ الخيط الزمني لصفحتك الرئيسة فارغ. قم بمتابعة المزيد من الناس كي يمتلأ.",
|
||||
"empty_column.list": "هذه القائمة فارغة مؤقتا و لكن سوف تمتلئ تدريجيا عندما يبدأ الأعضاء المُنتَمين إليها بنشر منشورات.",
|
||||
"empty_column.lists": "ليس عندك أية قائمة بعد. سوف تظهر قائمتك هنا إن قمت بإنشاء واحدة.",
|
||||
"empty_column.lists": "ليس عندك أية قائمة بعد. سوف تظهر قوائمك هنا إن قمت بإنشاء واحدة.",
|
||||
"empty_column.mutes": "لم تقم بكتم أي مستخدم بعد.",
|
||||
"empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.",
|
||||
"empty_column.public": "لا يوجد أي شيء هنا! قم بنشر شيء ما للعامة، أو اتبع المستخدمين الآخرين المتواجدين على الخوادم الأخرى لملء خيط المحادثات",
|
||||
|
|
@ -250,7 +251,7 @@
|
|||
"explore.search_results": "نتائج البحث",
|
||||
"explore.suggested_follows": "أشخاص",
|
||||
"explore.title": "استكشف",
|
||||
"explore.trending_links": "الأخبار",
|
||||
"explore.trending_links": "المُستجدّات",
|
||||
"explore.trending_statuses": "المنشورات",
|
||||
"explore.trending_tags": "وُسُوم",
|
||||
"filter_modal.added.context_mismatch_explanation": "فئة عامل التصفية هذه لا تنطبق على السياق الذي وصلت فيه إلى هذه المشاركة. إذا كنت ترغب في تصفية المنشور في هذا السياق أيضا، فسيتعين عليك تعديل عامل التصفية.",
|
||||
|
|
@ -266,7 +267,7 @@
|
|||
"filter_modal.select_filter.expired": "منتهية الصلاحيّة",
|
||||
"filter_modal.select_filter.prompt_new": "فئة جديدة: {name}",
|
||||
"filter_modal.select_filter.search": "البحث أو الإنشاء",
|
||||
"filter_modal.select_filter.subtitle": "استخدام فئة موجودة أو إنشاء فئة جديدة",
|
||||
"filter_modal.select_filter.subtitle": "استخدم فئة موجودة أو قم بإنشاء فئة جديدة",
|
||||
"filter_modal.select_filter.title": "تصفية هذا المنشور",
|
||||
"filter_modal.title.status": "تصفية منشور",
|
||||
"firehose.all": "الكل",
|
||||
|
|
@ -274,9 +275,9 @@
|
|||
"firehose.remote": "خوادم أخرى",
|
||||
"follow_request.authorize": "ترخيص",
|
||||
"follow_request.reject": "رفض",
|
||||
"follow_requests.unlocked_explanation": "على الرغم من أن حسابك غير مقفل، فإن موظفين الـ{domain} ظنوا أنك قد ترغب في مراجعة طلبات المتابعة من هذه الحسابات يدوياً.",
|
||||
"follow_requests.unlocked_explanation": "حتى وإن كان حسابك غير مقفل، يعتقد فريق {domain} أنك قد ترغب في مراجعة طلبات المتابعة من هذه الحسابات يدوياً.",
|
||||
"followed_tags": "الوسوم المتابَعة",
|
||||
"footer.about": "حَول",
|
||||
"footer.about": "عن",
|
||||
"footer.directory": "دليل الصفحات التعريفية",
|
||||
"footer.get_app": "احصل على التطبيق",
|
||||
"footer.invite": "دعوة أشخاص",
|
||||
|
|
@ -295,19 +296,26 @@
|
|||
"hashtag.column_settings.tag_mode.any": "أي كان مِن هذه",
|
||||
"hashtag.column_settings.tag_mode.none": "لا شيء مِن هذه",
|
||||
"hashtag.column_settings.tag_toggle": "إدراج الوسوم الإضافية لهذا العمود",
|
||||
"hashtag.counter_by_accounts": "{count, plural, zero {لَا مُشارك} one {مُشارَك واحد} two {مُشارِكان إثنان} few {{counter} مشاركين} many {{counter} مُشاركًا} other {{counter} مُشارِك}}",
|
||||
"hashtag.counter_by_uses": "{count, plural, zero {لَا منشورات} one {منشور واحد} two {منشوران إثنان} few {{counter} منشورات} many {{counter} منشورًا} other {{counter} منشور}}",
|
||||
"hashtag.counter_by_uses_today": "{count, plural, zero {لَا منشورات} one {منشور واحد} two {منشوران إثنان} few {{counter} منشورات} many {{counter} منشورًا} other {{counter} منشور}}",
|
||||
"hashtag.follow": "اتبع الوسم",
|
||||
"hashtag.unfollow": "ألغِ متابعة الوسم",
|
||||
"home.actions.go_to_explore": "اطّلع على الرائج حاليا",
|
||||
"hashtags.and_other": "…و {count, plural, zero {} one {# واحد آخر} two {# اثنان آخران} few {# آخرون} many {# آخَرًا}other {# آخرون}}",
|
||||
"home.actions.go_to_explore": "اطّلع على ما هو رائج حاليا",
|
||||
"home.actions.go_to_suggestions": "ابحث عن أشخاص لِمُتابعتهم",
|
||||
"home.column_settings.basic": "الأساسية",
|
||||
"home.column_settings.show_reblogs": "اعرض الترقيات",
|
||||
"home.column_settings.show_reblogs": "اعرض المعاد نشرها",
|
||||
"home.column_settings.show_replies": "اعرض الردود",
|
||||
"home.explore_prompt.body": "سوف يحتوي خيط أخبارك الرئيسي على مزيج من المشاركات من الوسوم التي اخترت متابعتها، والأشخاص الذين اخترت متابعتهم، والمنشورات التي قاموا بدعمها. ومع ذلك، إن كانت تبدو الأمور هادئة جدا، ماذا لو:",
|
||||
"home.explore_prompt.title": "هذا مقرك الرئيسي داخل ماستدون.",
|
||||
"home.explore_prompt.body": "سوف يحتوي خيط أخبارك الرئيسي على مزيج من المنشورات مِنها التي تحتوي على وسوم اخترتَ متابعتها، وأشخاص اخترتَ متابعتهم والمنشورات التي أعادوا نشرها. ومع ذلك، إن لا زال خيط أخبارك يبدو هادئا جدا، ماذا لو:",
|
||||
"home.explore_prompt.title": "هذه هي صفحتك الرئيسة في ماستدون.",
|
||||
"home.hide_announcements": "إخفاء الإعلانات",
|
||||
"home.pending_critical_update.body": "يرجى تحديث خادم ماستدون في أقرب وقت ممكن!",
|
||||
"home.pending_critical_update.link": "اطّلع على التحديثات",
|
||||
"home.pending_critical_update.title": "تحديث أمان حرج متوفر!",
|
||||
"home.show_announcements": "إظهار الإعلانات",
|
||||
"interaction_modal.description.favourite": "بفضل حساب على ماستدون، يمكنك إضافة هذا المنشور إلى مفضلتك لإبلاغ الناشر عن تقديرك وكذا للاحتفاظ بالمنشور إلى وقت لاحق.",
|
||||
"interaction_modal.description.follow": "مع حساب في ماستدون، يمكنك متابعة {name} وتلقي منشوراته على خيطك الرئيس.",
|
||||
"interaction_modal.description.follow": "بفضل حساب في ماستدون، يمكنك متابعة {name} وتلقي منشوراته في موجزات خيطك الرئيس.",
|
||||
"interaction_modal.description.reblog": "مع حساب في ماستدون، يمكنك تعزيز هذا المنشور ومشاركته مع مُتابِعيك.",
|
||||
"interaction_modal.description.reply": "مع حساب في ماستدون، يمكنك الرد على هذا المنشور.",
|
||||
"interaction_modal.login.action": "خذني إلى خادمي",
|
||||
|
|
@ -316,27 +324,27 @@
|
|||
"interaction_modal.on_another_server": "على خادم مختلف",
|
||||
"interaction_modal.on_this_server": "على هذا الخادم",
|
||||
"interaction_modal.sign_in": "لم تقم بتسجيل الدخول إلى هذا الخادم. أين هو مستضاف حسابك؟",
|
||||
"interaction_modal.sign_in_hint": "تلميح: هذا هو الموقع الذي سجّلت عن طريقه. إن لم تتذكّر/ين اسم الموقع، يمكنك البحث عن الرسالة الترحيبيّة في بريدك الالكتروني. يمكنك أيضاً استخدام إسم المستخدم/ـة الكامل! (مثلاً: @Mastadon@mastadon.social)",
|
||||
"interaction_modal.sign_in_hint": "تلميح: هذا هو الموقع الذي أنشأت فيه حسابك. إن لم تتذكّر/ين اسم الموقع، يمكنك البحث عن الرسالة الترحيبيّة في بريدك الإلكتروني. كما يمكنك أيضاً استخدام اسم المستخدم/ـة الكامل! (مثلاً: @Mastodon@mastodon.social)",
|
||||
"interaction_modal.title.favourite": "إضافة منشور {name} إلى المفضلة",
|
||||
"interaction_modal.title.follow": "اتبع {name}",
|
||||
"interaction_modal.title.reblog": "مشاركة منشور {name}",
|
||||
"interaction_modal.title.reblog": "إعادة نشر منشور {name}",
|
||||
"interaction_modal.title.reply": "الرد على منشور {name}",
|
||||
"intervals.full.days": "{number, plural, one {# يوم} other {# أيام}}",
|
||||
"intervals.full.hours": "{number, plural, one {# ساعة} other {# ساعات}}",
|
||||
"intervals.full.minutes": "{number, plural, one {# دقيقة} other {# دقائق}}",
|
||||
"keyboard_shortcuts.back": "للعودة",
|
||||
"keyboard_shortcuts.blocked": "لفتح قائمة المستخدمين المحظورين",
|
||||
"keyboard_shortcuts.boost": "للترقية",
|
||||
"keyboard_shortcuts.boost": "لإعادة النشر",
|
||||
"keyboard_shortcuts.column": "للتركيز على منشور على أحد الأعمدة",
|
||||
"keyboard_shortcuts.compose": "للتركيز على نافذة تحرير النصوص",
|
||||
"keyboard_shortcuts.description": "الوصف",
|
||||
"keyboard_shortcuts.direct": "to open direct messages column",
|
||||
"keyboard_shortcuts.direct": "لفتح عمود الإشارات الخاصة",
|
||||
"keyboard_shortcuts.down": "للانتقال إلى أسفل القائمة",
|
||||
"keyboard_shortcuts.enter": "لفتح المنشور",
|
||||
"keyboard_shortcuts.favourite": "لإضافة المنشور إلى المفضلة",
|
||||
"keyboard_shortcuts.favourites": "لفتح قائمة المفضلات",
|
||||
"keyboard_shortcuts.federated": "لفتح الخيط الزمني الفديرالي",
|
||||
"keyboard_shortcuts.heading": "Keyboard Shortcuts",
|
||||
"keyboard_shortcuts.heading": "اختصارات لوحة المفاتيح",
|
||||
"keyboard_shortcuts.home": "لفتح الخيط الرئيسي",
|
||||
"keyboard_shortcuts.hotkey": "مفتاح الاختصار",
|
||||
"keyboard_shortcuts.legend": "لعرض هذا المفتاح",
|
||||
|
|
@ -371,10 +379,10 @@
|
|||
"lists.delete": "احذف القائمة",
|
||||
"lists.edit": "عدّل القائمة",
|
||||
"lists.edit.submit": "تعديل العنوان",
|
||||
"lists.exclusive": "إخفاء هذه المشاركات من الصفحة الرئيسية",
|
||||
"lists.new.create": "إنشاء قائمة",
|
||||
"lists.exclusive": "إخفاء هذه المنشورات من الخيط الرئيسي",
|
||||
"lists.new.create": "إضافة قائمة",
|
||||
"lists.new.title_placeholder": "عنوان القائمة الجديدة",
|
||||
"lists.replies_policy.followed": "أي مستخدم متابِع",
|
||||
"lists.replies_policy.followed": "أي مستخدم متابَع",
|
||||
"lists.replies_policy.list": "أعضاء القائمة",
|
||||
"lists.replies_policy.none": "لا أحد",
|
||||
"lists.replies_policy.title": "عرض الردود لـ:",
|
||||
|
|
@ -402,10 +410,11 @@
|
|||
"navigation_bar.filters": "الكلمات المكتومة",
|
||||
"navigation_bar.follow_requests": "طلبات المتابعة",
|
||||
"navigation_bar.followed_tags": "الوسوم المتابَعة",
|
||||
"navigation_bar.follows_and_followers": "المتابِعين والمتابَعون",
|
||||
"navigation_bar.follows_and_followers": "المتابِعون والمتابَعون",
|
||||
"navigation_bar.lists": "القوائم",
|
||||
"navigation_bar.logout": "خروج",
|
||||
"navigation_bar.mutes": "الحسابات المكتومة",
|
||||
"navigation_bar.opened_in_classic_interface": "تُفتَح المنشورات والحسابات وغيرها من الصفحات الخاصة بشكل مبدئي على واجهة الويب التقليدية.",
|
||||
"navigation_bar.personal": "شخصي",
|
||||
"navigation_bar.pins": "المنشورات المُثَبَّتَة",
|
||||
"navigation_bar.preferences": "التفضيلات",
|
||||
|
|
@ -416,7 +425,7 @@
|
|||
"notification.admin.report": "{name} أبلغ عن {target}",
|
||||
"notification.admin.sign_up": "أنشأ {name} حسابًا",
|
||||
"notification.favourite": "أضاف {name} منشورك إلى مفضلته",
|
||||
"notification.follow": "{name} يتابعك",
|
||||
"notification.follow": "يتابعك {name}",
|
||||
"notification.follow_request": "لقد طلب {name} متابعتك",
|
||||
"notification.mention": "{name} ذكرك",
|
||||
"notification.own_poll": "انتهى استطلاعك للرأي",
|
||||
|
|
@ -426,7 +435,7 @@
|
|||
"notification.update": "عدّلَ {name} منشورًا",
|
||||
"notifications.clear": "مسح الإشعارات",
|
||||
"notifications.clear_confirmation": "متأكد من أنك تود مسح جميع الإشعارات الخاصة بك و المتلقاة إلى حد الآن ؟",
|
||||
"notifications.column_settings.admin.report": "التقارير الجديدة:",
|
||||
"notifications.column_settings.admin.report": "التبليغات الجديدة:",
|
||||
"notifications.column_settings.admin.sign_up": "التسجيلات الجديدة:",
|
||||
"notifications.column_settings.alert": "إشعارات سطح المكتب",
|
||||
"notifications.column_settings.favourite": "المفضلة:",
|
||||
|
|
@ -438,7 +447,7 @@
|
|||
"notifications.column_settings.mention": "الإشارات:",
|
||||
"notifications.column_settings.poll": "نتائج استطلاع الرأي:",
|
||||
"notifications.column_settings.push": "الإشعارات",
|
||||
"notifications.column_settings.reblog": "الترقيّات:",
|
||||
"notifications.column_settings.reblog": "المعاد نشرها:",
|
||||
"notifications.column_settings.show": "اعرِضها في عمود",
|
||||
"notifications.column_settings.sound": "أصدر صوتا",
|
||||
"notifications.column_settings.status": "منشورات جديدة:",
|
||||
|
|
@ -446,7 +455,7 @@
|
|||
"notifications.column_settings.unread_notifications.highlight": "علّم الإشعارات غير المقرؤة",
|
||||
"notifications.column_settings.update": "التعديلات:",
|
||||
"notifications.filter.all": "الكل",
|
||||
"notifications.filter.boosts": "الترقيات",
|
||||
"notifications.filter.boosts": "المعاد نشرها",
|
||||
"notifications.filter.favourites": "المفضلة",
|
||||
"notifications.filter.follows": "يتابِع",
|
||||
"notifications.filter.mentions": "الإشارات",
|
||||
|
|
@ -461,40 +470,40 @@
|
|||
"notifications_permission_banner.enable": "تفعيل إشعارات سطح المكتب",
|
||||
"notifications_permission_banner.how_to_control": "لتلقي الإشعارات عندما لا يكون ماستدون مفتوح، قم بتفعيل إشعارات سطح المكتب، يمكنك التحكم بدقة في أنواع التفاعلات التي تولد إشعارات سطح المكتب من خلال زر الـ{icon} أعلاه بمجرد تفعيلها.",
|
||||
"notifications_permission_banner.title": "لا تفوت شيئاً أبداً",
|
||||
"onboarding.action.back": "العودة للخلف",
|
||||
"onboarding.actions.back": "العودة للخلف",
|
||||
"onboarding.actions.go_to_explore": "See what's trending",
|
||||
"onboarding.actions.go_to_home": "Go to your home feed",
|
||||
"onboarding.action.back": "تراجع",
|
||||
"onboarding.actions.back": "تراجع",
|
||||
"onboarding.actions.go_to_explore": "خذني إلى المتداولة",
|
||||
"onboarding.actions.go_to_home": "خذني إلى وصلات خيطي الرئيس",
|
||||
"onboarding.compose.template": "مرحبا #ماستدون!",
|
||||
"onboarding.follows.empty": "نأسف، لا يمكن عرض نتائج في الوقت الحالي. جرب البحث أو انتقل لصفحة الاستكشاف لإيجاد أشخاص للمتابعة، أو حاول مرة أخرى.",
|
||||
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
|
||||
"onboarding.follows.title": "Popular on Mastodon",
|
||||
"onboarding.follows.lead": "مقتطفات خيطك الرئيس هي الطريقة الأساسية لتجربة ماستدون. كلما زاد عدد الأشخاص الذين تتبعهم، كلما زاد خيط أخبارك نشاطا وإثارة للاهتمام. بداية، إليك بعض الاقتراحات:",
|
||||
"onboarding.follows.title": "أضفِ طابعا شخصيا على موجزات خيطك الرئيس",
|
||||
"onboarding.share.lead": "اسمح للأشخاص بمعرفة إمكانية الوصول إليك على ماستدون!",
|
||||
"onboarding.share.message": "أنا {username} على #Mastodon! اتبعني على {url}",
|
||||
"onboarding.share.message": "أنا {username} في #Mastodon! تعال لمتابعتي على {url}",
|
||||
"onboarding.share.next_steps": "الخطوات المحتملة التالية:",
|
||||
"onboarding.share.title": "شارك ملفك التعريفي",
|
||||
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
|
||||
"onboarding.start.skip": "Want to skip right ahead?",
|
||||
"onboarding.start.title": "لقد قمت بها!",
|
||||
"onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
|
||||
"onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
|
||||
"onboarding.steps.publish_status.body": "Say hello to the world.",
|
||||
"onboarding.start.lead": "أنت الآن جزء من ماستدون، منصة إعلامية اجتماعية فريدة من نوعها ولا مركزية حيث أنت - وليست الخوارزميات - من يقوم بضبط تجربتك الخاصة. دعنا نبدأ على هذه الحدود الاجتماعية الجديدة:",
|
||||
"onboarding.start.skip": "ألست بحاجة للمساعدة للبداية؟",
|
||||
"onboarding.start.title": "لقد نجحت!",
|
||||
"onboarding.steps.follow_people.body": "إن متابعة الأشخاص المثيرين للاهتمام هي غاية ماستدون.",
|
||||
"onboarding.steps.follow_people.title": "أضفِ طابعا شخصيا على خيطك الرئيس",
|
||||
"onboarding.steps.publish_status.body": "قل مرحبا للعالَم عبر نصّ أو صور أو فيديوهات أو استطلاعات رأي {emoji}",
|
||||
"onboarding.steps.publish_status.title": "قم بإنشاء أول منشور لك",
|
||||
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
|
||||
"onboarding.steps.setup_profile.title": "Customize your profile",
|
||||
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
|
||||
"onboarding.steps.share_profile.title": "Share your profile",
|
||||
"onboarding.steps.setup_profile.body": "قم بتعزيز تفاعلاتك عبر الحصول على مِلَفّ شخصي شامل.",
|
||||
"onboarding.steps.setup_profile.title": "قم بتخصيص ملفك التعريفي",
|
||||
"onboarding.steps.share_profile.body": "أخبر أصدقائك بكيفية العثور عليك على ماستدون",
|
||||
"onboarding.steps.share_profile.title": "شارك مِلَفّ ماستدون التعريفي الخاص بك",
|
||||
"onboarding.tips.2fa": "<strong>هل تعلم؟</strong> يمكنك تأمين حسابك عن طريق إعداد المصادقة ذات عاملين في إعدادات حسابك. تعمل مع أي تطبيق TOTP من اختيارك، لا حاجة لرقم هاتف!",
|
||||
"onboarding.tips.accounts_from_other_servers": "<strong>هل تعلم؟</strong> لأن ماستدون لامركزية فإن بعض الحسابات التي تصادفها ستكون مستضافة على خوادم غير خادمك. ومع ذلك يمكنك التفاعل معها بسلاسة! خادمهم هو النصف الآخر من اسم المستخدم خاصتهم!",
|
||||
"onboarding.tips.migration": "<strong>هل تعلم؟</strong> إذا شعرت بأن {domain} ليس خياراً ممتازاً لك في المستقبل، فيمكنك الانتقال إلى خادم ماستدون آخر دون خسارة متابعيك. يمكنك حتى استضافة خادمك الخاص!",
|
||||
"onboarding.tips.verification": "<strong>هل تعلم؟</strong> يمكنك تأكيد حسابك عبر وضع رابط إلى ملفك الشخصي على ماستدون في موقعك الخاص وإضافة رابط موقعك على ملفك الشخصي. لا حاجة لأي رسوم أو مستندات!",
|
||||
"onboarding.tips.verification": "<strong>هل تعلم؟</strong> يمكنك تأكيد حسابك عبر وضع رابط إلى ملف ماستدون الشخصي الخاص بك في موقعك الخاص وإضافة رابط موقعك على ملفك الشخصي. لا حاجة لأي رسوم أو مستندات!",
|
||||
"password_confirmation.exceeds_maxlength": "تأكيد كلمة المرور يتجاوز الحد الأقصى لطول كلمة المرور",
|
||||
"password_confirmation.mismatching": "تأكيد كلمة المرور غير مطابق",
|
||||
"picture_in_picture.restore": "ضعها مرة أخرى",
|
||||
"poll.closed": "انتهى",
|
||||
"poll.refresh": "تحديث",
|
||||
"poll.reveal": "عرض النتائج",
|
||||
"poll.total_people": "{count, plural, one {# شخص} two {# شخصين} few {# أشخاص} many {# أشخاص} other {# أشخاص}}",
|
||||
"poll.total_people": "{count, plural, one {شخص واحد} two {شخصان} few {# أشخاص} many {# شخصًا} other {# شخصٍ}}",
|
||||
"poll.total_votes": "{count, plural, one {# صوت} other {# أصوات}}",
|
||||
"poll.vote": "صَوّت",
|
||||
"poll.voted": "لقد صوّتت على هذه الإجابة",
|
||||
|
|
@ -514,7 +523,7 @@
|
|||
"privacy_policy.title": "سياسة الخصوصية",
|
||||
"refresh": "أنعِش",
|
||||
"regeneration_indicator.label": "جارٍ التحميل…",
|
||||
"regeneration_indicator.sublabel": "جارٍ تجهيز تغذية صفحتك الرئيسية!",
|
||||
"regeneration_indicator.sublabel": "جارٍ تجهيز موجزات خيطك الرئيس!",
|
||||
"relative_time.days": "{number}ي",
|
||||
"relative_time.full.days": "منذ {number, plural, zero {} one {# يوم} two {# يومين} few {# أيام} many {# أيام} other {# يوم}}",
|
||||
"relative_time.full.hours": "منذ {number, plural, zero {} one {ساعة واحدة} two {ساعتَيْن} few {# ساعات} many {# ساعة} other {# ساعة}}",
|
||||
|
|
@ -528,7 +537,8 @@
|
|||
"relative_time.today": "اليوم",
|
||||
"reply_indicator.cancel": "إلغاء",
|
||||
"report.block": "حظر",
|
||||
"report.block_explanation": "لن ترى مشاركاتهم ولن يمكنهم متابعتك أو رؤية مشاركاتك، سيكون بديهيا لهم أنهم مكتمون.",
|
||||
"report.block_explanation": "لن ترى منشوراته ولن يمكنه متابعتك أو رؤية منشوراتك، سيكون بديهيا له أنه مكتوم.",
|
||||
"report.categories.legal": "إشعارات قانونية",
|
||||
"report.categories.other": "أخرى",
|
||||
"report.categories.spam": "مزعج",
|
||||
"report.categories.violation": "المحتوى ينتهك شرطا أو عدة شروط استخدام للخادم",
|
||||
|
|
@ -541,7 +551,7 @@
|
|||
"report.forward": "التحويل إلى {target}",
|
||||
"report.forward_hint": "هذا الحساب ينتمي إلى خادم آخَر. هل تودّ إرسال نسخة مجهولة مِن التقرير إلى هنالك أيضًا؟",
|
||||
"report.mute": "كتم",
|
||||
"report.mute_explanation": "لن ترى مشاركاتهم. لكن سيبقى بإمكانهم متابعتك ورؤية مشاركاتك دون أن يعرفوا أنهم مكتمون.",
|
||||
"report.mute_explanation": "لن ترى منشوراته. لكن سيبقى بإمكانه متابعتك ورؤية منشوراتك دون أن يعرف أنه مكتوم.",
|
||||
"report.next": "التالي",
|
||||
"report.placeholder": "تعليقات إضافية",
|
||||
"report.reasons.dislike": "لايعجبني",
|
||||
|
|
@ -557,7 +567,7 @@
|
|||
"report.rules.subtitle": "اختر كل ما ينطبق",
|
||||
"report.rules.title": "ما هي القواعد المنتهكة؟",
|
||||
"report.statuses.subtitle": "اختر كل ما ينطبق",
|
||||
"report.statuses.title": "هل توجد مشاركات تدعم صحة هذا البلاغ؟",
|
||||
"report.statuses.title": "هل هناك أي منشورات تدعم صحة هذا التبليغ؟",
|
||||
"report.submit": "إرسال",
|
||||
"report.target": "ابلغ عن {target}",
|
||||
"report.thanks.take_action": "فيما يلي خياراتك للتحكم بما يُعرَض عليك في ماستدون:",
|
||||
|
|
@ -565,7 +575,7 @@
|
|||
"report.thanks.title": "هل ترغب في مشاهدة هذا؟",
|
||||
"report.thanks.title_actionable": "شُكرًا لَكَ على الإبلاغ، سَوفَ نَنظُرُ فِي هَذَا الأمر.",
|
||||
"report.unfollow": "إلغاء متابعة @{name}",
|
||||
"report.unfollow_explanation": "أنت تتابع هذا الحساب، لإزالة مَنشوراته من تغذيَتِكَ الرئيسة ألغ متابعته.",
|
||||
"report.unfollow_explanation": "أنت تتابع هذا الحساب، لإزالة مَنشوراته من موجزات خيطك الرئيس، ألغ متابعته.",
|
||||
"report_notification.attached_statuses": "{count, plural, one {{count} منشور} other {{count} منشورات}} مرفقة",
|
||||
"report_notification.categories.legal": "أمور قانونية",
|
||||
"report_notification.categories.other": "آخر",
|
||||
|
|
@ -574,22 +584,26 @@
|
|||
"report_notification.open": "فتح التقرير",
|
||||
"search.no_recent_searches": "ما من عمليات بحث تمت مؤخرًا",
|
||||
"search.placeholder": "ابحث",
|
||||
"search.quick_action.account_search": "الملفات الشخصية المطابقة {x}",
|
||||
"search.quick_action.account_search": "الملفات التعريفية المطابقة لـ {x}",
|
||||
"search.quick_action.go_to_account": "الذهاب إلى الصفحة الشخصية لـ {x}",
|
||||
"search.quick_action.go_to_hashtag": "الذهاب إلى الوسم {x}",
|
||||
"search.quick_action.go_to_hashtag": "الذهاب إلى وسم {x}",
|
||||
"search.quick_action.open_url": "فتح الرابط التشعبي في ماستدون",
|
||||
"search.quick_action.status_search": "المشاركات المطابقة {x}",
|
||||
"search.quick_action.status_search": "المنشورات المطابقة لـ {x}",
|
||||
"search.search_or_paste": "ابحث أو أدخل رابطا تشعبيا URL",
|
||||
"search_popout.full_text_search_disabled_message": "غير متوفر على {domain}.",
|
||||
"search_popout.language_code": "رمز لغة ISO",
|
||||
"search_popout.options": "خيارات البحث",
|
||||
"search_popout.quick_actions": "الإجراءات السريعة",
|
||||
"search_popout.recent": "عمليات البحث الأخيرة",
|
||||
"search_popout.specific_date": "تاريخ محدد",
|
||||
"search_popout.user": "مستخدم",
|
||||
"search_results.accounts": "الصفحات التعريفية",
|
||||
"search_results.all": "الكل",
|
||||
"search_results.hashtags": "الوُسوم",
|
||||
"search_results.nothing_found": "تعذر العثور على نتائج تتضمن هذه المصطلحات",
|
||||
"search_results.see_all": "رؤية الكل",
|
||||
"search_results.statuses": "المنشورات",
|
||||
"search_results.statuses_fts_disabled": "البحث عن المنشورات عن طريق المحتوى ليس مفعل في خادم ماستدون هذا.",
|
||||
"search_results.title": "البحث عن {q}",
|
||||
"search_results.total": "{count, number} {count, plural, zero {} one {نتيجة} two {نتيجتين} few {نتائج} many {نتائج} other {نتائج}}",
|
||||
"server_banner.about_active_users": "الأشخاص الذين يستخدمون هذا الخادم خلال الأيام الثلاثين الأخيرة (المستخدمون النشطون شهريًا)",
|
||||
"server_banner.active_users": "مستخدم نشط",
|
||||
"server_banner.administered_by": "يُديره:",
|
||||
|
|
@ -605,8 +619,8 @@
|
|||
"status.admin_status": "افتح هذا المنشور على واجهة الإشراف",
|
||||
"status.block": "احجب @{name}",
|
||||
"status.bookmark": "أضفه إلى الفواصل المرجعية",
|
||||
"status.cancel_reblog_private": "إلغاء الترقية",
|
||||
"status.cannot_reblog": "تعذرت ترقية هذا المنشور",
|
||||
"status.cancel_reblog_private": "إلغاء إعادة النشر",
|
||||
"status.cannot_reblog": "لا يمكن إعادة نشر هذا المنشور",
|
||||
"status.copy": "انسخ رابط الرسالة",
|
||||
"status.delete": "احذف",
|
||||
"status.detailed_status": "تفاصيل المحادثة",
|
||||
|
|
@ -625,20 +639,20 @@
|
|||
"status.load_more": "حمّل المزيد",
|
||||
"status.media.open": "اضغط للفتح",
|
||||
"status.media.show": "اضغط للإظهار",
|
||||
"status.media_hidden": "الصورة مستترة",
|
||||
"status.media_hidden": "وسائط مخفية",
|
||||
"status.mention": "أذكُر @{name}",
|
||||
"status.more": "المزيد",
|
||||
"status.mute": "أكتم @{name}",
|
||||
"status.mute_conversation": "كتم المحادثة",
|
||||
"status.open": "وسع هذه المشاركة",
|
||||
"status.open": "وسّع هذا المنشور",
|
||||
"status.pin": "دبّسه على الصفحة التعريفية",
|
||||
"status.pinned": "منشور مثبَّت",
|
||||
"status.read_more": "اقرأ المزيد",
|
||||
"status.reblog": "رَقِّي",
|
||||
"status.reblog_private": "القيام بالترقية إلى الجمهور الأصلي",
|
||||
"status.reblog": "إعادة النشر",
|
||||
"status.reblog_private": "إعادة النشر إلى الجمهور الأصلي",
|
||||
"status.reblogged_by": "شارَكَه {name}",
|
||||
"status.reblogs.empty": "لم يقم أي أحد بمشاركة هذا المنشور بعد. عندما يقوم أحدهم بذلك سوف يظهر هنا.",
|
||||
"status.redraft": "إزالة و إعادة الصياغة",
|
||||
"status.redraft": "إزالة وإعادة الصياغة",
|
||||
"status.remove_bookmark": "احذفه مِن الفواصل المرجعية",
|
||||
"status.replied_to": "رَدًا على {name}",
|
||||
"status.reply": "ردّ",
|
||||
|
|
@ -653,16 +667,14 @@
|
|||
"status.show_more_all": "توسيع الكل",
|
||||
"status.show_original": "إظهار الأصل",
|
||||
"status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}",
|
||||
"status.translate": "ترجم",
|
||||
"status.translate": "ترجمة",
|
||||
"status.translated_from_with": "مترجم من {lang} باستخدام {provider}",
|
||||
"status.uncached_media_warning": "المعاينة غير متوفرة",
|
||||
"status.unmute_conversation": "فك الكتم عن المحادثة",
|
||||
"status.unpin": "فك التدبيس من الصفحة التعريفية",
|
||||
"subscribed_languages.lead": "فقط المشاركات في اللغات المحددة ستظهر في الرئيسيه وتسرد الجداول الزمنية بعد التغيير. حدد لا شيء لتلقي المشاركات بجميع اللغات.",
|
||||
"subscribed_languages.lead": "فقط المنشورات في اللغات المحددة ستظهر في خيطك الرئيسي وتسرد في الجداول الزمنية بعد تأكيد التغيير. لا تقم بأي خيار لتلقي المنشورات في جميع اللغات.",
|
||||
"subscribed_languages.save": "حفظ التغييرات",
|
||||
"subscribed_languages.target": "تغيير اللغات المشتركة لـ {target}",
|
||||
"suggestions.dismiss": "إلغاء الاقتراح",
|
||||
"suggestions.header": "يمكن أن يهمك…",
|
||||
"tabs_bar.home": "الرئيسية",
|
||||
"tabs_bar.notifications": "الإشعارات",
|
||||
"time_remaining.days": "{number, plural, one {# يوم} other {# أيام}} متبقية",
|
||||
|
|
@ -674,7 +686,7 @@
|
|||
"timeline_hint.resources.followers": "المتابِعون",
|
||||
"timeline_hint.resources.follows": "المتابَعون",
|
||||
"timeline_hint.resources.statuses": "المنشورات القديمة",
|
||||
"trends.counter_by_accounts": "{count, plural, one {{counter} شخص واحد} other {{counter} أشخاص}} في {days, plural, one {اليوم الماضي} other {{days} الأسام الماضية}}",
|
||||
"trends.counter_by_accounts": "{count, plural, one {شخص واحد} two {شخصان} few {{counter} أشخاصٍ} many {{counter} شخصًا} other {{counter} شخصًا}} {days, plural, one {خلال اليوم الماضي} two {خلال اليومَيْنِ الماضيَيْنِ} few {خلال {days} أيام الماضية} many {خلال {days} يومًا الماضية} other {خلال {days} يومٍ الماضية}}",
|
||||
"trends.trending_now": "المتداولة الآن",
|
||||
"ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.",
|
||||
"units.short.billion": "{count} مليار",
|
||||
|
|
|
|||
|
|
@ -218,6 +218,7 @@
|
|||
"home.column_settings.basic": "Configuración básica",
|
||||
"home.column_settings.show_reblogs": "Amosar los artículos compartíos",
|
||||
"home.column_settings.show_replies": "Amosar les rempuestes",
|
||||
"home.pending_critical_update.body": "¡Anueva'l sirvidor de Mastodon namás que puedas!",
|
||||
"interaction_modal.description.follow": "Con una cuenta de Mastodon, pues siguir a {name} pa recibir los artículos de so nel to feed d'aniciu.",
|
||||
"interaction_modal.description.reblog": "Con una cuenta de Mastodon, pues compartir esti artículu colos perfiles que te sigan.",
|
||||
"interaction_modal.description.reply": "Con una cuenta de Mastodon, pues responder a esti artículu.",
|
||||
|
|
@ -412,9 +413,7 @@
|
|||
"search_results.hashtags": "Etiquetes",
|
||||
"search_results.nothing_found": "Nun se pudo atopar nada con esos términos de busca",
|
||||
"search_results.statuses": "Artículos",
|
||||
"search_results.statuses_fts_disabled": "Esti sirvidor de Mastodon nun tien activada la busca d'artículos pol so conteníu.",
|
||||
"search_results.title": "Busca de: {q}",
|
||||
"search_results.total": "{count, number} {count, plural, one {resultáu} other {resultaos}}",
|
||||
"server_banner.introduction": "{domain} ye parte de la rede social descentralizada que tien la teunoloxía de {mastodon}.",
|
||||
"server_banner.learn_more": "Saber más",
|
||||
"server_banner.server_stats": "Estadístiques del sirvidor:",
|
||||
|
|
@ -465,7 +464,6 @@
|
|||
"status.uncached_media_warning": "La previsualización nun ta disponible",
|
||||
"status.unmute_conversation": "Activar los avisos de la conversación",
|
||||
"status.unpin": "Lliberar del perfil",
|
||||
"suggestions.header": "Quiciabes t'interese…",
|
||||
"tabs_bar.home": "Aniciu",
|
||||
"tabs_bar.notifications": "Avisos",
|
||||
"time_remaining.days": "{number, plural, one {Queda # día} other {Queden # díes}}",
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@
|
|||
"compose.language.search": "Шукаць мовы...",
|
||||
"compose.published.body": "Допіс апублікаваны.",
|
||||
"compose.published.open": "Адкрыць",
|
||||
"compose.saved.body": "Допіс захаваны.",
|
||||
"compose_form.direct_message_warning_learn_more": "Даведацца больш",
|
||||
"compose_form.encryption_warning": "Допісы ў Mastodon не абаронены скразным шыфраваннем. Не дзяліцеся ніякай канфідэнцыяльнай інфармацыяй праз Mastodon.",
|
||||
"compose_form.hashtag_warning": "Гэты допіс не будзе паказаны пад аніякім хэштэгам, бо ён не публічны. Толькі публічныя допісы можна знайсці па хэштэгу.",
|
||||
|
|
@ -300,6 +301,7 @@
|
|||
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} допіс} few {{counter} допісы} many {{counter} допісаў} other {{counter} допісу}} за сёння",
|
||||
"hashtag.follow": "Падпісацца на хэштэг",
|
||||
"hashtag.unfollow": "Адпісацца ад хэштэга",
|
||||
"hashtags.and_other": "…і яшчэ {count, plural, other {#}}",
|
||||
"home.actions.go_to_explore": "Паглядзіце, што ў трэндзе",
|
||||
"home.actions.go_to_suggestions": "Знайсці людзей, каб падпісацца",
|
||||
"home.column_settings.basic": "Асноўныя",
|
||||
|
|
@ -308,6 +310,9 @@
|
|||
"home.explore_prompt.body": "Ваша галоўная стужка змяшчае сумесь допісаў з хэштэгамі, за якімі вы вырашылі сачыць, допісаў ад людзей, за якімі вы вырашылі сачыць, і допісаў, якія яны пашыраюць. Зараз усё выглядае даволі ціха, так што як наконт:",
|
||||
"home.explore_prompt.title": "Гэта ваша апорная кропка ў Mastodon.",
|
||||
"home.hide_announcements": "Схаваць аб'явы",
|
||||
"home.pending_critical_update.body": "Калі ласка, абнавіце свой сервер Mastodon як мага хутчэй!",
|
||||
"home.pending_critical_update.link": "Прагледзець абнаўленні",
|
||||
"home.pending_critical_update.title": "Даступна крытычнае абнаўленне бяспекі!",
|
||||
"home.show_announcements": "Паказаць аб'явы",
|
||||
"interaction_modal.description.favourite": "Маючы ўліковы запіс Mastodon, вы можаце ўпадабаць гэты допіс, каб паведаміць аўтару, што ён вам падабаецца, і захаваць яго на будучыню.",
|
||||
"interaction_modal.description.follow": "Маючы акаўнт у Mastodon, вы можаце падпісацца на {name}, каб бачыць яго/яе допісы ў сваёй хатняй стужцы.",
|
||||
|
|
@ -409,6 +414,7 @@
|
|||
"navigation_bar.lists": "Спісы",
|
||||
"navigation_bar.logout": "Выйсці",
|
||||
"navigation_bar.mutes": "Ігнараваныя карыстальнікі",
|
||||
"navigation_bar.opened_in_classic_interface": "Допісы, уліковыя запісы і іншыя спецыфічныя старонкі па змоўчанні адчыняюцца ў класічным вэб-інтэрфейсе.",
|
||||
"navigation_bar.personal": "Асабістае",
|
||||
"navigation_bar.pins": "Замацаваныя допісы",
|
||||
"navigation_bar.preferences": "Параметры",
|
||||
|
|
@ -532,6 +538,7 @@
|
|||
"reply_indicator.cancel": "Скасаваць",
|
||||
"report.block": "Заблакіраваць",
|
||||
"report.block_explanation": "Вы перастанеце бачыць допісы гэтага карыстальніка. Ён не зможа сачыць за вамі і бачыць вашы допісы. Ён зможа зразумець, што яго заблакіравалі.",
|
||||
"report.categories.legal": "Права",
|
||||
"report.categories.other": "Іншае",
|
||||
"report.categories.spam": "Спам",
|
||||
"report.categories.violation": "Змест парушае адно ці некалькі правілаў сервера",
|
||||
|
|
@ -583,16 +590,20 @@
|
|||
"search.quick_action.open_url": "Адкрыць спасылку ў Mastodon",
|
||||
"search.quick_action.status_search": "Супадзенне паведамленняў {x}",
|
||||
"search.search_or_paste": "Пошук",
|
||||
"search_popout.full_text_search_disabled_message": "Недаступна на {domain}.",
|
||||
"search_popout.language_code": "ISO код мовы",
|
||||
"search_popout.options": "Параметры пошуку",
|
||||
"search_popout.quick_actions": "Хуткія дзеянні",
|
||||
"search_popout.recent": "Нядаўнія запыты",
|
||||
"search_popout.specific_date": "канкрэтная дата",
|
||||
"search_popout.user": "карыстальнік",
|
||||
"search_results.accounts": "Профілі",
|
||||
"search_results.all": "Усё",
|
||||
"search_results.hashtags": "Хэштэгі",
|
||||
"search_results.nothing_found": "Па дадзенаму запыту нічога не знойдзена",
|
||||
"search_results.see_all": "Праглядзець усе",
|
||||
"search_results.statuses": "Допісы",
|
||||
"search_results.statuses_fts_disabled": "Пошук публікацый па зместу не ўключаны на гэтым серверы Mastodon.",
|
||||
"search_results.title": "Пошук {q}",
|
||||
"search_results.total": "{count, number} {count, plural, one {вынік} few {вынікі} many {вынікаў} other {выніку}}",
|
||||
"server_banner.about_active_users": "Людзі, якія карыстаюцца гэтым сервера на працягу апошніх 30 дзён (Штомесячна Актыўныя Карыстальнікі)",
|
||||
"server_banner.active_users": "актыўныя карыстальнікі",
|
||||
"server_banner.administered_by": "Адміністратар:",
|
||||
|
|
@ -664,8 +675,6 @@
|
|||
"subscribed_languages.lead": "Толькі допісы ў абраных мовах будуць паказвацца ў вашых стужках пасля змены. Не абірайце нічога, каб бачыць допісы на ўсіх мовах.",
|
||||
"subscribed_languages.save": "Захаваць змены",
|
||||
"subscribed_languages.target": "Змяніць мовы падпіскі для {target}",
|
||||
"suggestions.dismiss": "Адхіліць прапанову",
|
||||
"suggestions.header": "Гэта можа Вас зацікавіць…",
|
||||
"tabs_bar.home": "Галоўная",
|
||||
"tabs_bar.notifications": "Апавяшчэнні",
|
||||
"time_remaining.days": "{number, plural, one {застаўся # дзень} few {засталося # дні} many {засталося # дзён} other {засталося # дня}}",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
"account.domain_blocked": "Блокиран домейн",
|
||||
"account.edit_profile": "Редактиране на профила",
|
||||
"account.enable_notifications": "Известяване при публикуване от @{name}",
|
||||
"account.endorse": "Характеристика на профила",
|
||||
"account.endorse": "Представи в профила",
|
||||
"account.featured_tags.last_status_at": "Последна публикация на {date}",
|
||||
"account.featured_tags.last_status_never": "Няма публикации",
|
||||
"account.featured_tags.title": "Главни хаштагове на {name}",
|
||||
|
|
@ -113,6 +113,7 @@
|
|||
"column.direct": "Частни споменавания",
|
||||
"column.directory": "Разглеждане на профили",
|
||||
"column.domain_blocks": "Блокирани домейни",
|
||||
"column.favourites": "Любими",
|
||||
"column.firehose": "Инфоканали на живо",
|
||||
"column.follow_requests": "Заявки за последване",
|
||||
"column.home": "Начало",
|
||||
|
|
@ -136,6 +137,7 @@
|
|||
"compose.language.search": "Търсене на езици...",
|
||||
"compose.published.body": "Публикувана публикация.",
|
||||
"compose.published.open": "Отваряне",
|
||||
"compose.saved.body": "Запазена публикация.",
|
||||
"compose_form.direct_message_warning_learn_more": "Още информация",
|
||||
"compose_form.encryption_warning": "Публикациите в Mastodon не са криптирани от край до край. Не споделяйте никаква чувствителна информация там.",
|
||||
"compose_form.hashtag_warning": "Тази публикация няма да се вписва под никакъв хаштаг, тъй като не е обществена. Само публични публикации могат да се търсят по хаштаг.",
|
||||
|
|
@ -180,6 +182,7 @@
|
|||
"confirmations.mute.explanation": "Това ще скрие публикациите от тях и публикации, които ги споменават, но все още ще им позволява да виждат публикациите ви и да ви следват.",
|
||||
"confirmations.mute.message": "Наистина ли искате да заглушите {name}?",
|
||||
"confirmations.redraft.confirm": "Изтриване и преработване",
|
||||
"confirmations.redraft.message": "Наистина ли искате да изтриете тази публикация и да я направите чернова? Означаванията като любими и подсилванията ще се изгубят, а и отговорите към първоначалната публикация ще осиротеят.",
|
||||
"confirmations.reply.confirm": "Отговор",
|
||||
"confirmations.reply.message": "Отговарянето сега ще замени съобщението, което в момента съставяте. Сигурни ли сте, че искате да продължите?",
|
||||
"confirmations.unfollow.confirm": "Без следване",
|
||||
|
|
@ -199,6 +202,7 @@
|
|||
"dismissable_banner.community_timeline": "Ето най-скорошните публични публикации от хора, чиито акаунти са разположени в {domain}.",
|
||||
"dismissable_banner.dismiss": "Отхвърляне",
|
||||
"dismissable_banner.explore_links": "Тези новини се разказват от хората в този и други сървъри на децентрализираната мрежа точно сега.",
|
||||
"dismissable_banner.explore_statuses": "Има публикации през социалната мрежа, които днес набират популярност. По-новите публикации с повече подсилвания и любими са класирани по-високо.",
|
||||
"dismissable_banner.explore_tags": "Тези хаштагове сега набират популярност сред хората в този и други сървъри на децентрализирата мрежа.",
|
||||
"dismissable_banner.public_timeline": "Ето най-новите обществени публикации от хора в социална мрежа, която хората в {domain} следват.",
|
||||
"embed.instructions": "Вградете публикацията в уебсайта си, копирайки кода долу.",
|
||||
|
|
@ -227,6 +231,8 @@
|
|||
"empty_column.direct": "Още нямате никакви частни споменавания. Тук ще се показват, изпращайки или получавайки едно.",
|
||||
"empty_column.domain_blocks": "Още няма блокирани домейни.",
|
||||
"empty_column.explore_statuses": "Няма нищо налагащо се в момента. Проверете пак по-късно!",
|
||||
"empty_column.favourited_statuses": "Още нямате никакви любими публикации. Правейки любима, то тя ще се покаже тук.",
|
||||
"empty_column.favourites": "Още никого не е слагал публикацията в любими. Когато някой го направи, този човек ще се покаже тук.",
|
||||
"empty_column.follow_requests": "Още нямате заявки за последване. Получавайки такава, то тя ще се покаже тук.",
|
||||
"empty_column.followed_tags": "Още не сте последвали никакви хаштагове. Последваните хаштагове ще се покажат тук.",
|
||||
"empty_column.hashtag": "Още няма нищо в този хаштаг.",
|
||||
|
|
@ -290,21 +296,36 @@
|
|||
"hashtag.column_settings.tag_mode.any": "Някое от тези",
|
||||
"hashtag.column_settings.tag_mode.none": "Никое от тези",
|
||||
"hashtag.column_settings.tag_toggle": "Включва допълнителни хаштагове за тази колона",
|
||||
"hashtag.counter_by_accounts": "{count, plural, one {{counter} участник} other {{counter} участници}}",
|
||||
"hashtag.counter_by_uses": "{count, plural, one {{counter} публикация} other {{counter} публикации}}",
|
||||
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} публикация} other {{counter} публикации}} днес",
|
||||
"hashtag.follow": "Следване на хаштаг",
|
||||
"hashtag.unfollow": "Спиране на следване на хаштаг",
|
||||
"hashtags.and_other": "…и {count, plural, other {# още}}",
|
||||
"home.actions.go_to_explore": "Вижте какво изгрява",
|
||||
"home.actions.go_to_suggestions": "Намиране на хора за следване",
|
||||
"home.column_settings.basic": "Основно",
|
||||
"home.column_settings.show_reblogs": "Показване на подсилванията",
|
||||
"home.column_settings.show_replies": "Показване на отговорите",
|
||||
"home.explore_prompt.body": "Вашият начален инфоканал ще е смес на публикации от хаштаговете, които сте избрали да следвате, избраните хора да следвате, а и публикациите, които са подсилили. Ако изглежда твърде тихо в момента, то може да искате да:",
|
||||
"home.explore_prompt.title": "Това е началната ви база с Mastodon.",
|
||||
"home.hide_announcements": "Скриване на оповестяванията",
|
||||
"home.pending_critical_update.body": "Обновете сървъра си в Mastodon възможно най-скоро!",
|
||||
"home.pending_critical_update.link": "Преглед на обновяванията",
|
||||
"home.pending_critical_update.title": "Налично критично обновяване на сигурността!",
|
||||
"home.show_announcements": "Показване на оповестяванията",
|
||||
"interaction_modal.description.favourite": "Имайки акаунт в Mastodon, може да сложите тази публикации в любими, за да позволите на автора да узнае, че я цените и да я запазите за по-късно.",
|
||||
"interaction_modal.description.follow": "С акаунт в Mastodon може да последвате {name}, за да получавате публикациите от този акаунт в началния си инфоканал.",
|
||||
"interaction_modal.description.reblog": "С акаунт в Mastodon може да подсилите тази публикация, за да я споделите с последователите си.",
|
||||
"interaction_modal.description.reply": "С акаунт в Mastodon може да добавите отговор към тази публикация.",
|
||||
"interaction_modal.login.action": "Към началото",
|
||||
"interaction_modal.login.prompt": "Домейнът на сървъра ви, примерно, mastodon.social",
|
||||
"interaction_modal.no_account_yet": "Още не е в Мастодон?",
|
||||
"interaction_modal.on_another_server": "На различен сървър",
|
||||
"interaction_modal.on_this_server": "На този сървър",
|
||||
"interaction_modal.sign_in": "Не сте влезли в този сървър. Къде се хоства акаунтът ви?",
|
||||
"interaction_modal.sign_in_hint": "Съвет: Ето уебсайта, където сте се регистрирали. Ако не помните, то погледнете е-писмо за добре дошли във входящата си поща. Може също да въведете пълното си потребителско име! (примерно: @Mastodon@mastodon.social)",
|
||||
"interaction_modal.title.favourite": "Означавам публикация на {name} като любима",
|
||||
"interaction_modal.title.follow": "Последване на {name}",
|
||||
"interaction_modal.title.reblog": "Подсилване на публикацията на {name}",
|
||||
"interaction_modal.title.reply": "Отговаряне на публикацията на {name}",
|
||||
|
|
@ -320,6 +341,8 @@
|
|||
"keyboard_shortcuts.direct": "за отваряне на колоната с частни споменавания",
|
||||
"keyboard_shortcuts.down": "Преместване надолу в списъка",
|
||||
"keyboard_shortcuts.enter": "Отваряне на публикация",
|
||||
"keyboard_shortcuts.favourite": "Любима публикация",
|
||||
"keyboard_shortcuts.favourites": "Отваряне на списъка с любими",
|
||||
"keyboard_shortcuts.federated": "Отваряне на федерирания инфопоток",
|
||||
"keyboard_shortcuts.heading": "Клавишни съчетания",
|
||||
"keyboard_shortcuts.home": "Отваряне на началната часова ос",
|
||||
|
|
@ -350,6 +373,7 @@
|
|||
"lightbox.previous": "Назад",
|
||||
"limited_account_hint.action": "Показване на профила въпреки това",
|
||||
"limited_account_hint.title": "Този профил е бил скрит от модераторите на {domain}.",
|
||||
"link_preview.author": "От {name}",
|
||||
"lists.account.add": "Добавяне към списък",
|
||||
"lists.account.remove": "Премахване от списъка",
|
||||
"lists.delete": "Изтриване на списъка",
|
||||
|
|
@ -369,7 +393,7 @@
|
|||
"media_gallery.toggle_visible": "Скриване на {number, plural, one {изображение} other {изображения}}",
|
||||
"moved_to_account_banner.text": "Вашият акаунт {disabledAccount} сега е изключен, защото се преместихте в {movedToAccount}.",
|
||||
"mute_modal.duration": "Времетраене",
|
||||
"mute_modal.hide_notifications": "Скривате ли известията от потребителя?",
|
||||
"mute_modal.hide_notifications": "Скриване на известия от този потребител?",
|
||||
"mute_modal.indefinite": "Неопределено",
|
||||
"navigation_bar.about": "Относно",
|
||||
"navigation_bar.advanced_interface": "Отваряне в разширен уебинтерфейс",
|
||||
|
|
@ -382,6 +406,7 @@
|
|||
"navigation_bar.domain_blocks": "Блокирани домейни",
|
||||
"navigation_bar.edit_profile": "Редактиране на профила",
|
||||
"navigation_bar.explore": "Изследване",
|
||||
"navigation_bar.favourites": "Любими",
|
||||
"navigation_bar.filters": "Заглушени думи",
|
||||
"navigation_bar.follow_requests": "Заявки за последване",
|
||||
"navigation_bar.followed_tags": "Последвани хаштагове",
|
||||
|
|
@ -389,6 +414,7 @@
|
|||
"navigation_bar.lists": "Списъци",
|
||||
"navigation_bar.logout": "Излизане",
|
||||
"navigation_bar.mutes": "Заглушени потребители",
|
||||
"navigation_bar.opened_in_classic_interface": "Публикации, акаунти и други особени страници се отварят по подразбиране в класическия мрежови интерфейс.",
|
||||
"navigation_bar.personal": "Лично",
|
||||
"navigation_bar.pins": "Закачени публикации",
|
||||
"navigation_bar.preferences": "Предпочитания",
|
||||
|
|
@ -398,6 +424,7 @@
|
|||
"not_signed_in_indicator.not_signed_in": "Трябва ви вход за достъп до ресурса.",
|
||||
"notification.admin.report": "{name} докладва {target}",
|
||||
"notification.admin.sign_up": "{name} се регистрира",
|
||||
"notification.favourite": "{name} направи любима публикацията ви",
|
||||
"notification.follow": "{name} ви последва",
|
||||
"notification.follow_request": "{name} поиска да ви последва",
|
||||
"notification.mention": "{name} ви спомена",
|
||||
|
|
@ -411,6 +438,7 @@
|
|||
"notifications.column_settings.admin.report": "Нови доклади:",
|
||||
"notifications.column_settings.admin.sign_up": "Нови регистрации:",
|
||||
"notifications.column_settings.alert": "Известия на работния плот",
|
||||
"notifications.column_settings.favourite": "Любими:",
|
||||
"notifications.column_settings.filter_bar.advanced": "Показване на всички категории",
|
||||
"notifications.column_settings.filter_bar.category": "Лента за бърз филтър",
|
||||
"notifications.column_settings.filter_bar.show_bar": "Показване на лентата с филтри",
|
||||
|
|
@ -428,6 +456,7 @@
|
|||
"notifications.column_settings.update": "Промени:",
|
||||
"notifications.filter.all": "Всичко",
|
||||
"notifications.filter.boosts": "Подсилвания",
|
||||
"notifications.filter.favourites": "Любими",
|
||||
"notifications.filter.follows": "Последвания",
|
||||
"notifications.filter.mentions": "Споменавания",
|
||||
"notifications.filter.polls": "Резултати от анкетата",
|
||||
|
|
@ -509,6 +538,7 @@
|
|||
"reply_indicator.cancel": "Отказ",
|
||||
"report.block": "Блокиране",
|
||||
"report.block_explanation": "Няма да им виждате публикациите. Те няма да могат да виждат публикациите ви или да ви последват. Те ще могат да казват, че са били блокирани.",
|
||||
"report.categories.legal": "Правни въпроси",
|
||||
"report.categories.other": "Друго",
|
||||
"report.categories.spam": "Спам",
|
||||
"report.categories.violation": "Съдържание, нарушаващо едно или повече правила на сървъра",
|
||||
|
|
@ -560,16 +590,20 @@
|
|||
"search.quick_action.open_url": "Отваряне на URL адреса в Mastodon",
|
||||
"search.quick_action.status_search": "Съвпадение на публикации {x}",
|
||||
"search.search_or_paste": "Търсене или поставяне на URL адрес",
|
||||
"search_popout.full_text_search_disabled_message": "Не е достъпно на {domain}.",
|
||||
"search_popout.language_code": "Код на езика по ISO",
|
||||
"search_popout.options": "Възможности при търсене",
|
||||
"search_popout.quick_actions": "Бързи действия",
|
||||
"search_popout.recent": "Скорошни търсения",
|
||||
"search_popout.specific_date": "особена дата",
|
||||
"search_popout.user": "потребител",
|
||||
"search_results.accounts": "Профили",
|
||||
"search_results.all": "Всичко",
|
||||
"search_results.hashtags": "Хаштагове",
|
||||
"search_results.nothing_found": "Не може да се намери каквото и да било за тези термини при търсене",
|
||||
"search_results.see_all": "Поглед на всички",
|
||||
"search_results.statuses": "Публикации",
|
||||
"search_results.statuses_fts_disabled": "Търсенето на публикации по съдържанието им не се включва в този сървър на Mastodon.",
|
||||
"search_results.title": "Търсене за {q}",
|
||||
"search_results.total": "{count, number} {count, plural, one {резултат} other {резултата}}",
|
||||
"server_banner.about_active_users": "Ползващите сървъра през последните 30 дни (дейните месечно потребители)",
|
||||
"server_banner.active_users": "дейни потребители",
|
||||
"server_banner.administered_by": "Администрира се от:",
|
||||
|
|
@ -578,6 +612,8 @@
|
|||
"server_banner.server_stats": "Статистика на сървъра:",
|
||||
"sign_in_banner.create_account": "Създаване на акаунт",
|
||||
"sign_in_banner.sign_in": "Вход",
|
||||
"sign_in_banner.sso_redirect": "Влизане или регистриране",
|
||||
"sign_in_banner.text": "Влезте, за да последвате профили или хаштагове, отбелязвате като любими, споделяте и отговаряте на публикации. Може също така да взаимодействате от акаунта си на друг сървър.",
|
||||
"status.admin_account": "Отваряне на интерфейс за модериране за @{name}",
|
||||
"status.admin_domain": "Отваряне на модериращия интерфейс за {domain}",
|
||||
"status.admin_status": "Отваряне на публикацията в модериращия интерфейс",
|
||||
|
|
@ -594,6 +630,7 @@
|
|||
"status.edited": "Редактирано на {date}",
|
||||
"status.edited_x_times": "Редактирано {count, plural,one {{count} път} other {{count} пъти}}",
|
||||
"status.embed": "Вграждане",
|
||||
"status.favourite": "Любимо",
|
||||
"status.filter": "Филтриране на публ.",
|
||||
"status.filtered": "Филтрирано",
|
||||
"status.hide": "Скриване на публ.",
|
||||
|
|
@ -638,8 +675,6 @@
|
|||
"subscribed_languages.lead": "Публикации само на избрани езици ще се явяват в началото ви и в списъка с часови оси след промяната. Изберете \"нищо\", за да получавате публикации на всички езици.",
|
||||
"subscribed_languages.save": "Запазване на промените",
|
||||
"subscribed_languages.target": "Смяна на езика за {target}",
|
||||
"suggestions.dismiss": "Отхвърляне на предложение",
|
||||
"suggestions.header": "Може да имате интерес от…",
|
||||
"tabs_bar.home": "Начало",
|
||||
"tabs_bar.notifications": "Известия",
|
||||
"time_remaining.days": "{number, plural, one {остава # ден} other {остават # дни}}",
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@
|
|||
"about.disclaimer": "ম্যাস্টোডন একটি ফ্রি, ওপেন সোর্স সফটওয়্যার এবং ম্যাস্টোডন জিজিএমবিএইচ এর একটি ট্রেডমার্ক।",
|
||||
"about.domain_blocks.no_reason_available": "কারণ দর্শানো যাচ্ছে না",
|
||||
"about.domain_blocks.preamble": "ম্যাস্টোডন সাধারণত আপনাকে ফেদিভার্স এ অন্য কোনও সার্ভারের ব্যবহারকারীদের থেকে সামগ্রী দেখতে এবং তাদের সাথে আলাপচারিতা করার সুযোগ দেয়। এই ব্যতিক্রম যে এই বিশেষ সার্ভারে তৈরি করা হয়েছে।",
|
||||
"about.domain_blocks.silenced.explanation": "আপনি সাধারণত এই সার্ভার থেকে প্রোফাইল এবং বিষয়বস্তু দেখতে পারবেন না, যদি না আপনি স্পষ্টভাবে এটি দেখেন বা অনুসরণ করে এটি নির্বাচন করেন৷",
|
||||
"about.domain_blocks.silenced.explanation": "আপনি সাধারণত এই সার্ভার থেকে প্রোফাইল এবং বিষয়বস্তু দেখতে পারবেন না, যদি না আপনি নিজে থেকেই এটাকে ফলো না করেন.",
|
||||
"about.domain_blocks.silenced.title": "সীমিত",
|
||||
"about.domain_blocks.suspended.explanation": "এই সার্ভার থেকে কোনও ডেটা প্রক্রিয়াজাতকরণ, সংরক্ষণ বা আদান-প্রদান করা হবে না, তাই এই সার্ভার ব্যবহারকারীদের সাথে কোনও মিথস্ক্রিয়া বা যোগাযোগকে অসম্ভব করে তুলেছে।",
|
||||
"about.domain_blocks.suspended.title": "সাসপেন্ড করা হয়েছে",
|
||||
"about.not_available": "এই তথ্য এই সার্ভারে উপলব্ধ করা হয়নি।",
|
||||
"about.not_available": "এই তথ্য এই সার্ভারে উন্মুক্ত করা হয়নি.",
|
||||
"about.powered_by": "{mastodon} দ্বারা তৈরি বিকেন্দ্রীভূত সামাজিক মিডিয়া।",
|
||||
"about.rules": "সার্ভারের নিয়মাবলী",
|
||||
"account.account_note_header": "বিজ্ঞপ্তি",
|
||||
|
|
@ -16,45 +16,45 @@
|
|||
"account.badges.bot": "বট",
|
||||
"account.badges.group": "দল",
|
||||
"account.block": "@{name} কে ব্লক করো",
|
||||
"account.block_domain": "{domain} থেকে সব লুকাও",
|
||||
"account.block_short": "অবরোধ",
|
||||
"account.block_domain": "{domain} কে ব্লক করুন",
|
||||
"account.block_short": "ব্লক",
|
||||
"account.blocked": "অবরুদ্ধ",
|
||||
"account.browse_more_on_origin_server": "মূল প্রোফাইলটিতে আরও ব্রাউজ করুন",
|
||||
"account.cancel_follow_request": "অনুসরণ অনুরোধ প্রত্যাহার করুন",
|
||||
"account.direct": "গোপনে মেনশন করুন @{name}",
|
||||
"account.disable_notifications": "আমাকে জানানো বন্ধ করো যখন @{name} পোস্ট করবে",
|
||||
"account.domain_blocked": "ডোমেন গোপন করুন",
|
||||
"account.edit_profile": "প্রোফাইল পরিবর্তন করুন",
|
||||
"account.domain_blocked": "ডোমেইন ব্লক করা",
|
||||
"account.edit_profile": "প্রোফাইল সম্পাদনা করুন",
|
||||
"account.enable_notifications": "আমাকে জানাবে যখন @{name} পোস্ট করবে",
|
||||
"account.endorse": "নিজের পাতায় দেখান",
|
||||
"account.endorse": "প্রোফাইলে ফিচার করুন",
|
||||
"account.featured_tags.last_status_at": "{date} এ সর্বশেষ পোস্ট",
|
||||
"account.featured_tags.last_status_never": "কোনো পোস্ট নেই",
|
||||
"account.featured_tags.title": "{name}-এর বৈশিষ্ট্যযুক্ত হ্যাশট্যাগগুলি৷",
|
||||
"account.featured_tags.title": "{name} এর ফিচার করা Hashtag সমূহ",
|
||||
"account.follow": "অনুসরণ",
|
||||
"account.followers": "অনুসরণকারী",
|
||||
"account.followers.empty": "এই ব্যক্তিকে এখনো কেউ অনুসরণ করে না।",
|
||||
"account.followers.empty": "এই ব্যক্তিকে এখনো কেউ অনুসরণ করে না.",
|
||||
"account.followers_counter": "{count, plural,one {{counter} জন অনুসরণকারী } other {{counter} জন অনুসরণকারী}}",
|
||||
"account.following": "অনুসরণ করা হচ্ছে",
|
||||
"account.following_counter": "{count, plural,one {{counter} জনকে অনুসরণ} other {{counter} জনকে অনুসরণ}}",
|
||||
"account.follows.empty": "এই সদস্য কাওকে এখনো অনুসরণ করেন না.",
|
||||
"account.follows_you": "তোমাকে অনুসরণ করে",
|
||||
"account.follows.empty": "এই সদস্য কাউকে এখনো ফলো করেন না.",
|
||||
"account.follows_you": "আপনাকে ফলো করে",
|
||||
"account.go_to_profile": "প্রোফাইলে যান",
|
||||
"account.hide_reblogs": "@{name}'র সমর্থনগুলি লুকিয়ে ফেলুন",
|
||||
"account.in_memoriam": "স্মৃতিসৌধে।",
|
||||
"account.in_memoriam": "স্মৃতিতে.",
|
||||
"account.joined_short": "যোগ দিয়েছেন",
|
||||
"account.languages": "সাবস্ক্রাইব করা ভাষা পরিবর্তন করুন",
|
||||
"account.link_verified_on": "এই লিংকের মালিকানা চেক করা হয়েছে {date} তারিখে",
|
||||
"account.locked_info": "এই নিবন্ধনের গোপনীয়তার ক্ষেত্র তালা দেওয়া আছে। নিবন্ধনকারী অনুসরণ করার অনুমতি যাদেরকে দেবেন, শুধু তারাই অনুসরণ করতে পারবেন।",
|
||||
"account.locked_info": "এই একাউন্ট লক করা। উনি যাদেরকে ফলো করার অনুমতি যাদেরকে দেবেন, শুধু তারাই ফলো করতে পারবেন.",
|
||||
"account.media": "মিডিয়া",
|
||||
"account.mention": "@{name} কে উল্লেখ করুন",
|
||||
"account.mention": "@{name} কে মেনশন করুন",
|
||||
"account.moved_to": "{name} নির্দেশ করেছে যে তাদের নতুন অ্যাকাউন্ট এখন হলো:",
|
||||
"account.mute": "@{name} কে নিঃশব্দ করুন",
|
||||
"account.mute_notifications_short": "বিজ্ঞপ্তি নিংশব্দ",
|
||||
"account.mute_short": "নিঃশব্দ",
|
||||
"account.muted": "নিঃশব্দ",
|
||||
"account.no_bio": "কোনো বর্ণনা দেওয়া হয়নি।",
|
||||
"account.mute_notifications_short": "নোটিফিকেশন মিউট করুন",
|
||||
"account.mute_short": "মিউট করুন",
|
||||
"account.muted": "মিউট করা",
|
||||
"account.no_bio": "কোনো বর্ণনা দেওয়া হয়নি.",
|
||||
"account.open_original_page": "মূল পৃষ্ঠা খুলুন",
|
||||
"account.posts": "টুট",
|
||||
"account.posts": "পোষ্টসমূহ",
|
||||
"account.posts_with_replies": "টুট এবং মতামত",
|
||||
"account.report": "@{name} কে রিপোর্ট করুন",
|
||||
"account.requested": "অনুমতির অপেক্ষা। অনুসরণ করার অনুরোধ বাতিল করতে এখানে ক্লিক করুন",
|
||||
|
|
@ -76,6 +76,9 @@
|
|||
"admin.dashboard.retention.average": "গড়",
|
||||
"admin.dashboard.retention.cohort": "সাইন আপের মাস",
|
||||
"admin.dashboard.retention.cohort_size": "নতুন ব্যবহারকারী",
|
||||
"admin.impact_report.instance_accounts": "যেসব একাউন্ট এর প্রোফাইল এটি ডিলিট করবে",
|
||||
"admin.impact_report.instance_followers": "যেসব ফলোয়ারদের আমাদের ইউজাররা হারাবে",
|
||||
"admin.impact_report.instance_follows": "যেসব ফলোয়ারদের তাদের ইউজার হারাবে",
|
||||
"alert.rate_limited.message": "{retry_time, time, medium} -এর পরে আবার প্রচেষ্টা করুন।",
|
||||
"alert.rate_limited.title": "হার সীমিত",
|
||||
"alert.unexpected.message": "সমস্যা অপ্রত্যাশিত.",
|
||||
|
|
@ -109,6 +112,8 @@
|
|||
"column.direct": "গোপনে মেনশন করুন",
|
||||
"column.directory": "প্রোফাইল ব্রাউজ করুন",
|
||||
"column.domain_blocks": "লুকোনো ডোমেনগুলি",
|
||||
"column.favourites": "পছন্দসমূহ",
|
||||
"column.firehose": "সরাসরি প্রবাহ",
|
||||
"column.follow_requests": "অনুসরণের অনুমতি অনুরোধকারী",
|
||||
"column.home": "বাড়ি",
|
||||
"column.lists": "তালিকাগুলো",
|
||||
|
|
@ -129,6 +134,9 @@
|
|||
"community.column_settings.remote_only": "শুধুমাত্র দূরবর্তী",
|
||||
"compose.language.change": "ভাষা পরিবর্তন করুন",
|
||||
"compose.language.search": "ভাষা অনুসন্ধান করুন...",
|
||||
"compose.published.body": "পোষ্ট publish করা হয়েছে.",
|
||||
"compose.published.open": "দেখো",
|
||||
"compose.saved.body": "পোস্ট সংরক্ষণ করা হয়েছে.",
|
||||
"compose_form.direct_message_warning_learn_more": "আরো জানুন",
|
||||
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.",
|
||||
"compose_form.hashtag_warning": "এই পোস্টটি কোনো হ্যাশট্যাগের বিষয় নয় কারণ এটি সর্বজনীনভাবে উপলব্ধ নয়। শুধুমাত্র জনসাধারণের কাছে পোস্ট করা বার্তাই হ্যাশট্যাগ দ্বারা অনুসন্ধান করা যেতে পারে।",
|
||||
|
|
@ -161,8 +169,12 @@
|
|||
"confirmations.delete.message": "আপনি কি নিশ্চিত যে এই লেখাটি মুছে ফেলতে চান ?",
|
||||
"confirmations.delete_list.confirm": "মুছে ফেলুন",
|
||||
"confirmations.delete_list.message": "আপনি কি নিশ্চিত যে আপনি এই তালিকাটি স্থায়িভাবে মুছে ফেলতে চান ?",
|
||||
"confirmations.discard_edit_media.confirm": "বাতিল করো",
|
||||
"confirmations.discard_edit_media.message": "মিডিয়া Description বা Preview তে আপনার আপনার অসংরক্ষিত পরিবর্তন আছে, সেগুলো বাতিল করবেন?",
|
||||
"confirmations.domain_block.confirm": "এই ডোমেন থেকে সব লুকান",
|
||||
"confirmations.domain_block.message": "আপনি কি সত্যিই সত্যই নিশ্চিত যে আপনি পুরো {domain}'টি ব্লক করতে চান? বেশিরভাগ ক্ষেত্রে কয়েকটি লক্ষ্যযুক্ত ব্লক বা নীরবতা যথেষ্ট এবং পছন্দসই। আপনি কোনও পাবলিক টাইমলাইন বা আপনার বিজ্ঞপ্তিগুলিতে সেই ডোমেন থেকে সামগ্রী দেখতে পাবেন না। সেই ডোমেন থেকে আপনার অনুসরণকারীদের সরানো হবে।",
|
||||
"confirmations.edit.confirm": "সম্পাদন",
|
||||
"confirmations.edit.message": "এখন সম্পাদনা করলে আপনি যে মেসেজ লিখছেন তা overwrite করবে, চালিয়ে যেতে চান?",
|
||||
"confirmations.logout.confirm": "প্রস্থান",
|
||||
"confirmations.logout.message": "আপনি লগ আউট করতে চান?",
|
||||
"confirmations.mute.confirm": "সরিয়ে ফেলুন",
|
||||
|
|
@ -177,15 +189,21 @@
|
|||
"conversation.mark_as_read": "পঠিত হিসেবে চিহ্নিত করুন",
|
||||
"conversation.open": "কথপোকথন দেখান",
|
||||
"conversation.with": "{names} এর সঙ্গে",
|
||||
"copypaste.copied": "অনুলিপিকৃত",
|
||||
"copypaste.copy_to_clipboard": "ক্লিপবোর্ডে কপি করুন",
|
||||
"directory.federated": "পরিচিত ফেডিভারসের থেকে",
|
||||
"directory.local": "শুধু {domain} থেকে",
|
||||
"directory.new_arrivals": "নতুন আগত",
|
||||
"directory.recently_active": "সম্প্রতি সক্রিয়",
|
||||
"disabled_account_banner.account_settings": "একাউন্ট সেটিংস",
|
||||
"disabled_account_banner.text": "আপনার একাউন্ট {disabledAccount} বর্তমানে বন্ধ করা.",
|
||||
"dismissable_banner.dismiss": "সরাও",
|
||||
"dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
|
||||
"dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
|
||||
"embed.instructions": "এই লেখাটি আপনার ওয়েবসাইটে যুক্ত করতে নিচের কোডটি বেবহার করুন।",
|
||||
"embed.preview": "সেটা দেখতে এরকম হবে:",
|
||||
"emoji_button.activity": "কার্যকলাপ",
|
||||
"emoji_button.clear": "পরিষ্কার",
|
||||
"emoji_button.custom": "প্রথা",
|
||||
"emoji_button.flags": "পতাকা",
|
||||
"emoji_button.food": "খাদ্য ও পানীয়",
|
||||
|
|
@ -217,10 +235,20 @@
|
|||
"error.unexpected_crash.next_steps": "পাতাটি রিফ্রেশ করে চেষ্টা করুন। তবুও যদি না হয়, তবে আপনি অন্য একটি ব্রাউজার অথবা আপনার ডিভাইসের জন্যে এপের মাধ্যমে মাস্টডন ব্যাবহার করতে পারবেন।.",
|
||||
"errors.unexpected_crash.copy_stacktrace": "স্টেকট্রেস ক্লিপবোর্ডে কপি করুন",
|
||||
"errors.unexpected_crash.report_issue": "সমস্যার প্রতিবেদন করুন",
|
||||
"explore.suggested_follows": "মানুষ",
|
||||
"explore.title": "পরিব্রাজন",
|
||||
"explore.trending_links": "সংবাদ",
|
||||
"explore.trending_statuses": "পোস্ট",
|
||||
"explore.trending_tags": "হ্যাশট্যাগ",
|
||||
"filter_modal.select_filter.expired": "মেয়াদোত্তীর্ণ",
|
||||
"firehose.all": "সব",
|
||||
"firehose.local": "এই সার্ভার",
|
||||
"follow_request.authorize": "অনুমতি দিন",
|
||||
"follow_request.reject": "প্রত্যাখ্যান করুন",
|
||||
"follow_requests.unlocked_explanation": "আপনার অ্যাকাউন্টটি লক না থাকলেও, {domain} কর্মীরা ভেবেছিলেন যে আপনি এই অ্যাকাউন্টগুলি থেকে ম্যানুয়ালি অনুসরণের অনুরোধগুলি পর্যালোচনা করতে চাইতে পারেন।",
|
||||
"footer.about": "পরিচিতি",
|
||||
"footer.get_app": "অ্যাপ নামাও",
|
||||
"footer.status": "অবস্থা",
|
||||
"generic.saved": "সংরক্ষণ হয়েছে",
|
||||
"getting_started.heading": "শুরু করা",
|
||||
"hashtag.column_header.tag_mode.all": "এবং {additional}",
|
||||
|
|
@ -274,6 +302,7 @@
|
|||
"lightbox.close": "বন্ধ",
|
||||
"lightbox.next": "পরবর্তী",
|
||||
"lightbox.previous": "পূর্ববর্তী",
|
||||
"link_preview.author": "{name} এর লিখা",
|
||||
"lists.account.add": "তালিকাতে যুক্ত করতে",
|
||||
"lists.account.remove": "তালিকা থেকে বাদ দিতে",
|
||||
"lists.delete": "তালিকা মুছে ফেলতে",
|
||||
|
|
@ -289,6 +318,8 @@
|
|||
"media_gallery.toggle_visible": "দৃশ্যতার অবস্থা বদলান",
|
||||
"mute_modal.duration": "সময়কাল",
|
||||
"mute_modal.hide_notifications": "এই ব্যবহারকারীর প্রজ্ঞাপন বন্ধ করবেন ?",
|
||||
"mute_modal.indefinite": "অনির্দিষ্ট",
|
||||
"navigation_bar.about": "পরিচিতি",
|
||||
"navigation_bar.blocks": "বন্ধ করা ব্যবহারকারী",
|
||||
"navigation_bar.bookmarks": "বুকমার্ক",
|
||||
"navigation_bar.community_timeline": "স্থানীয় সময়রেখা",
|
||||
|
|
@ -296,6 +327,8 @@
|
|||
"navigation_bar.discover": "ঘুরে দেখুন",
|
||||
"navigation_bar.domain_blocks": "লুকানো ডোমেনগুলি",
|
||||
"navigation_bar.edit_profile": "নিজের পাতা সম্পাদনা করতে",
|
||||
"navigation_bar.explore": "পরিব্রাজন",
|
||||
"navigation_bar.favourites": "পছন্দসমূহ",
|
||||
"navigation_bar.filters": "বন্ধ করা শব্দ",
|
||||
"navigation_bar.follow_requests": "অনুসরণের অনুরোধগুলি",
|
||||
"navigation_bar.follows_and_followers": "অনুসরণ এবং অনুসরণকারী",
|
||||
|
|
@ -306,6 +339,7 @@
|
|||
"navigation_bar.pins": "পিন দেওয়া টুট",
|
||||
"navigation_bar.preferences": "পছন্দসমূহ",
|
||||
"navigation_bar.public_timeline": "যুক্তবিশ্বের সময়রেখা",
|
||||
"navigation_bar.search": "অনুসন্ধান",
|
||||
"navigation_bar.security": "নিরাপত্তা",
|
||||
"not_signed_in_indicator.not_signed_in": "You need to sign in to access this resource.",
|
||||
"notification.follow": "{name} আপনাকে অনুসরণ করেছেন",
|
||||
|
|
@ -317,6 +351,7 @@
|
|||
"notifications.clear": "প্রজ্ঞাপনগুলো মুছে ফেলতে",
|
||||
"notifications.clear_confirmation": "আপনি কি নির্চিত প্রজ্ঞাপনগুলো মুছে ফেলতে চান ?",
|
||||
"notifications.column_settings.alert": "কম্পিউটারে প্রজ্ঞাপনগুলি",
|
||||
"notifications.column_settings.favourite": "পছন্দসমূহ:",
|
||||
"notifications.column_settings.filter_bar.advanced": "সব শ্রেণীগুলো দেখানো",
|
||||
"notifications.column_settings.filter_bar.category": "সংক্ষিপ্ত ছাঁকনি অংশ",
|
||||
"notifications.column_settings.follow": "নতুন অনুসরণকারীরা:",
|
||||
|
|
@ -328,8 +363,10 @@
|
|||
"notifications.column_settings.show": "কলামে দেখানো",
|
||||
"notifications.column_settings.sound": "শব্দ বাজানো",
|
||||
"notifications.column_settings.status": "New toots:",
|
||||
"notifications.column_settings.update": "সম্পাদনা:",
|
||||
"notifications.filter.all": "সব",
|
||||
"notifications.filter.boosts": "সমর্থনগুলো",
|
||||
"notifications.filter.favourites": "পছন্দসমূহ",
|
||||
"notifications.filter.follows": "অনুসরণের",
|
||||
"notifications.filter.mentions": "উল্লেখিত",
|
||||
"notifications.filter.polls": "নির্বাচনের ফলাফল",
|
||||
|
|
@ -348,8 +385,11 @@
|
|||
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
|
||||
"onboarding.steps.share_profile.title": "Share your profile",
|
||||
"onboarding.tips.accounts_from_other_servers": "<strong>তুমি কি জানতে?</strong> যেহেতু মাস্টোডন বিকেন্দ্রীভূত, কিছু অ্যাকাউন্ট তোমার নিজের ছাড়া অন্য কোনো সার্ভারে থাকতে পারে। অথচ তুমি তাদের সাথে কোনো সমস্যা ছাড়াই কথা বলতে পারছো! তাদের সার্ভার তাদের ব্যবহারকারী নামের দ্বিতীয় অর্ধাংশ!",
|
||||
"onboarding.tips.migration": "<strong>তুমি কি জানো?</strong> {domain} তোমার পছন্দ না হলে, ভবিষ্যতে তুমি অন্য কোনো সার্ভারে যেতে পারো তোমার অনুসরণকারীদেরকে না হারিয়েই। এমনকি তুমি নিজের সার্ভারও তৈরি করতে পারো!",
|
||||
"picture_in_picture.restore": "ফিরত রাখো",
|
||||
"poll.closed": "বন্ধ",
|
||||
"poll.refresh": "বদলেছে কিনা দেখতে",
|
||||
"poll.reveal": "ফলাফল দেখো",
|
||||
"poll.total_people": "{count, plural, one {# ব্যক্তি} other {# ব্যক্তি}}",
|
||||
"poll.total_votes": "{count, plural, one {# ভোট} other {# ভোট}}",
|
||||
"poll.vote": "ভোট",
|
||||
|
|
@ -367,23 +407,37 @@
|
|||
"regeneration_indicator.label": "আসছে…",
|
||||
"regeneration_indicator.sublabel": "আপনার বাড়ির-সময়রেখা প্রস্তূত করা হচ্ছে!",
|
||||
"relative_time.days": "{number} দিন",
|
||||
"relative_time.full.just_now": "এইমাত্র",
|
||||
"relative_time.hours": "{number} ঘন্টা",
|
||||
"relative_time.just_now": "এখন",
|
||||
"relative_time.minutes": "{number}মিঃ",
|
||||
"relative_time.seconds": "{number} সেকেন্ড",
|
||||
"relative_time.today": "আজ",
|
||||
"reply_indicator.cancel": "বাতিল করতে",
|
||||
"report.block": "অবরোধ",
|
||||
"report.categories.other": "অন্যান্য",
|
||||
"report.categories.spam": "স্প্যাম",
|
||||
"report.category.title_account": "প্রোফাইল",
|
||||
"report.category.title_status": "পোস্ট",
|
||||
"report.close": "সম্পন্ন",
|
||||
"report.forward": "এটা আরো পাঠান {target} তে",
|
||||
"report.forward_hint": "এই নিবন্ধনটি অন্য একটি সার্ভারে। অপ্রকাশিতনামাভাবে রিপোর্টের কপি সেখানেও কি পাঠাতে চান ?",
|
||||
"report.mute": "নিঃশব্দ",
|
||||
"report.next": "পরবর্তী",
|
||||
"report.placeholder": "অন্য কোনো মন্তব্য",
|
||||
"report.reasons.spam": "এটি স্প্যাম",
|
||||
"report.submit": "জমা দিন",
|
||||
"report.target": "{target} রিপোর্ট করুন",
|
||||
"report_notification.attached_statuses": "{count, plural, one {# post} other {# posts}} attached",
|
||||
"report_notification.categories.legal": "আইনি",
|
||||
"report_notification.categories.other": "অন্যান্য",
|
||||
"report_notification.categories.spam": "স্প্যাম",
|
||||
"search.placeholder": "অনুসন্ধান",
|
||||
"search_results.accounts": "প্রোফাইল",
|
||||
"search_results.all": "সব",
|
||||
"search_results.hashtags": "হ্যাশট্যাগগুলি",
|
||||
"search_results.statuses": "টুট",
|
||||
"search_results.statuses_fts_disabled": "তাদের সামগ্রী দ্বারা টুটগুলি অনুসন্ধান এই মস্তোডন সার্ভারে সক্ষম নয়।",
|
||||
"search_results.total": "{count, number} {count, plural, one {ফলাফল} other {ফলাফল}}",
|
||||
"server_banner.learn_more": "আরো জানো",
|
||||
"sign_in_banner.sign_in": "Sign in",
|
||||
"status.admin_account": "@{name} র জন্য পরিচালনার ইন্টারফেসে ঢুকুন",
|
||||
"status.admin_status": "যায় লেখাটি পরিচালনার ইন্টারফেসে খুলুন",
|
||||
|
|
@ -394,9 +448,12 @@
|
|||
"status.copy": "লেখাটির লিংক কপি করতে",
|
||||
"status.delete": "মুছে ফেলতে",
|
||||
"status.detailed_status": "বিস্তারিত কথোপকথনের হিসেবে দেখতে",
|
||||
"status.edit": "সম্পাদন",
|
||||
"status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}",
|
||||
"status.embed": "এমবেড করতে",
|
||||
"status.favourite": "পছন্দ",
|
||||
"status.filtered": "ছাঁকনিদিত",
|
||||
"status.hide": "পোস্ট লুকাও",
|
||||
"status.load_more": "আরো দেখুন",
|
||||
"status.media_hidden": "মিডিয়া লুকানো আছে",
|
||||
"status.mention": "@{name}কে উল্লেখ করতে",
|
||||
|
|
@ -423,10 +480,9 @@
|
|||
"status.show_more": "আরো দেখাতে",
|
||||
"status.show_more_all": "সবগুলোতে আরো দেখতে",
|
||||
"status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}",
|
||||
"status.translate": "অনুবাদ",
|
||||
"status.unmute_conversation": "আলোচনার প্রজ্ঞাপন চালু করতে",
|
||||
"status.unpin": "নিজের পাতা থেকে পিন করে রাখাটির পিন খুলতে",
|
||||
"suggestions.dismiss": "সাহায্যের পরামর্শগুলো সরাতে",
|
||||
"suggestions.header": "আপনি হয়তোবা এগুলোতে আগ্রহী হতে পারেন…",
|
||||
"tabs_bar.home": "বাড়ি",
|
||||
"tabs_bar.notifications": "প্রজ্ঞাপনগুলো",
|
||||
"time_remaining.days": "{number, plural, one {# day} other {# days}} বাকি আছে",
|
||||
|
|
@ -456,6 +512,7 @@
|
|||
"upload_form.video_description": "শ্রবণশক্তি হ্রাস বা চাক্ষুষ প্রতিবন্ধী ব্যক্তিদের জন্য বর্ণনা করুন",
|
||||
"upload_modal.analyzing_picture": "চিত্র বিশ্লেষণ করা হচ্ছে…",
|
||||
"upload_modal.apply": "প্রয়োগ করুন",
|
||||
"upload_modal.applying": "প্রয়োগ করা হচ্ছে…",
|
||||
"upload_modal.choose_image": "ছবি নির্বাচন করুন",
|
||||
"upload_modal.detect_text": "ছবি থেকে পাঠ্য সনাক্ত করুন",
|
||||
"upload_modal.edit_media": "মিডিয়া সম্পাদনা করুন",
|
||||
|
|
|
|||
|
|
@ -495,9 +495,7 @@
|
|||
"search_results.hashtags": "Gerioù-klik",
|
||||
"search_results.nothing_found": "Disoc'h ebet gant ar gerioù-se",
|
||||
"search_results.statuses": "Toudoù",
|
||||
"search_results.statuses_fts_disabled": "Klask toudoù dre oc'h endalc'h n'eo ket aotreet war ar servijer-mañ.",
|
||||
"search_results.title": "Klask {q}",
|
||||
"search_results.total": "{count, number} {count, plural, one {disoc'h} other {a zisoc'h}}",
|
||||
"server_banner.active_users": "implijerien·ezed oberiant",
|
||||
"server_banner.administered_by": "Meret gant :",
|
||||
"server_banner.learn_more": "Gouzout hiroc'h",
|
||||
|
|
@ -556,8 +554,6 @@
|
|||
"status.unpin": "Dispilhennañ eus ar profil",
|
||||
"subscribed_languages.save": "Enrollañ ar cheñchamantoù",
|
||||
"subscribed_languages.target": "Cheñch ar yezhoù koumanantet evit {target}",
|
||||
"suggestions.dismiss": "Dilezel damvenegoù",
|
||||
"suggestions.header": "Marteze e vefec'h dedenet gant…",
|
||||
"tabs_bar.home": "Degemer",
|
||||
"tabs_bar.notifications": "Kemennoù",
|
||||
"time_remaining.days": "{number, plural,one {# devezh} other {# a zevezh}} a chom",
|
||||
|
|
|
|||
|
|
@ -72,7 +72,6 @@
|
|||
"report.submit": "Submit report",
|
||||
"report.target": "Report {target}",
|
||||
"report_notification.attached_statuses": "{count, plural, one {# post} other {# posts}} attached",
|
||||
"search_results.total": "{count, plural, one {# result} other {# results}}",
|
||||
"sign_in_banner.sign_in": "Sign in",
|
||||
"status.admin_status": "Open this status in the moderation interface",
|
||||
"status.copy": "Copy link to status",
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@
|
|||
"compose.language.search": "Cerca idiomes...",
|
||||
"compose.published.body": "Tut publicat.",
|
||||
"compose.published.open": "Obre",
|
||||
"compose.saved.body": "Tut desat.",
|
||||
"compose_form.direct_message_warning_learn_more": "Més informació",
|
||||
"compose_form.encryption_warning": "Les publicacions a Mastodon no estant xifrades punt a punt. No comparteixis informació sensible mitjançant Mastodon.",
|
||||
"compose_form.hashtag_warning": "Aquest tut no apareixerà a les llistes d'etiquetes perquè no és públic. Només els tuts públics apareixen a les cerques per etiqueta.",
|
||||
|
|
@ -180,7 +181,7 @@
|
|||
"confirmations.mute.confirm": "Silencia",
|
||||
"confirmations.mute.explanation": "Això amagarà els tuts d'ells i els d'els que els mencionin, però encara els permetrà veure els teus tuts i seguir-te.",
|
||||
"confirmations.mute.message": "Segur que vols silenciar {name}?",
|
||||
"confirmations.redraft.confirm": "Elimina i reescriu-la",
|
||||
"confirmations.redraft.confirm": "Esborra i reescriu",
|
||||
"confirmations.redraft.message": "Segur que vols eliminar aquest tut i tornar a escriure'l? Es perdran tots els impulsos i els favorits, i les respostes al tut original quedaran aïllades.",
|
||||
"confirmations.reply.confirm": "Respon",
|
||||
"confirmations.reply.message": "Si respons ara, sobreescriuràs el missatge que estàs editant. Segur que vols continuar?",
|
||||
|
|
@ -300,14 +301,18 @@
|
|||
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} tut} other {{counter} tuts}} avui",
|
||||
"hashtag.follow": "Segueix l'etiqueta",
|
||||
"hashtag.unfollow": "Deixa de seguir l'etiqueta",
|
||||
"hashtags.and_other": "…i {count, plural, other {# més}}",
|
||||
"home.actions.go_to_explore": "Mira què és tendència",
|
||||
"home.actions.go_to_suggestions": "Troba persones a seguir",
|
||||
"home.column_settings.basic": "Bàsic",
|
||||
"home.column_settings.show_reblogs": "Mostra els impulsos",
|
||||
"home.column_settings.show_replies": "Mostra les respostes",
|
||||
"home.explore_prompt.body": "La teva línia de temps Inici tindrà una barreja dels tuts de les etiquetes que has triat seguir, de les persones que has triat seguir i dels tuts que s'impulsen. Ara mateix es veu força tranquil·la, què et sembla si:",
|
||||
"home.explore_prompt.title": "Aquest és la teva base a Mastodon.",
|
||||
"home.explore_prompt.title": "Aquesta és la teva base inicial a Mastodon.",
|
||||
"home.hide_announcements": "Amaga els anuncis",
|
||||
"home.pending_critical_update.body": "Si us plau actualitza el teu servidor Mastodon tant aviat com sigui possible!",
|
||||
"home.pending_critical_update.link": "Veure actualitzacions",
|
||||
"home.pending_critical_update.title": "Actualització de seguretat crítica disponible!",
|
||||
"home.show_announcements": "Mostra els anuncis",
|
||||
"interaction_modal.description.favourite": "Amb un compte a Mastodon pots afavorir aquest tut perquè l'autor sàpiga que t'ha agradat i desar-lo per a més endavant.",
|
||||
"interaction_modal.description.follow": "Amb un compte a Mastodon, pots seguir a {name} per a rebre els seus tuts en la teva línia de temps d'Inici.",
|
||||
|
|
@ -316,7 +321,7 @@
|
|||
"interaction_modal.login.action": "Torna a l'inici",
|
||||
"interaction_modal.login.prompt": "Domini del teu servidor domèstic, p.ex. mastodon.social",
|
||||
"interaction_modal.no_account_yet": "No a Mastodon?",
|
||||
"interaction_modal.on_another_server": "En un servidor diferent",
|
||||
"interaction_modal.on_another_server": "A un altre servidor",
|
||||
"interaction_modal.on_this_server": "En aquest servidor",
|
||||
"interaction_modal.sign_in": "No has iniciat sessió en aquest servidor. On tens el teu compte?",
|
||||
"interaction_modal.sign_in_hint": "Ajuda: Aquesta és la web on vas registrar-te. Si no ho recordes, mira el correu electrònic de benvinguda en la teva safata d'entrada. També pots introduïr el teu nom d'usuari complet! (per ex. @Mastodon@mastodon.social)",
|
||||
|
|
@ -386,7 +391,7 @@
|
|||
"load_pending": "{count, plural, one {# element nou} other {# elements nous}}",
|
||||
"loading_indicator.label": "Es carrega...",
|
||||
"media_gallery.toggle_visible": "{number, plural, one {Amaga la imatge} other {Amaga les imatges}}",
|
||||
"moved_to_account_banner.text": "El teu compte {disabledAccount} està actualment desactivat perquè l'has traslladat a {movedToAccount}.",
|
||||
"moved_to_account_banner.text": "El teu compte {disabledAccount} està desactivat perquè l'has mogut a {movedToAccount}.",
|
||||
"mute_modal.duration": "Durada",
|
||||
"mute_modal.hide_notifications": "Amagar les notificacions d'aquest usuari?",
|
||||
"mute_modal.indefinite": "Indefinit",
|
||||
|
|
@ -409,6 +414,7 @@
|
|||
"navigation_bar.lists": "Llistes",
|
||||
"navigation_bar.logout": "Tanca la sessió",
|
||||
"navigation_bar.mutes": "Usuaris silenciats",
|
||||
"navigation_bar.opened_in_classic_interface": "Els tuts, comptes i altres pàgines especifiques s'obren per defecte en la interfície web clàssica.",
|
||||
"navigation_bar.personal": "Personal",
|
||||
"navigation_bar.pins": "Tuts fixats",
|
||||
"navigation_bar.preferences": "Preferències",
|
||||
|
|
@ -420,8 +426,8 @@
|
|||
"notification.admin.sign_up": "{name} s'ha registrat",
|
||||
"notification.favourite": "{name} ha afavorit el teu tut",
|
||||
"notification.follow": "{name} et segueix",
|
||||
"notification.follow_request": "{name} ha sol·licitat seguir-te",
|
||||
"notification.mention": "{name} t'ha mencionat",
|
||||
"notification.follow_request": "{name} ha sol·licitat de seguir-te",
|
||||
"notification.mention": "{name} t'ha esmentat",
|
||||
"notification.own_poll": "La teva enquesta ha finalitzat",
|
||||
"notification.poll": "Ha finalitzat una enquesta en què has votat",
|
||||
"notification.reblog": "{name} t'ha impulsat",
|
||||
|
|
@ -445,7 +451,7 @@
|
|||
"notifications.column_settings.show": "Mostra a la columna",
|
||||
"notifications.column_settings.sound": "Reprodueix so",
|
||||
"notifications.column_settings.status": "Nous tuts:",
|
||||
"notifications.column_settings.unread_notifications.category": "Notificacions no llegides",
|
||||
"notifications.column_settings.unread_notifications.category": "Notificacions pendents de llegir",
|
||||
"notifications.column_settings.unread_notifications.highlight": "Destaca les notificacions no llegides",
|
||||
"notifications.column_settings.update": "Edicions:",
|
||||
"notifications.filter.all": "Totes",
|
||||
|
|
@ -467,25 +473,25 @@
|
|||
"onboarding.action.back": "Porta'm enrere",
|
||||
"onboarding.actions.back": "Porta'm enrere",
|
||||
"onboarding.actions.go_to_explore": "Mira què és tendència",
|
||||
"onboarding.actions.go_to_home": "Vés a la teva línia de temps inici",
|
||||
"onboarding.actions.go_to_home": "Ves a la teva línia de temps",
|
||||
"onboarding.compose.template": "Hola Mastodon!",
|
||||
"onboarding.follows.empty": "Malauradament, cap resultat pot ser mostrat ara mateix. Pots provar de fer servir la cerca o visitar la pàgina Explora per a trobar gent a qui seguir o provar-ho de nou més tard.",
|
||||
"onboarding.follows.lead": "Tu tens cura de la teva línia de temps inici. Com més gent segueixis, més activa i interessant serà. Aquests perfils poden ser un bon punt d'inici—sempre pots acabar deixant-los de seguir!",
|
||||
"onboarding.follows.title": "Popular a Mastodon",
|
||||
"onboarding.follows.lead": "La teva línia de temps inici només està a les teves mans. Com més gent segueixis, més activa i interessant serà. Aquests perfils poden ser un bon punt d'inici—sempre pots acabar deixant de seguir-los!:",
|
||||
"onboarding.follows.title": "Personalitza la pantalla d'inci",
|
||||
"onboarding.share.lead": "Permet que la gent sàpiga com trobar-te a Mastodon!",
|
||||
"onboarding.share.message": "Sóc {username} a #Mastodon! Vine i segueix-me a {url}",
|
||||
"onboarding.share.next_steps": "Possibles passes següents:",
|
||||
"onboarding.share.title": "Comparteix el teu perfil",
|
||||
"onboarding.start.lead": "El teu nou compte a Mastodon ja està preparat. Aquí tens com en pots treure tot el suc:",
|
||||
"onboarding.start.lead": "El teu nou compte ja està preparat a Mastodon, la xarxa social on tu—no un algorisme—té tot el control. Aquí tens com en pots treure tot el suc:",
|
||||
"onboarding.start.skip": "Vols saltar-te tota la resta?",
|
||||
"onboarding.start.title": "Llestos!",
|
||||
"onboarding.steps.follow_people.body": "Tu tens cura de la teva línia de temps. Omple-la de gent interessant.",
|
||||
"onboarding.steps.follow_people.title": "{count, plural, zero {No segueixes cap persona} one {Segeueixes ona persona} other {Segueixes # persones}}",
|
||||
"onboarding.steps.publish_status.body": "Saluda el món.",
|
||||
"onboarding.steps.follow_people.body": "Mastodon va de seguir a gent interessant.",
|
||||
"onboarding.steps.follow_people.title": "Personalitza la pantalla d'inci",
|
||||
"onboarding.steps.publish_status.body": "Saluda al món amb text, fotos, vídeos o enquestes {emoji}",
|
||||
"onboarding.steps.publish_status.title": "Fes el teu primer tut",
|
||||
"onboarding.steps.setup_profile.body": "És més fàcil que altres interaccionin amb tu si tens un perfil complet.",
|
||||
"onboarding.steps.setup_profile.body": "És més fàcil que altres interactuïn amb tu si tens un perfil complet.",
|
||||
"onboarding.steps.setup_profile.title": "Personalitza el perfil",
|
||||
"onboarding.steps.share_profile.body": "Permet als teus amics de saber com trobar-te a Mastodon!",
|
||||
"onboarding.steps.share_profile.body": "Fer saber als teus amics com trobar-te a Mastodon",
|
||||
"onboarding.steps.share_profile.title": "Comparteix el teu perfil",
|
||||
"onboarding.tips.2fa": "<strong>Ho sabies?</strong> Pots securitzar el teu compte activant l'autenticació de doble factor en la configuració del teu perfil. Funciona amb qualsevol aplicació TOTP de la teva elecció, no cal número de telèfon!",
|
||||
"onboarding.tips.accounts_from_other_servers": "<strong>Ho sabies?</strong> Com Mastodon és descentralitzat, et pots trobar amb perfils que són a servidors diferents del teu. I, tanmateix, també hi pots interactuar sense cap problema! El servidor és la segona part del seu nom d'usuari!",
|
||||
|
|
@ -532,6 +538,7 @@
|
|||
"reply_indicator.cancel": "Cancel·la",
|
||||
"report.block": "Bloca",
|
||||
"report.block_explanation": "No veuràs els seus tuts. Ells no podran veure els teus tuts ni et podran seguir. Podran saber que estan blocats.",
|
||||
"report.categories.legal": "Legal",
|
||||
"report.categories.other": "Altres",
|
||||
"report.categories.spam": "Brossa",
|
||||
"report.categories.violation": "El contingut viola una o més regles del servidor",
|
||||
|
|
@ -574,7 +581,7 @@
|
|||
"report_notification.categories.other": "Altres",
|
||||
"report_notification.categories.spam": "Brossa",
|
||||
"report_notification.categories.violation": "Violació de norma",
|
||||
"report_notification.open": "Obre un informe",
|
||||
"report_notification.open": "Obre l'informe",
|
||||
"search.no_recent_searches": "No hi ha cerques recents",
|
||||
"search.placeholder": "Cerca",
|
||||
"search.quick_action.account_search": "Perfils coincidint amb {x}",
|
||||
|
|
@ -583,16 +590,20 @@
|
|||
"search.quick_action.open_url": "Obrir enllaç a Mastodon",
|
||||
"search.quick_action.status_search": "Tuts coincidint amb {x}",
|
||||
"search.search_or_paste": "Cerca o escriu l'URL",
|
||||
"search_popout.full_text_search_disabled_message": "No disponible a {domain}.",
|
||||
"search_popout.language_code": "Codi de llengua ISO",
|
||||
"search_popout.options": "Opcions de cerca",
|
||||
"search_popout.quick_actions": "Accions ràpides",
|
||||
"search_popout.recent": "Cerques recents",
|
||||
"search_popout.specific_date": "data específica",
|
||||
"search_popout.user": "usuari",
|
||||
"search_results.accounts": "Perfils",
|
||||
"search_results.all": "Tots",
|
||||
"search_results.hashtags": "Etiquetes",
|
||||
"search_results.nothing_found": "No s'ha pogut trobar res per a aquests termes de cerca",
|
||||
"search_results.see_all": "Veure'ls tots",
|
||||
"search_results.statuses": "Tuts",
|
||||
"search_results.statuses_fts_disabled": "La cerca de tuts pel seu contingut no està habilitada en aquest servidor Mastodon.",
|
||||
"search_results.title": "Cerca de {q}",
|
||||
"search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}",
|
||||
"server_banner.about_active_users": "Gent que ha fet servir aquest servidor en els darrers 30 dies (Usuaris Actius Mensuals)",
|
||||
"server_banner.active_users": "usuaris actius",
|
||||
"server_banner.administered_by": "Administrat per:",
|
||||
|
|
@ -641,7 +652,7 @@
|
|||
"status.reblog_private": "Impulsa amb la visibilitat original",
|
||||
"status.reblogged_by": "impulsat per {name}",
|
||||
"status.reblogs.empty": "Encara no ha impulsat ningú aquest tut. Quan algú ho faci, apareixerà aquí.",
|
||||
"status.redraft": "Elimina i reescriu-la",
|
||||
"status.redraft": "Esborra i reescriu",
|
||||
"status.remove_bookmark": "Elimina el marcador",
|
||||
"status.replied_to": "En resposta a {name}",
|
||||
"status.reply": "Respon",
|
||||
|
|
@ -664,8 +675,6 @@
|
|||
"subscribed_languages.lead": "Només els tuts en les llengües seleccionades apareixeran en les teves línies de temps \"Inici\" i \"Llistes\" després del canvi. No en seleccionis cap per a rebre tuts en totes les llengües.",
|
||||
"subscribed_languages.save": "Desa els canvis",
|
||||
"subscribed_languages.target": "Canvia les llengües subscrites per a {target}",
|
||||
"suggestions.dismiss": "Ignora el suggeriment",
|
||||
"suggestions.header": "És possible que t'interessi…",
|
||||
"tabs_bar.home": "Inici",
|
||||
"tabs_bar.notifications": "Notificacions",
|
||||
"time_remaining.days": "{number, plural, one {# dia restant} other {# dies restants}}",
|
||||
|
|
|
|||
|
|
@ -523,9 +523,7 @@
|
|||
"search_results.hashtags": "هەشتاگ",
|
||||
"search_results.nothing_found": "هیچ بۆ ئەم زاراوە گەڕانانە نەدۆزراوەتەوە",
|
||||
"search_results.statuses": "توتەکان",
|
||||
"search_results.statuses_fts_disabled": "گەڕانی توتەکان بە ناوەڕۆکیان لەسەر ئەم ڕاژەی ماستۆدۆن چالاک نەکراوە.",
|
||||
"search_results.title": "گەڕان بەدوای {q}",
|
||||
"search_results.total": "{count, number} {count, plural, one {دەرئەنجام} other {دەرئەنجام}}",
|
||||
"server_banner.about_active_users": "ئەو کەسانەی لە ماوەی ٣٠ ڕۆژی ڕابردوودا ئەم سێرڤەرە بەکاردەهێنن (بەکارهێنەرانی چالاک مانگانە)",
|
||||
"server_banner.active_users": "بەکارهێنەرانی چالاک",
|
||||
"server_banner.administered_by": "بەڕێوەبردن لەلایەن:",
|
||||
|
|
@ -591,8 +589,6 @@
|
|||
"subscribed_languages.lead": "تەنها پۆستەکان بە زمانە هەڵبژێردراوەکان لە ماڵەکەتدا دەردەکەون و هێڵەکانی کاتی لیستەکەت دوای گۆڕانکارییەکە. هیچیان هەڵبژێرە بۆ وەرگرتنی پۆست بە هەموو زمانەکان.",
|
||||
"subscribed_languages.save": "پاشکەوتی گۆڕانکاریەکان",
|
||||
"subscribed_languages.target": "گۆڕینی زمانە بەشداربووەکان بۆ {target}",
|
||||
"suggestions.dismiss": "ڕەتکردنەوەی پێشنیار",
|
||||
"suggestions.header": "لەوانەیە حەزت لەمەش بێت…",
|
||||
"tabs_bar.home": "سەرەتا",
|
||||
"tabs_bar.notifications": "ئاگادارییەکان",
|
||||
"time_remaining.days": "{number, plural, one {# ڕۆژ} other {# ڕۆژ}} ماوە",
|
||||
|
|
|
|||
|
|
@ -345,8 +345,6 @@
|
|||
"search.placeholder": "Circà",
|
||||
"search_results.hashtags": "Hashtag",
|
||||
"search_results.statuses": "Statuti",
|
||||
"search_results.statuses_fts_disabled": "A ricerca di i cuntinuti di i statuti ùn hè micca attivata nant'à stu servore Mastodon.",
|
||||
"search_results.total": "{count, number} {count, plural, one {risultatu} other {risultati}}",
|
||||
"sign_in_banner.sign_in": "Sign in",
|
||||
"status.admin_account": "Apre l'interfaccia di muderazione per @{name}",
|
||||
"status.admin_status": "Apre stu statutu in l'interfaccia di muderazione",
|
||||
|
|
@ -388,8 +386,6 @@
|
|||
"status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}",
|
||||
"status.unmute_conversation": "Ùn piattà più a cunversazione",
|
||||
"status.unpin": "Spuntarulà da u prufile",
|
||||
"suggestions.dismiss": "Righjittà a pruposta",
|
||||
"suggestions.header": "Site forse interessatu·a da…",
|
||||
"tabs_bar.home": "Accolta",
|
||||
"tabs_bar.notifications": "Nutificazione",
|
||||
"time_remaining.days": "{number, plural, one {# ghjornu ferma} other {# ghjorni fermanu}}",
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@
|
|||
"column.directory": "Prozkoumat profily",
|
||||
"column.domain_blocks": "Blokované domény",
|
||||
"column.favourites": "Oblíbené",
|
||||
"column.firehose": "Živé kanály l",
|
||||
"column.firehose": "Živé kanály",
|
||||
"column.follow_requests": "Žádosti o sledování",
|
||||
"column.home": "Domů",
|
||||
"column.lists": "Seznamy",
|
||||
|
|
@ -137,6 +137,7 @@
|
|||
"compose.language.search": "Prohledat jazyky...",
|
||||
"compose.published.body": "Příspěvek zveřejněn.",
|
||||
"compose.published.open": "Otevřít",
|
||||
"compose.saved.body": "Příspěvek uložen.",
|
||||
"compose_form.direct_message_warning_learn_more": "Zjistit více",
|
||||
"compose_form.encryption_warning": "Příspěvky na Mastodonu nejsou end-to-end šifrovány. Nesdílejte přes Mastodon žádné citlivé informace.",
|
||||
"compose_form.hashtag_warning": "Tento příspěvek nebude zobrazen pod žádným hashtagem, protože není veřejný. Podle hashtagu lze vyhledávat jen veřejné příspěvky.",
|
||||
|
|
@ -305,6 +306,9 @@
|
|||
"home.explore_prompt.body": "Váš domovský kanál bude obsahovat směs příspěvků z hashtagů, které jste se rozhodli sledovat, lidí, které jste se rozhodli sledovat, a příspěvků, které boostují. Pokud vám to připadá příliš klidné, možná budete chtít:",
|
||||
"home.explore_prompt.title": "Toto je vaše domovská základna uvnitř Mastodonu.",
|
||||
"home.hide_announcements": "Skrýt oznámení",
|
||||
"home.pending_critical_update.body": "Aktualizujte, prosím, svůj Mastodon server co nejdříve!",
|
||||
"home.pending_critical_update.link": "Zobrazit aktualizace",
|
||||
"home.pending_critical_update.title": "K dispozici je kritická bezpečnostní aktualizace!",
|
||||
"home.show_announcements": "Zobrazit oznámení",
|
||||
"interaction_modal.description.favourite": "Pokud máte účet na Mastodonu, můžete tento příspěvek označit jako oblíbený a dát tak autorovi najevo, že si ho vážíte, a uložit si ho na později.",
|
||||
"interaction_modal.description.follow": "S účtem na Mastodonu můžete sledovat uživatele {name} a přijímat příspěvky ve vašem domovském kanálu.",
|
||||
|
|
@ -406,6 +410,7 @@
|
|||
"navigation_bar.lists": "Seznamy",
|
||||
"navigation_bar.logout": "Odhlásit se",
|
||||
"navigation_bar.mutes": "Skrytí uživatelé",
|
||||
"navigation_bar.opened_in_classic_interface": "Příspěvky, účty a další specifické stránky jsou ve výchozím nastavení otevřeny v klasickém webovém rozhraní.",
|
||||
"navigation_bar.personal": "Osobní",
|
||||
"navigation_bar.pins": "Připnuté příspěvky",
|
||||
"navigation_bar.preferences": "Předvolby",
|
||||
|
|
@ -580,16 +585,19 @@
|
|||
"search.quick_action.open_url": "Otevřít URL v Mastodonu",
|
||||
"search.quick_action.status_search": "Příspěvky odpovídající {x}",
|
||||
"search.search_or_paste": "Hledat nebo vložit URL",
|
||||
"search_popout.full_text_search_disabled_message": "Nedostupné na {domain}.",
|
||||
"search_popout.language_code": "Kód jazyka podle ISO",
|
||||
"search_popout.options": "Možnosti hledání",
|
||||
"search_popout.quick_actions": "Rychlé akce",
|
||||
"search_popout.recent": "Nedávná vyhledávání",
|
||||
"search_popout.user": "uživatel",
|
||||
"search_results.accounts": "Profily",
|
||||
"search_results.all": "Vše",
|
||||
"search_results.hashtags": "Hashtagy",
|
||||
"search_results.nothing_found": "Pro tyto hledané výrazy nebylo nic nenalezeno",
|
||||
"search_results.see_all": "Zobrazit vše",
|
||||
"search_results.statuses": "Příspěvky",
|
||||
"search_results.statuses_fts_disabled": "Vyhledávání příspěvků podle jejich obsahu není na tomto Mastodon serveru povoleno.",
|
||||
"search_results.title": "Hledat {q}",
|
||||
"search_results.total": "{count, number} {count, plural, one {výsledek} few {výsledky} many {výsledků} other {výsledků}}",
|
||||
"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.administered_by": "Spravováno:",
|
||||
|
|
@ -661,8 +669,6 @@
|
|||
"subscribed_languages.lead": "Ve vašem domovském kanálu a časových osách se po změně budou objevovat pouze příspěvky ve vybraných jazycích. Pro příjem příspěvků ve všech jazycích nevyberte žádný jazyk.",
|
||||
"subscribed_languages.save": "Uložit změny",
|
||||
"subscribed_languages.target": "Změnit odebírané jazyky na {target}",
|
||||
"suggestions.dismiss": "Odmítnout návrh",
|
||||
"suggestions.header": "Mohlo by vás zajímat…",
|
||||
"tabs_bar.home": "Domů",
|
||||
"tabs_bar.notifications": "Oznámení",
|
||||
"time_remaining.days": "{number, plural, one {Zbývá # den} few {Zbývají # dny} many {Zbývá # dní} other {Zbývá # dní}}",
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@
|
|||
"compose.language.search": "Chwilio ieithoedd...",
|
||||
"compose.published.body": "Postiad wedi ei gyhoeddi.",
|
||||
"compose.published.open": "Agor",
|
||||
"compose.saved.body": "Post wedi'i gadw.",
|
||||
"compose_form.direct_message_warning_learn_more": "Dysgu mwy",
|
||||
"compose_form.encryption_warning": "Dyw postiadau ar Mastodon ddim wedi'u hamgryptio o ben i ben. Peidiwch â rhannu unrhyw wybodaeth sensitif dros Mastodon.",
|
||||
"compose_form.hashtag_warning": "Ni fydd y postiad hwn wedi ei restru o dan unrhyw hashnod gan nad yw'n gyhoeddus. Dim ond postiadau cyhoeddus y mae modd eu chwilio drwy hashnod.",
|
||||
|
|
@ -198,11 +199,11 @@
|
|||
"directory.recently_active": "Ar-lein yn ddiweddar",
|
||||
"disabled_account_banner.account_settings": "Gosodiadau'r cyfrif",
|
||||
"disabled_account_banner.text": "Mae eich cyfrif {disabledAccount} wedi ei analluogi ar hyn o bryd.",
|
||||
"dismissable_banner.community_timeline": "Dyma'r postiadau cyhoeddus diweddaraf gan bobl gyda chyfrifon ar {domain}.",
|
||||
"dismissable_banner.community_timeline": "Dyma'r postiadau cyhoeddus diweddaraf gan bobl sydd â chyfrifon ar {domain}.",
|
||||
"dismissable_banner.dismiss": "Diddymu",
|
||||
"dismissable_banner.explore_links": "Dyma'r straeon newyddion sy'n cael eu trafod ar hyn o bryd gan bobl ar y gweinydd hwn a rhai eraill ar y rhwydwaith datganoledig yma.",
|
||||
"dismissable_banner.explore_statuses": "Mae'r rhain yn bostiadau o bob rhan o'r we gymdeithasol sy'n cael eu poblogeiddio heddiw. Mae postiadau mwy diweddar gyda mwy o hybiau a ffefrynnau yn cael eu graddio'n uwch.",
|
||||
"dismissable_banner.explore_tags": "Mae'r hashnodau hyn yn denu sylw ymhlith pobl ar y gweinydd hwn a gweinyddwyr eraill y rhwydwaith datganoledig ar hyn o bryd.",
|
||||
"dismissable_banner.explore_links": "Dyma straeon newyddion sy’n cael eu rhannu fwyaf ar y we gymdeithasol heddiw. Mae'r straeon newyddion diweddaraf sy'n cael eu postio gan fwy o unigolion gwahanol yn cael eu graddio'n uwch.",
|
||||
"dismissable_banner.explore_statuses": "Mae'r rhain yn bostiadau o bob rhan o'r we gymdeithasol sydd ar gynnydd heddiw. Mae postiadau mwy diweddar sydd â mwy o hybiau a ffefrynu'n cael eu graddio'n uwch.",
|
||||
"dismissable_banner.explore_tags": "Mae'r rhain yn hashnodau sydd ar gynnydd ar y we gymdeithasol heddiw. Mae hashnodau sy'n cael eu defnyddio gan fwy o unigolion gwahanol yn cael eu graddio'n uwch.",
|
||||
"dismissable_banner.public_timeline": "Dyma'r postiadau cyhoeddus diweddaraf gan bobl ar y we gymdeithasol y mae pobl ar {domain} yn eu dilyn.",
|
||||
"embed.instructions": "Gosodwch y post hwn ar eich gwefan drwy gopïo'r côd isod.",
|
||||
"embed.preview": "Dyma sut olwg fydd arno:",
|
||||
|
|
@ -295,16 +296,23 @@
|
|||
"hashtag.column_settings.tag_mode.any": "Unrhyw un o'r rhain",
|
||||
"hashtag.column_settings.tag_mode.none": "Dim o'r rhain",
|
||||
"hashtag.column_settings.tag_toggle": "Include additional tags in this column",
|
||||
"hashtag.counter_by_accounts": "{cyfrif, lluosog, un {{counter} cyfranogwr} arall {{counter} cyfranogwr}}",
|
||||
"hashtag.counter_by_uses": "{count, plural, one {postiad {counter}} other {postiad {counter}}}",
|
||||
"hashtag.counter_by_uses_today": "{cyfrif, lluosog, un {{counter} postiad} arall {{counter} postiad}} heddiw",
|
||||
"hashtag.follow": "Dilyn hashnod",
|
||||
"hashtag.unfollow": "Dad-ddilyn hashnod",
|
||||
"home.actions.go_to_explore": "Gweld beth sy'n tueddu",
|
||||
"hashtags.and_other": "…a {count, plural, other {# more}}",
|
||||
"home.actions.go_to_explore": "Gweld beth yw'r tuedd",
|
||||
"home.actions.go_to_suggestions": "Ffeindio pobl i'w dilyn",
|
||||
"home.column_settings.basic": "Syml",
|
||||
"home.column_settings.show_reblogs": "Dangos hybiau",
|
||||
"home.column_settings.show_replies": "Dangos atebion",
|
||||
"home.explore_prompt.body": "Bydd eich llif cartref yn cynnwys cymysgedd o bostiadau o'r hashnodau rydych chi wedi dewis eu dilyn, y bobl rydych chi wedi dewis eu dilyn, a'r postiadau maen nhw'n rhoi hwb iddyn nhw. Os yw hynny'n teimlo'n rhy dawel, efallai y byddwch am:",
|
||||
"home.explore_prompt.body": "Bydd eich llif cartref yn cynnwys cymysgedd o bostiadau o'r hashnodau rydych chi wedi dewis eu dilyn, y bobl rydych chi wedi dewis eu dilyn, a'r postiadau maen nhw'n rhoi hwb iddyn nhw. Os yw hynny'n teimlo'n rhy dawel, efallai y byddwch eisiau:",
|
||||
"home.explore_prompt.title": "Dyma'ch cartref o fewn Mastodon.",
|
||||
"home.hide_announcements": "Cuddio cyhoeddiadau",
|
||||
"home.pending_critical_update.body": "Diweddarwch eich gweinydd Mastodon cyn gynted â phosibl!",
|
||||
"home.pending_critical_update.link": "Gweld y diweddariadau",
|
||||
"home.pending_critical_update.title": "Mae diweddariad diogelwch hanfodol ar gael!",
|
||||
"home.show_announcements": "Dangos cyhoeddiadau",
|
||||
"interaction_modal.description.favourite": "Gyda chyfrif ar Mastodon, gallwch chi hoffi'r postiad hwn er mwyn roi gwybod i'r awdur eich bod chi'n ei werthfawrogi ac yn ei gadw ar gyfer nes ymlaen.",
|
||||
"interaction_modal.description.follow": "Gyda chyfrif ar Mastodon, gallwch ddilyn {name} i dderbyn eu postiadau yn eich llif cartref.",
|
||||
|
|
@ -406,6 +414,7 @@
|
|||
"navigation_bar.lists": "Rhestrau",
|
||||
"navigation_bar.logout": "Allgofnodi",
|
||||
"navigation_bar.mutes": "Defnyddwyr wedi'u tewi",
|
||||
"navigation_bar.opened_in_classic_interface": "Mae postiadau, cyfrifon a thudalennau penodol eraill yn cael eu hagor fel rhagosodiad yn y rhyngwyneb gwe clasurol.",
|
||||
"navigation_bar.personal": "Personol",
|
||||
"navigation_bar.pins": "Postiadau wedi eu pinio",
|
||||
"navigation_bar.preferences": "Dewisiadau",
|
||||
|
|
@ -529,6 +538,7 @@
|
|||
"reply_indicator.cancel": "Canslo",
|
||||
"report.block": "Blocio",
|
||||
"report.block_explanation": "Ni welwch chi eu postiadau. Ni allan nhw weld eich postiadau na'ch dilyn. Byddan nhw'n gallu gweld eu bod nhw wedi'u rhwystro.",
|
||||
"report.categories.legal": "Cyfreithiol",
|
||||
"report.categories.other": "Arall",
|
||||
"report.categories.spam": "Sbam",
|
||||
"report.categories.violation": "Mae cynnwys yn torri un neu fwy o reolau'r gweinydd",
|
||||
|
|
@ -580,16 +590,20 @@
|
|||
"search.quick_action.open_url": "Agor URL yn Mastodon",
|
||||
"search.quick_action.status_search": "Postiadau sy'n cyfateb i {x}",
|
||||
"search.search_or_paste": "Chwilio neu gludo URL",
|
||||
"search_popout.full_text_search_disabled_message": "Ddim ar gael ar {domain}.",
|
||||
"search_popout.language_code": "Cod iaith ISO",
|
||||
"search_popout.options": "Dewisiadau chwilio",
|
||||
"search_popout.quick_actions": "Gweithredoedd cyflym",
|
||||
"search_popout.recent": "Chwilio diweddar",
|
||||
"search_popout.specific_date": "dyddiad penodol",
|
||||
"search_popout.user": "defnyddiwr",
|
||||
"search_results.accounts": "Proffilau",
|
||||
"search_results.all": "Popeth",
|
||||
"search_results.hashtags": "Hashnodau",
|
||||
"search_results.nothing_found": "Methu dod o hyd i unrhyw beth ar gyfer y termau chwilio hyn",
|
||||
"search_results.see_all": "Gweld y cyfan",
|
||||
"search_results.statuses": "Postiadau",
|
||||
"search_results.statuses_fts_disabled": "Nid yw chwilio postiadau yn ôl eu cynnwys wedi'i alluogi ar y gweinydd Mastodon hwn.",
|
||||
"search_results.title": "Chwilio am {q}",
|
||||
"search_results.total": "{count, number} {count, plural, zero {canlyniad} one {canlyniad} two {ganlyniad} other {canlyniad}}",
|
||||
"server_banner.about_active_users": "Pobl sy'n defnyddio'r gweinydd hwn yn ystod y 30 diwrnod diwethaf (Defnyddwyr Gweithredol Misol)",
|
||||
"server_banner.active_users": "defnyddwyr gweithredol",
|
||||
"server_banner.administered_by": "Gweinyddir gan:",
|
||||
|
|
@ -661,8 +675,6 @@
|
|||
"subscribed_languages.lead": "Dim ond postiadau mewn ieithoedd penodol fydd yn ymddangos yn eich ffrydiau ar ôl y newid. Dewiswch ddim byd i dderbyn postiadau ym mhob iaith.",
|
||||
"subscribed_languages.save": "Cadw'r newidiadau",
|
||||
"subscribed_languages.target": "Newid ieithoedd tanysgrifio {target}",
|
||||
"suggestions.dismiss": "Diystyru'r awgrym",
|
||||
"suggestions.header": "Efallai y bydd gennych ddiddordeb mewn…",
|
||||
"tabs_bar.home": "Cartref",
|
||||
"tabs_bar.notifications": "Hysbysiadau",
|
||||
"time_remaining.days": "{number, plural, one {# diwrnod} other {# diwrnod}} ar ôl",
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@
|
|||
"compose.language.search": "Søg efter sprog...",
|
||||
"compose.published.body": "Indlæg udgivet.",
|
||||
"compose.published.open": "Åbn",
|
||||
"compose.saved.body": "Indlæg gemt.",
|
||||
"compose_form.direct_message_warning_learn_more": "Få mere at vide",
|
||||
"compose_form.encryption_warning": "Indlæg på Mastodon er ikke ende-til-ende-krypteret. Del derfor ikke sensitiv information via Mastodon.",
|
||||
"compose_form.hashtag_warning": "Da indlægget ikke er offentligt, vises det ikke under noget hashtag, da kun offentlige indlæg er søgbare via hashtags.",
|
||||
|
|
@ -198,7 +199,7 @@
|
|||
"directory.recently_active": "Aktive for nyligt",
|
||||
"disabled_account_banner.account_settings": "Kontoindstillinger",
|
||||
"disabled_account_banner.text": "Din konto {disabledAccount} er pt. deaktiveret.",
|
||||
"dismissable_banner.community_timeline": "Disse er de seneste offentlige indlæg fra personer med konti hostes af {domain}.",
|
||||
"dismissable_banner.community_timeline": "Disse er de seneste offentlige indlæg fra personer med konti hostet af {domain}.",
|
||||
"dismissable_banner.dismiss": "Afvis",
|
||||
"dismissable_banner.explore_links": "Der tales lige nu om disse nyhedshistorier af folk på denne og andre servere i det decentraliserede netværk.",
|
||||
"dismissable_banner.explore_statuses": "Disse indlæg fra diverse sociale netværk vinder fodfæste i dag. Nyere indlæg med flere boosts og favoritter rangeres højere.",
|
||||
|
|
@ -281,7 +282,7 @@
|
|||
"footer.get_app": "Hent appen",
|
||||
"footer.invite": "Invitér personer",
|
||||
"footer.keyboard_shortcuts": "Tastaturgenveje",
|
||||
"footer.privacy_policy": "Fortrolighedspolitik",
|
||||
"footer.privacy_policy": "Privatlivspolitik",
|
||||
"footer.source_code": "Vis kildekode",
|
||||
"footer.status": "Status",
|
||||
"generic.saved": "Gemt",
|
||||
|
|
@ -300,14 +301,18 @@
|
|||
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} indlæg} other {{counter} indlæg}} i dag",
|
||||
"hashtag.follow": "Følg hashtag",
|
||||
"hashtag.unfollow": "Stop med at følge hashtag",
|
||||
"hashtags.and_other": "…og {count, plural, one {}other {# flere}}",
|
||||
"home.actions.go_to_explore": "Se, hvad som trender",
|
||||
"home.actions.go_to_suggestions": "Find nogle personer at følge",
|
||||
"home.column_settings.basic": "Grundlæggende",
|
||||
"home.column_settings.show_reblogs": "Vis boosts",
|
||||
"home.column_settings.show_replies": "Vis svar",
|
||||
"home.explore_prompt.body": "Hjemmefeedet vil indeholde en blanding af indlæg fra de hashtags og personer, du følger samt de indlæg, de booster. Føles synes for stille, kan du prøve:",
|
||||
"home.explore_prompt.body": "Dit hjemmefeed vil have en blanding af indlæg fra de hashtags, du har valgt at følge, de personer, du har valgt at følge, og de indlæg, de booster. Hvis her virker for stille, kan du prøve:",
|
||||
"home.explore_prompt.title": "Dette er din hjemmebase i Mastodon.",
|
||||
"home.hide_announcements": "Skjul bekendtgørelser",
|
||||
"home.pending_critical_update.body": "Opdater din Mastodon-server snarest muligt!",
|
||||
"home.pending_critical_update.link": "Se opdateringer",
|
||||
"home.pending_critical_update.title": "Kritisk sikkerhedsopdatering tilgængelig!",
|
||||
"home.show_announcements": "Vis bekendtgørelser",
|
||||
"interaction_modal.description.favourite": "Med en konto på Mastodon kan dette indlæg gøres til favorit for at lade forfatteren vide, at det værdsættes og gemmes til senere.",
|
||||
"interaction_modal.description.follow": "Med en konto på Mastodon kan du følge {name} for at modtage vedkommendes indlæg i dit hjemmefeed.",
|
||||
|
|
@ -409,6 +414,7 @@
|
|||
"navigation_bar.lists": "Lister",
|
||||
"navigation_bar.logout": "Log af",
|
||||
"navigation_bar.mutes": "Skjulte brugere (mutede)",
|
||||
"navigation_bar.opened_in_classic_interface": "Indlæg, konti og visse andre sider åbnes som standard i den klassiske webgrænseflade.",
|
||||
"navigation_bar.personal": "Personlig",
|
||||
"navigation_bar.pins": "Fastgjorte indlæg",
|
||||
"navigation_bar.preferences": "Præferencer",
|
||||
|
|
@ -504,7 +510,7 @@
|
|||
"poll.votes": "{votes, plural, one {# stemme} other {# stemmer}}",
|
||||
"poll_button.add_poll": "Tilføj en afstemning",
|
||||
"poll_button.remove_poll": "Fjern afstemning",
|
||||
"privacy.change": "Justér indlægsfortrolighed",
|
||||
"privacy.change": "Tilpas indlægsfortrolighed",
|
||||
"privacy.direct.long": "Kun synlig for nævnte brugere",
|
||||
"privacy.direct.short": "Kun omtalte personer",
|
||||
"privacy.private.long": "Kun synlig for følgere",
|
||||
|
|
@ -514,7 +520,7 @@
|
|||
"privacy.unlisted.long": "Synlig for alle, men med fravalgt visning i opdagelsesfunktioner",
|
||||
"privacy.unlisted.short": "Diskret",
|
||||
"privacy_policy.last_updated": "Senest opdateret {date}",
|
||||
"privacy_policy.title": "Fortrolighedspolitik",
|
||||
"privacy_policy.title": "Privatlivspolitik",
|
||||
"refresh": "Genindlæs",
|
||||
"regeneration_indicator.label": "Indlæser…",
|
||||
"regeneration_indicator.sublabel": "Din hjemmetidslinje klargøres!",
|
||||
|
|
@ -532,6 +538,7 @@
|
|||
"reply_indicator.cancel": "Afbryd",
|
||||
"report.block": "Blokér",
|
||||
"report.block_explanation": "Du vil ikke se vedkommendes indlæg. Vedkommende vil ikke kunne se dine indlæg eller følge dig. Vedkommende vil kunne se, at de er blokeret.",
|
||||
"report.categories.legal": "Juridisk",
|
||||
"report.categories.other": "Andre",
|
||||
"report.categories.spam": "Spam",
|
||||
"report.categories.violation": "Indhold overtræder en eller flere serverregler",
|
||||
|
|
@ -583,16 +590,20 @@
|
|||
"search.quick_action.open_url": "Åbn URL i Mastodon",
|
||||
"search.quick_action.status_search": "Indlæg matchende {x}",
|
||||
"search.search_or_paste": "Søg efter eller angiv URL",
|
||||
"search_popout.full_text_search_disabled_message": "Utilgængelig på {domain}.",
|
||||
"search_popout.language_code": "ISO-sprogkode",
|
||||
"search_popout.options": "Søgevalg",
|
||||
"search_popout.quick_actions": "Hurtige handlinger",
|
||||
"search_popout.recent": "Seneste søgninger",
|
||||
"search_popout.specific_date": "bestemt dato",
|
||||
"search_popout.user": "bruger",
|
||||
"search_results.accounts": "Profiler",
|
||||
"search_results.all": "Alle",
|
||||
"search_results.hashtags": "Hashtags",
|
||||
"search_results.nothing_found": "Ingen resultater for disse søgeord",
|
||||
"search_results.see_all": "Vis alle",
|
||||
"search_results.statuses": "Indlæg",
|
||||
"search_results.statuses_fts_disabled": "Søgning på indlæg efter deres indhold ikke aktiveret på denne Mastodon-server.",
|
||||
"search_results.title": "Søg efter {q}",
|
||||
"search_results.total": "{count, number} {count, plural, one {resultat} other {resultater}}",
|
||||
"server_banner.about_active_users": "Folk, som brugte denne server de seneste 30 dage (månedlige aktive brugere)",
|
||||
"server_banner.active_users": "aktive brugere",
|
||||
"server_banner.administered_by": "Håndteres af:",
|
||||
|
|
@ -637,7 +648,7 @@
|
|||
"status.pin": "Fastgør til profil",
|
||||
"status.pinned": "Fastgjort indlæg",
|
||||
"status.read_more": "Læs mere",
|
||||
"status.reblog": "Boost",
|
||||
"status.reblog": "Fremhæv",
|
||||
"status.reblog_private": "Boost med oprindelig synlighed",
|
||||
"status.reblogged_by": "{name} fremhævede",
|
||||
"status.reblogs.empty": "Ingen har endnu fremhævet dette indlæg. Når nogen gør, vil det fremgå hér.",
|
||||
|
|
@ -664,8 +675,6 @@
|
|||
"subscribed_languages.lead": "Kun indlæg på udvalgte sprog vil fremgå på dine hjemme- og listetidslinjer efter ændringen. Vælg ingen for at modtage indlæg på alle sprog.",
|
||||
"subscribed_languages.save": "Gem ændringer",
|
||||
"subscribed_languages.target": "Skift abonnementssprog for {target}",
|
||||
"suggestions.dismiss": "Afvis forslag",
|
||||
"suggestions.header": "Du er måske interesseret i…",
|
||||
"tabs_bar.home": "Hjem",
|
||||
"tabs_bar.notifications": "Notifikationer",
|
||||
"time_remaining.days": "{number, plural, one {# dag} other {# dage}} tilbage",
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@
|
|||
"account.go_to_profile": "Profil aufrufen",
|
||||
"account.hide_reblogs": "Geteilte Beiträge von @{name} ausblenden",
|
||||
"account.in_memoriam": "Zum Andenken.",
|
||||
"account.joined_short": "Registriert",
|
||||
"account.languages": "Genutzte Sprachen überarbeiten",
|
||||
"account.joined_short": "Beigetreten",
|
||||
"account.languages": "Ausgewählte Sprachen ändern",
|
||||
"account.link_verified_on": "Das Profil mit dieser E-Mail-Adresse wurde bereits am {date} bestätigt",
|
||||
"account.locked_info": "Die Privatsphäre dieses Kontos wurde auf „geschützt“ gesetzt. Die Person bestimmt manuell, wer ihrem Profil folgen darf.",
|
||||
"account.media": "Medien",
|
||||
|
|
@ -71,8 +71,8 @@
|
|||
"account.unmute_notifications_short": "Stummschaltung der Benachrichtigungen aufheben",
|
||||
"account.unmute_short": "Stummschaltung aufheben",
|
||||
"account_note.placeholder": "Notiz durch Klicken hinzufügen",
|
||||
"admin.dashboard.daily_retention": "Verweildauer der Nutzer*innen pro Tag nach der Registrierung",
|
||||
"admin.dashboard.monthly_retention": "Verweildauer der Nutzer*innen pro Monat nach der Registrierung",
|
||||
"admin.dashboard.daily_retention": "Verweildauer der Benutzer*innen pro Tag nach der Registrierung",
|
||||
"admin.dashboard.monthly_retention": "Verweildauer der Benutzer*innen pro Monat nach der Registrierung",
|
||||
"admin.dashboard.retention.average": "Durchschnitt",
|
||||
"admin.dashboard.retention.cohort": "Monat der Registrierung",
|
||||
"admin.dashboard.retention.cohort_size": "Neue Konten",
|
||||
|
|
@ -88,7 +88,7 @@
|
|||
"attachments_list.unprocessed": "(ausstehend)",
|
||||
"audio.hide": "Audio ausblenden",
|
||||
"autosuggest_hashtag.per_week": "{count} pro Woche",
|
||||
"boost_modal.combo": "Mit {combo} wird dieses Fenster beim nächsten Mal nicht mehr angezeigt",
|
||||
"boost_modal.combo": "Drücke {combo}, um das beim nächsten Mal zu überspringen",
|
||||
"bundle_column_error.copy_stacktrace": "Fehlerbericht kopieren",
|
||||
"bundle_column_error.error.body": "Die angeforderte Seite konnte nicht dargestellt werden. Dies könnte auf einen Fehler in unserem Code oder auf ein Browser-Kompatibilitätsproblem zurückzuführen sein.",
|
||||
"bundle_column_error.error.title": "Oh nein!",
|
||||
|
|
@ -119,7 +119,7 @@
|
|||
"column.home": "Startseite",
|
||||
"column.lists": "Listen",
|
||||
"column.mutes": "Stummgeschaltete Profile",
|
||||
"column.notifications": "Mitteilungen",
|
||||
"column.notifications": "Benachrichtigungen",
|
||||
"column.pins": "Angeheftete Beiträge",
|
||||
"column.public": "Föderierte Timeline",
|
||||
"column_back_button.label": "Zurück",
|
||||
|
|
@ -137,12 +137,13 @@
|
|||
"compose.language.search": "Sprachen suchen …",
|
||||
"compose.published.body": "Beitrag veröffentlicht.",
|
||||
"compose.published.open": "Öffnen",
|
||||
"compose.saved.body": "Beitrag gespeichert.",
|
||||
"compose_form.direct_message_warning_learn_more": "Mehr erfahren",
|
||||
"compose_form.encryption_warning": "Beiträge auf Mastodon sind nicht Ende-zu-Ende-verschlüsselt. Teile keine sensiblen Informationen über Mastodon.",
|
||||
"compose_form.hashtag_warning": "Dieser Beitrag wird unter keinem Hashtag sichtbar sein, weil er nicht öffentlich ist. Nur öffentliche Beiträge können nach Hashtags durchsucht werden.",
|
||||
"compose_form.lock_disclaimer": "Dein Profil ist nicht {locked}. Andere können dir folgen und deine Beiträge sehen, die nur für Follower bestimmt sind.",
|
||||
"compose_form.lock_disclaimer.lock": "geschützt",
|
||||
"compose_form.placeholder": "Was gibt's Neues?",
|
||||
"compose_form.placeholder": "Was gibt’s Neues?",
|
||||
"compose_form.poll.add_option": "Auswahl",
|
||||
"compose_form.poll.duration": "Umfragedauer",
|
||||
"compose_form.poll.option_placeholder": "{number}. Auswahl",
|
||||
|
|
@ -166,9 +167,9 @@
|
|||
"confirmations.cancel_follow_request.confirm": "Anfrage zurückziehen",
|
||||
"confirmations.cancel_follow_request.message": "Möchtest du deine Anfrage, {name} zu folgen, wirklich zurückziehen?",
|
||||
"confirmations.delete.confirm": "Löschen",
|
||||
"confirmations.delete.message": "Bist du dir sicher, dass du diesen Beitrag löschen möchtest?",
|
||||
"confirmations.delete.message": "Möchtest du diesen Beitrag wirklich löschen?",
|
||||
"confirmations.delete_list.confirm": "Löschen",
|
||||
"confirmations.delete_list.message": "Möchtest du diese Liste endgültig löschen?",
|
||||
"confirmations.delete_list.message": "Möchtest du diese Liste für immer löschen?",
|
||||
"confirmations.discard_edit_media.confirm": "Verwerfen",
|
||||
"confirmations.discard_edit_media.message": "Du hast Änderungen an der Medienbeschreibung oder -vorschau vorgenommen, die noch nicht gespeichert sind. Trotzdem verwerfen?",
|
||||
"confirmations.domain_block.confirm": "Domain blockieren",
|
||||
|
|
@ -179,13 +180,13 @@
|
|||
"confirmations.logout.message": "Möchtest du dich wirklich abmelden?",
|
||||
"confirmations.mute.confirm": "Stummschalten",
|
||||
"confirmations.mute.explanation": "Dies wird Beiträge von dieser Person und Beiträge, die diese Person erwähnen, ausblenden, aber es wird der Person trotzdem erlauben, deine Beiträge zu sehen und dir zu folgen.",
|
||||
"confirmations.mute.message": "Bist du dir sicher, dass du {name} stummschalten möchtest?",
|
||||
"confirmations.mute.message": "Möchtest du {name} wirklich stummschalten?",
|
||||
"confirmations.redraft.confirm": "Löschen und neu erstellen",
|
||||
"confirmations.redraft.message": "Möchtest du diesen Beitrag wirklich löschen und neu verfassen? Favoriten und geteilte Beiträge gehen verloren, und Antworten auf den ursprünglichen Beitrag verlieren den Zusammenhang.",
|
||||
"confirmations.reply.confirm": "Antworten",
|
||||
"confirmations.reply.message": "Wenn du jetzt darauf antwortest, wird der andere Beitrag, an dem du gerade geschrieben hast, verworfen. Möchtest du wirklich fortfahren?",
|
||||
"confirmations.unfollow.confirm": "Entfolgen",
|
||||
"confirmations.unfollow.message": "Bist du dir sicher, dass du {name} entfolgen möchtest?",
|
||||
"confirmations.unfollow.message": "Möchtest du {name} wirklich entfolgen?",
|
||||
"conversation.delete": "Unterhaltung löschen",
|
||||
"conversation.mark_as_read": "Als gelesen markieren",
|
||||
"conversation.open": "Unterhaltung anzeigen",
|
||||
|
|
@ -194,7 +195,7 @@
|
|||
"copypaste.copy_to_clipboard": "In die Zwischenablage kopieren",
|
||||
"directory.federated": "Aus bekanntem Fediverse",
|
||||
"directory.local": "Nur von der Domain {domain}",
|
||||
"directory.new_arrivals": "Neue Profile",
|
||||
"directory.new_arrivals": "Neue Benutzer*innen",
|
||||
"directory.recently_active": "Kürzlich aktiv",
|
||||
"disabled_account_banner.account_settings": "Kontoeinstellungen",
|
||||
"disabled_account_banner.text": "Dein Konto {disabledAccount} ist derzeit deaktiviert.",
|
||||
|
|
@ -227,24 +228,24 @@
|
|||
"empty_column.blocks": "Du hast bisher keine Profile blockiert.",
|
||||
"empty_column.bookmarked_statuses": "Du hast bisher keine Beiträge als Lesezeichen abgelegt. Sobald du einen Beitrag als Lesezeichen speicherst, wird er hier erscheinen.",
|
||||
"empty_column.community": "Die lokale Timeline ist leer. Schreibe einen öffentlichen Beitrag, um den Stein ins Rollen zu bringen!",
|
||||
"empty_column.direct": "Du hast noch keine privaten Erwähnungen. Sobald du eine sendest oder erhältst, wird sie hier angezeigt.",
|
||||
"empty_column.direct": "Du hast noch keine privaten Erwähnungen. Sobald du eine sendest oder erhältst, wird sie hier erscheinen.",
|
||||
"empty_column.domain_blocks": "Du hast noch keine Domains blockiert.",
|
||||
"empty_column.explore_statuses": "Momentan ist nichts im Trend. Schau später wieder vorbei!",
|
||||
"empty_column.favourited_statuses": "Du hast noch keine Beiträge favorisiert. Sobald du einen favorisierst, wird er hier erscheinen.",
|
||||
"empty_column.favourites": "Diesen Beitrag hat bisher noch niemand favorisiert. Sobald es jemand tut, wird das Profil hier angezeigt.",
|
||||
"empty_column.follow_requests": "Es liegen derzeit keine Follower-Anfragen vor. Sobald du eine erhältst, wird sie hier angezeigt.",
|
||||
"empty_column.favourites": "Diesen Beitrag hat bisher noch niemand favorisiert. Sobald es jemand tut, wird das Profil hier erscheinen.",
|
||||
"empty_column.follow_requests": "Es liegen derzeit keine Follower-Anfragen vor. Sobald du eine erhältst, wird sie hier erscheinen.",
|
||||
"empty_column.followed_tags": "Du folgst noch keinen Hashtags. Wenn du dies tust, werden sie hier erscheinen.",
|
||||
"empty_column.hashtag": "Unter diesem Hashtag gibt es noch nichts.",
|
||||
"empty_column.home": "Die Timeline deiner Startseite ist leer! Folge mehr Leuten, um sie zu füllen.",
|
||||
"empty_column.list": "Diese Liste ist derzeit leer. Wenn Konten auf dieser Liste neue Beiträge veröffentlichen, werden sie hier erscheinen.",
|
||||
"empty_column.lists": "Du hast noch keine Listen. Sobald du eine anlegst, wird sie hier erscheinen.",
|
||||
"empty_column.mutes": "Du hast keine Profile stummgeschaltet.",
|
||||
"empty_column.notifications": "Du hast noch keine Mitteilungen. Sobald du mit anderen Personen interagierst, wirst du hier darüber benachrichtigt.",
|
||||
"empty_column.notifications": "Du hast noch keine Benachrichtigungen. Sobald andere Personen mit dir interagieren, wirst du hier darüber informiert.",
|
||||
"empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Profilen von anderen Servern, um die Timeline aufzufüllen",
|
||||
"error.unexpected_crash.explanation": "Wegen eines Fehlers in unserem Code oder aufgrund einer Browser-Inkompatibilität kann diese Seite nicht korrekt angezeigt werden.",
|
||||
"error.unexpected_crash.explanation_addons": "Diese Seite konnte nicht korrekt angezeigt werden. Dieser Fehler wird wahrscheinlich durch ein Browser-Add-on oder automatische Übersetzungswerkzeuge verursacht.",
|
||||
"error.unexpected_crash.next_steps": "Versuche, diese Seite zu aktualisieren. Wenn das nicht helfen sollte, kannst du das Webinterface von Mastodon vermutlich über einen anderen Browser erreichen – oder du nutzt eine mobile (native) App.",
|
||||
"error.unexpected_crash.next_steps_addons": "Versuche, die Seite zu deaktivieren und lade sie dann neu. Sollte das Problem weiter bestehen, kannst du das Webinterface von Mastodon vermutlich über einen anderen Browser erreichen – oder du nutzt eine mobile (native) App.",
|
||||
"error.unexpected_crash.next_steps": "Versuche, die Seite neu zu laden. Wenn das nicht helfen sollte, kannst du das Webinterface von Mastodon vermutlich über einen anderen Browser erreichen – oder du verwendest eine mobile (native) App.",
|
||||
"error.unexpected_crash.next_steps_addons": "Versuche, das Add-on oder Übersetzungswerkzeug zu deaktivieren und lade die Seite anschließend neu. Sollte das Problem weiter bestehen, kannst du das Webinterface von Mastodon vermutlich über einen anderen Browser erreichen – oder du verwendest eine mobile (native) App.",
|
||||
"errors.unexpected_crash.copy_stacktrace": "Fehlerdiagnose in die Zwischenablage kopieren",
|
||||
"errors.unexpected_crash.report_issue": "Fehler melden",
|
||||
"explore.search_results": "Suchergebnisse",
|
||||
|
|
@ -285,7 +286,7 @@
|
|||
"footer.source_code": "Quellcode anzeigen",
|
||||
"footer.status": "Status",
|
||||
"generic.saved": "Gespeichert",
|
||||
"getting_started.heading": "Auf geht's!",
|
||||
"getting_started.heading": "Auf geht’s!",
|
||||
"hashtag.column_header.tag_mode.all": "und {additional}",
|
||||
"hashtag.column_header.tag_mode.any": "oder {additional}",
|
||||
"hashtag.column_header.tag_mode.none": "ohne {additional}",
|
||||
|
|
@ -300,14 +301,18 @@
|
|||
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}} heute",
|
||||
"hashtag.follow": "Hashtag folgen",
|
||||
"hashtag.unfollow": "Hashtag entfolgen",
|
||||
"hashtags.and_other": "… und {count, plural, one{# weiterer} other {# weitere}}",
|
||||
"home.actions.go_to_explore": "Trends ansehen",
|
||||
"home.actions.go_to_suggestions": "Profile zum Folgen finden",
|
||||
"home.column_settings.basic": "Einfach",
|
||||
"home.column_settings.basic": "Allgemein",
|
||||
"home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen",
|
||||
"home.column_settings.show_replies": "Antworten anzeigen",
|
||||
"home.explore_prompt.body": "Deine Startseite wird eine Mischung aus Beiträgen mit Hashtags und den Profilen, denen du folgst sowie den Beiträgen, die sie teilen, enthalten. Sollte es sich zu still anfühlen:",
|
||||
"home.explore_prompt.title": "Das ist dein Zuhause bei Mastodon.",
|
||||
"home.hide_announcements": "Ankündigungen ausblenden",
|
||||
"home.pending_critical_update.body": "Bitte aktualisiere deinen Mastodon-Server so schnell wie möglich!",
|
||||
"home.pending_critical_update.link": "Updates ansehen",
|
||||
"home.pending_critical_update.title": "Kritisches Sicherheitsupdate verfügbar!",
|
||||
"home.show_announcements": "Ankündigungen anzeigen",
|
||||
"interaction_modal.description.favourite": "Mit einem Mastodon-Konto kannst du diesen Beitrag favorisieren, um deine Wertschätzung auszudrücken, und ihn für einen späteren Zeitpunkt speichern.",
|
||||
"interaction_modal.description.follow": "Mit einem Mastodon-Konto kannst du {name} folgen, um die Beiträge auf deiner Startseite zu sehen.",
|
||||
|
|
@ -347,7 +352,7 @@
|
|||
"keyboard_shortcuts.mention": "Profil erwähnen",
|
||||
"keyboard_shortcuts.muted": "Liste stummgeschalteter Profile öffnen",
|
||||
"keyboard_shortcuts.my_profile": "Eigenes Profil aufrufen",
|
||||
"keyboard_shortcuts.notifications": "Mitteilungen aufrufen",
|
||||
"keyboard_shortcuts.notifications": "Benachrichtigungen aufrufen",
|
||||
"keyboard_shortcuts.open_media": "Medieninhalt öffnen",
|
||||
"keyboard_shortcuts.pinned": "Liste angehefteter Beiträge öffnen",
|
||||
"keyboard_shortcuts.profile": "Profil aufrufen",
|
||||
|
|
@ -355,7 +360,7 @@
|
|||
"keyboard_shortcuts.requests": "Liste der Follower-Anfragen aufrufen",
|
||||
"keyboard_shortcuts.search": "Suchleiste fokussieren",
|
||||
"keyboard_shortcuts.spoilers": "Feld für Inhaltswarnung anzeigen/ausblenden",
|
||||
"keyboard_shortcuts.start": "„Auf geht's!“ öffnen",
|
||||
"keyboard_shortcuts.start": "„Auf geht’s!“ öffnen",
|
||||
"keyboard_shortcuts.toggle_hidden": "Beitragstext hinter der Inhaltswarnung anzeigen/ausblenden",
|
||||
"keyboard_shortcuts.toggle_sensitivity": "Medien anzeigen/ausblenden",
|
||||
"keyboard_shortcuts.toot": "Neuen Beitrag erstellen",
|
||||
|
|
@ -379,7 +384,7 @@
|
|||
"lists.new.title_placeholder": "Titel der neuen Liste",
|
||||
"lists.replies_policy.followed": "Alle folgenden Profile",
|
||||
"lists.replies_policy.list": "Mitglieder der Liste",
|
||||
"lists.replies_policy.none": "Niemandem",
|
||||
"lists.replies_policy.none": "Niemanden",
|
||||
"lists.replies_policy.title": "Antworten anzeigen für:",
|
||||
"lists.search": "Suche nach Leuten, denen du folgst",
|
||||
"lists.subheading": "Deine Listen",
|
||||
|
|
@ -409,6 +414,7 @@
|
|||
"navigation_bar.lists": "Listen",
|
||||
"navigation_bar.logout": "Abmelden",
|
||||
"navigation_bar.mutes": "Stummgeschaltete Profile",
|
||||
"navigation_bar.opened_in_classic_interface": "Beiträge, Konten und andere bestimmte Seiten werden standardmäßig im klassischen Webinterface geöffnet.",
|
||||
"navigation_bar.personal": "Persönlich",
|
||||
"navigation_bar.pins": "Angeheftete Beiträge",
|
||||
"navigation_bar.preferences": "Einstellungen",
|
||||
|
|
@ -425,10 +431,10 @@
|
|||
"notification.own_poll": "Deine Umfrage ist beendet",
|
||||
"notification.poll": "Eine Umfrage, an der du teilgenommen hast, ist beendet",
|
||||
"notification.reblog": "{name} teilte deinen Beitrag",
|
||||
"notification.status": "{name} veröffentlichte gerade",
|
||||
"notification.status": "{name} hat gerade etwas gepostet",
|
||||
"notification.update": "{name} bearbeitete einen Beitrag",
|
||||
"notifications.clear": "Mitteilungen löschen",
|
||||
"notifications.clear_confirmation": "Möchtest du diese Mitteilungen für immer löschen?",
|
||||
"notifications.clear": "Benachrichtigungen löschen",
|
||||
"notifications.clear_confirmation": "Möchtest du wirklich alle Benachrichtigungen für immer löschen?",
|
||||
"notifications.column_settings.admin.report": "Neue Meldungen:",
|
||||
"notifications.column_settings.admin.sign_up": "Neue Registrierungen:",
|
||||
"notifications.column_settings.alert": "Desktop-Benachrichtigungen",
|
||||
|
|
@ -442,23 +448,23 @@
|
|||
"notifications.column_settings.poll": "Umfrageergebnisse:",
|
||||
"notifications.column_settings.push": "Push-Benachrichtigungen",
|
||||
"notifications.column_settings.reblog": "Geteilte Beiträge:",
|
||||
"notifications.column_settings.show": "In diesem Feed anzeigen",
|
||||
"notifications.column_settings.show": "In dieser Spalte anzeigen",
|
||||
"notifications.column_settings.sound": "Ton abspielen",
|
||||
"notifications.column_settings.status": "Neue Beiträge:",
|
||||
"notifications.column_settings.unread_notifications.category": "Ungelesene Benachrichtigungen",
|
||||
"notifications.column_settings.unread_notifications.highlight": "Ungelesene Mitteilungen markieren",
|
||||
"notifications.column_settings.unread_notifications.highlight": "Ungelesene Benachrichtigungen hervorheben",
|
||||
"notifications.column_settings.update": "Überarbeitete Beiträge:",
|
||||
"notifications.filter.all": "Alles",
|
||||
"notifications.filter.boosts": "Geteilte Beiträge",
|
||||
"notifications.filter.favourites": "Favoriten",
|
||||
"notifications.filter.follows": "Neue Follower",
|
||||
"notifications.filter.follows": "Folgt",
|
||||
"notifications.filter.mentions": "Erwähnungen",
|
||||
"notifications.filter.polls": "Umfrageergebnisse",
|
||||
"notifications.filter.statuses": "Neue Beiträge von Personen, denen du folgst",
|
||||
"notifications.grant_permission": "Berechtigung erteilen.",
|
||||
"notifications.group": "{count} Benachrichtigungen",
|
||||
"notifications.mark_as_read": "Alles als gelesen markieren",
|
||||
"notifications.permission_denied": "Desktop-Benachrichtigungen können nicht aktiviert werden, da die Berechtigung verweigert wurde.",
|
||||
"notifications.mark_as_read": "Alle Benachrichtigungen als gelesen markieren",
|
||||
"notifications.permission_denied": "Desktop-Benachrichtigungen können aufgrund einer zuvor verweigerten Berechtigung nicht aktiviert werden",
|
||||
"notifications.permission_denied_alert": "Desktop-Benachrichtigungen können nicht aktiviert werden, da die Browser-Berechtigung zuvor verweigert wurde",
|
||||
"notifications.permission_required": "Desktop-Benachrichtigungen sind nicht verfügbar, da die erforderliche Berechtigung nicht erteilt wurde.",
|
||||
"notifications_permission_banner.enable": "Aktiviere Desktop-Benachrichtigungen",
|
||||
|
|
@ -470,7 +476,7 @@
|
|||
"onboarding.actions.go_to_home": "Bring mich zu meiner Startseite",
|
||||
"onboarding.compose.template": "Hallo #Mastodon!",
|
||||
"onboarding.follows.empty": "Bedauerlicherweise können aktuell keine Ergebnisse angezeigt werden. Du kannst die Suche verwenden oder den Reiter „Entdecken“ auswählen, um neue Leute zum Folgen zu finden – oder du versuchst es später erneut.",
|
||||
"onboarding.follows.lead": "Deine Startseite ist der primäre Anlaufpunkt, um Mastodon zu erleben. Je mehr Profilen du folgst, umso aktiver und interessanter wird sie. Damit du direkt loslegen kannst, gibt es hier ein paar Empfehlungen:",
|
||||
"onboarding.follows.lead": "Deine Startseite ist der primäre Anlaufpunkt, um Mastodon zu erleben. Je mehr Profilen du folgst, umso aktiver und interessanter wird sie. Damit du direkt loslegen kannst, gibt es hier ein paar Vorschläge:",
|
||||
"onboarding.follows.title": "Personalisiere deine Startseite",
|
||||
"onboarding.share.lead": "Lass die Leute wissen, wie sie dich auf Mastodon finden können!",
|
||||
"onboarding.share.message": "Ich bin {username} auf #Mastodon! Folge mir auf {url}",
|
||||
|
|
@ -483,7 +489,7 @@
|
|||
"onboarding.steps.follow_people.title": "Personalisiere deine Startseite",
|
||||
"onboarding.steps.publish_status.body": "Begrüße die Welt mit Text, Fotos, Videos oder Umfragen {emoji}",
|
||||
"onboarding.steps.publish_status.title": "Erstelle deinen ersten Beitrag",
|
||||
"onboarding.steps.setup_profile.body": "Mit einem ausgefüllten Profil interagieren andere eher mit dir.",
|
||||
"onboarding.steps.setup_profile.body": "Mit einem vollständigen Profil interagieren andere eher mit dir.",
|
||||
"onboarding.steps.setup_profile.title": "Personalisiere dein Profil",
|
||||
"onboarding.steps.share_profile.body": "Lass deine Freund*innen wissen, wie sie dich auf Mastodon finden können",
|
||||
"onboarding.steps.share_profile.title": "Teile dein Mastodon-Profil",
|
||||
|
|
@ -521,7 +527,7 @@
|
|||
"relative_time.days": "{number} T.",
|
||||
"relative_time.full.days": "vor {number, plural, one {# Tag} other {# Tagen}}",
|
||||
"relative_time.full.hours": "vor {number, plural, one {# Stunde} other {# Stunden}}",
|
||||
"relative_time.full.just_now": "soeben",
|
||||
"relative_time.full.just_now": "gerade eben",
|
||||
"relative_time.full.minutes": "vor {number, plural, one {# Minute} other {# Minuten}}",
|
||||
"relative_time.full.seconds": "vor {number, plural, one {1 Sekunde} other {# Sekunden}}",
|
||||
"relative_time.hours": "{number} Std.",
|
||||
|
|
@ -532,6 +538,7 @@
|
|||
"reply_indicator.cancel": "Abbrechen",
|
||||
"report.block": "Blockieren",
|
||||
"report.block_explanation": "Du wirst keine Beiträge mehr von diesem Konto sehen. Das blockierte Konto wird deine Beiträge nicht mehr sehen oder dir folgen können. Die Person könnte mitbekommen, dass du sie blockiert hast.",
|
||||
"report.categories.legal": "Rechtlich",
|
||||
"report.categories.other": "Andere",
|
||||
"report.categories.spam": "Spam",
|
||||
"report.categories.violation": "Der Inhalt verletzt eine oder mehrere Serverregeln",
|
||||
|
|
@ -554,8 +561,8 @@
|
|||
"report.reasons.other": "Es ist etwas anderes",
|
||||
"report.reasons.other_description": "Der Vorfall passt zu keiner dieser Kategorien",
|
||||
"report.reasons.spam": "Das ist Spam",
|
||||
"report.reasons.spam_description": "Bösartige Links, gefälschtes Engagement oder wiederholte Antworten",
|
||||
"report.reasons.violation": "Es verstößt gegen Serverregeln",
|
||||
"report.reasons.spam_description": "Bösartige Links, gefälschtes Engagement oder sich wiederholende Antworten",
|
||||
"report.reasons.violation": "Das verstößt gegen Serverregeln",
|
||||
"report.reasons.violation_description": "Du bist dir sicher, dass eine bestimmte Regel gebrochen wurde",
|
||||
"report.rules.subtitle": "Wähle alle zutreffenden Inhalte aus",
|
||||
"report.rules.title": "Welche Regeln werden verletzt?",
|
||||
|
|
@ -583,22 +590,26 @@
|
|||
"search.quick_action.open_url": "URL in Mastodon öffnen",
|
||||
"search.quick_action.status_search": "Beiträge passend zu {x}",
|
||||
"search.search_or_paste": "Suchen oder URL einfügen",
|
||||
"search_popout.full_text_search_disabled_message": "Auf {domain} nicht verfügbar.",
|
||||
"search_popout.language_code": "ISO-Sprachcode",
|
||||
"search_popout.options": "Suchoptionen",
|
||||
"search_popout.quick_actions": "Schnellaktionen",
|
||||
"search_popout.recent": "Frühere Suchanfragen",
|
||||
"search_popout.specific_date": "genaues Datum",
|
||||
"search_popout.user": "Profil",
|
||||
"search_results.accounts": "Profile",
|
||||
"search_results.all": "Alles",
|
||||
"search_results.hashtags": "Hashtags",
|
||||
"search_results.nothing_found": "Nichts zu diesen Suchbegriffen gefunden",
|
||||
"search_results.see_all": "Alle ansehen",
|
||||
"search_results.statuses": "Beiträge",
|
||||
"search_results.statuses_fts_disabled": "Die Suche nach Beitragsinhalten ist auf diesem Mastodon-Server deaktiviert.",
|
||||
"search_results.title": "Suchergebnisse für {q}",
|
||||
"search_results.total": "{count, number} {count, plural, one {Ergebnis} other {Ergebnisse}}",
|
||||
"server_banner.about_active_users": "Personen, die diesen Server in den vergangenen 30 Tagen verwendet haben (monatlich aktive Nutzer*innen)",
|
||||
"server_banner.active_users": "aktive Profile",
|
||||
"server_banner.administered_by": "Verwaltet von:",
|
||||
"server_banner.introduction": "{domain} ist Teil eines dezentralisierten sozialen Netzwerks, angetrieben von {mastodon}.",
|
||||
"server_banner.learn_more": "Mehr erfahren",
|
||||
"server_banner.server_stats": "Serverstatistiken:",
|
||||
"server_banner.server_stats": "Serverstatistik:",
|
||||
"sign_in_banner.create_account": "Konto erstellen",
|
||||
"sign_in_banner.sign_in": "Anmelden",
|
||||
"sign_in_banner.sso_redirect": "Anmelden oder registrieren",
|
||||
|
|
@ -625,7 +636,7 @@
|
|||
"status.hide": "Beitrag ausblenden",
|
||||
"status.history.created": "{name} erstellte {date}",
|
||||
"status.history.edited": "{name} bearbeitete {date}",
|
||||
"status.load_more": "Weitere laden",
|
||||
"status.load_more": "Mehr laden",
|
||||
"status.media.open": "Zum Öffnen anklicken",
|
||||
"status.media.show": "Zum Anzeigen anklicken",
|
||||
"status.media_hidden": "Inhalt ausgeblendet",
|
||||
|
|
@ -640,7 +651,7 @@
|
|||
"status.reblog": "Teilen",
|
||||
"status.reblog_private": "Mit der ursprünglichen Zielgruppe teilen",
|
||||
"status.reblogged_by": "{name} teilte",
|
||||
"status.reblogs.empty": "Diesen Beitrag hat bisher noch niemand geteilt. Sobald es jemand tut, wird das Profil hier angezeigt.",
|
||||
"status.reblogs.empty": "Diesen Beitrag hat bisher noch niemand geteilt. Sobald es jemand tut, wird das Profil hier erscheinen.",
|
||||
"status.redraft": "Löschen und neu erstellen",
|
||||
"status.remove_bookmark": "Lesezeichen entfernen",
|
||||
"status.replied_to": "Antwortete {name}",
|
||||
|
|
@ -651,9 +662,9 @@
|
|||
"status.share": "Teilen",
|
||||
"status.show_filter_reason": "Trotzdem anzeigen",
|
||||
"status.show_less": "Weniger anzeigen",
|
||||
"status.show_less_all": "Alle Inhaltswarnungen zuklappen",
|
||||
"status.show_less_all": "Alles einklappen",
|
||||
"status.show_more": "Mehr anzeigen",
|
||||
"status.show_more_all": "Alle Inhaltswarnungen aufklappen",
|
||||
"status.show_more_all": "Alles ausklappen",
|
||||
"status.show_original": "Ursprünglichen Beitrag anzeigen",
|
||||
"status.title.with_attachments": "{user} veröffentlichte {attachmentCount, plural, one {ein Medium} other {{attachmentCount} Medien}}",
|
||||
"status.translate": "Übersetzen",
|
||||
|
|
@ -664,10 +675,8 @@
|
|||
"subscribed_languages.lead": "Nach der Änderung werden nur noch Beiträge in den ausgewählten Sprachen in den Timelines deiner Startseite und deiner Listen angezeigt. Wähle keine Sprache aus, um alle Beiträge zu sehen.",
|
||||
"subscribed_languages.save": "Änderungen speichern",
|
||||
"subscribed_languages.target": "Abonnierte Sprachen für {target} ändern",
|
||||
"suggestions.dismiss": "Vorschlag ablehnen",
|
||||
"suggestions.header": "Du bist möglicherweise interessiert an …",
|
||||
"tabs_bar.home": "Startseite",
|
||||
"tabs_bar.notifications": "Mitteilungen",
|
||||
"tabs_bar.notifications": "Benachrichtigungen",
|
||||
"time_remaining.days": "noch {number, plural, one {# Tag} other {# Tage}}",
|
||||
"time_remaining.hours": "noch {number, plural, one {# Stunde} other {# Stunden}}",
|
||||
"time_remaining.minutes": "noch {number, plural, one {# Minute} other {# Minuten}}",
|
||||
|
|
@ -675,7 +684,7 @@
|
|||
"time_remaining.seconds": "noch {number, plural, one {# Sekunde} other {# Sekunden}}",
|
||||
"timeline_hint.remote_resource_not_displayed": "{resource} von anderen Servern werden nicht angezeigt.",
|
||||
"timeline_hint.resources.followers": "Follower",
|
||||
"timeline_hint.resources.follows": "Folge ich",
|
||||
"timeline_hint.resources.follows": "„Folge ich“",
|
||||
"timeline_hint.resources.statuses": "Ältere Beiträge",
|
||||
"trends.counter_by_accounts": "{count, plural, one {{counter} Profil} other {{counter} Profile}} {days, plural, one {seit gestern} other {in {days} Tagen}}",
|
||||
"trends.trending_now": "Aktuelle Trends",
|
||||
|
|
@ -708,7 +717,7 @@
|
|||
"upload_progress.processing": "Wird verarbeitet…",
|
||||
"username.taken": "Dieser Profilname ist vergeben. Versuche einen anderen",
|
||||
"video.close": "Video schließen",
|
||||
"video.download": "Video-Datei herunterladen",
|
||||
"video.download": "Datei herunterladen",
|
||||
"video.exit_fullscreen": "Vollbild verlassen",
|
||||
"video.expand": "Video vergrößern",
|
||||
"video.fullscreen": "Vollbild",
|
||||
|
|
@ -716,5 +725,5 @@
|
|||
"video.mute": "Stummschalten",
|
||||
"video.pause": "Pausieren",
|
||||
"video.play": "Abspielen",
|
||||
"video.unmute": "Ton einschalten"
|
||||
"video.unmute": "Stummschaltung aufheben"
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue