Merge branch 'origin/master' into sync/upstream

Conflicts:
	app/javascript/mastodon/components/status_list.js
	app/javascript/mastodon/features/notifications/index.js
	app/javascript/mastodon/features/ui/components/modal_root.js
	app/javascript/mastodon/features/ui/components/onboarding_modal.js
	app/javascript/mastodon/features/ui/index.js
	app/javascript/styles/about.scss
	app/javascript/styles/accounts.scss
	app/javascript/styles/components.scss
	app/presenters/instance_presenter.rb
	app/services/post_status_service.rb
	app/services/reblog_service.rb
	app/views/about/more.html.haml
	app/views/about/show.html.haml
	app/views/accounts/_header.html.haml
	config/webpack/loaders/babel.js
	spec/controllers/api/v1/accounts/credentials_controller_spec.rb
This commit is contained in:
David Yip 2017-09-09 14:27:47 -05:00
commit b9f7bc149b
352 changed files with 8629 additions and 2380 deletions

View File

@ -49,6 +49,7 @@ rules:
- warn - warn
- allow: - allow:
- error - error
- warn
no-fallthrough: error no-fallthrough: error
no-irregular-whitespace: error no-irregular-whitespace: error
no-mixed-spaces-and-tabs: warn no-mixed-spaces-and-tabs: warn

View File

@ -10,6 +10,7 @@ AllCops:
- 'node_modules/**/*' - 'node_modules/**/*'
- 'Vagrantfile' - 'Vagrantfile'
- 'vendor/**/*' - 'vendor/**/*'
- 'lib/json_ld/*'
Bundler/OrderedGems: Bundler/OrderedGems:
Enabled: false Enabled: false

15
CODEOWNERS Normal file
View File

@ -0,0 +1,15 @@
# CODEOWNERS for tootsuite/mastodon
# Translators
# To add translator, copy these lines, replace `fr` with appropriate language code and replace `@żelipapą` with user's GitHub nickname preceded by `@` sign or e-mail address.
# /app/javascript/mastodon/locales/fr.json @żelipapą
# /app/views/user_mailer/*.fr.html.erb @żelipapą
# /app/views/user_mailer/*.fr.text.erb @żelipapą
# /config/locales/*.fr.yml @żelipapą
# /config/locales/fr.yml @żelipapą
/app/javascript/mastodon/locales/pl.json @m4sk1n
/app/views/user_mailer/*.pl.html.erb @m4sk1n
/app/views/user_mailer/*.pl.text.erb @m4sk1n
/config/locales/*.pl.yml @m4sk1n
/config/locales/pl.yml @m4sk1n

View File

@ -1,4 +1,4 @@
FROM ruby:2.4.1-alpine FROM ruby:2.4.1-alpine3.6
LABEL maintainer="https://github.com/tootsuite/mastodon" \ LABEL maintainer="https://github.com/tootsuite/mastodon" \
description="A GNU Social-compatible microblogging server" description="A GNU Social-compatible microblogging server"
@ -14,9 +14,7 @@ EXPOSE 3000 4000
WORKDIR /mastodon WORKDIR /mastodon
RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \ RUN apk -U upgrade \
&& echo "@edge https://nl.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \
&& apk -U upgrade \
&& apk add -t build-dependencies \ && apk add -t build-dependencies \
build-base \ build-base \
icu-dev \ icu-dev \
@ -31,15 +29,15 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit
file \ file \
git \ git \
icu-libs \ icu-libs \
imagemagick@edge \ imagemagick \
libidn \ libidn \
libpq \ libpq \
nodejs-npm@edge \ nodejs-npm \
nodejs@edge \ nodejs \
protobuf \ protobuf \
su-exec \ su-exec \
tini \ tini \
yarn@edge \ yarn \
&& update-ca-certificates \ && update-ca-certificates \
&& wget -O libiconv.tar.gz "http://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \ && wget -O libiconv.tar.gz "http://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \
&& echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \ && echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \

View File

@ -22,7 +22,7 @@ gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.5' gem 'addressable', '~> 2.5'
gem 'bootsnap' gem 'bootsnap'
gem 'browser' gem 'browser'
gem 'charlock_holmes', '~> 0.7.3' gem 'charlock_holmes', '~> 0.7.5'
gem 'cld3', '~> 3.1' gem 'cld3', '~> 3.1'
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
gem 'devise-two-factor', '~> 3.0' gem 'devise-two-factor', '~> 3.0'
@ -68,6 +68,9 @@ gem 'tzinfo-data', '~> 1.2017'
gem 'webpacker', '~> 2.0' gem 'webpacker', '~> 2.0'
gem 'webpush' gem 'webpush'
gem 'json-ld-preloaded', '~> 2.2.1'
gem 'rdf-normalize', '~> 0.3.1'
group :development, :test do group :development, :test do
gem 'fabrication', '~> 2.16' gem 'fabrication', '~> 2.16'
gem 'fuubar', '~> 2.2' gem 'fuubar', '~> 2.2'

View File

@ -44,8 +44,8 @@ GEM
i18n (~> 0.7) i18n (~> 0.7)
minitest (~> 5.1) minitest (~> 5.1)
tzinfo (~> 1.1) tzinfo (~> 1.1)
addressable (2.5.1) addressable (2.5.2)
public_suffix (~> 2.0, >= 2.0.2) public_suffix (>= 2.0.2, < 4.0)
airbrussh (1.3.0) airbrussh (1.3.0)
sshkit (>= 1.6.1, != 1.7.0) sshkit (>= 1.6.1, != 1.7.0)
annotate (2.7.2) annotate (2.7.2)
@ -74,13 +74,13 @@ GEM
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootsnap (1.1.2) bootsnap (1.1.2)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (3.6.2) brakeman (3.7.2)
browser (2.4.0) browser (2.4.0)
builder (3.2.3) builder (3.2.3)
bullet (5.5.1) bullet (5.5.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0) uniform_notifier (~> 1.10.0)
bundler-audit (0.5.0) bundler-audit (0.6.0)
bundler (~> 1.2) bundler (~> 1.2)
thor (~> 0.18) thor (~> 0.18)
capistrano (3.8.2) capistrano (3.8.2)
@ -108,7 +108,7 @@ GEM
xpath (~> 2.0) xpath (~> 2.0)
case_transform (0.2) case_transform (0.2)
activesupport activesupport
charlock_holmes (0.7.3) charlock_holmes (0.7.5)
chunky_png (1.3.8) chunky_png (1.3.8)
cld3 (3.1.3) cld3 (3.1.3)
ffi (>= 1.1.0, < 1.10.0) ffi (>= 1.1.0, < 1.10.0)
@ -179,6 +179,8 @@ GEM
activesupport (>= 4.0.1) activesupport (>= 4.0.1)
hamlit (>= 1.2.0) hamlit (>= 1.2.0)
railties (>= 4.0.1) railties (>= 4.0.1)
hamster (3.0.0)
concurrent-ruby (~> 1.0)
hashdiff (0.3.5) hashdiff (0.3.5)
highline (1.7.8) highline (1.7.8)
hiredis (0.6.1) hiredis (0.6.1)
@ -211,6 +213,13 @@ GEM
idn-ruby (0.1.0) idn-ruby (0.1.0)
jmespath (1.3.1) jmespath (1.3.1)
json (2.1.0) json (2.1.0)
json-ld (2.1.5)
multi_json (~> 1.12)
rdf (~> 2.2)
json-ld-preloaded (2.2.1)
json-ld (~> 2.1, >= 2.1.5)
multi_json (~> 1.11)
rdf (~> 2.2)
jsonapi-renderer (0.1.3) jsonapi-renderer (0.1.3)
jwt (1.5.6) jwt (1.5.6)
kaminari (1.0.1) kaminari (1.0.1)
@ -298,7 +307,7 @@ GEM
slop (~> 3.4) slop (~> 3.4)
pry-rails (0.3.6) pry-rails (0.3.6)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (2.0.5) public_suffix (3.0.0)
puma (3.9.1) puma (3.9.1)
pundit (1.1.0) pundit (1.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -348,6 +357,11 @@ GEM
rainbow (2.2.2) rainbow (2.2.2)
rake rake
rake (12.0.0) rake (12.0.0)
rdf (2.2.8)
hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.2)
rdf (~> 2.0)
redis (3.3.3) redis (3.3.3)
redis-actionpack (5.0.1) redis-actionpack (5.0.1)
actionpack (>= 4.0, < 6) actionpack (>= 4.0, < 6)
@ -454,7 +468,7 @@ GEM
temple (0.8.0) temple (0.8.0)
terminal-table (1.8.0) terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1) unicode-display_width (~> 1.1, >= 1.1.1)
thor (0.19.4) thor (0.20.0)
thread (0.2.2) thread (0.2.2)
thread_safe (0.3.6) thread_safe (0.3.6)
tilt (2.0.8) tilt (2.0.8)
@ -511,7 +525,7 @@ DEPENDENCIES
capistrano-rbenv (~> 2.1) capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0) capistrano-yarn (~> 2.0)
capybara (~> 2.14) capybara (~> 2.14)
charlock_holmes (~> 0.7.3) charlock_holmes (~> 0.7.5)
cld3 (~> 3.1) cld3 (~> 3.1)
climate_control (~> 0.2) climate_control (~> 0.2)
devise (~> 4.2) devise (~> 4.2)
@ -531,6 +545,7 @@ DEPENDENCIES
httplog (~> 0.99) httplog (~> 0.99)
i18n-tasks (~> 0.9) i18n-tasks (~> 0.9)
idn-ruby idn-ruby
json-ld-preloaded (~> 2.2.1)
kaminari (~> 1.0) kaminari (~> 1.0)
letter_opener (~> 1.4) letter_opener (~> 1.4)
letter_opener_web (~> 1.3) letter_opener_web (~> 1.3)
@ -560,6 +575,7 @@ DEPENDENCIES
rails-controller-testing (~> 1.0) rails-controller-testing (~> 1.0)
rails-i18n (~> 5.0) rails-i18n (~> 5.0)
rails-settings-cached (~> 0.6) rails-settings-cached (~> 0.6)
rdf-normalize (~> 0.3.1)
redis (~> 3.3) redis (~> 3.3)
redis-namespace (~> 1.5) redis-namespace (~> 1.5)
redis-rails (~> 5.0) redis-rails (~> 5.0)
@ -590,4 +606,4 @@ RUBY VERSION
ruby 2.4.1p111 ruby 2.4.1p111
BUNDLED WITH BUNDLED WITH
1.15.3 1.15.4

View File

@ -7,8 +7,17 @@ class AccountsController < ApplicationController
def show def show
respond_to do |format| respond_to do |format|
format.html do format.html do
@statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(20, params[:max_id], params[:since_id]) @pinned_statuses = []
@statuses = cache_collection(@statuses, Status)
if current_account && @account.blocking?(current_account)
@statuses = []
return
end
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) unless media_requested?
@statuses = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses, Status)
@next_url = next_url unless @statuses.empty?
end end
format.atom do format.atom do
@ -17,14 +26,55 @@ class AccountsController < ApplicationController
end end
format.json do format.json do
render json: @account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter render json: @account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end end
end end
end end
private private
def filtered_statuses
default_statuses.tap do |statuses|
statuses.merge!(only_media_scope) if media_requested?
statuses.merge!(no_replies_scope) unless replies_requested?
end
end
def default_statuses
@account.statuses.where(visibility: [:public, :unlisted])
end
def only_media_scope
Status.where(id: account_media_status_ids)
end
def account_media_status_ids
@account.media_attachments.attached.reorder(nil).select(:status_id).distinct
end
def no_replies_scope
Status.without_replies
end
def set_account def set_account
@account = Account.find_local!(params[:username]) @account = Account.find_local!(params[:username])
end end
def next_url
if media_requested?
short_account_media_url(@account, max_id: @statuses.last.id)
elsif replies_requested?
short_account_with_replies_url(@account, max_id: @statuses.last.id)
else
short_account_url(@account, max_id: @statuses.last.id)
end
end
def media_requested?
request.path.ends_with?('/media')
end
def replies_requested?
request.path.ends_with?('/with_replies')
end
end end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
class ActivityPub::InboxesController < Api::BaseController
include SignatureVerification
before_action :set_account
def create
if signed_request_account
upgrade_account
process_payload
head 201
else
head 202
end
end
private
def set_account
@account = Account.find_local!(params[:account_username]) if params[:account_username]
end
def body
@body ||= request.body.read
end
def upgrade_account
return unless signed_request_account.subscribed?
Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id)
end
def process_payload
ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body.force_encoding('UTF-8'))
end
end

View File

@ -7,7 +7,7 @@ class ActivityPub::OutboxesController < Api::BaseController
@statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(20, params[:max_id], params[:since_id]) @statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(20, params[:max_id], params[:since_id])
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)
render json: outbox_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter render json: outbox_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end end
private private

View File

@ -17,7 +17,7 @@ module Admin
end end
def unsubscribe def unsubscribe
UnsubscribeService.new.call(@account) Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end

View File

@ -9,7 +9,7 @@ module Admin
before_action :set_account before_action :set_account
before_action :set_status, only: [:update, :destroy] before_action :set_status, only: [:update, :destroy]
PAR_PAGE = 20 PER_PAGE = 20
def index def index
@statuses = @account.statuses @statuses = @account.statuses
@ -17,7 +17,7 @@ module Admin
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
@statuses.merge!(Status.where(id: account_media_status_ids)) @statuses.merge!(Status.where(id: account_media_status_ids))
end end
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PAR_PAGE) @statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
@form = Form::StatusBatch.new @form = Form::StatusBatch.new
end end

View File

@ -43,7 +43,7 @@ class Api::BaseController < ApplicationController
links = [] links = []
links << [next_path, [%w(rel next)]] if next_path links << [next_path, [%w(rel next)]] if next_path
links << [prev_path, [%w(rel prev)]] if prev_path links << [prev_path, [%w(rel prev)]] if prev_path
response.headers['Link'] = LinkHeader.new(links) response.headers['Link'] = LinkHeader.new(links) unless links.empty?
end end
def limit_param(default_limit) def limit_param(default_limit)
@ -62,10 +62,11 @@ class Api::BaseController < ApplicationController
end end
def require_user! def require_user!
current_resource_owner if current_user
set_user_activity set_user_activity
rescue ActiveRecord::RecordNotFound else
render json: { error: 'This method requires an authenticated user' }, status: 422 render json: { error: 'This method requires an authenticated user' }, status: 422
end
end end
def render_empty def render_empty

View File

@ -4,14 +4,14 @@ class Api::OEmbedController < Api::BaseController
respond_to :json respond_to :json
def show def show
@stream_entry = find_stream_entry.stream_entry @status = status_finder.status
render json: @stream_entry, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default render json: @status, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default
end end
private private
def find_stream_entry def status_finder
StreamEntryFinder.new(params[:url]) StatusFinder.new(params[:url])
end end
def maxwidth_or_default def maxwidth_or_default

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Accounts::CredentialsController < Api::BaseController class Api::V1::Accounts::CredentialsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }, except: [:update]
before_action -> { doorkeeper_authorize! :write }, only: [:update] before_action -> { doorkeeper_authorize! :write }, only: [:update]
before_action :require_user! before_action :require_user!
@ -10,8 +11,9 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
end end
def update def update
current_account.update!(account_params)
@account = current_account @account = current_account
UpdateAccountService.new.call(@account, account_params, raise_error: true)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
render json: @account, serializer: REST::CredentialAccountSerializer render json: @account, serializer: REST::CredentialAccountSerializer
end end

View File

@ -29,6 +29,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
def account_statuses def account_statuses
default_statuses.tap do |statuses| default_statuses.tap do |statuses|
statuses.merge!(only_media_scope) if params[:only_media] statuses.merge!(only_media_scope) if params[:only_media]
statuses.merge!(pinned_scope) if params[:pinned]
statuses.merge!(no_replies_scope) if params[:exclude_replies] statuses.merge!(no_replies_scope) if params[:exclude_replies]
end end
end end
@ -53,6 +54,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
@account.media_attachments.attached.reorder(nil).select(:status_id).distinct @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
end end
def pinned_scope
@account.pinned_statuses
end
def no_replies_scope def no_replies_scope
Status.without_replies Status.without_replies
end end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
class Api::V1::Statuses::PinsController < Api::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write }
before_action :require_user!
before_action :set_status
respond_to :json
def create
StatusPin.create!(account: current_account, status: @status)
render json: @status, serializer: REST::StatusSerializer
end
def destroy
pin = StatusPin.find_by(account: current_account, status: @status)
pin&.destroy!
render json: @status, serializer: REST::StatusSerializer
end
private
def set_status
@status = Status.find(params[:status_id])
end
end

View File

@ -29,7 +29,7 @@ class Api::V1::StatusesController < Api::BaseController
end end
def card def card
@card = PreviewCard.find_by(status: @status) @card = @status.preview_cards.first
if @card.nil? if @card.nil?
render_empty render_empty

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class Api::Web::EmbedsController < Api::BaseController
respond_to :json
before_action :require_user!
def create
status = StatusFinder.new(params[:url]).status
render json: status, serializer: OEmbedSerializer, width: 400
rescue ActiveRecord::RecordNotFound
oembed = OEmbed::Providers.get(params[:url])
render json: Oj.dump(oembed.fields)
rescue OEmbed::NotFound
render json: {}, status: :not_found
end
end

View File

@ -23,6 +23,7 @@ module AccountControllerConcern
[ [
webfinger_account_link, webfinger_account_link,
atom_account_url_link, atom_account_url_link,
actor_url_link,
] ]
) )
end end
@ -41,6 +42,13 @@ module AccountControllerConcern
] ]
end end
def actor_url_link
[
ActivityPub::TagManager.instance.uri_for(@account),
[%w(rel alternate), %w(type application/activity+json)],
]
end
def webfinger_account_url def webfinger_account_url
webfinger_url(resource: @account.to_webfinger_s) webfinger_url(resource: @account.to_webfinger_s)
end end

View File

@ -31,7 +31,7 @@ module SignatureVerification
return return
end end
account = ResolveRemoteAccountService.new.call(signature_params['keyId'].gsub(/\Aacct:/, '')) account = account_from_key_id(signature_params['keyId'])
if account.nil? if account.nil?
@signed_request_account = nil @signed_request_account = nil
@ -49,6 +49,10 @@ module SignatureVerification
end end
end end
def request_body
@request_body ||= request.raw_post
end
private private
def build_signed_string(signed_headers) def build_signed_string(signed_headers)
@ -57,6 +61,8 @@ module SignatureVerification
signed_headers.split(' ').map do |signed_header| signed_headers.split(' ').map do |signed_header|
if signed_header == Request::REQUEST_TARGET if signed_header == Request::REQUEST_TARGET
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
elsif signed_header == 'digest'
"digest: #{body_digest}"
else else
"#{signed_header}: #{request.headers[to_header_name(signed_header)]}" "#{signed_header}: #{request.headers[to_header_name(signed_header)]}"
end end
@ -73,6 +79,10 @@ module SignatureVerification
(Time.now.utc - time_sent).abs <= 30 (Time.now.utc - time_sent).abs <= 30
end end
def body_digest
"SHA-256=#{Digest::SHA256.base64digest(request_body)}"
end
def to_header_name(name) def to_header_name(name)
name.split(/-/).map(&:capitalize).join('-') name.split(/-/).map(&:capitalize).join('-')
end end
@ -81,7 +91,16 @@ module SignatureVerification
signature_params['keyId'].blank? || signature_params['keyId'].blank? ||
signature_params['signature'].blank? || signature_params['signature'].blank? ||
signature_params['algorithm'].blank? || signature_params['algorithm'].blank? ||
signature_params['algorithm'] != 'rsa-sha256' || signature_params['algorithm'] != 'rsa-sha256'
!signature_params['keyId'].start_with?('acct:') end
def account_from_key_id(key_id)
if key_id.start_with?('acct:')
ResolveRemoteAccountService.new.call(key_id.gsub(/\Aacct:/, ''))
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
account ||= ActivityPub::FetchRemoteKeyService.new.call(key_id)
account
end
end end
end end

View File

@ -10,7 +10,7 @@ class FollowerAccountsController < ApplicationController
format.html format.html
format.json do format.json do
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end end
end end
end end

View File

@ -10,7 +10,7 @@ class FollowingAccountsController < ApplicationController
format.html format.html
format.json do format.json do
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end end
end end
end end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class IntentsController < ApplicationController
def show
uri = Addressable::URI.parse(params[:uri])
if uri.scheme == 'web+mastodon'
case uri.host
when 'follow'
return redirect_to authorize_follow_path(acct: uri.query_values['uri'].gsub(/\Aacct:/, ''))
when 'share'
return redirect_to share_path(text: uri.query_values['text'])
end
end
not_found
end
end

View File

@ -0,0 +1,72 @@
# frozen_string_literal: true
class Settings::ApplicationsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
before_action :set_application, only: [:show, :update, :destroy, :regenerate]
before_action :prepare_scopes, only: [:create, :update]
def index
@applications = current_user.applications.page(params[:page])
end
def new
@application = Doorkeeper::Application.new(
redirect_uri: Doorkeeper.configuration.native_redirect_uri,
scopes: 'read write follow'
)
end
def show; end
def create
@application = current_user.applications.build(application_params)
if @application.save
redirect_to settings_applications_path, notice: I18n.t('applications.created')
else
render :new
end
end
def update
if @application.update(application_params)
redirect_to settings_applications_path, notice: I18n.t('generic.changes_saved_msg')
else
render :show
end
end
def destroy
@application.destroy
redirect_to settings_applications_path, notice: I18n.t('applications.destroyed')
end
def regenerate
@access_token = current_user.token_for_app(@application)
@access_token.destroy
redirect_to settings_application_path(@application), notice: I18n.t('applications.token_regenerated')
end
private
def set_application
@application = current_user.applications.find(params[:id])
end
def application_params
params.require(:doorkeeper_application).permit(
:name,
:redirect_uri,
:scopes,
:website
)
end
def prepare_scopes
scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil)
params[:doorkeeper_application][:scopes] = scopes.join(' ') if scopes.is_a? Array
end
end

View File

@ -14,7 +14,8 @@ class Settings::ProfilesController < ApplicationController
def show; end def show; end
def update def update
if @account.update(account_params) if UpdateAccountService.new.call(@account, account_params)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg') redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg')
else else
render :show render :show

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
class SharesController < ApplicationController
layout 'modal'
before_action :authenticate_user!
before_action :set_body_classes
def show
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
@initial_state_json = serializable_resource.to_json
end
private
def initial_state_params
{
settings: Web::Setting.find_by(user: current_user)&.data || {},
push_subscription: current_account.user.web_push_subscription(current_session),
current_account: current_account,
token: current_session.token,
admin: Account.find_local(Setting.site_contact_username),
text: params[:text],
}
end
def set_body_classes
@body_classes = 'compose-standalone'
end
end

View File

@ -9,6 +9,7 @@ class StatusesController < ApplicationController
before_action :set_status before_action :set_status
before_action :set_link_headers before_action :set_link_headers
before_action :check_account_suspension before_action :check_account_suspension
before_action :redirect_to_original, only: [:show]
def show def show
respond_to do |format| respond_to do |format|
@ -20,13 +21,18 @@ class StatusesController < ApplicationController
end end
format.json do format.json do
render json: @status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter render json: @status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end end
end end
end end
def activity def activity
render json: @status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter render json: @status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
def embed
response.headers['X-Frame-Options'] = 'ALLOWALL'
render 'stream_entries/embed', layout: 'embedded'
end end
private private
@ -36,7 +42,12 @@ class StatusesController < ApplicationController
end end
def set_link_headers def set_link_headers
response.headers['Link'] = LinkHeader.new([[account_stream_entry_url(@account, @status.stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]]]) response.headers['Link'] = LinkHeader.new(
[
[account_stream_entry_url(@account, @status.stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]],
[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]],
]
)
end end
def set_status def set_status
@ -53,4 +64,8 @@ class StatusesController < ApplicationController
def check_account_suspension def check_account_suspension
gone if @account.suspended? gone if @account.suspended?
end end
def redirect_to_original
redirect_to ::TagManager.instance.url_for(@status.reblog) if @status.reblog?
end
end end

View File

@ -25,10 +25,7 @@ class StreamEntriesController < ApplicationController
end end
def embed def embed
response.headers['X-Frame-Options'] = 'ALLOWALL' redirect_to embed_short_account_status_url(@account, @stream_entry.activity), status: 301
return gone if @stream_entry.activity.nil?
render layout: 'embedded'
end end
private private
@ -38,7 +35,12 @@ class StreamEntriesController < ApplicationController
end end
def set_link_headers def set_link_headers
response.headers['Link'] = LinkHeader.new([[account_stream_entry_url(@account, @stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]]]) response.headers['Link'] = LinkHeader.new(
[
[account_stream_entry_url(@account, @stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]],
[ActivityPub::TagManager.instance.uri_for(@stream_entry.activity), [%w(rel alternate), %w(type application/activity+json)]],
]
)
end end
def set_stream_entry def set_stream_entry

View File

@ -12,7 +12,7 @@ class TagsController < ApplicationController
format.html format.html
format.json do format.json do
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end end
end end
end end

View File

@ -5,6 +5,10 @@ module ApplicationHelper
current_page?(path) ? 'active' : '' current_page?(path) ? 'active' : ''
end end
def active_link_to(label, path, options = {})
link_to label, path, options.merge(class: active_nav_class(path))
end
def show_landing_strip? def show_landing_strip?
!user_signed_in? && !single_user_mode? !user_signed_in? && !single_user_mode?
end end

View File

@ -0,0 +1,52 @@
# frozen_string_literal: true
module JsonLdHelper
def equals_or_includes?(haystack, needle)
haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle
end
def first_of_value(value)
value.is_a?(Array) ? value.first : value
end
def value_or_id(value)
value.is_a?(String) || value.nil? ? value : value['id']
end
def supported_context?(json)
!json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT)
end
def canonicalize(json)
graph = RDF::Graph.new << JSON::LD::API.toRdf(json)
graph.dump(:normalize)
end
def fetch_resource(uri)
response = build_request(uri).perform
return if response.code != 200
body_to_json(response.to_s)
end
def body_to_json(body)
body.is_a?(String) ? Oj.load(body, mode: :strict) : body
rescue Oj::ParseError
nil
end
def merge_context(context, new_context)
if context.is_a?(Array)
context << new_context
else
[context, new_context]
end
end
private
def build_request(uri)
request = Request.new(:get, uri)
request.add_headers('Accept' => 'application/activity+json, application/ld+json')
request
end
end

View File

@ -12,6 +12,8 @@ module RoutingHelper
end end
def full_asset_url(source, options = {}) def full_asset_url(source, options = {})
Rails.configuration.x.use_s3 ? source : URI.join(root_url, ActionController::Base.helpers.asset_url(source, options)).to_s source = ActionController::Base.helpers.asset_url(source, options) unless Rails.configuration.x.use_s3
URI.join(root_url, source).to_s
end end
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
module StreamEntriesHelper module StreamEntriesHelper
EMBEDDED_CONTROLLER = 'stream_entries' EMBEDDED_CONTROLLER = 'statuses'
EMBEDDED_ACTION = 'embed' EMBEDDED_ACTION = 'embed'
def display_name(account) def display_name(account)

View File

@ -24,6 +24,14 @@ export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS'; export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL'; export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
export const PIN_REQUEST = 'PIN_REQUEST';
export const PIN_SUCCESS = 'PIN_SUCCESS';
export const PIN_FAIL = 'PIN_FAIL';
export const UNPIN_REQUEST = 'UNPIN_REQUEST';
export const UNPIN_SUCCESS = 'UNPIN_SUCCESS';
export const UNPIN_FAIL = 'UNPIN_FAIL';
export function reblog(status) { export function reblog(status) {
return function (dispatch, getState) { return function (dispatch, getState) {
dispatch(reblogRequest(status)); dispatch(reblogRequest(status));
@ -233,3 +241,73 @@ export function fetchFavouritesFail(id, error) {
error, error,
}; };
}; };
export function pin(status) {
return (dispatch, getState) => {
dispatch(pinRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
dispatch(pinSuccess(status, response.data));
}).catch(error => {
dispatch(pinFail(status, error));
});
};
};
export function pinRequest(status) {
return {
type: PIN_REQUEST,
status,
};
};
export function pinSuccess(status, response) {
return {
type: PIN_SUCCESS,
status,
response,
};
};
export function pinFail(status, error) {
return {
type: PIN_FAIL,
status,
error,
};
};
export function unpin (status) {
return (dispatch, getState) => {
dispatch(unpinRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
dispatch(unpinSuccess(status, response.data));
}).catch(error => {
dispatch(unpinFail(status, error));
});
};
};
export function unpinRequest(status) {
return {
type: UNPIN_REQUEST,
status,
};
};
export function unpinSuccess(status, response) {
return {
type: UNPIN_SUCCESS,
status,
response,
};
};
export function unpinFail(status, error) {
return {
type: UNPIN_FAIL,
status,
error,
};
};

View File

@ -0,0 +1,94 @@
import createStream from '../stream';
import {
updateTimeline,
deleteFromTimelines,
refreshHomeTimeline,
connectTimeline,
disconnectTimeline,
} from './timelines';
import { updateNotifications, refreshNotifications } from './notifications';
import { getLocale } from '../locales';
const { messages } = getLocale();
export function connectTimelineStream (timelineId, path, pollingRefresh = null) {
return (dispatch, getState) => {
const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
const accessToken = getState().getIn(['meta', 'access_token']);
const locale = getState().getIn(['meta', 'locale']);
let polling = null;
const setupPolling = () => {
polling = setInterval(() => {
pollingRefresh(dispatch);
}, 20000);
};
const clearPolling = () => {
if (polling) {
clearInterval(polling);
polling = null;
}
};
const subscription = createStream(streamingAPIBaseURL, accessToken, path, {
connected () {
if (pollingRefresh) {
clearPolling();
}
dispatch(connectTimeline(timelineId));
},
disconnected () {
if (pollingRefresh) {
setupPolling();
}
dispatch(disconnectTimeline(timelineId));
},
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline(timelineId, JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
case 'notification':
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
break;
}
},
reconnected () {
if (pollingRefresh) {
clearPolling();
pollingRefresh(dispatch);
}
dispatch(connectTimeline(timelineId));
},
});
const disconnect = () => {
if (subscription) {
subscription.close();
}
clearPolling();
};
return disconnect;
};
}
function refreshHomeTimelineAndNotification (dispatch) {
dispatch(refreshHomeTimeline());
dispatch(refreshNotifications());
}
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
export const connectCommunityStream = () => connectTimelineStream('community', 'public:local');
export const connectMediaStream = () => connectTimelineStream('community', 'public:local');
export const connectPublicStream = () => connectTimelineStream('public', 'public');
export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);

View File

@ -26,6 +26,7 @@ export default class Account extends ImmutablePureComponent {
onBlock: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
hidden: PropTypes.bool,
}; };
handleFollow = () => { handleFollow = () => {
@ -41,12 +42,21 @@ export default class Account extends ImmutablePureComponent {
} }
render () { render () {
const { account, me, intl } = this.props; const { account, me, intl, hidden } = this.props;
if (!account) { if (!account) {
return <div />; return <div />;
} }
if (hidden) {
return (
<div>
{account.get('display_name')}
{account.get('username')}
</div>
);
}
let buttons; let buttons;
if (account.get('id') !== me && account.get('relationship', null) !== null) { if (account.get('id') !== me && account.get('relationship', null) !== null) {

View File

@ -33,7 +33,7 @@ export default class Column extends React.PureComponent {
} }
componentDidMount () { componentDidMount () {
this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents ? { passive: true } : false); this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
} }
componentWillUnmount () { componentWillUnmount () {

View File

@ -0,0 +1,122 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
export default class IntersectionObserverArticle extends ImmutablePureComponent {
static propTypes = {
intersectionObserverWrapper: PropTypes.object,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
children: PropTypes.node,
};
state = {
isHidden: false, // set to true in requestIdleCallback to trigger un-render
}
shouldComponentUpdate (nextProps, nextState) {
if (!nextState.isIntersecting && nextState.isHidden) {
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true
// that either "isIntersecting" or "isHidden" matter, and then they're
// the only things that matter (and updated ARIA attributes).
return this.state.isIntersecting || !this.state.isHidden || nextProps.listLength !== this.props.listLength;
} else if (nextState.isIntersecting && !this.state.isIntersecting) {
// If we're going from a non-intersecting state to an intersecting state,
// (i.e. offscreen to onscreen), then we definitely need to re-render
return true;
}
// Otherwise, diff based on "updateOnProps" and "updateOnStates"
return super.shouldComponentUpdate(nextProps, nextState);
}
componentDidMount () {
if (!this.props.intersectionObserverWrapper) {
// TODO: enable IntersectionObserver optimization for notification statuses.
// These are managed in notifications/index.js rather than status_list.js
return;
}
this.props.intersectionObserverWrapper.observe(
this.props.id,
this.node,
this.handleIntersection
);
this.componentMounted = true;
}
componentWillUnmount () {
if (this.props.intersectionObserverWrapper) {
this.props.intersectionObserverWrapper.unobserve(this.props.id, this.node);
}
this.componentMounted = false;
}
handleIntersection = (entry) => {
if (this.node && this.node.children.length !== 0) {
// save the height of the fully-rendered element
this.height = getRectFromEntry(entry).height;
if (this.props.onHeightChange) {
this.props.onHeightChange(this.props.status, this.height);
}
}
this.setState((prevState) => {
if (prevState.isIntersecting && !entry.isIntersecting) {
scheduleIdleTask(this.hideIfNotIntersecting);
}
return {
isIntersecting: entry.isIntersecting,
isHidden: false,
};
});
}
hideIfNotIntersecting = () => {
if (!this.componentMounted) {
return;
}
// When the browser gets a chance, test if we're still not intersecting,
// and if so, set our isHidden to true to trigger an unrender. The point of
// this is to save DOM nodes and avoid using up too much memory.
// See: https://github.com/tootsuite/mastodon/issues/2900
this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
}
handleRef = (node) => {
this.node = node;
}
render () {
const { children, id, index, listLength } = this.props;
const { isIntersecting, isHidden } = this.state;
if (!isIntersecting && isHidden) {
return (
<article
ref={this.handleRef}
aria-posinset={index}
aria-setsize={listLength}
style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }}
data-id={id}
tabIndex='0'
>
{children && React.cloneElement(children, { hidden: true })}
</article>
);
}
return (
<article ref={this.handleRef} aria-posinset={index} aria-setsize={listLength} data-id={id} tabIndex='0'>
{children && React.cloneElement(children, { hidden: false })}
</article>
);
}
}

View File

@ -0,0 +1,179 @@
import React, { PureComponent } from 'react';
import { ScrollContainer } from 'react-router-scroll';
import PropTypes from 'prop-types';
import IntersectionObserverArticle from './intersection_observer_article';
import LoadMore from './load_more';
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
import { throttle } from 'lodash';
export default class ScrollableList extends PureComponent {
static propTypes = {
scrollKey: PropTypes.string.isRequired,
onScrollToBottom: PropTypes.func,
onScrollToTop: PropTypes.func,
onScroll: PropTypes.func,
trackScroll: PropTypes.bool,
shouldUpdateScroll: PropTypes.func,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
prepend: PropTypes.node,
emptyMessage: PropTypes.node,
children: PropTypes.node,
};
static defaultProps = {
trackScroll: true,
};
intersectionObserverWrapper = new IntersectionObserverWrapper();
handleScroll = throttle(() => {
if (this.node) {
const { scrollTop, scrollHeight, clientHeight } = this.node;
const offset = scrollHeight - scrollTop - clientHeight;
this._oldScrollPosition = scrollHeight - scrollTop;
if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
this.props.onScrollToBottom();
} else if (scrollTop < 100 && this.props.onScrollToTop) {
this.props.onScrollToTop();
} else if (this.props.onScroll) {
this.props.onScroll();
}
}
}, 150, {
trailing: true,
});
componentDidMount () {
this.attachScrollListener();
this.attachIntersectionObserver();
// Handle initial scroll posiiton
this.handleScroll();
}
componentDidUpdate (prevProps) {
// Reset the scroll position when a new child comes in in order not to
// jerk the scrollbar around if you're already scrolled down the page.
if (React.Children.count(prevProps.children) < React.Children.count(this.props.children) && this._oldScrollPosition && this.node.scrollTop > 0) {
if (this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props)) {
const newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
if (this.node.scrollTop !== newScrollTop) {
this.node.scrollTop = newScrollTop;
}
} else {
this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop;
}
}
}
componentWillUnmount () {
this.detachScrollListener();
this.detachIntersectionObserver();
}
attachIntersectionObserver () {
this.intersectionObserverWrapper.connect({
root: this.node,
rootMargin: '300% 0px',
});
}
detachIntersectionObserver () {
this.intersectionObserverWrapper.disconnect();
}
attachScrollListener () {
this.node.addEventListener('scroll', this.handleScroll);
}
detachScrollListener () {
this.node.removeEventListener('scroll', this.handleScroll);
}
getFirstChildKey (props) {
const { children } = props;
const firstChild = Array.isArray(children) ? children[0] : children;
return firstChild && firstChild.key;
}
setRef = (c) => {
this.node = c;
}
handleLoadMore = (e) => {
e.preventDefault();
this.props.onScrollToBottom();
}
handleKeyDown = (e) => {
if (['PageDown', 'PageUp'].includes(e.key) || (e.ctrlKey && ['End', 'Home'].includes(e.key))) {
const article = (() => {
switch (e.key) {
case 'PageDown':
return e.target.nodeName === 'ARTICLE' && e.target.nextElementSibling;
case 'PageUp':
return e.target.nodeName === 'ARTICLE' && e.target.previousElementSibling;
case 'End':
return this.node.querySelector('[role="feed"] > article:last-of-type');
case 'Home':
return this.node.querySelector('[role="feed"] > article:first-of-type');
default:
return null;
}
})();
if (article) {
e.preventDefault();
article.focus();
article.scrollIntoView();
}
}
}
render () {
const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props;
const childrenCount = React.Children.count(children);
const loadMore = <LoadMore visible={!isLoading && childrenCount > 0 && hasMore} onClick={this.handleLoadMore} />;
let scrollableArea = null;
if (isLoading || childrenCount > 0 || !emptyMessage) {
scrollableArea = (
<div className='scrollable' ref={this.setRef}>
<div role='feed' className='item-list' onKeyDown={this.handleKeyDown}>
{prepend}
{React.Children.map(this.props.children, (child, index) => (
<IntersectionObserverArticle key={child.key} id={child.key} index={index} listLength={childrenCount} intersectionObserverWrapper={this.intersectionObserverWrapper}>
{child}
</IntersectionObserverArticle>
))}
{loadMore}
</div>
</div>
);
} else {
scrollableArea = (
<div className='empty-column-indicator' ref={this.setRef}>
{emptyMessage}
</div>
);
}
if (trackScroll) {
return (
<ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
{scrollableArea}
</ScrollContainer>
);
} else {
return scrollableArea;
}
}
}

View File

@ -12,13 +12,11 @@ import StatusContent from './status_content';
import StatusActionBar from './status_action_bar'; import StatusActionBar from './status_action_bar';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components'; import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components';
// We use the component (and not the container) since we do not want // We use the component (and not the container) since we do not want
// to use the progress bar to show download progress // to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle'; import Bundle from '../features/ui/components/bundle';
import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
export default class Status extends ImmutablePureComponent { export default class Status extends ImmutablePureComponent {
@ -29,27 +27,25 @@ export default class Status extends ImmutablePureComponent {
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
wrapped: PropTypes.bool,
onReply: PropTypes.func, onReply: PropTypes.func,
onFavourite: PropTypes.func, onFavourite: PropTypes.func,
onReblog: PropTypes.func, onReblog: PropTypes.func,
onDelete: PropTypes.func, onDelete: PropTypes.func,
onPin: PropTypes.func,
onOpenMedia: PropTypes.func, onOpenMedia: PropTypes.func,
onOpenVideo: PropTypes.func, onOpenVideo: PropTypes.func,
onBlock: PropTypes.func, onBlock: PropTypes.func,
onEmbed: PropTypes.func,
onHeightChange: PropTypes.func, onHeightChange: PropTypes.func,
me: PropTypes.number, me: PropTypes.number,
boostModal: PropTypes.bool, boostModal: PropTypes.bool,
autoPlayGif: PropTypes.bool, autoPlayGif: PropTypes.bool,
muted: PropTypes.bool, muted: PropTypes.bool,
intersectionObserverWrapper: PropTypes.object, hidden: PropTypes.bool,
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}; };
state = { state = {
isExpanded: false, isExpanded: false,
isHidden: false, // set to true in requestIdleCallback to trigger un-render
} }
// Avoid checking props that are functions (and whose equality will always // Avoid checking props that are functions (and whose equality will always
@ -57,91 +53,15 @@ export default class Status extends ImmutablePureComponent {
updateOnProps = [ updateOnProps = [
'status', 'status',
'account', 'account',
'wrapped',
'me', 'me',
'boostModal', 'boostModal',
'autoPlayGif', 'autoPlayGif',
'muted', 'muted',
'listLength', 'hidden',
] ]
updateOnStates = ['isExpanded'] updateOnStates = ['isExpanded']
shouldComponentUpdate (nextProps, nextState) {
if (!nextState.isIntersecting && nextState.isHidden) {
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true
// that either "isIntersecting" or "isHidden" matter, and then they're
// the only things that matter (and updated ARIA attributes).
return this.state.isIntersecting || !this.state.isHidden || nextProps.listLength !== this.props.listLength;
} else if (nextState.isIntersecting && !this.state.isIntersecting) {
// If we're going from a non-intersecting state to an intersecting state,
// (i.e. offscreen to onscreen), then we definitely need to re-render
return true;
}
// Otherwise, diff based on "updateOnProps" and "updateOnStates"
return super.shouldComponentUpdate(nextProps, nextState);
}
componentDidMount () {
if (!this.props.intersectionObserverWrapper) {
// TODO: enable IntersectionObserver optimization for notification statuses.
// These are managed in notifications/index.js rather than status_list.js
return;
}
this.props.intersectionObserverWrapper.observe(
this.props.id,
this.node,
this.handleIntersection
);
this.componentMounted = true;
}
componentWillUnmount () {
if (this.props.intersectionObserverWrapper) {
this.props.intersectionObserverWrapper.unobserve(this.props.id, this.node);
}
this.componentMounted = false;
}
handleIntersection = (entry) => {
if (this.node && this.node.children.length !== 0) {
// save the height of the fully-rendered element
this.height = getRectFromEntry(entry).height;
if (this.props.onHeightChange) {
this.props.onHeightChange(this.props.status, this.height);
}
}
this.setState((prevState) => {
if (prevState.isIntersecting && !entry.isIntersecting) {
scheduleIdleTask(this.hideIfNotIntersecting);
}
return {
isIntersecting: entry.isIntersecting,
isHidden: false,
};
});
}
hideIfNotIntersecting = () => {
if (!this.componentMounted) {
return;
}
// When the browser gets a chance, test if we're still not intersecting,
// and if so, set our isHidden to true to trigger an unrender. The point of
// this is to save DOM nodes and avoid using up too much memory.
// See: https://github.com/tootsuite/mastodon/issues/2900
this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
}
handleRef = (node) => {
this.node = node;
}
handleClick = () => { handleClick = () => {
if (!this.context.router) { if (!this.context.router) {
return; return;
@ -175,25 +95,19 @@ export default class Status extends ImmutablePureComponent {
let media = null; let media = null;
let statusAvatar; let statusAvatar;
// Exclude intersectionObserverWrapper from `other` variable const { status, account, hidden, ...other } = this.props;
// because intersection is managed in here. const { isExpanded } = this.state;
const { status, account, intersectionObserverWrapper, index, listLength, wrapped, ...other } = this.props;
const { isExpanded, isIntersecting, isHidden } = this.state;
if (status === null) { if (status === null) {
return null; return null;
} }
const hasIntersectionObserverWrapper = !!this.props.intersectionObserverWrapper; if (hidden) {
const isHiddenForSure = isIntersecting === false && isHidden;
const visibilityUnknownButHeightIsCached = isIntersecting === undefined && status.has('height');
if (hasIntersectionObserverWrapper && (isHiddenForSure || visibilityUnknownButHeightIsCached)) {
return ( return (
<article ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0' style={{ height: `${this.height || status.get('height')}px`, opacity: 0, overflow: 'hidden' }}> <div>
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
{status.get('content')} {status.get('content')}
</article> </div>
); );
} }
@ -201,14 +115,14 @@ export default class Status extends ImmutablePureComponent {
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) }; const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
return ( return (
<article className='status__wrapper' ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0'> <div className='status__wrapper' data-id={status.get('id')} >
<div className='status__prepend'> <div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div> <div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={display_name_html} /></a> }} /> <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={display_name_html} /></a> }} />
</div> </div>
<Status {...other} wrapped status={status.get('reblog')} account={status.get('account')} /> <Status {...other} status={status.get('reblog')} account={status.get('account')} />
</article> </div>
); );
} }
@ -237,7 +151,7 @@ export default class Status extends ImmutablePureComponent {
} }
return ( return (
<article aria-posinset={index} aria-setsize={listLength} className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')} tabIndex={wrapped ? null : '0'} ref={this.handleRef}> <div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')}>
<div className='status__info'> <div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
@ -255,7 +169,7 @@ export default class Status extends ImmutablePureComponent {
{media} {media}
<StatusActionBar {...this.props} /> <StatusActionBar {...this.props} />
</article> </div>
); );
} }

View File

@ -24,6 +24,9 @@ const messages = defineMessages({
report: { id: 'status.report', defaultMessage: 'Report @{name}' }, report: { id: 'status.report', defaultMessage: 'Report @{name}' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
embed: { id: 'status.embed', defaultMessage: 'Embed' },
}); });
@injectIntl @injectIntl
@ -43,7 +46,9 @@ export default class StatusActionBar extends ImmutablePureComponent {
onMute: PropTypes.func, onMute: PropTypes.func,
onBlock: PropTypes.func, onBlock: PropTypes.func,
onReport: PropTypes.func, onReport: PropTypes.func,
onEmbed: PropTypes.func,
onMuteConversation: PropTypes.func, onMuteConversation: PropTypes.func,
onPin: PropTypes.func,
me: PropTypes.number, me: PropTypes.number,
withDismiss: PropTypes.bool, withDismiss: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
@ -80,6 +85,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
this.props.onDelete(this.props.status); this.props.onDelete(this.props.status);
} }
handlePinClick = () => {
this.props.onPin(this.props.status);
}
handleMentionClick = () => { handleMentionClick = () => {
this.props.onMention(this.props.status.get('account'), this.context.router.history); this.props.onMention(this.props.status.get('account'), this.context.router.history);
} }
@ -96,6 +105,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
} }
handleEmbed = () => {
this.props.onEmbed(this.props.status);
}
handleReport = () => { handleReport = () => {
this.props.onReport(this.props.status); this.props.onReport(this.props.status);
} }
@ -106,9 +119,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
render () { render () {
const { status, me, intl, withDismiss } = this.props; const { status, me, intl, withDismiss } = this.props;
const reblogDisabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct';
const mutingConversation = status.get('muted'); const mutingConversation = status.get('muted');
const anonymousAccess = !me; const anonymousAccess = !me;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
let menu = []; let menu = [];
let reblogIcon = 'retweet'; let reblogIcon = 'retweet';
@ -116,6 +130,11 @@ export default class StatusActionBar extends ImmutablePureComponent {
let replyTitle; let replyTitle;
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen }); menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
if (publicStatus) {
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
}
menu.push(null); menu.push(null);
if (withDismiss) { if (withDismiss) {
@ -124,6 +143,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
} }
if (status.getIn(['account', 'id']) === me) { if (status.getIn(['account', 'id']) === me) {
if (publicStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
}
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
} else { } else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
@ -154,7 +177,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
return ( return (
<div className='status__action-bar'> <div className='status__action-bar'>
<IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} /> <IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} />
<IconButton className='status__action-bar-button' disabled={anonymousAccess || reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /> <IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
{shareButton} {shareButton}

View File

@ -1,12 +1,10 @@
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { ScrollContainer } from 'react-router-scroll';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import StatusContainer from '../../glitch/components/status/container'; import StatusContainer from '../../glitch/components/status/container';
import LoadMore from './load_more'; import LoadMore from './load_more';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'; import ScrollableList from './scrollable_list';
import { throttle } from 'lodash';
export default class StatusList extends ImmutablePureComponent { export default class StatusList extends ImmutablePureComponent {
@ -28,145 +26,21 @@ export default class StatusList extends ImmutablePureComponent {
trackScroll: true, trackScroll: true,
}; };
intersectionObserverWrapper = new IntersectionObserverWrapper();
handleScroll = throttle(() => {
if (this.node) {
const { scrollTop, scrollHeight, clientHeight } = this.node;
const offset = scrollHeight - scrollTop - clientHeight;
this._oldScrollPosition = scrollHeight - scrollTop;
if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
this.props.onScrollToBottom();
} else if (scrollTop < 100 && this.props.onScrollToTop) {
this.props.onScrollToTop();
} else if (this.props.onScroll) {
this.props.onScroll();
}
}
}, 150, {
trailing: true,
});
componentDidMount () {
this.attachScrollListener();
this.attachIntersectionObserver();
// Handle initial scroll posiiton
this.handleScroll();
}
componentDidUpdate (prevProps) {
// Reset the scroll position when a new toot comes in in order not to
// jerk the scrollbar around if you're already scrolled down the page.
if (prevProps.statusIds.size < this.props.statusIds.size && this._oldScrollPosition && this.node.scrollTop > 0) {
if (prevProps.statusIds.first() !== this.props.statusIds.first()) {
let newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
if (this.node.scrollTop !== newScrollTop) {
this.node.scrollTop = newScrollTop;
}
} else {
this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop;
}
}
}
componentWillUnmount () {
this.detachScrollListener();
this.detachIntersectionObserver();
}
attachIntersectionObserver () {
this.intersectionObserverWrapper.connect({
root: this.node,
rootMargin: '300% 0px',
});
}
detachIntersectionObserver () {
this.intersectionObserverWrapper.disconnect();
}
attachScrollListener () {
this.node.addEventListener('scroll', this.handleScroll);
}
detachScrollListener () {
this.node.removeEventListener('scroll', this.handleScroll);
}
setRef = (c) => {
this.node = c;
}
handleLoadMore = (e) => {
e.preventDefault();
this.props.onScrollToBottom();
}
handleKeyDown = (e) => {
if (['PageDown', 'PageUp'].includes(e.key) || (e.ctrlKey && ['End', 'Home'].includes(e.key))) {
const article = (() => {
switch (e.key) {
case 'PageDown':
return e.target.nodeName === 'ARTICLE' && e.target.nextElementSibling;
case 'PageUp':
return e.target.nodeName === 'ARTICLE' && e.target.previousElementSibling;
case 'End':
return this.node.querySelector('[role="feed"] > article:last-of-type');
case 'Home':
return this.node.querySelector('[role="feed"] > article:first-of-type');
default:
return null;
}
})();
if (article) {
e.preventDefault();
article.focus();
article.scrollIntoView();
}
}
}
render () { render () {
const { statusIds, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props; const { statusIds, ...other } = this.props;
const { isLoading } = other;
const loadMore = <LoadMore visible={!isLoading && statusIds.size > 0 && hasMore} onClick={this.handleLoadMore} />; const scrollableContent = (isLoading || statusIds.size > 0) ? (
let scrollableArea = null; statusIds.map((statusId) => (
<StatusContainer key={statusId} id={statusId} />
))
) : null;
if (isLoading || statusIds.size > 0 || !emptyMessage) { return (
scrollableArea = ( <ScrollableList {...other}>
<div className='scrollable' ref={this.setRef}> {scrollableContent}
<div role='feed' className='status-list' onKeyDown={this.handleKeyDown}> </ScrollableList>
{prepend} );
{statusIds.map((statusId, index) => {
return <StatusContainer key={statusId} id={statusId} index={index} listLength={statusIds.size} intersectionObserverWrapper={this.intersectionObserverWrapper} />;
})}
{loadMore}
</div>
</div>
);
} else {
scrollableArea = (
<div className='empty-column-indicator' ref={this.setRef}>
{emptyMessage}
</div>
);
}
if (trackScroll) {
return (
<ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
{scrollableArea}
</ScrollContainer>
);
} else {
return scrollableArea;
}
} }
} }

View File

@ -32,7 +32,7 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = (dispatch, { intl }) => ({ const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow (account) { onFollow (account) {
if (account.getIn(['relationship', 'following'])) { if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
if (this.unfollowModal) { if (this.unfollowModal) {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,

View File

@ -0,0 +1,39 @@
import React from 'react';
import { Provider } from 'react-redux';
import PropTypes from 'prop-types';
import configureStore from '../store/configureStore';
import { hydrateStore } from '../actions/store';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import Compose from '../features/standalone/compose';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
const store = configureStore();
const initialStateContainer = document.getElementById('initial-state');
if (initialStateContainer !== null) {
const initialState = JSON.parse(initialStateContainer.textContent);
store.dispatch(hydrateStore(initialState));
}
export default class TimelineContainer extends React.PureComponent {
static propTypes = {
locale: PropTypes.string.isRequired,
};
render () {
const { locale } = this.props;
return (
<IntlProvider locale={locale} messages={messages}>
<Provider store={store}>
<Compose />
</Provider>
</IntlProvider>
);
}
}

View File

@ -2,21 +2,13 @@ import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import configureStore from '../store/configureStore'; import configureStore from '../store/configureStore';
import {
updateTimeline,
deleteFromTimelines,
refreshHomeTimeline,
connectTimeline,
disconnectTimeline,
} from '../actions/timelines';
import { showOnboardingOnce } from '../actions/onboarding'; import { showOnboardingOnce } from '../actions/onboarding';
import { updateNotifications, refreshNotifications } from '../actions/notifications';
import BrowserRouter from 'react-router-dom/BrowserRouter'; import BrowserRouter from 'react-router-dom/BrowserRouter';
import Route from 'react-router-dom/Route'; import Route from 'react-router-dom/Route';
import ScrollContext from 'react-router-scroll/lib/ScrollBehaviorContext'; import ScrollContext from 'react-router-scroll/lib/ScrollBehaviorContext';
import UI from '../features/ui'; import UI from '../features/ui';
import { hydrateStore } from '../actions/store'; import { hydrateStore } from '../actions/store';
import createStream from '../stream'; import { connectUserStream } from '../actions/streaming';
import { IntlProvider, addLocaleData } from 'react-intl'; import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales'; import { getLocale } from '../locales';
const { localeData, messages } = getLocale(); const { localeData, messages } = getLocale();
@ -39,74 +31,28 @@ export default class Mastodon extends React.PureComponent {
}; };
componentDidMount() { componentDidMount() {
const { locale } = this.props; this.disconnect = store.dispatch(connectUserStream());
const streamingAPIBaseURL = store.getState().getIn(['meta', 'streaming_api_base_url']);
const accessToken = store.getState().getIn(['meta', 'access_token']);
const setupPolling = () => {
this.polling = setInterval(() => {
store.dispatch(refreshHomeTimeline());
store.dispatch(refreshNotifications());
}, 20000);
};
const clearPolling = () => {
clearInterval(this.polling);
this.polling = undefined;
};
this.subscription = createStream(streamingAPIBaseURL, accessToken, 'user', {
connected () {
clearPolling();
store.dispatch(connectTimeline('home'));
},
disconnected () {
setupPolling();
store.dispatch(disconnectTimeline('home'));
},
received (data) {
switch(data.event) {
case 'update':
store.dispatch(updateTimeline('home', JSON.parse(data.payload)));
break;
case 'delete':
store.dispatch(deleteFromTimelines(data.payload));
break;
case 'notification':
store.dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
break;
}
},
reconnected () {
clearPolling();
store.dispatch(connectTimeline('home'));
store.dispatch(refreshHomeTimeline());
store.dispatch(refreshNotifications());
},
});
// Desktop notifications // Desktop notifications
// Ask after 1 minute
if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') { if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
Notification.requestPermission(); window.setTimeout(() => Notification.requestPermission(), 60 * 1000);
}
// Protocol handler
// Ask after 5 minutes
if (typeof navigator.registerProtocolHandler !== 'undefined') {
const handlerUrl = window.location.protocol + '//' + window.location.host + '/intent?uri=%s';
window.setTimeout(() => navigator.registerProtocolHandler('web+mastodon', handlerUrl, 'Mastodon'), 5 * 60 * 1000);
} }
store.dispatch(showOnboardingOnce()); store.dispatch(showOnboardingOnce());
} }
componentWillUnmount () { componentWillUnmount () {
if (typeof this.subscription !== 'undefined') { if (this.disconnect) {
this.subscription.close(); this.disconnect();
this.subscription = null; this.disconnect = null;
}
if (typeof this.polling !== 'undefined') {
clearInterval(this.polling);
this.polling = null;
} }
} }

View File

@ -14,6 +14,8 @@ import {
favourite, favourite,
unreblog, unreblog,
unfavourite, unfavourite,
pin,
unpin,
} from '../actions/interactions'; } from '../actions/interactions';
import { import {
blockAccount, blockAccount,
@ -75,6 +77,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
} }
}, },
onPin (status) {
if (status.get('pinned')) {
dispatch(unpin(status));
} else {
dispatch(pin(status));
}
},
onEmbed (status) {
dispatch(openModal('EMBED', { url: status.get('url') }));
},
onDelete (status) { onDelete (status) {
if (!this.deleteModal) { if (!this.deleteModal) {
dispatch(deleteStatus(status.get('id'))); dispatch(deleteStatus(status.get('id')));

View File

@ -14,7 +14,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({ const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
}); });
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
@ -105,7 +105,7 @@ export default class Header extends ImmutablePureComponent {
if (account.getIn(['relationship', 'requested'])) { if (account.getIn(['relationship', 'requested'])) {
actionBtn = ( actionBtn = (
<div className='account--action-button'> <div className='account--action-button'>
<IconButton size={26} disabled icon='hourglass' title={intl.formatMessage(messages.requested)} /> <IconButton size={26} active icon='hourglass' title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />
</div> </div>
); );
} else if (!account.getIn(['relationship', 'blocking'])) { } else if (!account.getIn(['relationship', 'blocking'])) {

View File

@ -38,7 +38,7 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = (dispatch, { intl }) => ({ const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow (account) { onFollow (account) {
if (account.getIn(['relationship', 'following'])) { if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
if (this.unfollowModal) { if (this.unfollowModal) {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,

View File

@ -7,15 +7,11 @@ import ColumnHeader from '../../components/column_header';
import { import {
refreshCommunityTimeline, refreshCommunityTimeline,
expandCommunityTimeline, expandCommunityTimeline,
updateTimeline,
deleteFromTimelines,
connectTimeline,
disconnectTimeline,
} from '../../actions/timelines'; } from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container'; import ColumnSettingsContainer from './containers/column_settings_container';
import createStream from '../../stream'; import { connectCommunityStream } from '../../actions/streaming';
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'column.community', defaultMessage: 'Local timeline' }, title: { id: 'column.community', defaultMessage: 'Local timeline' },
@ -23,8 +19,6 @@ const messages = defineMessages({
const mapStateToProps = state => ({ const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0, hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0,
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
accessToken: state.getIn(['meta', 'access_token']),
}); });
@connect(mapStateToProps) @connect(mapStateToProps)
@ -35,8 +29,6 @@ export default class CommunityTimeline extends React.PureComponent {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string, columnId: PropTypes.string,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
streamingAPIBaseURL: PropTypes.string.isRequired,
accessToken: PropTypes.string.isRequired,
hasUnread: PropTypes.bool, hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
@ -61,46 +53,16 @@ export default class CommunityTimeline extends React.PureComponent {
} }
componentDidMount () { componentDidMount () {
const { dispatch, streamingAPIBaseURL, accessToken } = this.props; const { dispatch } = this.props;
dispatch(refreshCommunityTimeline()); dispatch(refreshCommunityTimeline());
this.disconnect = dispatch(connectCommunityStream());
if (typeof this._subscription !== 'undefined') {
return;
}
this._subscription = createStream(streamingAPIBaseURL, accessToken, 'public:local', {
connected () {
dispatch(connectTimeline('community'));
},
reconnected () {
dispatch(connectTimeline('community'));
},
disconnected () {
dispatch(disconnectTimeline('community'));
},
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline('community', JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
}
},
});
} }
componentWillUnmount () { componentWillUnmount () {
if (typeof this._subscription !== 'undefined') { if (this.disconnect) {
this._subscription.close(); this.disconnect();
this._subscription = null; this.disconnect = null;
} }
} }

View File

@ -16,6 +16,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({ const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'favourites', 'items']), statusIds: state.getIn(['status_lists', 'favourites', 'items']),
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
}); });
@connect(mapStateToProps) @connect(mapStateToProps)
@ -28,6 +29,7 @@ export default class Favourites extends ImmutablePureComponent {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
columnId: PropTypes.string, columnId: PropTypes.string,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
hasMore: PropTypes.bool,
}; };
componentWillMount () { componentWillMount () {
@ -62,7 +64,7 @@ export default class Favourites extends ImmutablePureComponent {
} }
render () { render () {
const { intl, statusIds, columnId, multiColumn } = this.props; const { intl, statusIds, columnId, multiColumn, hasMore } = this.props;
const pinned = !!columnId; const pinned = !!columnId;
return ( return (
@ -81,6 +83,7 @@ export default class Favourites extends ImmutablePureComponent {
trackScroll={!pinned} trackScroll={!pinned}
statusIds={statusIds} statusIds={statusIds}
scrollKey={`favourited_statuses-${columnId}`} scrollKey={`favourited_statuses-${columnId}`}
hasMore={hasMore}
onScrollToBottom={this.handleScrollToBottom} onScrollToBottom={this.handleScrollToBottom}
/> />
</Column> </Column>

View File

@ -7,17 +7,13 @@ import ColumnHeader from '../../components/column_header';
import { import {
refreshHashtagTimeline, refreshHashtagTimeline,
expandHashtagTimeline, expandHashtagTimeline,
updateTimeline,
deleteFromTimelines,
} from '../../actions/timelines'; } from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import createStream from '../../stream'; import { connectHashtagStream } from '../../actions/streaming';
const mapStateToProps = state => ({ const mapStateToProps = (state, props) => ({
hasUnread: state.getIn(['timelines', 'tag', 'unread']) > 0, hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0,
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
accessToken: state.getIn(['meta', 'access_token']),
}); });
@connect(mapStateToProps) @connect(mapStateToProps)
@ -27,8 +23,6 @@ export default class HashtagTimeline extends React.PureComponent {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
columnId: PropTypes.string, columnId: PropTypes.string,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
streamingAPIBaseURL: PropTypes.string.isRequired,
accessToken: PropTypes.string.isRequired,
hasUnread: PropTypes.bool, hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
@ -53,28 +47,13 @@ export default class HashtagTimeline extends React.PureComponent {
} }
_subscribe (dispatch, id) { _subscribe (dispatch, id) {
const { streamingAPIBaseURL, accessToken } = this.props; this.disconnect = dispatch(connectHashtagStream(id));
this.subscription = createStream(streamingAPIBaseURL, accessToken, `hashtag&tag=${id}`, {
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline(`hashtag:${id}`, JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
}
},
});
} }
_unsubscribe () { _unsubscribe () {
if (typeof this.subscription !== 'undefined') { if (this.disconnect) {
this.subscription.close(); this.disconnect();
this.subscription = null; this.disconnect = null;
} }
} }

View File

@ -2,6 +2,7 @@
// SEE INSTEAD : glitch/components/notification // SEE INSTEAD : glitch/components/notification
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import StatusContainer from '../../../containers/status_container'; import StatusContainer from '../../../containers/status_container';
import AccountContainer from '../../../containers/account_container'; import AccountContainer from '../../../containers/account_container';
@ -13,6 +14,7 @@ export default class Notification extends ImmutablePureComponent {
static propTypes = { static propTypes = {
notification: ImmutablePropTypes.map.isRequired, notification: ImmutablePropTypes.map.isRequired,
hidden: PropTypes.bool,
}; };
renderFollow (account, link) { renderFollow (account, link) {
@ -26,13 +28,13 @@ export default class Notification extends ImmutablePureComponent {
<FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} /> <FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} />
</div> </div>
<AccountContainer id={account.get('id')} withNote={false} /> <AccountContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} />
</div> </div>
); );
} }
renderMention (notification) { renderMention (notification) {
return <StatusContainer id={notification.get('status')} withDismiss />; return <StatusContainer id={notification.get('status')} withDismiss hidden={this.props.hidden} />;
} }
renderFavourite (notification, link) { renderFavourite (notification, link) {
@ -45,7 +47,7 @@ export default class Notification extends ImmutablePureComponent {
<FormattedMessage id='notification.favourite' defaultMessage='{name} favourited your status' values={{ name: link }} /> <FormattedMessage id='notification.favourite' defaultMessage='{name} favourited your status' values={{ name: link }} />
</div> </div>
<StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss /> <StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={!!this.props.hidden} />
</div> </div>
); );
} }
@ -60,7 +62,7 @@ export default class Notification extends ImmutablePureComponent {
<FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} /> <FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} />
</div> </div>
<StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss /> <StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={this.props.hidden} />
</div> </div>
); );
} }

View File

@ -16,8 +16,8 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container'; import ColumnSettingsContainer from './containers/column_settings_container';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import LoadMore from '../../components/load_more';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import ScrollableList from '../../components/scrollable_list';
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'column.notifications', defaultMessage: 'Notifications' }, title: { id: 'column.notifications', defaultMessage: 'Notifications' },
@ -68,40 +68,18 @@ export default class Notifications extends React.PureComponent {
trackScroll: true, trackScroll: true,
}; };
dispatchExpandNotifications = debounce(() => { handleScrollToBottom = debounce(() => {
this.props.dispatch(scrollTopNotifications(false));
this.props.dispatch(expandNotifications()); this.props.dispatch(expandNotifications());
}, 300, { leading: true }); }, 300, { leading: true });
dispatchScrollToTop = debounce((top) => { handleScrollToTop = debounce(() => {
this.props.dispatch(scrollTopNotifications(top)); this.props.dispatch(scrollTopNotifications(true));
}, 100); }, 100);
handleScroll = (e) => { handleScroll = debounce(() => {
const { scrollTop, scrollHeight, clientHeight } = e.target; this.props.dispatch(scrollTopNotifications(false));
const offset = scrollHeight - scrollTop - clientHeight; }, 100);
this._oldScrollPosition = scrollHeight - scrollTop;
if (250 > offset && this.props.hasMore && !this.props.isLoading) {
this.dispatchExpandNotifications();
}
if (scrollTop < 100) {
this.dispatchScrollToTop(true);
} else {
this.dispatchScrollToTop(false);
}
}
componentDidUpdate (prevProps) {
if (this.node.scrollTop > 0 && (prevProps.notifications.size < this.props.notifications.size && prevProps.notifications.first() !== this.props.notifications.first() && !!this._oldScrollPosition)) {
this.node.scrollTop = this.node.scrollHeight - this._oldScrollPosition;
}
}
handleLoadMore = (e) => {
e.preventDefault();
this.dispatchExpandNotifications();
}
handlePin = () => { handlePin = () => {
const { columnId, dispatch } = this.props; const { columnId, dispatch } = this.props;
@ -122,10 +100,6 @@ export default class Notifications extends React.PureComponent {
this.column.scrollTop(); this.column.scrollTop();
} }
setRef = (c) => {
this.node = c;
}
setColumnRef = c => { setColumnRef = c => {
this.column = c; this.column = c;
} }
@ -133,52 +107,34 @@ export default class Notifications extends React.PureComponent {
render () { render () {
const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore } = this.props; const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore } = this.props;
const pinned = !!columnId; const pinned = !!columnId;
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
let loadMore = ''; let scrollableContent = null;
let scrollableArea = '';
let unread = '';
let scrollContainer = '';
if (!isLoading && hasMore) { if (isLoading && this.scrollableContent) {
loadMore = <LoadMore onClick={this.handleLoadMore} />; scrollableContent = this.scrollableContent;
}
if (isUnread) {
unread = <div className='notifications__unread-indicator' />;
}
if (isLoading && this.scrollableArea) {
scrollableArea = this.scrollableArea;
} else if (notifications.size > 0 || hasMore) { } else if (notifications.size > 0 || hasMore) {
scrollableArea = ( scrollableContent = notifications.map((item) => <NotificationContainer key={item.get('id')} notification={item} accountId={item.get('account')} />);
<div className='scrollable' onScroll={this.handleScroll} ref={this.setRef}>
{unread}
<div>
{notifications.map(item => <NotificationContainer key={item.get('id')} notification={item} accountId={item.get('account')} />)}
{loadMore}
</div>
</div>
);
} else { } else {
scrollableArea = ( scrollableContent = null;
<div className='empty-column-indicator' ref={this.setRef}>
<FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />
</div>
);
} }
if (pinned) { this.scrollableContent = scrollableContent;
scrollContainer = scrollableArea;
} else {
scrollContainer = (
<ScrollContainer scrollKey={`notifications-${columnId}`} shouldUpdateScroll={shouldUpdateScroll}>
{scrollableArea}
</ScrollContainer>
);
}
this.scrollableArea = scrollableArea; const scrollContainer = (
<ScrollableList
scrollKey={`notifications-${columnId}`}
isLoading={isLoading}
hasMore={hasMore}
emptyMessage={emptyMessage}
onScrollToBottom={this.handleScrollToBottom}
onScrollToTop={this.handleScrollToTop}
onScroll={this.handleScroll}
shouldUpdateScroll={shouldUpdateScroll}
>
{scrollableContent}
</ScrollableList>
);
return ( return (
<Column <Column

View File

@ -7,15 +7,11 @@ import ColumnHeader from '../../components/column_header';
import { import {
refreshPublicTimeline, refreshPublicTimeline,
expandPublicTimeline, expandPublicTimeline,
updateTimeline,
deleteFromTimelines,
connectTimeline,
disconnectTimeline,
} from '../../actions/timelines'; } from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container'; import ColumnSettingsContainer from './containers/column_settings_container';
import createStream from '../../stream'; import { connectPublicStream } from '../../actions/streaming';
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'column.public', defaultMessage: 'Federated timeline' }, title: { id: 'column.public', defaultMessage: 'Federated timeline' },
@ -23,8 +19,6 @@ const messages = defineMessages({
const mapStateToProps = state => ({ const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0, hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0,
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
accessToken: state.getIn(['meta', 'access_token']),
}); });
@connect(mapStateToProps) @connect(mapStateToProps)
@ -36,8 +30,6 @@ export default class PublicTimeline extends React.PureComponent {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
columnId: PropTypes.string, columnId: PropTypes.string,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
streamingAPIBaseURL: PropTypes.string.isRequired,
accessToken: PropTypes.string.isRequired,
hasUnread: PropTypes.bool, hasUnread: PropTypes.bool,
}; };
@ -61,46 +53,16 @@ export default class PublicTimeline extends React.PureComponent {
} }
componentDidMount () { componentDidMount () {
const { dispatch, streamingAPIBaseURL, accessToken } = this.props; const { dispatch } = this.props;
dispatch(refreshPublicTimeline()); dispatch(refreshPublicTimeline());
this.disconnect = dispatch(connectPublicStream());
if (typeof this._subscription !== 'undefined') {
return;
}
this._subscription = createStream(streamingAPIBaseURL, accessToken, 'public', {
connected () {
dispatch(connectTimeline('public'));
},
reconnected () {
dispatch(connectTimeline('public'));
},
disconnected () {
dispatch(disconnectTimeline('public'));
},
received (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline('public', JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
}
},
});
} }
componentWillUnmount () { componentWillUnmount () {
if (typeof this._subscription !== 'undefined') { if (this.disconnect) {
this._subscription.close(); this.disconnect();
this._subscription = null; this.disconnect = null;
} }
} }

View File

@ -0,0 +1,18 @@
import React from 'react';
import ComposeFormContainer from '../../compose/containers/compose_form_container';
import NotificationsContainer from '../../ui/containers/notifications_container';
import LoadingBarContainer from '../../ui/containers/loading_bar_container';
export default class Compose extends React.PureComponent {
render () {
return (
<div>
<ComposeFormContainer />
<NotificationsContainer />
<LoadingBarContainer className='loading-bar' />
</div>
);
}
}

View File

@ -14,6 +14,9 @@ const messages = defineMessages({
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' }, report: { id: 'status.report', defaultMessage: 'Report @{name}' },
share: { id: 'status.share', defaultMessage: 'Share' }, share: { id: 'status.share', defaultMessage: 'Share' },
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
embed: { id: 'status.embed', defaultMessage: 'Embed' },
}); });
@injectIntl @injectIntl
@ -31,6 +34,8 @@ export default class ActionBar extends React.PureComponent {
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired,
onReport: PropTypes.func, onReport: PropTypes.func,
onPin: PropTypes.func,
onEmbed: PropTypes.func,
me: PropTypes.number.isRequired, me: PropTypes.number.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -59,6 +64,10 @@ export default class ActionBar extends React.PureComponent {
this.props.onReport(this.props.status); this.props.onReport(this.props.status);
} }
handlePinClick = () => {
this.props.onPin(this.props.status);
}
handleShare = () => { handleShare = () => {
navigator.share({ navigator.share({
text: this.props.status.get('search_index'), text: this.props.status.get('search_index'),
@ -66,12 +75,26 @@ export default class ActionBar extends React.PureComponent {
}); });
} }
handleEmbed = () => {
this.props.onEmbed(this.props.status);
}
render () { render () {
const { status, me, intl } = this.props; const { status, me, intl } = this.props;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
let menu = []; let menu = [];
if (publicStatus) {
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
}
if (me === status.getIn(['account', 'id'])) { if (me === status.getIn(['account', 'id'])) {
if (publicStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
}
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
} else { } else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import punycode from 'punycode'; import punycode from 'punycode';
import classnames from 'classnames';
const IDNA_PREFIX = 'xn--'; const IDNA_PREFIX = 'xn--';
@ -32,7 +33,7 @@ export default class Card extends React.PureComponent {
if (card.get('image')) { if (card.get('image')) {
image = ( image = (
<div className='status-card__image'> <div className='status-card__image'>
<img src={card.get('image')} alt={card.get('title')} className='status-card__image-image' /> <img src={card.get('image')} alt={card.get('title')} className='status-card__image-image' width={card.get('width')} height={card.get('height')} />
</div> </div>
); );
} }
@ -41,8 +42,12 @@ export default class Card extends React.PureComponent {
provider = decodeIDNA(getHostname(card.get('url'))); provider = decodeIDNA(getHostname(card.get('url')));
} }
const className = classnames('status-card', {
'horizontal': card.get('width') > card.get('height'),
});
return ( return (
<a href={card.get('url')} className='status-card' target='_blank' rel='noopener'> <a href={card.get('url')} className={className} target='_blank' rel='noopener'>
{image} {image}
<div className='status-card__content'> <div className='status-card__content'>

View File

@ -12,6 +12,8 @@ import {
unfavourite, unfavourite,
reblog, reblog,
unreblog, unreblog,
pin,
unpin,
} from '../../actions/interactions'; } from '../../actions/interactions';
import { import {
replyCompose, replyCompose,
@ -89,6 +91,14 @@ export default class Status extends ImmutablePureComponent {
} }
} }
handlePin = (status) => {
if (status.get('pinned')) {
this.props.dispatch(unpin(status));
} else {
this.props.dispatch(pin(status));
}
}
handleReplyClick = (status) => { handleReplyClick = (status) => {
this.props.dispatch(replyCompose(status, this.context.router.history)); this.props.dispatch(replyCompose(status, this.context.router.history));
} }
@ -139,6 +149,10 @@ export default class Status extends ImmutablePureComponent {
this.props.dispatch(initReport(status.get('account'), status)); this.props.dispatch(initReport(status.get('account'), status));
} }
handleEmbed = (status) => {
this.props.dispatch(openModal('EMBED', { url: status.get('url') }));
}
renderChildren (list) { renderChildren (list) {
return list.map(id => <StatusContainer key={id} id={id} />); return list.map(id => <StatusContainer key={id} id={id} />);
} }
@ -190,6 +204,8 @@ export default class Status extends ImmutablePureComponent {
onDelete={this.handleDeleteClick} onDelete={this.handleDeleteClick}
onMention={this.handleMentionClick} onMention={this.handleMentionClick}
onReport={this.handleReport} onReport={this.handleReport}
onPin={this.handlePin}
onEmbed={this.handleEmbed}
/> />
{descendants} {descendants}

View File

@ -25,6 +25,17 @@ export default class Column extends React.PureComponent {
this._interruptScrollAnimation = scrollTop(scrollable); this._interruptScrollAnimation = scrollTop(scrollable);
} }
scrollTop () {
const scrollable = this.node.querySelector('.scrollable');
if (!scrollable) {
return;
}
this._interruptScrollAnimation = scrollTop(scrollable);
}
handleScroll = debounce(() => { handleScroll = debounce(() => {
if (typeof this._interruptScrollAnimation !== 'undefined') { if (typeof this._interruptScrollAnimation !== 'undefined') {
this._interruptScrollAnimation(); this._interruptScrollAnimation();

View File

@ -12,6 +12,7 @@ import ColumnLoading from './column_loading';
import BundleColumnError from './bundle_column_error'; import BundleColumnError from './bundle_column_error';
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components'; import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components';
import detectPassiveEvents from 'detect-passive-events';
import { scrollRight } from '../../../scroll'; import { scrollRight } from '../../../scroll';
const componentMap = { const componentMap = {
@ -24,7 +25,7 @@ const componentMap = {
'FAVOURITES': FavouritedStatuses, 'FAVOURITES': FavouritedStatuses,
}; };
@injectIntl @component => injectIntl(component, { withRef: true })
export default class ColumnsArea extends ImmutablePureComponent { export default class ColumnsArea extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
@ -47,16 +48,36 @@ export default class ColumnsArea extends ImmutablePureComponent {
} }
componentDidMount() { componentDidMount() {
if (!this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
}
this.lastIndex = getIndex(this.context.router.history.location.pathname); this.lastIndex = getIndex(this.context.router.history.location.pathname);
this.setState({ shouldAnimate: true }); this.setState({ shouldAnimate: true });
} }
componentWillUpdate(nextProps) {
if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel);
}
}
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
}
this.lastIndex = getIndex(this.context.router.history.location.pathname); this.lastIndex = getIndex(this.context.router.history.location.pathname);
this.setState({ shouldAnimate: true }); this.setState({ shouldAnimate: true });
}
if (this.props.children !== prevProps.children && !this.props.singleColumn) { componentWillUnmount () {
scrollRight(this.node); if (!this.props.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel);
}
}
handleChildrenContentChange() {
if (!this.props.singleColumn) {
scrollRight(this.node, this.node.scrollWidth - window.innerWidth);
} }
} }
@ -80,6 +101,14 @@ export default class ColumnsArea extends ImmutablePureComponent {
} }
} }
handleWheel = () => {
if (typeof this._interruptScrollAnimation !== 'function') {
return;
}
this._interruptScrollAnimation();
}
setRef = (node) => { setRef = (node) => {
this.node = node; this.node = node;
} }

View File

@ -0,0 +1,84 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage, injectIntl } from 'react-intl';
import axios from 'axios';
@injectIntl
export default class EmbedModal extends ImmutablePureComponent {
static propTypes = {
url: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
}
state = {
loading: false,
oembed: null,
};
componentDidMount () {
const { url } = this.props;
this.setState({ loading: true });
axios.post('/api/web/embed', { url }).then(res => {
this.setState({ loading: false, oembed: res.data });
const iframeDocument = this.iframe.contentWindow.document;
iframeDocument.open();
iframeDocument.write(res.data.html);
iframeDocument.close();
iframeDocument.body.style.margin = 0;
this.iframe.height = iframeDocument.body.scrollHeight + 'px';
});
}
setIframeRef = c => {
this.iframe = c;
}
handleTextareaClick = (e) => {
e.target.select();
}
render () {
const { oembed } = this.state;
return (
<div className='modal-root__modal embed-modal'>
<h4><FormattedMessage id='status.embed' defaultMessage='Embed' /></h4>
<div className='embed-modal__container'>
<p className='hint'>
<FormattedMessage id='embed.instructions' defaultMessage='Embed this status on your website by copying the code below.' />
</p>
<input
type='text'
className='embed-modal__html'
readOnly
value={oembed && oembed.html || ''}
onClick={this.handleTextareaClick}
/>
<p className='hint'>
<FormattedMessage id='embed.preview' defaultMessage='Here is what it will look like:' />
</p>
<iframe
className='embed-modal__iframe'
scrolling='no'
frameBorder='0'
ref={this.setIframeRef}
title='preview'
/>
</div>
</div>
);
}
}

View File

@ -14,6 +14,7 @@ import {
ConfirmationModal, ConfirmationModal,
ReportModal, ReportModal,
SettingsModal, SettingsModal,
EmbedModal,
} from '../../../features/ui/util/async-components'; } from '../../../features/ui/util/async-components';
const MODAL_COMPONENTS = { const MODAL_COMPONENTS = {
@ -25,6 +26,7 @@ const MODAL_COMPONENTS = {
'REPORT': ReportModal, 'REPORT': ReportModal,
'SETTINGS': SettingsModal, 'SETTINGS': SettingsModal,
'ACTIONS': () => Promise.resolve({ default: ActionsModal }), 'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
'EMBED': EmbedModal,
}; };
export default class ModalRoot extends React.PureComponent { export default class ModalRoot extends React.PureComponent {

View File

@ -30,7 +30,7 @@ const PageOne = ({ acct, domain }) => (
<div> <div>
<h1><FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to {domain}!' values={{ domain }} /></h1> <h1><FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to {domain}!' values={{ domain }} /></h1>
<p><FormattedMessage id='onboarding.page_one.federation' defaultMessage='{domain} is an "instance" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.' values={{ domain }} /></p> <p><FormattedMessage id='onboarding.page_one.federation' defaultMessage='{domain} is an "instance" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.' values={{ domain }} /></p>
<p><FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, so your full handle is {handle}' values={{ domain, handle: <strong>{acct}@{domain}</strong> }} /></p> <p><FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, so your full handle is {handle}' values={{ domain, handle: <strong>@{acct}@{domain}</strong> }} /></p>
</div> </div>
</div> </div>
); );

View File

@ -5,4 +5,4 @@ const mapStateToProps = state => ({
columns: state.getIn(['settings', 'columns']), columns: state.getIn(['settings', 'columns']),
}); });
export default connect(mapStateToProps)(ColumnsArea); export default connect(mapStateToProps, null, null, { withRef: true })(ColumnsArea);

View File

@ -1,12 +1,11 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import Redirect from 'react-router-dom/Redirect';
import NotificationsContainer from './containers/notifications_container'; import NotificationsContainer from './containers/notifications_container';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import LoadingBarContainer from './containers/loading_bar_container'; import LoadingBarContainer from './containers/loading_bar_container';
import TabsBar from './components/tabs_bar'; import TabsBar from './components/tabs_bar';
import ModalContainer from './containers/modal_container'; import ModalContainer from './containers/modal_container';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Redirect, withRouter } from 'react-router-dom';
import { isMobile } from '../../is_mobile'; import { isMobile } from '../../is_mobile';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { uploadCompose } from '../../actions/compose'; import { uploadCompose } from '../../actions/compose';
@ -51,6 +50,7 @@ const mapStateToProps = state => ({
}); });
@connect(mapStateToProps) @connect(mapStateToProps)
@withRouter
export default class UI extends React.PureComponent { export default class UI extends React.PureComponent {
static contextTypes = { static contextTypes = {
@ -65,6 +65,7 @@ export default class UI extends React.PureComponent {
systemFontUi: PropTypes.bool, systemFontUi: PropTypes.bool,
navbarUnder: PropTypes.bool, navbarUnder: PropTypes.bool,
isComposing: PropTypes.bool, isComposing: PropTypes.bool,
location: PropTypes.object,
}; };
state = { state = {
@ -141,7 +142,7 @@ export default class UI extends React.PureComponent {
if (data.type === 'navigate') { if (data.type === 'navigate') {
this.context.router.history.push(data.path); this.context.router.history.push(data.path);
} else { } else {
console.warn('Unknown message type:', data.type); // eslint-disable-line no-console console.warn('Unknown message type:', data.type);
} }
} }
@ -175,6 +176,12 @@ export default class UI extends React.PureComponent {
return true; return true;
} }
componentDidUpdate (prevProps) {
if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) {
this.columnsAreaNode.handleChildrenContentChange();
}
}
componentWillUnmount () { componentWillUnmount () {
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
document.removeEventListener('dragenter', this.handleDragEnter); document.removeEventListener('dragenter', this.handleDragEnter);
@ -188,6 +195,10 @@ export default class UI extends React.PureComponent {
this.node = c; this.node = c;
} }
setColumnsAreaRef = (c) => {
this.columnsAreaNode = c.getWrappedInstance().getWrappedInstance();
}
render () { render () {
const { width, draggingOver } = this.state; const { width, draggingOver } = this.state;
const { children, layout, isWide, navbarUnder } = this.props; const { children, layout, isWide, navbarUnder } = this.props;
@ -212,7 +223,7 @@ export default class UI extends React.PureComponent {
return ( return (
<div className={className} ref={this.setRef}> <div className={className} ref={this.setRef}>
{navbarUnder ? null : (<TabsBar />)} {navbarUnder ? null : (<TabsBar />)}
<ColumnsAreaContainer singleColumn={isMobile(width, layout)}> <ColumnsAreaContainer ref={this.setColumnsAreaRef} singleColumn={isMobile(width, layout)}>
<WrappedSwitch> <WrappedSwitch>
<Redirect from='/' to='/getting-started' exact /> <Redirect from='/' to='/getting-started' exact />
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} /> <WrappedRoute path='/getting-started' component={GettingStarted} content={children} />

View File

@ -116,3 +116,7 @@ export function MediaGallery () {
export function VideoPlayer () { export function VideoPlayer () {
return import(/* webpackChunkName: "status/video_player" */'../../../components/video_player'); return import(/* webpackChunkName: "status/video_player" */'../../../components/video_player');
} }
export function EmbedModal () {
return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal');
}

View File

@ -47,7 +47,7 @@
"compose_form.lock_disclaimer.lock": "مقفل", "compose_form.lock_disclaimer.lock": "مقفل",
"compose_form.placeholder": "فيمَ تفكّر؟", "compose_form.placeholder": "فيمَ تفكّر؟",
"compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.", "compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.",
"compose_form.publish": "بوّق !", "compose_form.publish": "بوّق",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "ضع علامة على الوسيط باعتباره حسّاس", "compose_form.sensitive": "ضع علامة على الوسيط باعتباره حسّاس",
"compose_form.spoiler": "أخفِ النص واعرض تحذيرا", "compose_form.spoiler": "أخفِ النص واعرض تحذيرا",
@ -63,6 +63,8 @@
"confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟", "confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "الأنشطة", "emoji_button.activity": "الأنشطة",
"emoji_button.flags": "الأعلام", "emoji_button.flags": "الأعلام",
"emoji_button.food": "الطعام والشراب", "emoji_button.food": "الطعام والشراب",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "تعذرت ترقية هذا المنشور", "status.cannot_reblog": "تعذرت ترقية هذا المنشور",
"status.delete": "إحذف", "status.delete": "إحذف",
"status.embed": "Embed",
"status.favourite": "أضف إلى المفضلة", "status.favourite": "أضف إلى المفضلة",
"status.load_more": "حمّل المزيد", "status.load_more": "حمّل المزيد",
"status.media_hidden": "الصورة مستترة", "status.media_hidden": "الصورة مستترة",
"status.mention": "أذكُر @{name}", "status.mention": "أذكُر @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "وسع هذه المشاركة", "status.open": "وسع هذه المشاركة",
"status.pin": "Pin on profile",
"status.reblog": "رَقِّي", "status.reblog": "رَقِّي",
"status.reblogged_by": "{name} رقى", "status.reblogged_by": "{name} رقى",
"status.reply": "ردّ", "status.reply": "ردّ",
@ -179,6 +183,7 @@
"status.show_less": "إعرض أقلّ", "status.show_less": "إعرض أقلّ",
"status.show_more": "أظهر المزيد", "status.show_more": "أظهر المزيد",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "تحرير", "tabs_bar.compose": "تحرير",
"tabs_bar.federated_timeline": "الموحَّد", "tabs_bar.federated_timeline": "الموحَّد",
"tabs_bar.home": "الرئيسية", "tabs_bar.home": "الرئيسية",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Изтриване", "status.delete": "Изтриване",
"status.embed": "Embed",
"status.favourite": "Предпочитани", "status.favourite": "Предпочитани",
"status.load_more": "Load more", "status.load_more": "Load more",
"status.media_hidden": "Media hidden", "status.media_hidden": "Media hidden",
"status.mention": "Споменаване", "status.mention": "Споменаване",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "Expand this status", "status.open": "Expand this status",
"status.pin": "Pin on profile",
"status.reblog": "Споделяне", "status.reblog": "Споделяне",
"status.reblogged_by": "{name} сподели", "status.reblogged_by": "{name} сподели",
"status.reply": "Отговор", "status.reply": "Отговор",
@ -179,6 +183,7 @@
"status.show_less": "Show less", "status.show_less": "Show less",
"status.show_more": "Show more", "status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Съставяне", "tabs_bar.compose": "Съставяне",
"tabs_bar.federated_timeline": "Federated", "tabs_bar.federated_timeline": "Federated",
"tabs_bar.home": "Начало", "tabs_bar.home": "Начало",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "Estàs segur que vols silenciar {name}?", "confirmations.mute.message": "Estàs segur que vols silenciar {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activitat", "emoji_button.activity": "Activitat",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Menjar i Beure", "emoji_button.food": "Menjar i Beure",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "Aquesta publicació no pot ser retootejada", "status.cannot_reblog": "Aquesta publicació no pot ser retootejada",
"status.delete": "Esborrar", "status.delete": "Esborrar",
"status.embed": "Embed",
"status.favourite": "Favorit", "status.favourite": "Favorit",
"status.load_more": "Carrega més", "status.load_more": "Carrega més",
"status.media_hidden": "Multimèdia amagat", "status.media_hidden": "Multimèdia amagat",
"status.mention": "Esmentar @{name}", "status.mention": "Esmentar @{name}",
"status.mute_conversation": "Silenciar conversació", "status.mute_conversation": "Silenciar conversació",
"status.open": "Ampliar aquest estat", "status.open": "Ampliar aquest estat",
"status.pin": "Pin on profile",
"status.reblog": "Boost", "status.reblog": "Boost",
"status.reblogged_by": "{name} ha retootejat", "status.reblogged_by": "{name} ha retootejat",
"status.reply": "Respondre", "status.reply": "Respondre",
@ -179,6 +183,7 @@
"status.show_less": "Mostra menys", "status.show_less": "Mostra menys",
"status.show_more": "Mostra més", "status.show_more": "Mostra més",
"status.unmute_conversation": "Activar conversació", "status.unmute_conversation": "Activar conversació",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Compondre", "tabs_bar.compose": "Compondre",
"tabs_bar.federated_timeline": "Federada", "tabs_bar.federated_timeline": "Federada",
"tabs_bar.home": "Inici", "tabs_bar.home": "Inici",

View File

@ -1,31 +1,31 @@
{ {
"account.block": "@{name} blocken", "account.block": "@{name} blocken",
"account.block_domain": "Hide everything from {domain}", "account.block_domain": "Alles von {domain} verstecken",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.", "account.disclaimer_full": "Hier aufgeführten Informationen können unvollständig sein.",
"account.edit_profile": "Profil bearbeiten", "account.edit_profile": "Profil bearbeiten",
"account.follow": "Folgen", "account.follow": "Folgen",
"account.followers": "Folgende", "account.followers": "Folgende",
"account.follows": "Folgt", "account.follows": "Folgt",
"account.follows_you": "Folgt dir", "account.follows_you": "Folgt dir",
"account.media": "Media", "account.media": "Medien",
"account.mention": "@{name} erwähnen", "account.mention": "@{name} erwähnen",
"account.mute": "@{name} stummschalten", "account.mute": "@{name} stummschalten",
"account.posts": "Beiträge", "account.posts": "Beiträge",
"account.report": "@{name} melden", "account.report": "@{name} melden",
"account.requested": "Warte auf Erlaubnis", "account.requested": "Warte auf Erlaubnis. Klicke zum Abbrechen",
"account.share": "Share @{name}'s profile", "account.share": "Profil von @{name} teilen",
"account.unblock": "@{name} entblocken", "account.unblock": "@{name} entblocken",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "{domain} wieder anzeigen",
"account.unfollow": "Entfolgen", "account.unfollow": "Entfolgen",
"account.unmute": "@{name} nicht mehr stummschalten", "account.unmute": "@{name} nicht mehr stummschalten",
"account.view_full_profile": "View full profile", "account.view_full_profile": "Komplettes Profil anzeigen",
"boost_modal.combo": "Du kannst {combo} drücken, um dies beim nächsten Mal zu überspringen", "boost_modal.combo": "Du kannst {combo} drücken, um dies beim nächsten Mal zu überspringen",
"bundle_column_error.body": "Something went wrong while loading this component.", "bundle_column_error.body": "Etwas ist beim Laden schiefgelaufen.",
"bundle_column_error.retry": "Try again", "bundle_column_error.retry": "Erneut versuchen",
"bundle_column_error.title": "Network error", "bundle_column_error.title": "Netzwerkfehlher",
"bundle_modal_error.close": "Close", "bundle_modal_error.close": "Schließen",
"bundle_modal_error.message": "Something went wrong while loading this component.", "bundle_modal_error.message": "Etwas ist beim Laden schiefgelaufen.",
"bundle_modal_error.retry": "Try again", "bundle_modal_error.retry": "Erneut versuchen",
"column.blocks": "Blockierte Benutzer", "column.blocks": "Blockierte Benutzer",
"column.community": "Lokale Zeitleiste", "column.community": "Lokale Zeitleiste",
"column.favourites": "Favoriten", "column.favourites": "Favoriten",
@ -35,16 +35,16 @@
"column.notifications": "Mitteilungen", "column.notifications": "Mitteilungen",
"column.public": "Gesamtes bekanntes Netz", "column.public": "Gesamtes bekanntes Netz",
"column_back_button.label": "Zurück", "column_back_button.label": "Zurück",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Einstellungen verbergen",
"column_header.moveLeft_settings": "Move column to the left", "column_header.moveLeft_settings": "Spalte links verschieben",
"column_header.moveRight_settings": "Move column to the right", "column_header.moveRight_settings": "Spalte rechts verschieben",
"column_header.pin": "Pin", "column_header.pin": "Anheften",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Einstellungen anzeigen",
"column_header.unpin": "Unpin", "column_header.unpin": "Lösen",
"column_subheading.navigation": "Navigation", "column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings", "column_subheading.settings": "Einstellungen",
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", "compose_form.lock_disclaimer": "Dein Profil ist nicht {locked}. Jeder kann dir jederzeit folgen, um deine privaten Beiträge einzusehen.",
"compose_form.lock_disclaimer.lock": "locked", "compose_form.lock_disclaimer.lock": "gesperrt",
"compose_form.placeholder": "Worüber möchtest du schreiben?", "compose_form.placeholder": "Worüber möchtest du schreiben?",
"compose_form.privacy_disclaimer": "Dein privater Status wird an die genannten Benutzer auf den Domains {domains} zugestellt. Vertraust du {domainsCount, plural, one {diesem Server} other {diesen Servern}}? Private Beiträge funktionieren nur auf Mastodon-Instanzen. Wenn {domains} {domainsCount, plural, one {keine Mastodon-Instanz ist} other {keine Mastodon-Instanzen sind}}, wird es dort kein Anzeichen geben, dass dein Beitrag privat ist und er könnte geteilt oder anderweitig für unerwünschte Empfänger sichtbar gemacht werden.", "compose_form.privacy_disclaimer": "Dein privater Status wird an die genannten Benutzer auf den Domains {domains} zugestellt. Vertraust du {domainsCount, plural, one {diesem Server} other {diesen Servern}}? Private Beiträge funktionieren nur auf Mastodon-Instanzen. Wenn {domains} {domainsCount, plural, one {keine Mastodon-Instanz ist} other {keine Mastodon-Instanzen sind}}, wird es dort kein Anzeichen geben, dass dein Beitrag privat ist und er könnte geteilt oder anderweitig für unerwünschte Empfänger sichtbar gemacht werden.",
"compose_form.publish": "Tröt", "compose_form.publish": "Tröt",
@ -52,41 +52,43 @@
"compose_form.sensitive": "Medien als heikel markieren", "compose_form.sensitive": "Medien als heikel markieren",
"compose_form.spoiler": "Text hinter Warnung verbergen", "compose_form.spoiler": "Text hinter Warnung verbergen",
"compose_form.spoiler_placeholder": "Inhaltswarnung", "compose_form.spoiler_placeholder": "Inhaltswarnung",
"confirmation_modal.cancel": "Cancel", "confirmation_modal.cancel": "Abbrechen",
"confirmations.block.confirm": "Block", "confirmations.block.confirm": "Blockieren",
"confirmations.block.message": "Are you sure you want to block {name}?", "confirmations.block.message": "Bist du dir sicher, dass du {name} blockieren möchtest?",
"confirmations.delete.confirm": "Delete", "confirmations.delete.confirm": "Löschen",
"confirmations.delete.message": "Are you sure you want to delete this status?", "confirmations.delete.message": "Bist du dir sicher, dass du diesen Beitrag löschen möchstest?",
"confirmations.domain_block.confirm": "Hide entire domain", "confirmations.domain_block.confirm": "Die ganze Domain verbergen",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", "confirmations.domain_block.message": "Bist du dir wirklich sicher, dass du die ganze Domain {domain} verbergen willst? In den meisten Fällen sind ein paar gezielte Blocks genug.",
"confirmations.mute.confirm": "Mute", "confirmations.mute.confirm": "Stummschalten",
"confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.mute.message": "Bist du dir sicher, dass du {name} stummschalten möchstest?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Entfolgen",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Bist du dir sicher, dass du {name} entfolgen möchstest?",
"emoji_button.activity": "Activity", "embed.instructions": "Du kannst diesen Beitrag auf deiner Webseite einbetten, in dem du den folgenden Code einfügst.",
"emoji_button.flags": "Flags", "embed.preview": "So wird es aussehen:",
"emoji_button.food": "Food & Drink", "emoji_button.activity": "Aktivitäten",
"emoji_button.flags": "Flaggen",
"emoji_button.food": "Essen und Trinken",
"emoji_button.label": "Emoji einfügen", "emoji_button.label": "Emoji einfügen",
"emoji_button.nature": "Nature", "emoji_button.nature": "Natur",
"emoji_button.objects": "Objects", "emoji_button.objects": "Dinge",
"emoji_button.people": "People", "emoji_button.people": "Leute",
"emoji_button.search": "Search...", "emoji_button.search": "Suche…",
"emoji_button.symbols": "Symbols", "emoji_button.symbols": "Symbole",
"emoji_button.travel": "Travel & Places", "emoji_button.travel": "Reise und Orte",
"empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe etwas öffentlich, um den Ball ins Rollen zu bringen!", "empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe etwas öffentlich, um den Ball ins Rollen zu bringen!",
"empty_column.hashtag": "Es gibt noch nichts unter diesem Hashtag.", "empty_column.hashtag": "Es gibt noch nichts unter diesem Hashtag.",
"empty_column.home": "Du folgst noch niemandem. Besuche {public} oder benutze die Suche, um zu starten oder andere Benutzer anzutreffen.", "empty_column.home": "Du folgst noch niemandem. Besuche {public} oder benutze die Suche, um zu starten oder andere Benutzer anzutreffen.",
"empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.", "empty_column.home.inactivity": "Deine Zeitleiste ist leer. Falls du eine längere Zeit inaktiv gewesen bist, wird sie für dich so schnell wie möglich wiedererstellt.",
"empty_column.home.public_timeline": "die öffentliche Zeitleiste", "empty_column.home.public_timeline": "die öffentliche Zeitleiste",
"empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um die Konversation zu starten.", "empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um die Konversation zu starten.",
"empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Benutzern von anderen Instanzen, um es aufzufüllen.", "empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Benutzern von anderen Instanzen, um es aufzufüllen.",
"follow_request.authorize": "Erlauben", "follow_request.authorize": "Erlauben",
"follow_request.reject": "Ablehnen", "follow_request.reject": "Ablehnen",
"getting_started.appsshort": "Apps", "getting_started.appsshort": "Anwendungen",
"getting_started.faq": "FAQ", "getting_started.faq": "Häufig gestellte Fragen",
"getting_started.heading": "Erste Schritte", "getting_started.heading": "Erste Schritte",
"getting_started.open_source_notice": "Mastodon ist quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.", "getting_started.open_source_notice": "Mastodon ist quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.",
"getting_started.userguide": "User Guide", "getting_started.userguide": "Nutzeranleitung",
"home.column_settings.advanced": "Fortgeschritten", "home.column_settings.advanced": "Fortgeschritten",
"home.column_settings.basic": "Einfach", "home.column_settings.basic": "Einfach",
"home.column_settings.filter_regex": "Filter durch reguläre Ausdrücke", "home.column_settings.filter_regex": "Filter durch reguläre Ausdrücke",
@ -94,8 +96,8 @@
"home.column_settings.show_replies": "Antworten anzeigen", "home.column_settings.show_replies": "Antworten anzeigen",
"home.settings": "Spalteneinstellungen", "home.settings": "Spalteneinstellungen",
"lightbox.close": "Schließen", "lightbox.close": "Schließen",
"lightbox.next": "Next", "lightbox.next": "Weiter",
"lightbox.previous": "Previous", "lightbox.previous": "Zurück",
"loading_indicator.label": "Lade…", "loading_indicator.label": "Lade…",
"media_gallery.toggle_visible": "Sichtbarkeit einstellen", "media_gallery.toggle_visible": "Sichtbarkeit einstellen",
"missing_indicator.label": "Nicht gefunden", "missing_indicator.label": "Nicht gefunden",
@ -113,8 +115,8 @@
"notification.follow": "{name} folgt dir", "notification.follow": "{name} folgt dir",
"notification.mention": "{name} erwähnte dich", "notification.mention": "{name} erwähnte dich",
"notification.reblog": "{name} teilte deinen Status", "notification.reblog": "{name} teilte deinen Status",
"notifications.clear": "Mitteilungen beseitigen", "notifications.clear": "Mitteilungen löschen",
"notifications.clear_confirmation": "Bist du sicher, dass du alle Mitteilungen beseitigen willst?", "notifications.clear_confirmation": "Bist du dir sicher, dass du alle Mitteilungen löschen möchstest?",
"notifications.column_settings.alert": "Desktop-Benachrichtigungen", "notifications.column_settings.alert": "Desktop-Benachrichtigungen",
"notifications.column_settings.favourite": "Favorisierungen:", "notifications.column_settings.favourite": "Favorisierungen:",
"notifications.column_settings.follow": "Neue Folgende:", "notifications.column_settings.follow": "Neue Folgende:",
@ -124,26 +126,26 @@
"notifications.column_settings.reblog": "Geteilte Beiträge:", "notifications.column_settings.reblog": "Geteilte Beiträge:",
"notifications.column_settings.show": "In der Spalte anzeigen", "notifications.column_settings.show": "In der Spalte anzeigen",
"notifications.column_settings.sound": "Ton abspielen", "notifications.column_settings.sound": "Ton abspielen",
"onboarding.done": "Done", "onboarding.done": "Fertig",
"onboarding.next": "Next", "onboarding.next": "Weiter",
"onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", "onboarding.page_five.public_timelines": "Die lokale Zeitleiste zeigt alle Beiträge von Leuten, die auch auf deiner Instanz {domain} sind. Das gesamte bekannte Netz zeigt Beiträge von allen, die kollektiv aus deiner Instanz heraus gefolgt werden. Zusammen werden die beiden Leisten auch öffentliche Zeitleisten genannt, durch sie kannst du viel neues entdecken.",
"onboarding.page_four.home": "The home timeline shows posts from people you follow.", "onboarding.page_four.home": "Die Startseite zeigt dir Beiträge von Leuten, denen du folgst.",
"onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", "onboarding.page_four.notifications": "Wenn jemand mir dir interagiert, bekommst du eine Mitteilung.",
"onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_one.federation": "Mastodon ist ein soziales Netzwerk, das aus unabhängigen Servern besteht. Diese Server nennen wir auch Instanzen.",
"onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", "onboarding.page_one.handle": "Du bist auf der Instanz {domain}, also ist dein vollständiger Nutzername im Netzwerk {handle}",
"onboarding.page_one.welcome": "Welcome to Mastodon!", "onboarding.page_one.welcome": "Willkommen bei Mastodon!",
"onboarding.page_six.admin": "Your instance's admin is {admin}.", "onboarding.page_six.admin": "Für deine Instanz ist {admin} zuständig.",
"onboarding.page_six.almost_done": "Almost done...", "onboarding.page_six.almost_done": "Fast fertig…",
"onboarding.page_six.appetoot": "Bon Appetoot!", "onboarding.page_six.appetoot": "Guten Appetröt!",
"onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", "onboarding.page_six.apps_available": "Es gibt verschiedene {apps} für iOS, Android und andere Plattformen.",
"onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "onboarding.page_six.github": "Mastodon ist freie, quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.",
"onboarding.page_six.guidelines": "community guidelines", "onboarding.page_six.guidelines": "Richtlinien",
"onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", "onboarding.page_six.read_guidelines": "Bitte mach dich mit den {guidelines} von {domain} vertraut!",
"onboarding.page_six.various_app": "mobile apps", "onboarding.page_six.various_app": "mobile Anwendungen",
"onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", "onboarding.page_three.profile": "Bearbeite dein Profil, um dein Bild, deinen Namen oder deine Beschreibung anzupassen. Dort findest du auch andere Einstellungen.",
"onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", "onboarding.page_three.search": "Benutze die Suchfunktion, um Leute oder Themen zu finden. Zum Beispiel, die Hashtags {illustration} oder {introductions}. Um eine Person zu finden, die auf einer anderen Instanz ist, benutze den vollständigen Nutzernamen.",
"onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", "onboarding.page_two.compose": "Schreibe Beiträge aus der Schreiben-Spalte. Du kannst Bilder und kurze Videos hochladen, Sichtbarkeitseinstellungen ändern und Inhaltswarnungen hinzufügen.",
"onboarding.skip": "Skip", "onboarding.skip": "Überspringen",
"privacy.change": "Privatsphäre des Status anpassen", "privacy.change": "Privatsphäre des Status anpassen",
"privacy.direct.long": "Beitrag nur an erwähnte Benutzer", "privacy.direct.long": "Beitrag nur an erwähnte Benutzer",
"privacy.direct.short": "Direkt", "privacy.direct.short": "Direkt",
@ -159,15 +161,17 @@
"report.target": "Melden", "report.target": "Melden",
"search.placeholder": "Suche", "search.placeholder": "Suche",
"search_results.total": "{count, number} {count, plural, one {Ergebnis} other {Ergebnisse}}", "search_results.total": "{count, number} {count, plural, one {Ergebnis} other {Ergebnisse}}",
"standalone.public_title": "A look inside...", "standalone.public_title": "Vorschau…",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden",
"status.delete": "Löschen", "status.delete": "Löschen",
"status.embed": "Einbetten",
"status.favourite": "Favorisieren", "status.favourite": "Favorisieren",
"status.load_more": "Weitere laden", "status.load_more": "Weitere laden",
"status.media_hidden": "Medien versteckt", "status.media_hidden": "Medien versteckt",
"status.mention": "Erwähnen", "status.mention": "Erwähnen",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Thread stummschalten",
"status.open": "Öffnen", "status.open": "Öffnen",
"status.pin": "Auf dem Profil anheften",
"status.reblog": "Teilen", "status.reblog": "Teilen",
"status.reblogged_by": "{name} teilte", "status.reblogged_by": "{name} teilte",
"status.reply": "Antworten", "status.reply": "Antworten",
@ -175,13 +179,14 @@
"status.report": "@{name} melden", "status.report": "@{name} melden",
"status.sensitive_toggle": "Klicke, um sie zu sehen", "status.sensitive_toggle": "Klicke, um sie zu sehen",
"status.sensitive_warning": "Heikle Inhalte", "status.sensitive_warning": "Heikle Inhalte",
"status.share": "Share", "status.share": "Teilen",
"status.show_less": "Weniger anzeigen", "status.show_less": "Weniger anzeigen",
"status.show_more": "Mehr anzeigen", "status.show_more": "Mehr anzeigen",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Stummschaltung von Thread aufheben",
"status.unpin": "Vom Profil lösen",
"tabs_bar.compose": "Schreiben", "tabs_bar.compose": "Schreiben",
"tabs_bar.federated_timeline": "Föderation", "tabs_bar.federated_timeline": "Föderation",
"tabs_bar.home": "Home", "tabs_bar.home": "Startseite",
"tabs_bar.local_timeline": "Lokal", "tabs_bar.local_timeline": "Lokal",
"tabs_bar.notifications": "Mitteilungen", "tabs_bar.notifications": "Mitteilungen",
"upload_area.title": "Hereinziehen zum Hochladen", "upload_area.title": "Hereinziehen zum Hochladen",

View File

@ -189,6 +189,18 @@
{ {
"defaultMessage": "Unmute conversation", "defaultMessage": "Unmute conversation",
"id": "status.unmute_conversation" "id": "status.unmute_conversation"
},
{
"defaultMessage": "Pin on profile",
"id": "status.pin"
},
{
"defaultMessage": "Unpin from profile",
"id": "status.unpin"
},
{
"defaultMessage": "Embed",
"id": "status.embed"
} }
], ],
"path": "app/javascript/mastodon/components/status_action_bar.json" "path": "app/javascript/mastodon/components/status_action_bar.json"
@ -424,7 +436,7 @@
"id": "account.follow" "id": "account.follow"
}, },
{ {
"defaultMessage": "Awaiting approval", "defaultMessage": "Awaiting approval. Click to cancel follow request",
"id": "account.requested" "id": "account.requested"
}, },
{ {
@ -1035,6 +1047,18 @@
{ {
"defaultMessage": "Share", "defaultMessage": "Share",
"id": "status.share" "id": "status.share"
},
{
"defaultMessage": "Pin on profile",
"id": "status.pin"
},
{
"defaultMessage": "Unpin from profile",
"id": "status.unpin"
},
{
"defaultMessage": "Embed",
"id": "status.embed"
} }
], ],
"path": "app/javascript/mastodon/features/status/components/action_bar.json" "path": "app/javascript/mastodon/features/status/components/action_bar.json"
@ -1108,6 +1132,23 @@
], ],
"path": "app/javascript/mastodon/features/ui/components/confirmation_modal.json" "path": "app/javascript/mastodon/features/ui/components/confirmation_modal.json"
}, },
{
"descriptors": [
{
"defaultMessage": "Embed",
"id": "status.embed"
},
{
"defaultMessage": "Embed this status on your website by copying the code below.",
"id": "embed.instructions"
},
{
"defaultMessage": "Here is what it will look like:",
"id": "embed.preview"
}
],
"path": "app/javascript/mastodon/features/ui/components/embed_modal.json"
},
{ {
"descriptors": [ "descriptors": [
{ {

View File

@ -12,7 +12,7 @@
"account.mute": "Mute @{name}", "account.mute": "Mute @{name}",
"account.posts": "Posts", "account.posts": "Posts",
"account.report": "Report @{name}", "account.report": "Report @{name}",
"account.requested": "Awaiting approval", "account.requested": "Awaiting approval. Click to cancel follow request",
"account.share": "Share @{name}'s profile", "account.share": "Share @{name}'s profile",
"account.unblock": "Unblock @{name}", "account.unblock": "Unblock @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Unhide {domain}",
@ -63,6 +63,8 @@
"confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Delete", "status.delete": "Delete",
"status.embed": "Embed",
"status.favourite": "Favourite", "status.favourite": "Favourite",
"status.load_more": "Load more", "status.load_more": "Load more",
"status.media_hidden": "Media hidden", "status.media_hidden": "Media hidden",
"status.mention": "Mention @{name}", "status.mention": "Mention @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "Expand this status", "status.open": "Expand this status",
"status.pin": "Pin on profile",
"status.reblog": "Boost", "status.reblog": "Boost",
"status.reblogged_by": "{name} boosted", "status.reblogged_by": "{name} boosted",
"status.reply": "Reply", "status.reply": "Reply",
@ -179,6 +183,7 @@
"status.show_less": "Show less", "status.show_less": "Show less",
"status.show_more": "Show more", "status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Compose", "tabs_bar.compose": "Compose",
"tabs_bar.federated_timeline": "Federated", "tabs_bar.federated_timeline": "Federated",
"tabs_bar.home": "Home", "tabs_bar.home": "Home",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Forigi", "status.delete": "Forigi",
"status.embed": "Embed",
"status.favourite": "Favori", "status.favourite": "Favori",
"status.load_more": "Load more", "status.load_more": "Load more",
"status.media_hidden": "Media hidden", "status.media_hidden": "Media hidden",
"status.mention": "Mencii @{name}", "status.mention": "Mencii @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "Expand this status", "status.open": "Expand this status",
"status.pin": "Pin on profile",
"status.reblog": "Diskonigi", "status.reblog": "Diskonigi",
"status.reblogged_by": "{name} diskonigita", "status.reblogged_by": "{name} diskonigita",
"status.reply": "Respondi", "status.reply": "Respondi",
@ -179,6 +183,7 @@
"status.show_less": "Show less", "status.show_less": "Show less",
"status.show_more": "Show more", "status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Ekskribi", "tabs_bar.compose": "Ekskribi",
"tabs_bar.federated_timeline": "Federated", "tabs_bar.federated_timeline": "Federated",
"tabs_bar.home": "Hejmo", "tabs_bar.home": "Hejmo",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Borrar", "status.delete": "Borrar",
"status.embed": "Embed",
"status.favourite": "Favorito", "status.favourite": "Favorito",
"status.load_more": "Load more", "status.load_more": "Load more",
"status.media_hidden": "Media hidden", "status.media_hidden": "Media hidden",
"status.mention": "Mencionar", "status.mention": "Mencionar",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "Expandir estado", "status.open": "Expandir estado",
"status.pin": "Pin on profile",
"status.reblog": "Retoot", "status.reblog": "Retoot",
"status.reblogged_by": "Retooteado por {name}", "status.reblogged_by": "Retooteado por {name}",
"status.reply": "Responder", "status.reply": "Responder",
@ -179,6 +183,7 @@
"status.show_less": "Mostrar menos", "status.show_less": "Mostrar menos",
"status.show_more": "Mostrar más", "status.show_more": "Mostrar más",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Redactar", "tabs_bar.compose": "Redactar",
"tabs_bar.federated_timeline": "Federated", "tabs_bar.federated_timeline": "Federated",
"tabs_bar.home": "Inicio", "tabs_bar.home": "Inicio",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "آیا واقعاً می‌خواهید {name} را بی‌صدا کنید؟", "confirmations.mute.message": "آیا واقعاً می‌خواهید {name} را بی‌صدا کنید؟",
"confirmations.unfollow.confirm": "لغو پیگیری", "confirmations.unfollow.confirm": "لغو پیگیری",
"confirmations.unfollow.message": "آیا واقعاً می‌خواهید به پیگیری از {name} پایان دهید؟", "confirmations.unfollow.message": "آیا واقعاً می‌خواهید به پیگیری از {name} پایان دهید؟",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "فعالیت", "emoji_button.activity": "فعالیت",
"emoji_button.flags": "پرچم‌ها", "emoji_button.flags": "پرچم‌ها",
"emoji_button.food": "غذا و نوشیدنی", "emoji_button.food": "غذا و نوشیدنی",
@ -162,12 +164,14 @@
"standalone.public_title": "نگاهی به کاربران این سرور...", "standalone.public_title": "نگاهی به کاربران این سرور...",
"status.cannot_reblog": "این نوشته را نمی‌شود بازبوقید", "status.cannot_reblog": "این نوشته را نمی‌شود بازبوقید",
"status.delete": "پاک‌کردن", "status.delete": "پاک‌کردن",
"status.embed": "Embed",
"status.favourite": "پسندیدن", "status.favourite": "پسندیدن",
"status.load_more": "بیشتر نشان بده", "status.load_more": "بیشتر نشان بده",
"status.media_hidden": "تصویر پنهان شده", "status.media_hidden": "تصویر پنهان شده",
"status.mention": "نام‌بردن از @{name}", "status.mention": "نام‌بردن از @{name}",
"status.mute_conversation": "بی‌صداکردن گفتگو", "status.mute_conversation": "بی‌صداکردن گفتگو",
"status.open": "این نوشته را باز کن", "status.open": "این نوشته را باز کن",
"status.pin": "Pin on profile",
"status.reblog": "بازبوقیدن", "status.reblog": "بازبوقیدن",
"status.reblogged_by": "{name} بازبوقید", "status.reblogged_by": "{name} بازبوقید",
"status.reply": "پاسخ", "status.reply": "پاسخ",
@ -179,6 +183,7 @@
"status.show_less": "نهفتن", "status.show_less": "نهفتن",
"status.show_more": "نمایش", "status.show_more": "نمایش",
"status.unmute_conversation": "باصداکردن گفتگو", "status.unmute_conversation": "باصداکردن گفتگو",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "بنویسید", "tabs_bar.compose": "بنویسید",
"tabs_bar.federated_timeline": "همگانی", "tabs_bar.federated_timeline": "همگانی",
"tabs_bar.home": "خانه", "tabs_bar.home": "خانه",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Poista", "status.delete": "Poista",
"status.embed": "Embed",
"status.favourite": "Tykkää", "status.favourite": "Tykkää",
"status.load_more": "Load more", "status.load_more": "Load more",
"status.media_hidden": "Media hidden", "status.media_hidden": "Media hidden",
"status.mention": "Mainitse @{name}", "status.mention": "Mainitse @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "Expand this status", "status.open": "Expand this status",
"status.pin": "Pin on profile",
"status.reblog": "Buustaa", "status.reblog": "Buustaa",
"status.reblogged_by": "{name} buustasi", "status.reblogged_by": "{name} buustasi",
"status.reply": "Vastaa", "status.reply": "Vastaa",
@ -179,6 +183,7 @@
"status.show_less": "Show less", "status.show_less": "Show less",
"status.show_more": "Show more", "status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Luo", "tabs_bar.compose": "Luo",
"tabs_bar.federated_timeline": "Federated", "tabs_bar.federated_timeline": "Federated",
"tabs_bar.home": "Koti", "tabs_bar.home": "Koti",

View File

@ -20,11 +20,11 @@
"account.unmute": "Ne plus masquer", "account.unmute": "Ne plus masquer",
"account.view_full_profile": "Afficher le profil complet", "account.view_full_profile": "Afficher le profil complet",
"boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois", "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois",
"bundle_column_error.body": "Une erreur s'est produite lors du chargement de ce composant.", "bundle_column_error.body": "Une erreur sest produite lors du chargement de ce composant.",
"bundle_column_error.retry": "Réessayer", "bundle_column_error.retry": "Réessayer",
"bundle_column_error.title": "Erreur réseau", "bundle_column_error.title": "Erreur réseau",
"bundle_modal_error.close": "Fermer", "bundle_modal_error.close": "Fermer",
"bundle_modal_error.message": "Une erreur s'est produite lors du chargement de ce composant.", "bundle_modal_error.message": "Une erreur sest produite lors du chargement de ce composant.",
"bundle_modal_error.retry": "Réessayer", "bundle_modal_error.retry": "Réessayer",
"column.blocks": "Comptes bloqués", "column.blocks": "Comptes bloqués",
"column.community": "Fil public local", "column.community": "Fil public local",
@ -43,12 +43,12 @@
"column_header.unpin": "Retirer", "column_header.unpin": "Retirer",
"column_subheading.navigation": "Navigation", "column_subheading.navigation": "Navigation",
"column_subheading.settings": "Paramètres", "column_subheading.settings": "Paramètres",
"compose_form.lock_disclaimer": "Votre compte nest pas {locked}. Tout le monde peut vous suivre et voir vos pouets restreints.", "compose_form.lock_disclaimer": "Votre compte nest pas {locked}. Tout le monde peut vous suivre et voir vos pouets privés.",
"compose_form.lock_disclaimer.lock": "verrouillé", "compose_form.lock_disclaimer.lock": "verrouillé",
"compose_form.placeholder": "Quavez-vous en tête?", "compose_form.placeholder": "Quavez-vous en tête?",
"compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut? Les statuts privés ne fonctionnent que sur les instances de Mastodon. Si {domains} {domainsCount, plural, one {nest pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il ny aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible dune autre manière à dautres personnes imprévues.", "compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut? Les statuts privés ne fonctionnent que sur les instances de Mastodon. Si {domains} {domainsCount, plural, one {nest pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il ny aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible dune autre manière à dautres personnes imprévues.",
"compose_form.publish": "Pouet ", "compose_form.publish": "Pouet ",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marquer le média comme sensible", "compose_form.sensitive": "Marquer le média comme sensible",
"compose_form.spoiler": "Masquer le texte derrière un avertissement", "compose_form.spoiler": "Masquer le texte derrière un avertissement",
"compose_form.spoiler_placeholder": "Écrivez ici votre avertissement", "compose_form.spoiler_placeholder": "Écrivez ici votre avertissement",
@ -62,7 +62,9 @@
"confirmations.mute.confirm": "Masquer", "confirmations.mute.confirm": "Masquer",
"confirmations.mute.message": "Confirmez vous le masquage de {name}?", "confirmations.mute.message": "Confirmez vous le masquage de {name}?",
"confirmations.unfollow.confirm": "Ne plus suivre", "confirmations.unfollow.confirm": "Ne plus suivre",
"confirmations.unfollow.message": "Vous voulez-vous arrêter de suivre {name} ?", "confirmations.unfollow.message": "Vous voulez-vous arrêter de suivre {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activités", "emoji_button.activity": "Activités",
"emoji_button.flags": "Drapeaux", "emoji_button.flags": "Drapeaux",
"emoji_button.food": "Boire et manger", "emoji_button.food": "Boire et manger",
@ -134,8 +136,8 @@
"onboarding.page_one.welcome": "Bienvenue sur Mastodon!", "onboarding.page_one.welcome": "Bienvenue sur Mastodon!",
"onboarding.page_six.admin": "Ladministrateur⋅trice de votre instance est {admin}", "onboarding.page_six.admin": "Ladministrateur⋅trice de votre instance est {admin}",
"onboarding.page_six.almost_done": "Nous y sommes presque…", "onboarding.page_six.almost_done": "Nous y sommes presque…",
"onboarding.page_six.appetoot": "Bon Appétoot!", "onboarding.page_six.appetoot": "Bon appouétit!",
"onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres. Et maintenant… Bon Appétoot!", "onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres. Et maintenant… Bon appouétit!",
"onboarding.page_six.github": "Mastodon est un logiciel libre, gratuit et open-source. Vous pouvez rapporter des bogues, suggérer des fonctionnalités, ou contribuer à son développement sur {github}.", "onboarding.page_six.github": "Mastodon est un logiciel libre, gratuit et open-source. Vous pouvez rapporter des bogues, suggérer des fonctionnalités, ou contribuer à son développement sur {github}.",
"onboarding.page_six.guidelines": "règles de la communauté", "onboarding.page_six.guidelines": "règles de la communauté",
"onboarding.page_six.read_guidelines": "Sil vous plaît, noubliez pas de lire les {guidelines}!", "onboarding.page_six.read_guidelines": "Sil vous plaît, noubliez pas de lire les {guidelines}!",
@ -159,15 +161,17 @@
"report.target": "Signalement", "report.target": "Signalement",
"search.placeholder": "Rechercher", "search.placeholder": "Rechercher",
"search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}", "search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}",
"standalone.public_title": "Coup d'œil", "standalone.public_title": "Jeter un coup dœil…",
"status.cannot_reblog": "Cette publication ne peut être boostée", "status.cannot_reblog": "Cette publication ne peut être boostée",
"status.delete": "Effacer", "status.delete": "Effacer",
"status.embed": "Embed",
"status.favourite": "Ajouter aux favoris", "status.favourite": "Ajouter aux favoris",
"status.load_more": "Charger plus", "status.load_more": "Charger plus",
"status.media_hidden": "Média caché", "status.media_hidden": "Média caché",
"status.mention": "Mentionner", "status.mention": "Mentionner",
"status.mute_conversation": "Masquer la conversation", "status.mute_conversation": "Masquer la conversation",
"status.open": "Déplier ce statut", "status.open": "Déplier ce statut",
"status.pin": "Épingler sur le profil",
"status.reblog": "Partager", "status.reblog": "Partager",
"status.reblogged_by": "{name} a partagé:", "status.reblogged_by": "{name} a partagé:",
"status.reply": "Répondre", "status.reply": "Répondre",
@ -179,6 +183,7 @@
"status.show_less": "Replier", "status.show_less": "Replier",
"status.show_more": "Déplier", "status.show_more": "Déplier",
"status.unmute_conversation": "Ne plus masquer la conversation", "status.unmute_conversation": "Ne plus masquer la conversation",
"status.unpin": "Retirer du profil",
"tabs_bar.compose": "Composer", "tabs_bar.compose": "Composer",
"tabs_bar.federated_timeline": "Fil public global", "tabs_bar.federated_timeline": "Fil public global",
"tabs_bar.home": "Accueil", "tabs_bar.home": "Accueil",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "להשתיק את {name}?", "confirmations.mute.message": "להשתיק את {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "פעילות", "emoji_button.activity": "פעילות",
"emoji_button.flags": "דגלים", "emoji_button.flags": "דגלים",
"emoji_button.food": "אוכל ושתיה", "emoji_button.food": "אוכל ושתיה",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "לא ניתן להדהד הודעה זו", "status.cannot_reblog": "לא ניתן להדהד הודעה זו",
"status.delete": "מחיקה", "status.delete": "מחיקה",
"status.embed": "Embed",
"status.favourite": "חיבוב", "status.favourite": "חיבוב",
"status.load_more": "עוד", "status.load_more": "עוד",
"status.media_hidden": "מדיה מוסתרת", "status.media_hidden": "מדיה מוסתרת",
"status.mention": "פניה אל @{name}", "status.mention": "פניה אל @{name}",
"status.mute_conversation": "השתקת שיחה", "status.mute_conversation": "השתקת שיחה",
"status.open": "הרחבת הודעה", "status.open": "הרחבת הודעה",
"status.pin": "Pin on profile",
"status.reblog": "הדהוד", "status.reblog": "הדהוד",
"status.reblogged_by": "הודהד על ידי {name}", "status.reblogged_by": "הודהד על ידי {name}",
"status.reply": "תגובה", "status.reply": "תגובה",
@ -179,6 +183,7 @@
"status.show_less": "הראה פחות", "status.show_less": "הראה פחות",
"status.show_more": "הראה יותר", "status.show_more": "הראה יותר",
"status.unmute_conversation": "הסרת השתקת שיחה", "status.unmute_conversation": "הסרת השתקת שיחה",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "חיבור", "tabs_bar.compose": "חיבור",
"tabs_bar.federated_timeline": "ציר זמן בין-קהילתי", "tabs_bar.federated_timeline": "ציר זמן בין-קהילתי",
"tabs_bar.home": "בבית", "tabs_bar.home": "בבית",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?", "confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Aktivnost", "emoji_button.activity": "Aktivnost",
"emoji_button.flags": "Zastave", "emoji_button.flags": "Zastave",
"emoji_button.food": "Hrana & Piće", "emoji_button.food": "Hrana & Piće",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "Ovaj post ne može biti podignut", "status.cannot_reblog": "Ovaj post ne može biti podignut",
"status.delete": "Obriši", "status.delete": "Obriši",
"status.embed": "Embed",
"status.favourite": "Označi omiljenim", "status.favourite": "Označi omiljenim",
"status.load_more": "Učitaj više", "status.load_more": "Učitaj više",
"status.media_hidden": "Sakriven media sadržaj", "status.media_hidden": "Sakriven media sadržaj",
"status.mention": "Spomeni @{name}", "status.mention": "Spomeni @{name}",
"status.mute_conversation": "Utišaj razgovor", "status.mute_conversation": "Utišaj razgovor",
"status.open": "Proširi ovaj status", "status.open": "Proširi ovaj status",
"status.pin": "Pin on profile",
"status.reblog": "Podigni", "status.reblog": "Podigni",
"status.reblogged_by": "{name} je podigao", "status.reblogged_by": "{name} je podigao",
"status.reply": "Odgovori", "status.reply": "Odgovori",
@ -179,6 +183,7 @@
"status.show_less": "Pokaži manje", "status.show_less": "Pokaži manje",
"status.show_more": "Pokaži više", "status.show_more": "Pokaži više",
"status.unmute_conversation": "Poništi utišavanje razgovora", "status.unmute_conversation": "Poništi utišavanje razgovora",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Sastavi", "tabs_bar.compose": "Sastavi",
"tabs_bar.federated_timeline": "Federalni", "tabs_bar.federated_timeline": "Federalni",
"tabs_bar.home": "Dom", "tabs_bar.home": "Dom",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Törlés", "status.delete": "Törlés",
"status.embed": "Embed",
"status.favourite": "Kedvenc", "status.favourite": "Kedvenc",
"status.load_more": "Load more", "status.load_more": "Load more",
"status.media_hidden": "Media hidden", "status.media_hidden": "Media hidden",
"status.mention": "Említés", "status.mention": "Említés",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "Expand this status", "status.open": "Expand this status",
"status.pin": "Pin on profile",
"status.reblog": "Reblog", "status.reblog": "Reblog",
"status.reblogged_by": "{name} reblogolta", "status.reblogged_by": "{name} reblogolta",
"status.reply": "Válasz", "status.reply": "Válasz",
@ -179,6 +183,7 @@
"status.show_less": "Show less", "status.show_less": "Show less",
"status.show_more": "Show more", "status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Összeállítás", "tabs_bar.compose": "Összeállítás",
"tabs_bar.federated_timeline": "Federated", "tabs_bar.federated_timeline": "Federated",
"tabs_bar.home": "Kezdőlap", "tabs_bar.home": "Kezdőlap",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "Apa anda yakin ingin membisukan {name}?", "confirmations.mute.message": "Apa anda yakin ingin membisukan {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Aktivitas", "emoji_button.activity": "Aktivitas",
"emoji_button.flags": "Bendera", "emoji_button.flags": "Bendera",
"emoji_button.food": "Makanan & Minuman", "emoji_button.food": "Makanan & Minuman",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Hapus", "status.delete": "Hapus",
"status.embed": "Embed",
"status.favourite": "Difavoritkan", "status.favourite": "Difavoritkan",
"status.load_more": "Tampilkan semua", "status.load_more": "Tampilkan semua",
"status.media_hidden": "Media disembunyikan", "status.media_hidden": "Media disembunyikan",
"status.mention": "Balasan @{name}", "status.mention": "Balasan @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "Tampilkan status ini", "status.open": "Tampilkan status ini",
"status.pin": "Pin on profile",
"status.reblog": "Boost", "status.reblog": "Boost",
"status.reblogged_by": "di-boost {name}", "status.reblogged_by": "di-boost {name}",
"status.reply": "Balas", "status.reply": "Balas",
@ -179,6 +183,7 @@
"status.show_less": "Tampilkan lebih sedikit", "status.show_less": "Tampilkan lebih sedikit",
"status.show_more": "Tampilkan semua", "status.show_more": "Tampilkan semua",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Tulis", "tabs_bar.compose": "Tulis",
"tabs_bar.federated_timeline": "Gabungan", "tabs_bar.federated_timeline": "Gabungan",
"tabs_bar.home": "Beranda", "tabs_bar.home": "Beranda",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Efacar", "status.delete": "Efacar",
"status.embed": "Embed",
"status.favourite": "Favorizar", "status.favourite": "Favorizar",
"status.load_more": "Kargar pluse", "status.load_more": "Kargar pluse",
"status.media_hidden": "Kontenajo celita", "status.media_hidden": "Kontenajo celita",
"status.mention": "Mencionar @{name}", "status.mention": "Mencionar @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "Detaligar ca mesajo", "status.open": "Detaligar ca mesajo",
"status.pin": "Pin on profile",
"status.reblog": "Repetar", "status.reblog": "Repetar",
"status.reblogged_by": "{name} repetita", "status.reblogged_by": "{name} repetita",
"status.reply": "Respondar", "status.reply": "Respondar",
@ -179,6 +183,7 @@
"status.show_less": "Montrar mine", "status.show_less": "Montrar mine",
"status.show_more": "Montrar plue", "status.show_more": "Montrar plue",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Kompozar", "tabs_bar.compose": "Kompozar",
"tabs_bar.federated_timeline": "Federata", "tabs_bar.federated_timeline": "Federata",
"tabs_bar.home": "Hemo", "tabs_bar.home": "Hemo",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Elimina", "status.delete": "Elimina",
"status.embed": "Embed",
"status.favourite": "Apprezzato", "status.favourite": "Apprezzato",
"status.load_more": "Mostra di più", "status.load_more": "Mostra di più",
"status.media_hidden": "Allegato nascosto", "status.media_hidden": "Allegato nascosto",
"status.mention": "Nomina @{name}", "status.mention": "Nomina @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "Espandi questo post", "status.open": "Espandi questo post",
"status.pin": "Pin on profile",
"status.reblog": "Condividi", "status.reblog": "Condividi",
"status.reblogged_by": "{name} ha condiviso", "status.reblogged_by": "{name} ha condiviso",
"status.reply": "Rispondi", "status.reply": "Rispondi",
@ -179,6 +183,7 @@
"status.show_less": "Mostra meno", "status.show_less": "Mostra meno",
"status.show_more": "Mostra di più", "status.show_more": "Mostra di più",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Scrivi", "tabs_bar.compose": "Scrivi",
"tabs_bar.federated_timeline": "Federazione", "tabs_bar.federated_timeline": "Federazione",
"tabs_bar.home": "Home", "tabs_bar.home": "Home",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "本当に{name}をミュートしますか?", "confirmations.mute.message": "本当に{name}をミュートしますか?",
"confirmations.unfollow.confirm": "フォロー解除", "confirmations.unfollow.confirm": "フォロー解除",
"confirmations.unfollow.message": "本当に{name}をフォロー解除しますか?", "confirmations.unfollow.message": "本当に{name}をフォロー解除しますか?",
"embed.instructions": "下記のコードをコピーしてウェブサイトに埋め込みます。",
"embed.preview": "表示例:",
"emoji_button.activity": "活動", "emoji_button.activity": "活動",
"emoji_button.flags": "国旗", "emoji_button.flags": "国旗",
"emoji_button.food": "食べ物", "emoji_button.food": "食べ物",
@ -159,15 +161,17 @@
"report.target": "{target} を通報する", "report.target": "{target} を通報する",
"search.placeholder": "検索", "search.placeholder": "検索",
"search_results.total": "{count, number}件の結果", "search_results.total": "{count, number}件の結果",
"standalone.public_title": "連合タイムライン", "standalone.public_title": "今こんな話をしています",
"status.cannot_reblog": "この投稿はブーストできません", "status.cannot_reblog": "この投稿はブーストできません",
"status.delete": "削除", "status.delete": "削除",
"status.embed": "埋め込み",
"status.favourite": "お気に入り", "status.favourite": "お気に入り",
"status.load_more": "もっと見る", "status.load_more": "もっと見る",
"status.media_hidden": "非表示のメディア", "status.media_hidden": "非表示のメディア",
"status.mention": "返信", "status.mention": "返信",
"status.mute_conversation": "会話をミュート", "status.mute_conversation": "会話をミュート",
"status.open": "詳細を表示", "status.open": "詳細を表示",
"status.pin": "プロフィールに固定表示",
"status.reblog": "ブースト", "status.reblog": "ブースト",
"status.reblogged_by": "{name}さんにブーストされました", "status.reblogged_by": "{name}さんにブーストされました",
"status.reply": "返信", "status.reply": "返信",
@ -179,6 +183,7 @@
"status.show_less": "隠す", "status.show_less": "隠す",
"status.show_more": "もっと見る", "status.show_more": "もっと見る",
"status.unmute_conversation": "会話のミュートを解除", "status.unmute_conversation": "会話のミュートを解除",
"status.unpin": "プロフィールの固定表示を解除",
"tabs_bar.compose": "投稿", "tabs_bar.compose": "投稿",
"tabs_bar.federated_timeline": "連合", "tabs_bar.federated_timeline": "連合",
"tabs_bar.home": "ホーム", "tabs_bar.home": "ホーム",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?", "confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "활동", "emoji_button.activity": "활동",
"emoji_button.flags": "국기", "emoji_button.flags": "국기",
"emoji_button.food": "음식", "emoji_button.food": "음식",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다", "status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다",
"status.delete": "삭제", "status.delete": "삭제",
"status.embed": "Embed",
"status.favourite": "즐겨찾기", "status.favourite": "즐겨찾기",
"status.load_more": "더 보기", "status.load_more": "더 보기",
"status.media_hidden": "미디어 숨겨짐", "status.media_hidden": "미디어 숨겨짐",
"status.mention": "답장", "status.mention": "답장",
"status.mute_conversation": "이 대화를 뮤트", "status.mute_conversation": "이 대화를 뮤트",
"status.open": "상세 정보 표시", "status.open": "상세 정보 표시",
"status.pin": "Pin on profile",
"status.reblog": "부스트", "status.reblog": "부스트",
"status.reblogged_by": "{name}님이 부스트 했습니다", "status.reblogged_by": "{name}님이 부스트 했습니다",
"status.reply": "답장", "status.reply": "답장",
@ -179,6 +183,7 @@
"status.show_less": "숨기기", "status.show_less": "숨기기",
"status.show_more": "더 보기", "status.show_more": "더 보기",
"status.unmute_conversation": "이 대화의 뮤트 해제하기", "status.unmute_conversation": "이 대화의 뮤트 해제하기",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "포스트", "tabs_bar.compose": "포스트",
"tabs_bar.federated_timeline": "연합", "tabs_bar.federated_timeline": "연합",
"tabs_bar.home": "홈", "tabs_bar.home": "홈",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "Weet je het zeker dat je {name} wilt negeren?", "confirmations.mute.message": "Weet je het zeker dat je {name} wilt negeren?",
"confirmations.unfollow.confirm": "Ontvolgen", "confirmations.unfollow.confirm": "Ontvolgen",
"confirmations.unfollow.message": "Weet je het zeker dat je {name} wilt ontvolgen?", "confirmations.unfollow.message": "Weet je het zeker dat je {name} wilt ontvolgen?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activiteiten", "emoji_button.activity": "Activiteiten",
"emoji_button.flags": "Vlaggen", "emoji_button.flags": "Vlaggen",
"emoji_button.food": "Eten en drinken", "emoji_button.food": "Eten en drinken",
@ -162,12 +164,14 @@
"standalone.public_title": "Een kijkje binnenin...", "standalone.public_title": "Een kijkje binnenin...",
"status.cannot_reblog": "Deze toot kan niet geboost worden", "status.cannot_reblog": "Deze toot kan niet geboost worden",
"status.delete": "Verwijderen", "status.delete": "Verwijderen",
"status.embed": "Embed",
"status.favourite": "Favoriet", "status.favourite": "Favoriet",
"status.load_more": "Meer laden", "status.load_more": "Meer laden",
"status.media_hidden": "Media verborgen", "status.media_hidden": "Media verborgen",
"status.mention": "Vermeld @{name}", "status.mention": "Vermeld @{name}",
"status.mute_conversation": "Negeer conversatie", "status.mute_conversation": "Negeer conversatie",
"status.open": "Toot volledig tonen", "status.open": "Toot volledig tonen",
"status.pin": "Pin on profile",
"status.reblog": "Boost", "status.reblog": "Boost",
"status.reblogged_by": "{name} boostte", "status.reblogged_by": "{name} boostte",
"status.reply": "Reageren", "status.reply": "Reageren",
@ -179,6 +183,7 @@
"status.show_less": "Minder tonen", "status.show_less": "Minder tonen",
"status.show_more": "Meer tonen", "status.show_more": "Meer tonen",
"status.unmute_conversation": "Conversatie niet meer negeren", "status.unmute_conversation": "Conversatie niet meer negeren",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Schrijven", "tabs_bar.compose": "Schrijven",
"tabs_bar.federated_timeline": "Globaal", "tabs_bar.federated_timeline": "Globaal",
"tabs_bar.home": "Start", "tabs_bar.home": "Start",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "Er du sikker på at du vil dempe {name}?", "confirmations.mute.message": "Er du sikker på at du vil dempe {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Aktivitet", "emoji_button.activity": "Aktivitet",
"emoji_button.flags": "Flagg", "emoji_button.flags": "Flagg",
"emoji_button.food": "Mat og drikke", "emoji_button.food": "Mat og drikke",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "Denne posten kan ikke fremheves", "status.cannot_reblog": "Denne posten kan ikke fremheves",
"status.delete": "Slett", "status.delete": "Slett",
"status.embed": "Embed",
"status.favourite": "Lik", "status.favourite": "Lik",
"status.load_more": "Last mer", "status.load_more": "Last mer",
"status.media_hidden": "Media skjult", "status.media_hidden": "Media skjult",
"status.mention": "Nevn @{name}", "status.mention": "Nevn @{name}",
"status.mute_conversation": "Demp samtale", "status.mute_conversation": "Demp samtale",
"status.open": "Utvid denne statusen", "status.open": "Utvid denne statusen",
"status.pin": "Pin on profile",
"status.reblog": "Fremhev", "status.reblog": "Fremhev",
"status.reblogged_by": "Fremhevd av {name}", "status.reblogged_by": "Fremhevd av {name}",
"status.reply": "Svar", "status.reply": "Svar",
@ -179,6 +183,7 @@
"status.show_less": "Vis mindre", "status.show_less": "Vis mindre",
"status.show_more": "Vis mer", "status.show_more": "Vis mer",
"status.unmute_conversation": "Ikke demp samtale", "status.unmute_conversation": "Ikke demp samtale",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Komponer", "tabs_bar.compose": "Komponer",
"tabs_bar.federated_timeline": "Felles", "tabs_bar.federated_timeline": "Felles",
"tabs_bar.home": "Hjem", "tabs_bar.home": "Hjem",

View File

@ -45,24 +45,26 @@
"column_subheading.settings": "Paramètres", "column_subheading.settings": "Paramètres",
"compose_form.lock_disclaimer": "Vòstre compte es pas {locked}. Tot lo mond pòt vos sègre e veire los estatuts reservats als seguidors.", "compose_form.lock_disclaimer": "Vòstre compte es pas {locked}. Tot lo mond pòt vos sègre e veire los estatuts reservats als seguidors.",
"compose_form.lock_disclaimer.lock": "clavat", "compose_form.lock_disclaimer.lock": "clavat",
"compose_form.placeholder": "A de qué pensatz ?", "compose_form.placeholder": "A de qué pensatz?",
"compose_form.privacy_disclaimer": "Vòstre estatut privat serà enviat a las personas mencionadas sus {domains}. Vos fisatz daqueste {domainsCount, plural, one { servidor} other {s servidors}} per divulgar pas vòstre estatut ? Los estatuts privats foncionan pas que sus las instàncias de Mastodon. Se {domains} {domainsCount, plural, one {es pas una instància a Mastodon} other {son pas d'instàncias a Mastodon}}, i aurà pas dindicacion disent que vòstre estatut es privat e poirà èsser partejat o èsser visible a de mond pas prevists", "compose_form.privacy_disclaimer": "Vòstre estatut privat serà enviat a las personas mencionadas sus {domains}. Vos fisatz daqueste {domainsCount, plural, one { servidor} other {s servidors}} per divulgar pas vòstre estatut? Los estatuts privats foncionan pas que sus las instàncias de Mastodon. Se {domains} {domainsCount, plural, one {es pas una instància a Mastodon} other {son pas d'instàncias a Mastodon}}, i aurà pas dindicacion disent que vòstre estatut es privat e poirà èsser partejat o èsser visible a de mond pas prevists",
"compose_form.publish": "Tut", "compose_form.publish": "Tut",
"compose_form.publish_loud": "{publish} !", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marcar lo mèdia coma sensible", "compose_form.sensitive": "Marcar lo mèdia coma sensible",
"compose_form.spoiler": "Rescondre lo tèxte darrièr un avertiment", "compose_form.spoiler": "Rescondre lo tèxte darrièr un avertiment",
"compose_form.spoiler_placeholder": "Escrivètz lavertiment aquí", "compose_form.spoiler_placeholder": "Escrivètz lavertiment aquí",
"confirmation_modal.cancel": "Anullar", "confirmation_modal.cancel": "Anullar",
"confirmations.block.confirm": "Blocar", "confirmations.block.confirm": "Blocar",
"confirmations.block.message": "Sètz segur de voler blocar {name} ?", "confirmations.block.message": "Sètz segur de voler blocar {name}?",
"confirmations.delete.confirm": "Suprimir", "confirmations.delete.confirm": "Suprimir",
"confirmations.delete.message": "Sètz segur de voler suprimir lestatut ?", "confirmations.delete.message": "Sètz segur de voler suprimir lestatut?",
"confirmations.domain_block.confirm": "Amagar tot lo domeni", "confirmations.domain_block.confirm": "Amagar tot lo domeni",
"confirmations.domain_block.message": "Sètz segur segur de voler blocar completament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.", "confirmations.domain_block.message": "Sètz segur segur de voler blocar completament {domain}? De còps cal pas que blocar o rescondre unas personas solament.",
"confirmations.mute.confirm": "Metre en silenci", "confirmations.mute.confirm": "Metre en silenci",
"confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?", "confirmations.mute.message": "Sètz segur de voler metre en silenci {name}?",
"confirmations.unfollow.confirm": "Quitar de sègre", "confirmations.unfollow.confirm": "Quitar de sègre",
"confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?", "confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activitats", "emoji_button.activity": "Activitats",
"emoji_button.flags": "Drapèus", "emoji_button.flags": "Drapèus",
"emoji_button.food": "Beure e manjar", "emoji_button.food": "Beure e manjar",
@ -73,13 +75,13 @@
"emoji_button.search": "Cercar…", "emoji_button.search": "Cercar…",
"emoji_button.symbols": "Simbòls", "emoji_button.symbols": "Simbòls",
"emoji_button.travel": "Viatges & lòcs", "emoji_button.travel": "Viatges & lòcs",
"empty_column.community": "Lo flux public local es void. Escrivètz quicòm per lo garnir !", "empty_column.community": "Lo flux public local es void. Escrivètz quicòm per lo garnir!",
"empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag", "empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag",
"empty_column.home": "Pel moment seguètz pas degun. Visitatz {public} o utilizatz la recèrca per vos connectar a dautras personas.", "empty_column.home": "Pel moment seguètz pas degun. Visitatz {public} o utilizatz la recèrca per vos connectar a dautras personas.",
"empty_column.home.inactivity": "Vòstra pagina dacuèlh es voida. Se sètz estat inactiu per un moment, serà tornada generar per vos dins una estona.", "empty_column.home.inactivity": "Vòstra pagina dacuèlh es voida. Se sètz estat inactiu per un moment, serà tornada generar per vos dins una estona.",
"empty_column.home.public_timeline": "lo flux public", "empty_column.home.public_timeline": "lo flux public",
"empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualquun per començar una conversacion.", "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualquun per començar una conversacion.",
"empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas dautras instàncias per garnir lo flux public.", "empty_column.public": "I a pas res aquí! Escrivètz quicòm de public, o seguètz de personas dautras instàncias per garnir lo flux public.",
"follow_request.authorize": "Autorizar", "follow_request.authorize": "Autorizar",
"follow_request.reject": "Regetar", "follow_request.reject": "Regetar",
"getting_started.appsshort": "Apps", "getting_started.appsshort": "Apps",
@ -109,19 +111,19 @@
"navigation_bar.mutes": "Personas rescondudas", "navigation_bar.mutes": "Personas rescondudas",
"navigation_bar.preferences": "Preferéncias", "navigation_bar.preferences": "Preferéncias",
"navigation_bar.public_timeline": "Flux public global", "navigation_bar.public_timeline": "Flux public global",
"notification.favourite": "{name} a ajustat a sos favorits :", "notification.favourite": "{name} a ajustat a sos favorits:",
"notification.follow": "{name} vos sèc", "notification.follow": "{name} vos sèc",
"notification.mention": "{name} vos a mencionat :", "notification.mention": "{name} vos a mencionat:",
"notification.reblog": "{name} a partejat vòstre estatut :", "notification.reblog": "{name} a partejat vòstre estatut:",
"notifications.clear": "Escafar", "notifications.clear": "Escafar",
"notifications.clear_confirmation": "Volètz vertadièrament escafar totas vòstras las notificacions ?", "notifications.clear_confirmation": "Volètz vertadièrament escafar totas vòstras las notificacions?",
"notifications.column_settings.alert": "Notificacions localas", "notifications.column_settings.alert": "Notificacions localas",
"notifications.column_settings.favourite": "Favorits :", "notifications.column_settings.favourite": "Favorits:",
"notifications.column_settings.follow": "Nòus seguidors :", "notifications.column_settings.follow": "Nòus seguidors:",
"notifications.column_settings.mention": "Mencions :", "notifications.column_settings.mention": "Mencions:",
"notifications.column_settings.push": "Notificacions", "notifications.column_settings.push": "Notificacions",
"notifications.column_settings.push_meta": "Aqueste periferic", "notifications.column_settings.push_meta": "Aqueste periferic",
"notifications.column_settings.reblog": "Partatges :", "notifications.column_settings.reblog": "Partatges:",
"notifications.column_settings.show": "Mostrar dins la colomna", "notifications.column_settings.show": "Mostrar dins la colomna",
"notifications.column_settings.sound": "Emetre un son", "notifications.column_settings.sound": "Emetre un son",
"onboarding.done": "Fach", "onboarding.done": "Fach",
@ -131,14 +133,14 @@
"onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualquun interagís amb vos", "onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualquun interagís amb vos",
"onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per bastir un malhum ma larg. Òm los apèla instàncias.", "onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per bastir un malhum ma larg. Òm los apèla instàncias.",
"onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}", "onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}",
"onboarding.page_one.welcome": "Benvengut a Mastodon !", "onboarding.page_one.welcome": "Benvengut a Mastodon!",
"onboarding.page_six.admin": "Vòstre administrator dinstància es {admin}.", "onboarding.page_six.admin": "Vòstre administrator dinstància es {admin}.",
"onboarding.page_six.almost_done": "Gaireben acabat…", "onboarding.page_six.almost_done": "Gaireben acabat…",
"onboarding.page_six.appetoot": "Bon Appetut!", "onboarding.page_six.appetoot": "Bon Appetut!",
"onboarding.page_six.apps_available": "I a daplicacions per mobil per iOS, Android e mai.", "onboarding.page_six.apps_available": "I a daplicacions per mobil per iOS, Android e mai.",
"onboarding.page_six.github": "Mastodon es un logicial liure e open-source. Podètz senhalar de bugs, demandar de foncionalitats e contribuir al còdi sus {github}.", "onboarding.page_six.github": "Mastodon es un logicial liure e open-source. Podètz senhalar de bugs, demandar de foncionalitats e contribuir al còdi sus {github}.",
"onboarding.page_six.guidelines": "guida de la comunitat", "onboarding.page_six.guidelines": "guida de la comunitat",
"onboarding.page_six.read_guidelines": "Mercés de legir la {guidelines} a {domain} !", "onboarding.page_six.read_guidelines": "Mercés de legir la {guidelines} a {domain}!",
"onboarding.page_six.various_app": "aplicacions per mobil", "onboarding.page_six.various_app": "aplicacions per mobil",
"onboarding.page_three.profile": "Modificatz vòstre perfil per cambiar vòstre avatar, bio e escais-nom. I a enlà totas las preferéncias.", "onboarding.page_three.profile": "Modificatz vòstre perfil per cambiar vòstre avatar, bio e escais-nom. I a enlà totas las preferéncias.",
"onboarding.page_three.search": "Emplegatz la barra de recèrca per trobar de mond e engachatz las etiquetas coma {illustration} e {introductions}. Per trobar una persona duna autra instància, picatz son identificant complet.", "onboarding.page_three.search": "Emplegatz la barra de recèrca per trobar de mond e engachatz las etiquetas coma {illustration} e {introductions}. Per trobar una persona duna autra instància, picatz son identificant complet.",
@ -162,14 +164,16 @@
"standalone.public_title": "Una ulhada dedins…", "standalone.public_title": "Una ulhada dedins…",
"status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat", "status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat",
"status.delete": "Escafar", "status.delete": "Escafar",
"status.embed": "Embed",
"status.favourite": "Apondre als favorits", "status.favourite": "Apondre als favorits",
"status.load_more": "Cargar mai", "status.load_more": "Cargar mai",
"status.media_hidden": "Mèdia rescondut", "status.media_hidden": "Mèdia rescondut",
"status.mention": "Mencionar", "status.mention": "Mencionar",
"status.mute_conversation": "Rescondre la conversacion", "status.mute_conversation": "Rescondre la conversacion",
"status.open": "Desplegar aqueste estatut", "status.open": "Desplegar aqueste estatut",
"status.pin": "Penjar al perfil",
"status.reblog": "Partejar", "status.reblog": "Partejar",
"status.reblogged_by": "{name} a partejat :", "status.reblogged_by": "{name} a partejat:",
"status.reply": "Respondre", "status.reply": "Respondre",
"status.replyAll": "Respondre a la conversacion", "status.replyAll": "Respondre a la conversacion",
"status.report": "Senhalar @{name}", "status.report": "Senhalar @{name}",
@ -179,6 +183,7 @@
"status.show_less": "Tornar plegar", "status.show_less": "Tornar plegar",
"status.show_more": "Desplegar", "status.show_more": "Desplegar",
"status.unmute_conversation": "Conversacions amb silenci levat", "status.unmute_conversation": "Conversacions amb silenci levat",
"status.unpin": "Despenjar del perfil",
"tabs_bar.compose": "Compausar", "tabs_bar.compose": "Compausar",
"tabs_bar.federated_timeline": "Flux public global", "tabs_bar.federated_timeline": "Flux public global",
"tabs_bar.home": "Acuèlh", "tabs_bar.home": "Acuèlh",

View File

@ -10,7 +10,7 @@
"account.media": "Media", "account.media": "Media",
"account.mention": "Wspomnij o @{name}", "account.mention": "Wspomnij o @{name}",
"account.mute": "Wycisz @{name}", "account.mute": "Wycisz @{name}",
"account.posts": "Posty", "account.posts": "Wpisy",
"account.report": "Zgłoś @{name}", "account.report": "Zgłoś @{name}",
"account.requested": "Oczekująca prośba", "account.requested": "Oczekująca prośba",
"account.share": "Udostępnij profil @{name}", "account.share": "Udostępnij profil @{name}",
@ -43,10 +43,10 @@
"column_header.unpin": "Cofnij przypięcie", "column_header.unpin": "Cofnij przypięcie",
"column_subheading.navigation": "Nawigacja", "column_subheading.navigation": "Nawigacja",
"column_subheading.settings": "Ustawienia", "column_subheading.settings": "Ustawienia",
"compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy, kto Cię śledzi, może wyświetlać Twoje posty przeznaczone tylko dla śledzących.", "compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy, kto Cię śledzi, może wyświetlać Twoje wpisy przeznaczone tylko dla śledzących.",
"compose_form.lock_disclaimer.lock": "zablokowane", "compose_form.lock_disclaimer.lock": "zablokowane",
"compose_form.placeholder": "Co Ci chodzi po głowie?", "compose_form.placeholder": "Co Ci chodzi po głowie?",
"compose_form.privacy_disclaimer": "Twój post zostanie dostarczony do użytkowników z {domains}. Czy ufasz {domainsCount, plural, one {temu serwerowi} other {tym serwerom}}? Prywatność postów obowiązuje tylko na instancjach Mastodona. Jeżeli {domains} {domainsCount, plural, one {nie jest instancją Mastodona} other {nie są instancjami Mastodona}}, post może być widoczny dla niewłaściwych osób.", "compose_form.privacy_disclaimer": "Twój wpis zostanie dostarczony do użytkowników z {domains}. Czy ufasz {domainsCount, plural, one {temu serwerowi} other {tym serwerom}}? Prywatność wpisów obowiązuje tylko na instancjach Mastodona. Jeżeli {domains} {domainsCount, plural, one {nie jest instancją Mastodona} other {nie są instancjami Mastodona}}, wpis może być widoczny dla niewłaściwych osób.",
"compose_form.publish": "Wyślij", "compose_form.publish": "Wyślij",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Oznacz treści jako wrażliwe", "compose_form.sensitive": "Oznacz treści jako wrażliwe",
@ -63,6 +63,8 @@
"confirmations.mute.message": "Czy na pewno chcesz wyciszyć {name}?", "confirmations.mute.message": "Czy na pewno chcesz wyciszyć {name}?",
"confirmations.unfollow.confirm": "Przestań śledzić", "confirmations.unfollow.confirm": "Przestań śledzić",
"confirmations.unfollow.message": "Czy na pewno zamierzasz przestać śledzić {name}?", "confirmations.unfollow.message": "Czy na pewno zamierzasz przestać śledzić {name}?",
"embed.instructions": "Osadź ten status na swojej stronie wklejając poniższy kod.",
"embed.preview": "Tak będzie to wyglądać:",
"emoji_button.activity": "Aktywność", "emoji_button.activity": "Aktywność",
"emoji_button.flags": "Flagi", "emoji_button.flags": "Flagi",
"emoji_button.food": "Żywność i napoje", "emoji_button.food": "Żywność i napoje",
@ -70,11 +72,11 @@
"emoji_button.nature": "Natura", "emoji_button.nature": "Natura",
"emoji_button.objects": "Objekty", "emoji_button.objects": "Objekty",
"emoji_button.people": "Ludzie", "emoji_button.people": "Ludzie",
"emoji_button.search": "Szukaj...", "emoji_button.search": "Szukaj",
"emoji_button.symbols": "Symbole", "emoji_button.symbols": "Symbole",
"emoji_button.travel": "Podróże i miejsca", "emoji_button.travel": "Podróże i miejsca",
"empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!", "empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!",
"empty_column.hashtag": "Nie ma postów oznaczonych tym hashtagiem. Możesz napisać pierwszy!", "empty_column.hashtag": "Nie ma wpisów oznaczonych tym hashtagiem. Możesz napisać pierwszy!",
"empty_column.home": "Nie śledzisz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.", "empty_column.home": "Nie śledzisz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.",
"empty_column.home.inactivity": "Strumień jest pusty. Jeżeli nie było Cię tu ostatnio, zostanie on wypełniony wkrótce.", "empty_column.home.inactivity": "Strumień jest pusty. Jeżeli nie było Cię tu ostatnio, zostanie on wypełniony wkrótce.",
"empty_column.home.public_timeline": "publiczna oś czasu", "empty_column.home.public_timeline": "publiczna oś czasu",
@ -85,7 +87,7 @@
"getting_started.appsshort": "Aplikacje", "getting_started.appsshort": "Aplikacje",
"getting_started.faq": "FAQ", "getting_started.faq": "FAQ",
"getting_started.heading": "Naucz się korzystać", "getting_started.heading": "Naucz się korzystać",
"getting_started.open_source_notice": "Mastodon jest oprogramowaniem o otwartym źródle. Możesz pomóc w rozwoju lub zgłaszać błędy na GitHubie tutaj {github}.", "getting_started.open_source_notice": "Mastodon jest oprogramowaniem o otwartym źródle. Możesz pomóc w rozwoju lub zgłaszać błędy na GitHubie tutaj: {github}.",
"getting_started.userguide": "Podręcznik użytkownika", "getting_started.userguide": "Podręcznik użytkownika",
"home.column_settings.advanced": "Zaawansowane", "home.column_settings.advanced": "Zaawansowane",
"home.column_settings.basic": "Podstawowe", "home.column_settings.basic": "Podstawowe",
@ -96,7 +98,7 @@
"lightbox.close": "Zamknij", "lightbox.close": "Zamknij",
"lightbox.next": "Następne", "lightbox.next": "Następne",
"lightbox.previous": "Poprzednie", "lightbox.previous": "Poprzednie",
"loading_indicator.label": "Ładowanie...", "loading_indicator.label": "Ładowanie",
"media_gallery.toggle_visible": "Przełącz widoczność", "media_gallery.toggle_visible": "Przełącz widoczność",
"missing_indicator.label": "Nie znaleziono", "missing_indicator.label": "Nie znaleziono",
"navigation_bar.blocks": "Zablokowani użytkownicy", "navigation_bar.blocks": "Zablokowani użytkownicy",
@ -116,12 +118,12 @@
"notifications.clear": "Wyczyść powiadomienia", "notifications.clear": "Wyczyść powiadomienia",
"notifications.clear_confirmation": "Czy na pewno chcesz bezpowrotnie usunąć wszystkie powiadomienia?", "notifications.clear_confirmation": "Czy na pewno chcesz bezpowrotnie usunąć wszystkie powiadomienia?",
"notifications.column_settings.alert": "Powiadomienia na pulpicie", "notifications.column_settings.alert": "Powiadomienia na pulpicie",
"notifications.column_settings.favourite": "Ulubione:", "notifications.column_settings.favourite": "Dodanie do ulubionych:",
"notifications.column_settings.follow": "Nowi śledzący:", "notifications.column_settings.follow": "Nowi śledzący:",
"notifications.column_settings.mention": "Wspomniali:", "notifications.column_settings.mention": "Wspomnienia:",
"notifications.column_settings.push": "Powiadomienia push", "notifications.column_settings.push": "Powiadomienia push",
"notifications.column_settings.push_meta": "To urządzenie", "notifications.column_settings.push_meta": "To urządzenie",
"notifications.column_settings.reblog": "Podbili:", "notifications.column_settings.reblog": "Podbicia:",
"notifications.column_settings.show": "Pokaż w kolumnie", "notifications.column_settings.show": "Pokaż w kolumnie",
"notifications.column_settings.sound": "Odtwarzaj dźwięk", "notifications.column_settings.sound": "Odtwarzaj dźwięk",
"onboarding.done": "Gotowe", "onboarding.done": "Gotowe",
@ -142,15 +144,15 @@
"onboarding.page_six.various_app": "aplikacje mobilne", "onboarding.page_six.various_app": "aplikacje mobilne",
"onboarding.page_three.profile": "Edytuj profil, aby zmienić obraz profilowy, biografię, wyświetlaną nazwę i inne ustawienia.", "onboarding.page_three.profile": "Edytuj profil, aby zmienić obraz profilowy, biografię, wyświetlaną nazwę i inne ustawienia.",
"onboarding.page_three.search": "Użyj paska wyszukiwania aby znaleźć ludzi i hashtagi, takie jak {illustration} i {introductions}. Aby znaleźć osobę spoza tej instancji, musisz użyć pełnego adresu.", "onboarding.page_three.search": "Użyj paska wyszukiwania aby znaleźć ludzi i hashtagi, takie jak {illustration} i {introductions}. Aby znaleźć osobę spoza tej instancji, musisz użyć pełnego adresu.",
"onboarding.page_two.compose": "Napisz posty, aby wypełnić kolumnę. Możesz wysłać zdjęcia, zmienić ustawienia prywatności lub dodać ostrzeżenie o zawartości.", "onboarding.page_two.compose": "Utwórz wpisy, aby wypełnić kolumnę. Możesz wysłać zdjęcia, zmienić ustawienia prywatności lub dodać ostrzeżenie o zawartości.",
"onboarding.skip": "Pomiń", "onboarding.skip": "Pomiń",
"privacy.change": "Dostosuj widoczność postów", "privacy.change": "Dostosuj widoczność wpisów",
"privacy.direct.long": "Widoczne tylko dla oznaczonych", "privacy.direct.long": "Widoczny tylko dla wspomnianych",
"privacy.direct.short": "Bezpośrednio", "privacy.direct.short": "Bezpośrednio",
"privacy.private.long": "Widoczne tylko dla śledzących", "privacy.private.long": "Widoczny tylko dla osób, które Cię śledzą",
"privacy.private.short": "Tylko śledzący", "privacy.private.short": "Tylko dla śledzących",
"privacy.public.long": "Widoczne na publicznych osiach czasu", "privacy.public.long": "Widoczny na publicznych osiach czasu",
"privacy.public.short": "Publiczne", "privacy.public.short": "Publiczny",
"privacy.unlisted.long": "Niewidoczne na publicznych osiach czasu", "privacy.unlisted.long": "Niewidoczne na publicznych osiach czasu",
"privacy.unlisted.short": "Niewidoczne", "privacy.unlisted.short": "Niewidoczne",
"reply_indicator.cancel": "Anuluj", "reply_indicator.cancel": "Anuluj",
@ -160,14 +162,16 @@
"search.placeholder": "Szukaj", "search.placeholder": "Szukaj",
"search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}", "search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}",
"standalone.public_title": "Spojrzenie w głąb…", "standalone.public_title": "Spojrzenie w głąb…",
"status.cannot_reblog": "Ten post nie może zostać podbity", "status.cannot_reblog": "Ten wpis nie może zostać podbity",
"status.delete": "Usuń", "status.delete": "Usuń",
"status.embed": "Osadź",
"status.favourite": "Ulubione", "status.favourite": "Ulubione",
"status.load_more": "Załaduj więcej", "status.load_more": "Załaduj więcej",
"status.media_hidden": "Zawartość multimedialna ukryta", "status.media_hidden": "Zawartość multimedialna ukryta",
"status.mention": "Wspomnij o @{name}", "status.mention": "Wspomnij o @{name}",
"status.mute_conversation": "Wycisz konwersację", "status.mute_conversation": "Wycisz konwersację",
"status.open": "Rozszerz ten status", "status.open": "Rozszerz ten status",
"status.pin": "Przypnij do profilu",
"status.reblog": "Podbij", "status.reblog": "Podbij",
"status.reblogged_by": "{name} podbił", "status.reblogged_by": "{name} podbił",
"status.reply": "Odpowiedz", "status.reply": "Odpowiedz",
@ -179,6 +183,7 @@
"status.show_less": "Pokaż mniej", "status.show_less": "Pokaż mniej",
"status.show_more": "Pokaż więcej", "status.show_more": "Pokaż więcej",
"status.unmute_conversation": "Cofnij wyciszenie konwersacji", "status.unmute_conversation": "Cofnij wyciszenie konwersacji",
"status.unpin": "Odepnij z profilu",
"tabs_bar.compose": "Napisz", "tabs_bar.compose": "Napisz",
"tabs_bar.federated_timeline": "Globalne", "tabs_bar.federated_timeline": "Globalne",
"tabs_bar.home": "Strona główna", "tabs_bar.home": "Strona główna",

View File

@ -1,68 +1,70 @@
{ {
"account.block": "Bloquear @{name}", "account.block": "Bloquear @{name}",
"account.block_domain": "Hide everything from {domain}", "account.block_domain": "Esconder tudo de {domain}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.", "account.disclaimer_full": "As informações abaixo podem refletir o perfil do usuário de maneira incompleta.",
"account.edit_profile": "Editar perfil", "account.edit_profile": "Editar perfil",
"account.follow": "Seguir", "account.follow": "Seguir",
"account.followers": "Seguidores", "account.followers": "Seguidores",
"account.follows": "Segue", "account.follows": "Segue",
"account.follows_you": teu seguidor", "account.follows_you": seu seguidor",
"account.media": "Media", "account.media": "Mídia",
"account.mention": "Mencionar @{name}", "account.mention": "Mencionar @{name}",
"account.mute": "Silenciar @{name}", "account.mute": "Silenciar @{name}",
"account.posts": "Posts", "account.posts": "Posts",
"account.report": "Denunciar @{name}", "account.report": "Denunciar @{name}",
"account.requested": "A aguardar aprovação", "account.requested": "Aguardando aprovação",
"account.share": "Share @{name}'s profile", "account.share": "Compartilhar perfil de @{name}",
"account.unblock": "Não bloquear @{name}", "account.unblock": "Não bloquear @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Desbloquear {domain}",
"account.unfollow": "Deixar de seguir", "account.unfollow": "Deixar de seguir",
"account.unmute": "Não silenciar @{name}", "account.unmute": "Não silenciar @{name}",
"account.view_full_profile": "View full profile", "account.view_full_profile": "Ver perfil completo",
"boost_modal.combo": "Pode clicar {combo} para não voltar a ver", "boost_modal.combo": "Pode clicar {combo} para não voltar a ver",
"bundle_column_error.body": "Something went wrong while loading this component.", "bundle_column_error.body": "Something went wrong while loading this component.",
"bundle_column_error.retry": "Try again", "bundle_column_error.retry": "Tente novamente",
"bundle_column_error.title": "Network error", "bundle_column_error.title": "Network error",
"bundle_modal_error.close": "Close", "bundle_modal_error.close": "Fechar",
"bundle_modal_error.message": "Something went wrong while loading this component.", "bundle_modal_error.message": "Something went wrong while loading this component.",
"bundle_modal_error.retry": "Try again", "bundle_modal_error.retry": "Tente novamente",
"column.blocks": "Utilizadores Bloqueados", "column.blocks": "Usuários bloqueados",
"column.community": "Local", "column.community": "Local",
"column.favourites": "Favoritos", "column.favourites": "Favoritos",
"column.follow_requests": "Seguidores Pendentes", "column.follow_requests": "Seguidores pendentes",
"column.home": "Home", "column.home": "Página inicial",
"column.mutes": "Utilizadores silenciados", "column.mutes": "Usuários silenciados",
"column.notifications": "Notificações", "column.notifications": "Notificações",
"column.public": "Global", "column.public": "Global",
"column_back_button.label": "Voltar", "column_back_button.label": "Voltar",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Esconder configurações",
"column_header.moveLeft_settings": "Move column to the left", "column_header.moveLeft_settings": "Mover coluna para a esquerda",
"column_header.moveRight_settings": "Move column to the right", "column_header.moveRight_settings": "Mover coluna para a direita",
"column_header.pin": "Pin", "column_header.pin": "Fixar",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Mostrar configurações",
"column_header.unpin": "Unpin", "column_header.unpin": "Desafixar",
"column_subheading.navigation": "Navigation", "column_subheading.navigation": "Navegação",
"column_subheading.settings": "Settings", "column_subheading.settings": "Configurações",
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", "compose_form.lock_disclaimer": "A sua conta não está {locked}. Qualquer pessoa pode te seguir e visualizar as suas postagens só para seguidores.",
"compose_form.lock_disclaimer.lock": "locked", "compose_form.lock_disclaimer.lock": "locked",
"compose_form.placeholder": "Em que estás a pensar?", "compose_form.placeholder": "No que você está pensando?",
"compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.", "compose_form.privacy_disclaimer": "O seu conteúdo privado será compartilhado com os usuários do {domains}. Você confia {domainsCount, plural, one {neste servidor} other {nestes servidores}}? As configurações de privacidade só funcionam em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não há como garantir a privacidade de suas postagens, e elas podem ser compartilhadas com outros.",
"compose_form.publish": "Publicar", "compose_form.publish": "Publicar",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marcar media como conteúdo sensível", "compose_form.sensitive": "Marcar mídia como conteúdo sensível",
"compose_form.spoiler": "Esconder texto com aviso", "compose_form.spoiler": "Esconder texto com aviso",
"compose_form.spoiler_placeholder": "Aviso de conteúdo", "compose_form.spoiler_placeholder": "Aviso de conteúdo",
"confirmation_modal.cancel": "Cancel", "confirmation_modal.cancel": "Cancelar",
"confirmations.block.confirm": "Block", "confirmations.block.confirm": "Bloquear",
"confirmations.block.message": "Are you sure you want to block {name}?", "confirmations.block.message": "Você tem certeza de que quer bloquear {name}?",
"confirmations.delete.confirm": "Delete", "confirmations.delete.confirm": "Excluir",
"confirmations.delete.message": "Are you sure you want to delete this status?", "confirmations.delete.message": "Você tem certeza de que quer excluir este status?",
"confirmations.domain_block.confirm": "Hide entire domain", "confirmations.domain_block.confirm": "Esconder o domínio inteiro",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", "confirmations.domain_block.message": "Você quer mesmo bloquear {domain} inteiro? Na maioria dos casos, silenciar ou bloquear alguns usuários é o suficiente e o recomendado.",
"confirmations.mute.confirm": "Mute", "confirmations.mute.confirm": "Silenciar",
"confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.mute.message": "Você tem certeza de que quer silenciar {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Deixar de seguir",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Você tem certeza de que quer deixar de seguir {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Eliminar", "status.delete": "Eliminar",
"status.embed": "Embed",
"status.favourite": "Adicionar aos favoritos", "status.favourite": "Adicionar aos favoritos",
"status.load_more": "Carregar mais", "status.load_more": "Carregar mais",
"status.media_hidden": "Media escondida", "status.media_hidden": "Media escondida",
"status.mention": "Mencionar @{name}", "status.mention": "Mencionar @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "Expandir", "status.open": "Expandir",
"status.pin": "Pin on profile",
"status.reblog": "Partilhar", "status.reblog": "Partilhar",
"status.reblogged_by": "{name} partilhou", "status.reblogged_by": "{name} partilhou",
"status.reply": "Responder", "status.reply": "Responder",
@ -179,6 +183,7 @@
"status.show_less": "Mostrar menos", "status.show_less": "Mostrar menos",
"status.show_more": "Mostrar mais", "status.show_more": "Mostrar mais",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Criar", "tabs_bar.compose": "Criar",
"tabs_bar.federated_timeline": "Global", "tabs_bar.federated_timeline": "Global",
"tabs_bar.home": "Home", "tabs_bar.home": "Home",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Eliminar", "status.delete": "Eliminar",
"status.embed": "Embed",
"status.favourite": "Adicionar aos favoritos", "status.favourite": "Adicionar aos favoritos",
"status.load_more": "Carregar mais", "status.load_more": "Carregar mais",
"status.media_hidden": "Media escondida", "status.media_hidden": "Media escondida",
"status.mention": "Mencionar @{name}", "status.mention": "Mencionar @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "Expandir", "status.open": "Expandir",
"status.pin": "Pin on profile",
"status.reblog": "Partilhar", "status.reblog": "Partilhar",
"status.reblogged_by": "{name} partilhou", "status.reblogged_by": "{name} partilhou",
"status.reply": "Responder", "status.reply": "Responder",
@ -179,6 +183,7 @@
"status.show_less": "Mostrar menos", "status.show_less": "Mostrar menos",
"status.show_more": "Mostrar mais", "status.show_more": "Mostrar mais",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Criar", "tabs_bar.compose": "Criar",
"tabs_bar.federated_timeline": "Global", "tabs_bar.federated_timeline": "Global",
"tabs_bar.home": "Home", "tabs_bar.home": "Home",

View File

@ -1,7 +1,7 @@
{ {
"account.block": "Блокировать", "account.block": "Блокировать",
"account.block_domain": "Блокировать все с {domain}", "account.block_domain": "Блокировать все с {domain}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.", "account.disclaimer_full": "Нижеуказанная информация может не полностью отражать профиль пользователя.",
"account.edit_profile": "Изменить профиль", "account.edit_profile": "Изменить профиль",
"account.follow": "Подписаться", "account.follow": "Подписаться",
"account.followers": "Подписаны", "account.followers": "Подписаны",
@ -13,19 +13,19 @@
"account.posts": "Посты", "account.posts": "Посты",
"account.report": "Пожаловаться", "account.report": "Пожаловаться",
"account.requested": "Ожидает подтверждения", "account.requested": "Ожидает подтверждения",
"account.share": "Share @{name}'s profile", "account.share": "Поделиться профилем @{name}",
"account.unblock": "Разблокировать", "account.unblock": "Разблокировать",
"account.unblock_domain": "Разблокировать {domain}", "account.unblock_domain": "Разблокировать {domain}",
"account.unfollow": "Отписаться", "account.unfollow": "Отписаться",
"account.unmute": "Снять глушение", "account.unmute": "Снять глушение",
"account.view_full_profile": "View full profile", "account.view_full_profile": "Показать полный профиль",
"boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз", "boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз",
"bundle_column_error.body": "Something went wrong while loading this component.", "bundle_column_error.body": "Что-то пошло не так при загрузке этого компонента.",
"bundle_column_error.retry": "Try again", "bundle_column_error.retry": "Попробовать снова",
"bundle_column_error.title": "Network error", "bundle_column_error.title": "Ошибка сети",
"bundle_modal_error.close": "Close", "bundle_modal_error.close": "Закрыть",
"bundle_modal_error.message": "Something went wrong while loading this component.", "bundle_modal_error.message": "Что-то пошло не так при загрузке этого компонента.",
"bundle_modal_error.retry": "Try again", "bundle_modal_error.retry": "Попробовать снова",
"column.blocks": "Список блокировки", "column.blocks": "Список блокировки",
"column.community": "Локальная лента", "column.community": "Локальная лента",
"column.favourites": "Понравившееся", "column.favourites": "Понравившееся",
@ -35,11 +35,11 @@
"column.notifications": "Уведомления", "column.notifications": "Уведомления",
"column.public": "Глобальная лента", "column.public": "Глобальная лента",
"column_back_button.label": "Назад", "column_back_button.label": "Назад",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Скрыть настройки",
"column_header.moveLeft_settings": "Move column to the left", "column_header.moveLeft_settings": "Передвинуть колонку влево",
"column_header.moveRight_settings": "Move column to the right", "column_header.moveRight_settings": "Передвинуть колонку вправо",
"column_header.pin": "Закрепить", "column_header.pin": "Закрепить",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Показать настройки",
"column_header.unpin": "Открепить", "column_header.unpin": "Открепить",
"column_subheading.navigation": "Навигация", "column_subheading.navigation": "Навигация",
"column_subheading.settings": "Настройки", "column_subheading.settings": "Настройки",
@ -61,8 +61,10 @@
"confirmations.domain_block.message": "Вы на самом деле уверены, что хотите блокировать весь {domain}? В большинстве случаев нескольких отдельных блокировок или глушений достаточно.", "confirmations.domain_block.message": "Вы на самом деле уверены, что хотите блокировать весь {domain}? В большинстве случаев нескольких отдельных блокировок или глушений достаточно.",
"confirmations.mute.confirm": "Заглушить", "confirmations.mute.confirm": "Заглушить",
"confirmations.mute.message": "Вы уверены, что хотите заглушить {name}?", "confirmations.mute.message": "Вы уверены, что хотите заглушить {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Отписаться",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Вы уверены, что хотите отписаться от {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Занятия", "emoji_button.activity": "Занятия",
"emoji_button.flags": "Флаги", "emoji_button.flags": "Флаги",
"emoji_button.food": "Еда и напитки", "emoji_button.food": "Еда и напитки",
@ -94,8 +96,8 @@
"home.column_settings.show_replies": "Показывать ответы", "home.column_settings.show_replies": "Показывать ответы",
"home.settings": "Настройки колонки", "home.settings": "Настройки колонки",
"lightbox.close": "Закрыть", "lightbox.close": "Закрыть",
"lightbox.next": "Next", "lightbox.next": "Далее",
"lightbox.previous": "Previous", "lightbox.previous": "Назад",
"loading_indicator.label": "Загрузка...", "loading_indicator.label": "Загрузка...",
"media_gallery.toggle_visible": "Показать/скрыть", "media_gallery.toggle_visible": "Показать/скрыть",
"missing_indicator.label": "Не найдено", "missing_indicator.label": "Не найдено",
@ -119,8 +121,8 @@
"notifications.column_settings.favourite": "Нравится:", "notifications.column_settings.favourite": "Нравится:",
"notifications.column_settings.follow": "Новые подписчики:", "notifications.column_settings.follow": "Новые подписчики:",
"notifications.column_settings.mention": "Упоминания:", "notifications.column_settings.mention": "Упоминания:",
"notifications.column_settings.push": "Push notifications", "notifications.column_settings.push": "Push-уведомления",
"notifications.column_settings.push_meta": "This device", "notifications.column_settings.push_meta": "Это устройство",
"notifications.column_settings.reblog": "Продвижения:", "notifications.column_settings.reblog": "Продвижения:",
"notifications.column_settings.show": "Показывать в колонке", "notifications.column_settings.show": "Показывать в колонке",
"notifications.column_settings.sound": "Проигрывать звук", "notifications.column_settings.sound": "Проигрывать звук",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "Этот статус не может быть продвинут", "status.cannot_reblog": "Этот статус не может быть продвинут",
"status.delete": "Удалить", "status.delete": "Удалить",
"status.embed": "Embed",
"status.favourite": "Нравится", "status.favourite": "Нравится",
"status.load_more": "Показать еще", "status.load_more": "Показать еще",
"status.media_hidden": "Медиаконтент скрыт", "status.media_hidden": "Медиаконтент скрыт",
"status.mention": "Упомянуть @{name}", "status.mention": "Упомянуть @{name}",
"status.mute_conversation": "Заглушить тред", "status.mute_conversation": "Заглушить тред",
"status.open": "Развернуть статус", "status.open": "Развернуть статус",
"status.pin": "Pin on profile",
"status.reblog": "Продвинуть", "status.reblog": "Продвинуть",
"status.reblogged_by": "{name} продвинул(а)", "status.reblogged_by": "{name} продвинул(а)",
"status.reply": "Ответить", "status.reply": "Ответить",
@ -179,6 +183,7 @@
"status.show_less": "Свернуть", "status.show_less": "Свернуть",
"status.show_more": "Развернуть", "status.show_more": "Развернуть",
"status.unmute_conversation": "Снять глушение с треда", "status.unmute_conversation": "Снять глушение с треда",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Написать", "tabs_bar.compose": "Написать",
"tabs_bar.federated_timeline": "Глобальная", "tabs_bar.federated_timeline": "Глобальная",
"tabs_bar.home": "Главная", "tabs_bar.home": "Главная",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.flags": "Flags", "emoji_button.flags": "Flags",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Food & Drink",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "This post cannot be boosted",
"status.delete": "Delete", "status.delete": "Delete",
"status.embed": "Embed",
"status.favourite": "Favourite", "status.favourite": "Favourite",
"status.load_more": "Load more", "status.load_more": "Load more",
"status.media_hidden": "Media hidden", "status.media_hidden": "Media hidden",
"status.mention": "Mention @{name}", "status.mention": "Mention @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "Expand this status", "status.open": "Expand this status",
"status.pin": "Pin on profile",
"status.reblog": "Boost", "status.reblog": "Boost",
"status.reblogged_by": "{name} boosted", "status.reblogged_by": "{name} boosted",
"status.reply": "Reply", "status.reply": "Reply",
@ -179,6 +183,7 @@
"status.show_less": "Show less", "status.show_less": "Show less",
"status.show_more": "Show more", "status.show_more": "Show more",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Compose", "tabs_bar.compose": "Compose",
"tabs_bar.federated_timeline": "Federated", "tabs_bar.federated_timeline": "Federated",
"tabs_bar.home": "Home", "tabs_bar.home": "Home",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "{name} kullanıcısını sessize almak istiyor musunuz?", "confirmations.mute.message": "{name} kullanıcısını sessize almak istiyor musunuz?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Aktivite", "emoji_button.activity": "Aktivite",
"emoji_button.flags": "Bayraklar", "emoji_button.flags": "Bayraklar",
"emoji_button.food": "Yiyecek ve İçecek", "emoji_button.food": "Yiyecek ve İçecek",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "Bu gönderi boost edilemez", "status.cannot_reblog": "Bu gönderi boost edilemez",
"status.delete": "Sil", "status.delete": "Sil",
"status.embed": "Embed",
"status.favourite": "Favorilere ekle", "status.favourite": "Favorilere ekle",
"status.load_more": "Daha fazla", "status.load_more": "Daha fazla",
"status.media_hidden": "Gizli görsel", "status.media_hidden": "Gizli görsel",
"status.mention": "Bahset @{name}", "status.mention": "Bahset @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "Bu gönderiyi genişlet", "status.open": "Bu gönderiyi genişlet",
"status.pin": "Pin on profile",
"status.reblog": "Boost'la", "status.reblog": "Boost'la",
"status.reblogged_by": "{name} boost etti", "status.reblogged_by": "{name} boost etti",
"status.reply": "Cevapla", "status.reply": "Cevapla",
@ -179,6 +183,7 @@
"status.show_less": "Daha azı", "status.show_less": "Daha azı",
"status.show_more": "Daha fazlası", "status.show_more": "Daha fazlası",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Oluştur", "tabs_bar.compose": "Oluştur",
"tabs_bar.federated_timeline": "Federe", "tabs_bar.federated_timeline": "Federe",
"tabs_bar.home": "Ana sayfa", "tabs_bar.home": "Ana sayfa",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "Ви впевнені, що хочете заглушити {name}?", "confirmations.mute.message": "Ви впевнені, що хочете заглушити {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Заняття", "emoji_button.activity": "Заняття",
"emoji_button.flags": "Прапори", "emoji_button.flags": "Прапори",
"emoji_button.food": "Їжа та напої", "emoji_button.food": "Їжа та напої",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "Цей допис не може бути передмухнутий", "status.cannot_reblog": "Цей допис не може бути передмухнутий",
"status.delete": "Видалити", "status.delete": "Видалити",
"status.embed": "Embed",
"status.favourite": "Подобається", "status.favourite": "Подобається",
"status.load_more": "Завантажити більше", "status.load_more": "Завантажити більше",
"status.media_hidden": "Медіаконтент приховано", "status.media_hidden": "Медіаконтент приховано",
"status.mention": "Згадати", "status.mention": "Згадати",
"status.mute_conversation": "Заглушити діалог", "status.mute_conversation": "Заглушити діалог",
"status.open": "Розгорнути допис", "status.open": "Розгорнути допис",
"status.pin": "Pin on profile",
"status.reblog": "Передмухнути", "status.reblog": "Передмухнути",
"status.reblogged_by": "{name} передмухнув(-ла)", "status.reblogged_by": "{name} передмухнув(-ла)",
"status.reply": "Відповісти", "status.reply": "Відповісти",
@ -179,6 +183,7 @@
"status.show_less": "Згорнути", "status.show_less": "Згорнути",
"status.show_more": "Розгорнути", "status.show_more": "Розгорнути",
"status.unmute_conversation": "Зняти глушення з діалогу", "status.unmute_conversation": "Зняти глушення з діалогу",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "Написати", "tabs_bar.compose": "Написати",
"tabs_bar.federated_timeline": "Глобальна", "tabs_bar.federated_timeline": "Глобальна",
"tabs_bar.home": "Головна", "tabs_bar.home": "Головна",

View File

@ -5,7 +5,7 @@
"account.edit_profile": "修改个人资料", "account.edit_profile": "修改个人资料",
"account.follow": "关注", "account.follow": "关注",
"account.followers": "关注者", "account.followers": "关注者",
"account.follows": "正关注", "account.follows": "正关注",
"account.follows_you": "关注你", "account.follows_you": "关注你",
"account.media": "Media", "account.media": "Media",
"account.mention": "提及 @{name}", "account.mention": "提及 @{name}",
@ -13,19 +13,19 @@
"account.posts": "嘟文", "account.posts": "嘟文",
"account.report": "举报 @{name}", "account.report": "举报 @{name}",
"account.requested": "等待审批", "account.requested": "等待审批",
"account.share": "Share @{name}'s profile", "account.share": "分享 @{name}的个人资料",
"account.unblock": "解除对 @{name} 的屏蔽", "account.unblock": "解除对 @{name} 的屏蔽",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "解除封锁 {domain}",
"account.unfollow": "取消关注", "account.unfollow": "取消关注",
"account.unmute": "取消 @{name} 的静音", "account.unmute": "取消 @{name} 的静音",
"account.view_full_profile": "View full profile", "account.view_full_profile": "查看完整资料",
"boost_modal.combo": "如你想在下次路过时显示,请按{combo}", "boost_modal.combo": "如你想在下次路过时显示,请按{combo}",
"bundle_column_error.body": "Something went wrong while loading this component.", "bundle_column_error.body": "载入组件出错。",
"bundle_column_error.retry": "Try again", "bundle_column_error.retry": "再次尝试",
"bundle_column_error.title": "Network error", "bundle_column_error.title": "网络错误",
"bundle_modal_error.close": "Close", "bundle_modal_error.close": "关闭",
"bundle_modal_error.message": "Something went wrong while loading this component.", "bundle_modal_error.message": "载入组件出错。",
"bundle_modal_error.retry": "Try again", "bundle_modal_error.retry": "再次尝试",
"column.blocks": "屏蔽用户", "column.blocks": "屏蔽用户",
"column.community": "本站时间轴", "column.community": "本站时间轴",
"column.favourites": "赞过的嘟文", "column.favourites": "赞过的嘟文",
@ -34,7 +34,7 @@
"column.mutes": "被静音的用户", "column.mutes": "被静音的用户",
"column.notifications": "通知", "column.notifications": "通知",
"column.public": "跨站公共时间轴", "column.public": "跨站公共时间轴",
"column_back_button.label": "Back", "column_back_button.label": "返回",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left", "column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right", "column_header.moveRight_settings": "Move column to the right",
@ -61,8 +61,10 @@
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "静音", "confirmations.mute.confirm": "静音",
"confirmations.mute.message": "想好了,真的要静音 {name}?", "confirmations.mute.message": "想好了,真的要静音 {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "取消关注",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "确定要取消关注 {name}吗?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "活动", "emoji_button.activity": "活动",
"emoji_button.flags": "旗帜", "emoji_button.flags": "旗帜",
"emoji_button.food": "食物和饮料", "emoji_button.food": "食物和饮料",
@ -86,7 +88,7 @@
"getting_started.faq": "FAQ", "getting_started.faq": "FAQ",
"getting_started.heading": "开始使用", "getting_started.heading": "开始使用",
"getting_started.open_source_notice": "Mastodon 是一个开放源码的软件。你可以在官方 GitHub ({github}) 贡献或者回报问题。", "getting_started.open_source_notice": "Mastodon 是一个开放源码的软件。你可以在官方 GitHub ({github}) 贡献或者回报问题。",
"getting_started.userguide": "User Guide", "getting_started.userguide": "用户指南",
"home.column_settings.advanced": "高端", "home.column_settings.advanced": "高端",
"home.column_settings.basic": "基本", "home.column_settings.basic": "基本",
"home.column_settings.filter_regex": "使用正则表达式 (regex) 过滤", "home.column_settings.filter_regex": "使用正则表达式 (regex) 过滤",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "没法转嘟这条嘟文啦……", "status.cannot_reblog": "没法转嘟这条嘟文啦……",
"status.delete": "删除", "status.delete": "删除",
"status.embed": "Embed",
"status.favourite": "赞", "status.favourite": "赞",
"status.load_more": "加载更多", "status.load_more": "加载更多",
"status.media_hidden": "隐藏媒体内容", "status.media_hidden": "隐藏媒体内容",
"status.mention": "提及 @{name}", "status.mention": "提及 @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "展开嘟文", "status.open": "展开嘟文",
"status.pin": "Pin on profile",
"status.reblog": "转嘟", "status.reblog": "转嘟",
"status.reblogged_by": "{name} 转嘟", "status.reblogged_by": "{name} 转嘟",
"status.reply": "回应", "status.reply": "回应",
@ -179,6 +183,7 @@
"status.show_less": "减少显示", "status.show_less": "减少显示",
"status.show_more": "显示更多", "status.show_more": "显示更多",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "撰写", "tabs_bar.compose": "撰写",
"tabs_bar.federated_timeline": "跨站", "tabs_bar.federated_timeline": "跨站",
"tabs_bar.home": "主页", "tabs_bar.home": "主页",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "你確定要將{name}靜音嗎?", "confirmations.mute.message": "你確定要將{name}靜音嗎?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "活動", "emoji_button.activity": "活動",
"emoji_button.flags": "旗幟", "emoji_button.flags": "旗幟",
"emoji_button.food": "飲飲食食", "emoji_button.food": "飲飲食食",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "這篇文章無法被轉推", "status.cannot_reblog": "這篇文章無法被轉推",
"status.delete": "刪除", "status.delete": "刪除",
"status.embed": "Embed",
"status.favourite": "喜歡", "status.favourite": "喜歡",
"status.load_more": "載入更多", "status.load_more": "載入更多",
"status.media_hidden": "隱藏媒體內容", "status.media_hidden": "隱藏媒體內容",
"status.mention": "提及 @{name}", "status.mention": "提及 @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "展開文章", "status.open": "展開文章",
"status.pin": "Pin on profile",
"status.reblog": "轉推", "status.reblog": "轉推",
"status.reblogged_by": "{name} 轉推", "status.reblogged_by": "{name} 轉推",
"status.reply": "回應", "status.reply": "回應",
@ -179,6 +183,7 @@
"status.show_less": "減少顯示", "status.show_less": "減少顯示",
"status.show_more": "顯示更多", "status.show_more": "顯示更多",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "撰寫", "tabs_bar.compose": "撰寫",
"tabs_bar.federated_timeline": "跨站", "tabs_bar.federated_timeline": "跨站",
"tabs_bar.home": "主頁", "tabs_bar.home": "主頁",

View File

@ -63,6 +63,8 @@
"confirmations.mute.message": "你確定要消音 {name} ", "confirmations.mute.message": "你確定要消音 {name} ",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "活動", "emoji_button.activity": "活動",
"emoji_button.flags": "旗幟", "emoji_button.flags": "旗幟",
"emoji_button.food": "食物與飲料", "emoji_button.food": "食物與飲料",
@ -162,12 +164,14 @@
"standalone.public_title": "A look inside...", "standalone.public_title": "A look inside...",
"status.cannot_reblog": "此貼文無法轉推", "status.cannot_reblog": "此貼文無法轉推",
"status.delete": "刪除", "status.delete": "刪除",
"status.embed": "Embed",
"status.favourite": "喜愛", "status.favourite": "喜愛",
"status.load_more": "載入更多", "status.load_more": "載入更多",
"status.media_hidden": "媒體已隱藏", "status.media_hidden": "媒體已隱藏",
"status.mention": "提到 @{name}", "status.mention": "提到 @{name}",
"status.mute_conversation": "消音對話", "status.mute_conversation": "消音對話",
"status.open": "展開這個狀態", "status.open": "展開這個狀態",
"status.pin": "Pin on profile",
"status.reblog": "轉推", "status.reblog": "轉推",
"status.reblogged_by": "{name} 轉推了", "status.reblogged_by": "{name} 轉推了",
"status.reply": "回應", "status.reply": "回應",
@ -179,6 +183,7 @@
"status.show_less": "看少點", "status.show_less": "看少點",
"status.show_more": "看更多", "status.show_more": "看更多",
"status.unmute_conversation": "不消音對話", "status.unmute_conversation": "不消音對話",
"status.unpin": "Unpin from profile",
"tabs_bar.compose": "編輯", "tabs_bar.compose": "編輯",
"tabs_bar.federated_timeline": "聯盟", "tabs_bar.federated_timeline": "聯盟",
"tabs_bar.home": "家", "tabs_bar.home": "家",

View File

@ -149,10 +149,20 @@ const privacyPreference = (a, b) => {
} }
}; };
const hydrate = (state, hydratedState) => {
state = clearAll(state.merge(hydratedState));
if (hydratedState.has('text')) {
state = state.set('text', hydratedState.get('text'));
}
return state;
};
export default function compose(state = initialState, action) { export default function compose(state = initialState, action) {
switch(action.type) { switch(action.type) {
case STORE_HYDRATE: case STORE_HYDRATE:
return clearAll(state.merge(action.state.get('compose'))); return hydrate(state, action.state.get('compose'));
case COMPOSE_MOUNT: case COMPOSE_MOUNT:
return state.set('mounted', true); return state.set('mounted', true);
case COMPOSE_UNMOUNT: case COMPOSE_UNMOUNT:

View File

@ -3,6 +3,10 @@ import {
FAVOURITED_STATUSES_EXPAND_SUCCESS, FAVOURITED_STATUSES_EXPAND_SUCCESS,
} from '../actions/favourites'; } from '../actions/favourites';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import {
FAVOURITE_SUCCESS,
UNFAVOURITE_SUCCESS,
} from '../actions/interactions';
const initialState = ImmutableMap({ const initialState = ImmutableMap({
favourites: ImmutableMap({ favourites: ImmutableMap({
@ -27,12 +31,28 @@ const appendToList = (state, listType, statuses, next) => {
})); }));
}; };
const prependOneToList = (state, listType, status) => {
return state.update(listType, listMap => listMap.withMutations(map => {
map.set('items', map.get('items').unshift(status.get('id')));
}));
};
const removeOneFromList = (state, listType, status) => {
return state.update(listType, listMap => listMap.withMutations(map => {
map.set('items', map.get('items').filter(item => item !== status.get('id')));
}));
};
export default function statusLists(state = initialState, action) { export default function statusLists(state = initialState, action) {
switch(action.type) { switch(action.type) {
case FAVOURITED_STATUSES_FETCH_SUCCESS: case FAVOURITED_STATUSES_FETCH_SUCCESS:
return normalizeList(state, 'favourites', action.statuses, action.next); return normalizeList(state, 'favourites', action.statuses, action.next);
case FAVOURITED_STATUSES_EXPAND_SUCCESS: case FAVOURITED_STATUSES_EXPAND_SUCCESS:
return appendToList(state, 'favourites', action.statuses, action.next); return appendToList(state, 'favourites', action.statuses, action.next);
case FAVOURITE_SUCCESS:
return prependOneToList(state, 'favourites', action.status);
case UNFAVOURITE_SUCCESS:
return removeOneFromList(state, 'favourites', action.status);
default: default:
return state; return state;
} }

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