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: engines:
duplication: duplication:
enabled: true enabled: true
exclude_paths:
- app/assets/javascripts/components/locales/
config: config:
languages: languages:
- ruby - ruby

View file

@ -5,3 +5,4 @@ public/assets
node_modules node_modules
storybook storybook
neo4j 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 # SINGLE_USER_MODE=true
# Prevent registrations with following e-mail domains # Prevent registrations with following e-mail domains
# EMAIL_DOMAIN_BLACKLIST=example1.com|example2.de|etc # 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 # E-mail configuration
# Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers
SMTP_SERVER=smtp.mailgun.org SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=587 SMTP_PORT=587
SMTP_LOGIN= SMTP_LOGIN=
SMTP_PASSWORD= SMTP_PASSWORD=
SMTP_FROM_ADDRESS=notifications@example.com 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 # Optional asset host for multi-server setups
# CDN_HOST=assets.example.com # CDN_HOST=assets.example.com
@ -42,8 +53,22 @@ SMTP_FROM_ADDRESS=notifications@example.com
# S3_PROTOCOL=http # S3_PROTOCOL=http
# S3_HOSTNAME=192.168.1.123:9000 # 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 # Optional alias for S3 if you want to use Cloudfront or Cloudflare in front
# S3_CLOUDFRONT_HOST= # S3_CLOUDFRONT_HOST=
# Streaming API integration # Streaming API integration
# STREAMING_API_BASE_URL= # 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 # Ignore Capistrano customizations
config/deploy/* 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 postgresql: 9.4
rvm: rvm:
- 2.3.1 - 2.4.1
services: services:
- redis-server - 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 2. By working on the back-end application
3. By working on the front-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: 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 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. 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 \ ENV RAILS_ENV=production \
NODE_ENV=production NODE_ENV=production
EXPOSE 3000 4000
WORKDIR /mastodon WORKDIR /mastodon
COPY . /mastodon COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
RUN BUILD_DEPS=" \ RUN BUILD_DEPS=" \
postgresql-dev \ postgresql-dev \
@ -24,8 +29,11 @@ RUN BUILD_DEPS=" \
&& npm install -g npm@3 && npm install -g yarn \ && npm install -g npm@3 && npm install -g yarn \
&& bundle install --deployment --without test development \ && bundle install --deployment --without test development \
&& yarn \ && yarn \
&& npm cache clean \ && yarn cache clean \
&& npm -g cache clean \
&& apk del $BUILD_DEPS \ && apk del $BUILD_DEPS \
&& rm -rf /tmp/* /var/cache/apk/* && rm -rf /tmp/* /var/cache/apk/*
COPY . /mastodon
VOLUME /mastodon/public/system /mastodon/public/assets VOLUME /mastodon/public/system /mastodon/public/assets

49
Gemfile
View file

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

View file

@ -24,7 +24,7 @@ GEM
erubis (~> 2.7.0) erubis (~> 2.7.0)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3) 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) activejob (5.0.2)
activesupport (= 5.0.2) activesupport (= 5.0.2)
globalid (>= 0.3.6) globalid (>= 0.3.6)
@ -39,7 +39,7 @@ GEM
i18n (~> 0.7) i18n (~> 0.7)
minitest (~> 5.1) minitest (~> 5.1)
tzinfo (~> 1.1) tzinfo (~> 1.1)
addressable (2.5.0) addressable (2.5.1)
public_suffix (~> 2.0, >= 2.0.2) public_suffix (~> 2.0, >= 2.0.2)
airbrussh (1.1.2) airbrussh (1.1.2)
sshkit (>= 1.6.1, != 1.7.0) sshkit (>= 1.6.1, != 1.7.0)
@ -47,17 +47,17 @@ GEM
ast (2.3.0) ast (2.3.0)
attr_encrypted (3.0.3) attr_encrypted (3.0.3)
encryptor (~> 3.0.0) encryptor (~> 3.0.0)
autoprefixer-rails (6.5.0.2) autoprefixer-rails (6.7.7.1)
execjs execjs
av (0.9.0) av (0.9.0)
cocaine (~> 0.5.3) cocaine (~> 0.5.3)
aws-sdk (2.6.28) aws-sdk (2.9.6)
aws-sdk-resources (= 2.6.28) aws-sdk-resources (= 2.9.6)
aws-sdk-core (2.6.28) aws-sdk-core (2.9.6)
aws-sigv4 (~> 1.0) aws-sigv4 (~> 1.0)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-resources (2.6.28) aws-sdk-resources (2.9.6)
aws-sdk-core (= 2.6.28) aws-sdk-core (= 2.9.6)
aws-sigv4 (1.0.0) aws-sigv4 (1.0.0)
babel-source (5.8.35) babel-source (5.8.35)
babel-transpiler (0.7.0) babel-transpiler (0.7.0)
@ -78,12 +78,11 @@ GEM
railties (>= 4.0.0, < 5.1) railties (>= 4.0.0, < 5.1)
sprockets (>= 3.6.0) sprockets (>= 3.6.0)
builder (3.2.3) builder (3.2.3)
bullet (5.3.0) bullet (5.5.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0) uniform_notifier (~> 1.10.0)
capistrano (3.7.2) capistrano (3.8.0)
airbrussh (>= 1.0.0) airbrussh (>= 1.0.0)
capistrano-harrow
i18n i18n
rake (>= 10.0.0) rake (>= 10.0.0)
sshkit (>= 1.9.0) sshkit (>= 1.9.0)
@ -92,8 +91,7 @@ GEM
sshkit (~> 1.2) sshkit (~> 1.2)
capistrano-faster-assets (1.0.2) capistrano-faster-assets (1.0.2)
capistrano (>= 3.1) capistrano (>= 3.1)
capistrano-harrow (0.5.3) capistrano-rails (1.2.3)
capistrano-rails (1.2.2)
capistrano (~> 3.1) capistrano (~> 3.1)
capistrano-bundler (~> 1.1) capistrano-bundler (~> 1.1)
capistrano-rbenv (2.1.0) capistrano-rbenv (2.1.0)
@ -119,7 +117,7 @@ GEM
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
debug_inspector (0.0.2) debug_inspector (0.0.2)
devise (4.2.0) devise (4.2.1)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0, < 5.1) railties (>= 4.1.0, < 5.1)
@ -131,16 +129,16 @@ GEM
devise (~> 4.0) devise (~> 4.0)
railties railties
rotp (~> 2.0) rotp (~> 2.0)
diff-lcs (1.2.5) diff-lcs (1.3)
docile (1.1.5) docile (1.1.5)
domain_name (0.5.20161129) domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.0) doorkeeper (4.2.5)
railties (>= 4.2) railties (>= 4.2)
dotenv (2.1.1) dotenv (2.2.0)
dotenv-rails (2.1.1) dotenv-rails (2.2.0)
dotenv (= 2.1.1) dotenv (= 2.2.0)
railties (>= 4.0, < 5.1) railties (>= 3.2, < 5.1)
easy_translate (0.5.0) easy_translate (0.5.0)
json json
thread thread
@ -148,12 +146,14 @@ GEM
encryptor (3.0.0) encryptor (3.0.0)
erubis (2.7.0) erubis (2.7.0)
execjs (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) fast_blank (1.0.0)
font-awesome-rails (4.6.3.1) font-awesome-rails (4.7.0.1)
railties (>= 3.2, < 5.1) railties (>= 3.2, < 5.1)
fuubar (2.1.1) fuubar (2.2.0)
rspec (~> 3.0) rspec-core (~> 3.0)
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
globalid (0.3.7) globalid (0.3.7)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
@ -161,20 +161,20 @@ GEM
addressable (~> 2.4) addressable (~> 2.4)
http (~> 2.0) http (~> 2.0)
nokogiri (~> 1.6) nokogiri (~> 1.6)
hamlit (2.7.2) hamlit (2.8.1)
temple (~> 0.7.6) temple (>= 0.8.0)
thor thor
tilt tilt
hamlit-rails (0.1.0) hamlit-rails (0.2.0)
actionpack (>= 4.0.1) actionpack (>= 4.0.1)
activesupport (>= 4.0.1) activesupport (>= 4.0.1)
hamlit (>= 1.2.0) hamlit (>= 1.2.0)
railties (>= 4.0.1) railties (>= 4.0.1)
hashdiff (0.3.0) hashdiff (0.3.2)
highline (1.7.8) highline (1.7.8)
hiredis (0.6.1) hiredis (0.6.1)
htmlentities (4.3.4) htmlentities (4.3.4)
http (2.1.0) http (2.2.1)
addressable (~> 2.3) addressable (~> 2.3)
http-cookie (~> 1.0) http-cookie (~> 1.0)
http-form_data (~> 1.0.1) http-form_data (~> 1.0.1)
@ -182,11 +182,12 @@ GEM
http-cookie (1.0.3) http-cookie (1.0.3)
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (1.0.1) http-form_data (1.0.1)
http_accept_language (2.1.0)
http_parser.rb (0.6.0) http_parser.rb (0.6.0)
httplog (0.3.2) httplog (0.99.2)
colorize colorize
i18n (0.8.1) i18n (0.8.1)
i18n-tasks (0.9.6) i18n-tasks (0.9.13)
activesupport (>= 4.0.2) activesupport (>= 4.0.2)
ast (>= 2.1.0) ast (>= 2.1.0)
easy_translate (>= 0.5.0) easy_translate (>= 0.5.0)
@ -194,22 +195,31 @@ GEM
highline (>= 1.7.3) highline (>= 1.7.3)
i18n i18n
parser (>= 2.2.3.0) parser (>= 2.2.3.0)
term-ansicolor (>= 1.3.2) rainbow (~> 2.2)
terminal-table (>= 1.5.1) terminal-table (>= 1.5.1)
jbuilder (2.6.0)
activesupport (>= 3.0.0, < 5.1)
multi_json (~> 1.2)
jmespath (1.3.1) jmespath (1.3.1)
jquery-rails (4.1.1) jquery-rails (4.3.1)
rails-dom-testing (>= 1, < 3) rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0) railties (>= 4.2.0)
thor (>= 0.14, < 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) launchy (2.4.3)
addressable (~> 2.3) addressable (~> 2.3)
letter_opener (1.4.1) letter_opener (1.4.1)
launchy (~> 2.2) launchy (~> 2.2)
letter_opener_web (1.3.0) letter_opener_web (1.3.1)
actionmailer (>= 3.2) actionmailer (>= 3.2)
letter_opener (~> 1.0) letter_opener (~> 1.0)
railties (>= 3.2) railties (>= 3.2)
@ -229,19 +239,19 @@ GEM
mimemagic (0.3.2) mimemagic (0.3.2)
mini_portile2 (2.1.0) mini_portile2 (2.1.0)
minitest (5.10.1) minitest (5.10.1)
multi_json (1.12.1)
net-scp (1.2.1) net-scp (1.2.1)
net-ssh (>= 2.6.5) net-ssh (>= 2.6.5)
net-ssh (4.0.1) net-ssh (4.1.0)
nio4r (2.0.0) nio4r (2.0.0)
nokogiri (1.7.1) nokogiri (1.7.1)
mini_portile2 (~> 2.1.0) mini_portile2 (~> 2.1.0)
oj (2.17.3) oj (2.18.5)
orm_adapter (0.5.0) orm_adapter (0.5.0)
ostatus2 (1.0.2) ostatus2 (1.0.2)
addressable (~> 2.4) addressable (~> 2.4)
http (~> 2.0) http (~> 2.0)
nokogiri (~> 1.6) nokogiri (~> 1.6)
ox (2.4.11)
paperclip (5.1.0) paperclip (5.1.0)
activemodel (>= 4.2.0) activemodel (>= 4.2.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
@ -251,26 +261,26 @@ GEM
paperclip-av-transcoder (0.6.4) paperclip-av-transcoder (0.6.4)
av (~> 0.9.0) av (~> 0.9.0)
paperclip (>= 2.5.2) paperclip (>= 2.5.2)
parser (2.3.1.2) parser (2.4.0.0)
ast (~> 2.2) ast (~> 2.2)
pg (0.18.4) pg (0.20.0)
pghero (1.6.2) pghero (1.6.4)
activerecord activerecord
powerpack (0.1.1) powerpack (0.1.1)
pry (0.10.4) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.8.1)
slop (~> 3.4) slop (~> 3.4)
pry-rails (0.3.4) pry-rails (0.3.6)
pry (>= 0.9.10) pry (>= 0.10.4)
public_suffix (2.0.4) public_suffix (2.0.5)
puma (3.6.0) puma (3.8.2)
rabl (0.13.1) rabl (0.13.1)
activesupport (>= 2.3.14) activesupport (>= 2.3.14)
rack (2.0.1) rack (2.0.1)
rack-attack (5.0.1) rack-attack (5.0.1)
rack rack
rack-cors (0.4.0) rack-cors (0.4.1)
rack-protection (1.5.3) rack-protection (1.5.3)
rack rack
rack-test (0.6.3) rack-test (0.6.3)
@ -288,6 +298,10 @@ GEM
bundler (>= 1.3.0, < 2.0) bundler (>= 1.3.0, < 2.0)
railties (= 5.0.2) railties (= 5.0.2)
sprockets-rails (>= 2.0.0) 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) rails-dom-testing (2.0.2)
activesupport (>= 4.2.0, < 6.0) activesupport (>= 4.2.0, < 6.0)
nokogiri (~> 1.6) nokogiri (~> 1.6)
@ -306,44 +320,37 @@ GEM
method_source method_source
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.1.0) rainbow (2.2.1)
rake (12.0.0) rake (12.0.0)
rdoc (4.2.2) react-rails (1.11.0)
json (~> 1.4)
react-rails (1.10.0)
babel-transpiler (>= 0.7.0) babel-transpiler (>= 0.7.0)
coffee-script-source (~> 1.8)
connection_pool connection_pool
execjs execjs
railties (>= 3.2) railties (>= 3.2)
tilt tilt
redis (3.3.2) redis (3.3.3)
redis-actionpack (5.0.0) redis-actionpack (5.0.1)
actionpack (>= 4.0.0, < 6) actionpack (>= 4.0, < 6)
redis-rack (~> 2.0.0.pre) redis-rack (>= 1, < 3)
redis-store (~> 1.2.0.pre) redis-store (>= 1.1.0, < 1.4.0)
redis-activesupport (5.0.1) redis-activesupport (5.0.2)
activesupport (>= 3, < 6) activesupport (>= 3, < 6)
redis-store (~> 1.2.0) redis-store (~> 1.3.0)
redis-rack (2.0.0) redis-rack (2.0.1)
rack (~> 2.0) rack (>= 2.0, < 3)
redis-store (~> 1.2.0) redis-store (>= 1.2, < 1.4)
redis-rails (5.0.1) redis-rails (5.0.2)
redis-actionpack (~> 5.0.0) redis-actionpack (>= 5.0, < 6)
redis-activesupport (~> 5.0.0) redis-activesupport (>= 5.0, < 6)
redis-store (~> 1.2.0) redis-store (>= 1.2, < 2)
redis-store (1.2.0) redis-store (1.3.0)
redis (>= 2.2) redis (>= 2.2)
responders (2.3.0) responders (2.3.0)
railties (>= 4.2.0, < 5.1) railties (>= 4.2.0, < 5.1)
rotp (2.1.2) rotp (2.1.2)
rqrcode (0.10.1) rqrcode (0.10.1)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rspec (3.5.0) rspec-core (3.5.4)
rspec-core (~> 3.5.0)
rspec-expectations (~> 3.5.0)
rspec-mocks (~> 3.5.0)
rspec-core (3.5.2)
rspec-support (~> 3.5.0) rspec-support (~> 3.5.0)
rspec-expectations (3.5.0) rspec-expectations (3.5.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
@ -351,7 +358,7 @@ GEM
rspec-mocks (3.5.0) rspec-mocks (3.5.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.5.0) rspec-support (~> 3.5.0)
rspec-rails (3.5.1) rspec-rails (3.5.2)
actionpack (>= 3.0) actionpack (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
railties (>= 3.0) railties (>= 3.0)
@ -359,40 +366,40 @@ GEM
rspec-expectations (~> 3.5.0) rspec-expectations (~> 3.5.0)
rspec-mocks (~> 3.5.0) rspec-mocks (~> 3.5.0)
rspec-support (~> 3.5.0) rspec-support (~> 3.5.0)
rspec-sidekiq (2.2.0) rspec-sidekiq (3.0.0)
rspec (~> 3.0, >= 3.0.0) rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0) sidekiq (>= 2.4.0)
rspec-support (3.5.0) rspec-support (3.5.0)
rubocop (0.42.0) rubocop (0.48.1)
parser (>= 2.3.1.1, < 3.0) parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1) powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0) rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1) unicode-display_width (~> 1.0, >= 1.0.1)
ruby-oembed (0.10.1) ruby-oembed (0.12.0)
ruby-progressbar (1.8.1) ruby-progressbar (1.8.1)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sass (3.4.22) sass (3.4.23)
sass-rails (5.0.6) sass-rails (5.0.6)
railties (>= 4.0.0, < 6) railties (>= 4.0.0, < 6)
sass (~> 3.1) sass (~> 3.1)
sprockets (>= 2.8, < 4.0) sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0) sprockets-rails (>= 2.0, < 4.0)
tilt (>= 1.1, < 3) tilt (>= 1.1, < 3)
sdoc (0.4.1) sidekiq (4.2.10)
json (~> 1.7, >= 1.7.7)
rdoc (~> 4.0)
sidekiq (4.2.7)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0) connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0) rack-protection (>= 1.5.0)
redis (~> 3.2, >= 3.2.1) 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) activesupport (>= 2.3.2)
simple_form (3.2.1) simple_form (3.4.0)
actionpack (> 4, < 5.1) actionpack (> 4, < 5.1)
activemodel (> 4, < 5.1) activemodel (> 4, < 5.1)
simplecov (0.12.0) simplecov (0.14.1)
docile (~> 1.1.0) docile (~> 1.1.0)
json (>= 1.8, < 3) json (>= 1.8, < 3)
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
@ -405,43 +412,39 @@ GEM
actionpack (>= 4.0) actionpack (>= 4.0)
activesupport (>= 4.0) activesupport (>= 4.0)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sshkit (1.11.5) sshkit (1.13.1)
net-scp (>= 1.1.2) net-scp (>= 1.1.2)
net-ssh (>= 2.8.0) net-ssh (>= 2.8.0)
statsd-instrument (2.1.2) statsd-instrument (2.1.2)
temple (0.7.7) temple (0.8.0)
term-ansicolor (1.4.0) terminal-table (1.7.3)
tins (~> 1.0) unicode-display_width (~> 1.1.1)
terminal-table (1.7.0)
unicode-display_width (~> 1.1)
thor (0.19.4) thor (0.19.4)
thread (0.2.2) thread (0.2.2)
thread_safe (0.3.6) thread_safe (0.3.6)
tilt (2.0.6) tilt (2.0.7)
tins (1.12.0)
twitter-text (1.14.5) twitter-text (1.14.5)
unf (~> 0.1.0) unf (~> 0.1.0)
tzinfo (1.2.2) tzinfo (1.2.3)
thread_safe (~> 0.1) thread_safe (~> 0.1)
tzinfo-data (1.2017.2) tzinfo-data (1.2017.2)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
uglifier (3.0.1) uglifier (3.2.0)
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.2) unf_ext (0.0.7.2)
unicode-display_width (1.1.0) unicode-display_width (1.1.3)
uniform_notifier (1.10.0) uniform_notifier (1.10.0)
warden (1.2.6) warden (1.2.7)
rack (>= 1.0) rack (>= 1.0)
webmock (2.1.0) webmock (2.3.2)
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff
websocket-driver (0.6.5) websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2) websocket-extensions (0.1.2)
will_paginate (3.1.0)
PLATFORMS PLATFORMS
ruby ruby
@ -467,6 +470,7 @@ DEPENDENCIES
doorkeeper doorkeeper
dotenv-rails dotenv-rails
fabrication fabrication
faker
fast_blank fast_blank
font-awesome-rails font-awesome-rails
fuubar fuubar
@ -475,10 +479,11 @@ DEPENDENCIES
hiredis hiredis
htmlentities htmlentities
http http
http_accept_language
httplog httplog
i18n-tasks (~> 0.9.6) i18n-tasks (~> 0.9.6)
jbuilder (~> 2.0)
jquery-rails jquery-rails
kaminari
letter_opener letter_opener
letter_opener_web letter_opener_web
link_header link_header
@ -486,6 +491,7 @@ DEPENDENCIES
nokogiri nokogiri
oj oj
ostatus2 ostatus2
ox
paperclip (~> 5.1) paperclip (~> 5.1)
paperclip-av-transcoder paperclip-av-transcoder
pg pg
@ -497,6 +503,7 @@ DEPENDENCIES
rack-cors rack-cors
rack-timeout rack-timeout
rails (~> 5.0.2) rails (~> 5.0.2)
rails-controller-testing
rails-settings-cached rails-settings-cached
rails_12factor rails_12factor
react-rails react-rails
@ -508,8 +515,8 @@ DEPENDENCIES
rubocop rubocop
ruby-oembed ruby-oembed
sass-rails (~> 5.0) sass-rails (~> 5.0)
sdoc (~> 0.4.0)
sidekiq sidekiq
sidekiq-unique-jobs
simple-navigation simple-navigation
simple_form simple_form
simplecov simplecov
@ -518,10 +525,9 @@ DEPENDENCIES
tzinfo-data tzinfo-data
uglifier (>= 1.3.0) uglifier (>= 1.3.0)
webmock webmock
will_paginate
RUBY VERSION RUBY VERSION
ruby 2.3.1p112 ruby 2.4.1p111
BUNDLED WITH 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 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 [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` 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 ## 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) - [Use this tool to find Twitter friends on Mastodon](https://mastodon-bridge.herokuapp.com)
- [API overview](docs/Using-the-API/API.md) - [API overview](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md)
- [Frequently Asked Questions](docs/Using-Mastodon/FAQ.md) - [Frequently Asked Questions](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md)
- [List of apps](docs/Using-Mastodon/Apps.md) - [List of apps](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md)
## Features ## Features
@ -65,23 +65,54 @@ Consult the example configuration file, `.env.production.sample` for the full li
## Running with Docker and Docker-Compose ## 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 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 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 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. **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. This approach makes updating to the latest version a real breeze.
git pull 1. `git pull` to download updates from the repository
2. `docker-compose build` to compile the Docker image out of the changed source files
To pull down the updates, re-run 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
docker-compose build 5. `docker-compose up -d` to re-create (restart) containers and pick up the changes
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.
## Deployment without Docker ## 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) ## Deployment on Heroku (experimental)
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) [![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 ## 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. 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 ## 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" export PATH="$HOME/.rbenv/bin::$PATH"
eval "$(rbenv init -)" 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 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 # Configure database
sudo -u postgres createuser -U postgres vagrant -s sudo -u postgres createuser -U postgres vagrant -s
sudo -u postgres createdb -U postgres mastodon_development 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| config.vm.provider :virtualbox do |vb|
vb.name = "mastodon" vb.name = "mastodon"
vb.customize ["modifyvm", :id, "--memory", "1024"] 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 end
config.vm.hostname = "mastodon.dev" 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 # This uses the vagrant-hostsupdater plugin, and lets you
# access the development site at http://mastodon.dev. # access the development site at http://mastodon.dev.
# To install: # To install:
# $ vagrant plugin install hostsupdater # $ vagrant plugin install vagrant-hostsupdater
if defined?(VagrantPlugins::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 config.hostsupdater.remove_on_suspend = false
end end
config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'vers=3', 'tcp']
# Otherwise, you can access the site at http://localhost:3000 # Otherwise, you can access the site at http://localhost:3000
config.vm.network :forwarded_port, guest: 80, host: 3000 config.vm.network :forwarded_port, guest: 80, host: 3000

View file

@ -26,6 +26,10 @@
"description": "The secret key base", "description": "The secret key base",
"generator": "secret" "generator": "secret"
}, },
"OTP_SECRET": {
"description": "One-time password secret",
"generator": "secret"
},
"SINGLE_USER_MODE": { "SINGLE_USER_MODE": {
"description": "Should the instance run in single user mode? (Disable registrations, redirect to front page)", "description": "Should the instance run in single user mode? (Disable registrations, redirect to front page)",
"value": "false", "value": "false",
@ -75,6 +79,18 @@
"SMTP_FROM_ADDRESS": { "SMTP_FROM_ADDRESS": {
"description": "Address to send emails from", "description": "Address to send emails from",
"required": false "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": [ "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 { updateTimeline } from './timelines';
import * as emojione from 'emojione';
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE'; export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST'; export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS'; export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
@ -72,9 +74,8 @@ export function mentionCompose(account, router) {
export function submitCompose() { export function submitCompose() {
return function (dispatch, getState) { return function (dispatch, getState) {
dispatch(submitComposeRequest()); dispatch(submitComposeRequest());
api(getState).post('/api/v1/statuses', { 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), in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')), media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')),
sensitive: getState().getIn(['compose', 'sensitive']), 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() { export function refreshNotifications() {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(refreshNotificationsRequest()); dispatch(refreshNotificationsRequest());
@ -61,6 +63,8 @@ export function refreshNotifications() {
params.since_id = ids.first().get('id'); params.since_id = ids.first().get('id');
} }
params.exclude_types = excludeTypesFromSettings(getState());
api(getState).get('/api/v1/notifications', { params }).then(response => { api(getState).get('/api/v1/notifications', { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next'); const next = getLinks(response).refs.find(link => link.rel === 'next');
@ -105,11 +109,11 @@ export function expandNotifications() {
dispatch(expandNotificationsRequest()); dispatch(expandNotificationsRequest());
api(getState).get(url, { const params = {};
params: {
limit: 5 params.exclude_types = excludeTypesFromSettings(getState());
}
}).then(response => { api(getState).get(url, params).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next'); const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null)); dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));

View file

@ -5,10 +5,10 @@ export function showOnboardingOnce() {
return (dispatch, getState) => { return (dispatch, getState) => {
const alreadySeen = getState().getIn(['settings', 'onboarded']); const alreadySeen = getState().getIn(['settings', 'onboarded']);
if (!alreadySeen) { //if (!alreadySeen) {
dispatch(openModal('ONBOARDING')); dispatch(openModal('ONBOARDING'));
dispatch(changeSetting(['onboarded'], true)); dispatch(changeSetting(['onboarded'], true));
dispatch(saveSettings()); 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_SUCCESS = 'REPORT_SUBMIT_SUCCESS';
export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL'; 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) { export function initReport(account, status) {
return { return {
@ -62,3 +63,10 @@ export function submitReportFail(error) {
error error
}; };
}; };
export function changeReportComment(comment) {
return {
type: REPORT_COMMENT_CHANGE,
comment
};
};

View file

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

View file

@ -65,7 +65,7 @@ const Account = React.createClass({
<div className='account'> <div className='account'>
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
<Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}> <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} /> <DisplayName account={account} />
</Permalink> </Permalink>

View file

@ -1,103 +1,18 @@
import PureRenderMixin from 'react-addons-pure-render-mixin'; 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({ const Avatar = React.createClass({
propTypes: { propTypes: {
src: React.PropTypes.string.isRequired, src: React.PropTypes.string.isRequired,
staticSrc: React.PropTypes.string,
size: React.PropTypes.number.isRequired, size: React.PropTypes.number.isRequired,
style: React.PropTypes.object, style: React.PropTypes.object,
animated: React.PropTypes.bool animate: React.PropTypes.bool
}, },
getDefaultProps () { getDefaultProps () {
return { return {
animated: true animate: false
}; };
}, },
@ -117,38 +32,30 @@ const Avatar = React.createClass({
this.setState({ hovering: false }); 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 () { render () {
const { src, size, staticSrc, animate } = this.props;
const { hovering } = this.state; const { hovering } = this.state;
if (this.props.animated) { const style = {
return ( ...this.props.style,
<div style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px` }}> width: `${size}px`,
<img src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ borderRadius: '4px' }} /> height: `${size}px`,
</div> backgroundSize: `${size}px ${size}px`
); };
if (hovering || animate) {
style.backgroundImage = `url(${src})`;
} else {
style.backgroundImage = `url(${staticSrc})`;
} }
return ( return (
<div onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px`, position: 'relative' }}> <div
<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' }} /> className='avatar'
<canvas ref={this.setCanvasRef} style={{ borderRadius: '4px', width: this.props.size, height: this.props.size, opacity: hovering ? '0' : '1' }} /> onMouseEnter={this.handleMouseEnter}
</div> onMouseLeave={this.handleMouseLeave}
style={style}
/>
); );
} }

View file

@ -15,6 +15,7 @@ const ColumnCollapsable = React.createClass({
propTypes: { propTypes: {
icon: React.PropTypes.string.isRequired, icon: React.PropTypes.string.isRequired,
title: React.PropTypes.string,
fullHeight: React.PropTypes.number.isRequired, fullHeight: React.PropTypes.number.isRequired,
children: React.PropTypes.node, children: React.PropTypes.node,
onCollapse: React.PropTypes.func onCollapse: React.PropTypes.func
@ -39,13 +40,13 @@ const ColumnCollapsable = React.createClass({
}, },
render () { render () {
const { icon, fullHeight, children } = this.props; const { icon, title, fullHeight, children } = this.props;
const { collapsed } = this.state; const { collapsed } = this.state;
const collapsedClassName = collapsed ? 'collapsable-collapsed' : 'collapsable'; const collapsedClassName = collapsed ? 'collapsable-collapsed' : 'collapsable';
return ( return (
<div style={{ position: 'relative' }}> <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 }) }}> <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 }) => {({ opacity, height }) =>

View file

@ -3,15 +3,43 @@ import PureRenderMixin from 'react-addons-pure-render-mixin';
const ExtendedVideoPlayer = React.createClass({ const ExtendedVideoPlayer = React.createClass({
propTypes: { 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], 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 () { render () {
return ( return (
<div> <div className='extended-video-player'>
<video src={this.props.src} autoPlay muted loop /> <video
ref={this.setRef}
src={this.props.src}
autoPlay
muted={this.props.muted}
controls={this.props.controls}
loop={!this.props.controls}
/>
</div> </div>
); );
}, },

View file

@ -13,7 +13,8 @@ const IconButton = React.createClass({
activeStyle: React.PropTypes.object, activeStyle: React.PropTypes.object,
disabled: React.PropTypes.bool, disabled: React.PropTypes.bool,
inverted: React.PropTypes.bool, inverted: React.PropTypes.bool,
animate: React.PropTypes.bool animate: React.PropTypes.bool,
overlay: React.PropTypes.bool
}, },
getDefaultProps () { getDefaultProps () {
@ -21,7 +22,8 @@ const IconButton = React.createClass({
size: 18, size: 18,
active: false, active: false,
disabled: false, disabled: false,
animate: false animate: false,
overlay: false
}; };
}, },
@ -31,7 +33,7 @@ const IconButton = React.createClass({
e.preventDefault(); e.preventDefault();
if (!this.props.disabled) { if (!this.props.disabled) {
this.props.onClick(); this.props.onClick(e);
} }
}, },
@ -39,7 +41,7 @@ const IconButton = React.createClass({
let style = { let style = {
fontSize: `${this.props.size}px`, fontSize: `${this.props.size}px`,
width: `${this.props.size * 1.28571429}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`, lineHeight: `${this.props.size}px`,
...this.props.style ...this.props.style
}; };
@ -48,13 +50,31 @@ const IconButton = React.createClass({
style = { ...style, ...this.props.activeStyle }; 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 ( 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 }}> <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 }) => {({ rotate }) =>
<button <button
aria-label={this.props.title} aria-label={this.props.title}
title={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} onClick={this.handleClick}
style={style}> style={style}>
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' /> <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 = { const spoilerButtonStyle = {
position: 'absolute', position: 'absolute',
top: '6px', top: '4px',
left: '8px', left: '4px',
zIndex: '100' zIndex: '100'
}; };
@ -232,8 +232,8 @@ const MediaGallery = React.createClass({
return ( return (
<div style={{ ...outerStyle, height: `${this.props.height}px` }}> <div style={{ ...outerStyle, height: `${this.props.height}px` }}>
<div style={spoilerButtonStyle}> <div style={{ ...spoilerButtonStyle, display: !this.state.visible ? 'none' : 'block' }}>
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleOpen} /> <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />
</div> </div>
{children} {children}

View file

@ -25,8 +25,10 @@ const Status = React.createClass({
onReblog: React.PropTypes.func, onReblog: React.PropTypes.func,
onDelete: React.PropTypes.func, onDelete: React.PropTypes.func,
onOpenMedia: React.PropTypes.func, onOpenMedia: React.PropTypes.func,
onOpenVideo: React.PropTypes.func,
onBlock: React.PropTypes.func, onBlock: React.PropTypes.func,
me: React.PropTypes.number, me: React.PropTypes.number,
boostModal: React.PropTypes.bool,
muted: 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.get('media_attachments').size > 0 && !this.props.muted) {
if (status.getIn(['media_attachments', 0, 'type']) === 'video') { 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 { } else {
media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />; 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' }}> <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' }}> <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> </div>
<DisplayName account={status.get('account')} /> <DisplayName account={status.get('account')} />

View file

@ -7,6 +7,7 @@ import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' }, delete: { id: 'status.delete', defaultMessage: 'Delete' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' }, block: { id: 'account.block', defaultMessage: 'Block @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' }, reply: { id: 'status.reply', defaultMessage: 'Reply' },
reblog: { id: 'status.reblog', defaultMessage: 'Reblog' }, reblog: { id: 'status.reblog', defaultMessage: 'Reblog' },
@ -28,6 +29,7 @@ const StatusActionBar = React.createClass({
onReblog: React.PropTypes.func, onReblog: React.PropTypes.func,
onDelete: React.PropTypes.func, onDelete: React.PropTypes.func,
onMention: React.PropTypes.func, onMention: React.PropTypes.func,
onMute: React.PropTypes.func,
onBlock: React.PropTypes.func, onBlock: React.PropTypes.func,
onReport: React.PropTypes.func, onReport: React.PropTypes.func,
me: React.PropTypes.number.isRequired, me: React.PropTypes.number.isRequired,
@ -44,8 +46,8 @@ const StatusActionBar = React.createClass({
this.props.onFavourite(this.props.status); this.props.onFavourite(this.props.status);
}, },
handleReblogClick () { handleReblogClick (e) {
this.props.onReblog(this.props.status); this.props.onReblog(this.props.status, e);
}, },
handleDeleteClick () { handleDeleteClick () {
@ -56,6 +58,10 @@ const StatusActionBar = React.createClass({
this.props.onMention(this.props.status.get('account'), this.context.router); this.props.onMention(this.props.status.get('account'), this.context.router);
}, },
handleMuteClick () {
this.props.onMute(this.props.status.get('account'));
},
handleBlockClick () { handleBlockClick () {
this.props.onBlock(this.props.status.get('account')); this.props.onBlock(this.props.status.get('account'));
}, },
@ -81,6 +87,7 @@ const StatusActionBar = React.createClass({
} else { } else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
menu.push(null); 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.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); 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) { if (mention) {
link.addEventListener('click', this.onMentionClick.bind(this, mention), false); 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] === '#')) { } 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); link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
} else if (media) { } else if (media) {
@ -125,7 +126,7 @@ const StatusContent = React.createClass({
<div style={{ display: hidden ? 'none' : 'block', ...directionStyle }} dangerouslySetInnerHTML={content} /> <div style={{ display: hidden ? 'none' : 'block', ...directionStyle }} dangerouslySetInnerHTML={content} />
</div> </div>
); );
} else { } else if (this.props.onClick) {
return ( return (
<div <div
className='status__content' className='status__content'
@ -135,6 +136,14 @@ const StatusContent = React.createClass({
dangerouslySetInnerHTML={content} 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({ const messages = defineMessages({
toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' }, 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 = { const videoStyle = {
@ -21,8 +22,8 @@ const videoStyle = {
const muteStyle = { const muteStyle = {
position: 'absolute', position: 'absolute',
top: '10px', top: '4px',
right: '10px', right: '4px',
color: 'white', color: 'white',
textShadow: "0px 1px 1px black, 1px 0px 1px black", textShadow: "0px 1px 1px black, 1px 0px 1px black",
opacity: '0.8', opacity: '0.8',
@ -54,8 +55,17 @@ const spoilerSubSpanStyle = {
const spoilerButtonStyle = { const spoilerButtonStyle = {
position: 'absolute', position: 'absolute',
top: '6px', top: '4px',
left: '8px', 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', color: 'white',
textShadow: "0px 1px 1px black, 1px 0px 1px black", textShadow: "0px 1px 1px black, 1px 0px 1px black",
zIndex: '100' zIndex: '100'
@ -68,7 +78,8 @@ const VideoPlayer = React.createClass({
height: React.PropTypes.number, height: React.PropTypes.number,
sensitive: React.PropTypes.bool, sensitive: React.PropTypes.bool,
intl: React.PropTypes.object.isRequired, intl: React.PropTypes.object.isRequired,
autoplay: React.PropTypes.bool autoplay: React.PropTypes.bool,
onOpenVideo: React.PropTypes.func.isRequired
}, },
getDefaultProps () { getDefaultProps () {
@ -116,6 +127,11 @@ const VideoPlayer = React.createClass({
}); });
}, },
handleExpand () {
this.video.pause();
this.props.onOpenVideo(this.props.media, this.video.currentTime);
},
setRef (c) { setRef (c) {
this.video = c; this.video = c;
}, },
@ -154,8 +170,14 @@ const VideoPlayer = React.createClass({
const { media, intl, width, height, sensitive, autoplay } = this.props; const { media, intl, width, height, sensitive, autoplay } = this.props;
let spoilerButton = ( let spoilerButton = (
<div style={spoilerButtonStyle} > <div style={{...spoilerButtonStyle, display: !this.state.visible ? 'none' : 'block'}} >
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} /> <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> </div>
); );
@ -164,7 +186,7 @@ const VideoPlayer = React.createClass({
if (this.state.hasAudio) { if (this.state.hasAudio) {
muteButton = ( muteButton = (
<div style={muteStyle}> <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> </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' }}> <div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
{spoilerButton} {spoilerButton}
{muteButton} {muteButton}
{expandButton}
<video ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} /> <video ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
</div> </div>
); );

View file

@ -42,11 +42,20 @@ import Report from '../features/report';
import { IntlProvider, addLocaleData } from 'react-intl'; import { IntlProvider, addLocaleData } from 'react-intl';
import en from 'react-intl/locale-data/en'; import en from 'react-intl/locale-data/en';
import de from 'react-intl/locale-data/de'; 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 es from 'react-intl/locale-data/es';
import fi from 'react-intl/locale-data/fi';
import fr from 'react-intl/locale-data/fr'; 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 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 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 getMessagesForLocale from '../locales';
import { hydrateStore } from '../actions/store'; import { hydrateStore } from '../actions/store';
import createStream from '../stream'; import createStream from '../stream';
@ -59,7 +68,24 @@ const browserHistory = useRouterHistory(createBrowserHistory)({
basename: '/web' 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({ const Mastodon = React.createClass({

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
const AutosuggestAccount = ({ account }) => ( const AutosuggestAccount = ({ account }) => (
<div style={{ overflow: 'hidden' }} className='autosuggest-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} /> <DisplayName account={account} />
</div> </div>
); );

View file

@ -19,7 +19,7 @@ import TextIconButton from './text_icon_button';
const messages = defineMessages({ const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Content warning' }, 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({ const ComposeForm = React.createClass({
@ -83,11 +83,23 @@ const ComposeForm = React.createClass({
this.props.onChangeSpoilerText(e.target.value); 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) { componentDidUpdate (prevProps) {
if (this.props.focusDate !== prevProps.focusDate) { // This statement does several things:
// If replying to zero or one users, places the cursor at the end of the textbox. // - If we're beginning a reply, and,
// If replying to more than one user, selects any usernames past the first; // - Replying to zero or one users, places the cursor at the end of the textbox.
// this provides a convenient shortcut to drop everyone else from the conversation. // - 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; let selectionEnd, selectionStart;
if (this.props.preselectDate !== prevProps.preselectDate) { if (this.props.preselectDate !== prevProps.preselectDate) {
@ -118,7 +130,7 @@ const ComposeForm = React.createClass({
render () { render () {
const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props; 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 publishText = '';
let privacyWarning = ''; let privacyWarning = '';

View file

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

View file

@ -17,7 +17,7 @@ const NavigationBar = React.createClass({
render () { render () {
return ( return (
<div className='navigation-bar'> <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' }}> <div style={{ flex: '1 1 auto', marginLeft: '8px' }}>
<strong style={{ fontWeight: '500', display: 'block' }}>{this.props.account.get('acct')}</strong> <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> <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' }}> <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')} /> <DisplayName account={status.get('account')} />
</a> </a>
</div> </div>

View file

@ -12,7 +12,7 @@ import SearchResultsContainer from './containers/search_results_container';
const messages = defineMessages({ const messages = defineMessages({
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, 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' }, community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' } logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }

View file

@ -33,7 +33,7 @@ const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
<div> <div>
<div style={outerStyle}> <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' }}> <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} /> <DisplayName account={account} />
</Permalink> </Permalink>

View file

@ -7,11 +7,11 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
const messages = defineMessages({ const messages = defineMessages({
heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, 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' }, community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, 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' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' } 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='scrollable optionally-scrollable' style={{ display: 'flex', flexDirection: 'column' }}>
<div className='static-content getting-started'> <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>
</div> </div>
</Column> </Column>

View file

@ -6,7 +6,8 @@ import SettingToggle from '../../notifications/components/setting_toggle';
import SettingText from './setting_text'; import SettingText from './setting_text';
const messages = defineMessages({ 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 = { const outerStyle = {
@ -39,7 +40,7 @@ const ColumnSettings = React.createClass({
const { settings, onChange, onSave, intl } = this.props; const { settings, onChange, onSave, intl } = this.props;
return ( 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}> <div className='column-settings--outer' style={outerStyle}>
<span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span> <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>

View file

@ -1,21 +1,25 @@
const iconStyle = { import { defineMessages, injectIntl } from 'react-intl';
fontSize: '16px',
padding: '15px',
position: 'absolute',
right: '48px',
top: '0',
cursor: 'pointer',
zIndex: '2'
};
const ClearColumnButton = ({ onClick }) => ( const messages = defineMessages({
<div className='column-icon' style={iconStyle} onClick={onClick}> clear: { id: 'notifications.clear', defaultMessage: 'Clear notifications' }
<i className='fa fa-trash' /> });
</div>
);
ClearColumnButton.propTypes = { const ClearColumnButton = React.createClass({
onClick: React.PropTypes.func.isRequired
};
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 PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes'; 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 ColumnCollapsable from '../../../components/column_collapsable';
import SettingToggle from './setting_toggle'; import SettingToggle from './setting_toggle';
const messages = defineMessages({
settings: { id: 'notifications.settings', defaultMessage: 'Column settings' }
});
const outerStyle = { const outerStyle = {
padding: '15px' padding: '15px'
}; };
@ -23,21 +27,22 @@ const ColumnSettings = React.createClass({
propTypes: { propTypes: {
settings: ImmutablePropTypes.map.isRequired, settings: ImmutablePropTypes.map.isRequired,
intl: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func.isRequired, onChange: React.PropTypes.func.isRequired,
onSave: React.PropTypes.func.isRequired onSave: React.PropTypes.func.isRequired,
}, },
mixins: [PureRenderMixin], mixins: [PureRenderMixin],
render () { 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 alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />; const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />; const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
return ( 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}> <div className='column-settings--outer' style={outerStyle}>
<span className='column-settings--section' style={sectionStyle}><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span> <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) { renderFollow (account, link) {
return ( return (
<div className='notification'> <div className='notification notification-follow'>
<div className='notification__message'> <div className='notification__message'>
<div style={{ position: 'absolute', 'left': '-26px'}}> <div style={{ position: 'absolute', 'left': '-26px'}}>
<i className='fa fa-fw fa-user-plus' /> <i className='fa fa-fw fa-user-plus' />
@ -41,7 +41,7 @@ const Notification = React.createClass({
renderFavourite (notification, link) { renderFavourite (notification, link) {
return ( return (
<div className='notification'> <div className='notification notification-favourite'>
<div className='notification__message'> <div className='notification__message'>
<div style={{ position: 'absolute', 'left': '-26px'}}> <div style={{ position: 'absolute', 'left': '-26px'}}>
<i className='fa fa-fw fa-star' style={{ color: '#ca8f04' }} /> <i className='fa fa-fw fa-star' style={{ color: '#ca8f04' }} />
@ -57,7 +57,7 @@ const Notification = React.createClass({
renderReblog (notification, link) { renderReblog (notification, link) {
return ( return (
<div className='notification'> <div className='notification notification-reblog'>
<div className='notification__message'> <div className='notification__message'>
<div style={{ position: 'absolute', 'left': '-26px'}}> <div style={{ position: 'absolute', 'left': '-26px'}}>
<i className='fa fa-fw fa-retweet' /> <i className='fa fa-fw fa-retweet' />
@ -76,17 +76,17 @@ const Notification = React.createClass({
const account = notification.get('account'); const account = notification.get('account');
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username'); const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; 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')) { switch(notification.get('type')) {
case 'follow': case 'follow':
return this.renderFollow(account, link); return this.renderFollow(account, link);
case 'mention': case 'mention':
return this.renderMention(notification); return this.renderMention(notification);
case 'favourite': case 'favourite':
return this.renderFavourite(notification, link); return this.renderFavourite(notification, link);
case 'reblog': case 'reblog':
return this.renderReblog(notification, link); return this.renderReblog(notification, link);
} }
} }

View file

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

View file

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

View file

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

View file

@ -17,7 +17,8 @@ const DetailedStatus = React.createClass({
propTypes: { propTypes: {
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
onOpenMedia: React.PropTypes.func.isRequired onOpenMedia: React.PropTypes.func.isRequired,
onOpenVideo: React.PropTypes.func.isRequired,
}, },
mixins: [PureRenderMixin], mixins: [PureRenderMixin],
@ -39,7 +40,7 @@ const DetailedStatus = React.createClass({
if (status.get('media_attachments').size > 0) { if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'video') { 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 { } else {
media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} />; 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 ( return (
<div style={{ padding: '14px 10px' }} className='detailed-status'> <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' }}> <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')} /> <DisplayName account={status.get('account')} />
</a> </a>

View file

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

View file

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

View file

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

View file

@ -6,47 +6,52 @@ import Permalink from '../../../components/permalink';
const PageOne = ({ acct, domain }) => ( const PageOne = ({ acct, domain }) => (
<div className='onboarding-modal__page onboarding-modal__page-one'> <div className='onboarding-modal__page onboarding-modal__page-one'>
<h1><FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to Mastodon!' /></h1> <div style={{ flex: '0 0 auto' }}>
<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> <div className='onboarding-modal__page-one__elephant-friend' />
<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>
<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> </div>
); );
const PageTwo = ( const PageTwo = () => (
<div className='onboarding-modal__page onboarding-modal__page-two'> <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> <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> </div>
); );
const PageThree = () => (
const PageThree = (
<div className='onboarding-modal__page onboarding-modal__page-three'> <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.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> </div>
); );
const PageFour = ( const PageFour = () => (
<div className='onboarding-modal__page onboarding-modal__page-four'> <div className='onboarding-modal__page onboarding-modal__page-four'>
<img className="onboarding-modal__image onboard-column" src={'/onboarding/onboard-home.jpg'}></img> <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> <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> <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.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> <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> </div>
); );
const PageFive = ( const PageFive = () => (
<div className='onboarding-modal__page onboarding-modal__page-five'> <div className='onboarding-modal__page onboarding-modal__page-five'>
<img className="onboarding-modal__image onboard-column" src={"/onboarding/onboard-getting-started.jpg"}></img> <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> <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> <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> <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> <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.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> <p><FormattedMessage id='onboarding.page_five.public' defaultMessage='These are the Public Timelines, a great way to find people to follow.' /></p>
</div> </div>
); );
@ -63,8 +68,6 @@ const PageSix = ({ admin }) => (
</div> </div>
); );
const mapStateToProps = state => ({ const mapStateToProps = state => ({
me: state.getIn(['accounts', state.getIn(['meta', 'me'])]), me: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]), admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]),
@ -114,10 +117,10 @@ const OnboardingModal = React.createClass({
const pages = [ const pages = [
<PageOne acct={me.get('acct')} domain={domain} />, <PageOne acct={me.get('acct')} domain={domain} />,
PageTwo, <PageTwo />,
PageThree, <PageThree />,
PageFour, <PageFour />,
PageFive, <PageFive />,
<PageSix admin={admin} /> <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", "column_back_button.label": "Zurück",
"lightbox.close": "Schließen", "lightbox.close": "Schließen",
"loading_indicator.label": "Lade...", "loading_indicator.label": "Lade",
"status.mention": "Erwähnen", "status.mention": "Erwähnen",
"status.delete": "Löschen", "status.delete": "Löschen",
"status.reply": "Antworten", "status.reply": "Antworten",
"status.reblog": "Teilen", "status.reblog": "Teilen",
"status.favourite": "Favorisieren", "status.favourite": "Favorisieren",
"status.reblogged_by": "{name} teilte", "status.reblogged_by": "{name} teilte",
"status.sensitive_warning": "Sensible Inhalte", "status.sensitive_warning": "Heikle Inhalte",
"status.sensitive_toggle": "Klicken um zu zeigen", "status.sensitive_toggle": "Klicke, um sie zu sehen",
"status.open": "Öffnen", "status.open": "Öffnen",
"video_player.toggle_sound": "Ton umschalten", "video_player.toggle_sound": "Ton umschalten",
"account.mention": "Erwähnen", "account.mention": "Erwähnen",
@ -20,17 +20,17 @@ const en = {
"account.follow": "Folgen", "account.follow": "Folgen",
"account.posts": "Beiträge", "account.posts": "Beiträge",
"account.follows": "Folgt", "account.follows": "Folgt",
"account.followers": "Folger", "account.followers": "Folgende",
"account.follows_you": "Folgt dir", "account.follows_you": "Folgt dir",
"account.requested": "Warte auf Erlaubnis", "account.requested": "Warte auf Erlaubnis",
"getting_started.heading": "Erste Schritte", "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_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 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_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.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.", "getting_started.open_source_notice": "Mastodon ist quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.",
"column.home": "Home", "column.home": "Home",
"column.mentions": "Erwähnungen", "column.mentions": "Erwähnungen",
"column.public": "Gesamtes Bekanntes Netz", "column.public": "Gesamtes bekanntes Netz",
"column.notifications": "Mitteilungen", "column.notifications": "Mitteilungen",
"column.follow_requests": "Folgeanfragen", "column.follow_requests": "Folgeanfragen",
"tabs_bar.compose": "Schreiben", "tabs_bar.compose": "Schreiben",
@ -38,11 +38,11 @@ const en = {
"tabs_bar.mentions": "Erwähnungen", "tabs_bar.mentions": "Erwähnungen",
"tabs_bar.public": "Gesamtes Netz", "tabs_bar.public": "Gesamtes Netz",
"tabs_bar.notifications": "Mitteilungen", "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.publish": "Tröt",
"compose_form.sensitive": "Medien als sensitiv markieren", "compose_form.sensitive": "Medien als heikel markieren",
"compose_form.unlisted": "Öffentlich nicht auflisten",
"compose_form.private": "Als privat markieren", "compose_form.private": "Als privat markieren",
"compose_form.unlisted": "Nicht öffentlich auflisten",
"navigation_bar.edit_profile": "Profil bearbeiten", "navigation_bar.edit_profile": "Profil bearbeiten",
"navigation_bar.preferences": "Einstellungen", "navigation_bar.preferences": "Einstellungen",
"navigation_bar.public_timeline": "Öffentlich", "navigation_bar.public_timeline": "Öffentlich",
@ -52,15 +52,15 @@ const en = {
"search.placeholder": "Suche", "search.placeholder": "Suche",
"search.account": "Konto", "search.account": "Konto",
"search.hashtag": "Hashtag", "search.hashtag": "Hashtag",
"upload_button.label": "Media-Datei anfügen", "upload_button.label": "Mediendatei hinzufügen",
"upload_form.undo": "Entfernen", "upload_form.undo": "Entfernen",
"notification.follow": "{name} folgt dir", "notification.follow": "{name} folgt dir",
"notification.favourite": "{name} favorisierte deinen Status", "notification.favourite": "{name} favorisierte deinen Status",
"notification.reblog": "{name} teilte deinen Status", "notification.reblog": "{name} teilte deinen Status",
"notification.mention": "{name} erwähnte dich", "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.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.favourite": "Favorisierungen:",
"notifications.column_settings.mention": "Erwähnungen:", "notifications.column_settings.mention": "Erwähnungen:",
"notifications.column_settings.reblog": "Geteilte Beiträge:", "notifications.column_settings.reblog": "Geteilte Beiträge:",
@ -74,4 +74,4 @@ const en = {
"missing_indicator.label": "Nicht gefunden" "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 = { 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.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.follow": "Follow",
"account.posts": "Posts",
"account.follows": "Follows",
"account.followers": "Followers", "account.followers": "Followers",
"account.follows_you": "Follows you", "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", "account.requested": "Awaiting approval",
"getting_started.heading": "Getting started", "account.unblock": "Unblock @{name}",
"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.", "account.unfollow": "Unfollow",
"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.", "account.unmute": "Unmute @{name}",
"getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on github at {github}. {apps}.", "boost_modal.combo": "You can press {combo} to skip this next time",
"column.home": "Home", "column_back_button.label": "Back",
"column.blocks": "Blocked users",
"column.community": "Local timeline", "column.community": "Local timeline",
"column.public": "Federated timeline", "column.favourites": "Favourites",
"column.follow_requests": "Follow requests",
"column.home": "Home",
"column.notifications": "Notifications", "column.notifications": "Notifications",
"tabs_bar.compose": "Compose", "column.public": "Federated timeline",
"tabs_bar.home": "Home",
"tabs_bar.mentions": "Mentions",
"tabs_bar.public": "Federated timeline",
"tabs_bar.notifications": "Notifications",
"compose_form.placeholder": "What is on your mind?", "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.publish": "Toot",
"compose_form.sensitive": "Mark media as sensitive", "compose_form.sensitive": "Mark media as sensitive",
"compose_form.spoiler_placeholder": "Content warning",
"compose_form.spoiler": "Hide text behind warning", "compose_form.spoiler": "Hide text behind warning",
"compose_form.private": "Mark as private", "emoji_button.label": "Insert emoji",
"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.", "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
"compose_form.unlisted": "Do not display on public timelines", "empty_column.hashtag": "There is nothing in this hashtag yet.",
"navigation_bar.edit_profile": "Edit profile", "empty_column.home.public_timeline": "the public timeline",
"navigation_bar.preferences": "Preferences", "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.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", "navigation_bar.logout": "Logout",
"reply_indicator.cancel": "Cancel", "navigation_bar.preferences": "Preferences",
"search.placeholder": "Search", "navigation_bar.public_timeline": "Federated timeline",
"search.account": "Account",
"search.hashtag": "Hashtag",
"upload_button.label": "Add media",
"upload_form.undo": "Undo",
"notification.follow": "{name} followed you",
"notification.favourite": "{name} favourited your status", "notification.favourite": "{name} favourited your status",
"notification.follow": "{name} followed you",
"notification.reblog": "{name} boosted your status", "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.alert": "Desktop notifications",
"notifications.column_settings.show": "Show in column",
"notifications.column_settings.follow": "New followers:",
"notifications.column_settings.favourite": "Favourites:", "notifications.column_settings.favourite": "Favourites:",
"notifications.column_settings.follow": "New followers:",
"notifications.column_settings.mention": "Mentions:", "notifications.column_settings.mention": "Mentions:",
"notifications.column_settings.reblog": "Boosts:", "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; 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.mention": "Mencionar",
"status.delete": "Borrar", "status.delete": "Borrar",
"status.reply": "Responder", "status.reply": "Responder",
"status.reblog": "Republicar", "status.reblog": "Retoot",
"status.favourite": "Favorito", "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", "video_player.toggle_sound": "Act/Desac. sonido",
"account.mention": "Mención", "account.mention": "Mencionar",
"account.edit_profile": "Editar perfil", "account.edit_profile": "Editar perfil",
"account.unblock": "Desbloquear", "account.unblock": "Desbloquear",
"account.unfollow": "Dejar de seguir", "account.unfollow": "Dejar de seguir",
"account.mute": "Silenciar",
"account.block": "Bloquear", "account.block": "Bloquear",
"account.follow": "Seguir", "account.follow": "Seguir",
"account.block": "Bloquear",
"account.posts": "Publicaciones", "account.posts": "Publicaciones",
"account.follows": "Seguir", "account.follows": "Seguir",
"account.followers": "Seguidores", "account.followers": "Seguidores",
"account.follows_you": "Te sigue", "account.follows_you": "Te sigue",
"account.requested": "Esperando aprobación",
"getting_started.heading": "Primeros pasos", "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_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_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.home": "Inicio",
"column.mentions": "Menciones", "column.community": "Historia local",
"column.public": "Historia pública", "column.public": "Historia federada",
"column.notifications": "Notificaciones", "column.notifications": "Notificaciones",
"tabs_bar.compose": "Redactar", "tabs_bar.compose": "Redactar",
"tabs_bar.home": "Inicio", "tabs_bar.home": "Inicio",
@ -34,23 +41,47 @@ const es = {
"tabs_bar.public": "Público", "tabs_bar.public": "Público",
"tabs_bar.notifications": "Notificaciones", "tabs_bar.notifications": "Notificaciones",
"compose_form.placeholder": "¿En qué estás pensando?", "compose_form.placeholder": "¿En qué estás pensando?",
"compose_form.publish": "Publicar", "compose_form.publish": "Tootear",
"compose_form.sensitive": "Marcar el contenido como sensible", "compose_form.sensitive": "Marcar contenido como sensible",
"compose_form.unlisted": "Privado", "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.edit_profile": "Editar perfil",
"navigation_bar.preferences": "Preferencias", "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", "navigation_bar.logout": "Cerrar sesión",
"reply_indicator.cancel": "Cancelar", "reply_indicator.cancel": "Cancelar",
"search.placeholder": "Buscar", "search.placeholder": "Buscar",
"search.account": "Cuenta", "search.account": "Cuenta",
"search.hashtag": "Etiqueta", "search.hashtag": "Etiqueta",
"upload_button.label": "Añadir medio", "upload_button.label": "Subir multimedia",
"upload_form.undo": "Deshacer", "upload_form.undo": "Deshacer",
"notification.follow": "{name} le esta ahora siguiendo", "notification.follow": "{name} te empezó a seguir",
"notification.favourite": "{name} marcó como favorito su estado", "notification.favourite": "{name} marcó tu estado como favorito",
"notification.reblog": "{name} volvió a publicar su estado", "notification.reblog": "{name} ha retooteado tu estado",
"notification.mention": "Fue mencionado por {name}" "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; 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.reblogged_by": "{name} a partagé :",
"status.sensitive_warning": "Contenu délicat", "status.sensitive_warning": "Contenu délicat",
"status.sensitive_toggle": "Cliquer pour dévoiler", "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_sound": "Mettre/Couper le son",
"video_player.toggle_visible": "Afficher/Cacher la vidéo",
"account.mention": "Mentionner", "account.mention": "Mentionner",
"account.edit_profile": "Modifier le profil", "account.edit_profile": "Modifier le profil",
"account.unblock": "Débloquer", "account.unblock": "Débloquer",
@ -27,7 +34,7 @@ const fr = {
"account.report": "Signaler", "account.report": "Signaler",
"account.disclaimer": "Ce compte est situé sur une autre instance. Les nombres peuvent être plus grands.", "account.disclaimer": "Ce compte est situé sur une autre instance. Les nombres peuvent être plus grands.",
"getting_started.heading": "Pour commencer", "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_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.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.", "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.community": "Fil public local",
"column.public": "Fil public global", "column.public": "Fil public global",
"column.notifications": "Notifications", "column.notifications": "Notifications",
"column.public": "Fil public",
"column.blocks": "Utilisateurs bloqués", "column.blocks": "Utilisateurs bloqués",
"column.favourites": "Favoris", "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.compose": "Composer",
"tabs_bar.home": "Accueil", "tabs_bar.home": "Accueil",
"tabs_bar.mentions": "Mentions", "tabs_bar.mentions": "Mentions",
"tabs_bar.public": "Fil public global", "tabs_bar.public": "Fil public global",
"tabs_bar.notifications": "Notifications", "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.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.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.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.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", "compose_form.unlisted": "Ne pas afficher dans les fils publics",
@ -58,25 +74,32 @@ const fr = {
"navigation_bar.blocks": "Utilisateurs bloqués", "navigation_bar.blocks": "Utilisateurs bloqués",
"navigation_bar.favourites": "Favoris", "navigation_bar.favourites": "Favoris",
"navigation_bar.info": "Plus d'informations", "navigation_bar.info": "Plus d'informations",
"notification.favourite": "{name} a ajouté à ses favoris :",
"navigation_bar.logout": "Déconnexion", "navigation_bar.logout": "Déconnexion",
"navigation_bar.follow_requests": "Demandes de suivi",
"reply_indicator.cancel": "Annuler", "reply_indicator.cancel": "Annuler",
"search.placeholder": "Chercher", "search.placeholder": "Rechercher",
"search.account": "Compte", "search.account": "Compte",
"search.hashtag": "Mot-clé", "search.hashtag": "Mot-clé",
"search_results.total": "{count} {count, plural, one {résultat} other {résultats}}", "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_button.label": "Joindre un média",
"upload_form.undo": "Annuler", "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.follow": "{name} vous suit.",
"notification.favourite": "{name} a ajouté à ses favoris :", "notification.favourite": "{name} a ajouté à ses favoris :",
"notification.reblog": "{name} a partagé votre statut :", "notification.reblog": "{name} a partagé votre statut :",
"notification.mention": "{name} vous a mentionné⋅e :", "notification.mention": "{name} vous a mentionné⋅e :",
"notifications.column_settings.alert": "Notifications locales", "notifications.column_settings.alert": "Notifications locales",
"notifications.column_settings.show": "Afficher dans la colonne", "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.follow": "Nouveaux abonnés :",
"notifications.column_settings.favourite": "Favoris :", "notifications.column_settings.favourite": "Favoris :",
"notifications.column_settings.mention": "Mentions :", "notifications.column_settings.mention": "Mentions :",
"notifications.column_settings.reblog": "Partages :", "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.short": "Public",
"privacy.public.long": "Afficher dans les fils publics", "privacy.public.long": "Afficher dans les fils publics",
"privacy.unlisted.short": "Non-listé", "privacy.unlisted.short": "Non-listé",
@ -84,8 +107,22 @@ const fr = {
"privacy.private.short": "Privé", "privacy.private.short": "Privé",
"privacy.private.long": "Nafficher que pour vos abonné⋅e⋅s", "privacy.private.long": "Nafficher que pour vos abonné⋅e⋅s",
"privacy.direct.short": "Direct", "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", "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; export default fr;

View file

@ -3,8 +3,16 @@ import de from './de';
import es from './es'; import es from './es';
import hu from './hu'; import hu from './hu';
import fr from './fr'; import fr from './fr';
import nl from './nl';
import no from './no';
import pt from './pt'; import pt from './pt';
import uk from './uk'; 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 = { const locales = {
en, en,
@ -12,8 +20,16 @@ const locales = {
es, es,
hu, hu,
fr, fr,
nl,
no,
pt, pt,
uk uk,
fi,
eo,
ru,
ja,
'zh-HK': zh_hk,
bg,
}; };
export default function getMessagesForLocale (locale) { 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", "column_back_button.label": "Voltar",
"lightbox.close": "Fechar", "lightbox.close": "Fechar",
"loading_indicator.label": "Carregando...", "loading_indicator.label": "Carregando...",
"status.mention": "Menção", "status.mention": "Mencionar @{name}",
"status.delete": "Deletar", "status.delete": "Eliminar",
"status.reply": "Responder", "status.reply": "Responder",
"status.reblog": "Reblogar", "status.reblog": "Partilhar",
"status.favourite": "Favoritar", "status.favourite": "Adicionar aos favoritos",
"status.reblogged_by": "{name} reblogou", "status.reblogged_by": "{name} partilhou",
"video_player.toggle_sound": "Alterar som", "status.sensitive_warning": "Conteúdo sensível",
"account.mention": "Menção", "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.edit_profile": "Editar perfil",
"account.unblock": "Desbloquear", "account.unblock": "Não bloquear @{name}",
"account.unfollow": "Unfollow", "account.unfollow": "Não seguir",
"account.block": "Bloquear", "account.block": "Bloquear @{name}",
"account.mute": "Mute",
"account.unmute": "Remover Mute",
"account.follow": "Seguir", "account.follow": "Seguir",
"account.block": "Bloquear",
"account.posts": "Posts", "account.posts": "Posts",
"account.follows": "Segue", "account.follows": "Segue",
"account.followers": "Seguidores", "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.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_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.home": "Home",
"column.mentions": "Menções", "column.community": "Local",
"column.public": "Público", "column.public": "Global",
"tabs_bar.compose": "Compôr", "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.home": "Home",
"tabs_bar.mentions": "Menções", "tabs_bar.mentions": "Menções",
"tabs_bar.public": "Público", "tabs_bar.public": "Público",
"tabs_bar.notifications": "Notificações", "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.publish": "Publicar",
"compose_form.sensitive": "Marcar conteúdo como sensível", "compose_form.sensitive": "Marcar media como conteúdo sensível",
"compose_form.unlisted": "Modo não-listado", "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.edit_profile": "Editar perfil",
"navigation_bar.preferences": "Preferências", "navigation_bar.preferences": "Preferências",
"navigation_bar.public_timeline": "Timeline Pública", "navigation_bar.community_timeline": "Local",
"navigation_bar.logout": "Logout", "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", "reply_indicator.cancel": "Cancelar",
"search.placeholder": "Busca", "search.placeholder": "Pesquisar",
"search.account": "Conta", "search.account": "Conta",
"search.hashtag": "Hashtag", "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_button.label": "Adicionar media",
"upload_form.undo": "Desfazer", "upload_form.undo": "Anular",
"notification.follow": "{name} seguiu você", "upload_progress.label": "A gravar…",
"notification.favourite": "{name} favoritou seu post", "upload_area.title": "Arraste e solte para enviar",
"notification.reblog": "{name} reblogou o seu post", "notification.follow": "{name} seguiu-te",
"notification.mention": "{name} mecionou você" "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; 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('is_submitting', false);
map.set('in_reply_to', null); map.set('in_reply_to', null);
map.set('privacy', state.get('default_privacy')); map.set('privacy', state.get('default_privacy'));
map.set('sensitive', false);
map.update('media_attachments', list => list.clear()); map.update('media_attachments', list => list.clear());
}); });
}; };
@ -76,7 +77,8 @@ function appendMedia(state, media) {
map.update('media_attachments', list => list.push(media)); map.update('media_attachments', list => list.push(media));
map.set('is_uploading', false); map.set('is_uploading', false);
map.set('resetFileKey', Math.floor((Math.random() * 0x10000))); 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) { if (action.status.get('spoiler_text').length > 0) {
map.set('spoiler', true); map.set('spoiler', true);
map.set('spoiler_text', action.status.get('spoiler_text')); map.set('spoiler_text', action.status.get('spoiler_text'));
} else {
map.set('spoiler', false);
map.set('spoiler_text', '');
} }
}); });
case COMPOSE_REPLY_CANCEL: case COMPOSE_REPLY_CANCEL:

View file

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

View file

@ -114,10 +114,6 @@
padding: 20px; padding: 20px;
} }
.screenshot-with-signup .mascot {
display: none;
}
.features-list { .features-list {
display: block; display: block;
} }
@ -158,6 +154,14 @@
color: $color5; color: $color5;
} }
} }
@media screen and (max-width: 500px) {
flex-direction: column;
.section {
text-align: left;
}
}
} }
.owner { .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 { .features-list {
@ -319,7 +332,7 @@
} }
} }
.simple_form { .simple_form, .closed-registrations-message {
width: 300px; width: 300px;
flex: 0 0 auto; flex: 0 0 auto;
background: rgba(darken($color1, 7%), 0.5); 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 { &:after {
background: rgba($color8, 0.5); background: linear-gradient(rgba($color8, 0.5), rgba($color8, 0.8));
display: block; display: block;
content: ""; content: "";
position: absolute; position: absolute;
@ -34,6 +34,7 @@
text-align: center; text-align: center;
position: relative; position: relative;
z-index: 2; z-index: 2;
text-shadow: 0 0 2px $color8;
small { small {
display: block; display: block;
@ -82,7 +83,7 @@
.counter { .counter {
width: 80px; width: 80px;
color: $color3; color: $color3;
padding: 0 10px; padding: 5px 10px 0px;
margin-bottom: 10px; margin-bottom: 10px;
border-right: 1px solid $color3; border-right: 1px solid $color3;
cursor: default; cursor: default;
@ -128,6 +129,7 @@
text-transform: uppercase; text-transform: uppercase;
display: block; display: block;
margin-bottom: 5px; margin-bottom: 5px;
text-shadow: 0 0 2px $color8;
} }
.counter-number { .counter-number {
@ -146,7 +148,7 @@
order: 1; order: 1;
} }
@media screen and (max-width: 360px) { @media screen and (max-width: 480px) {
.details { .details {
display: block; display: block;
} }
@ -171,7 +173,7 @@
text-align: center; text-align: center;
overflow: hidden; overflow: hidden;
a, .current, .next_page, .previous_page, .gap { a, .current, .page, .gap {
font-size: 14px; font-size: 14px;
color: $color5; color: $color5;
font-weight: 500; font-weight: 500;
@ -191,12 +193,12 @@
cursor: default; cursor: default;
} }
.previous_page, .next_page { .prev, .next {
text-transform: uppercase; text-transform: uppercase;
color: $color2; color: $color2;
} }
.previous_page { .prev {
float: left; float: left;
padding-left: 0; padding-left: 0;
@ -206,7 +208,7 @@
} }
} }
.next_page { .next {
float: right; float: right;
padding-right: 0; padding-right: 0;
@ -224,11 +226,11 @@
@media screen and (max-width: 360px) { @media screen and (max-width: 360px) {
padding: 30px 20px; padding: 30px 20px;
a, .current, .next_page, .previous_page, .gap { a, .current, .next, .prev, .gap {
display: none; display: none;
} }
.next_page, .previous_page { .next, .prev {
display: inline-block; display: inline-block;
} }
} }

View file

@ -4,305 +4,13 @@
@import 'fonts/montserrat'; @import 'fonts/montserrat';
@import 'font-awesome'; @import 'font-awesome';
/* http://meyerweb.com/eric/tools/css/reset/ @import 'reset';
v2.0 | 20110126 @import 'basics';
License: none (public domain) @import 'containers';
*/ @import 'lists';
@import 'footer';
html, body, div, span, applet, object, iframe, @import 'compact_header';
h1, h2, h3, h4, h5, h6, p, blockquote, pre, @import 'landing_strip';
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 'forms'; @import 'forms';
@import 'accounts'; @import 'accounts';
@import 'stream_entries'; @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'; @import 'variables';
.app-body{
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
}
.button { .button {
background-color: darken($color4, 3%); background-color: darken($color4, 3%);
font-family: inherit; 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 { .icon-button {
display: inline-block; display: inline-block;
padding: 0; padding: 0;
@ -91,6 +112,18 @@
color: $color3; 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 { .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 { .lightbox .icon-button {
color: $color1; color: $color1;
} }
@ -321,6 +362,43 @@ a.status__content__spoiler-link {
.status__display-name { .status__display-name {
color: lighten($color1, 26%); 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 { .status-check-box {
@ -639,6 +717,12 @@ a.status__content__spoiler-link {
left: 8px; left: 8px;
} }
&.light {
&:before {
border-color: transparent transparent $color5 transparent;
}
}
& > ul { & > ul {
list-style: none; list-style: none;
background: $color2; background: $color2;
@ -656,7 +740,7 @@ a.status__content__spoiler-link {
} }
& > .emoji-dialog { & > .emoji-dialog {
left: -249px; left: -210px;
} }
} }
@ -710,7 +794,7 @@ a.status__content__spoiler-link {
@media screen and (min-width: 360px) { @media screen and (min-width: 360px) {
.columns-area { .columns-area {
margin: 10px; padding: 10px;
} }
} }
@ -718,9 +802,12 @@ a.status__content__spoiler-link {
width: 330px; width: 330px;
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;
background: $color1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
> .scrollable {
background: $color1;
}
} }
.ui { .ui {
@ -752,6 +839,58 @@ a.status__content__spoiler-link {
border-bottom: 2px solid transparent; 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) { @media screen and (min-width: 2560px) {
.columns-area { .columns-area {
justify-content: center; 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 { .tabs-bar {
display: flex; display: flex;
background: lighten($color1, 8%); background: lighten($color1, 8%);
@ -852,17 +960,18 @@ a.status__content__spoiler-link {
.tabs-bar__link { .tabs-bar__link {
display: block; display: block;
flex: 1 1 auto; flex: 1 1 auto;
padding: 10px 5px; padding: 15px 10px;
color: $color5; color: $color5;
text-decoration: none; text-decoration: none;
text-align: center; text-align: center;
font-size:12px; font-size: 14px;
font-weight: 500; font-weight: 500;
border-bottom: 2px solid lighten($color1, 8%); border-bottom: 2px solid lighten($color1, 8%);
transition: all 200ms linear; transition: all 200ms linear;
.fa { .fa {
font-weight: 400; font-weight: 400;
font-size: 16px;
} }
&.active { &.active {
@ -876,27 +985,13 @@ a.status__content__spoiler-link {
} }
span { span {
margin-left: 5px;
display: none; 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) { @media screen and (min-width: 600px) {
.tabs-bar__link { .tabs-bar__link {
.fa {
margin-right: 5px;
}
span { span {
display: inline; display: inline;
} }
@ -1149,10 +1244,9 @@ a.status__content__spoiler-link {
.getting-started { .getting-started {
box-sizing: border-box; box-sizing: border-box;
overflow-y: auto;
padding-bottom: 235px; padding-bottom: 235px;
background: image-url('mastodon-getting-started.png') no-repeat 0 100% local; background: image-url('mastodon-getting-started.png') no-repeat 0 100% local;
height: 100%; flex: 1 0 auto;
p { p {
color: $color2; color: $color2;
@ -1177,7 +1271,7 @@ a.status__content__spoiler-link {
@import 'boost'; @import 'boost';
button i.fa-retweet { button.icon-button i.fa-retweet {
height: 19px; height: 19px;
width: 22px; width: 22px;
background-position: 0 0; 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; transition-duration: 0.9s;
background-position: 0 100%; background-position: 0 100%;
} }
@ -1359,12 +1453,15 @@ button.active i.fa-retweet {
.empty-column-indicator { .empty-column-indicator {
color: lighten($color1, 20%); color: lighten($color1, 20%);
background: $color1;
text-align: center; text-align: center;
padding: 20px; padding: 20px;
padding-top: 100px;
font-size: 15px; font-size: 15px;
font-weight: 400; font-weight: 400;
cursor: default; cursor: default;
display: flex;
flex: 1 1 auto;
align-items: center;
a { a {
color: $color4; color: $color4;
@ -1390,22 +1487,23 @@ button.active i.fa-retweet {
} }
.emoji-dialog { .emoji-dialog {
width: 280px; width: 245px;
height: 220px; height: 270px;
background: $color2; background: $color5;
box-sizing: border-box; box-sizing: border-box;
border-radius: 2px; border-radius: 4px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
box-shadow: 0 0 15px rgba($color8, 0.4); box-shadow: 0 0 8px rgba($color8, 0.2);
.emojione { .emojione {
margin: 0; margin: 0;
width: 100%;
height: auto;
} }
.emoji-dialog-header { .emoji-dialog-header {
padding: 0 10px; padding: 0 10px;
background-color: $color3;
ul { ul {
padding: 0; padding: 0;
@ -1416,18 +1514,29 @@ button.active i.fa-retweet {
li { li {
display: inline-block; display: inline-block;
box-sizing: border-box; box-sizing: border-box;
height: 42px; padding: 10px 5px;
padding: 9px 5px;
cursor: pointer; cursor: pointer;
border-bottom: 2px solid transparent;
.emoji {
width: 18px;
height: 18px;
}
img, svg { img, svg {
width: 22px; width: 18px;
height: 22px; height: 18px;
filter: grayscale(100%); filter: grayscale(100%);
} }
&:hover {
img, svg {
filter: grayscale(0);
}
}
&.active { &.active {
background: lighten($color3, 6%); border-bottom-color: $color4;
img, svg { img, svg {
filter: grayscale(0); filter: grayscale(0);
@ -1451,7 +1560,7 @@ button.active i.fa-retweet {
.emoji-category-header { .emoji-category-header {
box-sizing: border-box; box-sizing: border-box;
overflow-y: hidden; overflow-y: hidden;
padding: 8px 16px 0; padding: 10px 8px 10px 16px;
display: table; display: table;
> * { > * {
@ -1461,10 +1570,10 @@ button.active i.fa-retweet {
} }
.emoji-category-title { .emoji-category-title {
font-size: 14px; font-size: 12px;
font-family: sans-serif; text-transform: uppercase;
font-weight: normal; font-weight: 500;
color: $color1; color: darken($color2, 18%);
cursor: default; cursor: default;
} }
@ -1504,7 +1613,7 @@ button.active i.fa-retweet {
width: 7px; width: 7px;
height: 7px; height: 7px;
border-radius: 10px; border-radius: 10px;
border: 2px solid $color1; border: 2px solid $color5;
top: 2px; top: 2px;
left: 2px; left: 2px;
} }
@ -1512,14 +1621,20 @@ button.active i.fa-retweet {
} }
.emoji-search-wrapper { .emoji-search-wrapper {
padding: 6px 16px; padding: 10px;
border-bottom: 1px solid lighten($color2, 4%);
} }
.emoji-search { .emoji-search {
font-size: 12px; font-size: 14px;
padding: 6px 4px; font-weight: 400;
padding: 7px 9px;
font-family: inherit;
display: block;
width: 100%; width: 100%;
border: 1px solid #ddd; background: rgba($color2, 0.3);
color: darken($color2, 18%);
border: 1px solid $color2;
border-radius: 4px; border-radius: 4px;
} }
@ -1532,11 +1647,21 @@ button.active i.fa-retweet {
} }
.emoji-search-wrapper + .emoji-categories-wrapper { .emoji-search-wrapper + .emoji-categories-wrapper {
top: 83px; top: 93px;
} }
.emoji-row .emoji:hover { .emoji-row .emoji {
background: lighten($color2, 3%); img, svg {
transition: transform 60ms ease-in-out;
}
&:hover {
background: lighten($color2, 3%);
img, svg {
transform: translateZ(0) scale(1.2);
}
}
} }
.emoji { .emoji {
@ -1976,8 +2101,8 @@ button.active i.fa-retweet {
} }
.onboarding-modal__page { .onboarding-modal__page {
text-align: center;
cursor: default; cursor: default;
line-height: 21px;
h1 { h1 {
font-size: 18px; font-size: 18px;
@ -1995,11 +2120,10 @@ button.active i.fa-retweet {
} }
p { p {
font-size: 18px; font-size: 16px;
color: lighten($color1, 8%); color: lighten($color1, 8%);
margin-top: 10px; margin-top: 10px;
margin-bottom: 10px; margin-bottom: 10px;
line-height: 10x;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
@ -2007,24 +2131,85 @@ button.active i.fa-retweet {
strong { strong {
font-weight: 500; font-weight: 500;
background: $color1;
color: $color2;
border-radius: 4px;
font-size: 14px;
padding: 3px 6px;
} }
} }
} }
.onboarding-modal__image { .onboarding-modal__page-one {
border-radius: 8px; display: flex;
width: 70vw; }
max-width: 450px;
max-height: auto; .onboarding-modal__page-one__elephant-friend {
display: block; background: image-url('elephant-friend.png') no-repeat 0 0;
margin: auto; 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 { .onboard-sliders {
display: inline-block; display: inline-block;
max-width: 30px; max-width: 30px;
max-height: auto; max-height: auto;
margin-left: 10px; 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; margin-bottom: 15px;
} }
strong {
font-weight: 500;
}
.label_input { .label_input {
display: flex; 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; background: transparent;
box-sizing: border-box; box-sizing: border-box;
border: 0; border: 0;
@ -224,7 +228,12 @@ code {
} }
} }
.qr-wrapper {
display: flex;
}
.qr-code { .qr-code {
flex: 0 0 auto;
background: #fff; background: #fff;
padding: 4px; padding: 4px;
margin-bottom: 20px; margin-bottom: 20px;
@ -236,3 +245,13 @@ code {
margin: 0; 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; margin-top: 8px;
height: 300px; height: 300px;
overflow: hidden; overflow: hidden;
position: relative;
video { video {
position: relative; position: relative;

View file

@ -2,28 +2,25 @@
class AboutController < ApplicationController class AboutController < ApplicationController
before_action :set_body_classes before_action :set_body_classes
before_action :set_instance_presenter, only: [:show, :more]
def index def show; end
@description = Setting.site_description
@user = User.new def more; end
@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 terms; end def terms; end
private 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 def set_body_classes
@body_classes = 'about-body' @body_classes = 'about-body'
end end

View file

@ -16,7 +16,8 @@ class AccountsController < ApplicationController
end end
format.atom do 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 end
format.activitystreams2 format.activitystreams2
@ -34,11 +35,11 @@ class AccountsController < ApplicationController
end end
def followers 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 end
def following 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 end
private private
@ -52,7 +53,7 @@ class AccountsController < ApplicationController
end end
def webfinger_account_url def webfinger_account_url
webfinger_url(resource: "acct:#{@account.acct}@#{Rails.configuration.x.local_domain}") webfinger_url(resource: @account.to_webfinger_s)
end end
def check_account_suspension def check_account_suspension

View file

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

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