Merge branch 'master' into feature-onboarding

This commit is contained in:
Eugen Rochko 2017-04-13 21:45:46 +02:00
commit 88763d2192
469 changed files with 11039 additions and 4248 deletions

2
.buildpacks Normal file
View file

@ -0,0 +1,2 @@
https://github.com/Scalingo/nodejs-buildpack
https://github.com/Scalingo/ruby-buildpack

View file

@ -1,6 +1,8 @@
engines:
duplication:
enabled: true
exclude_paths:
- app/assets/javascripts/components/locales/
config:
languages:
- ruby

View file

@ -5,3 +5,4 @@ public/assets
node_modules
storybook
neo4j
vendor/bundle

12
.editorconfig Normal file
View file

@ -0,0 +1,12 @@
# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 2

View file

@ -22,13 +22,24 @@ OTP_SECRET=
# SINGLE_USER_MODE=true
# Prevent registrations with following e-mail domains
# EMAIL_DOMAIN_BLACKLIST=example1.com|example2.de|etc
# Only allow registrations with the following e-mail domains
# EMAIL_DOMAIN_WHITELIST=example1.com|example2.de|etc
# Optionally change default language
# DEFAULT_LOCALE=de
# E-mail configuration
# Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers
SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=587
SMTP_LOGIN=
SMTP_PASSWORD=
SMTP_FROM_ADDRESS=notifications@example.com
#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
#SMTP_AUTH_METHOD=plain
#SMTP_OPENSSL_VERIFY_MODE=peer
#SMTP_ENABLE_STARTTLS_AUTO=true
# Optional asset host for multi-server setups
# CDN_HOST=assets.example.com
@ -42,8 +53,22 @@ SMTP_FROM_ADDRESS=notifications@example.com
# S3_PROTOCOL=http
# S3_HOSTNAME=192.168.1.123:9000
# S3 (Minio Config (optional) Please check Minio instance for details)
# S3_ENABLED=true
# S3_BUCKET=
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=
# S3_PROTOCOL=https
# S3_HOSTNAME=
# S3_ENDPOINT=
# Optional alias for S3 if you want to use Cloudfront or Cloudflare in front
# S3_CLOUDFRONT_HOST=
# Streaming API integration
# STREAMING_API_BASE_URL=
# Advanced settings
# If you need to use pgBouncer, you need to disable prepared statements:
# PREPARED_STATEMENTS=false

30
.eslintignore Normal file
View file

@ -0,0 +1,30 @@
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'
# Ignore bundler config.
/.bundle
# Ignore the default SQLite database.
/db/*.sqlite3
/db/*.sqlite3-journal
# Ignore all logfiles and tempfiles.
/log/*
!/log/.keep
/tmp
coverage
public/system
public/assets
.env
.env.production
node_modules/
neo4j/
# Ignore Vagrant files
.vagrant/
# Ignore Capistrano customizations
config/deploy/*

8
.gitignore vendored
View file

@ -28,3 +28,11 @@ neo4j/
# Ignore Capistrano customizations
config/deploy/*
# Ignore IDE files
.vscode/
# Ignore postgres + redis volume optionally created by docker-compose
postgres
redis

View file

@ -1 +1 @@
2.3.1
2.4.1

5
.slugignore Normal file
View file

@ -0,0 +1,5 @@
node_modules/
.cache/
docs/
spec/
storybook/

View file

@ -16,7 +16,7 @@ addons:
postgresql: 9.4
rvm:
- 2.3.1
- 2.4.1
services:
- redis-server

View file

@ -7,7 +7,7 @@ There are three ways in which you can contribute to this repository:
2. By working on the back-end application
3. By working on the front-end application
Choosing what to work on in a large open source project is not easy. The list of GitHub issues may provide some ideas, but not every feature request has been greenlit. Likewise, not every change or feature that resolves a personal itch will be merged into the main repository. Some communication ahead of time may be wise.
Choosing what to work on in a large open source project is not easy. The list of GitHub issues may provide some ideas, but not every feature request has been greenlit. Likewise, not every change or feature that resolves a personal itch will be merged into the main repository. Some communication ahead of time may be wise. If your addition creates a new feature or setting, or otherwise changes how things work in some substantial way, please remember to submit a correlating pull request to document your changes in the [documentation](http://github.com/tootsuite/documentation).
Below are the guidelines for working on pull requests:
@ -41,3 +41,4 @@ It is expected that you have a working development environment set up (see back-
* If you are introducing new strings, they must be using localization methods
If the JavaScript or CSS assets won't compile due to a syntax error, it's a good sign that the pull request isn't ready for submission yet.

View file

@ -1,11 +1,16 @@
FROM ruby:2.3.1-alpine
FROM ruby:2.4.1-alpine
LABEL maintainer="https://github.com/tootsuite/mastodon" \
description="A GNU Social-compatible microblogging server"
ENV RAILS_ENV=production \
NODE_ENV=production
EXPOSE 3000 4000
WORKDIR /mastodon
COPY . /mastodon
COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
RUN BUILD_DEPS=" \
postgresql-dev \
@ -24,8 +29,11 @@ RUN BUILD_DEPS=" \
&& npm install -g npm@3 && npm install -g yarn \
&& bundle install --deployment --without test development \
&& yarn \
&& npm cache clean \
&& yarn cache clean \
&& npm -g cache clean \
&& apk del $BUILD_DEPS \
&& rm -rf /tmp/* /var/cache/apk/*
COPY . /mastodon
VOLUME /mastodon/public/system /mastodon/public/assets

49
Gemfile
View file

@ -1,15 +1,13 @@
# frozen_string_literal: true
source 'https://rubygems.org'
ruby '2.3.1'
ruby '2.4.1'
gem 'rails', '~> 5.0.2'
gem 'sass-rails', '~> 5.0'
gem 'uglifier', '>= 1.3.0'
gem 'coffee-rails', '~> 4.1.0'
gem 'jquery-rails'
gem 'jbuilder', '~> 2.0'
gem 'sdoc', '~> 0.4.0', group: :doc
gem 'puma'
gem 'hamlit-rails'
@ -23,34 +21,37 @@ gem 'paperclip', '~> 5.1'
gem 'paperclip-av-transcoder'
gem 'aws-sdk', '>= 2.0'
gem 'http'
gem 'httplog'
gem 'addressable'
gem 'nokogiri'
gem 'link_header'
gem 'ostatus2'
gem 'goldfinger'
gem 'devise'
gem 'devise-two-factor'
gem 'doorkeeper'
gem 'rabl'
gem 'rqrcode'
gem 'twitter-text'
gem 'oj'
gem 'hiredis'
gem 'redis', '~>3.2'
gem 'fast_blank'
gem 'goldfinger'
gem 'hiredis'
gem 'htmlentities'
gem 'simple_form'
gem 'will_paginate'
gem 'http'
gem 'http_accept_language'
gem 'httplog'
gem 'kaminari'
gem 'link_header'
gem 'nokogiri'
gem 'oj'
gem 'ostatus2'
gem 'ox'
gem 'rabl'
gem 'rack-attack'
gem 'rack-cors', require: 'rack/cors'
gem 'sidekiq'
gem 'rails-settings-cached'
gem 'simple-navigation'
gem 'statsd-instrument'
gem 'ruby-oembed', require: 'oembed'
gem 'rack-timeout'
gem 'rails-settings-cached'
gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
gem 'rqrcode'
gem 'ruby-oembed', require: 'oembed'
gem 'sidekiq'
gem 'sidekiq-unique-jobs'
gem 'simple-navigation'
gem 'simple_form'
gem 'statsd-instrument'
gem 'twitter-text'
gem 'tzinfo-data'
gem 'react-rails'
@ -66,9 +67,11 @@ group :development, :test do
end
group :test do
gem 'faker'
gem 'rails-controller-testing'
gem 'rspec-sidekiq'
gem 'simplecov', require: false
gem 'webmock'
gem 'rspec-sidekiq'
end
group :development do

View file

@ -24,7 +24,7 @@ GEM
erubis (~> 2.7.0)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
active_record_query_trace (1.5.3)
active_record_query_trace (1.5.4)
activejob (5.0.2)
activesupport (= 5.0.2)
globalid (>= 0.3.6)
@ -39,7 +39,7 @@ GEM
i18n (~> 0.7)
minitest (~> 5.1)
tzinfo (~> 1.1)
addressable (2.5.0)
addressable (2.5.1)
public_suffix (~> 2.0, >= 2.0.2)
airbrussh (1.1.2)
sshkit (>= 1.6.1, != 1.7.0)
@ -47,17 +47,17 @@ GEM
ast (2.3.0)
attr_encrypted (3.0.3)
encryptor (~> 3.0.0)
autoprefixer-rails (6.5.0.2)
autoprefixer-rails (6.7.7.1)
execjs
av (0.9.0)
cocaine (~> 0.5.3)
aws-sdk (2.6.28)
aws-sdk-resources (= 2.6.28)
aws-sdk-core (2.6.28)
aws-sdk (2.9.6)
aws-sdk-resources (= 2.9.6)
aws-sdk-core (2.9.6)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
aws-sdk-resources (2.6.28)
aws-sdk-core (= 2.6.28)
aws-sdk-resources (2.9.6)
aws-sdk-core (= 2.9.6)
aws-sigv4 (1.0.0)
babel-source (5.8.35)
babel-transpiler (0.7.0)
@ -78,12 +78,11 @@ GEM
railties (>= 4.0.0, < 5.1)
sprockets (>= 3.6.0)
builder (3.2.3)
bullet (5.3.0)
bullet (5.5.1)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0)
capistrano (3.7.2)
capistrano (3.8.0)
airbrussh (>= 1.0.0)
capistrano-harrow
i18n
rake (>= 10.0.0)
sshkit (>= 1.9.0)
@ -92,8 +91,7 @@ GEM
sshkit (~> 1.2)
capistrano-faster-assets (1.0.2)
capistrano (>= 3.1)
capistrano-harrow (0.5.3)
capistrano-rails (1.2.2)
capistrano-rails (1.2.3)
capistrano (~> 3.1)
capistrano-bundler (~> 1.1)
capistrano-rbenv (2.1.0)
@ -119,7 +117,7 @@ GEM
crack (0.4.3)
safe_yaml (~> 1.0.0)
debug_inspector (0.0.2)
devise (4.2.0)
devise (4.2.1)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0, < 5.1)
@ -131,16 +129,16 @@ GEM
devise (~> 4.0)
railties
rotp (~> 2.0)
diff-lcs (1.2.5)
diff-lcs (1.3)
docile (1.1.5)
domain_name (0.5.20161129)
domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.0)
doorkeeper (4.2.5)
railties (>= 4.2)
dotenv (2.1.1)
dotenv-rails (2.1.1)
dotenv (= 2.1.1)
railties (>= 4.0, < 5.1)
dotenv (2.2.0)
dotenv-rails (2.2.0)
dotenv (= 2.2.0)
railties (>= 3.2, < 5.1)
easy_translate (0.5.0)
json
thread
@ -148,12 +146,14 @@ GEM
encryptor (3.0.0)
erubis (2.7.0)
execjs (2.7.0)
fabrication (2.15.2)
fabrication (2.16.1)
faker (1.7.3)
i18n (~> 0.5)
fast_blank (1.0.0)
font-awesome-rails (4.6.3.1)
font-awesome-rails (4.7.0.1)
railties (>= 3.2, < 5.1)
fuubar (2.1.1)
rspec (~> 3.0)
fuubar (2.2.0)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
globalid (0.3.7)
activesupport (>= 4.1.0)
@ -161,20 +161,20 @@ GEM
addressable (~> 2.4)
http (~> 2.0)
nokogiri (~> 1.6)
hamlit (2.7.2)
temple (~> 0.7.6)
hamlit (2.8.1)
temple (>= 0.8.0)
thor
tilt
hamlit-rails (0.1.0)
hamlit-rails (0.2.0)
actionpack (>= 4.0.1)
activesupport (>= 4.0.1)
hamlit (>= 1.2.0)
railties (>= 4.0.1)
hashdiff (0.3.0)
hashdiff (0.3.2)
highline (1.7.8)
hiredis (0.6.1)
htmlentities (4.3.4)
http (2.1.0)
http (2.2.1)
addressable (~> 2.3)
http-cookie (~> 1.0)
http-form_data (~> 1.0.1)
@ -182,11 +182,12 @@ GEM
http-cookie (1.0.3)
domain_name (~> 0.5)
http-form_data (1.0.1)
http_accept_language (2.1.0)
http_parser.rb (0.6.0)
httplog (0.3.2)
httplog (0.99.2)
colorize
i18n (0.8.1)
i18n-tasks (0.9.6)
i18n-tasks (0.9.13)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
easy_translate (>= 0.5.0)
@ -194,22 +195,31 @@ GEM
highline (>= 1.7.3)
i18n
parser (>= 2.2.3.0)
term-ansicolor (>= 1.3.2)
rainbow (~> 2.2)
terminal-table (>= 1.5.1)
jbuilder (2.6.0)
activesupport (>= 3.0.0, < 5.1)
multi_json (~> 1.2)
jmespath (1.3.1)
jquery-rails (4.1.1)
jquery-rails (4.3.1)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (1.8.3)
json (2.0.3)
kaminari (1.0.1)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.0.1)
kaminari-activerecord (= 1.0.1)
kaminari-core (= 1.0.1)
kaminari-actionview (1.0.1)
actionview
kaminari-core (= 1.0.1)
kaminari-activerecord (1.0.1)
activerecord
kaminari-core (= 1.0.1)
kaminari-core (1.0.1)
launchy (2.4.3)
addressable (~> 2.3)
letter_opener (1.4.1)
launchy (~> 2.2)
letter_opener_web (1.3.0)
letter_opener_web (1.3.1)
actionmailer (>= 3.2)
letter_opener (~> 1.0)
railties (>= 3.2)
@ -229,19 +239,19 @@ GEM
mimemagic (0.3.2)
mini_portile2 (2.1.0)
minitest (5.10.1)
multi_json (1.12.1)
net-scp (1.2.1)
net-ssh (>= 2.6.5)
net-ssh (4.0.1)
net-ssh (4.1.0)
nio4r (2.0.0)
nokogiri (1.7.1)
mini_portile2 (~> 2.1.0)
oj (2.17.3)
oj (2.18.5)
orm_adapter (0.5.0)
ostatus2 (1.0.2)
addressable (~> 2.4)
http (~> 2.0)
nokogiri (~> 1.6)
ox (2.4.11)
paperclip (5.1.0)
activemodel (>= 4.2.0)
activesupport (>= 4.2.0)
@ -251,26 +261,26 @@ GEM
paperclip-av-transcoder (0.6.4)
av (~> 0.9.0)
paperclip (>= 2.5.2)
parser (2.3.1.2)
parser (2.4.0.0)
ast (~> 2.2)
pg (0.18.4)
pghero (1.6.2)
pg (0.20.0)
pghero (1.6.4)
activerecord
powerpack (0.1.1)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
pry-rails (0.3.4)
pry (>= 0.9.10)
public_suffix (2.0.4)
puma (3.6.0)
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (2.0.5)
puma (3.8.2)
rabl (0.13.1)
activesupport (>= 2.3.14)
rack (2.0.1)
rack-attack (5.0.1)
rack
rack-cors (0.4.0)
rack-cors (0.4.1)
rack-protection (1.5.3)
rack
rack-test (0.6.3)
@ -288,6 +298,10 @@ GEM
bundler (>= 1.3.0, < 2.0)
railties (= 5.0.2)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.1)
actionpack (~> 5.x)
actionview (~> 5.x)
activesupport (~> 5.x)
rails-dom-testing (2.0.2)
activesupport (>= 4.2.0, < 6.0)
nokogiri (~> 1.6)
@ -306,44 +320,37 @@ GEM
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.1.0)
rainbow (2.2.1)
rake (12.0.0)
rdoc (4.2.2)
json (~> 1.4)
react-rails (1.10.0)
react-rails (1.11.0)
babel-transpiler (>= 0.7.0)
coffee-script-source (~> 1.8)
connection_pool
execjs
railties (>= 3.2)
tilt
redis (3.3.2)
redis-actionpack (5.0.0)
actionpack (>= 4.0.0, < 6)
redis-rack (~> 2.0.0.pre)
redis-store (~> 1.2.0.pre)
redis-activesupport (5.0.1)
redis (3.3.3)
redis-actionpack (5.0.1)
actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3)
redis-store (>= 1.1.0, < 1.4.0)
redis-activesupport (5.0.2)
activesupport (>= 3, < 6)
redis-store (~> 1.2.0)
redis-rack (2.0.0)
rack (~> 2.0)
redis-store (~> 1.2.0)
redis-rails (5.0.1)
redis-actionpack (~> 5.0.0)
redis-activesupport (~> 5.0.0)
redis-store (~> 1.2.0)
redis-store (1.2.0)
redis-store (~> 1.3.0)
redis-rack (2.0.1)
rack (>= 2.0, < 3)
redis-store (>= 1.2, < 1.4)
redis-rails (5.0.2)
redis-actionpack (>= 5.0, < 6)
redis-activesupport (>= 5.0, < 6)
redis-store (>= 1.2, < 2)
redis-store (1.3.0)
redis (>= 2.2)
responders (2.3.0)
railties (>= 4.2.0, < 5.1)
rotp (2.1.2)
rqrcode (0.10.1)
chunky_png (~> 1.0)
rspec (3.5.0)
rspec-core (~> 3.5.0)
rspec-expectations (~> 3.5.0)
rspec-mocks (~> 3.5.0)
rspec-core (3.5.2)
rspec-core (3.5.4)
rspec-support (~> 3.5.0)
rspec-expectations (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
@ -351,7 +358,7 @@ GEM
rspec-mocks (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.5.0)
rspec-rails (3.5.1)
rspec-rails (3.5.2)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
@ -359,40 +366,40 @@ GEM
rspec-expectations (~> 3.5.0)
rspec-mocks (~> 3.5.0)
rspec-support (~> 3.5.0)
rspec-sidekiq (2.2.0)
rspec (~> 3.0, >= 3.0.0)
rspec-sidekiq (3.0.0)
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.5.0)
rubocop (0.42.0)
parser (>= 2.3.1.1, < 3.0)
rubocop (0.48.1)
parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-oembed (0.10.1)
ruby-oembed (0.12.0)
ruby-progressbar (1.8.1)
safe_yaml (1.0.4)
sass (3.4.22)
sass (3.4.23)
sass-rails (5.0.6)
railties (>= 4.0.0, < 6)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0)
tilt (>= 1.1, < 3)
sdoc (0.4.1)
json (~> 1.7, >= 1.7.7)
rdoc (~> 4.0)
sidekiq (4.2.7)
sidekiq (4.2.10)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
redis (~> 3.2, >= 3.2.1)
simple-navigation (4.0.3)
sidekiq-unique-jobs (5.0.0)
sidekiq (>= 4.0)
thor
simple-navigation (4.0.5)
activesupport (>= 2.3.2)
simple_form (3.2.1)
simple_form (3.4.0)
actionpack (> 4, < 5.1)
activemodel (> 4, < 5.1)
simplecov (0.12.0)
simplecov (0.14.1)
docile (~> 1.1.0)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
@ -405,43 +412,39 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sshkit (1.11.5)
sshkit (1.13.1)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
statsd-instrument (2.1.2)
temple (0.7.7)
term-ansicolor (1.4.0)
tins (~> 1.0)
terminal-table (1.7.0)
unicode-display_width (~> 1.1)
temple (0.8.0)
terminal-table (1.7.3)
unicode-display_width (~> 1.1.1)
thor (0.19.4)
thread (0.2.2)
thread_safe (0.3.6)
tilt (2.0.6)
tins (1.12.0)
tilt (2.0.7)
twitter-text (1.14.5)
unf (~> 0.1.0)
tzinfo (1.2.2)
tzinfo (1.2.3)
thread_safe (~> 0.1)
tzinfo-data (1.2017.2)
tzinfo (>= 1.0.0)
uglifier (3.0.1)
uglifier (3.2.0)
execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.2)
unicode-display_width (1.1.0)
unicode-display_width (1.1.3)
uniform_notifier (1.10.0)
warden (1.2.6)
warden (1.2.7)
rack (>= 1.0)
webmock (2.1.0)
webmock (2.3.2)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
will_paginate (3.1.0)
PLATFORMS
ruby
@ -467,6 +470,7 @@ DEPENDENCIES
doorkeeper
dotenv-rails
fabrication
faker
fast_blank
font-awesome-rails
fuubar
@ -475,10 +479,11 @@ DEPENDENCIES
hiredis
htmlentities
http
http_accept_language
httplog
i18n-tasks (~> 0.9.6)
jbuilder (~> 2.0)
jquery-rails
kaminari
letter_opener
letter_opener_web
link_header
@ -486,6 +491,7 @@ DEPENDENCIES
nokogiri
oj
ostatus2
ox
paperclip (~> 5.1)
paperclip-av-transcoder
pg
@ -497,6 +503,7 @@ DEPENDENCIES
rack-cors
rack-timeout
rails (~> 5.0.2)
rails-controller-testing
rails-settings-cached
rails_12factor
react-rails
@ -508,8 +515,8 @@ DEPENDENCIES
rubocop
ruby-oembed
sass-rails (~> 5.0)
sdoc (~> 0.4.0)
sidekiq
sidekiq-unique-jobs
simple-navigation
simple_form
simplecov
@ -518,10 +525,9 @@ DEPENDENCIES
tzinfo-data
uglifier (>= 1.3.0)
webmock
will_paginate
RUBY VERSION
ruby 2.3.1p112
ruby 2.4.1p111
BUNDLED WITH
1.14.3
1.14.6

5
ISSUE_TEMPLATE.md Normal file
View file

@ -0,0 +1,5 @@
[Issue text goes here].
* * * *
- [ ] I searched or browsed the repos other issues to ensure this is not a duplicate.

View file

@ -1,2 +1,2 @@
web: bundle exec puma -C config/puma.rb
worker: bundle exec sidekiq -q default -q mailers -q push
worker: bundle exec sidekiq -q default -q push -q pull -q mailers

View file

@ -17,7 +17,7 @@ Click on the screenshot to watch a demo of the UI:
[youtube_demo]: https://www.youtube.com/watch?v=YO1jQ8_rAMU
Focus of the project on a clean REST API and a good user interface. Ruby on Rails is used for the back-end, while React.js and Redux are used for the dynamic front-end. A static front-end for public resources (profiles and statuses) is also provided.
The project focus is a clean REST API and a good user interface. Ruby on Rails is used for the back-end, while React.js and Redux are used for the dynamic front-end. A static front-end for public resources (profiles and statuses) is also provided.
If you would like, you can [support the development of this project on Patreon][patreon]. Alternatively, you can donate to this BTC address: `17j2g7vpgHhLuXhN4bueZFCvdxxieyRVWd`
@ -25,11 +25,11 @@ If you would like, you can [support the development of this project on Patreon][
## Resources
- [List of Mastodon instances](docs/Using-Mastodon/List-of-Mastodon-instances.md)
- [List of Mastodon instances](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md)
- [Use this tool to find Twitter friends on Mastodon](https://mastodon-bridge.herokuapp.com)
- [API overview](docs/Using-the-API/API.md)
- [Frequently Asked Questions](docs/Using-Mastodon/FAQ.md)
- [List of apps](docs/Using-Mastodon/Apps.md)
- [API overview](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md)
- [Frequently Asked Questions](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md)
- [List of apps](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md)
## Features
@ -65,23 +65,54 @@ Consult the example configuration file, `.env.production.sample` for the full li
## Running with Docker and Docker-Compose
The project now includes a `Dockerfile` and a `docker-compose.yml`. You need to turn `.env.production.sample` into `.env.production` with all the variables set before you can:
[![](https://images.microbadger.com/badges/version/gargron/mastodon.svg)](https://microbadger.com/images/gargron/mastodon "Get your own version badge on microbadger.com") [![](https://images.microbadger.com/badges/image/gargron/mastodon.svg)](https://microbadger.com/images/gargron/mastodon "Get your own image badge on microbadger.com")
The project now includes a `Dockerfile` and a `docker-compose.yml` file (which requires at least docker-compose version `1.10.0`).
Review the settings in `docker-compose.yml`. Note that it is not default to store the postgresql database and redis databases in a persistent storage location,
so you may need or want to adjust the settings there.
Before running the first time, you need to build the images:
docker-compose build
And finally
Then, you need to fill in the `.env.production` file:
docker-compose up -d
cp .env.production.sample .env.production
nano .env.production
As usual, the first thing you would need to do would be to run migrations:
Do NOT change the `REDIS_*` or `DB_*` settings when running with the default docker configurations.
You will need to fill in, at least: `LOCAL_DOMAIN`, `LOCAL_HTTPS`, `PAPERCLIP_SECRET`, `SECRET_KEY_BASE`, `OTP_SECRET`, and the `SMTP_*` settings. To generate the `PAPERCLIP_SECRET`, `SECRET_KEY_BASE`, and `OTP_SECRET`, you may use:
docker-compose run --rm web rake secret
Do this once for each of those keys, and copy the result into the `.env.production` file in the appropriate field.
Then you should run the `db:migrate` command to create the database, or migrate it from an older release:
docker-compose run --rm web rails db:migrate
And since the instance running in the container will be running in production mode, you need to pre-compile assets:
Then, you will also need to precompile the assets:
docker-compose run --rm web rails assets:precompile
The container has two volumes, for the assets and for user uploads. The default docker-compose.yml maps them to the repository's `public/assets` and `public/system` directories, you may wish to put them somewhere else. Likewise, the PostgreSQL and Redis images have data containers that you may wish to map somewhere where you know how to find them and back them up.
before you can launch the docker image with:
docker-compose up
If you wish to run this as a daemon process instead of monitoring it on console, use instead:
docker-compose up -d
Then you may login to your new Mastodon instance by browsing to http://localhost:3000/
Following that, make sure that you read the [production guide](docs/Running-Mastodon/Production-guide.md). You are probably going to want to understand how
to configure Nginx to make your Mastodon instance available to the rest of the world.
The container has two volumes, for the assets and for user uploads, and optionally two more, for the postgresql and redis databases.
The default docker-compose.yml maps them to the repository's `public/assets` and `public/system` directories, you may wish to put them somewhere else. Likewise, the PostgreSQL and Redis images have data containers that you may wish to map somewhere where you know how to find them and back them up.
**Note**: The `--rm` option for docker-compose will remove the container that is created to run a one-off command after it completes. As data is stored in volumes it is not affected by that container clean-up.
@ -101,33 +132,33 @@ Running any of these tasks via docker-compose would look like this:
This approach makes updating to the latest version a real breeze.
git pull
To pull down the updates, re-run
docker-compose build
And finally,
docker-compose up -d
Which will re-create the updated containers, leaving databases and data as is. Depending on what files have been updated, you might need to re-run migrations and asset compilation.
1. `git pull` to download updates from the repository
2. `docker-compose build` to compile the Docker image out of the changed source files
3. (optional) `docker-compose run --rm web rails db:migrate` to perform database migrations. Does nothing if your database is up to date
4. (optional) `docker-compose run --rm web rails assets:precompile` to compile new JS and CSS assets
5. `docker-compose up -d` to re-create (restart) containers and pick up the changes
## Deployment without Docker
Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](docs/Running-Mastodon/Production-guide.md) for examples, configuration and instructions.
Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Production-guide.md) for examples, configuration and instructions.
## Deployment on Scalingo
[![Deploy on Scalingo](https://cdn.scalingo.com/deploy/button.svg)](https://my.scalingo.com/deploy?source=https://github.com/tootsuite/mastodon#master)
[You can view a guide for deployment on Scalingo here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Scalingo-guide.md)
## Deployment on Heroku (experimental)
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)
Mastodon can theoretically run indefinitely on a free [Heroku](https://heroku.com) app. [You can view a guide for deployment on Heroku here.](docs/Running-Mastodon/Heroku-guide.md)
Mastodon can run on [Heroku](https://heroku.com), but it gets expensive and impractical due to how Heroku prices resource usage. [You can view a guide for deployment on Heroku here](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Heroku-guide.md), but you have been warned.
## Development with Vagrant
A quick way to get a development environment up and running is with Vagrant. You will need recent versions of [Vagrant](https://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) installed.
[You can find the guide for setting up a Vagrant development environment here.](docs/Running-Mastodon/Vagrant-guide.md)
[You can find the guide for setting up a Vagrant development environment here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Vagrant-guide.md)
## Contributing

24
Vagrantfile vendored
View file

@ -46,12 +46,12 @@ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
export PATH="$HOME/.rbenv/bin::$PATH"
eval "$(rbenv init -)"
echo "Compiling Ruby 2.3.1: warning, this takes a while!!!"
rbenv install 2.3.1
rbenv global 2.3.1
cd /vagrant
echo "Compiling Ruby $(cat .ruby-version): warning, this takes a while!!!"
rbenv install $(cat .ruby-version)
rbenv global $(cat .ruby-version)
# Configure database
sudo -u postgres createuser -U postgres vagrant -s
sudo -u postgres createdb -U postgres mastodon_development
@ -84,6 +84,16 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.provider :virtualbox do |vb|
vb.name = "mastodon"
vb.customize ["modifyvm", :id, "--memory", "1024"]
# Disable VirtualBox DNS proxy to skip long-delay IPv6 resolutions.
# https://github.com/mitchellh/vagrant/issues/1172
vb.customize ["modifyvm", :id, "--natdnsproxy1", "off"]
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "off"]
# Use "virtio" network interfaces for better performance.
vb.customize ["modifyvm", :id, "--nictype1", "virtio"]
vb.customize ["modifyvm", :id, "--nictype2", "virtio"]
end
config.vm.hostname = "mastodon.dev"
@ -91,12 +101,14 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# This uses the vagrant-hostsupdater plugin, and lets you
# access the development site at http://mastodon.dev.
# To install:
# $ vagrant plugin install hostsupdater
# $ vagrant plugin install vagrant-hostsupdater
if defined?(VagrantPlugins::HostsUpdater)
config.vm.network :private_network, ip: "192.168.42.42"
config.vm.network :private_network, ip: "192.168.42.42", nictype: "virtio"
config.hostsupdater.remove_on_suspend = false
end
config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'vers=3', 'tcp']
# Otherwise, you can access the site at http://localhost:3000
config.vm.network :forwarded_port, guest: 80, host: 3000

View file

@ -26,6 +26,10 @@
"description": "The secret key base",
"generator": "secret"
},
"OTP_SECRET": {
"description": "One-time password secret",
"generator": "secret"
},
"SINGLE_USER_MODE": {
"description": "Should the instance run in single user mode? (Disable registrations, redirect to front page)",
"value": "false",
@ -75,6 +79,18 @@
"SMTP_FROM_ADDRESS": {
"description": "Address to send emails from",
"required": false
},
"SMTP_AUTH_METHOD": {
"description": "Authentication method to use with SMTP server. Default is 'plain'.",
"required": false
},
"SMTP_OPENSSL_VERIFY_MODE": {
"description": "SMTP server certificate verification mode. Defaults is 'peer'.",
"required": false
},
"SMTP_ENABLE_STARTTLS_AUTO": {
"description": "Enable STARTTLS if SMTP server supports it? Default is true.",
"required": false
}
},
"buildpacks": [

Binary file not shown.

Before

Width:  |  Height:  |  Size: 874 KiB

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 59 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" height="1000" width="1000"><g fill="#189efc"><path d="M500 0A500 500 0 0 0 0 500a500 500 0 0 0 500 500 500 500 0 0 0 500-500A500 500 0 0 0 500 0zm-2.5 271.1h107.24c-20.56 14.471-27.24 57.064-27.24 78.927v202.145c0 43.726-35.202 78.928-80 78.928s-80-35.202-80-78.928V350.027c0-43.725 35.202-78.927 80-78.927zm-276 48.9c44.798 0 80 35.202 80 78.928v202.144c0 21.863 6.68 64.456 27.24 78.928H221.5c-44.798 0-80-35.202-80-78.928V398.928c0-43.726 35.202-78.928 80-78.928zm550.24 0c44.799 0 80 35.202 80 78.928v202.144c0 43.726-35.201 78.928-80 78.928H664.5c20.56-14.472 27.24-57.065 27.24-78.928V398.928c0-43.726 35.202-78.928 80-78.928z"/><g transform="translate(-2)"><circle cx="223.5" cy="410.5" r="27.5"/><circle cx="223.5" cy="500.5" r="27.5"/><circle cx="223.5" cy="590.5" r="27.5"/></g><g transform="matrix(1 0 0 -1 274 951)"><circle cx="223.5" cy="410.5" r="27.5"/><circle cx="223.5" cy="500.5" r="27.5"/><circle cx="223.5" cy="590.5" r="27.5"/></g><g transform="matrix(-1 0 0 1 995 0)"><circle cx="223.5" cy="410.5" r="27.5"/><circle cx="223.5" cy="500.5" r="27.5"/><circle cx="223.5" cy="590.5" r="27.5"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -2,6 +2,8 @@ import api from '../api';
import { updateTimeline } from './timelines';
import * as emojione from 'emojione';
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
@ -72,9 +74,8 @@ export function mentionCompose(account, router) {
export function submitCompose() {
return function (dispatch, getState) {
dispatch(submitComposeRequest());
api(getState).post('/api/v1/statuses', {
status: getState().getIn(['compose', 'text'], ''),
status: emojione.shortnameToUnicode(getState().getIn(['compose', 'text'], '')),
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')),
sensitive: getState().getIn(['compose', 'sensitive']),

View file

@ -50,6 +50,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
};
};
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
export function refreshNotifications() {
return (dispatch, getState) => {
dispatch(refreshNotificationsRequest());
@ -61,6 +63,8 @@ export function refreshNotifications() {
params.since_id = ids.first().get('id');
}
params.exclude_types = excludeTypesFromSettings(getState());
api(getState).get('/api/v1/notifications', { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
@ -105,11 +109,11 @@ export function expandNotifications() {
dispatch(expandNotificationsRequest());
api(getState).get(url, {
params: {
limit: 5
}
}).then(response => {
const params = {};
params.exclude_types = excludeTypesFromSettings(getState());
api(getState).get(url, params).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));

View file

@ -5,10 +5,10 @@ export function showOnboardingOnce() {
return (dispatch, getState) => {
const alreadySeen = getState().getIn(['settings', 'onboarded']);
if (!alreadySeen) {
//if (!alreadySeen) {
dispatch(openModal('ONBOARDING'));
dispatch(changeSetting(['onboarded'], true));
dispatch(saveSettings());
}
//}
};
};

View file

@ -7,7 +7,8 @@ export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST';
export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS';
export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL';
export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE';
export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE';
export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
export function initReport(account, status) {
return {
@ -62,3 +63,10 @@ export function submitReportFail(error) {
error
};
};
export function changeReportComment(comment) {
return {
type: REPORT_COMMENT_CHANGE,
comment
};
};

View file

@ -1,5 +1,5 @@
import axios from 'axios';
import LinkHeader from 'http-link-header';
import LinkHeader from './link_header';
export const getLinks = response => {
const value = response.headers.link;

View file

@ -65,7 +65,7 @@ const Account = React.createClass({
<div className='account'>
<div style={{ display: 'flex' }}>
<Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
<div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={36} /></div>
<div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={36} /></div>
<DisplayName account={account} />
</Permalink>

View file

@ -1,103 +1,18 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
// From: http://stackoverflow.com/a/18320662
const resample = (canvas, width, height, resize_canvas) => {
let width_source = canvas.width;
let height_source = canvas.height;
width = Math.round(width);
height = Math.round(height);
let ratio_w = width_source / width;
let ratio_h = height_source / height;
let ratio_w_half = Math.ceil(ratio_w / 2);
let ratio_h_half = Math.ceil(ratio_h / 2);
let ctx = canvas.getContext("2d");
let img = ctx.getImageData(0, 0, width_source, height_source);
let img2 = ctx.createImageData(width, height);
let data = img.data;
let data2 = img2.data;
for (let j = 0; j < height; j++) {
for (let i = 0; i < width; i++) {
let x2 = (i + j * width) * 4;
let weight = 0;
let weights = 0;
let weights_alpha = 0;
let gx_r = 0;
let gx_g = 0;
let gx_b = 0;
let gx_a = 0;
let center_y = (j + 0.5) * ratio_h;
let yy_start = Math.floor(j * ratio_h);
let yy_stop = Math.ceil((j + 1) * ratio_h);
for (let yy = yy_start; yy < yy_stop; yy++) {
let dy = Math.abs(center_y - (yy + 0.5)) / ratio_h_half;
let center_x = (i + 0.5) * ratio_w;
let w0 = dy * dy; //pre-calc part of w
let xx_start = Math.floor(i * ratio_w);
let xx_stop = Math.ceil((i + 1) * ratio_w);
for (let xx = xx_start; xx < xx_stop; xx++) {
let dx = Math.abs(center_x - (xx + 0.5)) / ratio_w_half;
let w = Math.sqrt(w0 + dx * dx);
if (w >= 1) {
// pixel too far
continue;
}
// hermite filter
weight = 2 * w * w * w - 3 * w * w + 1;
let pos_x = 4 * (xx + yy * width_source);
// alpha
gx_a += weight * data[pos_x + 3];
weights_alpha += weight;
// colors
if (data[pos_x + 3] < 255)
weight = weight * data[pos_x + 3] / 250;
gx_r += weight * data[pos_x];
gx_g += weight * data[pos_x + 1];
gx_b += weight * data[pos_x + 2];
weights += weight;
}
}
data2[x2] = gx_r / weights;
data2[x2 + 1] = gx_g / weights;
data2[x2 + 2] = gx_b / weights;
data2[x2 + 3] = gx_a / weights_alpha;
}
}
// clear and resize canvas
if (resize_canvas === true) {
canvas.width = width;
canvas.height = height;
} else {
ctx.clearRect(0, 0, width_source, height_source);
}
// draw
ctx.putImageData(img2, 0, 0);
};
const Avatar = React.createClass({
propTypes: {
src: React.PropTypes.string.isRequired,
staticSrc: React.PropTypes.string,
size: React.PropTypes.number.isRequired,
style: React.PropTypes.object,
animated: React.PropTypes.bool
animate: React.PropTypes.bool
},
getDefaultProps () {
return {
animated: true
animate: false
};
},
@ -117,38 +32,30 @@ const Avatar = React.createClass({
this.setState({ hovering: false });
},
handleLoad () {
this.canvas.width = this.image.naturalWidth;
this.canvas.height = this.image.naturalHeight;
this.canvas.getContext('2d').drawImage(this.image, 0, 0);
resample(this.canvas, this.props.size * window.devicePixelRatio, this.props.size * window.devicePixelRatio, true);
},
setImageRef (c) {
this.image = c;
},
setCanvasRef (c) {
this.canvas = c;
},
render () {
const { src, size, staticSrc, animate } = this.props;
const { hovering } = this.state;
if (this.props.animated) {
return (
<div style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px` }}>
<img src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ borderRadius: '4px' }} />
</div>
);
const style = {
...this.props.style,
width: `${size}px`,
height: `${size}px`,
backgroundSize: `${size}px ${size}px`
};
if (hovering || animate) {
style.backgroundImage = `url(${src})`;
} else {
style.backgroundImage = `url(${staticSrc})`;
}
return (
<div onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px`, position: 'relative' }}>
<img ref={this.setImageRef} onLoad={this.handleLoad} src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ position: 'absolute', top: '0', left: '0', opacity: hovering ? '1' : '0', borderRadius: '4px' }} />
<canvas ref={this.setCanvasRef} style={{ borderRadius: '4px', width: this.props.size, height: this.props.size, opacity: hovering ? '0' : '1' }} />
</div>
<div
className='avatar'
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
style={style}
/>
);
}

View file

@ -15,6 +15,7 @@ const ColumnCollapsable = React.createClass({
propTypes: {
icon: React.PropTypes.string.isRequired,
title: React.PropTypes.string,
fullHeight: React.PropTypes.number.isRequired,
children: React.PropTypes.node,
onCollapse: React.PropTypes.func
@ -39,13 +40,13 @@ const ColumnCollapsable = React.createClass({
},
render () {
const { icon, fullHeight, children } = this.props;
const { icon, title, fullHeight, children } = this.props;
const { collapsed } = this.state;
const collapsedClassName = collapsed ? 'collapsable-collapsed' : 'collapsable';
return (
<div style={{ position: 'relative' }}>
<div style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div>
<div title={`${title}`} style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div>
<Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}>
{({ opacity, height }) =>

View file

@ -3,15 +3,43 @@ import PureRenderMixin from 'react-addons-pure-render-mixin';
const ExtendedVideoPlayer = React.createClass({
propTypes: {
src: React.PropTypes.string.isRequired
src: React.PropTypes.string.isRequired,
time: React.PropTypes.number,
controls: React.PropTypes.bool.isRequired,
muted: React.PropTypes.bool.isRequired
},
mixins: [PureRenderMixin],
handleLoadedData () {
if (this.props.time) {
this.video.currentTime = this.props.time;
}
},
componentDidMount () {
this.video.addEventListener('loadeddata', this.handleLoadedData);
},
componentWillUnmount () {
this.video.removeEventListener('loadeddata', this.handleLoadedData);
},
setRef (c) {
this.video = c;
},
render () {
return (
<div>
<video src={this.props.src} autoPlay muted loop />
<div className='extended-video-player'>
<video
ref={this.setRef}
src={this.props.src}
autoPlay
muted={this.props.muted}
controls={this.props.controls}
loop={!this.props.controls}
/>
</div>
);
},

View file

@ -13,7 +13,8 @@ const IconButton = React.createClass({
activeStyle: React.PropTypes.object,
disabled: React.PropTypes.bool,
inverted: React.PropTypes.bool,
animate: React.PropTypes.bool
animate: React.PropTypes.bool,
overlay: React.PropTypes.bool
},
getDefaultProps () {
@ -21,7 +22,8 @@ const IconButton = React.createClass({
size: 18,
active: false,
disabled: false,
animate: false
animate: false,
overlay: false
};
},
@ -31,7 +33,7 @@ const IconButton = React.createClass({
e.preventDefault();
if (!this.props.disabled) {
this.props.onClick();
this.props.onClick(e);
}
},
@ -39,7 +41,7 @@ const IconButton = React.createClass({
let style = {
fontSize: `${this.props.size}px`,
width: `${this.props.size * 1.28571429}px`,
height: `${this.props.size}px`,
height: `${this.props.size * 1.28571429}px`,
lineHeight: `${this.props.size}px`,
...this.props.style
};
@ -48,13 +50,31 @@ const IconButton = React.createClass({
style = { ...style, ...this.props.activeStyle };
}
const classes = ['icon-button'];
if (this.props.active) {
classes.push('active');
}
if (this.props.disabled) {
classes.push('disabled');
}
if (this.props.inverted) {
classes.push('inverted');
}
if (this.props.overlay) {
classes.push('overlayed');
}
return (
<Motion defaultStyle={{ rotate: this.props.active ? -360 : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
{({ rotate }) =>
<button
aria-label={this.props.title}
title={this.props.title}
className={`icon-button ${this.props.active ? 'active' : ''} ${this.props.disabled ? 'disabled' : ''} ${this.props.inverted ? 'inverted' : ''}`}
className={classes.join(' ')}
onClick={this.handleClick}
style={style}>
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' />

View file

@ -39,8 +39,8 @@ const spoilerSubSpanStyle = {
const spoilerButtonStyle = {
position: 'absolute',
top: '6px',
left: '8px',
top: '4px',
left: '4px',
zIndex: '100'
};
@ -232,8 +232,8 @@ const MediaGallery = React.createClass({
return (
<div style={{ ...outerStyle, height: `${this.props.height}px` }}>
<div style={spoilerButtonStyle}>
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleOpen} />
<div style={{ ...spoilerButtonStyle, display: !this.state.visible ? 'none' : 'block' }}>
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />
</div>
{children}

View file

@ -25,8 +25,10 @@ const Status = React.createClass({
onReblog: React.PropTypes.func,
onDelete: React.PropTypes.func,
onOpenMedia: React.PropTypes.func,
onOpenVideo: React.PropTypes.func,
onBlock: React.PropTypes.func,
me: React.PropTypes.number,
boostModal: React.PropTypes.bool,
muted: React.PropTypes.bool
},
@ -75,7 +77,7 @@ const Status = React.createClass({
if (status.get('media_attachments').size > 0 && !this.props.muted) {
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} />;
media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />;
} else {
media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />;
}
@ -90,7 +92,7 @@ const Status = React.createClass({
<a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px' }}>
<div className='status__avatar' style={{ position: 'absolute', left: '10px', top: '10px', width: '48px', height: '48px' }}>
<Avatar src={status.getIn(['account', 'avatar'])} size={48} />
<Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} />
</div>
<DisplayName account={status.get('account')} />

View file

@ -7,6 +7,7 @@ import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
reblog: { id: 'status.reblog', defaultMessage: 'Reblog' },
@ -28,6 +29,7 @@ const StatusActionBar = React.createClass({
onReblog: React.PropTypes.func,
onDelete: React.PropTypes.func,
onMention: React.PropTypes.func,
onMute: React.PropTypes.func,
onBlock: React.PropTypes.func,
onReport: React.PropTypes.func,
me: React.PropTypes.number.isRequired,
@ -44,8 +46,8 @@ const StatusActionBar = React.createClass({
this.props.onFavourite(this.props.status);
},
handleReblogClick () {
this.props.onReblog(this.props.status);
handleReblogClick (e) {
this.props.onReblog(this.props.status, e);
},
handleDeleteClick () {
@ -56,6 +58,10 @@ const StatusActionBar = React.createClass({
this.props.onMention(this.props.status.get('account'), this.context.router);
},
handleMuteClick () {
this.props.onMute(this.props.status.get('account'));
},
handleBlockClick () {
this.props.onBlock(this.props.status.get('account'));
},
@ -81,6 +87,7 @@ const StatusActionBar = React.createClass({
} else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
}

View file

@ -36,6 +36,7 @@ const StatusContent = React.createClass({
if (mention) {
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
link.setAttribute('title', mention.get('acct'));
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
} else if (media) {
@ -125,7 +126,7 @@ const StatusContent = React.createClass({
<div style={{ display: hidden ? 'none' : 'block', ...directionStyle }} dangerouslySetInnerHTML={content} />
</div>
);
} else {
} else if (this.props.onClick) {
return (
<div
className='status__content'
@ -135,6 +136,14 @@ const StatusContent = React.createClass({
dangerouslySetInnerHTML={content}
/>
);
} else {
return (
<div
className='status__content'
style={{ ...directionStyle }}
dangerouslySetInnerHTML={content}
/>
);
}
},

View file

@ -6,7 +6,8 @@ import { isIOS } from '../is_mobile';
const messages = defineMessages({
toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' },
toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' }
toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' },
expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' }
});
const videoStyle = {
@ -21,8 +22,8 @@ const videoStyle = {
const muteStyle = {
position: 'absolute',
top: '10px',
right: '10px',
top: '4px',
right: '4px',
color: 'white',
textShadow: "0px 1px 1px black, 1px 0px 1px black",
opacity: '0.8',
@ -54,8 +55,17 @@ const spoilerSubSpanStyle = {
const spoilerButtonStyle = {
position: 'absolute',
top: '6px',
left: '8px',
top: '4px',
left: '4px',
color: 'white',
textShadow: "0px 1px 1px black, 1px 0px 1px black",
zIndex: '100'
};
const expandButtonStyle = {
position: 'absolute',
bottom: '4px',
right: '4px',
color: 'white',
textShadow: "0px 1px 1px black, 1px 0px 1px black",
zIndex: '100'
@ -68,7 +78,8 @@ const VideoPlayer = React.createClass({
height: React.PropTypes.number,
sensitive: React.PropTypes.bool,
intl: React.PropTypes.object.isRequired,
autoplay: React.PropTypes.bool
autoplay: React.PropTypes.bool,
onOpenVideo: React.PropTypes.func.isRequired
},
getDefaultProps () {
@ -116,6 +127,11 @@ const VideoPlayer = React.createClass({
});
},
handleExpand () {
this.video.pause();
this.props.onOpenVideo(this.props.media, this.video.currentTime);
},
setRef (c) {
this.video = c;
},
@ -154,8 +170,14 @@ const VideoPlayer = React.createClass({
const { media, intl, width, height, sensitive, autoplay } = this.props;
let spoilerButton = (
<div style={spoilerButtonStyle} >
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} />
<div style={{...spoilerButtonStyle, display: !this.state.visible ? 'none' : 'block'}} >
<IconButton overlay title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} />
</div>
);
let expandButton = (
<div style={expandButtonStyle} >
<IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} />
</div>
);
@ -164,7 +186,7 @@ const VideoPlayer = React.createClass({
if (this.state.hasAudio) {
muteButton = (
<div style={muteStyle}>
<IconButton title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} />
<IconButton overlay title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} />
</div>
);
}
@ -202,6 +224,7 @@ const VideoPlayer = React.createClass({
<div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
{spoilerButton}
{muteButton}
{expandButton}
<video ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
</div>
);

View file

@ -42,11 +42,20 @@ import Report from '../features/report';
import { IntlProvider, addLocaleData } from 'react-intl';
import en from 'react-intl/locale-data/en';
import de from 'react-intl/locale-data/de';
import eo from 'react-intl/locale-data/eo';
import es from 'react-intl/locale-data/es';
import fi from 'react-intl/locale-data/fi';
import fr from 'react-intl/locale-data/fr';
import pt from 'react-intl/locale-data/pt';
import hu from 'react-intl/locale-data/hu';
import ja from 'react-intl/locale-data/ja';
import pt from 'react-intl/locale-data/pt';
import nl from 'react-intl/locale-data/nl';
import no from 'react-intl/locale-data/no';
import ru from 'react-intl/locale-data/ru';
import uk from 'react-intl/locale-data/uk';
import zh from 'react-intl/locale-data/zh';
import bg from 'react-intl/locale-data/bg';
import { localeData as zh_hk } from '../locales/zh-hk';
import getMessagesForLocale from '../locales';
import { hydrateStore } from '../actions/store';
import createStream from '../stream';
@ -59,7 +68,24 @@ const browserHistory = useRouterHistory(createBrowserHistory)({
basename: '/web'
});
addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk]);
addLocaleData([
...en,
...de,
...eo,
...es,
...fi,
...fr,
...hu,
...ja,
...pt,
...nl,
...no,
...ru,
...uk,
...zh,
...zh_hk,
...bg,
]);
const Mastodon = React.createClass({

View file

@ -26,7 +26,8 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => ({
status: getStatus(state, props.id),
me: state.getIn(['meta', 'me'])
me: state.getIn(['meta', 'me']),
boostModal: state.getIn(['meta', 'boost_modal'])
});
return mapStateToProps;
@ -38,11 +39,19 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(replyCompose(status, router));
},
onReblog (status) {
onModalReblog (status) {
dispatch(reblog(status));
},
onReblog (status, e) {
if (status.get('reblogged')) {
dispatch(unreblog(status));
} else {
dispatch(reblog(status));
if (e.shiftKey || !this.boostModal) {
this.onModalReblog(status);
} else {
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
}
}
},
@ -66,6 +75,10 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(openModal('MEDIA', { media, index }));
},
onOpenVideo (media, time) {
dispatch(openModal('VIDEO', { media, time }));
},
onBlock (account) {
dispatch(blockAccount(account.get('id')));
},

View file

@ -14,7 +14,7 @@ import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import createStream from '../../stream';
const messages = defineMessages({
title: { id: 'column.community', defaultMessage: 'Local' }
title: { id: 'column.community', defaultMessage: 'Local timeline' }
});
const mapStateToProps = state => ({

View file

@ -4,7 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
const AutosuggestAccount = ({ account }) => (
<div style={{ overflow: 'hidden' }} className='autosuggest-account'>
<div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} size={18} /></div>
<div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={18} /></div>
<DisplayName account={account} />
</div>
);

View file

@ -19,7 +19,7 @@ import TextIconButton from './text_icon_button';
const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Content warning' },
publish: { id: 'compose_form.publish', defaultMessage: 'Publish' }
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' }
});
const ComposeForm = React.createClass({
@ -83,11 +83,23 @@ const ComposeForm = React.createClass({
this.props.onChangeSpoilerText(e.target.value);
},
componentWillReceiveProps (nextProps) {
// If this is the update where we've finished uploading,
// save the last caret position so we can restore it below!
if (!nextProps.is_uploading && this.props.is_uploading) {
this._restoreCaret = this.autosuggestTextarea.textarea.selectionStart;
}
},
componentDidUpdate (prevProps) {
if (this.props.focusDate !== prevProps.focusDate) {
// If replying to zero or one users, places the cursor at the end of the textbox.
// If replying to more than one user, selects any usernames past the first;
// this provides a convenient shortcut to drop everyone else from the conversation.
// This statement does several things:
// - If we're beginning a reply, and,
// - Replying to zero or one users, places the cursor at the end of the textbox.
// - Replying to more than one user, selects any usernames past the first;
// this provides a convenient shortcut to drop everyone else from the conversation.
// - If we've just finished uploading an image, and have a saved caret position,
// restores the cursor to that position after the text changes!
if (this.props.focusDate !== prevProps.focusDate || (prevProps.is_uploading && !this.props.is_uploading && typeof this._restoreCaret === 'number')) {
let selectionEnd, selectionStart;
if (this.props.preselectDate !== prevProps.preselectDate) {
@ -118,7 +130,7 @@ const ComposeForm = React.createClass({
render () {
const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props;
const disabled = this.props.is_submitting || this.props.is_uploading;
const disabled = this.props.is_submitting;
let publishText = '';
let privacyWarning = '';

View file

@ -46,8 +46,8 @@ const EmojiPickerDropdown = React.createClass({
<img draggable="false" className="emojione" alt="🙂" src="/emoji/1f602.svg" />
</DropdownTrigger>
<DropdownContent className='dropdown__left'>
<EmojiPicker emojione={settings} onChange={this.handleChange} />
<DropdownContent className='dropdown__left light'>
<EmojiPicker emojione={settings} onChange={this.handleChange} search={true} />
</DropdownContent>
</Dropdown>
);

View file

@ -17,7 +17,7 @@ const NavigationBar = React.createClass({
render () {
return (
<div className='navigation-bar'>
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} size={40} /></Permalink>
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} animate size={40} /></Permalink>
<div style={{ flex: '1 1 auto', marginLeft: '8px' }}>
<strong style={{ fontWeight: '500', display: 'block' }}>{this.props.account.get('acct')}</strong>

View file

@ -50,7 +50,7 @@ const ReplyIndicator = React.createClass({
<div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div>
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}>
<div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} /></div>
<div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} /></div>
<DisplayName account={status.get('account')} />
</a>
</div>

View file

@ -12,7 +12,7 @@ import SearchResultsContainer from './containers/search_results_container';
const messages = defineMessages({
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Whole Known Network' },
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }

View file

@ -33,7 +33,7 @@ const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
<div>
<div style={outerStyle}>
<Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={48} /></div>
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={48} /></div>
<DisplayName account={account} />
</Permalink>

View file

@ -7,11 +7,11 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
const messages = defineMessages({
heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Whole Known Network' },
public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Sign out' },
sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }
@ -43,7 +43,7 @@ const GettingStarted = ({ intl, me }) => {
<div className='scrollable optionally-scrollable' style={{ display: 'flex', flexDirection: 'column' }}>
<div className='static-content getting-started'>
<p><FormattedMessage id='getting_started.open_source_notice' defaultMessage='Mastodon is open source software. You can contribute or report issues on github at {github}. {apps}.' values={{ github: <a href="https://github.com/tootsuite/mastodon" target="_blank">tootsuite/mastodon</a>, apps: <a href="https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/Apps.md" target="_blank"><FormattedMessage id='getting_started.apps' defaultMessage='Various apps are available' /></a> }} /></p>
<p><FormattedMessage id='getting_started.open_source_notice' defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.' values={{ github: <a href="https://github.com/tootsuite/mastodon" target="_blank">tootsuite/mastodon</a>, apps: <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md" target="_blank"><FormattedMessage id='getting_started.apps' defaultMessage='Various apps are available' /></a> }} /></p>
</div>
</div>
</Column>

View file

@ -6,7 +6,8 @@ import SettingToggle from '../../notifications/components/setting_toggle';
import SettingText from './setting_text';
const messages = defineMessages({
filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' }
filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' },
settings: { id: 'home.settings', defaultMessage: 'Column settings' }
});
const outerStyle = {
@ -39,7 +40,7 @@ const ColumnSettings = React.createClass({
const { settings, onChange, onSave, intl } = this.props;
return (
<ColumnCollapsable icon='sliders' fullHeight={209} onCollapse={onSave}>
<ColumnCollapsable icon='sliders' title={intl.formatMessage(messages.settings)} fullHeight={209} onCollapse={onSave}>
<div className='column-settings--outer' style={outerStyle}>
<span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>

View file

@ -1,21 +1,25 @@
const iconStyle = {
fontSize: '16px',
padding: '15px',
position: 'absolute',
right: '48px',
top: '0',
cursor: 'pointer',
zIndex: '2'
};
import { defineMessages, injectIntl } from 'react-intl';
const ClearColumnButton = ({ onClick }) => (
<div className='column-icon' style={iconStyle} onClick={onClick}>
<i className='fa fa-trash' />
</div>
);
const messages = defineMessages({
clear: { id: 'notifications.clear', defaultMessage: 'Clear notifications' }
});
ClearColumnButton.propTypes = {
onClick: React.PropTypes.func.isRequired
};
const ClearColumnButton = React.createClass({
export default ClearColumnButton;
propTypes: {
onClick: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
},
render () {
const { intl } = this.props;
return (
<div title={intl.formatMessage(messages.clear)} className='column-icon column-icon-clear' tabIndex='0' onClick={this.props.onClick}>
<i className='fa fa-eraser' />
</div>
);
}
})
export default injectIntl(ClearColumnButton);

View file

@ -1,9 +1,13 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnCollapsable from '../../../components/column_collapsable';
import SettingToggle from './setting_toggle';
const messages = defineMessages({
settings: { id: 'notifications.settings', defaultMessage: 'Column settings' }
});
const outerStyle = {
padding: '15px'
};
@ -23,21 +27,22 @@ const ColumnSettings = React.createClass({
propTypes: {
settings: ImmutablePropTypes.map.isRequired,
intl: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func.isRequired,
onSave: React.PropTypes.func.isRequired
onSave: React.PropTypes.func.isRequired,
},
mixins: [PureRenderMixin],
render () {
const { settings, onChange, onSave } = this.props;
const { settings, intl, onChange, onSave } = this.props;
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
return (
<ColumnCollapsable icon='sliders' fullHeight={616} onCollapse={onSave}>
<ColumnCollapsable icon='sliders' title={intl.formatMessage(messages.settings)} fullHeight={616} onCollapse={onSave}>
<div className='column-settings--outer' style={outerStyle}>
<span className='column-settings--section' style={sectionStyle}><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
@ -77,4 +82,4 @@ const ColumnSettings = React.createClass({
});
export default ColumnSettings;
export default injectIntl(ColumnSettings);

View file

@ -21,7 +21,7 @@ const Notification = React.createClass({
renderFollow (account, link) {
return (
<div className='notification'>
<div className='notification notification-follow'>
<div className='notification__message'>
<div style={{ position: 'absolute', 'left': '-26px'}}>
<i className='fa fa-fw fa-user-plus' />
@ -41,7 +41,7 @@ const Notification = React.createClass({
renderFavourite (notification, link) {
return (
<div className='notification'>
<div className='notification notification-favourite'>
<div className='notification__message'>
<div style={{ position: 'absolute', 'left': '-26px'}}>
<i className='fa fa-fw fa-star' style={{ color: '#ca8f04' }} />
@ -57,7 +57,7 @@ const Notification = React.createClass({
renderReblog (notification, link) {
return (
<div className='notification'>
<div className='notification notification-reblog'>
<div className='notification__message'>
<div style={{ position: 'absolute', 'left': '-26px'}}>
<i className='fa fa-fw fa-retweet' />
@ -76,17 +76,17 @@ const Notification = React.createClass({
const account = notification.get('account');
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
const link = <Permalink className='notification__display-name' style={linkStyle} href={account.get('url')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />;
const link = <Permalink className='notification__display-name' style={linkStyle} href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />;
switch(notification.get('type')) {
case 'follow':
return this.renderFollow(account, link);
case 'mention':
return this.renderMention(notification);
case 'favourite':
return this.renderFavourite(notification, link);
case 'reblog':
return this.renderReblog(notification, link);
case 'follow':
return this.renderFollow(account, link);
case 'mention':
return this.renderMention(notification);
case 'favourite':
return this.renderFavourite(notification, link);
case 'reblog':
return this.renderReblog(notification, link);
}
}

View file

@ -14,7 +14,7 @@ import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import createStream from '../../stream';
const messages = defineMessages({
title: { id: 'column.public', defaultMessage: 'Whole Known Network' }
title: { id: 'column.public', defaultMessage: 'Federated timeline' }
});
const mapStateToProps = state => ({

View file

@ -47,7 +47,7 @@ const Report = React.createClass({
propTypes: {
isSubmitting: React.PropTypes.bool,
account: ImmutablePropTypes.map,
statusIds: ImmutablePropTypes.list.isRequired,
statusIds: ImmutablePropTypes.orderedSet.isRequired,
comment: React.PropTypes.string.isRequired,
dispatch: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
@ -94,7 +94,8 @@ const Report = React.createClass({
return (
<Column heading={intl.formatMessage(messages.heading)} icon='flag'>
<ColumnBackButtonSlim />
<div className='report' style={{ display: 'flex', flexDirection: 'column', maxHeight: '100%', boxSizing: 'border-box' }}>
<div className='report scrollable' style={{ display: 'flex', flexDirection: 'column', maxHeight: '100%', boxSizing: 'border-box' }}>
<div className='report__target' style={{ flex: '0 0 auto', padding: '10px' }}>
<FormattedMessage id='report.target' defaultMessage='Reporting' />
<strong>{account.get('acct')}</strong>
@ -106,7 +107,7 @@ const Report = React.createClass({
</div>
</div>
<div style={{ flex: '0 0 160px', padding: '10px' }}>
<div style={{ flex: '0 0 100px', padding: '10px' }}>
<textarea
className='report__textarea'
placeholder={intl.formatMessage(messages.placeholder)}

View file

@ -37,8 +37,8 @@ const ActionBar = React.createClass({
this.props.onReply(this.props.status);
},
handleReblogClick () {
this.props.onReblog(this.props.status);
handleReblogClick (e) {
this.props.onReblog(this.props.status, e);
},
handleFavouriteClick () {

View file

@ -17,7 +17,8 @@ const DetailedStatus = React.createClass({
propTypes: {
status: ImmutablePropTypes.map.isRequired,
onOpenMedia: React.PropTypes.func.isRequired
onOpenMedia: React.PropTypes.func.isRequired,
onOpenVideo: React.PropTypes.func.isRequired,
},
mixins: [PureRenderMixin],
@ -39,7 +40,7 @@ const DetailedStatus = React.createClass({
if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} autoplay />;
media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} onOpenVideo={this.props.onOpenVideo} autoplay />;
} else {
media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} />;
}
@ -54,7 +55,7 @@ const DetailedStatus = React.createClass({
return (
<div style={{ padding: '14px 10px' }} className='detailed-status'>
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} size={48} /></div>
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /></div>
<DisplayName account={status.get('account')} />
</a>

View file

@ -38,7 +38,8 @@ const makeMapStateToProps = () => {
status: getStatus(state, Number(props.params.statusId)),
ancestorsIds: state.getIn(['timelines', 'ancestors', Number(props.params.statusId)]),
descendantsIds: state.getIn(['timelines', 'descendants', Number(props.params.statusId)]),
me: state.getIn(['meta', 'me'])
me: state.getIn(['meta', 'me']),
boostModal: state.getIn(['meta', 'boost_modal'])
});
return mapStateToProps;
@ -55,7 +56,8 @@ const Status = React.createClass({
status: ImmutablePropTypes.map,
ancestorsIds: ImmutablePropTypes.list,
descendantsIds: ImmutablePropTypes.list,
me: React.PropTypes.number
me: React.PropTypes.number,
boostModal: React.PropTypes.bool
},
mixins: [PureRenderMixin],
@ -82,11 +84,19 @@ const Status = React.createClass({
this.props.dispatch(replyCompose(status, this.context.router));
},
handleReblogClick (status) {
handleModalReblog (status) {
this.props.dispatch(reblog(status));
},
handleReblogClick (status, e) {
if (status.get('reblogged')) {
this.props.dispatch(unreblog(status));
} else {
this.props.dispatch(reblog(status));
if (e.shiftKey || !this.props.boostModal) {
this.handleModalReblog(status);
} else {
this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
}
}
},
@ -102,6 +112,10 @@ const Status = React.createClass({
this.props.dispatch(openModal('MEDIA', { media, index }));
},
handleOpenVideo (media, time) {
this.props.dispatch(openModal('VIDEO', { media, time }));
},
handleReport (status) {
this.props.dispatch(initReport(status.get('account'), status));
},
@ -141,7 +155,7 @@ const Status = React.createClass({
<div className='scrollable'>
{ancestors}
<DetailedStatus status={status} me={me} onOpenMedia={this.handleOpenMedia} />
<DetailedStatus status={status} me={me} onOpenVideo={this.handleOpenVideo} onOpenMedia={this.handleOpenMedia} />
<ActionBar status={status} me={me} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onDelete={this.handleDeleteClick} onMention={this.handleMentionClick} onReport={this.handleReport} />
{descendants}

View file

@ -0,0 +1,77 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import IconButton from '../../../components/icon_button';
import Button from '../../../components/button';
import StatusContent from '../../../components/status_content';
import Avatar from '../../../components/avatar';
import RelativeTimestamp from '../../../components/relative_timestamp';
import DisplayName from '../../../components/display_name';
const messages = defineMessages({
reblog: { id: 'status.reblog', defaultMessage: 'Boost' }
});
const BoostModal = React.createClass({
contextTypes: {
router: React.PropTypes.object
},
propTypes: {
status: ImmutablePropTypes.map.isRequired,
onReblog: React.PropTypes.func.isRequired,
onClose: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
handleReblog() {
this.props.onReblog(this.props.status);
this.props.onClose();
},
handleAccountClick (e) {
if (e.button === 0) {
e.preventDefault();
this.props.onClose();
this.context.router.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
}
},
render () {
const { status, intl, onClose } = this.props;
return (
<div className='modal-root__modal boost-modal'>
<div className='boost-modal__container'>
<div className='status light'>
<div style={{ fontSize: '15px' }}>
<div style={{ float: 'right', fontSize: '14px' }}>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
</div>
<a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px' }}>
<div className='status__avatar' style={{ position: 'absolute', left: '10px', top: '10px', width: '48px', height: '48px' }}>
<Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} />
</div>
<DisplayName account={status.get('account')} />
</a>
</div>
<StatusContent status={status} />
</div>
</div>
<div className='boost-modal__action-bar'>
<div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <i className='fa fa-retweet' /></span> }} /></div>
<Button text={intl.formatMessage(messages.reblog)} onClick={this.handleReblog} />
</div>
</div>
);
}
});
export default injectIntl(BoostModal);

View file

@ -41,8 +41,11 @@ const Column = React.createClass({
mixins: [PureRenderMixin],
handleHeaderClick () {
let node = ReactDOM.findDOMNode(this);
this._interruptScrollAnimation = scrollTop(node.querySelector('.scrollable'));
const scrollable = ReactDOM.findDOMNode(this).querySelector('.scrollable');
if (!scrollable) {
return;
}
this._interruptScrollAnimation = scrollTop(scrollable);
},
handleWheel () {

View file

@ -111,7 +111,7 @@ const MediaModal = React.createClass({
if (attachment.get('type') === 'image') {
content = <ImageLoader src={url} imgProps={{ style: { display: 'block' } }} />;
} else if (attachment.get('type') === 'gifv') {
content = <ExtendedVideoPlayer src={url} />;
content = <ExtendedVideoPlayer src={url} muted={true} controls={false} />;
}
return (

View file

@ -1,11 +1,15 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import MediaModal from './media_modal';
import OnboardingModal from './onboarding_modal';
import VideoModal from './video_modal';
import BoostModal from './boost_modal';
import { TransitionMotion, spring } from 'react-motion';
const MODAL_COMPONENTS = {
'MEDIA': MediaModal,
'ONBOARDING': OnboardingModal
'ONBOARDING': OnboardingModal,
'VIDEO': VideoModal,
'BOOST': BoostModal
};
const ModalRoot = React.createClass({

View file

@ -6,47 +6,52 @@ import Permalink from '../../../components/permalink';
const PageOne = ({ acct, domain }) => (
<div className='onboarding-modal__page onboarding-modal__page-one'>
<h1><FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to Mastodon!' /></h1>
<p><FormattedMessage id='onboarding.page_one.federation' defaultMessage='Mastodon is a decentralized federation of {instances} linking up and forming one larger social network.' values={{ instances: <a href='https://instances.mastodon.xyz' target='_blank' rel='noopener'><FormattedMessage id='onboarding.page_one.different_instances' defaultMessage='different server instances' /></a> }} /></p>
<p><FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, your full handle is {handle}' values={{ domain: <strong>{domain}</strong>, handle: <strong>@{acct}@{domain}</strong> }}/></p>
<div style={{ flex: '0 0 auto' }}>
<div className='onboarding-modal__page-one__elephant-friend' />
</div>
<div>
<h1><FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to Mastodon!' /></h1>
<p><FormattedMessage id='onboarding.page_one.federation' defaultMessage='Mastodon is a decentralized federation of different server instances linking up and forming one larger social network.' /></p>
<p><FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, so your full handle is {handle}' values={{ domain: domain, handle: <strong>{acct}@{domain}</strong> }}/></p>
</div>
</div>
);
const PageTwo = (
const PageTwo = () => (
<div className='onboarding-modal__page onboarding-modal__page-two'>
<img className="onboarding-modal__image onboard-compose" src={'/onboarding/onboard-compose.jpg'}></img>
<img className="onboarding-modal__image onboard-compose" src={'/onboarding/onboard-compose.jpg'} />
<p><FormattedMessage id='onboarding.page_two.compose' defaultMessage='Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.' /></p>
</div>
);
const PageThree = (
const PageThree = () => (
<div className='onboarding-modal__page onboarding-modal__page-three'>
<img className="onboarding-modal__image onboard-compose" src={'/onboarding/onboard-search.jpg'}></img>
<img className="onboarding-modal__image onboard-compose" src={'/onboarding/onboard-search.jpg'} />
<p><FormattedMessage id='onboarding.page_three.search' defaultMessage='Use the search bar to find users and look at hashtags, such as #MastoArt and #Introductions.' /></p>
<p><FormattedMessage id='onboarding.page_three.profile' defaultMessage='Click "Edit Profile" to change your avatar, bio, and display name.' /></p>
<p><FormattedMessage id='onboarding.page_three.profile' defaultMessage='Click "Edit Profile" to change your avatar, bio, and display name.' /></p>
</div>
);
const PageFour = (
const PageFour = () => (
<div className='onboarding-modal__page onboarding-modal__page-four'>
<img className="onboarding-modal__image onboard-column" src={'/onboarding/onboard-home.jpg'}></img>
<p><FormattedMessage id='onboarding.page_four.home' defaultMessage='The Home Timeline shows posts from users you follow.'/></p>
<img className="onboarding-modal__image onboard-column" src={"/onboarding/onboard-notifications.jpg"}></img>
<p><FormattedMessage id='onboarding.page_four.notifications' defaultMessage='The Notifications Column shows when a user boosts, favorites, or replies to your posts; and when you have a new follower.' /></p>
<p><FormattedMessage id='onboarding.page_four.filter' defaultMessage='Each column can be customized using the settings menu in the top right.' /><img className="onboard-sliders" src={"/onboarding/onboard-sliders.png"}></img></p>
<img className="onboarding-modal__image onboard-column" src={'/onboarding/onboard-home.jpg'} />
<p><FormattedMessage id='onboarding.page_four.home' defaultMessage='The Home Timeline shows posts from users you follow.'/></p>
<img className="onboarding-modal__image onboard-column" src={"/onboarding/onboard-notifications.jpg"} />
<p><FormattedMessage id='onboarding.page_four.notifications' defaultMessage='The Notifications Column shows when a user boosts, favorites, or replies to your posts; and when you have a new follower.' /></p>
<p><FormattedMessage id='onboarding.page_four.filter' defaultMessage='Each column can be customized using the settings menu in the top right.' /><img className="onboard-sliders" src={"/onboarding/onboard-sliders.png"}></img></p>
</div>
);
const PageFive = (
const PageFive = () => (
<div className='onboarding-modal__page onboarding-modal__page-five'>
<img className="onboarding-modal__image onboard-column" src={"/onboarding/onboard-getting-started.jpg"}></img>
<p><FormattedMessage id='onboarding.page_five.getting-started' defaultMessage='The Getting Started Column changes based on your needs.'/></p>
<img className="onboarding-modal__image onboard-column" src={"/onboarding/onboard-local-timeline.jpg"}></img>
<p><FormattedMessage id='onboarding.page_five.local-timeline' defaultMessage='The Local Timeline shows public posts from every user on your instance.' /></p>
<img className="onboarding-modal__image onboard-column" src={"/onboarding/onboard-federated-timeline.jpg"}></img>
<p><FormattedMessage id='onboarding.page_five.federated-timeline' defaultMessage='The Federated Timeline shows public posts from the whole known network of instances.' /></p>
<p><FormattedMessage id='onboarding.page_five.public' defaultMessage='These are the Public Timelines, a great way to find people to follow.' /></p>
<img className="onboarding-modal__image onboard-column" src={"/onboarding/onboard-getting-started.jpg"} />
<p><FormattedMessage id='onboarding.page_five.getting-started' defaultMessage='The Getting Started Column changes based on your needs.'/></p>
<img className="onboarding-modal__image onboard-column" src={"/onboarding/onboard-local-timeline.jpg"} />
<p><FormattedMessage id='onboarding.page_five.local-timeline' defaultMessage='The Local Timeline shows public posts from every user on your instance.' /></p>
<img className="onboarding-modal__image onboard-column" src={"/onboarding/onboard-federated-timeline.jpg"} />
<p><FormattedMessage id='onboarding.page_five.federated-timeline' defaultMessage='The Federated Timeline shows public posts from the whole known network of instances.' /></p>
<p><FormattedMessage id='onboarding.page_five.public' defaultMessage='These are the Public Timelines, a great way to find people to follow.' /></p>
</div>
);
@ -63,8 +68,6 @@ const PageSix = ({ admin }) => (
</div>
);
const mapStateToProps = state => ({
me: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]),
@ -114,10 +117,10 @@ const OnboardingModal = React.createClass({
const pages = [
<PageOne acct={me.get('acct')} domain={domain} />,
PageTwo,
PageThree,
PageFour,
PageFive,
<PageTwo />,
<PageThree />,
<PageFour />,
<PageFive />,
<PageSix admin={admin} />
];

View file

@ -0,0 +1,47 @@
import LoadingIndicator from '../../../components/loading_indicator';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ExtendedVideoPlayer from '../../../components/extended_video_player';
import { defineMessages, injectIntl } from 'react-intl';
import IconButton from '../../../components/icon_button';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' }
});
const closeStyle = {
position: 'absolute',
zIndex: '100',
top: '4px',
right: '4px'
};
const VideoModal = React.createClass({
propTypes: {
media: ImmutablePropTypes.map.isRequired,
time: React.PropTypes.number,
onClose: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
render () {
const { media, intl, time, onClose } = this.props;
const url = media.get('url');
return (
<div className='modal-root__modal media-modal'>
<div>
<div style={closeStyle}><IconButton title={intl.formatMessage(messages.close)} icon='times' overlay onClick={onClose} /></div>
<ExtendedVideoPlayer src={url} muted={false} controls={true} time={time} />
</div>
</div>
);
}
});
export default injectIntl(VideoModal);

View file

@ -0,0 +1,33 @@
import Link from 'http-link-header';
import querystring from 'querystring';
Link.parseAttrs = (link, parts) => {
let match = null
let attr = ''
let value = ''
let attrs = ''
let uriAttrs = /<(.*)>;\s*(.*)/gi.exec(parts)
if(uriAttrs) {
attrs = uriAttrs[2]
link = Link.parseParams(link, uriAttrs[1])
}
while(match = Link.attrPattern.exec(attrs)) {
attr = match[1].toLowerCase()
value = match[4] || match[3] || match[2]
if( /\*$/.test(attr)) {
Link.setAttr(link, attr, Link.parseExtendedValue(value))
} else if(/%/.test(value)) {
Link.setAttr(link, attr, querystring.decode(value))
} else {
Link.setAttr(link, attr, value)
}
}
return link
};
export default Link;

View file

@ -0,0 +1,68 @@
const bg = {
"column_back_button.label": "Назад",
"lightbox.close": "Затвори",
"loading_indicator.label": "Зареждане...",
"status.mention": "Споменаване",
"status.delete": "Изтриване",
"status.reply": "Отговор",
"status.reblog": "Споделяне",
"status.favourite": "Предпочитани",
"status.reblogged_by": "{name} сподели",
"status.sensitive_warning": "Деликатно съдържание",
"status.sensitive_toggle": "Покажи",
"video_player.toggle_sound": "Звук",
"account.mention": "Споменаване",
"account.edit_profile": "Редактирай профила си",
"account.unblock": "Не блокирай",
"account.unfollow": "Не следвай",
"account.block": "Блокирай",
"account.follow": "Последвай",
"account.posts": "Публикации",
"account.follows": "Следвам",
"account.followers": "Последователи",
"account.follows_you": "Твой последовател",
"account.requested": "В очакване на одобрение",
"getting_started.heading": "Първи стъпки",
"getting_started.about_addressing": "Можеш да последваш потребител, ако знаеш потребителското му име и домейна, на който се намира, като в полето за търсене ги въведеш по този начин: име@домейн",
"getting_started.about_shortcuts": "Ако с търсения потребител се намирате на един и същ домейн, достатъчно е да въведеш само името. Същото важи и за споменаване на хора в публикации.",
"getting_started.about_developer": "Можеш да потърсиш разработчика на този проект като: Gargron@mastodon.social",
"getting_started.open_source_notice": "Mastodon е софтуер с отворен код. Можеш да помогнеш или да докладваш за проблеми в Github: {github}.",
"column.home": "Начало",
"column.mentions": "Споменавания",
"column.public": "Публичен канал",
"column.notifications": "Известия",
"tabs_bar.compose": "Съставяне",
"tabs_bar.home": "Начало",
"tabs_bar.mentions": "Споменавания",
"tabs_bar.public": "Публичен канал",
"tabs_bar.notifications": "Известия",
"compose_form.placeholder": "Какво си мислиш?",
"compose_form.publish": "Раздумай",
"compose_form.sensitive": "Отбележи съдържанието като деликатно",
"compose_form.spoiler": "Скрий текста зад предупреждение",
"compose_form.private": "Отбележи като поверително",
"compose_form.privacy_disclaimer": "Поверителни публикации ще бъдат изпратени до споменатите потребители на {domains}. Доверяваш ли се на {domainsCount, plural, one {that server} other {those servers}}, че няма да издаде твоята публикация?",
"compose_form.unlisted": "Не показвай в публичния канал",
"navigation_bar.edit_profile": "Редактирай профил",
"navigation_bar.preferences": "Предпочитания",
"navigation_bar.public_timeline": "Публичен канал",
"navigation_bar.logout": "Излизане",
"reply_indicator.cancel": "Отказ",
"search.placeholder": "Търсене",
"search.account": "Акаунт",
"search.hashtag": "Хаштаг",
"upload_button.label": "Добави медия",
"upload_form.undo": "Отмяна",
"notification.follow": "{name} те последва",
"notification.favourite": "{name} хареса твоята публикация",
"notification.reblog": "{name} сподели твоята публикация",
"notification.mention": "{name} те спомена",
"notifications.column_settings.alert": "Десктоп известия",
"notifications.column_settings.show": "Покажи в колона",
"notifications.column_settings.follow": "Нови последователи:",
"notifications.column_settings.favourite": "Предпочитани:",
"notifications.column_settings.mention": "Споменавания:",
"notifications.column_settings.reblog": "Споделяния:",
};
export default bg;

View file

@ -1,15 +1,15 @@
const en = {
const de = {
"column_back_button.label": "Zurück",
"lightbox.close": "Schließen",
"loading_indicator.label": "Lade...",
"loading_indicator.label": "Lade",
"status.mention": "Erwähnen",
"status.delete": "Löschen",
"status.reply": "Antworten",
"status.reblog": "Teilen",
"status.favourite": "Favorisieren",
"status.reblogged_by": "{name} teilte",
"status.sensitive_warning": "Sensible Inhalte",
"status.sensitive_toggle": "Klicken um zu zeigen",
"status.sensitive_warning": "Heikle Inhalte",
"status.sensitive_toggle": "Klicke, um sie zu sehen",
"status.open": "Öffnen",
"video_player.toggle_sound": "Ton umschalten",
"account.mention": "Erwähnen",
@ -20,17 +20,17 @@ const en = {
"account.follow": "Folgen",
"account.posts": "Beiträge",
"account.follows": "Folgt",
"account.followers": "Folger",
"account.followers": "Folgende",
"account.follows_you": "Folgt dir",
"account.requested": "Warte auf Erlaubnis",
"getting_started.heading": "Erste Schritte",
"getting_started.about_addressing": "Du kannst Leuten folgen, falls du ihren Nutzernamen und ihre Domain kennst, in dem du eine e-mail-artige Addresse in das Suchfeld oben an der Seite eingibst.",
"getting_started.about_shortcuts": "Falls der Zielnutzer an derselben Domain ist wie du, funktioniert der Nutzername auch alleine. Das gilt auch für Erwähnungen in Beiträgen.",
"getting_started.about_addressing": "Du kannst Leuten folgen, falls du ihren Nutzernamen und ihre Domain kennst, in dem du eine e-mail-artige Addresse in das Suchfeld oben auf der Seite eingibst.",
"getting_started.about_shortcuts": "Falls die Person auf derselben Domain ist wie du, reicht auch ihr Nutzername alleine. Das gilt auch für Erwähnungen in Beiträgen.",
"getting_started.about_developer": "Der Entwickler des Projekts kann unter Gargron@mastodon.social gefunden werden",
"getting_started.open_source_notice": "Mastodon ist quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.",
"column.home": "Home",
"column.mentions": "Erwähnungen",
"column.public": "Gesamtes Bekanntes Netz",
"column.public": "Gesamtes bekanntes Netz",
"column.notifications": "Mitteilungen",
"column.follow_requests": "Folgeanfragen",
"tabs_bar.compose": "Schreiben",
@ -38,11 +38,11 @@ const en = {
"tabs_bar.mentions": "Erwähnungen",
"tabs_bar.public": "Gesamtes Netz",
"tabs_bar.notifications": "Mitteilungen",
"compose_form.placeholder": "Worüber möchstest du schreiben?",
"compose_form.placeholder": "Worüber möchtest du schreiben?",
"compose_form.publish": "Tröt",
"compose_form.sensitive": "Medien als sensitiv markieren",
"compose_form.unlisted": "Öffentlich nicht auflisten",
"compose_form.sensitive": "Medien als heikel markieren",
"compose_form.private": "Als privat markieren",
"compose_form.unlisted": "Nicht öffentlich auflisten",
"navigation_bar.edit_profile": "Profil bearbeiten",
"navigation_bar.preferences": "Einstellungen",
"navigation_bar.public_timeline": "Öffentlich",
@ -52,15 +52,15 @@ const en = {
"search.placeholder": "Suche",
"search.account": "Konto",
"search.hashtag": "Hashtag",
"upload_button.label": "Media-Datei anfügen",
"upload_button.label": "Mediendatei hinzufügen",
"upload_form.undo": "Entfernen",
"notification.follow": "{name} folgt dir",
"notification.favourite": "{name} favorisierte deinen Status",
"notification.reblog": "{name} teilte deinen Status",
"notification.mention": "{name} erwähnte dich",
"notifications.column_settings.alert": "Desktop-Benachrichtigunen",
"notifications.column_settings.alert": "Desktop-Benachrichtigungen",
"notifications.column_settings.show": "In der Spalte anzeigen",
"notifications.column_settings.follow": "Neue Folger:",
"notifications.column_settings.follow": "Neue Folgende:",
"notifications.column_settings.favourite": "Favorisierungen:",
"notifications.column_settings.mention": "Erwähnungen:",
"notifications.column_settings.reblog": "Geteilte Beiträge:",
@ -74,4 +74,4 @@ const en = {
"missing_indicator.label": "Nicht gefunden"
};
export default en;
export default de;

View file

@ -1,68 +1,131 @@
/**
* Note for Contributors:
* This file (en.jsx) serve as a template for other languages.
* To make other contributors' life easier, please REMEMBER:
* 1. to add your new string here; and
* 2. to remove old strings that are no longer needed; and
* 3. to sort the strings by the key.
* 4. To rename the `en` const name and export default name to match your locale.
* Thanks!
*/
const en = {
"column_back_button.label": "Back",
"lightbox.close": "Close",
"loading_indicator.label": "Loading...",
"status.mention": "Mention @{name}",
"status.delete": "Delete",
"status.reply": "Reply",
"status.reblog": "Boost",
"status.favourite": "Favourite",
"status.reblogged_by": "{name} boosted",
"status.sensitive_warning": "Sensitive content",
"status.sensitive_toggle": "Click to view",
"video_player.toggle_sound": "Toggle sound",
"account.mention": "Mention @{name}",
"account.edit_profile": "Edit profile",
"account.unblock": "Unblock @{name}",
"account.unfollow": "Unfollow",
"account.block": "Block @{name}",
"account.disclaimer": "This user is from another instance. This number may be larger.",
"account.edit_profile": "Edit profile",
"account.follow": "Follow",
"account.posts": "Posts",
"account.follows": "Follows",
"account.followers": "Followers",
"account.follows_you": "Follows you",
"account.follows": "Follows",
"account.mention": "Mention @{name}",
"account.mute": "Mute @{name}",
"account.posts": "Posts",
"account.report": "Report @{name}",
"account.requested": "Awaiting approval",
"getting_started.heading": "Getting started",
"getting_started.about_addressing": "You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the search form.",
"getting_started.about_shortcuts": "If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.",
"getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on github at {github}. {apps}.",
"column.home": "Home",
"account.unblock": "Unblock @{name}",
"account.unfollow": "Unfollow",
"account.unmute": "Unmute @{name}",
"boost_modal.combo": "You can press {combo} to skip this next time",
"column_back_button.label": "Back",
"column.blocks": "Blocked users",
"column.community": "Local timeline",
"column.public": "Federated timeline",
"column.favourites": "Favourites",
"column.follow_requests": "Follow requests",
"column.home": "Home",
"column.notifications": "Notifications",
"tabs_bar.compose": "Compose",
"tabs_bar.home": "Home",
"tabs_bar.mentions": "Mentions",
"tabs_bar.public": "Federated timeline",
"tabs_bar.notifications": "Notifications",
"column.public": "Federated timeline",
"compose_form.placeholder": "What is on your mind?",
"compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.",
"compose_form.publish": "Toot",
"compose_form.sensitive": "Mark media as sensitive",
"compose_form.spoiler_placeholder": "Content warning",
"compose_form.spoiler": "Hide text behind warning",
"compose_form.private": "Mark as private",
"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.unlisted": "Do not display on public timelines",
"navigation_bar.edit_profile": "Edit profile",
"navigation_bar.preferences": "Preferences",
"emoji_button.label": "Insert emoji",
"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.public_timeline": "the public timeline",
"empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.",
"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.apps": "Various apps are available",
"getting_started.heading": "Getting started",
"getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.",
"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",
"lightbox.close": "Close",
"loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Not found",
"navigation_bar.blocks": "Blocked users",
"navigation_bar.community_timeline": "Local timeline",
"navigation_bar.public_timeline": "Federated timeline",
"navigation_bar.edit_profile": "Edit profile",
"navigation_bar.favourites": "Favourites",
"navigation_bar.follow_requests": "Follow requests",
"navigation_bar.info": "Extended information",
"navigation_bar.logout": "Logout",
"reply_indicator.cancel": "Cancel",
"search.placeholder": "Search",
"search.account": "Account",
"search.hashtag": "Hashtag",
"upload_button.label": "Add media",
"upload_form.undo": "Undo",
"notification.follow": "{name} followed you",
"navigation_bar.preferences": "Preferences",
"navigation_bar.public_timeline": "Federated timeline",
"notification.favourite": "{name} favourited your status",
"notification.follow": "{name} followed you",
"notification.reblog": "{name} boosted your status",
"notification.mention": "{name} mentioned you",
"notifications.clear_confirmation": "Are you sure you want to clear all your notifications?",
"notifications.clear": "Clear notifications",
"notifications.column_settings.alert": "Desktop notifications",
"notifications.column_settings.show": "Show in column",
"notifications.column_settings.follow": "New followers:",
"notifications.column_settings.favourite": "Favourites:",
"notifications.column_settings.follow": "New followers:",
"notifications.column_settings.mention": "Mentions:",
"notifications.column_settings.reblog": "Boosts:",
"notifications.column_settings.show": "Show in column",
"notifications.column_settings.sound": "Play sound",
"notifications.settings": "Column settings",
"privacy.change": "Adjust status privacy",
"privacy.direct.long": "Post to mentioned users only",
"privacy.direct.short": "Direct",
"privacy.private.long": "Post to followers only",
"privacy.private.short": "Private",
"privacy.public.long": "Post to public timelines",
"privacy.public.short": "Public",
"privacy.unlisted.long": "Do not show in public timelines",
"privacy.unlisted.short": "Unlisted",
"reply_indicator.cancel": "Cancel",
"report.heading": "New report",
"report.placeholder": "Additional comments",
"report.submit": "Submit",
"report.target": "Reporting",
"search_results.total": "{count} {count, plural, one {result} other {results}}",
"search.placeholder": "Search",
"search.status_by": "Status by {name}",
"status.delete": "Delete",
"status.favourite": "Favourite",
"status.load_more": "Load more",
"status.media_hidden": "Media hidden",
"status.mention": "Mention @{name}",
"status.open": "Expand this status",
"status.reblog": "Boost",
"status.reblogged_by": "{name} boosted",
"status.reply": "Reply",
"status.report": "Report @{name}",
"status.sensitive_toggle": "Click to view",
"status.sensitive_warning": "Sensitive content",
"status.show_less": "Show less",
"status.show_more": "Show more",
"tabs_bar.compose": "Compose",
"tabs_bar.federated_timeline": "Federated",
"tabs_bar.home": "Home",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notifications",
"upload_area.title": "Drag & drop to upload",
"upload_button.label": "Add media",
"upload_form.undo": "Undo",
"upload_progress.label": "Uploading...",
"video_player.toggle_sound": "Toggle sound",
"video_player.toggle_visible": "Toggle visibility",
"video_player.expand": "Expand video",
};
export default en;

View file

@ -0,0 +1,68 @@
const eo = {
"column_back_button.label": "Reveni",
"lightbox.close": "Fermi",
"loading_indicator.label": "Ŝarĝanta...",
"status.mention": "Mencii @{name}",
"status.delete": "Forigi",
"status.reply": "Respondi",
"status.reblog": "Diskonigi",
"status.favourite": "Favori",
"status.reblogged_by": "{name} diskonigita",
"status.sensitive_warning": "Tikla enhavo",
"status.sensitive_toggle": "Alklaki por vidi",
"video_player.toggle_sound": "Aktivigi sonojn",
"account.mention": "Mencii @{name}",
"account.edit_profile": "Redakti la profilon",
"account.unblock": "Malbloki @{name}",
"account.unfollow": "Malsekvi",
"account.block": "Bloki @{name}",
"account.follow": "Sekvi",
"account.posts": "Mesaĝoj",
"account.follows": "Sekvatoj",
"account.followers": "Sekvantoj",
"account.follows_you": "Sekvas vin",
"account.requested": "Atendas aprobon",
"getting_started.heading": "Por komenci",
"getting_started.about_addressing": "Vi povas sekvi homojn se vi konas la uzantnomon kaj domajnon tajpinte retpoŝtecan adreson en la serĉilon.",
"getting_started.about_shortcuts": "Se la celita uzanto troviĝas en la sama domajno de vi, uzi nur la uzantnomon sufiĉos. La sama regulo validas por mencii aliajn uzantojn en mesaĝo.",
"getting_started.open_source_notice": "Mastodon estas malfermitkoda programo. Vi povas kontribui aŭ raporti problemojn en github je {github}. {apps}.",
"column.home": "Hejmo",
"column.community": "Loka tempolinio",
"column.public": "Fratara tempolinio",
"column.notifications": "Sciigoj",
"tabs_bar.compose": "Ekskribi",
"tabs_bar.home": "Hejmo",
"tabs_bar.mentions": "Sciigoj",
"tabs_bar.public": "Fratara tempolinio",
"tabs_bar.notifications": "Sciigoj",
"compose_form.placeholder": "Pri kio vi pensas?",
"compose_form.publish": "Hup",
"compose_form.sensitive": "Marki ke la enhavo estas tikla",
"compose_form.spoiler": "Kaŝi la tekston malantaŭ averto",
"compose_form.private": "Marki ke la enhavo estas privata",
"compose_form.privacy_disclaimer": "Via privata mesaĝo estos sendita nur al menciitaj uzantoj en {domains}. Ĉu vi fidas {domainsCount, plural, one {tiun servilon} other {tiujn servilojn}}? Mesaĝa privateco funkcias nur en aperaĵoj de Mastodon. Se {domains} {domainsCount, plural, one {ne estas aperaĵo de Mastodon} other {ne estas aperaĵoj de Mastodon}}, estos neniu indiko ke via mesaĝo estas privata, kaj ĝi povus esti diskonigita aŭ videbligita al necelitaj ricevantoj.",
"compose_form.unlisted": "Ne afiŝi en publikaj tempolinioj",
"navigation_bar.edit_profile": "Redakti la profilon",
"navigation_bar.preferences": "Preferoj",
"navigation_bar.community_timeline": "Loka tempolinio",
"navigation_bar.public_timeline": "Fratara tempolinio",
"navigation_bar.logout": "Elsaluti",
"reply_indicator.cancel": "Rezigni",
"search.placeholder": "Serĉi",
"search.account": "Konto",
"search.hashtag": "Kradvorto",
"upload_button.label": "Aldoni enhavaĵon",
"upload_form.undo": "Malfari",
"notification.follow": "{name} sekvis vin",
"notification.favourite": "{name} favoris vian mesaĝon",
"notification.reblog": "{name} diskonigis vian mesaĝon",
"notification.mention": "{name} menciis vin",
"notifications.column_settings.alert": "Retumilaj atentigoj",
"notifications.column_settings.show": "Montri en kolono",
"notifications.column_settings.follow": "Novaj sekvantoj:",
"notifications.column_settings.favourite": "Favoroj:",
"notifications.column_settings.mention": "Mencioj:",
"notifications.column_settings.reblog": "Diskonigoj:",
};
export default eo;

View file

@ -5,28 +5,35 @@ const es = {
"status.mention": "Mencionar",
"status.delete": "Borrar",
"status.reply": "Responder",
"status.reblog": "Republicar",
"status.reblog": "Retoot",
"status.favourite": "Favorito",
"status.reblogged_by": "{name} republicado",
"status.reblogged_by": "Retooteado por {name}",
"status.sensitive_warning": "Contenido sensible",
"status.sensitive_toggle": "Click para ver",
"status.show_more": "Mostrar más",
"status.show_less": "Mostrar menos",
"status.open": "Expandir estado",
"status.report": "Reportar",
"video_player.toggle_sound": "Act/Desac. sonido",
"account.mention": "Mención",
"account.mention": "Mencionar",
"account.edit_profile": "Editar perfil",
"account.unblock": "Desbloquear",
"account.unfollow": "Dejar de seguir",
"account.mute": "Silenciar",
"account.block": "Bloquear",
"account.follow": "Seguir",
"account.block": "Bloquear",
"account.posts": "Publicaciones",
"account.follows": "Seguir",
"account.followers": "Seguidores",
"account.follows_you": "Te sigue",
"account.requested": "Esperando aprobación",
"getting_started.heading": "Primeros pasos",
"getting_started.about_addressing": "Puedes seguir a gente si conoces su nombre de usuario y el dominio en el que están registrados, introduciendo algo similar a una dirección de correo electrónico en el formulario en la parte superior de la barra lateral.",
"getting_started.about_shortcuts": "Si el usuario que buscas está en el mismo dominio que tú, simplemente funcionará introduciendo el nombre de usuario. La misma regla se aplica para mencionar a usuarios.",
"getting_started.about_developer": "Puedes seguir al desarrollador de este proyecto en Gargron@mastodon.social",
"getting_started.open_source_notice": "Mastodon es software libre. Puedes contribuir o reportar errores en {github}. {apps}.",
"column.home": "Inicio",
"column.mentions": "Menciones",
"column.public": "Historia pública",
"column.community": "Historia local",
"column.public": "Historia federada",
"column.notifications": "Notificaciones",
"tabs_bar.compose": "Redactar",
"tabs_bar.home": "Inicio",
@ -34,23 +41,47 @@ const es = {
"tabs_bar.public": "Público",
"tabs_bar.notifications": "Notificaciones",
"compose_form.placeholder": "¿En qué estás pensando?",
"compose_form.publish": "Publicar",
"compose_form.sensitive": "Marcar el contenido como sensible",
"compose_form.unlisted": "Privado",
"compose_form.publish": "Tootear",
"compose_form.sensitive": "Marcar contenido como sensible",
"compose_form.spoiler": "Ocultar texto tras advertencia",
"compose_form.spoiler_placeholder": "Advertencia de contenido",
"composer_form.private": "Marcar como privado",
"composer_form.privacy_disclaimer": "Tu estado se mostrará a los usuarios mencionados en {domains}. Tu estado podrá ser visto en otras instancias, quizás no quieras que tu estado sea visto por otros usuarios.",
"compose_form.unlisted": "No mostrar en la historia federada",
"navigation_bar.edit_profile": "Editar perfil",
"navigation_bar.preferences": "Preferencias",
"navigation_bar.public_timeline": "Público",
"navigation_bar.community_timeline": "Historia local",
"navigation_bar.public_timeline": "Historia federada",
"navigation_bar.favourites": "Favoritos",
"navigation_bar.blocks": "Usuarios bloqueados",
"navigation_bar.info": "Información adicional",
"navigation_bar.logout": "Cerrar sesión",
"reply_indicator.cancel": "Cancelar",
"search.placeholder": "Buscar",
"search.account": "Cuenta",
"search.hashtag": "Etiqueta",
"upload_button.label": "Añadir medio",
"upload_button.label": "Subir multimedia",
"upload_form.undo": "Deshacer",
"notification.follow": "{name} le esta ahora siguiendo",
"notification.favourite": "{name} marcó como favorito su estado",
"notification.reblog": "{name} volvió a publicar su estado",
"notification.mention": "Fue mencionado por {name}"
"notification.follow": "{name} te empezó a seguir",
"notification.favourite": "{name} marcó tu estado como favorito",
"notification.reblog": "{name} ha retooteado tu estado",
"notification.mention": "{name} te ha mencionado",
"notifications.column_settings.alert": "Notificaciones de escritorio",
"notifications.column_settings.show": "Mostrar en columna",
"notifications.column_settings.follow": "Nuevos seguidores:",
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.mention": "Menciones:",
"notifications.column_settings.reblog": "Retoots:",
"emoji_button.label": "Insertar emoji",
"privacy.public.short": "Público",
"privacy.public.long": "Mostrar en la historia federada",
"privacy.unlisted.short": "Sin federar",
"privacy.unlisted.long": "No mostrar en la historia federada",
"privacy.private.short": "Privado",
"privacy.private.long": "Sólo mostrar a seguidores",
"privacy.direct.short": "Directo",
"privacy.direct.long": "Sólo mostrar a los usuarios mencionados",
"privacy.change": "Ajustar privacidad"
};
export default es;

View file

@ -0,0 +1,68 @@
const fi = {
"column_back_button.label": "Takaisin",
"lightbox.close": "Sulje",
"loading_indicator.label": "Ladataan...",
"status.mention": "Mainitse @{name}",
"status.delete": "Poista",
"status.reply": "Vastaa",
"status.reblog": "Buustaa",
"status.favourite": "Tykkää",
"status.reblogged_by": "{name} buustasi",
"status.sensitive_warning": "Arkaluontoista sisältöä",
"status.sensitive_toggle": "Klikkaa nähdäksesi",
"video_player.toggle_sound": "Äänet päälle/pois",
"account.mention": "Mainitse @{name}",
"account.edit_profile": "Muokkaa",
"account.unblock": "Salli @{name}",
"account.unfollow": "Lopeta seuraaminen",
"account.block": "Estä @{name}",
"account.follow": "Seuraa",
"account.posts": "Postit",
"account.follows": "Seuraa",
"account.followers": "Seuraajia",
"account.follows_you": "Seuraa sinua",
"account.requested": "Odottaa hyväksyntää",
"getting_started.heading": "Aloitus",
"getting_started.about_addressing": "Voit seurata ihmisiä jos tiedät heidän käyttäjänimensä ja domainin missä he ovat syöttämällä e-mail-esque osoitteen Etsi kenttään.",
"getting_started.about_shortcuts": "Jos etsimäsi henkilö on samassa domainissa kuin sinä, pelkkä käyttäjänimi kelpaa. Sama pätee kun mainitset ihmisiä statuksessasi",
"getting_started.open_source_notice": "Mastodon Mastodon on avoimen lähdekoodin ohjelma. Voit avustaa tai raportoida ongelmia GitHub palvelussa {github}. {apps}.",
"column.home": "Koti",
"column.community": "Paikallinen aikajana",
"column.public": "Yleinen aikajana",
"column.notifications": "Ilmoitukset",
"tabs_bar.compose": "Luo",
"tabs_bar.home": "Koti",
"tabs_bar.mentions": "Maininnat",
"tabs_bar.public": "Yleinen aikajana",
"tabs_bar.notifications": "Ilmoitukset",
"compose_form.placeholder": "Mitä sinulla on mielessä?",
"compose_form.publish": "Toot",
"compose_form.sensitive": "Merkitse media herkäksi",
"compose_form.spoiler": "Piiloita teksti varoituksen taakse",
"compose_form.private": "Merkitse yksityiseksi",
"compose_form.privacy_disclaimer": "Sinun yksityinen status toimitetaan mainitsemallesi käyttäjille domaineissa {domains}. Luotatko {domainsCount, plural, one {tähän palvelimeen} other {näihin palvelimiin}}? Postauksen yksityisyys toimii van Mastodon palvelimilla. Jos {domains} {domainsCount, plural, one {ei ole Mastodon palvelin} other {eivät ole Mastodon palvelin}}, viestiin ei tule Yksityinen-merkintää, ja sitä voidaan boostata tai muuten tehdä näkyväksi muille vastaanottajille.",
"compose_form.unlisted": "Älä näytä yleisillä aikajanoilla",
"navigation_bar.edit_profile": "Muokkaa profiilia",
"navigation_bar.preferences": "Ominaisuudet",
"navigation_bar.community_timeline": "Paikallinen aikajana",
"navigation_bar.public_timeline": "Yleinen aikajana",
"navigation_bar.logout": "Kirjaudu ulos",
"reply_indicator.cancel": "Peruuta",
"search.placeholder": "Hae",
"search.account": "Tili",
"search.hashtag": "Hashtag",
"upload_button.label": "Lisää mediaa",
"upload_form.undo": "Peru",
"notification.follow": "{name} seurasi sinua",
"notification.favourite": "{name} tykkäsi statuksestasi",
"notification.reblog": "{name} buustasi statustasi",
"notification.mention": "{name} mainitsi sinut",
"notifications.column_settings.alert": "Työpöytä ilmoitukset",
"notifications.column_settings.show": "Näytä sarakkeessa",
"notifications.column_settings.follow": "Uusia seuraajia:",
"notifications.column_settings.favourite": "Tykkäyksiä:",
"notifications.column_settings.mention": "Mainintoja:",
"notifications.column_settings.reblog": "Buusteja:",
};
export default fi;

View file

@ -10,7 +10,14 @@ const fr = {
"status.reblogged_by": "{name} a partagé :",
"status.sensitive_warning": "Contenu délicat",
"status.sensitive_toggle": "Cliquer pour dévoiler",
"status.show_more": "Déplier",
"status.show_less": "Replier",
"status.open": "Déplier ce statut",
"status.report": "Signaler @{name}",
"status.load_more": "Charger plus",
"status.media_hidden": "Média caché",
"video_player.toggle_sound": "Mettre/Couper le son",
"video_player.toggle_visible": "Afficher/Cacher la vidéo",
"account.mention": "Mentionner",
"account.edit_profile": "Modifier le profil",
"account.unblock": "Débloquer",
@ -27,7 +34,7 @@ const fr = {
"account.report": "Signaler",
"account.disclaimer": "Ce compte est situé sur une autre instance. Les nombres peuvent être plus grands.",
"getting_started.heading": "Pour commencer",
"getting_started.about_addressing": "Vous pouvez suivre les statuts de quelquun en entrant dans le champs de recherche leur identifiant et le domaine de leur instance, séparés par un @ à la manière dune adresse courriel.",
"getting_started.about_addressing": "Vous pouvez suivre les statuts de quelquun en entrant dans le champ de recherche leur identifiant et le domaine de leur instance, séparés par un @ à la manière dune adresse courriel.",
"getting_started.about_shortcuts": "Si cette personne utilise la même instance que vous, lidentifiant suffit. Cest le même principe pour mentionner quelquun dans vos statuts.",
"getting_started.about_developer": "Pour suivre le développeur de ce projet, cest Gargron@mastodon.social",
"getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer et envoyer vos commentaires et rapports de bogues via {github} sur GitHub.",
@ -35,18 +42,27 @@ const fr = {
"column.community": "Fil public local",
"column.public": "Fil public global",
"column.notifications": "Notifications",
"column.public": "Fil public",
"column.blocks": "Utilisateurs bloqués",
"column.favourites": "Favoris",
"column.follow_requests": "Demandes de suivi",
"empty_column.notifications": "Vous navez pas encore de notification. Interagissez avec dautres utilisateurs⋅trices pour débuter la conversation.",
"empty_column.public": "Il n'y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateurs d'autres instances pour remplir le fil public.",
"empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d'autres utilisateurs.",
"empty_column.home.public_timeline": "le fil public",
"empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir !",
"empty_column.hashtag": "Il n'y a encore aucun contenu relatif à ce hashtag",
"tabs_bar.compose": "Composer",
"tabs_bar.home": "Accueil",
"tabs_bar.mentions": "Mentions",
"tabs_bar.public": "Fil public global",
"tabs_bar.notifications": "Notifications",
"tabs_bar.local_timeline": "Fil public local",
"tabs_bar.federated_timeline": "Fil public global",
"compose_form.placeholder": "Quavez-vous en tête ?",
"compose_form.publish": "Pouet ",
"compose_form.publish": "Pouet",
"compose_form.sensitive": "Marquer le média comme délicat",
"compose_form.spoiler": "Masquer le texte par un avertissement",
"compose_form.spoiler": "Masquer le texte derrière un avertissement",
"compose_form.spoiler_placeholder": "Avertissement",
"compose_form.private": "Rendre privé",
"compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodons. Si {domains} {domainsCount, plural, one {n'est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n'y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d'une autre manière à d'autres personnes imprévues",
"compose_form.unlisted": "Ne pas afficher dans les fils publics",
@ -58,25 +74,32 @@ const fr = {
"navigation_bar.blocks": "Utilisateurs bloqués",
"navigation_bar.favourites": "Favoris",
"navigation_bar.info": "Plus d'informations",
"notification.favourite": "{name} a ajouté à ses favoris :",
"navigation_bar.logout": "Déconnexion",
"navigation_bar.follow_requests": "Demandes de suivi",
"reply_indicator.cancel": "Annuler",
"search.placeholder": "Chercher",
"search.placeholder": "Rechercher",
"search.account": "Compte",
"search.hashtag": "Mot-clé",
"search_results.total": "{count} {count, plural, one {résultat} other {résultats}}",
"search.status_by": "Statuts de {name}",
"upload_button.label": "Joindre un média",
"upload_form.undo": "Annuler",
"upload_progress.label": "Envoi en cours…",
"upload_area.title": "Glissez et déposez pour envoyer",
"notification.follow": "{name} vous suit.",
"notification.favourite": "{name} a ajouté à ses favoris :",
"notification.reblog": "{name} a partagé votre statut :",
"notification.mention": "{name} vous a mentionné⋅e :",
"notifications.column_settings.alert": "Notifications locales",
"notifications.column_settings.show": "Afficher dans la colonne",
"notifications.column_settings.sound": "Émettre un son",
"notifications.column_settings.follow": "Nouveaux abonnés :",
"notifications.column_settings.favourite": "Favoris :",
"notifications.column_settings.mention": "Mentions :",
"notifications.column_settings.reblog": "Partages :",
"notifications.clear": "Nettoyer",
"notifications.clear_confirmation": "Voulez-vous vraiment nettoyer toutes vos notifications ?",
"notifications.settings": "Paramètres de la colonne",
"privacy.public.short": "Public",
"privacy.public.long": "Afficher dans les fils publics",
"privacy.unlisted.short": "Non-listé",
@ -84,8 +107,22 @@ const fr = {
"privacy.private.short": "Privé",
"privacy.private.long": "Nafficher que pour vos abonné⋅e⋅s",
"privacy.direct.short": "Direct",
"privacy.direct.long": "Nafficher que pour les personnes mentionnées",
"privacy.direct.long": "Nafficher que pour les personnes mentionnées",
"privacy.change": "Ajuster la confidentialité du message",
"media_gallery.toggle_visible": "Modifier la visibilité",
"missing_indicator.label": "Non trouvé",
"follow_request.authorize": "Autoriser",
"follow_request.reject": "Rejeter",
"home.settings": "Paramètres de la colonne",
"home.column_settings.basic": "Basique",
"home.column_settings.show_reblogs": "Afficher les partages",
"home.column_settings.show_replies": "Afficher les réponses",
"home.column_settings.advanced": "Avancé",
"home.column_settings.filter_regex": "Filtrer avec une expression rationnelle",
"report.heading": "Nouveau signalement",
"report.placeholder": "Commentaires additionnels",
"report.submit": "Envoyer",
"report.target": "Signalement"
};
export default fr;

View file

@ -3,8 +3,16 @@ import de from './de';
import es from './es';
import hu from './hu';
import fr from './fr';
import nl from './nl';
import no from './no';
import pt from './pt';
import uk from './uk';
import fi from './fi';
import eo from './eo';
import ru from './ru';
import ja from './ja';
import zh_hk from './zh-hk';
import bg from './bg';
const locales = {
en,
@ -12,8 +20,16 @@ const locales = {
es,
hu,
fr,
nl,
no,
pt,
uk
uk,
fi,
eo,
ru,
ja,
'zh-HK': zh_hk,
bg,
};
export default function getMessagesForLocale (locale) {

View file

@ -0,0 +1,119 @@
const ja = {
"column_back_button.label": "戻る",
"lightbox.close": "閉じる",
"loading_indicator.label": "読み込み中...",
"status.mention": "@{name} さんへの返信",
"status.delete": "削除",
"status.reply": "返信",
"status.reblog": "ブースト",
"status.favourite": "お気に入り",
"status.reblogged_by": "{name} さんにブーストされました",
"status.sensitive_warning": "不適切なコンテンツ",
"status.sensitive_toggle": "クリックして表示",
"status.show_more": "もっと見る",
"status.load_more": "もっと見る",
"status.show_less": "隠す",
"status.open": "Expand this status",
"status.report": "@{name} さんを通報",
"status.media_hidden": "非表示のメデイア",
"video_player.toggle_sound": "音の切り替え",
"account.mention": "@{name} さんに返信",
"account.edit_profile": "プロフィールを編集",
"account.unblock": "@{name} さんのブロックを解除",
"account.unfollow": "フォロー解除",
"account.block": "@{name} さんをブロック",
"account.mute": "ミュート",
"account.unmute": "ミュート解除",
"account.follow": "フォロー",
"account.report": "@{name}を通報する",
"account.posts": "投稿",
"account.follows": "フォロー",
"account.followers": "フォロワー",
"account.follows_you": "フォローされています",
"account.requested": "承認待ち",
"follow_request.authorize": "許可",
"follow_request.reject": "拒否",
"getting_started.heading": "スタート",
"getting_started.about_addressing": "ドメインとユーザー名を知っているなら検索フォームに入力すればフォローできます。",
"getting_started.about_shortcuts": "対象のアカウントがあなたと同じドメインのユーザーならばユーザー名のみで検索できます。これは返信のときも一緒です。",
"getting_started.open_source_notice": "Mastodon はオープンソースソフトウェアです。誰でも GitHub{github})から開発に参加したり、問題を報告したりできます。 {apps}",
"column.home": "ホーム",
"column.community": "ローカルタイムライン",
"column.public": "連合タイムライン",
"column.notifications": "通知",
"column.favourites": "お気に入り",
"tabs_bar.compose": "投稿",
"tabs_bar.home": "ホーム",
"tabs_bar.mentions": "返信",
"tabs_bar.local_timeline": "ローカル",
"tabs_bar.federated_timeline": "連合",
"tabs_bar.notifications": "通知",
"compose_form.placeholder": "今なにしてる?",
"compose_form.publish": "トゥート",
"compose_form.sensitive": "メディアを不適切なコンテンツとしてマークする",
"compose_form.spoiler": "テキストを隠す",
"compose_form.spoiler_placeholder": "内容注意メッセージ",
"compose_form.private": "非公開にする",
"compose_form.privacy_disclaimer": "あなたの非公開トゥートは返信先のユーザーat {domains})に公開されます。{domainsCount, plural, one {that server} other {those servers}}を信頼しますか投稿のプライバシー保護はMastodonサーバー内でのみ有効です。 もし{domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}ならばあなたの投稿のプライバシーは保護されず、ブーストされたり予期しないユーザーに見られる可能性があります。",
"compose_form.unlisted": "公開タイムラインに表示しない",
"privacy.public.short": "公開",
"privacy.public.long": "公開TLに投稿する",
"privacy.unlisted.short": "未収載",
"privacy.unlisted.long": "公開TLで表示しない",
"privacy.private.short": "非公開",
"privacy.private.long": "フォロワーだけに公開",
"privacy.direct.short": "ダイレクト",
"privacy.direct.long": "含んだユーザーだけに公開",
"privacy.change": "投稿のプライバシーを変更2",
"report.heading": "新規通報",
"report.placeholder": "コメント",
"report.target": "問題のユーザー",
"report.submit": "通報する",
"navigation_bar.edit_profile": "プロフィールを編集",
"navigation_bar.preferences": "ユーザー設定",
"navigation_bar.community_timeline": "ローカルタイムライン",
"navigation_bar.public_timeline": "連合タイムライン",
"navigation_bar.logout": "ログアウト",
"navigation_bar.favourites": "お気に入り",
"navigation_bar.blocks": "ブロックしたユーザー",
"navigation_bar.info": "サーバー情報",
"reply_indicator.cancel": "キャンセル",
"search.placeholder": "検索",
"search.account": "アカウント",
"search.hashtag": "ハッシュタグ",
"search.status_by": "{uuuname}からの投稿",
"upload_area.title": "ファイルをこちらにドラッグしてください",
"upload_button.label": "メディアを追加",
"upload_form.undo": "やり直す",
"notification.follow": "{name} さんにフォローされました",
"notification.favourite": "{name} さんがあなたのトゥートをお気に入りに登録しました",
"notification.reblog": "{name} さんがあなたのトゥートをブーストしました",
"notification.mention": "{name} さんがあなたに返信しました",
"notifications.clear": "通知を片付ける",
"notifications.clear_confirmation": "通知を全部片付けます。大丈夫ですか?",
"notifications.column_settings.alert": "デスクトップ通知",
"notifications.column_settings.show": "カラムに表示",
"notifications.column_settings.follow": "新しいフォロワー",
"notifications.column_settings.favourite": "お気に入り",
"notifications.column_settings.mention": "返信",
"notifications.column_settings.reblog": "ブースト",
"notifications.column_settings.sound": "通知音を再生",
"empty_column.home": "まだ誰もフォローしていません。{public}を見に行くか、検索を使って他のユーザーを見つけましょう。",
"empty_column.home.public_timeline": "連合タイムライン",
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
"empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!",
"empty_column.hashtag": "このハッシュタグはまだ使っていません。",
"upload_progress.label": "アップロード中…",
"emoji_button.label": "絵文字を追加",
"home.column_settings.basic": "シンプル",
"home.column_settings.advanced": "エキスパート",
"home.column_settings.show_reblogs": "ブースト表示",
"home.column_settings.show_replies": "返信表示",
"home.column_settings.filter_regex": "正規表現でフィルター",
"home.settings": "カラム設定",
"notification.settings": "カラム設定",
"missing_indicator.label": "見つかりません",
"boost_modal.combo": "次は{combo}を押せば、これをスキップできます。"
};
export default ja;

View file

@ -0,0 +1,68 @@
const nl = {
"column_back_button.label": "terug",
"lightbox.close": "Sluiten",
"loading_indicator.label": "Laden...",
"status.mention": "Vermeld @{name}",
"status.delete": "Verwijder",
"status.reply": "Reageer",
"status.reblog": "Boost",
"status.favourite": "Favoriet",
"status.reblogged_by": "{name} boostte",
"status.sensitive_warning": "Gevoelige inhoud",
"status.sensitive_toggle": "Klik om te zien",
"video_player.toggle_sound": "Geluid omschakelen",
"account.mention": "Vermeld @{name}",
"account.edit_profile": "Bewerk profiel",
"account.unblock": "Deblokkeer @{name}",
"account.unfollow": "Ontvolg",
"account.block": "Blokkeer @{name}",
"account.follow": "Volg",
"account.posts": "Berichten",
"account.follows": "Volgt",
"account.followers": "Volgers",
"account.follows_you": "Volgt jou",
"account.requested": "Wacht op goedkeuring",
"getting_started.heading": "Beginnen",
"getting_started.about_addressing": "Je kunt mensen volgen als je hun gebruikersnaam en het domein van hun server kent, door het e-mailachtige adres in het zoekscherm in te voeren.",
"getting_started.about_shortcuts": "Als de gezochte gebruiker op hetzelfde domein zit als jijzelf, is invoeren van de gebruikersnaam genoeg. Dat geldt ook als je mensen in de statussen wilt vermelden.",
"getting_started.open_source_notice": "Mastodon is open source software. Je kunt bijdragen of problemen melden op GitHub via {github}. {apps}.",
"column.home": "Thuis",
"column.community": "Lokale tijdlijn",
"column.public": "Federatietijdlijn",
"column.notifications": "Meldingen",
"tabs_bar.compose": "Schrijven",
"tabs_bar.home": "Thuis",
"tabs_bar.mentions": "Vermeldingen",
"tabs_bar.public": "Federatietijdlijn",
"tabs_bar.notifications": "Meldingen",
"compose_form.placeholder": "Waar ben je mee bezig?",
"compose_form.publish": "Toot",
"compose_form.sensitive": "Markeer media als gevoelig",
"compose_form.spoiler": "Verberg tekst achter waarschuwing",
"compose_form.private": "Mark als privé",
"compose_form.privacy_disclaimer": "Je besloten status wordt afgeleverd aan vermelde gebruikers op {domains}. Vertrouw je {domainsCount, plural, one {that server} andere {those servers}}? Privé plaatsen werkt alleen op Mastodon servers. Als {domains} {domainsCount, plural, een {is not a Mastodon instance} andere {are not Mastodon instances}}, dan wordt er geen indicatie gegeven dat he bericht besloten is, waardoor het kan worden geboost of op andere manier zichtbaar worden voor niet bedoelde lezers.",
"compose_form.unlisted": "Niet tonen op openbare tijdlijnen",
"navigation_bar.edit_profile": "Bewerk profiel",
"navigation_bar.preferences": "Voorkeuren",
"navigation_bar.community_timeline": "Lokale tijdlijn",
"navigation_bar.public_timeline": "Federatietijdlijn",
"navigation_bar.logout": "Uitloggen",
"reply_indicator.cancel": "Annuleren",
"search.placeholder": "Zoeken",
"search.account": "Account",
"search.hashtag": "Hashtag",
"upload_button.label": "Toevoegen media",
"upload_form.undo": "Ongedaan maken",
"notification.follow": "{name} volgde jou",
"notification.favourite": "{name} markeerde je status als favoriet",
"notification.reblog": "{name} boostte je status",
"notification.mention": "{name} vermeldde jou",
"notifications.column_settings.alert": "Desktopmeldingen",
"notifications.column_settings.show": "Tonen in kolom",
"notifications.column_settings.follow": "Nieuwe volgers:",
"notifications.column_settings.favourite": "Favoriten:",
"notifications.column_settings.mention": "Vermeldingen:",
"notifications.column_settings.reblog": "Boosts:",
};
export default nl;

View file

@ -0,0 +1,77 @@
const no = {
"column_back_button.label": "Tilbake",
"lightbox.close": "Lukk",
"loading_indicator.label": "Laster...",
"status.mention": "Nevn @{name}",
"status.delete": "Slett",
"status.reply": "Svar",
"status.reblog": "Reblogg",
"status.favourite": "Lik",
"status.reblogged_by": "{name} reblogget",
"status.sensitive_warning": "Sensitivt innhold",
"status.sensitive_toggle": "Klikk for å vise",
"status.show_more": "Vis mer",
"status.show_less": "Vis mindre",
"status.open": "Utvid denne statusen",
"status.report": "Rapporter @{name}",
"video_player.toggle_sound": "Veksle lyd",
"account.mention": "Nevn @{name}",
"account.edit_profile": "Rediger profil",
"account.unblock": "Avblokker @{name}",
"account.unfollow": "Avfølg",
"account.block": "Blokker @{name}",
"account.follow": "Følg",
"account.posts": "Poster",
"account.follows": "Følginger",
"account.followers": "Følgere",
"account.follows_you": "Folger deg",
"account.requested": "Venter på godkjennelse",
"getting_started.heading": "Kom i gang",
"getting_started.about_addressing": "Du kan følge noen hvis du vet brukernavnet deres og domenet de er på ved å skrive en e-postadresse inn i søkeskjemaet.",
"getting_started.about_shortcuts": "Hvis målbrukeren er på samme domene som deg, vil kun brukernavnet også fungere. Den samme regelen gjelder når man nevner noen i statuser.",
"getting_started.open_source_notice": "Mastodon er programvare med fri kildekode. Du kan bidra eller rapportere problemer på GitHub på {github}. {apps}.",
"column.home": "Hjem",
"column.community": "Lokal tidslinje",
"column.public": "Forent tidslinje",
"column.notifications": "Varslinger",
"column.blocks": "Blokkerte brukere",
"column.favourites": "Likt",
"tabs_bar.compose": "Komponer",
"tabs_bar.home": "Hjem",
"tabs_bar.mentions": "Nevninger",
"tabs_bar.public": "Forent tidslinje",
"tabs_bar.notifications": "Varslinger",
"compose_form.placeholder": "Hva har du på hjertet?",
"compose_form.publish": "Tut",
"compose_form.sensitive": "Merk media som følsomt",
"compose_form.spoiler": "Skjul tekst bak advarsel",
"compose_form.private": "Merk som privat",
"compose_form.privacy_disclaimer": "Din private status vil leveres til nevnte brukere på {domains}. Stoler du på {domainsCount, plural, one {den serveren} other {de serverne}}? Synlighet fungerer kun på Mastodon-instanser. Hvis {domains} {domainsCount, plural, one {ike er en Mastodon-instans} other {ikke er Mastodon-instanser}}, vil det ikke indikeres at posten din er privat, og den kan kanskje bli reblogget eller på annen måte bli synlig for uventede mottakere.",
"compose_form.unlisted": "Ikke vis på offentlige tidslinjer",
"navigation_bar.edit_profile": "Rediger profil",
"navigation_bar.preferences": "Preferanser",
"navigation_bar.community_timeline": "Lokal tidslinje",
"navigation_bar.public_timeline": "Forent tidslinje",
"navigation_bar.logout": "Logg ut",
"navigation_bar.blocks": "Blokkerte brukere",
"navigation_bar.info": "Utvidet informasjon",
"navigation_bar.favourites": "Likt",
"reply_indicator.cancel": "Avbryt",
"search.placeholder": "Søk",
"search.account": "Konto",
"search.hashtag": "Hashtag",
"upload_button.label": "Legg til media",
"upload_form.undo": "Angre",
"notification.follow": "{name} fulgte deg",
"notification.favourite": "{name} likte din status",
"notification.reblog": "{name} reblogget din status",
"notification.mention": "{name} nevnte deg",
"notifications.column_settings.alert": "Skrivebordsvarslinger",
"notifications.column_settings.show": "Vis i kolonne",
"notifications.column_settings.follow": "Nye følgere:",
"notifications.column_settings.favourite": "Likt:",
"notifications.column_settings.mention": "Nevninger:",
"notifications.column_settings.reblog": "Reblogginger:",
};
export default no;

View file

@ -2,54 +2,127 @@ const pt = {
"column_back_button.label": "Voltar",
"lightbox.close": "Fechar",
"loading_indicator.label": "Carregando...",
"status.mention": "Menção",
"status.delete": "Deletar",
"status.mention": "Mencionar @{name}",
"status.delete": "Eliminar",
"status.reply": "Responder",
"status.reblog": "Reblogar",
"status.favourite": "Favoritar",
"status.reblogged_by": "{name} reblogou",
"video_player.toggle_sound": "Alterar som",
"account.mention": "Menção",
"status.reblog": "Partilhar",
"status.favourite": "Adicionar aos favoritos",
"status.reblogged_by": "{name} partilhou",
"status.sensitive_warning": "Conteúdo sensível",
"status.sensitive_toggle": "Clique para ver",
"status.show_more": "Mostrar mais",
"status.show_less": "Mostrar menos",
"status.open": "Expandir",
"status.report": "Reportar @{name}",
"status.load_more": "Carregar mais",
"status.media_hidden": "Media escondida",
"video_player.toggle_sound": "Ligar/Desligar som",
"video_player.toggle_visible": "Ligar/Desligar vídeo",
"account.mention": "Mencionar @{name}",
"account.edit_profile": "Editar perfil",
"account.unblock": "Desbloquear",
"account.unfollow": "Unfollow",
"account.block": "Bloquear",
"account.unblock": "Não bloquear @{name}",
"account.unfollow": "Não seguir",
"account.block": "Bloquear @{name}",
"account.mute": "Mute",
"account.unmute": "Remover Mute",
"account.follow": "Seguir",
"account.block": "Bloquear",
"account.posts": "Posts",
"account.follows": "Segue",
"account.followers": "Seguidores",
"account.follows_you": "Segue você",
"account.follows_you": "É teu seguidor",
"account.requested": "A aguardar aprovação",
"account.report": "Denunciar",
"account.disclaimer": "Essa conta está localizado em outra instância. Os nomes podem ser maiores.",
"getting_started.heading": "Primeiros passos",
"getting_started.about_addressing": "Podes seguir pessoas se sabes o nome de usuário deles e o domínio em que estão entrando um endereço similar a e-mail no campo no topo da barra lateral.",
"getting_started.about_addressing": "Podes seguir pessoas se sabes o nome de usuário deles e o domínio em que estão colocando um endereço similar a e-mail no campo no topo da barra lateral.",
"getting_started.about_shortcuts": "Se o usuário alvo está no mesmo domínio, só o nome funcionará. A mesma regra se aplica a mencionar pessoas nas postagens.",
"getting_started.about_developer": "O desenvolvedor desse projeto pode ser seguido em Gargron@mastodon.social",
"getting_started.about_developer": "Pode seguir o developer deste projecto em Gargron@mastodon.social",
"getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}. {apps}.",
"column.home": "Home",
"column.mentions": "Menções",
"column.public": "Público",
"tabs_bar.compose": "Compôr",
"column.community": "Local",
"column.public": "Global",
"column.notifications": "Notificações",
"column.blocks": "Utilizadores Bloqueados",
"column.favourites": "Favoritos",
"column.follow_requests": "Seguidores Pendentes",
"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.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.",
"empty_column.home.public_timeline": "global",
"empty_column.community": "Ainda não existem conteúdo local para mostrar!",
"empty_column.hashtag": "Não existe qualquer conteúdo com essa hashtag",
"tabs_bar.compose": "Criar",
"tabs_bar.home": "Home",
"tabs_bar.mentions": "Menções",
"tabs_bar.public": "Público",
"tabs_bar.notifications": "Notificações",
"compose_form.placeholder": "Que estás pensando?",
"tabs_bar.local_timeline": "Local",
"tabs_bar.federated_timeline": "Global",
"compose_form.placeholder": "Em que estás a pensar?",
"compose_form.publish": "Publicar",
"compose_form.sensitive": "Marcar conteúdo como sensível",
"compose_form.unlisted": "Modo não-listado",
"compose_form.sensitive": "Marcar media como conteúdo sensível",
"compose_form.spoiler": "Esconder texto com aviso",
"compose_form.spoiler_placeholder": "Aviso",
"compose_form.private": "Tornar privado",
"compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.",
"compose_form.unlisted": "Não mostrar na listagem pública",
"emoji_button.label": "Inserir Emoji",
"navigation_bar.edit_profile": "Editar perfil",
"navigation_bar.preferences": "Preferências",
"navigation_bar.public_timeline": "Timeline Pública",
"navigation_bar.logout": "Logout",
"navigation_bar.community_timeline": "Local",
"navigation_bar.public_timeline": "Global",
"navigation_bar.blocks": "Utilizadores bloqueados",
"navigation_bar.favourites": "Favoritos",
"navigation_bar.info": "Mais informações",
"navigation_bar.logout": "Sair",
"navigation_bar.follow_requests": "Seguidores pendentes",
"reply_indicator.cancel": "Cancelar",
"search.placeholder": "Busca",
"search.placeholder": "Pesquisar",
"search.account": "Conta",
"search.hashtag": "Hashtag",
"search_results.total": "{count} {count, plural, one {resultado} other {resultados}}",
"search.status_by": "Post de {name}",
"upload_button.label": "Adicionar media",
"upload_form.undo": "Desfazer",
"notification.follow": "{name} seguiu você",
"notification.favourite": "{name} favoritou seu post",
"notification.reblog": "{name} reblogou o seu post",
"notification.mention": "{name} mecionou você"
"upload_form.undo": "Anular",
"upload_progress.label": "A gravar…",
"upload_area.title": "Arraste e solte para enviar",
"notification.follow": "{name} seguiu-te",
"notification.favourite": "{name} adicionou o teu post aos favoritos",
"notification.reblog": "{name} partilhou o teu post",
"notification.mention": "{name} mencionou-te",
"notifications.column_settings.alert": "Notificações no computador",
"notifications.column_settings.show": "Mostrar nas colunas",
"notifications.column_settings.sound": "Reproduzir som",
"notifications.column_settings.follow": "Novos seguidores:",
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.mention": "Menções:",
"notifications.column_settings.reblog": "Partilhas:",
"notifications.clear": "Limpar notificações",
"notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?",
"notifications.settings": "Parâmetros da lista de Notificações",
"privacy.public.short": "Público",
"privacy.public.long": "Publicar em todos os feeds",
"privacy.unlisted.short": "Não listar",
"privacy.unlisted.long": "Não publicar nos feeds públicos",
"privacy.private.short": "Privado",
"privacy.private.long": "Apenas para os seguidores",
"privacy.direct.short": "Directo",
"privacy.direct.long": "Apenas para utilizadores mencionados",
"privacy.change": "Ajustar a privacidade da mensagem",
"media_gallery.toggle_visible": "Modificar a visibilidade",
"missing_indicator.label": "Não encontrado",
"follow_request.authorize": "Autorizar",
"follow_request.reject": "Rejeitar",
"home.settings": "Parâmetros da coluna Home",
"home.column_settings.basic": "Básico",
"home.column_settings.show_reblogs": "Mostrar as partilhas",
"home.column_settings.show_replies": "Mostrar as respostas",
"home.column_settings.advanced": "Avançadas",
"home.column_settings.filter_regex": "Filtrar com uma expressão regular",
"report.heading": "Nova denuncia",
"report.placeholder": "Comentários adicionais",
"report.submit": "Enviar",
"report.target": "Denunciar"
};
export default pt;

View file

@ -0,0 +1,101 @@
const ru = {
"column_back_button.label": "Назад",
"lightbox.close": "Закрыть",
"loading_indicator.label": "Загрузка...",
"status.mention": "Упомянуть @{name}",
"status.delete": "Удалить",
"status.reply": "Ответить",
"status.reblog": "Продвинуть",
"status.favourite": "Нравится",
"status.reblogged_by": "{name} продвинул(а)",
"status.sensitive_warning": "Чувствительный контент",
"status.sensitive_toggle": "Нажмите для просмотра",
"status.show_more": "Развернуть",
"status.show_less": "Свернуть",
"status.open": "Развернуть статус",
"status.report": "Пожаловаться",
"status.load_more": "Показать еще",
"video_player.toggle_sound": "Вкл./выкл. звук",
"account.mention": "Упомянуть",
"account.edit_profile": "Изменить профиль",
"account.unblock": "Разблокировать",
"account.unfollow": "Отписаться",
"account.block": "Блокировать",
"account.mute": "Заглушить",
"account.follow": "Подписаться",
"account.posts": "Посты",
"account.follows": "Подписки",
"account.followers": "Подписаны",
"account.follows_you": "Подписан(а) на Вас",
"account.requested": "Ожидает подтверждения",
"getting_started.heading": "Добро пожаловать",
"getting_started.about_addressing": "Вы можете подписаться на человека, зная имя пользователя и домен, на котором он находится, введя e-mail-подобный адрес в форму поиска.",
"getting_started.about_shortcuts": "Если пользователь находится на одном с Вами домене, можно использовать только имя. То же правило применимо к упоминанию пользователей в статусах.",
"getting_started.open_source_notice": "Mastodon - программа с открытым исходным кодом. Вы можете помочь проекту или сообщить о проблемах на GitHub по адресу {github}. {apps}.",
"getting_started.apps": "Доступны различные приложения.",
"column.home": "Главная",
"column.community": "Локальная лента",
"column.public": "Глобальная лента",
"column.notifications": "Уведомления",
"tabs_bar.compose": "Написать",
"tabs_bar.home": "Главная",
"tabs_bar.mentions": "Упоминания",
"tabs_bar.public": "Глобальная лента",
"tabs_bar.notifications": "Уведомления",
"compose_form.placeholder": "О чем Вы думаете?",
"compose_form.publish": "Трубить",
"compose_form.sensitive": "Отметить как чувствительный контент",
"compose_form.spoiler": "Скрыть текст за предупреждением",
"compose_form.private": "Отметить как приватное",
"compose_form.privacy_disclaimer": "Ваш приватный статус будет доставлен упомянутым пользователям на доменах {domains}. Доверяете ли вы {domainsCount, plural, one {этому серверу} other {этим серверам}}? Приватность постов работает только на узлах Mastodon. Если {domains} {domainsCount, plural, one {не является узлом Mastodon} other {не являются узлами Mastodon}}, приватность поста не будет указана, и он может оказаться продвинут или иным образом показан не обозначенным Вами пользователям.",
"compose_form.unlisted": "Не отображать в публичных лентах",
"navigation_bar.edit_profile": "Изменить профиль",
"navigation_bar.preferences": "Опции",
"navigation_bar.community_timeline": "Локальная лента",
"navigation_bar.public_timeline": "Глобальная лента",
"navigation_bar.logout": "Выйти",
"navigation_bar.info": "Об узле",
"navigation_bar.favourites": "Понравившееся",
"navigation_bar.blocks": "Список блокировки",
"reply_indicator.cancel": "Отмена",
"search.placeholder": "Поиск",
"search.account": "Аккаунт",
"search.hashtag": "Хэштег",
"upload_button.label": "Добавить медиаконтент",
"upload_form.undo": "Отменить",
"notification.follow": "{name} подписался(-лась) на Вас",
"notification.favourite": "{name} понравился Ваш статус",
"notification.reblog": "{name} продвинул(а) Ваш статус",
"notification.mention": "{name} упомянул(а) Вас",
"home.settings": "Настройки колонки",
"home.column_settings.basic": "Основные",
"home.column_settings.advanced": "Дополнительные",
"home.column_settings.filter_regex": "Отфильтровать регулярным выражением",
"home.column_settings.show_replies": "Показывать продвижения",
"home.column_settings.show_replies": "Показывать ответы",
"notifications.clear": "Очистить уведомления",
"notifications.settings": "Настройки колонки",
"notifications.column_settings.alert": "Десктопные уведомления",
"notifications.column_settings.show": "Показывать в колонке",
"notifications.column_settings.follow": "Новые подписчики:",
"notifications.column_settings.favourite": "Нравится:",
"notifications.column_settings.mention": "Упоминания:",
"notifications.column_settings.reblog": "Продвижения:",
"notifications.column_settings.sound": "Проигрывать звук",
"empty_column.notifications": "У Вас еще нет уведомлений. Заведите знакомство с другими пользователями, чтобы начать разговор.",
"empty_column.hashtag": "Статусов с таким хэштегом еще не существует.",
"empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!",
"empty_column.public": "Здесь ничего нет! Опубликуйте что-нибудь или подпишитесь на пользователей с других узлов, чтобы заполнить ленту.",
"empty_column.home": "Пока Вы ни на кого не подписаны. Полистайте {public} или используйте поиск, чтобы освоиться и завести новые знакомства.",
"empty_column.home.public_timeline": "публичные ленты",
"privacy.public.short": "Публичный",
"privacy.public.long": "Показать в публичных лентах",
"privacy.unlisted.short": "Скрытый",
"privacy.unlisted.long": "Не показывать в лентах",
"privacy.private.short": "Приватный",
"privacy.private.long": "Показать только подписчикам",
"privacy.direct.short": "Направленный",
"privacy.direct.long": "Показать только упомянутым",
};
export default ru;

View file

@ -0,0 +1,113 @@
import zh from 'react-intl/locale-data/zh';
const localeData = zh.reduce(function (acc, localeData) {
if (localeData.locale === "zh-Hant-HK") {
// rename the locale "zh-Hant-HK" as "zh-HK"
// (match the code usually used in Accepted-Language header)
acc.push(Object.assign({},
localeData,
{
"locale": "zh-HK",
"parentLocale": "zh-Hant-HK",
}
));
}
return acc;
}, []);
export { localeData as localeData };
const zh_hk = {
"account.block": "封鎖 @{name}",
"account.edit_profile": "修改個人資料",
"account.follow": "關注",
"account.followers": "關注的人",
"account.follows_you": "關注你",
"account.follows": "正在關注",
"account.mention": "提及 @{name}",
"account.posts": "文章",
"account.requested": "等候審批",
"account.unblock": "解除對 @{name} 的封鎖",
"account.unfollow": "取消關注",
"column_back_button.label": "先前顯示",
"column.community": "本站時間軸",
"column.home": "家",
"column.notifications": "通知",
"column.public": "跨站公共時間軸",
"compose_form.placeholder": "你在想甚麼?",
"compose_form.privacy_disclaimer": "你的私人文章,將被遞送至你所提及的 {domains} 用戶。你是否信任 {domainsCount, plural, one {這個網站} other {這些網站}}?請留意,文章私隱設定只適用於各 Mastodon 服務站,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服務站} other {之中有些不是 Mastodon 服務站}},對方將無法收到這篇文章的私隱設定,然後可能被轉推給不能預知的用戶閱讀。",
"compose_form.private": "標示為「只有關注你的人能看」",
"compose_form.publish": "發文",
"compose_form.sensitive": "將媒體檔案標示為「敏感內容」",
"compose_form.spoiler": "將部份文字藏於警告訊息之後",
"compose_form.unlisted": "請勿在公共時間軸顯示",
"empty_column.community": "本站時間軸暫時未有內容,快貼文來搶頭香啊!",
"empty_column.hashtag": "這個標籤暫時未有內容。",
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
"empty_column.home.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.",
"getting_started.about_addressing": "只要你知道一位用戶的用戶名稱和域名,你可以用「@用戶名稱@域名」的格式在搜尋欄尋找該用戶。",
"getting_started.about_shortcuts": "只要該用戶是在你現在的服務站開立,你可以直接輸入用戶𠱷搜尋。同樣的規則適用於在文章提及別的用戶。",
"getting_started.apps": "手機或桌面應用程式",
"getting_started.heading": "開始使用",
"getting_started.open_source_notice": "Mastodon 是一個開放源碼的軟件。你可以在官方 GitHub ({github}) 貢獻或者回報問題。你亦可透過{apps}閱讀 Mastodon 上的消息。",
"home.column_settings.basic": "基本",
"home.column_settings.show_reblogs": "顯示被轉推的文章",
"home.column_settings.show_replies": "顯示回應文章",
"home.column_settings.advanced": "進階",
"lightbox.close": "關閉",
"loading_indicator.label": "載入中...",
"missing_indicator.label": "找不到內容",
"navigation_bar.community_timeline": "本站時間軸",
"navigation_bar.edit_profile": "修改個人資料",
"navigation_bar.logout": "登出",
"navigation_bar.preferences": "個人設定",
"navigation_bar.public_timeline": "跨站公共時間軸",
"notification.favourite": "{name} 喜歡你的文章",
"notification.follow": "{name} 開始開始你",
"notification.mention": "{name} 提及你",
"notification.reblog": "{name} 轉推你的文章",
"notifications.column_settings.alert": "顯示桌面通知",
"notifications.column_settings.favourite": "喜歡你的文章:",
"notifications.column_settings.follow": "關注你:",
"notifications.column_settings.mention": "提及你:",
"notifications.column_settings.reblog": "轉推你的文章:",
"notifications.column_settings.show": "在通知欄顯示",
"notifications.column_settings.sound": "播放音效",
"reply_indicator.cancel": "取消",
"report.target": "Reporting",
"search.account": "用戶",
"search.hashtag": "標籤",
"search.placeholder": "搜尋",
"search_results.total": "{count} 項結果",
"search.status_by": "按用戶名稱搜尋文章",
"status.delete": "刪除",
"status.favourite": "喜歡",
"status.load_more": "載入更多",
"status.media_hidden": "隱藏媒體內容",
"status.mention": "提及 @{name}",
"status.open": "展開文章",
"status.reblog": "轉推",
"status.reblogged_by": "{name} 轉推",
"status.reply": "回應",
"status.report": "舉報 @{name}",
"status.sensitive_toggle": "點擊顯示",
"status.sensitive_warning": "敏感內容",
"status.show_less": "減少顯示",
"status.show_more": "顯示更多",
"tabs_bar.compose": "撰寫",
"tabs_bar.home": "家",
"tabs_bar.local_timeline": "本站",
"tabs_bar.mentions": "提及",
"tabs_bar.notifications": "通知",
"tabs_bar.public": "跨站公共時間軸",
"tabs_bar.federated_timeline": "跨站",
"upload_area.title": "將檔案拖放至此上載",
"upload_button.label": "上載媒體檔案",
"upload_progress.label": "上載中……",
"upload_form.undo": "還原",
"video_player.toggle_sound": "開關音效",
};
export default zh_hk;

View file

@ -67,6 +67,7 @@ function clearAll(state) {
map.set('is_submitting', false);
map.set('in_reply_to', null);
map.set('privacy', state.get('default_privacy'));
map.set('sensitive', false);
map.update('media_attachments', list => list.clear());
});
};
@ -76,7 +77,8 @@ function appendMedia(state, media) {
map.update('media_attachments', list => list.push(media));
map.set('is_uploading', false);
map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
map.update('text', oldText => `${oldText} ${media.get('text_url')}`.trim());
map.set('focusDate', new Date());
map.update('text', oldText => `${oldText.trim()} ${media.get('text_url')}`.trim() + ' ');
});
};
@ -156,6 +158,9 @@ export default function compose(state = initialState, action) {
if (action.status.get('spoiler_text').length > 0) {
map.set('spoiler', true);
map.set('spoiler_text', action.status.get('spoiler_text'));
} else {
map.set('spoiler', false);
map.set('spoiler_text', '');
}
});
case COMPOSE_REPLY_CANCEL:

View file

@ -4,7 +4,8 @@ import {
REPORT_SUBMIT_SUCCESS,
REPORT_SUBMIT_FAIL,
REPORT_CANCEL,
REPORT_STATUS_TOGGLE
REPORT_STATUS_TOGGLE,
REPORT_COMMENT_CHANGE
} from '../actions/reports';
import Immutable from 'immutable';
@ -39,6 +40,8 @@ export default function reports(state = initialState, action) {
return set.remove(action.statusId);
});
case REPORT_COMMENT_CHANGE:
return state.setIn(['new', 'comment'], action.comment);
case REPORT_SUBMIT_REQUEST:
return state.setIn(['new', 'isSubmitting'], true);
case REPORT_SUBMIT_FAIL:

View file

@ -114,10 +114,6 @@
padding: 20px;
}
.screenshot-with-signup .mascot {
display: none;
}
.features-list {
display: block;
}
@ -158,6 +154,14 @@
color: $color5;
}
}
@media screen and (max-width: 500px) {
flex-direction: column;
.section {
text-align: left;
}
}
}
.owner {
@ -281,6 +285,15 @@
}
}
}
@media screen and (max-width: 625px) {
flex-direction: column;
.sidebar {
border: 1px solid lighten($color1, 10%);
width: auto;
}
}
}
.features-list {
@ -319,7 +332,7 @@
}
}
.simple_form {
.simple_form, .closed-registrations-message {
width: 300px;
flex: 0 0 auto;
background: rgba(darken($color1, 7%), 0.5);
@ -339,4 +352,22 @@
}
}
}
@media screen and (max-width: 625px) {
.mascot {
display: none;
}
.simple_form, .closed-registrations-message {
flex: auto;
}
}
}
.closed-registrations-message {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}

View file

@ -14,7 +14,7 @@
}
&:after {
background: rgba($color8, 0.5);
background: linear-gradient(rgba($color8, 0.5), rgba($color8, 0.8));
display: block;
content: "";
position: absolute;
@ -34,6 +34,7 @@
text-align: center;
position: relative;
z-index: 2;
text-shadow: 0 0 2px $color8;
small {
display: block;
@ -82,7 +83,7 @@
.counter {
width: 80px;
color: $color3;
padding: 0 10px;
padding: 5px 10px 0px;
margin-bottom: 10px;
border-right: 1px solid $color3;
cursor: default;
@ -128,6 +129,7 @@
text-transform: uppercase;
display: block;
margin-bottom: 5px;
text-shadow: 0 0 2px $color8;
}
.counter-number {
@ -146,7 +148,7 @@
order: 1;
}
@media screen and (max-width: 360px) {
@media screen and (max-width: 480px) {
.details {
display: block;
}
@ -171,7 +173,7 @@
text-align: center;
overflow: hidden;
a, .current, .next_page, .previous_page, .gap {
a, .current, .page, .gap {
font-size: 14px;
color: $color5;
font-weight: 500;
@ -191,12 +193,12 @@
cursor: default;
}
.previous_page, .next_page {
.prev, .next {
text-transform: uppercase;
color: $color2;
}
.previous_page {
.prev {
float: left;
padding-left: 0;
@ -206,7 +208,7 @@
}
}
.next_page {
.next {
float: right;
padding-right: 0;
@ -224,11 +226,11 @@
@media screen and (max-width: 360px) {
padding: 30px 20px;
a, .current, .next_page, .previous_page, .gap {
a, .current, .next, .prev, .gap {
display: none;
}
.next_page, .previous_page {
.next, .prev {
display: inline-block;
}
}

View file

@ -4,305 +4,13 @@
@import 'fonts/montserrat';
@import 'font-awesome';
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-thumb {
background: lighten($color1, 4%);
border: 0px none $color5;
border-radius: 50px;
}
::-webkit-scrollbar-thumb:hover {
background: lighten($color1, 6%);
}
::-webkit-scrollbar-thumb:active {
background: lighten($color1, 4%);
}
::-webkit-scrollbar-track {
border: 0px none $color5;
border-radius: 0;
background: rgba($color8, 0.1);
}
::-webkit-scrollbar-track:hover {
background: $color1;
}
::-webkit-scrollbar-track:active {
background: $color1;
}
::-webkit-scrollbar-corner {
background: transparent;
}
body {
font-family: 'Roboto', sans-serif;
background: $color1 image-url('background-photo.jpeg');
background-size: cover;
background-attachment: fixed;
font-size: 13px;
line-height: 18px;
font-weight: 400;
color: $color5;
padding-bottom: 140px;
text-rendering: optimizelegibility;
font-feature-settings: "kern";
text-size-adjust: none;
&.app-body {
position: fixed;
width: 100%;
height: 100%;
padding: 0;
background: $color1;
}
&.embed {
background: transparent;
margin: 0;
.container {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
}
}
&.admin {
background: darken($color1, 4%);
position: fixed;
width: 100%;
height: 100%;
padding: 0;
}
@media screen and (max-width: 360px) {
padding-bottom: 0;
}
}
button:focus {
outline: none;
}
.app-holder {
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
}
.container {
width: 700px;
margin: 0 auto;
margin-top: 40px;
@media screen and (max-width: 700px) {
width: 100%;
margin: 0;
}
}
.logo-container {
max-width: 400px;
margin: 100px auto;
margin-bottom: 0;
cursor: default;
@media screen and (max-width: 360px) {
margin: 30px auto;
}
h1 {
display: block;
text-align: center;
color: $color5;
font-size: 48px;
font-weight: 500;
img {
display: block;
margin: 20px auto;
width: 180px;
height: 180px;
}
a {
color: inherit;
text-decoration: none;
outline: 0;
img {
opacity: 0.8;
transition: all 0.8s ease;
}
&:hover {
img {
opacity: 1;
transition-duration: 0.2s;
}
}
}
small {
display: block;
font-size: 12px;
font-weight: 400;
font-family: 'Roboto Mono', monospace;
}
}
}
.no-list {
list-style: none;
li {
display: inline-block;
margin: 0 5px;
}
}
.footer {
text-align: center;
margin-top: 30px;
font-size: 12px;
color: darken($color2, 25%);
.domain {
font-weight: 500;
a {
color: inherit;
text-decoration: none;
}
}
.powered-by {
font-weight: 400;
a {
color: inherit;
text-decoration: underline;
font-weight: 500;
&:hover {
text-decoration: none;
}
}
}
}
.compact-header {
h1 {
font-size: 24px;
line-height: 28px;
color: $color3;
overflow: hidden;
font-weight: 500;
margin-bottom: 20px;
a {
color: inherit;
text-decoration: none;
}
small {
font-weight: 400;
color: $color2;
}
img {
display: inline-block;
margin-bottom: -5px;
margin-right: 15px;
width: 36px;
height: 36px;
}
}
}
.landing-strip {
background: rgba(darken($color1, 7%), 0.8);
color: $color3;
font-weight: 400;
padding: 14px;
border-radius: 4px;
margin-bottom: 20px;
strong, a {
font-weight: 500;
}
a {
color: inherit;
text-decoration: underline;
}
}
@import 'reset';
@import 'basics';
@import 'containers';
@import 'lists';
@import 'footer';
@import 'compact_header';
@import 'landing_strip';
@import 'forms';
@import 'accounts';
@import 'stream_entries';

View file

@ -0,0 +1,58 @@
body {
font-family: 'Roboto', sans-serif;
background: $color1 image-url('background-photo.jpeg');
background-size: cover;
background-attachment: fixed;
font-size: 13px;
line-height: 18px;
font-weight: 400;
color: $color5;
padding-bottom: 140px;
text-rendering: optimizelegibility;
font-feature-settings: "kern";
text-size-adjust: none;
&.app-body {
position: fixed;
width: 100%;
height: 100%;
padding: 0;
background: $color1;
}
&.embed {
background: transparent;
margin: 0;
.container {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
}
}
&.admin {
background: darken($color1, 4%);
position: fixed;
width: 100%;
height: 100%;
padding: 0;
}
@media screen and (max-width: 360px) {
padding-bottom: 0;
}
}
button:focus {
outline: none;
}
.app-holder {
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,28 @@
.compact-header {
h1 {
font-size: 24px;
line-height: 28px;
color: $color3;
overflow: hidden;
font-weight: 500;
margin-bottom: 20px;
a {
color: inherit;
text-decoration: none;
}
small {
font-weight: 400;
color: $color2;
}
img {
display: inline-block;
margin-bottom: -5px;
margin-right: 15px;
width: 36px;
height: 36px;
}
}
}

View file

@ -1,5 +1,10 @@
@import 'variables';
.app-body{
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
}
.button {
background-color: darken($color4, 3%);
font-family: inherit;
@ -45,6 +50,22 @@
}
}
.column-icon-clear {
font-size: 16px;
padding: 15px;
position: absolute;
right: 48px;
top: 0;
cursor: pointer;
z-index: 2;
}
@media screen and (min-width: 1025px) {
.column-icon-clear {
top: 10px;
}
}
.icon-button {
display: inline-block;
padding: 0;
@ -91,6 +112,18 @@
color: $color3;
}
}
&.overlayed {
box-sizing: content-box;
background: rgba($color8, 0.6);
color: rgba($color5, 0.7);
border-radius: 4px;
padding: 2px;
&:hover {
background: rgba($color8, 0.9);
}
}
}
.text-icon-button {
@ -145,6 +178,14 @@
}
}
.avatar {
border-radius: 4px;
background: transparent no-repeat;
background-position: 50%;
background-clip: padding-box;
position: relative;
}
.lightbox .icon-button {
color: $color1;
}
@ -321,6 +362,43 @@ a.status__content__spoiler-link {
.status__display-name {
color: lighten($color1, 26%);
}
&.light {
.status__relative-time {
color: $color3;
}
.status__display-name {
color: $color1;
}
.display-name {
strong {
color: $color1;
}
span {
color: $color3;
}
}
.status__content {
color: $color1;
a {
color: $color4;
}
a.status__content__spoiler-link {
color: $color5;
background: $color3;
&:hover {
background: lighten($color3, 8%);
}
}
}
}
}
.status-check-box {
@ -639,6 +717,12 @@ a.status__content__spoiler-link {
left: 8px;
}
&.light {
&:before {
border-color: transparent transparent $color5 transparent;
}
}
& > ul {
list-style: none;
background: $color2;
@ -656,7 +740,7 @@ a.status__content__spoiler-link {
}
& > .emoji-dialog {
left: -249px;
left: -210px;
}
}
@ -710,7 +794,7 @@ a.status__content__spoiler-link {
@media screen and (min-width: 360px) {
.columns-area {
margin: 10px;
padding: 10px;
}
}
@ -718,9 +802,12 @@ a.status__content__spoiler-link {
width: 330px;
position: relative;
box-sizing: border-box;
background: $color1;
display: flex;
flex-direction: column;
> .scrollable {
background: $color1;
}
}
.ui {
@ -752,6 +839,58 @@ a.status__content__spoiler-link {
border-bottom: 2px solid transparent;
}
.column, .drawer {
flex: 1 1 100%;
overflow: hidden;
}
@media screen and (min-width: 360px) {
.tabs-bar {
margin: 10px;
margin-bottom: 0;
}
.search {
margin-bottom: 10px;
}
}
@media screen and (max-width: 1024px) {
.column, .drawer {
width: 100%;
padding: 0;
}
.columns-area {
flex-direction: column;
}
.search__input, .autosuggest-textarea__textarea {
font-size: 16px;
}
}
@media screen and (min-width: 1025px) {
.columns-area {
padding: 0;
}
.column, .drawer {
flex: 0 0 auto;
padding: 10px;
padding-left: 5px;
padding-right: 5px;
&:first-child {
padding-left: 10px;
}
&:last-child {
padding-right: 10px;
}
}
}
@media screen and (min-width: 2560px) {
.columns-area {
justify-content: center;
@ -811,37 +950,6 @@ a.status__content__spoiler-link {
}
}
.column, .drawer {
margin-left: 5px;
margin-right: 5px;
flex: 0 0 auto;
overflow: hidden;
}
.column:first-child, .drawer:first-child {
margin-left: 0;
}
.column:last-child, .drawer:last-child {
margin-right: 0;
}
@media screen and (max-width: 1024px) {
.column, .drawer {
width: 100%;
margin: 0;
flex: 1 1 100%;
}
.columns-area {
flex-direction: column;
}
.search__input, .autosuggest-textarea__textarea {
font-size: 16px;
}
}
.tabs-bar {
display: flex;
background: lighten($color1, 8%);
@ -852,17 +960,18 @@ a.status__content__spoiler-link {
.tabs-bar__link {
display: block;
flex: 1 1 auto;
padding: 10px 5px;
padding: 15px 10px;
color: $color5;
text-decoration: none;
text-align: center;
font-size:12px;
font-size: 14px;
font-weight: 500;
border-bottom: 2px solid lighten($color1, 8%);
transition: all 200ms linear;
.fa {
font-weight: 400;
font-size: 16px;
}
&.active {
@ -876,27 +985,13 @@ a.status__content__spoiler-link {
}
span {
margin-left: 5px;
display: none;
}
}
@media screen and (min-width: 360px) {
.tabs-bar {
margin: 10px;
margin-bottom: 0;
}
.search {
margin-bottom: 10px;
}
}
@media screen and (min-width: 600px) {
.tabs-bar__link {
.fa {
margin-right: 5px;
}
span {
display: inline;
}
@ -1149,10 +1244,9 @@ a.status__content__spoiler-link {
.getting-started {
box-sizing: border-box;
overflow-y: auto;
padding-bottom: 235px;
background: image-url('mastodon-getting-started.png') no-repeat 0 100% local;
height: 100%;
flex: 1 0 auto;
p {
color: $color2;
@ -1177,7 +1271,7 @@ a.status__content__spoiler-link {
@import 'boost';
button i.fa-retweet {
button.icon-button i.fa-retweet {
height: 19px;
width: 22px;
background-position: 0 0;
@ -1189,7 +1283,7 @@ button i.fa-retweet {
}
}
button.active i.fa-retweet {
button.icon-button.active i.fa-retweet {
transition-duration: 0.9s;
background-position: 0 100%;
}
@ -1359,12 +1453,15 @@ button.active i.fa-retweet {
.empty-column-indicator {
color: lighten($color1, 20%);
background: $color1;
text-align: center;
padding: 20px;
padding-top: 100px;
font-size: 15px;
font-weight: 400;
cursor: default;
display: flex;
flex: 1 1 auto;
align-items: center;
a {
color: $color4;
@ -1390,22 +1487,23 @@ button.active i.fa-retweet {
}
.emoji-dialog {
width: 280px;
height: 220px;
background: $color2;
width: 245px;
height: 270px;
background: $color5;
box-sizing: border-box;
border-radius: 2px;
border-radius: 4px;
overflow: hidden;
position: relative;
box-shadow: 0 0 15px rgba($color8, 0.4);
box-shadow: 0 0 8px rgba($color8, 0.2);
.emojione {
margin: 0;
width: 100%;
height: auto;
}
.emoji-dialog-header {
padding: 0 10px;
background-color: $color3;
ul {
padding: 0;
@ -1416,18 +1514,29 @@ button.active i.fa-retweet {
li {
display: inline-block;
box-sizing: border-box;
height: 42px;
padding: 9px 5px;
padding: 10px 5px;
cursor: pointer;
border-bottom: 2px solid transparent;
.emoji {
width: 18px;
height: 18px;
}
img, svg {
width: 22px;
height: 22px;
width: 18px;
height: 18px;
filter: grayscale(100%);
}
&:hover {
img, svg {
filter: grayscale(0);
}
}
&.active {
background: lighten($color3, 6%);
border-bottom-color: $color4;
img, svg {
filter: grayscale(0);
@ -1451,7 +1560,7 @@ button.active i.fa-retweet {
.emoji-category-header {
box-sizing: border-box;
overflow-y: hidden;
padding: 8px 16px 0;
padding: 10px 8px 10px 16px;
display: table;
> * {
@ -1461,10 +1570,10 @@ button.active i.fa-retweet {
}
.emoji-category-title {
font-size: 14px;
font-family: sans-serif;
font-weight: normal;
color: $color1;
font-size: 12px;
text-transform: uppercase;
font-weight: 500;
color: darken($color2, 18%);
cursor: default;
}
@ -1504,7 +1613,7 @@ button.active i.fa-retweet {
width: 7px;
height: 7px;
border-radius: 10px;
border: 2px solid $color1;
border: 2px solid $color5;
top: 2px;
left: 2px;
}
@ -1512,14 +1621,20 @@ button.active i.fa-retweet {
}
.emoji-search-wrapper {
padding: 6px 16px;
padding: 10px;
border-bottom: 1px solid lighten($color2, 4%);
}
.emoji-search {
font-size: 12px;
padding: 6px 4px;
font-size: 14px;
font-weight: 400;
padding: 7px 9px;
font-family: inherit;
display: block;
width: 100%;
border: 1px solid #ddd;
background: rgba($color2, 0.3);
color: darken($color2, 18%);
border: 1px solid $color2;
border-radius: 4px;
}
@ -1532,11 +1647,21 @@ button.active i.fa-retweet {
}
.emoji-search-wrapper + .emoji-categories-wrapper {
top: 83px;
top: 93px;
}
.emoji-row .emoji:hover {
background: lighten($color2, 3%);
.emoji-row .emoji {
img, svg {
transition: transform 60ms ease-in-out;
}
&:hover {
background: lighten($color2, 3%);
img, svg {
transform: translateZ(0) scale(1.2);
}
}
}
.emoji {
@ -1976,8 +2101,8 @@ button.active i.fa-retweet {
}
.onboarding-modal__page {
text-align: center;
cursor: default;
line-height: 21px;
h1 {
font-size: 18px;
@ -1995,11 +2120,10 @@ button.active i.fa-retweet {
}
p {
font-size: 18px;
font-size: 16px;
color: lighten($color1, 8%);
margin-top: 10px;
margin-bottom: 10px;
line-height: 10x;
&:last-child {
margin-bottom: 0;
@ -2007,24 +2131,85 @@ button.active i.fa-retweet {
strong {
font-weight: 500;
background: $color1;
color: $color2;
border-radius: 4px;
font-size: 14px;
padding: 3px 6px;
}
}
}
.onboarding-modal__image {
border-radius: 8px;
width: 70vw;
max-width: 450px;
max-height: auto;
display: block;
margin: auto;
.onboarding-modal__page-one {
display: flex;
}
.onboarding-modal__page-one__elephant-friend {
background: image-url('elephant-friend.png') no-repeat 0 0;
width: 147px;
height: 160px;
margin-right: 10px;
}
.onboarding-modal__page-two__compose {
position: relative;
height: 300px;
& > div {
position: absolute;
}
}
.onboarding-modal__image {
border-radius: 8px;
width: 70vw;
max-width: 450px;
max-height: auto;
display: block;
margin: auto;
}
.onboard-sliders {
display: inline-block;
max-width: 30px;
max-height: auto;
margin-left: 10px;
display: inline-block;
max-width: 30px;
max-height: auto;
margin-left: 10px;
}
.boost-modal {
background: lighten($color2, 8%);
color: $color1;
border-radius: 8px;
overflow: hidden;
max-width: 90vw;
width: 480px;
position: relative;
flex-direction: column;
}
.boost-modal__container {
padding: 10px;
.status {
user-select: text;
border-bottom: 0;
}
}
.boost-modal__action-bar {
display: flex;
background: $color2;
padding: 10px;
line-height: 36px;
& > div {
flex: 1 1 auto;
text-align: right;
color: lighten($color1, 33%);
padding-right: 10px;
}
.button {
flex: 0 0 auto;
}
}

View file

@ -0,0 +1,61 @@
.container {
width: 700px;
margin: 0 auto;
margin-top: 40px;
@media screen and (max-width: 700px) {
width: 100%;
margin: 0;
}
}
.logo-container {
max-width: 400px;
margin: 100px auto;
margin-bottom: 0;
cursor: default;
@media screen and (max-width: 360px) {
margin: 30px auto;
}
h1 {
display: block;
text-align: center;
color: $color5;
font-size: 48px;
font-weight: 500;
img {
display: block;
margin: 20px auto;
width: 180px;
height: 180px;
}
a {
color: inherit;
text-decoration: none;
outline: 0;
img {
opacity: 0.8;
transition: all 0.8s ease;
}
&:hover {
img {
opacity: 1;
transition-duration: 0.2s;
}
}
}
small {
display: block;
font-size: 12px;
font-weight: 400;
font-family: 'Roboto Mono', monospace;
}
}
}

View file

@ -0,0 +1,29 @@
.footer {
text-align: center;
margin-top: 30px;
font-size: 12px;
color: darken($color2, 25%);
.domain {
font-weight: 500;
a {
color: inherit;
text-decoration: none;
}
}
.powered-by {
font-weight: 400;
a {
color: inherit;
text-decoration: underline;
font-weight: 500;
&:hover {
text-decoration: none;
}
}
}
}

View file

@ -25,6 +25,10 @@ code {
margin-bottom: 15px;
}
strong {
font-weight: 500;
}
.label_input {
display: flex;
@ -84,7 +88,7 @@ code {
}
}
input[type=text], input[type=email], input[type=password], textarea {
input[type=text], input[type=number], input[type=email], input[type=password], textarea {
background: transparent;
box-sizing: border-box;
border: 0;
@ -224,7 +228,12 @@ code {
}
}
.qr-wrapper {
display: flex;
}
.qr-code {
flex: 0 0 auto;
background: #fff;
padding: 4px;
margin-bottom: 20px;
@ -236,3 +245,13 @@ code {
margin: 0;
}
}
.qr-alternative {
margin-left: 10px;
color: $color3;
samp {
display: block;
font-size: 14px;
}
}

View file

@ -0,0 +1,17 @@
.landing-strip {
background: rgba(darken($color1, 7%), 0.8);
color: $color3;
font-weight: 400;
padding: 14px;
border-radius: 4px;
margin-bottom: 20px;
strong, a {
font-weight: 500;
}
a {
color: inherit;
text-decoration: underline;
}
}

View file

@ -0,0 +1,8 @@
.no-list {
list-style: none;
li {
display: inline-block;
margin: 0 5px;
}
}

View file

@ -0,0 +1,91 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-thumb {
background: lighten($color1, 4%);
border: 0px none $color5;
border-radius: 50px;
}
::-webkit-scrollbar-thumb:hover {
background: lighten($color1, 6%);
}
::-webkit-scrollbar-thumb:active {
background: lighten($color1, 4%);
}
::-webkit-scrollbar-track {
border: 0px none $color5;
border-radius: 0;
background: rgba($color8, 0.1);
}
::-webkit-scrollbar-track:hover {
background: $color1;
}
::-webkit-scrollbar-track:active {
background: $color1;
}
::-webkit-scrollbar-corner {
background: transparent;
}

View file

@ -218,6 +218,7 @@
margin-top: 8px;
height: 300px;
overflow: hidden;
position: relative;
video {
position: relative;

View file

@ -2,28 +2,25 @@
class AboutController < ApplicationController
before_action :set_body_classes
before_action :set_instance_presenter, only: [:show, :more]
def index
@description = Setting.site_description
def show; end
@user = User.new
@user.build_account
end
def more
@description = Setting.site_description
@extended_description = Setting.site_extended_description
@contact_account = Account.find_local(Setting.site_contact_username)
@contact_email = Setting.site_contact_email
@user_count = Rails.cache.fetch('user_count') { User.count }
@status_count = Rails.cache.fetch('local_status_count') { Status.local.count }
@domain_count = Rails.cache.fetch('distinct_domain_count') { Account.distinct.count(:domain) }
end
def more; end
def terms; end
private
def new_user
User.new.tap(&:build_account)
end
helper_method :new_user
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
def set_body_classes
@body_classes = 'about-body'
end

View file

@ -16,7 +16,8 @@ class AccountsController < ApplicationController
end
format.atom do
@entries = @account.stream_entries.order('id desc').where(activity_type: 'Status').where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
@entries = @account.stream_entries.order('id desc').where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
render xml: AtomSerializer.render(AtomSerializer.new.feed(@account, @entries.to_a))
end
format.activitystreams2
@ -34,11 +35,11 @@ class AccountsController < ApplicationController
end
def followers
@followers = @account.followers.order('follows.created_at desc').paginate(page: params[:page], per_page: 12)
@followers = @account.followers.order('follows.created_at desc').page(params[:page]).per(12)
end
def following
@following = @account.following.order('follows.created_at desc').paginate(page: params[:page], per_page: 12)
@following = @account.following.order('follows.created_at desc').page(params[:page]).per(12)
end
private
@ -52,7 +53,7 @@ class AccountsController < ApplicationController
end
def webfinger_account_url
webfinger_url(resource: "acct:#{@account.acct}@#{Rails.configuration.x.local_domain}")
webfinger_url(resource: @account.to_webfinger_s)
end
def check_account_suspension

View file

@ -1,51 +1,30 @@
# frozen_string_literal: true
class Admin::AccountsController < ApplicationController
before_action :require_admin!
before_action :set_account, except: :index
module Admin
class AccountsController < BaseController
def index
@accounts = filtered_accounts.page(params[:page])
end
layout 'admin'
def show
@account = Account.find(params[:id])
end
def index
@accounts = Account.alphabetic.paginate(page: params[:page], per_page: 40)
private
@accounts = @accounts.local if params[:local].present?
@accounts = @accounts.remote if params[:remote].present?
@accounts = @accounts.where(domain: params[:by_domain]) if params[:by_domain].present?
@accounts = @accounts.silenced if params[:silenced].present?
@accounts = @accounts.recent if params[:recent].present?
@accounts = @accounts.suspended if params[:suspended].present?
end
def filtered_accounts
AccountFilter.new(filter_params).results
end
def show; end
def suspend
Admin::SuspensionWorker.perform_async(@account.id)
redirect_to admin_accounts_path
end
def unsuspend
@account.update(suspended: false)
redirect_to admin_accounts_path
end
def silence
@account.update(silenced: true)
redirect_to admin_accounts_path
end
def unsilence
@account.update(silenced: false)
redirect_to admin_accounts_path
end
private
def set_account
@account = Account.find(params[:id])
end
def account_params
params.require(:account).permit(:silenced, :suspended)
def filter_params
params.permit(
:local,
:remote,
:by_domain,
:silenced,
:recent,
:suspended
)
end
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
module Admin
class BaseController < ApplicationController
before_action :require_admin!
layout 'admin'
end
end

View file

@ -1,14 +1,30 @@
# frozen_string_literal: true
class Admin::DomainBlocksController < ApplicationController
before_action :require_admin!
module Admin
class DomainBlocksController < BaseController
def index
@blocks = DomainBlock.page(params[:page])
end
layout 'admin'
def new
@domain_block = DomainBlock.new
end
def index
@blocks = DomainBlock.paginate(page: params[:page], per_page: 40)
end
def create
@domain_block = DomainBlock.new(resource_params)
def create
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id)
redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed'
else
render action: :new
end
end
private
def resource_params
params.require(:domain_block).permit(:domain, :severity)
end
end
end

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