Merge commit '4aea3f88a6d30f102a79c2da7fcfac96465ba1a8' into merging-upstream

main
Ondřej Hruška 7 years ago
commit 9330ea1f4d
No known key found for this signature in database
GPG Key ID: 2C5FD5035250423D

@ -101,11 +101,19 @@ SMTP_FROM_ADDRESS=notifications@example.com
# Swift (optional)
# SWIFT_ENABLED=true
# SWIFT_USERNAME=
# For Keystone V3, the value for SWIFT_TENANT should be the project name
# SWIFT_TENANT=
# SWIFT_PASSWORD=
# Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid
# issues with token rate-limiting during high load.
# SWIFT_AUTH_URL=
# SWIFT_CONTAINER=
# SWIFT_OBJECT_URL=
# SWIFT_REGION=
# Defaults to 'default'
# SWIFT_DOMAIN_NAME=
# Defaults to 60 seconds. Set to 0 to disable
# SWIFT_CACHE_TTL=
# Optional alias for S3 if you want to use Cloudfront or Cloudflare in front
# S3_CLOUDFRONT_HOST=

@ -1 +1 @@
2.4.1
2.4.2

@ -26,18 +26,16 @@ addons:
postgresql: 9.4
apt:
sources:
- ubuntu-toolchain-r-test
- trusty-media
packages:
- ffmpeg
- g++-6
- libprotobuf-dev
- protobuf-compiler
- libicu-dev
rvm:
- 2.3.4
- 2.4.1
- 2.4.2
services:
- redis-server

@ -1,4 +1,5 @@
ffmpeg
libicu[0-9][0-9]
libicu-dev
libidn11
libidn11-dev

@ -1,4 +1,4 @@
FROM ruby:2.4.1-alpine3.6
FROM ruby:2.4.2-alpine3.6
LABEL maintainer="https://github.com/tootsuite/mastodon" \
description="A GNU Social-compatible microblogging server"

@ -5,8 +5,8 @@ ruby '>= 2.3.0', '< 2.5.0'
gem 'pkg-config', '~> 1.2'
gem 'puma', '~> 3.8'
gem 'rails', '~> 5.1.0'
gem 'puma', '~> 3.10'
gem 'rails', '~> 5.1.4'
gem 'uglifier', '~> 3.2'
gem 'hamlit-rails', '~> 0.2'
@ -25,7 +25,7 @@ gem 'bootsnap'
gem 'browser'
gem 'charlock_holmes', '~> 0.7.5'
gem 'iso-639'
gem 'cld3', '~> 3.1'
gem 'cld3', '~> 3.2.0'
gem 'devise', '~> 4.2'
gem 'devise-two-factor', '~> 3.0'
gem 'doorkeeper', '~> 4.2'

@ -1,25 +1,25 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (5.1.3)
actionpack (= 5.1.3)
actioncable (5.1.4)
actionpack (= 5.1.4)
nio4r (~> 2.0)
websocket-driver (~> 0.6.1)
actionmailer (5.1.3)
actionpack (= 5.1.3)
actionview (= 5.1.3)
activejob (= 5.1.3)
actionmailer (5.1.4)
actionpack (= 5.1.4)
actionview (= 5.1.4)
activejob (= 5.1.4)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.1.3)
actionview (= 5.1.3)
activesupport (= 5.1.3)
actionpack (5.1.4)
actionview (= 5.1.4)
activesupport (= 5.1.4)
rack (~> 2.0)
rack-test (~> 0.6.3)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.1.3)
activesupport (= 5.1.3)
actionview (5.1.4)
activesupport (= 5.1.4)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -30,16 +30,16 @@ GEM
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.2)
active_record_query_trace (1.5.4)
activejob (5.1.3)
activesupport (= 5.1.3)
activejob (5.1.4)
activesupport (= 5.1.4)
globalid (>= 0.3.6)
activemodel (5.1.3)
activesupport (= 5.1.3)
activerecord (5.1.3)
activemodel (= 5.1.3)
activesupport (= 5.1.3)
activemodel (5.1.4)
activesupport (= 5.1.4)
activerecord (5.1.4)
activemodel (= 5.1.4)
activesupport (= 5.1.4)
arel (~> 8.0)
activesupport (5.1.3)
activesupport (5.1.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7)
minitest (~> 5.1)
@ -57,33 +57,33 @@ GEM
encryptor (~> 3.0.0)
av (0.9.0)
cocaine (~> 0.5.3)
aws-sdk (2.10.21)
aws-sdk-resources (= 2.10.21)
aws-sdk-core (2.10.21)
aws-sdk (2.10.46)
aws-sdk-resources (= 2.10.46)
aws-sdk-core (2.10.46)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
aws-sdk-resources (2.10.21)
aws-sdk-core (= 2.10.21)
aws-sigv4 (1.0.1)
aws-sdk-resources (2.10.46)
aws-sdk-core (= 2.10.46)
aws-sigv4 (1.0.2)
bcrypt (3.1.11)
better_errors (2.1.1)
better_errors (2.3.0)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
erubi (>= 1.0.0)
rack (>= 0.9.0)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootsnap (1.1.2)
bootsnap (1.1.3)
msgpack (~> 1.0)
brakeman (3.7.2)
browser (2.4.0)
browser (2.5.1)
builder (3.2.3)
bullet (5.5.1)
bullet (5.6.1)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0)
bundler-audit (0.6.0)
bundler (~> 1.2)
thor (~> 0.18)
capistrano (3.8.2)
capistrano (3.9.1)
airbrussh (>= 1.0.0)
i18n
rake (>= 10.0.0)
@ -99,9 +99,9 @@ GEM
sshkit (~> 1.3)
capistrano-yarn (2.0.2)
capistrano (~> 3.0)
capybara (2.14.4)
capybara (2.15.1)
addressable
mime-types (>= 1.16)
mini_mime (>= 0.1.3)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
@ -110,12 +110,12 @@ GEM
activesupport
charlock_holmes (0.7.5)
chunky_png (1.3.8)
cld3 (3.1.3)
cld3 (3.2.0)
ffi (>= 1.1.0, < 1.10.0)
climate_control (0.2.0)
cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0)
coderay (1.1.1)
coderay (1.1.2)
colorize (0.8.1)
concurrent-ruby (1.0.5)
connection_pool (2.2.1)
@ -151,13 +151,12 @@ GEM
thread_safe
encryptor (3.0.0)
erubi (1.6.1)
erubis (2.7.0)
et-orbi (1.0.5)
tzinfo
excon (0.58.0)
excon (0.59.0)
execjs (2.7.0)
fabrication (2.16.2)
faker (1.7.3)
fabrication (2.16.3)
faker (1.8.4)
i18n (~> 0.5)
fast_blank (1.0.0)
ffi (1.9.18)
@ -194,7 +193,7 @@ GEM
railties (>= 4.0.1)
hamster (3.0.0)
concurrent-ruby (~> 1.0)
hashdiff (0.3.5)
hashdiff (0.3.6)
highline (1.7.8)
hiredis (0.6.1)
hkdf (0.3.0)
@ -213,11 +212,11 @@ GEM
colorize
rack
i18n (0.8.6)
i18n-tasks (0.9.16)
i18n-tasks (0.9.18)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
easy_translate (>= 0.5.0)
erubis
erubi
highline (>= 1.7.3)
i18n
parser (>= 2.2.3.0)
@ -231,7 +230,7 @@ GEM
json-ld (2.1.5)
multi_json (~> 1.12)
rdf (~> 2.2)
json-ld-preloaded (2.2.1)
json-ld-preloaded (2.2.2)
json-ld (~> 2.1, >= 2.1.5)
multi_json (~> 1.11)
rdf (~> 2.2)
@ -258,10 +257,11 @@ GEM
letter_opener (~> 1.0)
railties (>= 3.2)
link_header (0.0.8)
lograge (0.5.1)
lograge (0.6.0)
actionpack (>= 4, < 5.2)
activesupport (>= 4, < 5.2)
railties (>= 4, < 5.2)
request_store (~> 1.0)
loofah (2.0.3)
nokogiri (>= 1.5.9)
mail (2.6.6)
@ -276,27 +276,28 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mimemagic (0.3.2)
mini_mime (0.1.4)
mini_portile2 (2.2.0)
minitest (5.10.3)
msgpack (1.1.0)
multi_json (1.12.1)
multi_json (1.12.2)
net-scp (1.2.1)
net-ssh (>= 2.6.5)
net-ssh (4.1.0)
net-ssh (4.2.0)
nio4r (2.1.0)
nokogiri (1.8.0)
mini_portile2 (~> 2.2.0)
nokogumbo (1.4.13)
nokogiri
oj (3.3.4)
openssl (2.0.4)
oj (3.3.5)
openssl (2.0.5)
orm_adapter (0.5.0)
ostatus2 (2.0.1)
addressable (~> 2.4)
http (~> 2.0)
nokogiri (~> 1.6)
openssl (~> 2.0)
ox (2.5.0)
ox (2.6.0)
paperclip (5.1.0)
activemodel (>= 4.2.0)
activesupport (>= 4.2.0)
@ -306,15 +307,15 @@ GEM
paperclip-av-transcoder (0.6.4)
av (~> 0.9.0)
paperclip (>= 2.5.2)
parallel (1.11.2)
parallel_tests (2.14.2)
parallel (1.12.0)
parallel_tests (2.15.0)
parallel
parser (2.4.0.0)
ast (~> 2.2)
pg (0.21.0)
pghero (1.7.0)
activerecord
pkg-config (1.2.4)
pkg-config (1.2.7)
powerpack (0.1.1)
pry (0.10.4)
coderay (~> 1.1.0)
@ -323,7 +324,7 @@ GEM
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (3.0.0)
puma (3.9.1)
puma (3.10.0)
pundit (1.1.0)
activesupport (>= 3.0.0)
rabl (0.13.1)
@ -334,20 +335,20 @@ GEM
rack-cors (0.4.1)
rack-protection (2.0.0)
rack
rack-test (0.6.3)
rack (>= 1.0)
rack-test (0.7.0)
rack (>= 1.0, < 3)
rack-timeout (0.4.2)
rails (5.1.3)
actioncable (= 5.1.3)
actionmailer (= 5.1.3)
actionpack (= 5.1.3)
actionview (= 5.1.3)
activejob (= 5.1.3)
activemodel (= 5.1.3)
activerecord (= 5.1.3)
activesupport (= 5.1.3)
rails (5.1.4)
actioncable (= 5.1.4)
actionmailer (= 5.1.4)
actionpack (= 5.1.4)
actionview (= 5.1.4)
activejob (= 5.1.4)
activemodel (= 5.1.4)
activerecord (= 5.1.4)
activesupport (= 5.1.4)
bundler (>= 1.3.0)
railties (= 5.1.3)
railties (= 5.1.4)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1)
@ -363,16 +364,16 @@ GEM
railties (~> 5.0)
rails-settings-cached (0.6.6)
rails (>= 4.2.0)
railties (5.1.3)
actionpack (= 5.1.3)
activesupport (= 5.1.3)
railties (5.1.4)
actionpack (= 5.1.4)
activesupport (= 5.1.4)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.2.2)
rake
rake (12.0.0)
rdf (2.2.8)
rake (12.1.0)
rdf (2.2.9)
hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.2)
@ -396,6 +397,7 @@ GEM
redis-store (>= 1.2, < 2)
redis-store (1.3.0)
redis (>= 2.2)
request_store (1.3.2)
responders (2.4.0)
actionpack (>= 4.2.0, < 5.3)
railties (>= 4.2.0, < 5.3)
@ -410,7 +412,7 @@ GEM
rspec-mocks (3.6.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0)
rspec-rails (3.6.0)
rspec-rails (3.6.1)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
@ -422,15 +424,15 @@ GEM
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.6.0)
rubocop (0.49.1)
rubocop (0.50.0)
parallel (~> 1.10)
parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
rainbow (>= 2.2.2, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-oembed (0.12.0)
ruby-progressbar (1.8.1)
ruby-progressbar (1.8.3)
rufus-scheduler (3.4.2)
et-orbi (~> 1.0)
safe_yaml (1.0.4)
@ -438,7 +440,7 @@ GEM
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4.1)
sass (3.4.24)
sass (3.4.25)
scss_lint (0.54.0)
rake (>= 0.9, < 13)
sass (~> 3.4.20)
@ -450,12 +452,12 @@ GEM
sidekiq-bulk (0.1.1)
activesupport
sidekiq
sidekiq-scheduler (2.1.8)
sidekiq-scheduler (2.1.9)
redis (~> 3)
rufus-scheduler (~> 3.2)
sidekiq (>= 3)
tilt (>= 1.4.0)
sidekiq-unique-jobs (5.0.9)
sidekiq-unique-jobs (5.0.10)
sidekiq (>= 4.0, <= 6.0)
thor (~> 0)
simple-navigation (4.0.5)
@ -463,20 +465,20 @@ GEM
simple_form (3.5.0)
actionpack (> 4, < 5.2)
activemodel (> 4, < 5.2)
simplecov (0.14.1)
simplecov (0.15.1)
docile (~> 1.1.0)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.1)
simplecov-html (0.10.2)
slop (3.6.0)
sprockets (3.7.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.0)
sprockets-rails (3.2.1)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sshkit (1.13.1)
sshkit (1.14.0)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
statsd-instrument (2.1.4)
@ -541,7 +543,7 @@ DEPENDENCIES
capistrano-yarn (~> 2.0)
capybara (~> 2.14)
charlock_holmes (~> 0.7.5)
cld3 (~> 3.1)
cld3 (~> 3.2.0)
climate_control (~> 0.2)
devise (~> 4.2)
devise-two-factor (~> 3.0)
@ -582,13 +584,13 @@ DEPENDENCIES
pghero (~> 1.7)
pkg-config (~> 1.2)
pry-rails (~> 0.3)
puma (~> 3.8)
puma (~> 3.10)
pundit (~> 1.1)
rabl (~> 0.13)
rack-attack (~> 5.0)
rack-cors (~> 0.4)
rack-timeout (~> 0.4)
rails (~> 5.1.0)
rails (~> 5.1.4)
rails-controller-testing (~> 1.0)
rails-i18n (~> 5.0)
rails-settings-cached (~> 0.6)
@ -620,7 +622,7 @@ DEPENDENCIES
webpush
RUBY VERSION
ruby 2.4.1p111
ruby 2.4.2p198
BUNDLED WITH
1.15.4

@ -0,0 +1,34 @@
# frozen_string_literal: true
module Admin
class CustomEmojisController < BaseController
def index
@custom_emojis = CustomEmoji.where(domain: nil)
end
def new
@custom_emoji = CustomEmoji.new
end
def create
@custom_emoji = CustomEmoji.new(resource_params)
if @custom_emoji.save
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg')
else
render :new
end
end
def destroy
CustomEmoji.find(params[:id]).destroy
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
end
private
def resource_params
params.require(:custom_emoji).permit(:shortcode, :image)
end
end
end

@ -14,8 +14,12 @@ module Admin
private
def filtered_instances
InstanceFilter.new(filter_params).results
end
def paginated_instances
Account.remote.by_domain_accounts.page(params[:page])
filtered_instances.page(params[:page])
end
helper_method :paginated_instances
@ -27,5 +31,11 @@ module Admin
def subscribeable_accounts
Account.with_followers.remote.where(domain: params[:by_domain])
end
def filter_params
params.permit(
:domain_name
)
end
end
end

@ -14,6 +14,7 @@ module Admin
open_deletion
timeline_preview
bootstrap_timeline_accounts
thumbnail
).freeze
BOOLEAN_SETTINGS = %w(
@ -22,14 +23,23 @@ module Admin
timeline_preview
).freeze
UPLOAD_SETTINGS = %w(
thumbnail
).freeze
def edit
@admin_settings = Form::AdminSettings.new
end
def update
settings_params.each do |key, value|
setting = Setting.where(var: key).first_or_initialize(var: key)
setting.update(value: value_for_update(key, value))
if UPLOAD_SETTINGS.include?(key)
upload = SiteUpload.where(var: key).first_or_initialize(var: key)
upload.update(file: value)
else
setting = Setting.where(var: key).first_or_initialize(var: key)
setting.update(value: value_for_update(key, value))
end
end
flash[:notice] = I18n.t('generic.changes_saved_msg')

@ -12,7 +12,30 @@ class HomeController < ApplicationController
private
def authenticate_user!
redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in?
return if user_signed_in?
matches = request.path.match(/\A\/web\/(statuses|accounts)\/([\d]+)\z/)
if matches
case matches[1]
when 'statuses'
status = Status.find_by(id: matches[2])
if status && (status.public_visibility? || status.unlisted_visibility?)
redirect_to(ActivityPub::TagManager.instance.url_for(status))
return
end
when 'accounts'
account = Account.find_by(id: matches[2])
if account
redirect_to(ActivityPub::TagManager.instance.url_for(account))
return
end
end
end
redirect_to(default_redirect_path)
end
def set_initial_state_json
@ -29,4 +52,14 @@ class HomeController < ApplicationController
admin: Account.find_local(Setting.site_contact_username),
}
end
def default_redirect_path
if request.path.start_with?('/web')
new_user_session_path
elsif single_user_mode?
short_account_path(Account.first)
else
about_path
end
end
end

@ -0,0 +1,40 @@
# frozen_string_literal: true
class MediaProxyController < ApplicationController
include RoutingHelper
def show
RedisLock.acquire(lock_options) do |lock|
if lock.acquired?
@media_attachment = MediaAttachment.remote.find(params[:id])
redownload! if @media_attachment.needs_redownload? && !reject_media?
end
end
redirect_to full_asset_url(@media_attachment.file.url(version))
end
private
def redownload!
@media_attachment.file_remote_url = @media_attachment.remote_url
@media_attachment.created_at = Time.now.utc
@media_attachment.save!
end
def version
if request.path.ends_with?('/small')
:small
else
:original
end
end
def lock_options
{ redis: Redis.current, key: "media_download:#{params[:id]}" }
end
def reject_media?
DomainBlock.find_by(domain: @media_attachment.account.domain)&.reject_media?
end
end

@ -42,4 +42,8 @@ module ApplicationHelper
content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
end
def opengraph(property, content)
tag(:meta, content: content, property: property)
end
end

@ -41,7 +41,7 @@ module SettingsHelper
end
def filterable_languages
I18n.available_locales.map { |locale| locale.to_s.split('-').first.to_sym }.uniq
LanguageDetector.instance.language_names.select(&HUMAN_LOCALES.method(:key?))
end
def hash_to_object(hash)

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="61.076954mm" height="65.47831mm" viewBox="0 0 216.4144 232.00976"><path d="M211.80734 139.0875c-3.18125 16.36625-28.4925 34.2775-57.5625 37.74875-15.15875 1.80875-30.08375 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.39125 27.9425 21.11625.7225 39.91875-5.20625 39.91875-5.20625l.8675 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23234 213.82 1.40609 165.31125.20859 116.09125c-.365-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67234 3.45375 78.20359.2425 107.86484 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.975 14.7525 32.975 65.0825 0 0 .41375 37.13375-4.59875 62.915" fill="#3088d4"/><path d="M177.50984 80.077v60.94125h-24.14375v-59.15c0-12.46875-5.24625-18.7975-15.74-18.7975-11.6025 0-17.4175 7.5075-17.4175 22.3525v32.37625H96.20734V85.42325c0-14.845-5.81625-22.3525-17.41875-22.3525-10.49375 0-15.74 6.32875-15.74 18.7975v59.15H38.90484V80.077c0-12.455 3.17125-22.3525 9.54125-29.675 6.56875-7.3225 15.17125-11.07625 25.85-11.07625 12.355 0 21.71125 4.74875 27.8975 14.2475l6.01375 10.08125 6.015-10.08125c6.185-9.49875 15.54125-14.2475 27.8975-14.2475 10.6775 0 19.28 3.75375 25.85 11.07625 6.36875 7.3225 9.54 17.22 9.54 29.675" fill="#fff"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976"><path d="M211.80734 139.0875c-3.18125 16.36625-28.4925 34.2775-57.5625 37.74875-15.15875 1.80875-30.08375 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.39125 27.9425 21.11625.7225 39.91875-5.20625 39.91875-5.20625l.8675 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23234 213.82 1.40609 165.31125.20859 116.09125c-.365-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67234 3.45375 78.20359.2425 107.86484 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.975 14.7525 32.975 65.0825 0 0 .41375 37.13375-4.59875 62.915" fill="#3088d4"/><path d="M177.50984 80.077v60.94125h-24.14375v-59.15c0-12.46875-5.24625-18.7975-15.74-18.7975-11.6025 0-17.4175 7.5075-17.4175 22.3525v32.37625H96.20734V85.42325c0-14.845-5.81625-22.3525-17.41875-22.3525-10.49375 0-15.74 6.32875-15.74 18.7975v59.15H38.90484V80.077c0-12.455 3.17125-22.3525 9.54125-29.675 6.56875-7.3225 15.17125-11.07625 25.85-11.07625 12.355 0 21.71125 4.74875 27.8975 14.2475l6.01375 10.08125 6.015-10.08125c6.185-9.49875 15.54125-14.2475 27.8975-14.2475 10.6775 0 19.28 3.75375 25.85 11.07625 6.36875 7.3225 9.54 17.22 9.54 29.675" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="61.077141mm" height="65.47831mm" viewBox="0 0 216.41507 232.00976"><path d="M211.80683 139.0875c-3.1825 16.36625-28.4925 34.2775-57.5625 37.74875-15.16 1.80875-30.0825 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.3925 27.9425 21.115.7225 39.91625-5.20625 39.91625-5.20625l.86875 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23183 213.82 1.40558 165.31125.20808 116.09125c-.36375-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67058 3.45375 78.20308.2425 107.86433 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.97625 14.7525 32.97625 65.0825 0 0 .4125 37.13375-4.6 62.915" fill="#3088d4"/><path d="M65.68743 96.45938c0 9.01375-7.3075 16.32125-16.3225 16.32125-9.01375 0-16.32-7.3075-16.32-16.32125 0-9.01375 7.30625-16.3225 16.32-16.3225 9.015 0 16.3225 7.30875 16.3225 16.3225M124.52893 96.45938c0 9.01375-7.30875 16.32125-16.3225 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.3225 7.30875 16.3225 16.3225M183.36933 96.45938c0 9.01375-7.3075 16.32125-16.32125 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.32125 7.30875 16.32125 16.3225" fill="#fff"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.41507 232.00976"><path d="M211.80683 139.0875c-3.1825 16.36625-28.4925 34.2775-57.5625 37.74875-15.16 1.80875-30.0825 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.3925 27.9425 21.115.7225 39.91625-5.20625 39.91625-5.20625l.86875 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23183 213.82 1.40558 165.31125.20808 116.09125c-.36375-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67058 3.45375 78.20308.2425 107.86433 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.97625 14.7525 32.97625 65.0825 0 0 .4125 37.13375-4.6 62.915" fill="#3088d4"/><path d="M65.68743 96.45938c0 9.01375-7.3075 16.32125-16.3225 16.32125-9.01375 0-16.32-7.3075-16.32-16.32125 0-9.01375 7.30625-16.3225 16.32-16.3225 9.015 0 16.3225 7.30875 16.3225 16.3225M124.52893 96.45938c0 9.01375-7.30875 16.32125-16.3225 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.3225 7.30875 16.3225 16.3225M183.36933 96.45938c0 9.01375-7.3075 16.32125-16.32125 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.32125 7.30875 16.32125 16.3225" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

@ -0,0 +1,17 @@
export const HEIGHT_CACHE_SET = 'HEIGHT_CACHE_SET';
export const HEIGHT_CACHE_CLEAR = 'HEIGHT_CACHE_CLEAR';
export function setHeight (key, id, height) {
return {
type: HEIGHT_CACHE_SET,
key,
id,
height,
};
};
export function clearHeight () {
return {
type: HEIGHT_CACHE_CLEAR,
};
};

@ -23,9 +23,6 @@ export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST';
export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
export const STATUS_SET_HEIGHT = 'STATUS_SET_HEIGHT';
export const STATUSES_CLEAR_HEIGHT = 'STATUSES_CLEAR_HEIGHT';
export function fetchStatusRequest(id, skipLoading) {
return {
type: STATUS_FETCH_REQUEST,
@ -218,17 +215,3 @@ export function unmuteStatusFail(id, error) {
error,
};
};
export function setStatusHeight (id, height) {
return {
type: STATUS_SET_HEIGHT,
id,
height,
};
};
export function clearStatusesHeight () {
return {
type: STATUSES_CLEAR_HEIGHT,
};
};

@ -7,10 +7,13 @@ import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
export default class IntersectionObserverArticle extends ImmutablePureComponent {
static propTypes = {
intersectionObserverWrapper: PropTypes.object,
intersectionObserverWrapper: PropTypes.object.isRequired,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
saveHeightKey: PropTypes.string,
cachedHeight: PropTypes.number,
onHeightChange: PropTypes.func,
children: PropTypes.node,
};
@ -34,13 +37,10 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
}
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,
const { intersectionObserverWrapper, id } = this.props;
intersectionObserverWrapper.observe(
id,
this.node,
this.handleIntersection
);
@ -49,20 +49,21 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
}
componentWillUnmount () {
if (this.props.intersectionObserverWrapper) {
this.props.intersectionObserverWrapper.unobserve(this.props.id, this.node);
}
const { intersectionObserverWrapper, id } = this.props;
intersectionObserverWrapper.unobserve(id, this.node);
this.componentMounted = false;
}
handleIntersection = (entry) => {
const { onHeightChange, saveHeightKey, id } = this.props;
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);
if (onHeightChange && saveHeightKey) {
onHeightChange(saveHeightKey, id, this.height);
}
}
@ -94,16 +95,16 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
}
render () {
const { children, id, index, listLength } = this.props;
const { children, id, index, listLength, cachedHeight } = this.props;
const { isIntersecting, isHidden } = this.state;
if (!isIntersecting && isHidden) {
if (!isIntersecting && (isHidden || cachedHeight)) {
return (
<article
ref={this.handleRef}
aria-posinset={index}
aria-setsize={listLength}
style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }}
style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
data-id={id}
tabIndex='0'
>

@ -17,7 +17,7 @@ export default class LoadMore extends React.PureComponent {
const { visible } = this.props;
return (
<button className='load-more' disabled={!visible} style={{ opacity: visible ? 1 : 0 }} onClick={this.props.onClick}>
<button className='load-more' disabled={!visible} style={{ visibility: visible ? 'visible' : 'hidden' }} onClick={this.props.onClick}>
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
</button>
);

@ -122,8 +122,8 @@ class Item extends React.PureComponent {
const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
const srcSet = hasSize && `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w`;
const sizes = hasSize && `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw`;
const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null;
const sizes = hasSize ? `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw` : null;
thumbnail = (
<a

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import { ScrollContainer } from 'react-router-scroll';
import PropTypes from 'prop-types';
import IntersectionObserverArticle from './intersection_observer_article';
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
import LoadMore from './load_more';
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
import { throttle } from 'lodash';
@ -9,6 +9,10 @@ import { List as ImmutableList } from 'immutable';
export default class ScrollableList extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
scrollKey: PropTypes.string.isRequired,
onScrollToBottom: PropTypes.func,
@ -163,7 +167,7 @@ export default class ScrollableList extends PureComponent {
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} />;
const loadMore = (hasMore && childrenCount > 0) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
let scrollableArea = null;
if (isLoading || childrenCount > 0 || !emptyMessage) {
@ -173,9 +177,16 @@ export default class ScrollableList extends PureComponent {
{prepend}
{React.Children.map(this.props.children, (child, index) => (
<IntersectionObserverArticle key={child.key} id={child.key} index={index} listLength={childrenCount} intersectionObserverWrapper={this.intersectionObserverWrapper}>
<IntersectionObserverArticleContainer
key={child.key}
id={child.key}
index={index}
listLength={childrenCount}
intersectionObserverWrapper={this.intersectionObserverWrapper}
saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
>
{child}
</IntersectionObserverArticle>
</IntersectionObserverArticleContainer>
))}
{loadMore}

@ -12,7 +12,7 @@ import StatusContent from './status_content';
import StatusActionBar from './status_action_bar';
import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components';
import { MediaGallery, Video } from '../features/ui/util/async-components';
// We use the component (and not the container) since we do not want
// to use the progress bar to show download progress
@ -91,6 +91,10 @@ export default class Status extends ImmutablePureComponent {
return <div className='media-spoiler-video' style={{ height: '110px' }} />;
}
handleOpenVideo = startTime => {
this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime);
}
render () {
let media = null;
let statusAvatar;
@ -130,9 +134,18 @@ export default class Status extends ImmutablePureComponent {
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const video = status.getIn(['media_attachments', 0]);
media = (
<Bundle fetchComponent={VideoPlayer} loading={this.renderLoadingVideoPlayer} >
{Component => <Component media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />}
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
{Component => <Component
preview={video.get('preview_url')}
src={video.get('url')}
width={239}
height={110}
sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo}
/>}
</Bundle>
);
} else {

@ -0,0 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import Card from '../features/status/components/card';
import { fromJS } from 'immutable';
export default class CardContainer extends React.PureComponent {
static propTypes = {
locale: PropTypes.string,
card: PropTypes.array.isRequired,
};
render () {
const { card, ...props } = this.props;
return <Card card={fromJS(card)} {...props} />;
}
}

@ -0,0 +1,17 @@
import { connect } from 'react-redux';
import IntersectionObserverArticle from '../components/intersection_observer_article';
import { setHeight } from '../actions/height_cache';
const makeMapStateToProps = (state, props) => ({
cachedHeight: state.getIn(['height_cache', props.saveHeightKey, props.id]),
});
const mapDispatchToProps = (dispatch) => ({
onHeightChange (key, id, height) {
dispatch(setHeight(key, id, height));
},
});
export default connect(makeMapStateToProps, mapDispatchToProps)(IntersectionObserverArticle);

@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import MediaGallery from '../components/media_gallery';
import { fromJS } from 'immutable';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
export default class MediaGalleryContainer extends React.PureComponent {
static propTypes = {
locale: PropTypes.string.isRequired,
media: PropTypes.array.isRequired,
};
handleOpenMedia = () => {}
render () {
const { locale, media, ...props } = this.props;
return (
<IntlProvider locale={locale} messages={messages}>
<MediaGallery
{...props}
media={fromJS(media)}
onOpenMedia={this.handleOpenMedia}
/>
</IntlProvider>
);
}
}

@ -21,7 +21,7 @@ import {
blockAccount,
muteAccount,
} from '../actions/accounts';
import { muteStatus, unmuteStatus, deleteStatus, setStatusHeight } from '../actions/statuses';
import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses';
import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
@ -141,10 +141,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onHeightChange (status, height) {
dispatch(setStatusHeight(status.get('id'), height));
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));

@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import Video from '../features/video';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
export default class VideoContainer extends React.PureComponent {
static propTypes = {
locale: PropTypes.string.isRequired,
};
render () {
const { locale, ...props } = this.props;
return (
<IntlProvider locale={locale} messages={messages}>
<Video {...props} />
</IntlProvider>
);
}
}

@ -3,28 +3,48 @@ import Trie from 'substring-trie';
const trie = new Trie(Object.keys(unicodeMapping));
const emojify = str => {
let rtn = '';
for (;;) {
let match, i = 0;
while (i < str.length && str[i] !== '<' && !(match = trie.search(str.slice(i)))) {
i += str.codePointAt(i) < 65536 ? 1 : 2;
}
if (i === str.length)
break;
else if (str[i] === '<') {
let tagend = str.indexOf('>', i + 1) + 1;
if (!tagend)
break;
rtn += str.slice(0, tagend);
str = str.slice(tagend);
} else {
const [filename, shortCode] = unicodeMapping[match];
rtn += str.slice(0, i) + `<img draggable="false" class="emojione" alt="${match}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`;
str = str.slice(i + match.length);
const emojify = (str, customEmojis = {}) => {
// This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.)
// and replacing valid unicode strings
// that _aren't_ within tags with an <img> version.
// The goal is to be the same as an emojione.regUnicode replacement, but faster.
let i = -1;
let insideTag = false;
let insideShortname = false;
let shortnameStartIndex = -1;
let match;
while (++i < str.length) {
const char = str.charAt(i);
if (insideShortname && char === ':') {
const shortname = str.substring(shortnameStartIndex, i + 1);
if (shortname in customEmojis) {
const replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${customEmojis[shortname]}" />`;
str = str.substring(0, shortnameStartIndex) + replacement + str.substring(i + 1);
i += (replacement.length - shortname.length - 1); // jump ahead the length we've added to the string
} else {
i--;
}
insideShortname = false;
} else if (insideTag && char === '>') {
insideTag = false;
} else if (char === '<') {
insideTag = true;
insideShortname = false;
} else if (!insideTag && char === ':') {
insideShortname = true;
shortnameStartIndex = i;
} else if (!insideTag && (match = trie.search(str.substring(i)))) {
const unicodeStr = match;
if (unicodeStr in unicodeMapping) {
const [filename, shortCode] = unicodeMapping[unicodeStr];
const alt = unicodeStr;
const replacement = `<img draggable="false" class="emojione" alt="${alt}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`;
str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length);
i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string
}
}
}
return rtn + str;
return str;
};
export default emojify;

@ -1,7 +1,9 @@
import { urlRegex } from './url_regex';
const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx';
export function countableText(inputText) {
return inputText
.replace(/https?:\/\/\S+/g, urlPlaceholder)
.replace(urlRegex, urlPlaceholder)
.replace(/(?:^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+)/ig, '@$2');
};

@ -0,0 +1,196 @@
const regexen = {};
const regexSupplant = function(regex, flags) {
flags = flags || '';
if (typeof regex !== 'string') {
if (regex.global && flags.indexOf('g') < 0) {
flags += 'g';
}
if (regex.ignoreCase && flags.indexOf('i') < 0) {
flags += 'i';
}
if (regex.multiline && flags.indexOf('m') < 0) {
flags += 'm';
}
regex = regex.source;
}
return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) {
var newRegex = regexen[name] || '';
if (typeof newRegex !== 'string') {
newRegex = newRegex.source;
}
return newRegex;
}), flags);
};
const stringSupplant = function(str, values) {
return str.replace(/#\{(\w+)\}/g, function(match, name) {
return values[name] || '';
});
};
export const urlRegex = (function() {
regexen.spaces_group = /\x09-\x0D\x20\x85\xA0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000/;
regexen.invalid_chars_group = /\uFFFE\uFEFF\uFFFF\u202A-\u202E/;
regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$/;
regexen.validUrlPrecedingChars = regexSupplant(/(?:[^A-Za-z0-9@$##{invalid_chars_group}]|^)/);
regexen.invalidDomainChars = stringSupplant('#{punct}#{spaces_group}#{invalid_chars_group}', regexen);
regexen.validDomainChars = regexSupplant(/[^#{invalidDomainChars}]/);
regexen.validSubdomain = regexSupplant(/(?:(?:#{validDomainChars}(?:[_-]|#{validDomainChars})*)?#{validDomainChars}\.)/);
regexen.validDomainName = regexSupplant(/(?:(?:#{validDomainChars}(?:-|#{validDomainChars})*)?#{validDomainChars}\.)/);
regexen.validGTLD = regexSupplant(RegExp(
'(?:(?:' +
'삼성|닷컴|닷넷|香格里拉|餐厅|食品|飞利浦|電訊盈科|集团|通販|购物|谷歌|诺基亚|联通|网络|网站|网店|网址|组织机构|移动|珠宝|点看|游戏|淡马锡|机构|書籍|时尚|新闻|政府|' +
'政务|手表|手机|我爱你|慈善|微博|广东|工行|家電|娱乐|天主教|大拿|大众汽车|在线|嘉里大酒店|嘉里|商标|商店|商城|公益|公司|八卦|健康|信息|佛山|企业|中文网|中信|世界|' +
'ポイント|ファッション|セール|ストア|コム|グーグル|クラウド|みんな|คอม|संगठन|नेट|कॉम|همراه|موقع|موبايلي|كوم|كاثوليك|عرب|شبكة|' +
'بيتك|بازار|العليان|ارامكو|اتصالات|ابوظبي|קום|сайт|рус|орг|онлайн|москва|ком|католик|дети|' +
'zuerich|zone|zippo|zip|zero|zara|zappos|yun|youtube|you|yokohama|yoga|yodobashi|yandex|yamaxun|' +
'yahoo|yachts|xyz|xxx|xperia|xin|xihuan|xfinity|xerox|xbox|wtf|wtc|wow|world|works|work|woodside|' +
'wolterskluwer|wme|winners|wine|windows|win|williamhill|wiki|wien|whoswho|weir|weibo|wedding|wed|' +
'website|weber|webcam|weatherchannel|weather|watches|watch|warman|wanggou|wang|walter|walmart|' +
'wales|vuelos|voyage|voto|voting|vote|volvo|volkswagen|vodka|vlaanderen|vivo|viva|vistaprint|' +
'vista|vision|visa|virgin|vip|vin|villas|viking|vig|video|viajes|vet|versicherung|' +
'vermögensberatung|vermögensberater|verisign|ventures|vegas|vanguard|vana|vacations|ups|uol|uno|' +
'university|unicom|uconnect|ubs|ubank|tvs|tushu|tunes|tui|tube|trv|trust|travelersinsurance|' +
'travelers|travelchannel|travel|training|trading|trade|toys|toyota|town|tours|total|toshiba|' +
'toray|top|tools|tokyo|today|tmall|tkmaxx|tjx|tjmaxx|tirol|tires|tips|tiffany|tienda|tickets|' +
'tiaa|theatre|theater|thd|teva|tennis|temasek|telefonica|telecity|tel|technology|tech|team|tdk|' +
'tci|taxi|tax|tattoo|tatar|tatamotors|target|taobao|talk|taipei|tab|systems|symantec|sydney|' +
'swiss|swiftcover|swatch|suzuki|surgery|surf|support|supply|supplies|sucks|style|study|studio|' +
'stream|store|storage|stockholm|stcgroup|stc|statoil|statefarm|statebank|starhub|star|staples|' +
'stada|srt|srl|spreadbetting|spot|spiegel|space|soy|sony|song|solutions|solar|sohu|software|' +
'softbank|social|soccer|sncf|smile|smart|sling|skype|sky|skin|ski|site|singles|sina|silk|shriram|' +
'showtime|show|shouji|shopping|shop|shoes|shiksha|shia|shell|shaw|sharp|shangrila|sfr|sexy|sex|' +
'sew|seven|ses|services|sener|select|seek|security|secure|seat|search|scot|scor|scjohnson|' +
'science|schwarz|schule|school|scholarships|schmidt|schaeffler|scb|sca|sbs|sbi|saxo|save|sas|' +
'sarl|sapo|sap|sanofi|sandvikcoromant|sandvik|samsung|samsclub|salon|sale|sakura|safety|safe|' +
'saarland|ryukyu|rwe|run|ruhr|rugby|rsvp|room|rogers|rodeo|rocks|rocher|rmit|rip|rio|ril|' +
'rightathome|ricoh|richardli|rich|rexroth|reviews|review|restaurant|rest|republican|report|' +
'repair|rentals|rent|ren|reliance|reit|reisen|reise|rehab|redumbrella|redstone|red|recipes|' +
'realty|realtor|realestate|read|raid|radio|racing|qvc|quest|quebec|qpon|pwc|pub|prudential|pru|' +
'protection|property|properties|promo|progressive|prof|productions|prod|pro|prime|press|praxi|' +
'pramerica|post|porn|politie|poker|pohl|pnc|plus|plumbing|playstation|play|place|pizza|pioneer|' +
'pink|ping|pin|pid|pictures|pictet|pics|piaget|physio|photos|photography|photo|phone|philips|phd|' +
'pharmacy|pfizer|pet|pccw|pay|passagens|party|parts|partners|pars|paris|panerai|panasonic|' +
'pamperedchef|page|ovh|ott|otsuka|osaka|origins|orientexpress|organic|org|orange|oracle|open|ooo|' +
'onyourside|online|onl|ong|one|omega|ollo|oldnavy|olayangroup|olayan|okinawa|office|off|observer|' +
'obi|nyc|ntt|nrw|nra|nowtv|nowruz|now|norton|northwesternmutual|nokia|nissay|nissan|ninja|nikon|' +
'nike|nico|nhk|ngo|nfl|nexus|nextdirect|next|news|newholland|new|neustar|network|netflix|netbank|' +
'net|nec|nba|navy|natura|nationwide|name|nagoya|nadex|nab|mutuelle|mutual|museum|mtr|mtpc|mtn|' +
'msd|movistar|movie|mov|motorcycles|moto|moscow|mortgage|mormon|mopar|montblanc|monster|money|' +
'monash|mom|moi|moe|moda|mobily|mobile|mobi|mma|mls|mlb|mitsubishi|mit|mint|mini|mil|microsoft|' +
'miami|metlife|merckmsd|meo|menu|men|memorial|meme|melbourne|meet|media|med|mckinsey|mcdonalds|' +
'mcd|mba|mattel|maserati|marshalls|marriott|markets|marketing|market|map|mango|management|man|' +
'makeup|maison|maif|madrid|macys|luxury|luxe|lupin|lundbeck|ltda|ltd|lplfinancial|lpl|love|lotto|' +
'lotte|london|lol|loft|locus|locker|loans|loan|lixil|living|live|lipsy|link|linde|lincoln|limo|' +
'limited|lilly|like|lighting|lifestyle|lifeinsurance|life|lidl|liaison|lgbt|lexus|lego|legal|' +
'lefrak|leclerc|lease|lds|lawyer|law|latrobe|latino|lat|lasalle|lanxess|landrover|land|lancome|' +
'lancia|lancaster|lamer|lamborghini|ladbrokes|lacaixa|kyoto|kuokgroup|kred|krd|kpn|kpmg|kosher|' +
'komatsu|koeln|kiwi|kitchen|kindle|kinder|kim|kia|kfh|kerryproperties|kerrylogistics|kerryhotels|' +
'kddi|kaufen|juniper|juegos|jprs|jpmorgan|joy|jot|joburg|jobs|jnj|jmp|jll|jlc|jio|jewelry|jetzt|' +
'jeep|jcp|jcb|java|jaguar|iwc|iveco|itv|itau|istanbul|ist|ismaili|iselect|irish|ipiranga|' +
'investments|intuit|international|intel|int|insure|insurance|institute|ink|ing|info|infiniti|' +
'industries|immobilien|immo|imdb|imamat|ikano|iinet|ifm|ieee|icu|ice|icbc|ibm|hyundai|hyatt|' +
'hughes|htc|hsbc|how|house|hotmail|hotels|hoteles|hot|hosting|host|hospital|horse|honeywell|' +
'honda|homesense|homes|homegoods|homedepot|holiday|holdings|hockey|hkt|hiv|hitachi|hisamitsu|' +
'hiphop|hgtv|hermes|here|helsinki|help|healthcare|health|hdfcbank|hdfc|hbo|haus|hangout|hamburg|' +
'hair|guru|guitars|guide|guge|gucci|guardian|group|grocery|gripe|green|gratis|graphics|grainger|' +
'gov|got|gop|google|goog|goodyear|goodhands|goo|golf|goldpoint|gold|godaddy|gmx|gmo|gmbh|gmail|' +
'globo|global|gle|glass|glade|giving|gives|gifts|gift|ggee|george|genting|gent|gea|gdn|gbiz|' +
'garden|gap|games|game|gallup|gallo|gallery|gal|fyi|futbol|furniture|fund|fun|fujixerox|fujitsu|' +
'ftr|frontier|frontdoor|frogans|frl|fresenius|free|fox|foundation|forum|forsale|forex|ford|' +
'football|foodnetwork|food|foo|fly|flsmidth|flowers|florist|flir|flights|flickr|fitness|fit|' +
'fishing|fish|firmdale|firestone|fire|financial|finance|final|film|fido|fidelity|fiat|ferrero|' +
'ferrari|feedback|fedex|fast|fashion|farmers|farm|fans|fan|family|faith|fairwinds|fail|fage|' +
'extraspace|express|exposed|expert|exchange|everbank|events|eus|eurovision|etisalat|esurance|' +
'estate|esq|erni|ericsson|equipment|epson|epost|enterprises|engineering|engineer|energy|emerck|' +
'email|education|edu|edeka|eco|eat|earth|dvr|dvag|durban|dupont|duns|dunlop|duck|dubai|dtv|drive|' +
'download|dot|doosan|domains|doha|dog|dodge|doctor|docs|dnp|diy|dish|discover|discount|directory|' +
'direct|digital|diet|diamonds|dhl|dev|design|desi|dentist|dental|democrat|delta|deloitte|dell|' +
'delivery|degree|deals|dealer|deal|dds|dclk|day|datsun|dating|date|data|dance|dad|dabur|cyou|' +
'cymru|cuisinella|csc|cruises|cruise|crs|crown|cricket|creditunion|creditcard|credit|courses|' +
'coupons|coupon|country|corsica|coop|cool|cookingchannel|cooking|contractors|contact|consulting|' +
'construction|condos|comsec|computer|compare|company|community|commbank|comcast|com|cologne|' +
'college|coffee|codes|coach|clubmed|club|cloud|clothing|clinique|clinic|click|cleaning|claims|' +
'cityeats|city|citic|citi|citadel|cisco|circle|cipriani|church|chrysler|chrome|christmas|chloe|' +
'chintai|cheap|chat|chase|channel|chanel|cfd|cfa|cern|ceo|center|ceb|cbs|cbre|cbn|cba|catholic|' +
'catering|cat|casino|cash|caseih|case|casa|cartier|cars|careers|career|care|cards|caravan|car|' +
'capitalone|capital|capetown|canon|cancerresearch|camp|camera|cam|calvinklein|call|cal|cafe|cab|' +
'bzh|buzz|buy|business|builders|build|bugatti|budapest|brussels|brother|broker|broadway|' +
'bridgestone|bradesco|box|boutique|bot|boston|bostik|bosch|boots|booking|book|boo|bond|bom|bofa|' +
'boehringer|boats|bnpparibas|bnl|bmw|bms|blue|bloomberg|blog|blockbuster|blanco|blackfriday|' +
'black|biz|bio|bingo|bing|bike|bid|bible|bharti|bet|bestbuy|best|berlin|bentley|beer|beauty|' +
'beats|bcn|bcg|bbva|bbt|bbc|bayern|bauhaus|basketball|baseball|bargains|barefoot|barclays|' +
'barclaycard|barcelona|bar|bank|band|bananarepublic|banamex|baidu|baby|azure|axa|aws|avianca|' +
'autos|auto|author|auspost|audio|audible|audi|auction|attorney|athleta|associates|asia|asda|arte|' +
'art|arpa|army|archi|aramco|arab|aquarelle|apple|app|apartments|aol|anz|anquan|android|analytics|' +
'amsterdam|amica|amfam|amex|americanfamily|americanexpress|alstom|alsace|ally|allstate|allfinanz|' +
'alipay|alibaba|alfaromeo|akdn|airtel|airforce|airbus|aigo|aig|agency|agakhan|africa|afl|' +
'afamilycompany|aetna|aero|aeg|adult|ads|adac|actor|active|aco|accountants|accountant|accenture|' +
'academy|abudhabi|abogado|able|abc|abbvie|abbott|abb|abarth|aarp|aaa|onion' +
')(?=[^0-9a-zA-Z@]|$))'));
regexen.validCCTLD = regexSupplant(RegExp(
'(?:(?:' +
'한국|香港|澳門|新加坡|台灣|台湾|中國|中国|გე|ไทย|ලංකා|ഭാരതം|ಭಾರತ|భారత్|சிங்கப்பூர்|இலங்கை|இந்தியா|ଭାରତ|ભારત|ਭਾਰਤ|' +
'ভাৰত|ভারত|বাংলা|भारोत|भारतम्|भारत|ڀارت|پاکستان|مليسيا|مصر|قطر|فلسطين|عمان|عراق|سورية|سودان|تونس|' +
'بھارت|بارت|ایران|امارات|المغرب|السعودية|الجزائر|الاردن|հայ|қаз|укр|срб|рф|мон|мкд|ею|бел|бг|ελ|' +
'zw|zm|za|yt|ye|ws|wf|vu|vn|vi|vg|ve|vc|va|uz|uy|us|um|uk|ug|ua|tz|tw|tv|tt|tr|tp|to|tn|tm|tl|tk|' +
'tj|th|tg|tf|td|tc|sz|sy|sx|sv|su|st|ss|sr|so|sn|sm|sl|sk|sj|si|sh|sg|se|sd|sc|sb|sa|rw|ru|rs|ro|' +
're|qa|py|pw|pt|ps|pr|pn|pm|pl|pk|ph|pg|pf|pe|pa|om|nz|nu|nr|np|no|nl|ni|ng|nf|ne|nc|na|mz|my|mx|' +
'mw|mv|mu|mt|ms|mr|mq|mp|mo|mn|mm|ml|mk|mh|mg|mf|me|md|mc|ma|ly|lv|lu|lt|ls|lr|lk|li|lc|lb|la|kz|' +
'ky|kw|kr|kp|kn|km|ki|kh|kg|ke|jp|jo|jm|je|it|is|ir|iq|io|in|im|il|ie|id|hu|ht|hr|hn|hm|hk|gy|gw|' +
'gu|gt|gs|gr|gq|gp|gn|gm|gl|gi|gh|gg|gf|ge|gd|gb|ga|fr|fo|fm|fk|fj|fi|eu|et|es|er|eh|eg|ee|ec|dz|' +
'do|dm|dk|dj|de|cz|cy|cx|cw|cv|cu|cr|co|cn|cm|cl|ck|ci|ch|cg|cf|cd|cc|ca|bz|by|bw|bv|bt|bs|br|bq|' +
'bo|bn|bm|bl|bj|bi|bh|bg|bf|be|bd|bb|ba|az|ax|aw|au|at|as|ar|aq|ao|an|am|al|ai|ag|af|ae|ad|ac' +
')(?=[^0-9a-zA-Z@]|$))'));
regexen.validPunycode = /(?:xn--[0-9a-z]+)/;
regexen.validSpecialCCTLD = /(?:(?:co|tv)(?=[^0-9a-zA-Z@]|$))/;
regexen.validDomain = regexSupplant(/(?:#{validSubdomain}*#{validDomainName}(?:#{validGTLD}|#{validCCTLD}|#{validPunycode}))/);
regexen.validPortNumber = /[0-9]+/;
regexen.pd = /\u002d\u058a\u05be\u1400\u1806\u2010-\u2015\u2e17\u2e1a\u2e3a\u2e40\u301c\u3030\u30a0\ufe31\ufe58\ufe63\uff0d/;
regexen.validGeneralUrlPathChars = regexSupplant(/[^#{spaces_group}\(\)\?]/i);
// Allow URL paths to contain up to two nested levels of balanced parens
// 1. Used in Wikipedia URLs like /Primer_(film)
// 2. Used in IIS sessions like /S(dfd346)/
// 3. Used in Rdio URLs like /track/We_Up_(Album_Version_(Edited))/
regexen.validUrlBalancedParens = regexSupplant(
'\\(' +
'(?:' +
'#{validGeneralUrlPathChars}+' +
'|' +
// allow one nested level of balanced parentheses
'(?:' +
'#{validGeneralUrlPathChars}*' +
'\\(' +
'#{validGeneralUrlPathChars}+' +
'\\)' +
'#{validGeneralUrlPathChars}*' +
')' +
')' +
'\\)'
, 'i');
// Valid end-of-path chracters (so /foo. does not gobble the period).
// 1. Allow =&# for empty URL parameters and other URL-join artifacts
regexen.validUrlPathEndingChars = regexSupplant(/[^#{spaces_group}\(\)\?!\*';:=\,\.\$%\[\]#{pd}~&\|@]|(?:#{validUrlBalancedParens})/i);
// Allow @ in a url, but only in the middle. Catch things like http://example.com/@user/
regexen.validUrlPath = regexSupplant('(?:' +
'(?:' +
'#{validGeneralUrlPathChars}*' +
'(?:#{validUrlBalancedParens}#{validGeneralUrlPathChars}*)*' +
'#{validUrlPathEndingChars}'+
')|(?:@#{validGeneralUrlPathChars}+\/)'+
')', 'i');
regexen.validUrlQueryChars = /[a-z0-9!?\*'@\(\);:&=\+\$\/%#\[\]\-_\.,~|]/i;
regexen.validUrlQueryEndingChars = /[a-z0-9_&=#\/]/i;
regexen.validUrl = regexSupplant(
'(' + // $1 URL
'(https?:\\/\\/)' + // $2 Protocol
'(#{validDomain})' + // $3 Domain(s)
'(?::(#{validPortNumber}))?' + // $4 Port number (optional)
'(\\/#{validUrlPath}*)?' + // $5 URL Path
'(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $6 Query String
')'
, 'gi');
return regexen.validUrl;
}());

@ -2,6 +2,7 @@ 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';
import ModalContainer from '../../ui/containers/modal_container';
export default class Compose extends React.PureComponent {
@ -10,6 +11,7 @@ export default class Compose extends React.PureComponent {
<div>
<ComposeFormContainer />
<NotificationsContainer />
<ModalContainer />
<LoadingBarContainer className='loading-bar' />
</div>
);

@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import punycode from 'punycode';
import classnames from 'classnames';
@ -22,10 +23,15 @@ export default class Card extends React.PureComponent {
static propTypes = {
card: ImmutablePropTypes.map,
maxDescription: PropTypes.number,
};
static defaultProps = {
maxDescription: 50,
};
renderLink () {
const { card } = this.props;
const { card, maxDescription } = this.props;
let image = '';
let provider = card.get('provider_name');
@ -52,7 +58,7 @@ export default class Card extends React.PureComponent {
<div className='status-card__content'>
<strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>
<p className='status-card__description'>{(card.get('description') || '').substring(0, 50)}</p>
<p className='status-card__description'>{(card.get('description') || '').substring(0, maxDescription)}</p>
<span className='status-card__host'>{provider}</span>
</div>
</a>

@ -11,6 +11,7 @@ import Link from 'react-router-dom/Link';
import { FormattedDate, FormattedNumber } from 'react-intl';
import CardContainer from '../containers/card_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Video from '../../video';
import VisibilityIcon from '../../../../glitch/components/status/visibility_icon';
export default class DetailedStatus extends ImmutablePureComponent {
@ -36,6 +37,10 @@ export default class DetailedStatus extends ImmutablePureComponent {
e.stopPropagation();
}
handleOpenVideo = startTime => {
this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime);
}
render () {
const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status;
const { settings } = this.props;

@ -78,7 +78,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
handleChildrenContentChange() {
if (!this.props.singleColumn) {
scrollRight(this.node, this.node.scrollWidth - window.innerWidth);
this._interruptScrollAnimation = scrollRight(this.node, this.node.scrollWidth - window.innerWidth);
}
}

@ -1,35 +1,29 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import ExtendedVideoPlayer from '../../../components/extended_video_player';
import { defineMessages, injectIntl } from 'react-intl';
import IconButton from '../../../components/icon_button';
import Video from '../../video';
import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
@injectIntl
export default class VideoModal extends ImmutablePureComponent {
static propTypes = {
media: ImmutablePropTypes.map.isRequired,
time: PropTypes.number,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
render () {
const { media, intl, time, onClose } = this.props;
const url = media.get('url');
const { media, time, onClose } = this.props;
return (
<div className='modal-root__modal media-modal'>
<div>
<div className='media-modal__close'><IconButton title={intl.formatMessage(messages.close)} icon='times' overlay onClick={onClose} /></div>
<ExtendedVideoPlayer src={url} muted={false} controls time={time} />
<Video
preview={media.get('preview_url')}
src={media.get('url')}
startTime={time}
onCloseVideo={onClose}
/>
</div>
</div>
);

@ -11,7 +11,7 @@ import { debounce } from 'lodash';
import { uploadCompose } from '../../actions/compose';
import { refreshHomeTimeline } from '../../actions/timelines';
import { refreshNotifications } from '../../actions/notifications';
import { clearStatusesHeight } from '../../actions/statuses';
import { clearHeight } from '../../actions/height_cache';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
import UploadArea from './components/upload_area';
import ColumnsAreaContainer from './containers/columns_area_container';
@ -77,7 +77,7 @@ export default class UI extends React.PureComponent {
handleResize = debounce(() => {
// The cached heights are no longer accurate, invalidate
this.props.dispatch(clearStatusesHeight());
this.props.dispatch(clearHeight());
this.setState({ width: window.innerWidth });
}, 500, {

@ -109,6 +109,10 @@ export function VideoPlayer () {
return import(/* webpackChunkName: "status/video_player" */'../../../components/video_player');
}
export function Video () {
return import(/* webpackChunkName: "features/video" */'../../video');
}
export function EmbedModal () {
return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal');
}

@ -0,0 +1,304 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { throttle } from 'lodash';
import classNames from 'classnames';
const messages = defineMessages({
play: { id: 'video.play', defaultMessage: 'Play' },
pause: { id: 'video.pause', defaultMessage: 'Pause' },
mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
hide: { id: 'video.hide', defaultMessage: 'Hide video' },
expand: { id: 'video.expand', defaultMessage: 'Expand video' },
close: { id: 'video.close', defaultMessage: 'Close video' },
fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
});
const findElementPosition = el => {
let box;
if (el.getBoundingClientRect && el.parentNode) {
box = el.getBoundingClientRect();
}
if (!box) {
return {
left: 0,
top: 0,
};
}
const docEl = document.documentElement;
const body = document.body;
const clientLeft = docEl.clientLeft || body.clientLeft || 0;
const scrollLeft = window.pageXOffset || body.scrollLeft;
const left = (box.left + scrollLeft) - clientLeft;
const clientTop = docEl.clientTop || body.clientTop || 0;
const scrollTop = window.pageYOffset || body.scrollTop;
const top = (box.top + scrollTop) - clientTop;
return {
left: Math.round(left),
top: Math.round(top),
};
};
const getPointerPosition = (el, event) => {
const position = {};
const box = findElementPosition(el);
const boxW = el.offsetWidth;
const boxH = el.offsetHeight;
const boxY = box.top;
const boxX = box.left;
let pageY = event.pageY;
let pageX = event.pageX;
if (event.changedTouches) {
pageX = event.changedTouches[0].pageX;
pageY = event.changedTouches[0].pageY;
}
position.y = Math.max(0, Math.min(1, ((boxY - pageY) + boxH) / boxH));
position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW));
return position;
};
const isFullscreen = () => document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement;
const exitFullscreen = () => {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
};
const requestFullscreen = el => {
if (el.requestFullscreen) {
el.requestFullscreen();
} else if (el.webkitRequestFullscreen) {
el.webkitRequestFullscreen();
} else if (el.mozRequestFullScreen) {
el.mozRequestFullScreen();
} else if (el.msRequestFullscreen) {
el.msRequestFullscreen();
}
};
@injectIntl
export default class Video extends React.PureComponent {
static propTypes = {
preview: PropTypes.string,
src: PropTypes.string.isRequired,
width: PropTypes.number,
height: PropTypes.number,
sensitive: PropTypes.bool,
startTime: PropTypes.number,
onOpenVideo: PropTypes.func,
onCloseVideo: PropTypes.func,
intl: PropTypes.object.isRequired,
};
state = {
progress: 0,
paused: true,
dragging: false,
fullscreen: false,
hovered: false,
muted: false,
revealed: !this.props.sensitive,
};
setPlayerRef = c => {
this.player = c;
}
setVideoRef = c => {
this.video = c;
}
setSeekRef = c => {
this.seek = c;
}
handlePlay = () => {
this.setState({ paused: false });
}
handlePause = () => {
this.setState({ paused: true });
}
handleTimeUpdate = () => {
this.setState({ progress: 100 * (this.video.currentTime / this.video.duration) });
}
handleMouseDown = e => {
document.addEventListener('mousemove', this.handleMouseMove, true);
document.addEventListener('mouseup', this.handleMouseUp, true);
document.addEventListener('touchmove', this.handleMouseMove, true);
document.addEventListener('touchend', this.handleMouseUp, true);
this.setState({ dragging: true });
this.video.pause();
this.handleMouseMove(e);
}
handleMouseUp = () => {
document.removeEventListener('mousemove', this.handleMouseMove, true);
document.removeEventListener('mouseup', this.handleMouseUp, true);
document.removeEventListener('touchmove', this.handleMouseMove, true);
document.removeEventListener('touchend', this.handleMouseUp, true);
this.setState({ dragging: false });
this.video.play();
}
handleMouseMove = throttle(e => {
const { x } = getPointerPosition(this.seek, e);
this.video.currentTime = this.video.duration * x;
this.setState({ progress: x * 100 });
}, 60);
togglePlay = () => {
if (this.state.paused) {
this.video.play();
} else {
this.video.pause();
}
}
toggleFullscreen = () => {
if (isFullscreen()) {
exitFullscreen();
} else {
requestFullscreen(this.player);
}
}
componentDidMount () {
document.addEventListener('fullscreenchange', this.handleFullscreenChange, true);
document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
}
componentWillUnmount () {
document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true);
document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
}
handleFullscreenChange = () => {
this.setState({ fullscreen: isFullscreen() });
}
handleMouseEnter = () => {
this.setState({ hovered: true });
}
handleMouseLeave = () => {
this.setState({ hovered: false });
}
toggleMute = () => {
this.video.muted = !this.video.muted;
this.setState({ muted: this.video.muted });
}
toggleReveal = () => {
if (this.state.revealed) {
this.video.pause();
}
this.setState({ revealed: !this.state.revealed });
}
handleLoadedData = () => {
if (this.props.startTime) {
this.video.currentTime = this.props.startTime;
this.video.play();
}
}
handleOpenVideo = () => {
this.video.pause();
this.props.onOpenVideo(this.video.currentTime);
}
handleCloseVideo = () => {
this.video.pause();
this.props.onCloseVideo();
}
render () {
const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl } = this.props;
const { progress, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
return (
<div className={classNames('video-player', { inactive: !revealed, inline: width && height && !fullscreen, fullscreen })} style={{ width, height }} ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<video
ref={this.setVideoRef}
src={src}
poster={preview}
preload={!!startTime}
loop
role='button'
tabIndex='0'
width={width}
height={height}
onClick={this.togglePlay}
onPlay={this.handlePlay}
onPause={this.handlePause}
onTimeUpdate={this.handleTimeUpdate}
onLoadedData={this.handleLoadedData}
/>
<button className={classNames('video-player__spoiler', { active: !revealed })} onClick={this.toggleReveal}>
<span className='video-player__spoiler__title'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
<span className='video-player__spoiler__subtitle'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</button>
<div className={classNames('video-player__controls', { active: paused || hovered })}>
<div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
<div className='video-player__seek__progress' style={{ width: `${progress}%` }} />
<span
className={classNames('video-player__seek__handle', { active: dragging })}
tabIndex='0'
style={{ left: `${progress}%` }}
/>
</div>
<div className='video-player__buttons left'>
<button aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><i className={classNames('fa fa-fw', { 'fa-play': paused, 'fa-pause': !paused })} /></button>
<button aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><i className={classNames('fa fa-fw', { 'fa-volume-off': muted, 'fa-volume-up': !muted })} /></button>
{!onCloseVideo && <button aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>}
</div>
<div className='video-player__buttons right'>
{(!fullscreen && onOpenVideo) && <button aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>}
{onCloseVideo && <button aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-times' /></button>}
<button aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><i className={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen, 'fa-compress': fullscreen })} /></button>
</div>
</div>
</div>
);
}
}

@ -33,6 +33,7 @@
"column.home": "الرئيسية",
"column.mutes": "الحسابات المكتومة",
"column.notifications": "الإشعارات",
"column.pins": "Pinned toot",
"column.public": "الخيط العام الموحد",
"column_back_button.label": "العودة",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "معلومات إضافية",
"navigation_bar.logout": "خروج",
"navigation_bar.mutes": "الحسابات المكتومة",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "التفضيلات",
"navigation_bar.public_timeline": "الخيط العام الموحد",
"notification.favourite": "{name} أعجب بمنشورك",
@ -193,6 +195,15 @@
"upload_button.label": "إضافة وسائط",
"upload_form.undo": "إلغاء",
"upload_progress.label": "يرفع...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "وسّع الفيديو",
"video_player.toggle_sound": "تبديل الصوت",
"video_player.toggle_visible": "إظهار / إخفاء الفيديو",

@ -33,6 +33,7 @@
"column.home": "Начало",
"column.mutes": "Muted users",
"column.notifications": "Известия",
"column.pins": "Pinned toot",
"column.public": "Публичен канал",
"column_back_button.label": "Назад",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Extended information",
"navigation_bar.logout": "Излизане",
"navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Предпочитания",
"navigation_bar.public_timeline": "Публичен канал",
"notification.favourite": "{name} хареса твоята публикация",
@ -193,6 +195,15 @@
"upload_button.label": "Добави медия",
"upload_form.undo": "Отмяна",
"upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expand video",
"video_player.toggle_sound": "Звук",
"video_player.toggle_visible": "Toggle visibility",

@ -33,6 +33,7 @@
"column.home": "Inici",
"column.mutes": "Usuaris silenciats",
"column.notifications": "Notificacions",
"column.pins": "Pinned toot",
"column.public": "Línia de temps federada",
"column_back_button.label": "Enrere",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Informació addicional",
"navigation_bar.logout": "Tancar sessió",
"navigation_bar.mutes": "Usuaris silenciats",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferències",
"navigation_bar.public_timeline": "Línia de temps federada",
"notification.favourite": "{name} ha afavorit el teu estat",
@ -193,6 +195,15 @@
"upload_button.label": "Afegir multimèdia",
"upload_form.undo": "Desfer",
"upload_progress.label": "Pujant...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Ampliar el vídeo",
"video_player.toggle_sound": "Alternar so",
"video_player.toggle_visible": "Alternar visibilitat",

@ -33,6 +33,7 @@
"column.home": "Startseite",
"column.mutes": "Stummgeschaltete Profile",
"column.notifications": "Mitteilungen",
"column.pins": "Pinned toot",
"column.public": "Gesamtes bekanntes Netz",
"column_back_button.label": "Zurück",
"column_header.hide_settings": "Einstellungen verbergen",
@ -109,6 +110,7 @@
"navigation_bar.info": "Erweiterte Informationen",
"navigation_bar.logout": "Abmelden",
"navigation_bar.mutes": "Stummgeschaltete Profile",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Einstellungen",
"navigation_bar.public_timeline": "Föderierte Zeitleiste",
"notification.favourite": "{name} favorisierte deinen Status",
@ -193,6 +195,15 @@
"upload_button.label": "Mediendatei hinzufügen",
"upload_form.undo": "Entfernen",
"upload_progress.label": "Lade hoch…",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Videoanzeige vergrößern",
"video_player.toggle_sound": "Ton umschalten",
"video_player.toggle_visible": "Sichtbarkeit umschalten",

@ -812,6 +812,10 @@
"defaultMessage": "Extended information",
"id": "navigation_bar.info"
},
{
"defaultMessage": "Pinned toots",
"id": "navigation_bar.pins"
},
{
"defaultMessage": "FAQ",
"id": "getting_started.faq"
@ -992,6 +996,15 @@
],
"path": "app/javascript/mastodon/features/notifications/index.json"
},
{
"descriptors": [
{
"defaultMessage": "Pinned toot",
"id": "column.pins"
}
],
"path": "app/javascript/mastodon/features/pinned_statuses/index.json"
},
{
"descriptors": [
{
@ -1326,5 +1339,54 @@
}
],
"path": "app/javascript/mastodon/features/ui/components/video_modal.json"
},
{
"descriptors": [
{
"defaultMessage": "Play",
"id": "video.play"
},
{
"defaultMessage": "Pause",
"id": "video.pause"
},
{
"defaultMessage": "Mute sound",
"id": "video.mute"
},
{
"defaultMessage": "Unmute sound",
"id": "video.unmute"
},
{
"defaultMessage": "Hide video",
"id": "video.hide"
},
{
"defaultMessage": "Expand video",
"id": "video.expand"
},
{
"defaultMessage": "Close video",
"id": "video.close"
},
{
"defaultMessage": "Full screen",
"id": "video.fullscreen"
},
{
"defaultMessage": "Exit full screen",
"id": "video.exit_fullscreen"
},
{
"defaultMessage": "Sensitive content",
"id": "status.sensitive_warning"
},
{
"defaultMessage": "Click to view",
"id": "status.sensitive_toggle"
}
],
"path": "app/javascript/mastodon/features/video/index.json"
}
]

@ -33,8 +33,8 @@
"column.home": "Home",
"column.mutes": "Muted users",
"column.notifications": "Notifications",
"column.public": "Federated timeline",
"column.pins": "Pinned toots",
"column.public": "Federated timeline",
"column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
@ -110,9 +110,9 @@
"navigation_bar.info": "About this instance",
"navigation_bar.logout": "Logout",
"navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferences",
"navigation_bar.public_timeline": "Federated timeline",
"navigation_bar.pins": "Pinned toots",
"notification.favourite": "{name} favourited your status",
"notification.follow": "{name} followed you",
"notification.mention": "{name} mentioned you",
@ -195,6 +195,15 @@
"upload_button.label": "Add media",
"upload_form.undo": "Undo",
"upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expand video",
"video_player.toggle_sound": "Toggle sound",
"video_player.toggle_visible": "Toggle visibility",

@ -33,6 +33,7 @@
"column.home": "Hejmo",
"column.mutes": "Muted users",
"column.notifications": "Sciigoj",
"column.pins": "Pinned toot",
"column.public": "Fratara tempolinio",
"column_back_button.label": "Reveni",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Extended information",
"navigation_bar.logout": "Elsaluti",
"navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferoj",
"navigation_bar.public_timeline": "Fratara tempolinio",
"notification.favourite": "{name} favoris vian mesaĝon",
@ -193,6 +195,15 @@
"upload_button.label": "Aldoni enhavaĵon",
"upload_form.undo": "Malfari",
"upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expand video",
"video_player.toggle_sound": "Aktivigi sonojn",
"video_player.toggle_visible": "Toggle visibility",

@ -1,106 +1,107 @@
{
"account.block": "Bloquear",
"account.block_domain": "Hide everything from {domain}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.block_domain": "Ocultar todo de {domain}",
"account.disclaimer_full": "La siguiente información del usuario puede estar incompleta.",
"account.edit_profile": "Editar perfil",
"account.follow": "Seguir",
"account.followers": "Seguidores",
"account.follows": "Seguir",
"account.follows": "Sigue",
"account.follows_you": "Te sigue",
"account.media": "Media",
"account.mention": "Mencionar",
"account.mute": "Silenciar",
"account.mention": "Mencionar a @{name}",
"account.mute": "Silenciar a @{name}",
"account.posts": "Publicaciones",
"account.report": "Report @{name}",
"account.report": "Reportar a @{name}",
"account.requested": "Esperando aprobación",
"account.share": "Share @{name}'s profile",
"account.unblock": "Desbloquear",
"account.unblock_domain": "Unhide {domain}",
"account.share": "Compartir el perfil de @{name}",
"account.unblock": "Desbloquear a @{name}",
"account.unblock_domain": "Mostrar a {domain}",
"account.unfollow": "Dejar de seguir",
"account.unmute": "Unmute @{name}",
"account.view_full_profile": "View full profile",
"boost_modal.combo": "You can press {combo} to skip this next time",
"bundle_column_error.body": "Something went wrong while loading this component.",
"bundle_column_error.retry": "Try again",
"bundle_column_error.title": "Network error",
"bundle_modal_error.close": "Close",
"bundle_modal_error.message": "Something went wrong while loading this component.",
"bundle_modal_error.retry": "Try again",
"account.unmute": "Dejar de silenciar a @{name}",
"account.view_full_profile": "Ver perfil completo",
"boost_modal.combo": "Puedes presionar {combo} para saltear este aviso la próxima vez",
"bundle_column_error.body": "Algo salió mal al cargar este componente.",
"bundle_column_error.retry": "Inténtalo de nuevo",
"bundle_column_error.title": "Error de red",
"bundle_modal_error.close": "Cerrar",
"bundle_modal_error.message": "Algo salió mal al cargar este componente.",
"bundle_modal_error.retry": "Inténtalo de nuevo",
"column.blocks": "Usuarios bloqueados",
"column.community": "Historia local",
"column.community": "Línea de tiempo local",
"column.favourites": "Favoritos",
"column.follow_requests": "Solicitudes para seguirte",
"column.follow_requests": "Solicitudes de seguimiento",
"column.home": "Inicio",
"column.mutes": "Usuarios silenciados",
"column.notifications": "Notificaciones",
"column.pins": "Toot fijado",
"column.public": "Historia federada",
"column_back_button.label": "Atrás",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_subheading.navigation": "Navigation",
"column_subheading.settings": "Settings",
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
"compose_form.lock_disclaimer.lock": "locked",
"column_header.hide_settings": "Ocultar ajustes",
"column_header.moveLeft_settings": "Mover columna a la izquierda",
"column_header.moveRight_settings": "Mover columna a la derecha",
"column_header.pin": "Fijar",
"column_header.show_settings": "Mostrar ajustes",
"column_header.unpin": "Dejar de fijar",
"column_subheading.navigation": "Navegación",
"column_subheading.settings": "Ajustes",
"compose_form.lock_disclaimer": "Tu cuenta no está bloqueada. Todos pueden seguirte para ver tus toots solo para seguidores.",
"compose_form.lock_disclaimer.lock": "bloqueado",
"compose_form.placeholder": "¿En qué estás pensando?",
"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": "Tu toot privado será enviado a usuario/s mencionados de {domains}. ¿Confías en {domainsCount, plural, one {ese servidor} other {esos servidores}}? La privacidad del toot funcionará solamente en instancias de Mastodon. Si {domains} {domainsCount, plural, one {no es una instancia de Mastodon} other {no son instancias de Mastodon}}, no habrá indicación de que tu toot es privado, y puede hacerse visible a remitentes inesperados.",
"compose_form.publish": "Tootear",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marcar contenido como sensible",
"compose_form.spoiler": "Ocultar texto tras advertencia",
"compose_form.spoiler": "Ocultar texto tras una advertencia",
"compose_form.spoiler_placeholder": "Advertencia de contenido",
"confirmation_modal.cancel": "Cancel",
"confirmations.block.confirm": "Block",
"confirmations.block.message": "Are you sure you want to block {name}?",
"confirmations.delete.confirm": "Delete",
"confirmations.delete.message": "Are you sure you want to delete this status?",
"confirmations.domain_block.confirm": "Hide entire domain",
"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": "Mute",
"confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.unfollow.confirm": "Unfollow",
"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.flags": "Flags",
"emoji_button.food": "Food & Drink",
"confirmation_modal.cancel": "Cancelar",
"confirmations.block.confirm": "Bloquear",
"confirmations.block.message": "¿Estás seguro de que quieres bloquear a {name}?",
"confirmations.delete.confirm": "Eliminar",
"confirmations.delete.message": "¿Estás seguro de que quieres borrar este toot?",
"confirmations.domain_block.confirm": "Ocultar dominio entero",
"confirmations.domain_block.message": "¿Seguro de que quieres bloquear al dominio entero? En algunos casos es preferible bloquear o silenciar objetivos determinados.",
"confirmations.mute.confirm": "Silenciar",
"confirmations.mute.message": "¿Estás seguro de que quieres silenciar a {name}?",
"confirmations.unfollow.confirm": "Dejar de seguir",
"confirmations.unfollow.message": "¿Estás seguro de que quieres dejar de seguir a {name}?",
"embed.instructions": "Añade este toot a tu sitio web con el siguiente código.",
"embed.preview": "Así es como se verá:",
"emoji_button.activity": "Actividad",
"emoji_button.flags": "Marcas",
"emoji_button.food": "Comida y bebida",
"emoji_button.label": "Insertar emoji",
"emoji_button.nature": "Nature",
"emoji_button.objects": "Objects",
"emoji_button.people": "People",
"emoji_button.search": "Search...",
"emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.",
"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.public_timeline": "the public timeline",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up",
"follow_request.authorize": "Authorize",
"follow_request.reject": "Reject",
"getting_started.appsshort": "Apps",
"emoji_button.nature": "Naturaleza",
"emoji_button.objects": "Objetos",
"emoji_button.people": "Gente",
"emoji_button.search": "Buscar…",
"emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viajes y lugares",
"empty_column.community": "La línea de tiempo local está vacía. ¡Escribe algo para empezar la fiesta!",
"empty_column.hashtag": "No hay nada en este hashtag aún.",
"empty_column.home": "No estás siguiendo a nadie aún. Visita {public} o haz búsquedas para empezar y conocer gente nueva.",
"empty_column.home.inactivity": "Tus notificaciones están vacías. Si has estado inactivo por un tiempo, se regenerará para ti pronto.",
"empty_column.home.public_timeline": "la línea de tiempo pública",
"empty_column.notifications": "No tienes ninguna notificación aún. Interactúa con otros para empezar una conversación.",
"empty_column.public": "¡No hay nada aquí! Escribe algo públicamente, o sigue usuarios de otras instancias manualmente para llenarlo.",
"follow_request.authorize": "Autorizar",
"follow_request.reject": "Rechazar",
"getting_started.appsshort": "Aplicaciones",
"getting_started.faq": "FAQ",
"getting_started.heading": "Primeros pasos",
"getting_started.open_source_notice": "Mastodon es software libre. Puedes contribuir o reportar errores en {github}.",
"getting_started.userguide": "User Guide",
"home.column_settings.advanced": "Advanced",
"home.column_settings.basic": "Basic",
"home.column_settings.filter_regex": "Filter out by regular expressions",
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
"home.settings": "Column settings",
"getting_started.userguide": "Guía de usuario",
"home.column_settings.advanced": "Avanzado",
"home.column_settings.basic": "Básico",
"home.column_settings.filter_regex": "Filtrar con expresiones regulares",
"home.column_settings.show_reblogs": "Mostrar retoots",
"home.column_settings.show_replies": "Mostrar respuestas",
"home.settings": "Ajustes de columna",
"lightbox.close": "Cerrar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"loading_indicator.label": "Cargando...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
"lightbox.next": "Siguiente",
"lightbox.previous": "Anterior",
"loading_indicator.label": "Cargando",
"media_gallery.toggle_visible": "Cambiar visibilidad",
"missing_indicator.label": "No encontrado",
"navigation_bar.blocks": "Usuarios bloqueados",
"navigation_bar.community_timeline": "Historia local",
"navigation_bar.edit_profile": "Editar perfil",
@ -109,43 +110,44 @@
"navigation_bar.info": "Información adicional",
"navigation_bar.logout": "Cerrar sesión",
"navigation_bar.mutes": "Usuarios silenciados",
"navigation_bar.pins": "Toots fijados",
"navigation_bar.preferences": "Preferencias",
"navigation_bar.public_timeline": "Historia federada",
"notification.favourite": "{name} marcó tu estado como favorito",
"notification.follow": "{name} te empezó a seguir",
"notification.mention": "{name} te ha mencionado",
"notification.reblog": "{name} ha retooteado tu estado",
"notifications.clear": "Clear notifications",
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
"notifications.clear": "Limpiar notificaciones",
"notifications.clear_confirmation": "¿Seguro que quieres limpiar permanentemente todas tus notificaciones?",
"notifications.column_settings.alert": "Notificaciones de escritorio",
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.follow": "Nuevos seguidores:",
"notifications.column_settings.mention": "Menciones:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.push": "Notificaciones push:",
"notifications.column_settings.push_meta": "Este dispositivo:",
"notifications.column_settings.reblog": "Retoots:",
"notifications.column_settings.show": "Mostrar en columna",
"notifications.column_settings.sound": "Play sound",
"onboarding.done": "Done",
"onboarding.next": "Next",
"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_four.home": "The home timeline shows posts from people you follow.",
"onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
"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.handle": "You are on {domain}, so your full handle is {handle}",
"onboarding.page_one.welcome": "Welcome to Mastodon!",
"onboarding.page_six.admin": "Your instance's admin is {admin}.",
"onboarding.page_six.almost_done": "Almost done...",
"onboarding.page_six.appetoot": "Bon Appetoot!",
"onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
"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.guidelines": "community guidelines",
"onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
"onboarding.page_six.various_app": "mobile apps",
"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.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_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
"onboarding.skip": "Skip",
"notifications.column_settings.sound": "Reproducir sonido",
"onboarding.done": "Listo",
"onboarding.next": "Siguiente",
"onboarding.page_five.public_timelines": "La línea de tiempo local muestra toots públicos de todos en {domain}. La línea de tiempo federada muestra toots públicos de cualquiera a quien la gente de {domain} siga. Estas son las líneas de tiempo públicas, una buena forma de conocer gente nueva.",
"onboarding.page_four.home": "La línea de tiempo principal muestra toots de gente que sigues.",
"onboarding.page_four.notifications": "Las notificaciones se muestran cuando alguien interactúa contigo.",
"onboarding.page_one.federation": "Mastodon es una red de servidores federados que conforman una red social aún más grande. Llamamos a estos servidores instancias.",
"onboarding.page_one.handle": "Estás en {domain}, así que tu nombre de usuario completo es {handle}",
"onboarding.page_one.welcome": "¡Bienvenido a Mastodon!",
"onboarding.page_six.admin": "El administrador de tu instancia es {admin}.",
"onboarding.page_six.almost_done": "Ya casi…",
"onboarding.page_six.appetoot": "¡Bon Appetoot!",
"onboarding.page_six.apps_available": "Hay {apps} disponibles para iOS, Android y otras plataformas.",
"onboarding.page_six.github": "Mastodon es software libre. Puedes reportar errores, pedir funciones nuevas, o contribuir al código en {github}.",
"onboarding.page_six.guidelines": "guías de la comunidad",
"onboarding.page_six.read_guidelines": "¡Por favor lee las {guidelines} de {domain}!",
"onboarding.page_six.various_app": "aplicaciones móviles",
"onboarding.page_three.profile": "Edita tu perfil para cambiar tu avatar, biografía y nombre de cabecera. Ahí, también encontrarás otros ajustes.",
"onboarding.page_three.search": "Usa la barra de búsqueda y revisa hashtags, como {illustration} y {introductions}. Para ver a alguien que no es de tu propia instancia, usa su nombre de usuario completo.",
"onboarding.page_two.compose": "Escribe toots en la columna de redacción. Puedes subir imágenes, cambiar ajustes de privacidad, y añadir advertencias de contenido con los siguientes íconos.",
"onboarding.skip": "Saltar",
"privacy.change": "Ajustar privacidad",
"privacy.direct.long": "Sólo mostrar a los usuarios mencionados",
"privacy.direct.short": "Directo",
@ -156,45 +158,54 @@
"privacy.unlisted.long": "No mostrar en la historia federada",
"privacy.unlisted.short": "Sin federar",
"reply_indicator.cancel": "Cancelar",
"report.placeholder": "Additional comments",
"report.submit": "Submit",
"report.target": "Reporting",
"report.placeholder": "Comentarios adicionales",
"report.submit": "Publicar",
"report.target": "Reportando",
"search.placeholder": "Buscar",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted",
"search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
"standalone.public_title": "Un pequeño vistazo...",
"status.cannot_reblog": "Este toot no puede retootearse",
"status.delete": "Borrar",
"status.embed": "Embed",
"status.embed": "Incrustado",
"status.favourite": "Favorito",
"status.load_more": "Load more",
"status.media_hidden": "Media hidden",
"status.load_more": "Cargar más",
"status.media_hidden": "Contenido multimedia oculto",
"status.mention": "Mencionar",
"status.mute_conversation": "Mute conversation",
"status.mute_conversation": "Silenciar conversación",
"status.open": "Expandir estado",
"status.pin": "Pin on profile",
"status.reblog": "Retoot",
"status.pin": "Fijar",
"status.reblog": "Retootear",
"status.reblogged_by": "Retooteado por {name}",
"status.reply": "Responder",
"status.replyAll": "Reply to thread",
"status.replyAll": "Responder al hilo",
"status.report": "Reportar",
"status.sensitive_toggle": "Click para ver",
"status.sensitive_toggle": "Haz clic para ver",
"status.sensitive_warning": "Contenido sensible",
"status.share": "Share",
"status.share": "Compartir",
"status.show_less": "Mostrar menos",
"status.show_more": "Mostrar más",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"status.unmute_conversation": "Dejar de silenciar conversación",
"status.unpin": "Dejar de fijar",
"tabs_bar.compose": "Redactar",
"tabs_bar.federated_timeline": "Federated",
"tabs_bar.federated_timeline": "Federado",
"tabs_bar.home": "Inicio",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notificaciones",
"upload_area.title": "Drag & drop to upload",
"upload_area.title": "Arrastra y suelta para subir",
"upload_button.label": "Subir multimedia",
"upload_form.undo": "Deshacer",
"upload_progress.label": "Uploading...",
"video_player.expand": "Expand video",
"video_player.toggle_sound": "Act/Desac. sonido",
"video_player.toggle_visible": "Toggle visibility",
"video_player.video_error": "Video could not be played"
"upload_progress.label": "Subiendo…",
"video.close": "Cerrar video",
"video.exit_fullscreen": "Salir de pantalla completa",
"video.expand": "Expandir vídeo",
"video.fullscreen": "Pantalla completa",
"video.hide": "Ocultar vídeo",
"video.mute": "Silenciar sonido",
"video.pause": "Pausar",
"video.play": "Reproducir",
"video.unmute": "Dejar de silenciar sonido",
"video_player.expand": "Expandir vídeo",
"video_player.toggle_sound": "Activar/Desactivar sonido",
"video_player.toggle_visible": "Cambiar visibilidad",
"video_player.video_error": "No se pudo reproducir el vídeo"
}

@ -33,6 +33,7 @@
"column.home": "خانه",
"column.mutes": "کاربران بی‌صداشده",
"column.notifications": "اعلان‌ها",
"column.pins": "نوشته‌های ثابت",
"column.public": "نوشته‌های همه‌جا",
"column_back_button.label": "بازگشت",
"column_header.hide_settings": "نهفتن تنظیمات",
@ -109,6 +110,7 @@
"navigation_bar.info": "اطلاعات تکمیلی",
"navigation_bar.logout": "خروج",
"navigation_bar.mutes": "کاربران بی‌صداشده",
"navigation_bar.pins": "نوشته‌های ثابت",
"navigation_bar.preferences": "ترجیحات",
"navigation_bar.public_timeline": "نوشته‌های همه‌جا",
"notification.favourite": "{name} نوشتهٔ شما را پسندید",
@ -193,6 +195,15 @@
"upload_button.label": "افزودن تصویر",
"upload_form.undo": "واگردانی",
"upload_progress.label": "بارگذاری...",
"video.close": "بستن ویدیو",
"video.exit_fullscreen": "خروج از حالت تمام صفحه",
"video.expand": "بزرگ‌کردن ویدیو",
"video.fullscreen": "تمام صفحه",
"video.hide": "نهفتن ویدیو",
"video.mute": "قطع صدا",
"video.pause": "توقف",
"video.play": "پخش",
"video.unmute": "پخش صدا",
"video_player.expand": "بازکردن ویدیو",
"video_player.toggle_sound": "تغییر صداداری",
"video_player.toggle_visible": "تغییر پیدایی",

@ -33,6 +33,7 @@
"column.home": "Koti",
"column.mutes": "Muted users",
"column.notifications": "Ilmoitukset",
"column.pins": "Pinned toot",
"column.public": "Yleinen aikajana",
"column_back_button.label": "Takaisin",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Extended information",
"navigation_bar.logout": "Kirjaudu ulos",
"navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Ominaisuudet",
"navigation_bar.public_timeline": "Yleinen aikajana",
"notification.favourite": "{name} tykkäsi statuksestasi",
@ -193,6 +195,15 @@
"upload_button.label": "Lisää mediaa",
"upload_form.undo": "Peru",
"upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expand video",
"video_player.toggle_sound": "Äänet päälle/pois",
"video_player.toggle_visible": "Toggle visibility",

@ -33,8 +33,8 @@
"column.home": "Accueil",
"column.mutes": "Comptes masqués",
"column.notifications": "Notifications",
"column.public": "Fil public global",
"column.pins": "Pouets épinglés",
"column.public": "Fil public global",
"column_back_button.label": "Retour",
"column_header.hide_settings": "Masquer les paramètres",
"column_header.moveLeft_settings": "Déplacer la colonne vers la gauche",
@ -110,9 +110,9 @@
"navigation_bar.info": "Plus dinformations",
"navigation_bar.logout": "Déconnexion",
"navigation_bar.mutes": "Comptes masqués",
"navigation_bar.pins": "Pouets épinglés",
"navigation_bar.preferences": "Préférences",
"navigation_bar.public_timeline": "Fil public global",
"navigation_bar.pins": "Pouets épinglés",
"notification.favourite": "{name} a ajouté à ses favoris:",
"notification.follow": "{name} vous suit.",
"notification.mention": "{name} vous a mentionné⋅e:",
@ -166,7 +166,7 @@
"standalone.public_title": "Jeter un coup dœil…",
"status.cannot_reblog": "Cette publication ne peut être boostée",
"status.delete": "Effacer",
"status.embed": "Embed",
"status.embed": "Intégrer",
"status.favourite": "Ajouter aux favoris",
"status.load_more": "Charger plus",
"status.media_hidden": "Média caché",
@ -195,6 +195,15 @@
"upload_button.label": "Joindre un média",
"upload_form.undo": "Annuler",
"upload_progress.label": "Envoi en cours…",
"video.close": "Fermer la vidéo",
"video.exit_fullscreen": "Quitter plein écran",
"video.expand": "Agrandir la vidéo",
"video.fullscreen": "Plein écran",
"video.hide": "Masquer la vidéo",
"video.mute": "Couper le son",
"video.pause": "Pause",
"video.play": "Lecture",
"video.unmute": "Rétablir le son",
"video_player.expand": "Agrandir la vidéo",
"video_player.toggle_sound": "Activer/Désactiver le son",
"video_player.toggle_visible": "Afficher/Cacher la vidéo",

@ -33,6 +33,7 @@
"column.home": "בבית",
"column.mutes": "השתקות",
"column.notifications": "התראות",
"column.pins": "Pinned toot",
"column.public": "בפרהסיה",
"column_back_button.label": "חזרה",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "מידע נוסף",
"navigation_bar.logout": "יציאה",
"navigation_bar.mutes": "השתקות",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "העדפות",
"navigation_bar.public_timeline": "ציר זמן בין-קהילתי",
"notification.favourite": "חצרוצך חובב על ידי {name}",
@ -193,6 +195,15 @@
"upload_button.label": "הוספת מדיה",
"upload_form.undo": "ביטול",
"upload_progress.label": "עולה...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "הרחבת וידאו",
"video_player.toggle_sound": "הפעלת\\ביטול שמע",
"video_player.toggle_visible": "הפעלת\\ביטול תצוגה",

@ -33,6 +33,7 @@
"column.home": "Dom",
"column.mutes": "Utišani korisnici",
"column.notifications": "Notifikacije",
"column.pins": "Pinned toot",
"column.public": "Federalni timeline",
"column_back_button.label": "Natrag",
"column_header.hide_settings": "Hide settings",
@ -61,7 +62,6 @@
"confirmations.domain_block.message": "Jesi li zaista, zaista siguran da želiš potpuno blokirati {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Utišaj",
"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.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.",
@ -110,6 +110,7 @@
"navigation_bar.info": "Više informacija",
"navigation_bar.logout": "Odjavi se",
"navigation_bar.mutes": "Utišani korisnici",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Postavke",
"navigation_bar.public_timeline": "Federalni timeline",
"notification.favourite": "{name} je lajkao tvoj status",
@ -194,6 +195,15 @@
"upload_button.label": "Dodaj media",
"upload_form.undo": "Poništi",
"upload_progress.label": "Uploadam...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Proširi video",
"video_player.toggle_sound": "Toggle zvuk",
"video_player.toggle_visible": "Preklopi vidljivost",

@ -33,6 +33,7 @@
"column.home": "Kezdőlap",
"column.mutes": "Muted users",
"column.notifications": "Értesítések",
"column.pins": "Pinned toot",
"column.public": "Nyilvános",
"column_back_button.label": "Vissza",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Extended information",
"navigation_bar.logout": "Kijelentkezés",
"navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Beállítások",
"navigation_bar.public_timeline": "Nyilvános időfolyam",
"notification.favourite": "{name} kedvencnek jelölte az állapotod",
@ -193,6 +195,15 @@
"upload_button.label": "Média hozzáadása",
"upload_form.undo": "Mégsem",
"upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expand video",
"video_player.toggle_sound": "Hang kapcsolása",
"video_player.toggle_visible": "Toggle visibility",

@ -33,6 +33,7 @@
"column.home": "Beranda",
"column.mutes": "Pengguna dibisukan",
"column.notifications": "Notifikasi",
"column.pins": "Pinned toot",
"column.public": "Linimasa gabunggan",
"column_back_button.label": "Kembali",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Informasi selengkapnya",
"navigation_bar.logout": "Keluar",
"navigation_bar.mutes": "Pengguna dibisukan",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Pengaturan",
"navigation_bar.public_timeline": "Linimasa gabungan",
"notification.favourite": "{name} menyukai status anda",
@ -193,6 +195,15 @@
"upload_button.label": "Tambahkan media",
"upload_form.undo": "Undo",
"upload_progress.label": "Mengunggah...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Tampilkan video",
"video_player.toggle_sound": "Suara",
"video_player.toggle_visible": "Tampilan",

@ -33,6 +33,7 @@
"column.home": "Hemo",
"column.mutes": "Celita uzeri",
"column.notifications": "Savigi",
"column.pins": "Pinned toot",
"column.public": "Federata tempolineo",
"column_back_button.label": "Retro",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Detaloza informi",
"navigation_bar.logout": "Ekirar",
"navigation_bar.mutes": "Celita uzeri",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferi",
"navigation_bar.public_timeline": "Federata tempolineo",
"notification.favourite": "{name} favorizis tua mesajo",
@ -193,6 +195,15 @@
"upload_button.label": "Adjuntar kontenajo",
"upload_form.undo": "Desfacar",
"upload_progress.label": "Kargante...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Extensar video",
"video_player.toggle_sound": "Acendar sono",
"video_player.toggle_visible": "Chanjar videbleso",

@ -33,6 +33,7 @@
"column.home": "Home",
"column.mutes": "Utenti silenziati",
"column.notifications": "Notifiche",
"column.pins": "Pinned toot",
"column.public": "Timeline federata",
"column_back_button.label": "Indietro",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Informazioni estese",
"navigation_bar.logout": "Logout",
"navigation_bar.mutes": "Utenti silenziati",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Impostazioni",
"navigation_bar.public_timeline": "Timeline federata",
"notification.favourite": "{name} ha apprezzato il tuo post",
@ -193,6 +195,15 @@
"upload_button.label": "Aggiungi file multimediale",
"upload_form.undo": "Annulla",
"upload_progress.label": "Sto caricando...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Espandi video",
"video_player.toggle_sound": "Attiva suono",
"video_player.toggle_visible": "Attiva visibilità",

@ -33,8 +33,8 @@
"column.home": "ホーム",
"column.mutes": "ミュートしたユーザー",
"column.notifications": "通知",
"column.public": "連合タイムライン",
"column.pins": "固定されたトゥート",
"column.public": "連合タイムライン",
"column_back_button.label": "戻る",
"column_header.hide_settings": "設定を隠す",
"column_header.moveLeft_settings": "カラムを左に移動する",
@ -97,8 +97,8 @@
"home.column_settings.show_replies": "返信表示",
"home.settings": "カラム設定",
"lightbox.close": "閉じる",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"lightbox.next": "",
"lightbox.previous": "",
"loading_indicator.label": "読み込み中...",
"media_gallery.toggle_visible": "表示切り替え",
"missing_indicator.label": "見つかりません",
@ -110,9 +110,9 @@
"navigation_bar.info": "このインスタンスについて",
"navigation_bar.logout": "ログアウト",
"navigation_bar.mutes": "ミュートしたユーザー",
"navigation_bar.pins": "固定されたトゥート",
"navigation_bar.preferences": "ユーザー設定",
"navigation_bar.public_timeline": "連合タイムライン",
"navigation_bar.pins": "固定されたトゥート",
"notification.favourite": "{name}さんがあなたのトゥートをお気に入りに登録しました",
"notification.follow": "{name}さんにフォローされました",
"notification.mention": "{name}さんがあなたに返信しました",
@ -195,6 +195,15 @@
"upload_button.label": "メディアを追加",
"upload_form.undo": "やり直す",
"upload_progress.label": "アップロード中...",
"video.close": "動画を閉じる",
"video.exit_fullscreen": "全画面を終了する",
"video.expand": "動画を拡大する",
"video.fullscreen": "全画面",
"video.hide": "動画を閉じる",
"video.mute": "ミュート",
"video.pause": "一時停止",
"video.play": "再生",
"video.unmute": "ミュートを解除する",
"video_player.expand": "動画の詳細",
"video_player.toggle_sound": "音の切り替え",
"video_player.toggle_visible": "表示切り替え",

@ -33,8 +33,8 @@
"column.home": "홈",
"column.mutes": "뮤트 중인 사용자",
"column.notifications": "알림",
"column.public": "연합 타임라인",
"column.pins": "고정된 Toot",
"column.public": "연합 타임라인",
"column_back_button.label": "돌아가기",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
@ -110,9 +110,9 @@
"navigation_bar.info": "이 인스턴스에 대해서",
"navigation_bar.logout": "로그아웃",
"navigation_bar.mutes": "뮤트 중인 사용자",
"navigation_bar.pins": "고정된 Toot",
"navigation_bar.preferences": "사용자 설정",
"navigation_bar.public_timeline": "연합 타임라인",
"navigation_bar.pins": "고정된 Toot",
"notification.favourite": "{name}님이 즐겨찾기 했습니다",
"notification.follow": "{name}님이 나를 팔로우 했습니다",
"notification.mention": "{name}님이 답글을 보냈습니다",
@ -195,6 +195,15 @@
"upload_button.label": "미디어 추가",
"upload_form.undo": "재시도",
"upload_progress.label": "업로드 중...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "동영상 자세히 보기",
"video_player.toggle_sound": "소리 토글하기",
"video_player.toggle_visible": "표시 전환",

@ -12,7 +12,7 @@
"account.mute": "Negeer @{name}",
"account.posts": "Toots",
"account.report": "Rapporteer @{name}",
"account.requested": "Wacht op goedkeuring",
"account.requested": "Wacht op goedkeuring. Klik om volgverzoek te annuleren.",
"account.share": "Profiel van @{name} delen",
"account.unblock": "Deblokkeer @{name}",
"account.unblock_domain": "{domain} niet meer negeren",
@ -33,11 +33,13 @@
"column.home": "Start",
"column.mutes": "Genegeerde gebruikers",
"column.notifications": "Meldingen",
"column.pins": "Pinned toot",
"column.public": "Globale tijdlijn",
"column.pins": "Vastgezette toots",
"column_back_button.label": "terug",
"column_header.hide_settings": "Instellingen verbergen",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.moveLeft_settings": "Kolom naar links verplaatsen",
"column_header.moveRight_settings": "Kolom naar rechts verplaatsen",
"column_header.pin": "Vastmaken",
"column_header.show_settings": "Instellingen tonen",
"column_header.unpin": "Losmaken",
@ -63,8 +65,8 @@
"confirmations.mute.message": "Weet je het zeker dat je {name} wilt negeren?",
"confirmations.unfollow.confirm": "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:",
"embed.instructions": "Embed deze toot op jouw website, door de onderstaande code te kopiëren.",
"embed.preview": "Zo komt het eruit te zien:",
"emoji_button.activity": "Activiteiten",
"emoji_button.flags": "Vlaggen",
"emoji_button.food": "Eten en drinken",
@ -85,6 +87,7 @@
"follow_request.authorize": "Goedkeuren",
"follow_request.reject": "Afkeuren",
"getting_started.appsshort": "Apps",
"getting_started.donate": "Doneren",
"getting_started.faq": "FAQ",
"getting_started.heading": "Beginnen",
"getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}.",
@ -109,8 +112,10 @@
"navigation_bar.info": "Uitgebreide informatie",
"navigation_bar.logout": "Afmelden",
"navigation_bar.mutes": "Genegeerde gebruikers",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Instellingen",
"navigation_bar.public_timeline": "Globale tijdlijn",
"navigation_bar.pins": "Vastgezette toots",
"notification.favourite": "{name} markeerde jouw toot als favoriet",
"notification.follow": "{name} volgt jou nu",
"notification.mention": "{name} vermeldde jou",
@ -171,7 +176,7 @@
"status.mention": "Vermeld @{name}",
"status.mute_conversation": "Negeer conversatie",
"status.open": "Toot volledig tonen",
"status.pin": "Pin on profile",
"status.pin": "Aan profielpagina vastmaken",
"status.reblog": "Boost",
"status.reblogged_by": "{name} boostte",
"status.reply": "Reageren",
@ -183,7 +188,7 @@
"status.show_less": "Minder tonen",
"status.show_more": "Meer tonen",
"status.unmute_conversation": "Conversatie niet meer negeren",
"status.unpin": "Unpin from profile",
"status.unpin": "Van profielpagina losmaken",
"tabs_bar.compose": "Schrijven",
"tabs_bar.federated_timeline": "Globaal",
"tabs_bar.home": "Start",
@ -193,6 +198,15 @@
"upload_button.label": "Media toevoegen",
"upload_form.undo": "Ongedaan maken",
"upload_progress.label": "Uploaden...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Video groter maken",
"video.fullscreen": "Volledig scherm",
"video.hide": "Video verbergen",
"video.mute": "Geluid uitschakelen",
"video.pause": "Pauze",
"video.play": "Afspelen",
"video.unmute": "Geluid inschakelen",
"video_player.expand": "Video groter maken",
"video_player.toggle_sound": "Geluid in-/uitschakelen",
"video_player.toggle_visible": "Video wel/niet tonen",

@ -33,6 +33,7 @@
"column.home": "Hjem",
"column.mutes": "Dempede brukere",
"column.notifications": "Varsler",
"column.pins": "Pinned toot",
"column.public": "Felles tidslinje",
"column_back_button.label": "Tilbake",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Utvidet informasjon",
"navigation_bar.logout": "Logg ut",
"navigation_bar.mutes": "Dempede brukere",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferanser",
"navigation_bar.public_timeline": "Felles tidslinje",
"notification.favourite": "{name} likte din status",
@ -193,6 +195,15 @@
"upload_button.label": "Legg til media",
"upload_form.undo": "Angre",
"upload_progress.label": "Laster opp...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Utvid video",
"video_player.toggle_sound": "Veksle lyd",
"video_player.toggle_visible": "Veksle synlighet",

@ -33,8 +33,8 @@
"column.home": "Acuèlh",
"column.mutes": "Personas en silenci",
"column.notifications": "Notificacions",
"column.public": "Flux public global",
"column.pins": "Tuts penjats",
"column.public": "Flux public global",
"column_back_button.label": "Tornar",
"column_header.hide_settings": "Amagar los paramètres",
"column_header.moveLeft_settings": "Desplaçar la colomna a man drecha",
@ -64,7 +64,7 @@
"confirmations.mute.message": "Sètz segur de voler metre en silenci {name}?",
"confirmations.unfollow.confirm": "Quitar de sègre",
"confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name}?",
"embed.instructions": "Embarcar aqueste estatut per o far veire sus un site Internet en copiar lo còdi çai-jos.",
"embed.instructions": "Embarcar aqueste estatut per lo far veire sus un site Internet en copiar lo còdi çai-jos.",
"embed.preview": "Semblarà aquò:",
"emoji_button.activity": "Activitats",
"emoji_button.flags": "Drapèus",
@ -110,9 +110,9 @@
"navigation_bar.info": "Mai informacions",
"navigation_bar.logout": "Desconnexion",
"navigation_bar.mutes": "Personas rescondudas",
"navigation_bar.pins": "Tuts penjats",
"navigation_bar.preferences": "Preferéncias",
"navigation_bar.public_timeline": "Flux public global",
"navigation_bar.pins": "Tuts penjats",
"notification.favourite": "{name} a ajustat a sos favorits:",
"notification.follow": "{name} vos sèc",
"notification.mention": "{name} vos a mencionat:",
@ -195,6 +195,15 @@
"upload_button.label": "Ajustar un mèdia",
"upload_form.undo": "Anullar",
"upload_progress.label": "Mandadís…",
"video.close": "Tampar la vidèo",
"video.exit_fullscreen": "Sortir plen ecran",
"video.expand": "Agrandir la vidèo",
"video.fullscreen": "Ecran complet",
"video.hide": "Amagar la vidèo",
"video.mute": "Copar lo son",
"video.pause": "Pausa",
"video.play": "Lectura",
"video.unmute": "Restablir lo son",
"video_player.expand": "Mostrar la vidèo",
"video_player.toggle_sound": "Activar/Desactivar lo son",
"video_player.toggle_visible": "Mostrar/Rescondre la vidèo",

@ -195,7 +195,16 @@
"upload_button.label": "Dodaj zawartość multimedialną",
"upload_form.undo": "Cofnij",
"upload_progress.label": "Wysyłanie",
"video_player.expand": "Przełącz wideo",
"video.close": "Zamknij film",
"video.exit_fullscreen": "Opuść tryb pełnoekranowy",
"video.expand": "Rozszerz film",
"video.fullscreen": "Pełny ekran",
"video.hide": "Ukryj film",
"video.mute": "Wycisz",
"video.pause": "Pauzuj",
"video.play": "Odtwórz",
"video.unmute": "Cofnij wyciszenie",
"video_player.expand": "Rozszerz film",
"video_player.toggle_sound": "Przełącz dźwięk",
"video_player.toggle_visible": "Przełącz widoczność",
"video_player.video_error": "Nie można odtworzyć pliku wideo"

@ -6,25 +6,25 @@
"account.follow": "Seguir",
"account.followers": "Seguidores",
"account.follows": "Segue",
"account.follows_you": "É seu seguidor",
"account.follows_you": "Segue você",
"account.media": "Mídia",
"account.mention": "Mencionar @{name}",
"account.mute": "Silenciar @{name}",
"account.posts": "Posts",
"account.report": "Denunciar @{name}",
"account.requested": "Aguardando aprovação",
"account.requested": "Aguardando aprovação. Clique para cancelar a solicitação.",
"account.share": "Compartilhar perfil de @{name}",
"account.unblock": "Não bloquear @{name}",
"account.unblock": "Desbloquear @{name}",
"account.unblock_domain": "Desbloquear {domain}",
"account.unfollow": "Deixar de seguir",
"account.unmute": "Não silenciar @{name}",
"account.view_full_profile": "Ver perfil completo",
"boost_modal.combo": "Pode clicar {combo} para não voltar a ver",
"bundle_column_error.body": "Something went wrong while loading this component.",
"boost_modal.combo": "Você pode pressionar {combo} para ignorar este diálogo na próxima vez",
"bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.",
"bundle_column_error.retry": "Tente novamente",
"bundle_column_error.title": "Network error",
"bundle_column_error.title": "Erro de rede",
"bundle_modal_error.close": "Fechar",
"bundle_modal_error.message": "Something went wrong while loading this component.",
"bundle_modal_error.message": "Algo de errado aconteceu enquanto este componente era carregado.",
"bundle_modal_error.retry": "Tente novamente",
"column.blocks": "Usuários bloqueados",
"column.community": "Local",
@ -33,7 +33,9 @@
"column.home": "Página inicial",
"column.mutes": "Usuários silenciados",
"column.notifications": "Notificações",
"column.pins": "Postagens fixadas",
"column.public": "Global",
"column.pins": "Postagens fixadas",
"column_back_button.label": "Voltar",
"column_header.hide_settings": "Esconder configurações",
"column_header.moveLeft_settings": "Mover coluna para a esquerda",
@ -43,156 +45,169 @@
"column_header.unpin": "Desafixar",
"column_subheading.navigation": "Navegação",
"column_subheading.settings": "Configurações",
"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": "A sua conta não está {locked}. Qualquer pessoa pode te seguir e visualizar postagens direcionadas a apenas seguidores.",
"compose_form.lock_disclaimer.lock": "trancado",
"compose_form.placeholder": "No que você está pensando?",
"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.privacy_disclaimer": "O seu conteúdo privado será compartilhado com os usuários de {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 destinatários indesejados.",
"compose_form.publish": "Publicar",
"compose_form.publish_loud": "{publish}!",
"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 de conteúdo",
"compose_form.spoiler_placeholder": "Aviso de conteúdo",
"confirmation_modal.cancel": "Cancelar",
"confirmations.block.confirm": "Bloquear",
"confirmations.block.message": "Você tem certeza de que quer bloquear {name}?",
"confirmations.delete.confirm": "Excluir",
"confirmations.delete.message": "Você tem certeza de que quer excluir este status?",
"confirmations.delete.message": "Você tem certeza de que quer excluir esta postagem?",
"confirmations.domain_block.confirm": "Esconder o domínio inteiro",
"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": "Silenciar",
"confirmations.mute.message": "Você tem certeza de que quer silenciar {name}?",
"confirmations.unfollow.confirm": "Deixar de seguir",
"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.flags": "Flags",
"emoji_button.food": "Food & Drink",
"embed.instructions": "Incorpore esta postagem em seu site copiando o código abaixo:",
"embed.preview": "Aqui está uma previsão de como ficará:",
"emoji_button.activity": "Atividades",
"emoji_button.flags": "Bandeiras",
"emoji_button.food": "Comidas & Bebidas",
"emoji_button.label": "Inserir Emoji",
"emoji_button.nature": "Nature",
"emoji_button.objects": "Objects",
"emoji_button.people": "People",
"emoji_button.search": "Search...",
"emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places",
"empty_column.community": "Ainda não existem conteúdo local para mostrar!",
"empty_column.hashtag": "Ainda não existe qualquer conteúdo com essa hashtag",
"empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.",
"empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
"emoji_button.nature": "Natureza",
"emoji_button.objects": "Objetos",
"emoji_button.people": "Pessoas",
"emoji_button.search": "Buscar...",
"emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viagens & Lugares",
"empty_column.community": "A timeline local está vazia. Escreva algo publicamente para começar!",
"empty_column.hashtag": "Ainda não qualquer conteúdo com essa hashtag",
"empty_column.home": "Você ainda não segue usuário algo. Visite a timeline {public} ou use o buscador para procurar e conhecer outros usuários.",
"empty_column.home.inactivity": "A sua página inicial está vazia. Se você esteve inativo por um tempo, ela irá se regenerar em alguns intantes.",
"empty_column.home.public_timeline": "global",
"empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.",
"empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.",
"empty_column.notifications": "Você ainda não possui notificações. Interaja com outros usuários para começar a conversar!",
"empty_column.public": "Não há nada aqui! Escreva algo publicamente ou siga manualmente usuários de outras instâncias.",
"follow_request.authorize": "Autorizar",
"follow_request.reject": "Rejeitar",
"getting_started.appsshort": "Apps",
"getting_started.faq": "FAQ",
"getting_started.heading": "Primeiros passos",
"getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}.",
"getting_started.userguide": "User Guide",
"getting_started.open_source_notice": "Mastodon é um software de código aberto. Você pode contribuir ou reportar problemas na página do GitHub do projeto: {github}.",
"getting_started.userguide": "Guia de usuário",
"home.column_settings.advanced": "Avançado",
"home.column_settings.basic": "Básico",
"home.column_settings.filter_regex": "Filtrar com uma expressão regular",
"home.column_settings.show_reblogs": "Mostrar as partilhas",
"home.column_settings.show_reblogs": "Mostrar compartilhamentos",
"home.column_settings.show_replies": "Mostrar as respostas",
"home.settings": "Parâmetros da listagem",
"home.settings": "Configurações de colunas",
"lightbox.close": "Fechar",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"lightbox.next": "Próximo",
"lightbox.previous": "Anterior",
"loading_indicator.label": "Carregando...",
"media_gallery.toggle_visible": "Esconder/Mostrar",
"missing_indicator.label": "Não encontrado",
"navigation_bar.blocks": "Utilizadores bloqueados",
"navigation_bar.blocks": "Usuários bloqueados",
"navigation_bar.community_timeline": "Local",
"navigation_bar.edit_profile": "Editar perfil",
"navigation_bar.favourites": "Favoritos",
"navigation_bar.follow_requests": "Seguidores pendentes",
"navigation_bar.info": "Mais informações",
"navigation_bar.logout": "Sair",
"navigation_bar.mutes": "Utilizadores silenciados",
"navigation_bar.mutes": "Usuários silenciados",
"navigation_bar.pins": "Postagens fixadas",
"navigation_bar.preferences": "Preferências",
"navigation_bar.public_timeline": "Global",
"notification.favourite": "{name} adicionou o teu post aos favoritos",
"notification.follow": "{name} seguiu-te",
"notification.mention": "{name} mencionou-te",
"notification.reblog": "{name} partilhou o teu post",
"navigation_bar.preferences": "Preferências",
"navigation_bar.public_timeline": "Global",
"navigation_bar.pins": "Postagens fixadas",
"notification.favourite": "{name} adicionou a sua postagem aos favoritos",
"notification.follow": "{name} te seguiu",
"notification.mention": "{name} te mencionou",
"notification.reblog": "{name} compartilhou a sua postagem",
"notifications.clear": "Limpar notificações",
"notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?",
"notifications.clear_confirmation": "Você tem certeza de que quer limpar todas as suas notificações permanentemente?",
"notifications.column_settings.alert": "Notificações no computador",
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.follow": "Novos seguidores:",
"notifications.column_settings.mention": "Menções:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "Partilhas:",
"notifications.column_settings.push": "Enviar notificações",
"notifications.column_settings.push_meta": "Este aparelho",
"notifications.column_settings.reblog": "Compartilhamento:",
"notifications.column_settings.show": "Mostrar nas colunas",
"notifications.column_settings.sound": "Reproduzir som",
"onboarding.done": "Done",
"onboarding.next": "Next",
"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_four.home": "The home timeline shows posts from people you follow.",
"onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
"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.handle": "You are on {domain}, so your full handle is {handle}",
"onboarding.page_one.welcome": "Welcome to Mastodon!",
"onboarding.page_six.admin": "Your instance's admin is {admin}.",
"onboarding.page_six.almost_done": "Almost done...",
"onboarding.done": "Pronto",
"onboarding.next": "Próximo",
"onboarding.page_five.public_timelines": "A timeline local mostra postagens públicas de todos os usuários no {domain}. A timeline federada mostra todas as postagens de todas as pessoas que pessoas no {domain} seguem. Estas são as timelines públicas, uma ótima maneira de conhecer novas pessoas.",
"onboarding.page_four.home": "A página inicial mostra postagens de pessoas que você segue.",
"onboarding.page_four.notifications": "A coluna de notificações te mostra quando alguém interage com você.",
"onboarding.page_one.federation": "Mastodon é uma rede d servidores independentes se juntando para fazer uma grande rede social. Nós chamamos estes servidores de instâncias.",
"onboarding.page_one.handle": "Você está no {domain}, então o seu nome de usuário completo é {handle}",
"onboarding.page_one.welcome": "Seja bem-vindo(a) ao Mastodon!",
"onboarding.page_six.admin": "O administrador de sua instância é {admin}.",
"onboarding.page_six.almost_done": "Quase acabando...",
"onboarding.page_six.appetoot": "Bon Appetoot!",
"onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
"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.guidelines": "community guidelines",
"onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
"onboarding.page_six.various_app": "mobile apps",
"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.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_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
"onboarding.skip": "Skip",
"onboarding.page_six.apps_available": "Há {apps} disponíveis para iOS, Android e outras plataformas.",
"onboarding.page_six.github": "Mastodon é um software gratuito e de código aberto. Você pode reportar bugs, prequisitar novas funções ou contribuir para o código no {github}.",
"onboarding.page_six.guidelines": "diretrizes da comunidade",
"onboarding.page_six.read_guidelines": "Por favor, leia as {guidelines} do {domain}!",
"onboarding.page_six.various_app": "aplicativos móveis",
"onboarding.page_three.profile": "Edite o seu perfil para mudar o seu o seu avatar, bio e nome de exibição. No menu de configurações, você também encontrará outras preferências.",
"onboarding.page_three.search": "Use a barra de buscas para encontrar pessoas e consultar hashtahs, como #illustrations e #introductions. Para procurar por uma pessoa que não estiver nesta instância, use o nome de usuário completo dela.",
"onboarding.page_two.compose": "Escreva postagens na coluna de escrita. Você pode hospedar imagens, mudar as configurações de privacidade e adicionar alertas de conteúdo através dos ícones abaixo.",
"onboarding.skip": "Pular",
"privacy.change": "Ajustar a privacidade da mensagem",
"privacy.direct.long": "Apenas para utilizadores mencionados",
"privacy.direct.short": "Directo",
"privacy.private.long": "Apenas para os seguidores",
"privacy.private.short": "Privado",
"privacy.direct.long": "Apenas para usuários mencionados",
"privacy.direct.short": "Direta",
"privacy.private.long": "Apenas para seus seguidores",
"privacy.private.short": "Privada",
"privacy.public.long": "Publicar em todos os feeds",
"privacy.public.short": "Público",
"privacy.unlisted.long": "Não publicar nos feeds públicos",
"privacy.unlisted.short": "Não listar",
"privacy.public.short": "Pública",
"privacy.unlisted.long": "Não publicar em feeds públicos",
"privacy.unlisted.short": "Não listada",
"reply_indicator.cancel": "Cancelar",
"report.placeholder": "Comentários adicionais",
"report.submit": "Enviar",
"report.target": "Denunciar",
"search.placeholder": "Pesquisar",
"search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
"standalone.public_title": "A look inside...",
"status.cannot_reblog": "This post cannot be boosted",
"standalone.public_title": "Dê uma espiada...",
"status.cannot_reblog": "Esta postagem não pode ser compartilhada",
"status.delete": "Eliminar",
"status.embed": "Embed",
"status.embed": "Incorporar",
"status.favourite": "Adicionar aos favoritos",
"status.load_more": "Carregar mais",
"status.media_hidden": "Media escondida",
"status.media_hidden": "Mídia escondida",
"status.mention": "Mencionar @{name}",
"status.mute_conversation": "Mute conversation",
"status.mute_conversation": "Silenciar conversa",
"status.open": "Expandir",
"status.pin": "Pin on profile",
"status.reblog": "Partilhar",
"status.reblogged_by": "{name} partilhou",
"status.pin": "Fixar no perfil",
"status.reblog": "Compartilhar",
"status.reblogged_by": "{name} compartilhou",
"status.reply": "Responder",
"status.replyAll": "Reply to thread",
"status.report": "Denúnciar @{name}",
"status.replyAll": "Responder à sequência",
"status.report": "Denunciar @{name}",
"status.sensitive_toggle": "Clique para ver",
"status.sensitive_warning": "Conteúdo sensível",
"status.share": "Share",
"status.share": "Compartilhar",
"status.show_less": "Mostrar menos",
"status.show_more": "Mostrar mais",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"status.unmute_conversation": "Desativar silêncio desta conversa",
"status.unpin": "Desafixar do perfil",
"tabs_bar.compose": "Criar",
"tabs_bar.federated_timeline": "Global",
"tabs_bar.home": "Home",
"tabs_bar.home": "Página inicial",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notificações",
"upload_area.title": "Arraste e solte para enviar",
"upload_button.label": "Adicionar media",
"upload_button.label": "Adicionar mídia",
"upload_form.undo": "Anular",
"upload_progress.label": "A gravar...",
"upload_progress.label": "Salvando...",
"video.close": "Fechar vídeo",
"video.exit_fullscreen": "Sair da tela cheia",
"video.expand": "Expandir vídeo",
"video.fullscreen": "Tela cheia",
"video.hide": "Esconder vídeo",
"video.mute": "Silenciar vídeo",
"video.pause": "Parar",
"video.play": "Reproduzir",
"video.unmute": "Retirar silêncio",
"video_player.expand": "Expandir vídeo",
"video_player.toggle_sound": "Ligar/Desligar som",
"video_player.toggle_visible": "Ligar/Desligar vídeo",

@ -33,6 +33,7 @@
"column.home": "Home",
"column.mutes": "Utilizadores silenciados",
"column.notifications": "Notificações",
"column.pins": "Pinned toot",
"column.public": "Global",
"column_back_button.label": "Voltar",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Mais informações",
"navigation_bar.logout": "Sair",
"navigation_bar.mutes": "Utilizadores silenciados",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferências",
"navigation_bar.public_timeline": "Global",
"notification.favourite": "{name} adicionou o teu post aos favoritos",
@ -193,6 +195,15 @@
"upload_button.label": "Adicionar media",
"upload_form.undo": "Anular",
"upload_progress.label": "A gravar...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expandir vídeo",
"video_player.toggle_sound": "Ligar/Desligar som",
"video_player.toggle_visible": "Ligar/Desligar vídeo",

@ -33,6 +33,7 @@
"column.home": "Главная",
"column.mutes": "Список глушения",
"column.notifications": "Уведомления",
"column.pins": "Pinned toot",
"column.public": "Глобальная лента",
"column_back_button.label": "Назад",
"column_header.hide_settings": "Скрыть настройки",
@ -109,6 +110,7 @@
"navigation_bar.info": "Об узле",
"navigation_bar.logout": "Выйти",
"navigation_bar.mutes": "Список глушения",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Опции",
"navigation_bar.public_timeline": "Глобальная лента",
"notification.favourite": "{name} понравился Ваш статус",
@ -193,6 +195,15 @@
"upload_button.label": "Добавить медиаконтент",
"upload_form.undo": "Отменить",
"upload_progress.label": "Загрузка...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Развернуть видео",
"video_player.toggle_sound": "Вкл./выкл. звук",
"video_player.toggle_visible": "Показать/скрыть",

@ -33,6 +33,7 @@
"column.home": "Home",
"column.mutes": "Muted users",
"column.notifications": "Notifications",
"column.pins": "Pinned toot",
"column.public": "Federated timeline",
"column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "About this instance",
"navigation_bar.logout": "Logout",
"navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferences",
"navigation_bar.public_timeline": "Federated timeline",
"notification.favourite": "{name} favourited your status",
@ -193,6 +195,15 @@
"upload_button.label": "Add media",
"upload_form.undo": "Undo",
"upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expand video",
"video_player.toggle_sound": "Toggle sound",
"video_player.toggle_visible": "Toggle visibility",

@ -33,6 +33,7 @@
"column.home": "Anasayfa",
"column.mutes": "Susturulmuş kullanıcılar",
"column.notifications": "Bildirimler",
"column.pins": "Pinned toot",
"column.public": "Federe zaman tüneli",
"column_back_button.label": "Geri",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Genişletilmiş bilgi",
"navigation_bar.logout": ıkış",
"navigation_bar.mutes": "Sessize alınmış kullanıcılar",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Tercihler",
"navigation_bar.public_timeline": "Federe zaman tüneli",
"notification.favourite": "{name} senin durumunu favorilere ekledi",
@ -193,6 +195,15 @@
"upload_button.label": "Görsel ekle",
"upload_form.undo": "Geri al",
"upload_progress.label": "Yükleniyor...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Videoyu genişlet",
"video_player.toggle_sound": "Sesi aç/kapa",
"video_player.toggle_visible": "Göster/gizle",

@ -33,6 +33,7 @@
"column.home": "Головна",
"column.mutes": "Заглушені користувачі",
"column.notifications": "Сповіщення",
"column.pins": "Pinned toot",
"column.public": "Глобальна стрічка",
"column_back_button.label": "Назад",
"column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Про інстанцію",
"navigation_bar.logout": "Вийти",
"navigation_bar.mutes": "Заглушені користувачі",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Налаштування",
"navigation_bar.public_timeline": "Глобальна стрічка",
"notification.favourite": "{name} сподобався ваш допис",
@ -193,6 +195,15 @@
"upload_button.label": "Додати медіаконтент",
"upload_form.undo": "Відмінити",
"upload_progress.label": "Завантаження...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Розгорнути ",
"video_player.toggle_sound": "Увімкнути/вимкнути звук",
"video_player.toggle_visible": "Показати/приховати",

@ -1,13 +1,13 @@
{
"account.block": "屏蔽 @{name}",
"account.block_domain": "Hide everything from {domain}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.block_domain": "隐藏一切来自 {domain} 的嘟文",
"account.disclaimer_full": "下列资料不一定完整。",
"account.edit_profile": "修改个人资料",
"account.follow": "关注",
"account.followers": "关注者",
"account.follows": "正关注",
"account.follows": "正关注",
"account.follows_you": "关注你",
"account.media": "Media",
"account.media": "媒体",
"account.mention": "提及 @{name}",
"account.mute": "将 @{name} 静音",
"account.posts": "嘟文",
@ -15,40 +15,41 @@
"account.requested": "等待审批",
"account.share": "分享 @{name}的个人资料",
"account.unblock": "解除对 @{name} 的屏蔽",
"account.unblock_domain": "解除封锁 {domain}",
"account.unblock_domain": "不再隐藏 {domain}",
"account.unfollow": "取消关注",
"account.unmute": "取消 @{name} 的静音",
"account.view_full_profile": "查看完整资料",
"boost_modal.combo": "如你想在下次路过时显示,请按{combo}",
"bundle_column_error.body": "载入组件出错。",
"bundle_column_error.retry": "再次尝试",
"bundle_column_error.retry": "试",
"bundle_column_error.title": "网络错误",
"bundle_modal_error.close": "关闭",
"bundle_modal_error.message": "载入组件出错。",
"bundle_modal_error.retry": "再次尝试",
"bundle_modal_error.retry": "试",
"column.blocks": "屏蔽用户",
"column.community": "本站时间轴",
"column.favourites": "过的嘟文",
"column.favourites": "收藏过的嘟文",
"column.follow_requests": "关注请求",
"column.home": "主页",
"column.mutes": "被静音的用户",
"column.notifications": "通知",
"column.pins": "Pinned toot",
"column.public": "跨站公共时间轴",
"column_back_button.label": "返回",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_header.hide_settings": "隐藏设置",
"column_header.moveLeft_settings": "将栏左移",
"column_header.moveRight_settings": "将栏右移",
"column_header.pin": "置顶",
"column_header.show_settings": "显示设置",
"column_header.unpin": "撤顶",
"column_subheading.navigation": "导航",
"column_subheading.settings": "设置",
"compose_form.lock_disclaimer": "你的户没 {locked}. 任何人可以通过关注你来查看只有关注者可见的嘟文.",
"compose_form.lock_disclaimer": "你的户没 {locked}. 任何人可以通过关注你来查看只有关注者可见的嘟文.",
"compose_form.lock_disclaimer.lock": "被保护",
"compose_form.placeholder": "在想啥?",
"compose_form.privacy_disclaimer": "你的私人嘟文,将被发送至你所提及的 {domains} 用户。你是否信任{domainsCount, plural, one {这个网站} other {这些网站}}?请留意,嘟文隐私设置只适用于各 Mastodon 服务器实例,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服务器实例} other {之中有些不是 Mastodon 服务器实例}},对方将无法收到这篇嘟文的隐私设置,然后可能被转嘟给不能预知的用户阅读。",
"compose_form.publish": "嘟嘟",
"compose_form.publish_loud": "{publish}!",
"compose_form.publish_loud": "{publish}",
"compose_form.sensitive": "将媒体文件标示为“敏感内容”",
"compose_form.spoiler": "将部分文本藏于警告消息之后",
"compose_form.spoiler_placeholder": "敏感内容的警告消息",
@ -57,14 +58,14 @@
"confirmations.block.message": "想好了,真的要屏蔽 {name}?",
"confirmations.delete.confirm": "删除",
"confirmations.delete.message": "想好了,真的要删除这条嘟文?",
"confirmations.domain_block.confirm": "Hide entire domain",
"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.confirm": "隐藏整个网站",
"confirmations.domain_block.message": "你真的真的确定要隐藏整个 {domain} ?多数情况下,封锁或静音几个特定目标就好。",
"confirmations.mute.confirm": "静音",
"confirmations.mute.message": "想好了,真的要静音 {name}?",
"confirmations.unfollow.confirm": "取消关注",
"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:",
"embed.instructions": "要内嵌此嘟文,请将以下代码贴进你的网站。",
"embed.preview": "到时大概长这样:",
"emoji_button.activity": "活动",
"emoji_button.flags": "旗帜",
"emoji_button.food": "食物和饮料",
@ -72,13 +73,13 @@
"emoji_button.nature": "自然",
"emoji_button.objects": "物体",
"emoji_button.people": "人物",
"emoji_button.search": "搜索...",
"emoji_button.search": "搜索",
"emoji_button.symbols": "符号",
"emoji_button.travel": "旅途和地点",
"empty_column.community": "本站时间轴暂时未有内容,快贴文来抢头香啊!",
"empty_column.community": "本站时间轴暂时未有内容,快嘟几个来抢头香啊!",
"empty_column.hashtag": "这个标签暂时未有内容。",
"empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。",
"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": "你的主页暂时没有内容。也许你太久没有来了?如果是这样,文章会慢慢出来,请稍后再看。",
"empty_column.home.public_timeline": "公共时间轴",
"empty_column.notifications": "你没有任何通知纪录,快向其他用户搭讪吧。",
"empty_column.public": "跨站公共时间轴暂时没有内容!快写一些公共的嘟文,或者关注另一些服务器实例的用户吧!你和本站、友站的交流,将决定这里出现的内容。",
@ -96,33 +97,34 @@
"home.column_settings.show_replies": "显示回应嘟文",
"home.settings": "字段设置",
"lightbox.close": "关闭",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"lightbox.next": "下一步",
"lightbox.previous": "上一步",
"loading_indicator.label": "加载中……",
"media_gallery.toggle_visible": "打开或关上",
"missing_indicator.label": "找不到内容",
"navigation_bar.blocks": "被屏蔽的用户",
"navigation_bar.community_timeline": "本站时间轴",
"navigation_bar.edit_profile": "修改个人资料",
"navigation_bar.favourites": "的内容",
"navigation_bar.favourites": "收藏的内容",
"navigation_bar.follow_requests": "关注请求",
"navigation_bar.info": "关于本站",
"navigation_bar.logout": "注销",
"navigation_bar.mutes": "被静音的用户",
"navigation_bar.pins": "置顶嘟文",
"navigation_bar.preferences": "首选项",
"navigation_bar.public_timeline": "跨站公共时间轴",
"notification.favourite": "{name} 了你的嘟文",
"notification.favourite": "{name} 收藏了你的嘟文",
"notification.follow": "{name} 开始关注你",
"notification.mention": "{name} 提及你",
"notification.reblog": "{name} 转嘟了你的嘟文",
"notifications.clear": "清空通知纪录",
"notifications.clear_confirmation": "你确定要清空通知纪录吗?",
"notifications.column_settings.alert": "显示桌面通知",
"notifications.column_settings.favourite": "你的嘟文被",
"notifications.column_settings.favourite": "你的嘟文被收藏",
"notifications.column_settings.follow": "关注你:",
"notifications.column_settings.mention": "提及你:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.push": "推送通知",
"notifications.column_settings.push_meta": "此设备",
"notifications.column_settings.reblog": "你的嘟文被转嘟:",
"notifications.column_settings.show": "在通知栏显示",
"notifications.column_settings.sound": "播放音效",
@ -132,18 +134,18 @@
"onboarding.page_four.home": "你的主时间轴上是你关注的用户的嘟文.",
"onboarding.page_four.notifications": "如果你和他人产生了互动,便会出现在通知列上啦~",
"onboarding.page_one.federation": "Mastodon是由一系列独立的服务器共同打造的强大的社交网络我们将这些独立但又相互连接的服务器叫做服务器实例。",
"onboarding.page_one.handle": "你在 {domain}, {handle} 就是你的完整户名称。",
"onboarding.page_one.handle": "你在 {domain}, {handle} 就是你的完整户名称。",
"onboarding.page_one.welcome": "欢迎来到 Mastodon!",
"onboarding.page_six.admin": "{admin} 是你所在服务器实例的管理员.",
"onboarding.page_six.almost_done": "快完成了...",
"onboarding.page_six.almost_done": "差不多了…",
"onboarding.page_six.appetoot": "嗷呜~",
"onboarding.page_six.apps_available": "也有适用于 iOS, Android 和其它平台的 {apps} 咯~",
"onboarding.page_six.github": "Mastodon 是自由的开放源代码软件。欢迎来 {github} 报告问题,提交功能请求,或者贡献代码 :-)",
"onboarding.page_six.guidelines": "社区指南",
"onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的 {guidelines}!",
"onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的 {guidelines}",
"onboarding.page_six.various_app": "移动应用程序",
"onboarding.page_three.profile": "修改你的个人资料,比如头像、简介、和昵称等等。在那还可以找到其它首选项。",
"onboarding.page_three.search": "用搜索来找人和标签吧,比如 {illustration} 或者 {introductions}。想找其它服务器实例上的人,用完整户名称(用户名@域名)啦。",
"onboarding.page_three.search": "用搜索来找人和标签吧,比如 {illustration} 或者 {introductions}。想找其它服务器实例上的人,用完整户名称(用户名@域名)啦。",
"onboarding.page_two.compose": "从这里开始嘟!上面的按钮提供了上传图片,修改隐私设置和提示敏感内容等多种功能。.",
"onboarding.skip": "好啦好啦我知道啦",
"privacy.change": "调整隐私设置",
@ -161,29 +163,29 @@
"report.target": "Reporting",
"search.placeholder": "搜索",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"standalone.public_title": "A look inside...",
"standalone.public_title": "大家都在干啥?",
"status.cannot_reblog": "没法转嘟这条嘟文啦……",
"status.delete": "删除",
"status.embed": "Embed",
"status.favourite": "",
"status.embed": "嵌入",
"status.favourite": "收藏",
"status.load_more": "加载更多",
"status.media_hidden": "隐藏媒体内容",
"status.mention": "提及 @{name}",
"status.mute_conversation": "Mute conversation",
"status.mute_conversation": "静音对话",
"status.open": "展开嘟文",
"status.pin": "Pin on profile",
"status.pin": "置顶到资料",
"status.reblog": "转嘟",
"status.reblogged_by": "{name} 转嘟",
"status.reply": "回应",
"status.replyAll": "Reply to thread",
"status.replyAll": "回应整串",
"status.report": "举报 @{name}",
"status.sensitive_toggle": "点击显示",
"status.sensitive_warning": "敏感内容",
"status.share": "Share",
"status.show_less": "减少显示",
"status.show_more": "显示更多",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"status.unmute_conversation": "解禁对话",
"status.unpin": "解除置顶",
"tabs_bar.compose": "撰写",
"tabs_bar.federated_timeline": "跨站",
"tabs_bar.home": "主页",
@ -193,6 +195,15 @@
"upload_button.label": "上传媒体文件",
"upload_form.undo": "还原",
"upload_progress.label": "上传中……",
"video.close": "关闭影片",
"video.exit_fullscreen": "退出全荧幕",
"video.expand": "展开影片",
"video.fullscreen": "全荧幕",
"video.hide": "隐藏影片",
"video.mute": "静音",
"video.pause": "暂停",
"video.play": "播放",
"video.unmute": "解除静音",
"video_player.expand": "展开影片",
"video_player.toggle_sound": "开关音效",
"video_player.toggle_visible": "打开或关上",

@ -1,46 +1,47 @@
{
"account.block": "封鎖 @{name}",
"account.block_domain": "Hide everything from {domain}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.block_domain": "隱藏來自 {domain} 的一切文章",
"account.disclaimer_full": "下列資料不一定完整。",
"account.edit_profile": "修改個人資料",
"account.follow": "關注",
"account.followers": "關注的人",
"account.follows": "正關注",
"account.follows": "正關注",
"account.follows_you": "關注你",
"account.media": "Media",
"account.media": "媒體",
"account.mention": "提及 @{name}",
"account.mute": "將 @{name} 靜音",
"account.posts": "文章",
"account.report": "舉報 @{name}",
"account.requested": "等候審批",
"account.share": "Share @{name}'s profile",
"account.share": "分享 @{name} 的個人資料",
"account.unblock": "解除對 @{name} 的封鎖",
"account.unblock_domain": "Unhide {domain}",
"account.unblock_domain": "不再隱藏 {domain}",
"account.unfollow": "取消關注",
"account.unmute": "取消 @{name} 的靜音",
"account.view_full_profile": "View full profile",
"account.view_full_profile": "查看完整資料",
"boost_modal.combo": "如你想在下次路過這顯示,請按{combo}",
"bundle_column_error.body": "Something went wrong while loading this component.",
"bundle_column_error.retry": "Try again",
"bundle_column_error.title": "Network error",
"bundle_modal_error.close": "Close",
"bundle_modal_error.message": "Something went wrong while loading this component.",
"bundle_modal_error.retry": "Try again",
"bundle_column_error.body": "加載本組件出錯。",
"bundle_column_error.retry": "重試",
"bundle_column_error.title": "網絡錯誤",
"bundle_modal_error.close": "關閉",
"bundle_modal_error.message": "加載本組件出錯。",
"bundle_modal_error.retry": "重試",
"column.blocks": "封鎖用戶",
"column.community": "本站時間軸",
"column.favourites": "喜歡的文章",
"column.favourites": "最愛的文章",
"column.follow_requests": "關注請求",
"column.home": "主頁",
"column.mutes": "靜音名單",
"column.notifications": "通知",
"column.pins": "Pinned toot",
"column.public": "跨站時間軸",
"column_back_button.label": "返回",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_header.hide_settings": "隱藏設定",
"column_header.moveLeft_settings": "將欄左移",
"column_header.moveRight_settings": "將欄右移",
"column_header.pin": "置頂",
"column_header.show_settings": "顯示設定",
"column_header.unpin": "撤頂",
"column_subheading.navigation": "瀏覽",
"column_subheading.settings": "設定",
"compose_form.lock_disclaimer": "你的用戶狀態為「{locked}」,任何人都能立即關注你,然後看到「只有關注者能看」的文章。",
@ -48,7 +49,7 @@
"compose_form.placeholder": "你在想甚麼?",
"compose_form.privacy_disclaimer": "你的私人文章,將被遞送至 {domains}。你是否信任{domainsCount, plural, one {這個網站} other {這些網站}}?請留意,文章私隱設定只適用於 Mastodon 服務站,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服務站} other {之中有些不是 Mastodon 服務站}},對方將可無視文章的私隱設定,轉推文章給其他用戶閱讀。",
"compose_form.publish": "發文",
"compose_form.publish_loud": "{publish}!",
"compose_form.publish_loud": "{publish}",
"compose_form.sensitive": "將媒體檔案標示為「敏感內容」",
"compose_form.spoiler": "將部份文字藏於警告訊息之後",
"compose_form.spoiler_placeholder": "敏感警告訊息",
@ -57,14 +58,14 @@
"confirmations.block.message": "你確定要封鎖{name}嗎?",
"confirmations.delete.confirm": "刪除",
"confirmations.delete.message": "你確定要刪除{name}嗎?",
"confirmations.domain_block.confirm": "Hide entire domain",
"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.confirm": "隱藏整個網站",
"confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或靜音幾個特定目標就好。",
"confirmations.mute.confirm": "靜音",
"confirmations.mute.message": "你確定要將{name}靜音嗎?",
"confirmations.unfollow.confirm": "Unfollow",
"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:",
"confirmations.unfollow.confirm": "取消關注",
"confirmations.unfollow.message": "真的不要繼續關注 {name} 了嗎?",
"embed.instructions": "要內嵌此文章,請將以下代碼貼進你的網站。",
"embed.preview": "看上去會是這樣:",
"emoji_button.activity": "活動",
"emoji_button.flags": "旗幟",
"emoji_button.food": "飲飲食食",
@ -75,7 +76,7 @@
"emoji_button.search": "搜尋…",
"emoji_button.symbols": "符號",
"emoji_button.travel": "旅遊景物",
"empty_column.community": "本站時間軸暫時未有內容,快文來搶頭香啊!",
"empty_column.community": "本站時間軸暫時未有內容,快來搶頭香啊!",
"empty_column.hashtag": "這個標籤暫時未有內容。",
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
"empty_column.home.inactivity": "你的主頁暫時沒有內容。也許你太久沒有來?如果是這樣,文章會慢慢出來,請稍後再看。",
@ -96,34 +97,35 @@
"home.column_settings.show_replies": "顯示回應文章",
"home.settings": "欄位設定",
"lightbox.close": "關閉",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"lightbox.next": "繼續",
"lightbox.previous": "回退",
"loading_indicator.label": "載入中...",
"media_gallery.toggle_visible": "打開或關上",
"missing_indicator.label": "找不到內容",
"navigation_bar.blocks": "被你封鎖的用戶",
"navigation_bar.community_timeline": "本站時間軸",
"navigation_bar.edit_profile": "修改個人資料",
"navigation_bar.favourites": "喜歡的內容",
"navigation_bar.favourites": "最愛的內容",
"navigation_bar.follow_requests": "關注請求",
"navigation_bar.info": "關於本服務站",
"navigation_bar.logout": "登出",
"navigation_bar.mutes": "被你靜音的用戶",
"navigation_bar.pins": "置頂文章",
"navigation_bar.preferences": "偏好設定",
"navigation_bar.public_timeline": "跨站時間軸",
"notification.favourite": "{name} 喜歡你的文章",
"notification.favourite": "{name} 收藏了你的文章",
"notification.follow": "{name} 開始關注你",
"notification.mention": "{name} 提及你",
"notification.reblog": "{name} 轉推你的文章",
"notifications.clear": "清空通知紀錄",
"notifications.clear_confirmation": "你確定要清空通知紀錄嗎?",
"notifications.column_settings.alert": "顯示桌面通知",
"notifications.column_settings.favourite": "喜歡你的文章:",
"notifications.column_settings.follow": "關注你:",
"notifications.column_settings.mention": "提及你:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.reblog": "轉推你的文章:",
"notifications.column_settings.favourite": "收藏了你的文章:",
"notifications.column_settings.follow": "關注你",
"notifications.column_settings.mention": "提及你",
"notifications.column_settings.push": "推送通知",
"notifications.column_settings.push_meta": "這臺設備",
"notifications.column_settings.reblog": "轉推你的文章",
"notifications.column_settings.show": "在通知欄顯示",
"notifications.column_settings.sound": "播放音效",
"onboarding.done": "開始使用",
@ -161,17 +163,17 @@
"report.target": "舉報",
"search.placeholder": "搜尋",
"search_results.total": "{count, number} 項結果",
"standalone.public_title": "A look inside...",
"standalone.public_title": "站點一瞥…",
"status.cannot_reblog": "這篇文章無法被轉推",
"status.delete": "刪除",
"status.embed": "Embed",
"status.favourite": "喜歡",
"status.embed": "鑲嵌",
"status.favourite": "收藏",
"status.load_more": "載入更多",
"status.media_hidden": "隱藏媒體內容",
"status.mention": "提及 @{name}",
"status.mute_conversation": "Mute conversation",
"status.mute_conversation": "靜音對話",
"status.open": "展開文章",
"status.pin": "Pin on profile",
"status.pin": "置頂到資料頁",
"status.reblog": "轉推",
"status.reblogged_by": "{name} 轉推",
"status.reply": "回應",
@ -182,8 +184,8 @@
"status.share": "Share",
"status.show_less": "減少顯示",
"status.show_more": "顯示更多",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"status.unmute_conversation": "解禁對話",
"status.unpin": "解除置頂",
"tabs_bar.compose": "撰寫",
"tabs_bar.federated_timeline": "跨站",
"tabs_bar.home": "主頁",
@ -193,6 +195,15 @@
"upload_button.label": "上載媒體檔案",
"upload_form.undo": "還原",
"upload_progress.label": "上載中……",
"video.close": "關閉影片",
"video.exit_fullscreen": "退出全熒幕",
"video.expand": "展開影片",
"video.fullscreen": "全熒幕",
"video.hide": "隱藏影片",
"video.mute": "靜音",
"video.pause": "暫停",
"video.play": "播放",
"video.unmute": "解除靜音",
"video_player.expand": "展開影片",
"video_player.toggle_sound": "開關音效",
"video_player.toggle_visible": "打開或關上",

@ -1,11 +1,11 @@
{
"account.block": "封鎖 @{name}",
"account.block_domain": "隱藏來自 {domain} 的一切",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.edit_profile": "編輯用資訊",
"account.block_domain": "隱藏來自 {domain} 的一切貼文",
"account.disclaimer_full": "下列資料不一定完整。",
"account.edit_profile": "編輯用資訊",
"account.follow": "關注",
"account.followers": "專注者",
"account.follows": "正關注",
"account.follows": "正關注",
"account.follows_you": "關注你",
"account.media": "媒體",
"account.mention": "提到 @{name}",
@ -13,19 +13,19 @@
"account.posts": "貼文",
"account.report": "檢舉 @{name}",
"account.requested": "正在等待許可",
"account.share": "Share @{name}'s profile",
"account.share": "分享 @{name} 的用者資訊",
"account.unblock": "取消封鎖 @{name}",
"account.unblock_domain": "不再隱藏 {domain}",
"account.unfollow": "取消關注",
"account.unmute": "不再消音 @{name}",
"account.view_full_profile": "View full profile",
"account.view_full_profile": "查看完整資訊",
"boost_modal.combo": "下次你可以按 {combo} 來跳過",
"bundle_column_error.body": "Something went wrong while loading this component.",
"bundle_column_error.retry": "Try again",
"bundle_column_error.title": "Network error",
"bundle_modal_error.close": "Close",
"bundle_modal_error.message": "Something went wrong while loading this component.",
"bundle_modal_error.retry": "Try again",
"bundle_column_error.body": "加載本組件出錯。",
"bundle_column_error.retry": "重試",
"bundle_column_error.title": "網路錯誤",
"bundle_modal_error.close": "關閉",
"bundle_modal_error.message": "加載本組件出錯。",
"bundle_modal_error.retry": "重試",
"column.blocks": "封鎖的使用者",
"column.community": "本地時間軸",
"column.favourites": "最愛",
@ -33,21 +33,22 @@
"column.home": "家",
"column.mutes": "消音的使用者",
"column.notifications": "通知",
"column.pins": "置頂貼文",
"column.public": "聯盟時間軸",
"column_back_button.label": "上一頁",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
"column_header.pin": "Pin",
"column_header.show_settings": "Show settings",
"column_header.unpin": "Unpin",
"column_header.hide_settings": "隱藏設定",
"column_header.moveLeft_settings": "將欄左移",
"column_header.moveRight_settings": "將欄右移",
"column_header.pin": "置頂",
"column_header.show_settings": "顯示設定",
"column_header.unpin": "撤頂",
"column_subheading.navigation": "瀏覽",
"column_subheading.settings": "設定",
"compose_form.lock_disclaimer": "你的帳號沒有{locked}。任何人都可以關注你,看到發給關注者的貼文。",
"compose_form.lock_disclaimer.lock": "上鎖",
"compose_form.placeholder": "在想些什麼?",
"compose_form.privacy_disclaimer": "你的貼文會被傳到 {domains} 上被提到的使用者。你信任 {domainsCount, plural, one {這個伺服器} other {這些伺服器}}嗎?貼文的隱私設定只會在 Mastodon 副本上生效。如果 {domains} {domainsCount, plural, one {不是一個 Mastodon 副本} other {都不是 Mastodon 副本}},就不會被標記為非公開貼文,而且可能會被轉推或是讓不預期的人看見。",
"compose_form.publish": "",
"compose_form.publish": "貼掉",
"compose_form.publish_loud": "{publish}",
"compose_form.sensitive": "將此媒體標為敏感",
"compose_form.spoiler": "將訊息隱藏在警告訊息之後",
@ -58,13 +59,13 @@
"confirmations.delete.confirm": "刪除",
"confirmations.delete.message": "你確定要刪除這個狀態?",
"confirmations.domain_block.confirm": "隱藏整個網域",
"confirmations.domain_block.message": "你真的真的確定要封鎖整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。",
"confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。",
"confirmations.mute.confirm": "消音",
"confirmations.mute.message": "你確定要消音 {name} ",
"confirmations.unfollow.confirm": "Unfollow",
"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:",
"confirmations.unfollow.confirm": "取消關注",
"confirmations.unfollow.message": "真的不要繼續關注 {name} 了嗎?",
"embed.instructions": "要內嵌此貼文,請將以下代碼貼進你的網站。",
"embed.preview": "看上去會變成這樣:",
"emoji_button.activity": "活動",
"emoji_button.flags": "旗幟",
"emoji_button.food": "食物與飲料",
@ -72,12 +73,12 @@
"emoji_button.nature": "自然",
"emoji_button.objects": "物件",
"emoji_button.people": "人",
"emoji_button.search": "搜尋...",
"emoji_button.search": "搜尋",
"emoji_button.symbols": "符號",
"emoji_button.travel": "旅遊與地點",
"empty_column.community": "本地時間軸是空的。公開寫點什麼吧!",
"empty_column.hashtag": "這個主題標籤下什麼都沒有。",
"empty_column.home": "你還沒關注任何人。造訪{public}或利用搜尋功能找到其他用。",
"empty_column.home": "你還沒關注任何人。造訪{public}或利用搜尋功能找到其他用。",
"empty_column.home.inactivity": "你家的訊息摘要是空的。如果你很久沒活動了,很快它就會重新產生。",
"empty_column.home.public_timeline": "公開時間軸",
"empty_column.notifications": "還沒有任何通知。和別的使用者互動來開始對話。",
@ -96,22 +97,23 @@
"home.column_settings.show_replies": "顯示回應",
"home.settings": "欄位設定",
"lightbox.close": "關閉",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
"lightbox.next": "繼續",
"lightbox.previous": "回退",
"loading_indicator.label": "讀取中...",
"media_gallery.toggle_visible": "切換可見性",
"missing_indicator.label": "找不到",
"navigation_bar.blocks": "封鎖的使用者",
"navigation_bar.community_timeline": "本地時間軸",
"navigation_bar.edit_profile": "編輯用資訊",
"navigation_bar.edit_profile": "編輯用資訊",
"navigation_bar.favourites": "最愛",
"navigation_bar.follow_requests": "關注請求",
"navigation_bar.info": "關於本站",
"navigation_bar.logout": "登出",
"navigation_bar.mutes": "消音的使用者",
"navigation_bar.pins": "置頂貼文",
"navigation_bar.preferences": "偏好設定",
"navigation_bar.public_timeline": "聯盟時間軸",
"notification.favourite": "{name}喜歡你的狀態",
"notification.favourite": "{name}收藏了你的狀態",
"notification.follow": "{name}關注了你",
"notification.mention": "{name}提到了你",
"notification.reblog": "{name}推了你的狀態",
@ -121,8 +123,8 @@
"notifications.column_settings.favourite": "最愛:",
"notifications.column_settings.follow": "新的關注者:",
"notifications.column_settings.mention": "提到:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push_meta": "This device",
"notifications.column_settings.push": "推送通知",
"notifications.column_settings.push_meta": "這臺設備",
"notifications.column_settings.reblog": "轉推:",
"notifications.column_settings.show": "顯示在欄位中",
"notifications.column_settings.sound": "播放音效",
@ -135,8 +137,8 @@
"onboarding.page_one.handle": "你在 {domain} 上,所以你的帳號全名是 {handle}",
"onboarding.page_one.welcome": "歡迎來到 Mastodon ",
"onboarding.page_six.admin": "你的副本的管理員是 {admin} 。",
"onboarding.page_six.almost_done": "快好了...",
"onboarding.page_six.appetoot": "口大開!",
"onboarding.page_six.almost_done": "快好了",
"onboarding.page_six.appetoot": "口大開!",
"onboarding.page_six.apps_available": "在 iOS 、 Android 和其他平台上有這些 {apps} 可以用。",
"onboarding.page_six.github": "Mastodon 是自由的開源軟體。你可以在 {github} 上回報臭蟲、請求新功能或是做出貢獻。",
"onboarding.page_six.guidelines": "社群指南",
@ -161,17 +163,17 @@
"report.target": "通報中",
"search.placeholder": "搜尋",
"search_results.total": "{count, number} 項結果",
"standalone.public_title": "A look inside...",
"standalone.public_title": "站點一瞥…",
"status.cannot_reblog": "此貼文無法轉推",
"status.delete": "刪除",
"status.embed": "Embed",
"status.favourite": "喜愛",
"status.favourite": "收藏",
"status.load_more": "載入更多",
"status.media_hidden": "媒體已隱藏",
"status.mention": "提到 @{name}",
"status.mute_conversation": "消音對話",
"status.open": "展開這個狀態",
"status.pin": "Pin on profile",
"status.pin": "置頂到個人資訊頁",
"status.reblog": "轉推",
"status.reblogged_by": "{name} 轉推了",
"status.reply": "回應",
@ -183,7 +185,7 @@
"status.show_less": "看少點",
"status.show_more": "看更多",
"status.unmute_conversation": "不消音對話",
"status.unpin": "Unpin from profile",
"status.unpin": "解除置頂",
"tabs_bar.compose": "編輯",
"tabs_bar.federated_timeline": "聯盟",
"tabs_bar.home": "家",
@ -193,6 +195,15 @@
"upload_button.label": "增加媒體",
"upload_form.undo": "復原",
"upload_progress.label": "上傳中...",
"video.close": "關閉影片",
"video.exit_fullscreen": "退出全熒幕",
"video.expand": "展開影片",
"video.fullscreen": "全熒幕",
"video.hide": "隱藏影片",
"video.mute": "消音",
"video.pause": "暫停",
"video.play": "播放",
"video.unmute": "解除消音",
"video_player.expand": "展開影片",
"video_player.toggle_sound": "切換音效",
"video_player.toggle_visible": "切換可見性",

@ -0,0 +1,23 @@
import { Map as ImmutableMap } from 'immutable';
import { HEIGHT_CACHE_SET, HEIGHT_CACHE_CLEAR } from '../actions/height_cache';
const initialState = ImmutableMap();
const setHeight = (state, key, id, height) => {
return state.update(key, ImmutableMap(), map => map.set(id, height));
};
const clearHeights = () => {
return ImmutableMap();
};
export default function statuses(state = initialState, action) {
switch(action.type) {
case HEIGHT_CACHE_SET:
return setHeight(state, action.key, action.id, action.height);
case HEIGHT_CACHE_CLEAR:
return clearHeights();
default:
return state;
}
};

@ -21,6 +21,7 @@ import compose from './compose';
import search from './search';
import media_attachments from './media_attachments';
import notifications from './notifications';
import height_cache from './height_cache';
const reducers = {
timelines,
@ -45,6 +46,7 @@ const reducers = {
search,
media_attachments,
notifications,
height_cache,
};
export default combineReducers(reducers);

@ -15,8 +15,6 @@ import {
CONTEXT_FETCH_SUCCESS,
STATUS_MUTE_SUCCESS,
STATUS_UNMUTE_SUCCESS,
STATUS_SET_HEIGHT,
STATUSES_CLEAR_HEIGHT,
} from '../actions/statuses';
import {
TIMELINE_REFRESH_SUCCESS,
@ -60,9 +58,14 @@ const normalizeStatus = (state, status) => {
}
const searchContent = [status.spoiler_text, status.content].join(' ').replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n');
const emojiMap = normalStatus.emojis.reduce((obj, emoji) => {
obj[`:${emoji.shortcode}:`] = emoji.url;
return obj;
}, {});
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
normalStatus.contentHtml = emojify(normalStatus.content);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''));
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''), emojiMap);
return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus)));
};
@ -95,18 +98,6 @@ const filterStatuses = (state, relationship) => {
return state;
};
const setHeight = (state, id, height) => {
return state.update(id, ImmutableMap(), map => map.set('height', height));
};
const clearHeights = (state) => {
state.forEach(status => {
state = state.deleteIn([status.get('id'), 'height']);
});
return state;
};
const initialState = ImmutableMap();
export default function statuses(state = initialState, action) {
@ -148,10 +139,6 @@ export default function statuses(state = initialState, action) {
return deleteStatus(state, action.id, action.references);
case ACCOUNT_BLOCK_SUCCESS:
return filterStatuses(state, action.relationship);
case STATUS_SET_HEIGHT:
return setHeight(state, action.id, action.height);
case STATUSES_CLEAR_HEIGHT:
return clearHeights(state);
default:
return state;
}

@ -25,6 +25,11 @@ function main() {
const emojify = require('../mastodon/emoji').default;
const { getLocale } = require('../mastodon/locales');
const { localeData } = getLocale();
const VideoContainer = require('../mastodon/containers/video_container').default;
const MediaGalleryContainer = require('../mastodon/containers/media_gallery_container').default;
const CardContainer = require('../mastodon/containers/card_container').default;
const React = require('react');
const ReactDOM = require('react-dom');
localeData.forEach(IntlRelativeFormat.__addLocaleData);
@ -66,22 +71,21 @@ function main() {
window.open(e.target.href, 'mastodon-intent', 'width=400,height=400,resizable=no,menubar=no,status=no,scrollbars=yes');
});
});
});
delegate(document, '.video-player video', 'click', ({ target }) => {
if (target.paused) {
target.play();
} else {
target.pause();
}
});
[].forEach.call(document.querySelectorAll('[data-component="Video"]'), (content) => {
const props = JSON.parse(content.getAttribute('data-props'));
ReactDOM.render(<VideoContainer locale={locale} {...props} />, content);
});
delegate(document, '.activity-stream .media-spoiler-wrapper .media-spoiler', 'click', function() {
this.parentNode.classList.add('media-spoiler-wrapper__visible');
});
[].forEach.call(document.querySelectorAll('[data-component="MediaGallery"]'), (content) => {
const props = JSON.parse(content.getAttribute('data-props'));
ReactDOM.render(<MediaGalleryContainer locale={locale} {...props} />, content);
});
delegate(document, '.activity-stream .media-spoiler-wrapper .spoiler-button', 'click', function() {
this.parentNode.classList.remove('media-spoiler-wrapper__visible');
[].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => {
const props = JSON.parse(content.getAttribute('data-props'));
ReactDOM.render(<CardContainer locale={locale} {...props} />, content);
});
});
delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
@ -126,7 +130,7 @@ function main() {
delegate(document, '#account_avatar', 'change', ({ target }) => {
const avatar = document.querySelector('.card.compact .avatar img');
const [file] = target.files || [];
const url = URL.createObjectURL(file);
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
avatar.src = url;
});
@ -134,7 +138,7 @@ function main() {
delegate(document, '#account_header', 'change', ({ target }) => {
const header = document.querySelector('.card.compact');
const [file] = target.files || [];
const url = URL.createObjectURL(file);
const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc;
header.style.backgroundImage = `url(${url})`;
});

@ -137,7 +137,7 @@
padding-bottom: 15px;
.hero .heading {
padding-bottom: 30px;
padding-bottom: 20px;
font-family: 'mastodon-font-sans-serif', sans-serif;
font-size: 16px;
font-weight: 400;
@ -327,7 +327,7 @@
.about-short {
background: darken($ui-base-color, 4%);
padding: 50px 0;
padding: 50px 0 30px;
font-family: 'mastodon-font-sans-serif', sans-serif;
font-size: 16px;
font-weight: 400;
@ -640,8 +640,11 @@
.header-wrapper {
padding-top: 0;
&.compact {
padding-bottom: 0;
}
&.compact .hero .heading {
padding-bottom: 20px;
text-align: initial;
}
}

@ -97,6 +97,14 @@
margin-bottom: 40px;
}
h3 {
color: $ui-secondary-color;
font-size: 20px;
line-height: 28px;
font-weight: 400;
margin-bottom: 30px;
}
h6 {
font-size: 16px;
color: $ui-secondary-color;

@ -631,6 +631,10 @@
opacity: 1;
animation: fade 150ms linear;
.video-player {
margin-top: 8px;
}
&.status-direct {
background: lighten($ui-base-color, 8%);
@ -867,6 +871,10 @@
height: 22px;
}
}
.video-player {
margin-top: 8px;
}
}
.detailed-status__meta {
@ -1610,9 +1618,8 @@
.column,
.drawer {
@supports(display: grid) { // hack to fix Chrome <57
contain: strict;
}
flex: 1 1 100%;
overflow: hidden;
}
@include limited-single-column('screen and (max-width: 360px)', $parent: null) {
@ -1790,9 +1797,7 @@
overflow-x: hidden;
flex: 1 1 auto;
-webkit-overflow-scrolling: touch;
@supports(display: grid) { // hack to fix Chrome <57
contain: strict;
}
will-change: transform; // improves perf in mobile Chrome
&.optionally-scrollable {
overflow-y: auto;
@ -2642,7 +2647,7 @@ button.icon-button.active i.fa-retweet {
.media-spoiler {
background: $base-overlay-background;
color: $primary-text-color;
color: $ui-primary-color;
border: 0;
width: 100%;
height: 100%;
@ -4206,6 +4211,182 @@ button.icon-button.active i.fa-retweet {
z-index: 5;
}
.video-player {
overflow: hidden;
position: relative;
background: $base-shadow-color;
max-width: 100%;
video {
height: 100%;
width: 100%;
z-index: 1;
}
&.fullscreen {
width: 100% !important;
height: 100% !important;
margin: 0;
video {
max-width: 100% !important;
max-height: 100% !important;
}
}
&.inline {
video {
object-fit: cover;
position: relative;
top: 50%;
transform: translateY(-50%);
}
}
&__controls {
position: absolute;
z-index: 2;
bottom: 0;
left: 0;
right: 0;
box-sizing: border-box;
background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 60%, transparent);
padding: 0 10px;
opacity: 0;
transition: opacity .1s ease;
&.active {
opacity: 1;
}
}
&.inactive {
video,
.video-player__controls {
visibility: hidden;
}
}
&__spoiler {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 4;
border: 0;
background: $base-shadow-color;
color: $ui-primary-color;
transition: none;
pointer-events: none;
&.active {
display: block;
pointer-events: auto;
&:hover,
&:active,
&:focus {
color: lighten($ui-primary-color, 8%);
}
}
&__title {
display: block;
font-size: 14px;
}
&__subtitle {
display: block;
font-size: 11px;
font-weight: 500;
}
}
&__buttons {
padding-bottom: 10px;
font-size: 16px;
&.left {
float: left;
button {
padding-right: 10px;
}
}
&.right {
float: right;
button {
padding-left: 10px;
}
}
button {
background: transparent;
padding: 0;
border: 0;
color: $white;
&:active,
&:hover,
&:focus {
color: $ui-highlight-color;
}
}
}
&__seek {
cursor: pointer;
height: 24px;
position: relative;
&::before {
content: "";
width: 100%;
background: rgba($white, 0.35);
display: block;
position: absolute;
height: 4px;
top: 10px;
}
&__progress {
display: block;
position: absolute;
height: 4px;
top: 10px;
background: $ui-highlight-color;
}
&__handle {
position: absolute;
z-index: 3;
opacity: 0;
border-radius: 50%;
width: 12px;
height: 12px;
top: 6px;
margin-left: -6px;
transition: opacity .1s ease;
background: $ui-highlight-color;
pointer-events: none;
&.active {
opacity: 1;
}
}
&:hover {
.video-player__seek__handle {
opacity: 1;
}
}
}
}
.media-spoiler-video {
background-size: cover;
background-repeat: no-repeat;

@ -349,9 +349,46 @@ code {
box-shadow: 0 0 5px rgba($base-shadow-color, 0.2);
text-align: center;
p {
margin-bottom: 15px;
}
.oauth-code {
color: $ui-secondary-color;
outline: 0;
box-sizing: border-box;
display: block;
width: 100%;
border: none;
padding: 10px;
font-family: 'mastodon-font-monospace', monospace;
background: $ui-base-color;
color: $ui-primary-color;
font-size: 14px;
margin: 0;
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
&:focus {
background: lighten($ui-base-color, 4%);
}
}
strong {
font-weight: 500;
}
@media screen and (max-width: 740px) and (min-width: 441px) {
margin-top: 40px;
}
}
.form-footer {

@ -140,19 +140,6 @@
}
}
}
.status__attachments {
margin-top: 8px;
overflow: hidden;
width: 100%;
box-sizing: border-box;
position: relative;
.status__attachments__inner {
display: flex;
height: 214px;
}
}
}
.detailed-status.light {
@ -233,139 +220,35 @@
}
}
.detailed-status__attachments {
margin-top: 8px;
overflow: hidden;
width: 100%;
box-sizing: border-box;
position: relative;
.status-card {
border-color: lighten($ui-secondary-color, 4%);
color: darken($ui-primary-color, 4%);
.status__attachments__inner {
display: flex;
height: 360px;
&:hover {
background: lighten($ui-secondary-color, 4%);
}
}
.video-player {
margin-top: 8px;
height: 300px;
overflow: hidden;
position: relative;
video {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
object-fit: cover;
top: 50%;
transform: translateY(-50%);
}
}
}
.media-item,
.video-item {
box-sizing: border-box;
position: relative;
left: auto;
top: auto;
right: auto;
bottom: auto;
float: left;
border: medium none;
display: block;
flex: 1 1 auto;
width: 100%;
height: 100%;
overflow: hidden;
margin-right: 2px;
&:last-child {
margin-right: 0;
}
a {
display: block;
width: 100%;
height: 100%;
background: no-repeat scroll center center / cover;
text-decoration: none;
cursor: zoom-in;
}
video {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
object-fit: cover;
top: 50%;
transform: translateY(-50%);
}
}
.video-item {
a {
cursor: pointer;
.status-card__title,
.status-card__description {
color: $ui-base-color;
}
.video-item__play {
position: absolute;
top: 50%;
left: 50%;
font-size: 36px;
transform: translate(-50%, -50%);
padding: 5px;
border-radius: 100px;
color: rgba($primary-text-color, 0.8);
z-index: 1;
.status-card__image {
background: $ui-secondary-color;
}
}
.media-spoiler {
background: $ui-primary-color;
width: 100%;
height: 100%;
cursor: pointer;
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
color: $white;
transition: all 100ms linear;
z-index: 2;
&:hover {
&:hover,
&:active,
&:focus {
background: darken($ui-primary-color, 5%);
}
span {
display: block;
&:first-child {
font-size: 14px;
}
&:last-child {
font-size: 11px;
font-weight: 500;
}
}
}
.media-spoiler-wrapper {
&.media-spoiler-wrapper__visible {
.media-spoiler {
display: none;
}
.spoiler-button {
display: block;
}
color: unset;
}
}

@ -11,7 +11,12 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
return status unless status.nil?
status = Status.create!(account: @account, reblog: original_status, uri: @json['id'])
status = Status.create!(
account: @account,
reblog: original_status,
uri: @json['id'],
created_at: @json['published'] || Time.now.utc
)
distribute(status)
status
end

@ -4,26 +4,31 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def perform
return if delete_arrived_first?(object_uri) || unsupported_object_type?
status = find_existing_status
RedisLock.acquire(lock_options) do |lock|
if lock.acquired?
@status = find_existing_status
process_status if @status.nil?
end
end
@status
end
return status unless status.nil?
private
def process_status
ApplicationRecord.transaction do
status = Status.create!(status_params)
@status = Status.create!(status_params)
process_tags(status)
process_attachments(status)
process_tags(@status)
process_attachments(@status)
end
resolve_thread(status)
distribute(status)
forward_for_reply if status.public_visibility? || status.unlisted_visibility?
status
resolve_thread(@status)
distribute(@status)
forward_for_reply if @status.public_visibility? || @status.unlisted_visibility?
end
private
def find_existing_status
status = status_from_uri(object_uri)
status ||= Status.find_by(uri: @object['atomUri']) if @object['atomUri'].present?
@ -56,6 +61,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
process_hashtag tag, status
when 'Mention'
process_mention tag, status
when 'Emoji'
process_emoji tag, status
end
end
end
@ -74,6 +81,17 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
account.mentions.create(status: status)
end
def process_emoji(tag, _status)
shortcode = tag['name'].delete(':')
emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)
return if !emoji.nil? || skip_download?
emoji = CustomEmoji.new(domain: @account.domain, shortcode: shortcode)
emoji.image_remote_url = tag['href']
emoji.save
end
def process_attachments(status)
return unless @object['attachment'].is_a?(Array)
@ -182,4 +200,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
return unless @json['signature'].present? && reply_to_local?
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id)
end
def lock_options
{ redis: Redis.current, key: "create:#{@object['id']}" }
end
end

@ -14,6 +14,8 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
'atomUri' => 'ostatus:atomUri',
'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri',
'conversation' => 'ostatus:conversation',
'toot' => 'http://joinmastodon.org/ns#',
'Emoji' => 'toot:Emoji',
},
],
}.freeze
@ -28,7 +30,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
def serializable_hash(options = nil)
options = serialization_options(options)
serialized_hash = CONTEXT.merge(ActiveModelSerializers::Adapter::Attributes.new(serializer, instance_options).serializable_hash(options))
self.class.transform_key_casing!(serialized_hash, instance_options)
serialized_hash = ActiveModelSerializers::Adapter::Attributes.new(serializer, instance_options).serializable_hash(options)
CONTEXT.merge(self.class.transform_key_casing!(serialized_hash, instance_options))
end
end

@ -37,7 +37,7 @@ class ActivityPub::TagManager
end
def activity_uri_for(target)
return nil unless %i(note comment activity).include?(target.object_type) && target.local?
raise ArgumentError, 'target must be a local activity' unless %i(note comment activity).include?(target.object_type) && target.local?
activity_account_status_url(target.account, target)
end

@ -9,7 +9,7 @@ class Formatter
include ActionView::Helpers::TextHelper
def format(status)
def format(status, options = {})
if status.reblog?
prepend_reblog = status.reblog.account.acct
status = status.proper
@ -19,7 +19,11 @@ class Formatter
raw_content = status.text
return reformat(raw_content) unless status.local?
unless status.local?
html = reformat(raw_content)
html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify]
return html
end
linkable_accounts = status.mentions.map(&:account)
linkable_accounts << status.account
@ -27,6 +31,7 @@ class Formatter
html = raw_content
html = "RT @#{prepend_reblog} #{html}" if prepend_reblog
html = encode_and_link_urls(html, linkable_accounts)
html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify]
html = simple_format(html, {}, sanitize: false)
html = html.delete("\n")
@ -39,7 +44,9 @@ class Formatter
def plaintext(status)
return status.text if status.local?
strip_tags(status.text)
text = status.text.gsub(/(<br \/>|<br>|<\/p>)+/) { |match| "#{match}\n" }
strip_tags(text)
end
def simplified_format(account)
@ -76,6 +83,47 @@ class Formatter
end
end
def encode_custom_emojis(html, emojis)
return html if emojis.empty?
emoji_map = emojis.map { |e| [e.shortcode, full_asset_url(e.image.url)] }.to_h
i = -1
inside_tag = false
inside_shortname = false
shortname_start_index = -1
while i + 1 < html.size
i += 1
if inside_shortname && html[i] == ':'
shortcode = html[shortname_start_index + 1..i - 1]
emoji = emoji_map[shortcode]
if emoji
replacement = "<img draggable=\"false\" class=\"emojione\" alt=\":#{shortcode}:\" title=\":#{shortcode}:\" src=\"#{emoji}\" />"
before_html = shortname_start_index.positive? ? html[0..shortname_start_index - 1] : ''
html = before_html + replacement + html[i + 1..-1]
i += replacement.size - (shortcode.size + 2) - 1
else
i -= 1
end
inside_shortname = false
elsif inside_tag && html[i] == '>'
inside_tag = false
elsif html[i] == '<'
inside_tag = true
inside_shortname = false
elsif !inside_tag && html[i] == ':'
inside_shortname = true
shortname_start_index = i
end
end
html
end
def rewrite(text, entities)
chars = text.to_s.to_char_a
@ -131,13 +179,13 @@ class Formatter
end
def link_html(url)
url = Addressable::URI.parse(url).display_uri.to_s
url = Addressable::URI.parse(url).to_s
prefix = url.match(/\Ahttps?:\/\/(www\.)?/).to_s
text = url[prefix.length, 30]
suffix = url[prefix.length + 30..-1]
cutoff = url[prefix.length..-1].length > 30
"<span class=\"invisible\">#{prefix}</span><span class=\"#{cutoff ? 'ellipsis' : ''}\">#{text}</span><span class=\"invisible\">#{suffix}</span>"
"<span class=\"invisible\">#{encode(prefix)}</span><span class=\"#{cutoff ? 'ellipsis' : ''}\">#{encode(text)}</span><span class=\"invisible\">#{encode(suffix)}</span>"
end
def hashtag_html(tag)

@ -1,26 +1,31 @@
# frozen_string_literal: true
class LanguageDetector
attr_reader :text, :account
include Singleton
def initialize(text, account = nil)
@text = text
@account = account
def initialize
@identifier = CLD3::NNetLanguageIdentifier.new(1, 2048)
end
def to_iso_s
detected_language_code || default_locale
def detect(text, account)
detect_language_code(text) || default_locale(account)
end
def prepared_text
simplified_text.strip
def language_names
@language_names =
CLD3::TaskContextParams::LANGUAGE_NAMES.map { |name| iso6391(name.to_s).to_sym }
.uniq
end
private
def detected_language_code
iso6391(result.language).to_sym if detected_language_reliable?
def prepare_text(text)
simplify_text(text).strip
end
def detect_language_code(text)
result = @identifier.find_language(prepare_text(text))
iso6391(result.language.to_s).to_sym if result.reliable?
end
def iso6391(bcp47)
@ -32,15 +37,7 @@ class LanguageDetector
ISO_639.find(iso639).alpha2
end
def result
@result ||= @identifier.find_language(prepared_text)
end
def detected_language_reliable?
result.reliable?
end
def simplified_text
def simplify_text(text)
text.dup.tap do |new_text|
new_text.gsub!(FetchLinkCardService::URL_PATTERN, '')
new_text.gsub!(Account::MENTION_RE, '')
@ -49,7 +46,7 @@ class LanguageDetector
end
end
def default_locale
account&.user_locale&.to_sym || nil
def default_locale(account)
account.user_locale&.to_sym
end
end

@ -42,6 +42,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
save_mentions(status)
save_hashtags(status)
save_media(status)
save_emojis(status)
end
if thread? && status.thread.nil?
@ -150,6 +151,25 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
end
end
def save_emojis(parent)
do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media?
return if do_not_download
@xml.xpath('./xmlns:link[@rel="emoji"]', xmlns: TagManager::XMLNS).each do |link|
next unless link['href'] && link['name']
shortcode = link['name'].delete(':')
emoji = CustomEmoji.find_by(shortcode: shortcode, domain: parent.account.domain)
next unless emoji.nil?
emoji = CustomEmoji.new(shortcode: shortcode, domain: parent.account.domain)
emoji.image_remote_url = link['href']
emoji.save
end
end
def account_from_href(href)
url = Addressable::URI.parse(href).normalize

@ -368,5 +368,9 @@ class OStatus::AtomSerializer
end
append_element(entry, 'mastodon:scope', status.visibility)
status.emojis.each do |emoji|
append_element(entry, 'link', nil, rel: :emoji, href: full_asset_url(emoji.image.url), name: emoji.shortcode)
end
end
end

@ -31,6 +31,8 @@ class Request
def perform
http_client.headers(headers).public_send(@verb, @url.to_s, @options)
rescue => e
raise e.class, "#{e.message} on #{@url}"
end
def headers

@ -87,7 +87,7 @@ class TagManager
def local_url?(url)
uri = Addressable::URI.parse(url).normalize
domain = uri.host + (uri.port ? ":#{uri.port}" : '')
TagManager.instance.local_domain?(domain)
TagManager.instance.web_domain?(domain)
end
def uri_for(target)

@ -106,6 +106,7 @@ class Account < ApplicationRecord
scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
delegate :email,
:current_sign_in_ip,
@ -174,6 +175,10 @@ class Account < ApplicationRecord
end
class << self
def readonly_attributes
super - %w(statuses_count following_count followers_count)
end
def domains
reorder(nil).pluck('distinct accounts.domain')
end

@ -27,9 +27,11 @@ module Remotable
matches = response.headers['content-disposition']&.match(/filename="([^"]*)"/)
filename = matches.nil? ? parsed_url.path.split('/').last : matches[1]
basename = SecureRandom.hex(8)
extname = File.extname(filename)
send("#{attachment_name}=", StringIO.new(response.to_s))
send("#{attachment_name}_file_name=", filename)
send("#{attachment_name}_file_name=", basename + extname)
self[attribute_name] = url if has_attribute?(attribute_name)
rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError => e

@ -0,0 +1,38 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: custom_emojis
#
# id :integer not null, primary key
# shortcode :string default(""), not null
# domain :string
# image_file_name :string
# image_content_type :string
# image_file_size :integer
# image_updated_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
#
class CustomEmoji < ApplicationRecord
SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}'
SCAN_RE = /(?<=[^[:alnum:]:]|\n|^)
:(#{SHORTCODE_RE_FRAGMENT}):
(?=[^[:alnum:]:]|$)/x
has_attached_file :image
validates_attachment :image, content_type: { content_type: 'image/png' }, presence: true, size: { in: 0..50.kilobytes }
validates :shortcode, uniqueness: { scope: :domain }, format: { with: /\A#{SHORTCODE_RE_FRAGMENT}\z/ }, length: { minimum: 2 }
include Remotable
class << self
def from_text(text, domain)
return [] if text.blank?
shortcodes = text.scan(SCAN_RE).map(&:first)
where(shortcode: shortcodes, domain: domain)
end
end
end

@ -0,0 +1,28 @@
# frozen_string_literal: true
class InstanceFilter
attr_reader :params
def initialize(params)
@params = params
end
def results
scope = Account.remote.by_domain_accounts
params.each do |key, value|
scope.merge!(scope_for(key, value)) if value.present?
end
scope
end
private
def scope_for(key, value)
case key.to_s
when 'domain_name'
Account.matches_domain(value)
else
raise "Unknown filter: #{key}"
end
end
end

@ -56,15 +56,21 @@ class MediaAttachment < ApplicationRecord
validates :account, presence: true
scope :attached, -> { where.not(status_id: nil) }
scope :attached, -> { where.not(status_id: nil) }
scope :unattached, -> { where(status_id: nil) }
scope :local, -> { where(remote_url: '') }
scope :local, -> { where(remote_url: '') }
scope :remote, -> { where.not(remote_url: '') }
default_scope { order(id: :asc) }
def local?
remote_url.blank?
end
def needs_redownload?
file.blank? && remote_url.present?
end
def to_param
shortcode
end

@ -0,0 +1,44 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: site_uploads
#
# id :integer not null, primary key
# var :string default(""), not null
# file_file_name :string
# file_content_type :string
# file_file_size :integer
# file_updated_at :datetime
# meta :json
# created_at :datetime not null
# updated_at :datetime not null
#
class SiteUpload < ApplicationRecord
has_attached_file :file
validates_attachment_content_type :file, content_type: /\Aimage\/.*\z/
validates :var, presence: true, uniqueness: true
before_save :set_meta
after_commit :clear_cache
def cache_key
"site_uploads/#{var}"
end
private
def set_meta
tempfile = file.queued_for_write[:original]
return if tempfile.nil?
geometry = Paperclip::Geometry.from_file(tempfile)
self.meta = { width: geometry.width.to_i, height: geometry.height.to_i }
end
def clear_cache
Rails.cache.delete(cache_key)
end
end

@ -55,7 +55,7 @@ class Status < ApplicationRecord
has_one :notification, as: :activity, dependent: :destroy
has_one :stream_entry, as: :activity, inverse_of: :status
validates :uri, uniqueness: true, unless: :local?
validates :uri, uniqueness: true, presence: true, unless: :local?
validates :text, presence: true, unless: :reblog?
validates_with StatusLengthValidator
validates :reblog, uniqueness: { scope: :account }, if: :reblog?
@ -70,7 +70,6 @@ class Status < ApplicationRecord
scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') }
scope :with_public_visibility, -> { where(visibility: :public) }
scope :tagged_with, ->(tag) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag }) }
scope :local_only, -> { left_outer_joins(:account).where(accounts: { domain: nil }) }
scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced: false }) }
scope :including_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced: true }) }
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
@ -132,6 +131,10 @@ class Status < ApplicationRecord
!sensitive? && media_attachments.any?
end
def emojis
CustomEmoji.from_text(text, account.domain)
end
after_create :store_uri, if: :local?
before_validation :prepare_contents, if: :local?
@ -221,7 +224,7 @@ class Status < ApplicationRecord
private
def timeline_scope(local_only = false)
starting_scope = local_only ? Status.local_only : Status
starting_scope = local_only ? Status.local : Status
starting_scope
.with_public_visibility
.without_reblogs

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

Loading…
Cancel
Save