Merge branch 'master' into skylight
This commit is contained in:
commit
7a1903cdf7
12
.editorconfig
Normal file
12
.editorconfig
Normal 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
|
|
@ -35,6 +35,10 @@ SMTP_PORT=587
|
|||
SMTP_LOGIN=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM_ADDRESS=notifications@example.com
|
||||
#SMTP_AUTH_METHOD=plain
|
||||
#SMTP_OPENSSL_VERIFY_MODE=peer
|
||||
#SMTP_ENABLE_STARTTLS_AUTO=true
|
||||
|
||||
|
||||
# Optional asset host for multi-server setups
|
||||
# CDN_HOST=assets.example.com
|
||||
|
|
30
.eslintignore
Normal file
30
.eslintignore
Normal 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/*
|
|
@ -1 +1 @@
|
|||
2.3.1
|
||||
2.4.1
|
||||
|
|
|
@ -16,7 +16,7 @@ addons:
|
|||
postgresql: 9.4
|
||||
|
||||
rvm:
|
||||
- 2.3.1
|
||||
- 2.4.1
|
||||
|
||||
services:
|
||||
- redis-server
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
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"
|
||||
|
|
5
Gemfile
5
Gemfile
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
ruby '2.3.1'
|
||||
ruby '2.4.1'
|
||||
|
||||
gem 'rails', '~> 5.0.2'
|
||||
gem 'sass-rails', '~> 5.0'
|
||||
|
@ -32,6 +32,7 @@ gem 'htmlentities'
|
|||
gem 'http'
|
||||
gem 'http_accept_language'
|
||||
gem 'httplog'
|
||||
gem 'kaminari'
|
||||
gem 'link_header'
|
||||
gem 'nokogiri'
|
||||
gem 'oj'
|
||||
|
@ -52,7 +53,6 @@ gem 'simple_form'
|
|||
gem 'statsd-instrument'
|
||||
gem 'twitter-text'
|
||||
gem 'tzinfo-data'
|
||||
gem 'will_paginate'
|
||||
|
||||
gem 'react-rails'
|
||||
gem 'browserify-rails'
|
||||
|
@ -71,6 +71,7 @@ end
|
|||
|
||||
group :test do
|
||||
gem 'faker'
|
||||
gem 'rails-controller-testing'
|
||||
gem 'rspec-sidekiq'
|
||||
gem 'simplecov', require: false
|
||||
gem 'webmock'
|
||||
|
|
208
Gemfile.lock
208
Gemfile.lock
|
@ -24,7 +24,7 @@ GEM
|
|||
erubis (~> 2.7.0)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
||||
active_record_query_trace (1.5.3)
|
||||
active_record_query_trace (1.5.4)
|
||||
activejob (5.0.2)
|
||||
activesupport (= 5.0.2)
|
||||
globalid (>= 0.3.6)
|
||||
|
@ -39,7 +39,7 @@ GEM
|
|||
i18n (~> 0.7)
|
||||
minitest (~> 5.1)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.5.0)
|
||||
addressable (2.5.1)
|
||||
public_suffix (~> 2.0, >= 2.0.2)
|
||||
airbrussh (1.1.2)
|
||||
sshkit (>= 1.6.1, != 1.7.0)
|
||||
|
@ -47,17 +47,17 @@ GEM
|
|||
ast (2.3.0)
|
||||
attr_encrypted (3.0.3)
|
||||
encryptor (~> 3.0.0)
|
||||
autoprefixer-rails (6.5.0.2)
|
||||
autoprefixer-rails (6.7.7.1)
|
||||
execjs
|
||||
av (0.9.0)
|
||||
cocaine (~> 0.5.3)
|
||||
aws-sdk (2.6.28)
|
||||
aws-sdk-resources (= 2.6.28)
|
||||
aws-sdk-core (2.6.28)
|
||||
aws-sdk (2.9.6)
|
||||
aws-sdk-resources (= 2.9.6)
|
||||
aws-sdk-core (2.9.6)
|
||||
aws-sigv4 (~> 1.0)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-resources (2.6.28)
|
||||
aws-sdk-core (= 2.6.28)
|
||||
aws-sdk-resources (2.9.6)
|
||||
aws-sdk-core (= 2.9.6)
|
||||
aws-sigv4 (1.0.0)
|
||||
babel-source (5.8.35)
|
||||
babel-transpiler (0.7.0)
|
||||
|
@ -78,12 +78,11 @@ GEM
|
|||
railties (>= 4.0.0, < 5.1)
|
||||
sprockets (>= 3.6.0)
|
||||
builder (3.2.3)
|
||||
bullet (5.3.0)
|
||||
bullet (5.5.1)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.10.0)
|
||||
capistrano (3.7.2)
|
||||
capistrano (3.8.0)
|
||||
airbrussh (>= 1.0.0)
|
||||
capistrano-harrow
|
||||
i18n
|
||||
rake (>= 10.0.0)
|
||||
sshkit (>= 1.9.0)
|
||||
|
@ -92,8 +91,7 @@ GEM
|
|||
sshkit (~> 1.2)
|
||||
capistrano-faster-assets (1.0.2)
|
||||
capistrano (>= 3.1)
|
||||
capistrano-harrow (0.5.3)
|
||||
capistrano-rails (1.2.2)
|
||||
capistrano-rails (1.2.3)
|
||||
capistrano (~> 3.1)
|
||||
capistrano-bundler (~> 1.1)
|
||||
capistrano-rbenv (2.1.0)
|
||||
|
@ -119,7 +117,7 @@ GEM
|
|||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
debug_inspector (0.0.2)
|
||||
devise (4.2.0)
|
||||
devise (4.2.1)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0, < 5.1)
|
||||
|
@ -131,16 +129,16 @@ GEM
|
|||
devise (~> 4.0)
|
||||
railties
|
||||
rotp (~> 2.0)
|
||||
diff-lcs (1.2.5)
|
||||
diff-lcs (1.3)
|
||||
docile (1.1.5)
|
||||
domain_name (0.5.20161129)
|
||||
domain_name (0.5.20170404)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (4.2.0)
|
||||
doorkeeper (4.2.5)
|
||||
railties (>= 4.2)
|
||||
dotenv (2.1.1)
|
||||
dotenv-rails (2.1.1)
|
||||
dotenv (= 2.1.1)
|
||||
railties (>= 4.0, < 5.1)
|
||||
dotenv (2.2.0)
|
||||
dotenv-rails (2.2.0)
|
||||
dotenv (= 2.2.0)
|
||||
railties (>= 3.2, < 5.1)
|
||||
easy_translate (0.5.0)
|
||||
json
|
||||
thread
|
||||
|
@ -148,14 +146,14 @@ GEM
|
|||
encryptor (3.0.0)
|
||||
erubis (2.7.0)
|
||||
execjs (2.7.0)
|
||||
fabrication (2.15.2)
|
||||
faker (1.6.6)
|
||||
fabrication (2.16.1)
|
||||
faker (1.7.3)
|
||||
i18n (~> 0.5)
|
||||
fast_blank (1.0.0)
|
||||
font-awesome-rails (4.6.3.1)
|
||||
font-awesome-rails (4.7.0.1)
|
||||
railties (>= 3.2, < 5.1)
|
||||
fuubar (2.1.1)
|
||||
rspec (~> 3.0)
|
||||
fuubar (2.2.0)
|
||||
rspec-core (~> 3.0)
|
||||
ruby-progressbar (~> 1.4)
|
||||
globalid (0.3.7)
|
||||
activesupport (>= 4.1.0)
|
||||
|
@ -163,20 +161,20 @@ GEM
|
|||
addressable (~> 2.4)
|
||||
http (~> 2.0)
|
||||
nokogiri (~> 1.6)
|
||||
hamlit (2.7.2)
|
||||
temple (~> 0.7.6)
|
||||
hamlit (2.8.1)
|
||||
temple (>= 0.8.0)
|
||||
thor
|
||||
tilt
|
||||
hamlit-rails (0.1.0)
|
||||
hamlit-rails (0.2.0)
|
||||
actionpack (>= 4.0.1)
|
||||
activesupport (>= 4.0.1)
|
||||
hamlit (>= 1.2.0)
|
||||
railties (>= 4.0.1)
|
||||
hashdiff (0.3.0)
|
||||
hashdiff (0.3.2)
|
||||
highline (1.7.8)
|
||||
hiredis (0.6.1)
|
||||
htmlentities (4.3.4)
|
||||
http (2.1.0)
|
||||
http (2.2.1)
|
||||
addressable (~> 2.3)
|
||||
http-cookie (~> 1.0)
|
||||
http-form_data (~> 1.0.1)
|
||||
|
@ -186,10 +184,10 @@ GEM
|
|||
http-form_data (1.0.1)
|
||||
http_accept_language (2.1.0)
|
||||
http_parser.rb (0.6.0)
|
||||
httplog (0.3.2)
|
||||
httplog (0.99.2)
|
||||
colorize
|
||||
i18n (0.8.1)
|
||||
i18n-tasks (0.9.6)
|
||||
i18n-tasks (0.9.13)
|
||||
activesupport (>= 4.0.2)
|
||||
ast (>= 2.1.0)
|
||||
easy_translate (>= 0.5.0)
|
||||
|
@ -197,19 +195,31 @@ GEM
|
|||
highline (>= 1.7.3)
|
||||
i18n
|
||||
parser (>= 2.2.3.0)
|
||||
term-ansicolor (>= 1.3.2)
|
||||
rainbow (~> 2.2)
|
||||
terminal-table (>= 1.5.1)
|
||||
jmespath (1.3.1)
|
||||
jquery-rails (4.1.1)
|
||||
jquery-rails (4.3.1)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (1.8.3)
|
||||
json (2.0.3)
|
||||
kaminari (1.0.1)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.0.1)
|
||||
kaminari-activerecord (= 1.0.1)
|
||||
kaminari-core (= 1.0.1)
|
||||
kaminari-actionview (1.0.1)
|
||||
actionview
|
||||
kaminari-core (= 1.0.1)
|
||||
kaminari-activerecord (1.0.1)
|
||||
activerecord
|
||||
kaminari-core (= 1.0.1)
|
||||
kaminari-core (1.0.1)
|
||||
launchy (2.4.3)
|
||||
addressable (~> 2.3)
|
||||
letter_opener (1.4.1)
|
||||
launchy (~> 2.2)
|
||||
letter_opener_web (1.3.0)
|
||||
letter_opener_web (1.3.1)
|
||||
actionmailer (>= 3.2)
|
||||
letter_opener (~> 1.0)
|
||||
railties (>= 3.2)
|
||||
|
@ -231,11 +241,11 @@ GEM
|
|||
minitest (5.10.1)
|
||||
net-scp (1.2.1)
|
||||
net-ssh (>= 2.6.5)
|
||||
net-ssh (4.0.1)
|
||||
net-ssh (4.1.0)
|
||||
nio4r (2.0.0)
|
||||
nokogiri (1.7.1)
|
||||
mini_portile2 (~> 2.1.0)
|
||||
oj (2.17.3)
|
||||
oj (2.18.5)
|
||||
orm_adapter (0.5.0)
|
||||
ostatus2 (1.0.2)
|
||||
addressable (~> 2.4)
|
||||
|
@ -251,26 +261,26 @@ GEM
|
|||
paperclip-av-transcoder (0.6.4)
|
||||
av (~> 0.9.0)
|
||||
paperclip (>= 2.5.2)
|
||||
parser (2.3.1.2)
|
||||
parser (2.4.0.0)
|
||||
ast (~> 2.2)
|
||||
pg (0.18.4)
|
||||
pghero (1.6.2)
|
||||
pg (0.20.0)
|
||||
pghero (1.6.4)
|
||||
activerecord
|
||||
powerpack (0.1.1)
|
||||
pry (0.10.4)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.8.1)
|
||||
slop (~> 3.4)
|
||||
pry-rails (0.3.4)
|
||||
pry (>= 0.9.10)
|
||||
public_suffix (2.0.4)
|
||||
puma (3.6.0)
|
||||
pry-rails (0.3.6)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (2.0.5)
|
||||
puma (3.8.2)
|
||||
rabl (0.13.1)
|
||||
activesupport (>= 2.3.14)
|
||||
rack (2.0.1)
|
||||
rack-attack (5.0.1)
|
||||
rack
|
||||
rack-cors (0.4.0)
|
||||
rack-cors (0.4.1)
|
||||
rack-protection (1.5.3)
|
||||
rack
|
||||
rack-test (0.6.3)
|
||||
|
@ -288,6 +298,10 @@ GEM
|
|||
bundler (>= 1.3.0, < 2.0)
|
||||
railties (= 5.0.2)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-controller-testing (1.0.1)
|
||||
actionpack (~> 5.x)
|
||||
actionview (~> 5.x)
|
||||
activesupport (~> 5.x)
|
||||
rails-dom-testing (2.0.2)
|
||||
activesupport (>= 4.2.0, < 6.0)
|
||||
nokogiri (~> 1.6)
|
||||
|
@ -306,42 +320,37 @@ GEM
|
|||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
rainbow (2.1.0)
|
||||
rainbow (2.2.1)
|
||||
rake (12.0.0)
|
||||
react-rails (1.10.0)
|
||||
react-rails (1.11.0)
|
||||
babel-transpiler (>= 0.7.0)
|
||||
coffee-script-source (~> 1.8)
|
||||
connection_pool
|
||||
execjs
|
||||
railties (>= 3.2)
|
||||
tilt
|
||||
redis (3.3.2)
|
||||
redis-actionpack (5.0.0)
|
||||
actionpack (>= 4.0.0, < 6)
|
||||
redis-rack (~> 2.0.0.pre)
|
||||
redis-store (~> 1.2.0.pre)
|
||||
redis-activesupport (5.0.1)
|
||||
redis (3.3.3)
|
||||
redis-actionpack (5.0.1)
|
||||
actionpack (>= 4.0, < 6)
|
||||
redis-rack (>= 1, < 3)
|
||||
redis-store (>= 1.1.0, < 1.4.0)
|
||||
redis-activesupport (5.0.2)
|
||||
activesupport (>= 3, < 6)
|
||||
redis-store (~> 1.2.0)
|
||||
redis-rack (2.0.0)
|
||||
rack (~> 2.0)
|
||||
redis-store (~> 1.2.0)
|
||||
redis-rails (5.0.1)
|
||||
redis-actionpack (~> 5.0.0)
|
||||
redis-activesupport (~> 5.0.0)
|
||||
redis-store (~> 1.2.0)
|
||||
redis-store (1.2.0)
|
||||
redis-store (~> 1.3.0)
|
||||
redis-rack (2.0.1)
|
||||
rack (>= 2.0, < 3)
|
||||
redis-store (>= 1.2, < 1.4)
|
||||
redis-rails (5.0.2)
|
||||
redis-actionpack (>= 5.0, < 6)
|
||||
redis-activesupport (>= 5.0, < 6)
|
||||
redis-store (>= 1.2, < 2)
|
||||
redis-store (1.3.0)
|
||||
redis (>= 2.2)
|
||||
responders (2.3.0)
|
||||
railties (>= 4.2.0, < 5.1)
|
||||
rotp (2.1.2)
|
||||
rqrcode (0.10.1)
|
||||
chunky_png (~> 1.0)
|
||||
rspec (3.5.0)
|
||||
rspec-core (~> 3.5.0)
|
||||
rspec-expectations (~> 3.5.0)
|
||||
rspec-mocks (~> 3.5.0)
|
||||
rspec-core (3.5.2)
|
||||
rspec-core (3.5.4)
|
||||
rspec-support (~> 3.5.0)
|
||||
rspec-expectations (3.5.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
|
@ -349,7 +358,7 @@ GEM
|
|||
rspec-mocks (3.5.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.5.0)
|
||||
rspec-rails (3.5.1)
|
||||
rspec-rails (3.5.2)
|
||||
actionpack (>= 3.0)
|
||||
activesupport (>= 3.0)
|
||||
railties (>= 3.0)
|
||||
|
@ -357,27 +366,27 @@ GEM
|
|||
rspec-expectations (~> 3.5.0)
|
||||
rspec-mocks (~> 3.5.0)
|
||||
rspec-support (~> 3.5.0)
|
||||
rspec-sidekiq (2.2.0)
|
||||
rspec (~> 3.0, >= 3.0.0)
|
||||
rspec-sidekiq (3.0.0)
|
||||
rspec-core (~> 3.0, >= 3.0.0)
|
||||
sidekiq (>= 2.4.0)
|
||||
rspec-support (3.5.0)
|
||||
rubocop (0.42.0)
|
||||
parser (>= 2.3.1.1, < 3.0)
|
||||
rubocop (0.48.1)
|
||||
parser (>= 2.3.3.1, < 3.0)
|
||||
powerpack (~> 0.1)
|
||||
rainbow (>= 1.99.1, < 3.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (~> 1.0, >= 1.0.1)
|
||||
ruby-oembed (0.10.1)
|
||||
ruby-oembed (0.12.0)
|
||||
ruby-progressbar (1.8.1)
|
||||
safe_yaml (1.0.4)
|
||||
sass (3.4.22)
|
||||
sass (3.4.23)
|
||||
sass-rails (5.0.6)
|
||||
railties (>= 4.0.0, < 6)
|
||||
sass (~> 3.1)
|
||||
sprockets (>= 2.8, < 4.0)
|
||||
sprockets-rails (>= 2.0, < 4.0)
|
||||
tilt (>= 1.1, < 3)
|
||||
sidekiq (4.2.7)
|
||||
sidekiq (4.2.10)
|
||||
concurrent-ruby (~> 1.0)
|
||||
connection_pool (~> 2.2, >= 2.2.0)
|
||||
rack-protection (>= 1.5.0)
|
||||
|
@ -385,20 +394,20 @@ GEM
|
|||
sidekiq-skylight (0.2.0)
|
||||
sidekiq (>= 3.3.0)
|
||||
skylight (>= 0.5.2)
|
||||
sidekiq-unique-jobs (4.0.18)
|
||||
sidekiq (>= 2.6)
|
||||
sidekiq-unique-jobs (5.0.0)
|
||||
sidekiq (>= 4.0)
|
||||
thor
|
||||
simple-navigation (4.0.3)
|
||||
simple-navigation (4.0.5)
|
||||
activesupport (>= 2.3.2)
|
||||
simple_form (3.2.1)
|
||||
simple_form (3.4.0)
|
||||
actionpack (> 4, < 5.1)
|
||||
activemodel (> 4, < 5.1)
|
||||
simplecov (0.12.0)
|
||||
simplecov (0.14.1)
|
||||
docile (~> 1.1.0)
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.0)
|
||||
skylight (1.1.0)
|
||||
skylight (1.2.0)
|
||||
activesupport (>= 3.0.0)
|
||||
slop (3.6.0)
|
||||
sprockets (3.7.1)
|
||||
|
@ -408,43 +417,39 @@ GEM
|
|||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
sshkit (1.11.5)
|
||||
sshkit (1.13.1)
|
||||
net-scp (>= 1.1.2)
|
||||
net-ssh (>= 2.8.0)
|
||||
statsd-instrument (2.1.2)
|
||||
temple (0.7.7)
|
||||
term-ansicolor (1.4.0)
|
||||
tins (~> 1.0)
|
||||
terminal-table (1.7.0)
|
||||
unicode-display_width (~> 1.1)
|
||||
temple (0.8.0)
|
||||
terminal-table (1.7.3)
|
||||
unicode-display_width (~> 1.1.1)
|
||||
thor (0.19.4)
|
||||
thread (0.2.2)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.6)
|
||||
tins (1.12.0)
|
||||
tilt (2.0.7)
|
||||
twitter-text (1.14.5)
|
||||
unf (~> 0.1.0)
|
||||
tzinfo (1.2.2)
|
||||
tzinfo (1.2.3)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo-data (1.2017.2)
|
||||
tzinfo (>= 1.0.0)
|
||||
uglifier (3.0.1)
|
||||
uglifier (3.2.0)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.2)
|
||||
unicode-display_width (1.1.0)
|
||||
unicode-display_width (1.1.3)
|
||||
uniform_notifier (1.10.0)
|
||||
warden (1.2.6)
|
||||
warden (1.2.7)
|
||||
rack (>= 1.0)
|
||||
webmock (2.1.0)
|
||||
webmock (2.3.2)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff
|
||||
websocket-driver (0.6.5)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.2)
|
||||
will_paginate (3.1.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
@ -483,6 +488,7 @@ DEPENDENCIES
|
|||
httplog
|
||||
i18n-tasks (~> 0.9.6)
|
||||
jquery-rails
|
||||
kaminari
|
||||
letter_opener
|
||||
letter_opener_web
|
||||
link_header
|
||||
|
@ -502,6 +508,7 @@ DEPENDENCIES
|
|||
rack-cors
|
||||
rack-timeout
|
||||
rails (~> 5.0.2)
|
||||
rails-controller-testing
|
||||
rails-settings-cached
|
||||
rails_12factor
|
||||
react-rails
|
||||
|
@ -525,10 +532,9 @@ DEPENDENCIES
|
|||
tzinfo-data
|
||||
uglifier (>= 1.3.0)
|
||||
webmock
|
||||
will_paginate
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.3.1p112
|
||||
ruby 2.4.1p111
|
||||
|
||||
BUNDLED WITH
|
||||
1.14.5
|
||||
1.14.6
|
||||
|
|
18
README.md
18
README.md
|
@ -25,11 +25,11 @@ If you would like, you can [support the development of this project on Patreon][
|
|||
|
||||
## Resources
|
||||
|
||||
- [List of Mastodon instances](docs/Using-Mastodon/List-of-Mastodon-instances.md)
|
||||
- [List of Mastodon instances](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md)
|
||||
- [Use this tool to find Twitter friends on Mastodon](https://mastodon-bridge.herokuapp.com)
|
||||
- [API overview](docs/Using-the-API/API.md)
|
||||
- [Frequently Asked Questions](docs/Using-Mastodon/FAQ.md)
|
||||
- [List of apps](docs/Using-Mastodon/Apps.md)
|
||||
- [API overview](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md)
|
||||
- [Frequently Asked Questions](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md)
|
||||
- [List of apps](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md)
|
||||
|
||||
## Features
|
||||
|
||||
|
@ -67,7 +67,7 @@ Consult the example configuration file, `.env.production.sample` for the full li
|
|||
|
||||
[![](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`. You need to turn `.env.production.sample` into `.env.production` with all the variables set before you can:
|
||||
The project now includes a `Dockerfile` and a `docker-compose.yml` file (which requires at least docker-compose version `1.10.0`). You need to turn `.env.production.sample` into `.env.production` with all the variables set before you can:
|
||||
|
||||
docker-compose build
|
||||
|
||||
|
@ -117,25 +117,25 @@ Which will re-create the updated containers, leaving databases and data as is. D
|
|||
|
||||
## 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.](docs/Running-Mastodon/Scalingo-guide.md)
|
||||
[You can view a guide for deployment on Scalingo here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Scalingo-guide.md)
|
||||
|
||||
## Deployment on Heroku (experimental)
|
||||
|
||||
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)
|
||||
|
||||
Mastodon can theoretically run indefinitely on a free [Heroku](https://heroku.com) app. [You can view a guide for deployment on Heroku here.](docs/Running-Mastodon/Heroku-guide.md)
|
||||
Mastodon can theoretically run indefinitely on a free [Heroku](https://heroku.com) app. [You can view a guide for deployment on Heroku here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Heroku-guide.md)
|
||||
|
||||
## Development with Vagrant
|
||||
|
||||
A quick way to get a development environment up and running is with Vagrant. You will need recent versions of [Vagrant](https://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) installed.
|
||||
|
||||
[You can find the guide for setting up a Vagrant development environment here.](docs/Running-Mastodon/Vagrant-guide.md)
|
||||
[You can find the guide for setting up a Vagrant development environment here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Vagrant-guide.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
8
Vagrantfile
vendored
8
Vagrantfile
vendored
|
@ -46,12 +46,12 @@ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
|
|||
export PATH="$HOME/.rbenv/bin::$PATH"
|
||||
eval "$(rbenv init -)"
|
||||
|
||||
echo "Compiling Ruby 2.3.1: warning, this takes a while!!!"
|
||||
rbenv install 2.3.1
|
||||
rbenv global 2.3.1
|
||||
|
||||
cd /vagrant
|
||||
|
||||
echo "Compiling Ruby $(cat .ruby-version): warning, this takes a while!!!"
|
||||
rbenv install $(cat .ruby-version)
|
||||
rbenv global $(cat .ruby-version)
|
||||
|
||||
# Configure database
|
||||
sudo -u postgres createuser -U postgres vagrant -s
|
||||
sudo -u postgres createdb -U postgres mastodon_development
|
||||
|
|
12
app.json
12
app.json
|
@ -79,6 +79,18 @@
|
|||
"SMTP_FROM_ADDRESS": {
|
||||
"description": "Address to send emails from",
|
||||
"required": false
|
||||
},
|
||||
"SMTP_AUTH_METHOD": {
|
||||
"description": "Authentication method to use with SMTP server. Default is 'plain'.",
|
||||
"required": false
|
||||
},
|
||||
"SMTP_OPENSSL_VERIFY_MODE": {
|
||||
"description": "SMTP server certificate verification mode. Defaults is 'peer'.",
|
||||
"required": false
|
||||
},
|
||||
"SMTP_ENABLE_STARTTLS_AUTO": {
|
||||
"description": "Enable STARTTLS if SMTP server supports it? Default is true.",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"buildpacks": [
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 209 KiB After Width: | Height: | Size: 258 KiB |
|
@ -50,6 +50,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
|||
};
|
||||
};
|
||||
|
||||
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
||||
|
||||
export function refreshNotifications() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(refreshNotificationsRequest());
|
||||
|
@ -61,6 +63,8 @@ export function refreshNotifications() {
|
|||
params.since_id = ids.first().get('id');
|
||||
}
|
||||
|
||||
params.exclude_types = excludeTypesFromSettings(getState());
|
||||
|
||||
api(getState).get('/api/v1/notifications', { params }).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
|
@ -105,11 +109,11 @@ export function expandNotifications() {
|
|||
|
||||
dispatch(expandNotificationsRequest());
|
||||
|
||||
api(getState).get(url, {
|
||||
params: {
|
||||
limit: 5
|
||||
}
|
||||
}).then(response => {
|
||||
const params = {};
|
||||
|
||||
params.exclude_types = excludeTypesFromSettings(getState());
|
||||
|
||||
api(getState).get(url, params).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import axios from 'axios';
|
||||
import LinkHeader from 'http-link-header';
|
||||
import LinkHeader from './link_header';
|
||||
|
||||
export const getLinks = response => {
|
||||
const value = response.headers.link;
|
||||
|
|
|
@ -65,7 +65,7 @@ const Account = React.createClass({
|
|||
<div className='account'>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
|
||||
<div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={36} /></div>
|
||||
<div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</Permalink>
|
||||
|
||||
|
|
|
@ -1,103 +1,18 @@
|
|||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
|
||||
// From: http://stackoverflow.com/a/18320662
|
||||
const resample = (canvas, width, height, resize_canvas) => {
|
||||
let width_source = canvas.width;
|
||||
let height_source = canvas.height;
|
||||
width = Math.round(width);
|
||||
height = Math.round(height);
|
||||
|
||||
let ratio_w = width_source / width;
|
||||
let ratio_h = height_source / height;
|
||||
let ratio_w_half = Math.ceil(ratio_w / 2);
|
||||
let ratio_h_half = Math.ceil(ratio_h / 2);
|
||||
|
||||
let ctx = canvas.getContext("2d");
|
||||
let img = ctx.getImageData(0, 0, width_source, height_source);
|
||||
let img2 = ctx.createImageData(width, height);
|
||||
let data = img.data;
|
||||
let data2 = img2.data;
|
||||
|
||||
for (let j = 0; j < height; j++) {
|
||||
for (let i = 0; i < width; i++) {
|
||||
let x2 = (i + j * width) * 4;
|
||||
let weight = 0;
|
||||
let weights = 0;
|
||||
let weights_alpha = 0;
|
||||
let gx_r = 0;
|
||||
let gx_g = 0;
|
||||
let gx_b = 0;
|
||||
let gx_a = 0;
|
||||
let center_y = (j + 0.5) * ratio_h;
|
||||
let yy_start = Math.floor(j * ratio_h);
|
||||
let yy_stop = Math.ceil((j + 1) * ratio_h);
|
||||
|
||||
for (let yy = yy_start; yy < yy_stop; yy++) {
|
||||
let dy = Math.abs(center_y - (yy + 0.5)) / ratio_h_half;
|
||||
let center_x = (i + 0.5) * ratio_w;
|
||||
let w0 = dy * dy; //pre-calc part of w
|
||||
let xx_start = Math.floor(i * ratio_w);
|
||||
let xx_stop = Math.ceil((i + 1) * ratio_w);
|
||||
|
||||
for (let xx = xx_start; xx < xx_stop; xx++) {
|
||||
let dx = Math.abs(center_x - (xx + 0.5)) / ratio_w_half;
|
||||
let w = Math.sqrt(w0 + dx * dx);
|
||||
|
||||
if (w >= 1) {
|
||||
// pixel too far
|
||||
continue;
|
||||
}
|
||||
|
||||
// hermite filter
|
||||
weight = 2 * w * w * w - 3 * w * w + 1;
|
||||
let pos_x = 4 * (xx + yy * width_source);
|
||||
|
||||
// alpha
|
||||
gx_a += weight * data[pos_x + 3];
|
||||
weights_alpha += weight;
|
||||
|
||||
// colors
|
||||
if (data[pos_x + 3] < 255)
|
||||
weight = weight * data[pos_x + 3] / 250;
|
||||
|
||||
gx_r += weight * data[pos_x];
|
||||
gx_g += weight * data[pos_x + 1];
|
||||
gx_b += weight * data[pos_x + 2];
|
||||
weights += weight;
|
||||
}
|
||||
}
|
||||
|
||||
data2[x2] = gx_r / weights;
|
||||
data2[x2 + 1] = gx_g / weights;
|
||||
data2[x2 + 2] = gx_b / weights;
|
||||
data2[x2 + 3] = gx_a / weights_alpha;
|
||||
}
|
||||
}
|
||||
|
||||
// clear and resize canvas
|
||||
if (resize_canvas === true) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
} else {
|
||||
ctx.clearRect(0, 0, width_source, height_source);
|
||||
}
|
||||
|
||||
// draw
|
||||
ctx.putImageData(img2, 0, 0);
|
||||
};
|
||||
|
||||
const Avatar = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
src: React.PropTypes.string.isRequired,
|
||||
staticSrc: React.PropTypes.string,
|
||||
size: React.PropTypes.number.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
animated: React.PropTypes.bool
|
||||
animate: React.PropTypes.bool
|
||||
},
|
||||
|
||||
getDefaultProps () {
|
||||
return {
|
||||
animated: true
|
||||
animate: false
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -117,38 +32,30 @@ const Avatar = React.createClass({
|
|||
this.setState({ hovering: false });
|
||||
},
|
||||
|
||||
handleLoad () {
|
||||
this.canvas.width = this.image.naturalWidth;
|
||||
this.canvas.height = this.image.naturalHeight;
|
||||
this.canvas.getContext('2d').drawImage(this.image, 0, 0);
|
||||
|
||||
resample(this.canvas, this.props.size * window.devicePixelRatio, this.props.size * window.devicePixelRatio, true);
|
||||
},
|
||||
|
||||
setImageRef (c) {
|
||||
this.image = c;
|
||||
},
|
||||
|
||||
setCanvasRef (c) {
|
||||
this.canvas = c;
|
||||
},
|
||||
|
||||
render () {
|
||||
const { src, size, staticSrc, animate } = this.props;
|
||||
const { hovering } = this.state;
|
||||
|
||||
if (this.props.animated) {
|
||||
return (
|
||||
<div style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px` }}>
|
||||
<img src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ borderRadius: '4px' }} />
|
||||
</div>
|
||||
);
|
||||
const style = {
|
||||
...this.props.style,
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
backgroundSize: `${size}px ${size}px`
|
||||
};
|
||||
|
||||
if (hovering || animate) {
|
||||
style.backgroundImage = `url(${src})`;
|
||||
} else {
|
||||
style.backgroundImage = `url(${staticSrc})`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px`, position: 'relative' }}>
|
||||
<img ref={this.setImageRef} onLoad={this.handleLoad} src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ position: 'absolute', top: '0', left: '0', opacity: hovering ? '1' : '0', borderRadius: '4px' }} />
|
||||
<canvas ref={this.setCanvasRef} style={{ borderRadius: '4px', width: this.props.size, height: this.props.size, opacity: hovering ? '0' : '1' }} />
|
||||
</div>
|
||||
<div
|
||||
className='avatar'
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ const IconButton = React.createClass({
|
|||
e.preventDefault();
|
||||
|
||||
if (!this.props.disabled) {
|
||||
this.props.onClick();
|
||||
this.props.onClick(e);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ const Status = React.createClass({
|
|||
onOpenMedia: React.PropTypes.func,
|
||||
onBlock: React.PropTypes.func,
|
||||
me: React.PropTypes.number,
|
||||
boostModal: React.PropTypes.bool,
|
||||
muted: React.PropTypes.bool
|
||||
},
|
||||
|
||||
|
@ -90,7 +91,7 @@ const Status = React.createClass({
|
|||
|
||||
<a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px' }}>
|
||||
<div className='status__avatar' style={{ position: 'absolute', left: '10px', top: '10px', width: '48px', height: '48px' }}>
|
||||
<Avatar src={status.getIn(['account', 'avatar'])} size={48} />
|
||||
<Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} />
|
||||
</div>
|
||||
|
||||
<DisplayName account={status.get('account')} />
|
||||
|
|
|
@ -46,8 +46,8 @@ const StatusActionBar = React.createClass({
|
|||
this.props.onFavourite(this.props.status);
|
||||
},
|
||||
|
||||
handleReblogClick () {
|
||||
this.props.onReblog(this.props.status);
|
||||
handleReblogClick (e) {
|
||||
this.props.onReblog(this.props.status, e);
|
||||
},
|
||||
|
||||
handleDeleteClick () {
|
||||
|
|
|
@ -36,6 +36,7 @@ const StatusContent = React.createClass({
|
|||
|
||||
if (mention) {
|
||||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
||||
link.setAttribute('title', mention.get('acct'));
|
||||
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
||||
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
||||
} else if (media) {
|
||||
|
@ -91,7 +92,7 @@ const StatusContent = React.createClass({
|
|||
const { status } = this.props;
|
||||
const { hidden } = this.state;
|
||||
|
||||
const content = { __html: emojify(status.get('content')) };
|
||||
const content = { __html: emojify(status.get('content')).replace(/\n/g, '') };
|
||||
const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) };
|
||||
const directionStyle = { direction: 'ltr' };
|
||||
|
||||
|
@ -125,7 +126,7 @@ const StatusContent = React.createClass({
|
|||
<div style={{ display: hidden ? 'none' : 'block', ...directionStyle }} dangerouslySetInnerHTML={content} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
} else if (this.props.onClick) {
|
||||
return (
|
||||
<div
|
||||
className='status__content'
|
||||
|
@ -135,6 +136,14 @@ const StatusContent = React.createClass({
|
|||
dangerouslySetInnerHTML={content}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className='status__content'
|
||||
style={{ ...directionStyle }}
|
||||
dangerouslySetInnerHTML={content}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -48,6 +48,9 @@ import hu from 'react-intl/locale-data/hu';
|
|||
import uk from 'react-intl/locale-data/uk';
|
||||
import fi from 'react-intl/locale-data/fi';
|
||||
import eo from 'react-intl/locale-data/eo';
|
||||
import ru from 'react-intl/locale-data/ru';
|
||||
import ja from 'react-intl/locale-data/ja';
|
||||
|
||||
import getMessagesForLocale from '../locales';
|
||||
import { hydrateStore } from '../actions/store';
|
||||
import createStream from '../stream';
|
||||
|
@ -60,7 +63,9 @@ const browserHistory = useRouterHistory(createBrowserHistory)({
|
|||
basename: '/web'
|
||||
});
|
||||
|
||||
addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi, ...eo]);
|
||||
|
||||
addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi, ...eo, ...ru, ...ja]);
|
||||
|
||||
|
||||
const Mastodon = React.createClass({
|
||||
|
||||
|
|
|
@ -26,7 +26,8 @@ const makeMapStateToProps = () => {
|
|||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
status: getStatus(state, props.id),
|
||||
me: state.getIn(['meta', 'me'])
|
||||
me: state.getIn(['meta', 'me']),
|
||||
boostModal: state.getIn(['meta', 'boost_modal'])
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
|
@ -38,11 +39,19 @@ const mapDispatchToProps = (dispatch) => ({
|
|||
dispatch(replyCompose(status, router));
|
||||
},
|
||||
|
||||
onReblog (status) {
|
||||
onModalReblog (status) {
|
||||
dispatch(reblog(status));
|
||||
},
|
||||
|
||||
onReblog (status, e) {
|
||||
if (status.get('reblogged')) {
|
||||
dispatch(unreblog(status));
|
||||
} else {
|
||||
dispatch(reblog(status));
|
||||
if (e.altKey || !this.boostModal) {
|
||||
this.onModalReblog(status);
|
||||
} else {
|
||||
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
|
||||
const AutosuggestAccount = ({ account }) => (
|
||||
<div style={{ overflow: 'hidden' }} className='autosuggest-account'>
|
||||
<div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} size={18} /></div>
|
||||
<div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={18} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -83,11 +83,23 @@ const ComposeForm = React.createClass({
|
|||
this.props.onChangeSpoilerText(e.target.value);
|
||||
},
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
// If this is the update where we've finished uploading,
|
||||
// save the last caret position so we can restore it below!
|
||||
if (!nextProps.is_uploading && this.props.is_uploading) {
|
||||
this._restoreCaret = this.autosuggestTextarea.textarea.selectionStart;
|
||||
}
|
||||
},
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (this.props.focusDate !== prevProps.focusDate) {
|
||||
// If replying to zero or one users, places the cursor at the end of the textbox.
|
||||
// If replying to more than one user, selects any usernames past the first;
|
||||
// this provides a convenient shortcut to drop everyone else from the conversation.
|
||||
// This statement does several things:
|
||||
// - If we're beginning a reply, and,
|
||||
// - Replying to zero or one users, places the cursor at the end of the textbox.
|
||||
// - Replying to more than one user, selects any usernames past the first;
|
||||
// this provides a convenient shortcut to drop everyone else from the conversation.
|
||||
// - If we've just finished uploading an image, and have a saved caret position,
|
||||
// restores the cursor to that position after the text changes!
|
||||
if (this.props.focusDate !== prevProps.focusDate || (prevProps.is_uploading && !this.props.is_uploading && typeof this._restoreCaret === 'number')) {
|
||||
let selectionEnd, selectionStart;
|
||||
|
||||
if (this.props.preselectDate !== prevProps.preselectDate) {
|
||||
|
@ -118,7 +130,7 @@ const ComposeForm = React.createClass({
|
|||
|
||||
render () {
|
||||
const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props;
|
||||
const disabled = this.props.is_submitting || this.props.is_uploading;
|
||||
const disabled = this.props.is_submitting;
|
||||
|
||||
let publishText = '';
|
||||
let privacyWarning = '';
|
||||
|
|
|
@ -46,8 +46,8 @@ const EmojiPickerDropdown = React.createClass({
|
|||
<img draggable="false" className="emojione" alt="🙂" src="/emoji/1f602.svg" />
|
||||
</DropdownTrigger>
|
||||
|
||||
<DropdownContent className='dropdown__left'>
|
||||
<EmojiPicker emojione={settings} onChange={this.handleChange} />
|
||||
<DropdownContent className='dropdown__left light'>
|
||||
<EmojiPicker emojione={settings} onChange={this.handleChange} search={true} />
|
||||
</DropdownContent>
|
||||
</Dropdown>
|
||||
);
|
||||
|
|
|
@ -17,7 +17,7 @@ const NavigationBar = React.createClass({
|
|||
render () {
|
||||
return (
|
||||
<div className='navigation-bar'>
|
||||
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} size={40} /></Permalink>
|
||||
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} animate size={40} /></Permalink>
|
||||
|
||||
<div style={{ flex: '1 1 auto', marginLeft: '8px' }}>
|
||||
<strong style={{ fontWeight: '500', display: 'block' }}>{this.props.account.get('acct')}</strong>
|
||||
|
|
|
@ -50,7 +50,7 @@ const ReplyIndicator = React.createClass({
|
|||
<div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div>
|
||||
|
||||
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}>
|
||||
<div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} /></div>
|
||||
<div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} /></div>
|
||||
<DisplayName account={status.get('account')} />
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -33,7 +33,7 @@ const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
|
|||
<div>
|
||||
<div style={outerStyle}>
|
||||
<Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
|
||||
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={48} /></div>
|
||||
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={48} /></div>
|
||||
<DisplayName account={account} />
|
||||
</Permalink>
|
||||
|
||||
|
|
|
@ -4,16 +4,6 @@ const messages = defineMessages({
|
|||
clear: { id: 'notifications.clear', defaultMessage: 'Clear notifications' }
|
||||
});
|
||||
|
||||
const iconStyle = {
|
||||
fontSize: '16px',
|
||||
padding: '15px',
|
||||
position: 'absolute',
|
||||
right: '48px',
|
||||
top: '0',
|
||||
cursor: 'pointer',
|
||||
zIndex: '2'
|
||||
};
|
||||
|
||||
const ClearColumnButton = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
|
@ -25,7 +15,7 @@ const ClearColumnButton = React.createClass({
|
|||
const { intl } = this.props;
|
||||
|
||||
return (
|
||||
<div title={intl.formatMessage(messages.clear)} className='column-icon' tabIndex='0' style={iconStyle} onClick={this.props.onClick}>
|
||||
<div title={intl.formatMessage(messages.clear)} className='column-icon column-icon-clear' tabIndex='0' onClick={this.props.onClick}>
|
||||
<i className='fa fa-eraser' />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -21,7 +21,7 @@ const Notification = React.createClass({
|
|||
|
||||
renderFollow (account, link) {
|
||||
return (
|
||||
<div className='notification'>
|
||||
<div className='notification notification-follow'>
|
||||
<div className='notification__message'>
|
||||
<div style={{ position: 'absolute', 'left': '-26px'}}>
|
||||
<i className='fa fa-fw fa-user-plus' />
|
||||
|
@ -41,7 +41,7 @@ const Notification = React.createClass({
|
|||
|
||||
renderFavourite (notification, link) {
|
||||
return (
|
||||
<div className='notification'>
|
||||
<div className='notification notification-favourite'>
|
||||
<div className='notification__message'>
|
||||
<div style={{ position: 'absolute', 'left': '-26px'}}>
|
||||
<i className='fa fa-fw fa-star' style={{ color: '#ca8f04' }} />
|
||||
|
@ -57,7 +57,7 @@ const Notification = React.createClass({
|
|||
|
||||
renderReblog (notification, link) {
|
||||
return (
|
||||
<div className='notification'>
|
||||
<div className='notification notification-reblog'>
|
||||
<div className='notification__message'>
|
||||
<div style={{ position: 'absolute', 'left': '-26px'}}>
|
||||
<i className='fa fa-fw fa-retweet' />
|
||||
|
@ -76,17 +76,17 @@ const Notification = React.createClass({
|
|||
const account = notification.get('account');
|
||||
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
|
||||
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
||||
const link = <Permalink className='notification__display-name' style={linkStyle} href={account.get('url')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />;
|
||||
const link = <Permalink className='notification__display-name' style={linkStyle} href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />;
|
||||
|
||||
switch(notification.get('type')) {
|
||||
case 'follow':
|
||||
return this.renderFollow(account, link);
|
||||
case 'mention':
|
||||
return this.renderMention(notification);
|
||||
case 'favourite':
|
||||
return this.renderFavourite(notification, link);
|
||||
case 'reblog':
|
||||
return this.renderReblog(notification, link);
|
||||
case 'follow':
|
||||
return this.renderFollow(account, link);
|
||||
case 'mention':
|
||||
return this.renderMention(notification);
|
||||
case 'favourite':
|
||||
return this.renderFavourite(notification, link);
|
||||
case 'reblog':
|
||||
return this.renderReblog(notification, link);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,8 +37,8 @@ const ActionBar = React.createClass({
|
|||
this.props.onReply(this.props.status);
|
||||
},
|
||||
|
||||
handleReblogClick () {
|
||||
this.props.onReblog(this.props.status);
|
||||
handleReblogClick (e) {
|
||||
this.props.onReblog(this.props.status, e);
|
||||
},
|
||||
|
||||
handleFavouriteClick () {
|
||||
|
|
|
@ -54,7 +54,7 @@ const DetailedStatus = React.createClass({
|
|||
return (
|
||||
<div style={{ padding: '14px 10px' }} className='detailed-status'>
|
||||
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
|
||||
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} size={48} /></div>
|
||||
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /></div>
|
||||
<DisplayName account={status.get('account')} />
|
||||
</a>
|
||||
|
||||
|
|
|
@ -38,7 +38,8 @@ const makeMapStateToProps = () => {
|
|||
status: getStatus(state, Number(props.params.statusId)),
|
||||
ancestorsIds: state.getIn(['timelines', 'ancestors', Number(props.params.statusId)]),
|
||||
descendantsIds: state.getIn(['timelines', 'descendants', Number(props.params.statusId)]),
|
||||
me: state.getIn(['meta', 'me'])
|
||||
me: state.getIn(['meta', 'me']),
|
||||
boostModal: state.getIn(['meta', 'boost_modal'])
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
|
@ -55,7 +56,8 @@ const Status = React.createClass({
|
|||
status: ImmutablePropTypes.map,
|
||||
ancestorsIds: ImmutablePropTypes.list,
|
||||
descendantsIds: ImmutablePropTypes.list,
|
||||
me: React.PropTypes.number
|
||||
me: React.PropTypes.number,
|
||||
boostModal: React.PropTypes.bool
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
|
@ -82,11 +84,19 @@ const Status = React.createClass({
|
|||
this.props.dispatch(replyCompose(status, this.context.router));
|
||||
},
|
||||
|
||||
handleReblogClick (status) {
|
||||
handleModalReblog (status) {
|
||||
this.props.dispatch(reblog(status));
|
||||
},
|
||||
|
||||
handleReblogClick (status, e) {
|
||||
if (status.get('reblogged')) {
|
||||
this.props.dispatch(unreblog(status));
|
||||
} else {
|
||||
this.props.dispatch(reblog(status));
|
||||
if (e.altKey || !this.props.boostModal) {
|
||||
this.handleModalReblog(status);
|
||||
} else {
|
||||
this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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>Alt + <i className='fa fa-retweet' /></span> }} /></div>
|
||||
<Button text={intl.formatMessage(messages.reblog)} onClick={this.handleReblog} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(BoostModal);
|
|
@ -41,8 +41,11 @@ const Column = React.createClass({
|
|||
mixins: [PureRenderMixin],
|
||||
|
||||
handleHeaderClick () {
|
||||
let node = ReactDOM.findDOMNode(this);
|
||||
this._interruptScrollAnimation = scrollTop(node.querySelector('.scrollable'));
|
||||
const scrollable = ReactDOM.findDOMNode(this).querySelector('.scrollable');
|
||||
if (!scrollable) {
|
||||
return;
|
||||
}
|
||||
this._interruptScrollAnimation = scrollTop(scrollable);
|
||||
},
|
||||
|
||||
handleWheel () {
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import MediaModal from './media_modal';
|
||||
import BoostModal from './boost_modal';
|
||||
import { TransitionMotion, spring } from 'react-motion';
|
||||
|
||||
const MODAL_COMPONENTS = {
|
||||
'MEDIA': MediaModal
|
||||
'MEDIA': MediaModal,
|
||||
'BOOST': BoostModal
|
||||
};
|
||||
|
||||
const ModalRoot = React.createClass({
|
||||
|
|
33
app/assets/javascripts/components/link_header.jsx
Normal file
33
app/assets/javascripts/components/link_header.jsx
Normal 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;
|
|
@ -12,10 +12,12 @@ const fr = {
|
|||
"status.sensitive_toggle": "Cliquer pour dévoiler",
|
||||
"status.show_more": "Déplier",
|
||||
"status.show_less": "Replier",
|
||||
"status.open": "Déplier ce status",
|
||||
"status.open": "Déplier ce statut",
|
||||
"status.report": "Signaler @{name}",
|
||||
"status.load_more": "Charger plus",
|
||||
"status.media_hidden": "Média caché",
|
||||
"video_player.toggle_sound": "Mettre/Couper le son",
|
||||
"video_player.toggle_visible": "Afficher/Cacher la vidéo",
|
||||
"account.mention": "Mentionner",
|
||||
"account.edit_profile": "Modifier le profil",
|
||||
"account.unblock": "Débloquer",
|
||||
|
@ -42,16 +44,25 @@ const fr = {
|
|||
"column.notifications": "Notifications",
|
||||
"column.blocks": "Utilisateurs bloqués",
|
||||
"column.favourites": "Favoris",
|
||||
"column.follow_requests": "Demandes de suivi",
|
||||
"empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateurs⋅trices pour débuter la conversation.",
|
||||
"empty_column.public": "Il n'y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateurs d'autres instances pour remplir le fil public.",
|
||||
"empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d'autres utilisateurs.",
|
||||
"empty_column.home.public_timeline": "le fil public",
|
||||
"empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir !",
|
||||
"empty_column.hashtag": "Il n'y a encore aucun contenu relatif à ce hashtag",
|
||||
"tabs_bar.compose": "Composer",
|
||||
"tabs_bar.home": "Accueil",
|
||||
"tabs_bar.mentions": "Mentions",
|
||||
"tabs_bar.public": "Fil public global",
|
||||
"tabs_bar.notifications": "Notifications",
|
||||
"tabs_bar.local_timeline": "Fil public local",
|
||||
"tabs_bar.federated_timeline": "Fil public global",
|
||||
"compose_form.placeholder": "Qu’avez-vous en tête ?",
|
||||
"compose_form.publish": "Pouet",
|
||||
"compose_form.sensitive": "Marquer le média comme délicat",
|
||||
"compose_form.spoiler": "Masquer le texte derrière un avertissement",
|
||||
"compose_form.spoiler_placeholder": "Avertissement",
|
||||
"compose_form.private": "Rendre privé",
|
||||
"compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodons. Si {domains} {domainsCount, plural, one {n'est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n'y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d'une autre manière à d'autres personnes imprévues",
|
||||
"compose_form.unlisted": "Ne pas afficher dans les fils publics",
|
||||
|
@ -64,23 +75,31 @@ const fr = {
|
|||
"navigation_bar.favourites": "Favoris",
|
||||
"navigation_bar.info": "Plus d'informations",
|
||||
"navigation_bar.logout": "Déconnexion",
|
||||
"navigation_bar.follow_requests": "Demandes de suivi",
|
||||
"reply_indicator.cancel": "Annuler",
|
||||
"search.placeholder": "Chercher",
|
||||
"search.placeholder": "Rechercher",
|
||||
"search.account": "Compte",
|
||||
"search.hashtag": "Mot-clé",
|
||||
"search_results.total": "{count} {count, plural, one {résultat} other {résultats}}",
|
||||
"search.status_by": "Statuts de {name}",
|
||||
"upload_button.label": "Joindre un média",
|
||||
"upload_form.undo": "Annuler",
|
||||
"upload_progress.label": "Envoi en cours…",
|
||||
"upload_area.title": "Glissez et déposez pour envoyer",
|
||||
"notification.follow": "{name} vous suit.",
|
||||
"notification.favourite": "{name} a ajouté à ses favoris :",
|
||||
"notification.reblog": "{name} a partagé votre statut :",
|
||||
"notification.mention": "{name} vous a mentionné⋅e :",
|
||||
"notifications.column_settings.alert": "Notifications locales",
|
||||
"notifications.column_settings.show": "Afficher dans la colonne",
|
||||
"notifications.column_settings.sound": "Émettre un son",
|
||||
"notifications.column_settings.follow": "Nouveaux abonnés :",
|
||||
"notifications.column_settings.favourite": "Favoris :",
|
||||
"notifications.column_settings.mention": "Mentions :",
|
||||
"notifications.column_settings.reblog": "Partages :",
|
||||
"notifications.clear": "Nettoyer",
|
||||
"notifications.clear_confirmation": "Voulez-vous vraiment nettoyer toutes vos notifications ?",
|
||||
"notifications.settings": "Paramètres de la colonne",
|
||||
"privacy.public.short": "Public",
|
||||
"privacy.public.long": "Afficher dans les fils publics",
|
||||
"privacy.unlisted.short": "Non-listé",
|
||||
|
@ -90,6 +109,20 @@ const fr = {
|
|||
"privacy.direct.short": "Direct",
|
||||
"privacy.direct.long": "N’afficher que pour les personnes mentionné⋅e⋅s",
|
||||
"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;
|
||||
|
|
|
@ -7,6 +7,9 @@ import pt from './pt';
|
|||
import uk from './uk';
|
||||
import fi from './fi';
|
||||
import eo from './eo';
|
||||
import ru from './ru';
|
||||
import ja from './ja';
|
||||
|
||||
|
||||
const locales = {
|
||||
en,
|
||||
|
@ -17,7 +20,10 @@ const locales = {
|
|||
pt,
|
||||
uk,
|
||||
fi,
|
||||
eo
|
||||
eo,
|
||||
ru,
|
||||
ja
|
||||
|
||||
};
|
||||
|
||||
export default function getMessagesForLocale (locale) {
|
||||
|
|
72
app/assets/javascripts/components/locales/ja.jsx
Normal file
72
app/assets/javascripts/components/locales/ja.jsx
Normal file
|
@ -0,0 +1,72 @@
|
|||
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.show_less": "隠す",
|
||||
"status.open": "Expand this status",
|
||||
"status.report": "@{name}さんを報告",
|
||||
"video_player.toggle_sound": "音切り替え",
|
||||
"account.mention": "@{name}さんに返信",
|
||||
"account.edit_profile": "プロフィール返信",
|
||||
"account.unblock": "@{name}さんのブロックを解除",
|
||||
"account.unfollow": "フォロー解除",
|
||||
"account.block": "@{name}さんをブロック",
|
||||
"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.open_source_notice": "Mastodon はオープンソースのソフトウェアです。誰でもGitHub({github})から開発に参加したり、問題を報告したりできます。 {apps}",
|
||||
"column.home": "ホーム",
|
||||
"column.community": "ローカルタイムライン",
|
||||
"column.public": "連邦タイムライン",
|
||||
"column.notifications": "通知",
|
||||
"tabs_bar.compose": "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": "あなたの非公開トゥートは返信先のユーザー(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": "公開タイムラインに表示しない",
|
||||
"navigation_bar.edit_profile": "プロフィール編集",
|
||||
"navigation_bar.preferences": "ユーザー設定",
|
||||
"navigation_bar.community_timeline": "ローカルタイムライン",
|
||||
"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 ja;
|
68
app/assets/javascripts/components/locales/ru.jsx
Normal file
68
app/assets/javascripts/components/locales/ru.jsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
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": "Нажмите для просмотра",
|
||||
"video_player.toggle_sound": "Вкл./выкл. звук",
|
||||
"account.mention": "Упомянуть @{name}",
|
||||
"account.edit_profile": "Изменить профиль",
|
||||
"account.unblock": "Разблокировать @{name}",
|
||||
"account.unfollow": "Отписаться",
|
||||
"account.block": "Блокировать @{name}",
|
||||
"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}.",
|
||||
"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": "Выйти",
|
||||
"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 ru;
|
|
@ -72,6 +72,7 @@
|
|||
position: relative;
|
||||
z-index: 2;
|
||||
flex-direction: row;
|
||||
background: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.details-counters {
|
||||
|
@ -83,7 +84,7 @@
|
|||
.counter {
|
||||
width: 80px;
|
||||
color: $color3;
|
||||
padding: 0 10px;
|
||||
padding: 5px 10px 0px;
|
||||
margin-bottom: 10px;
|
||||
border-right: 1px solid $color3;
|
||||
cursor: default;
|
||||
|
@ -148,7 +149,7 @@
|
|||
order: 1;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 360px) {
|
||||
@media screen and (max-width: 480px) {
|
||||
.details {
|
||||
display: block;
|
||||
}
|
||||
|
@ -173,7 +174,7 @@
|
|||
text-align: center;
|
||||
overflow: hidden;
|
||||
|
||||
a, .current, .next_page, .previous_page, .gap {
|
||||
a, .current, .page, .gap {
|
||||
font-size: 14px;
|
||||
color: $color5;
|
||||
font-weight: 500;
|
||||
|
@ -193,12 +194,12 @@
|
|||
cursor: default;
|
||||
}
|
||||
|
||||
.previous_page, .next_page {
|
||||
.prev, .next {
|
||||
text-transform: uppercase;
|
||||
color: $color2;
|
||||
}
|
||||
|
||||
.previous_page {
|
||||
.prev {
|
||||
float: left;
|
||||
padding-left: 0;
|
||||
|
||||
|
@ -208,7 +209,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.next_page {
|
||||
.next {
|
||||
float: right;
|
||||
padding-right: 0;
|
||||
|
||||
|
@ -226,11 +227,11 @@
|
|||
@media screen and (max-width: 360px) {
|
||||
padding: 30px 20px;
|
||||
|
||||
a, .current, .next_page, .previous_page, .gap {
|
||||
a, .current, .next, .prev, .gap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.next_page, .previous_page {
|
||||
.next, .prev {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,7 +1,7 @@
|
|||
@import 'variables';
|
||||
|
||||
.app-body{
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
}
|
||||
|
||||
.button {
|
||||
|
@ -49,6 +49,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: 1024px) {
|
||||
.column-icon-clear {
|
||||
top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
|
@ -149,6 +165,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: 4px;
|
||||
background: transparent no-repeat;
|
||||
background-position: 50%;
|
||||
background-clip: padding-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.lightbox .icon-button {
|
||||
color: $color1;
|
||||
}
|
||||
|
@ -325,6 +349,43 @@ a.status__content__spoiler-link {
|
|||
.status__display-name {
|
||||
color: lighten($color1, 26%);
|
||||
}
|
||||
|
||||
&.light {
|
||||
.status__relative-time {
|
||||
color: $color3;
|
||||
}
|
||||
|
||||
.status__display-name {
|
||||
color: $color1;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
strong {
|
||||
color: $color1;
|
||||
}
|
||||
|
||||
span {
|
||||
color: $color3;
|
||||
}
|
||||
}
|
||||
|
||||
.status__content {
|
||||
color: $color1;
|
||||
|
||||
a {
|
||||
color: $color4;
|
||||
}
|
||||
|
||||
a.status__content__spoiler-link {
|
||||
color: $color5;
|
||||
background: $color3;
|
||||
|
||||
&:hover {
|
||||
background: lighten($color3, 8%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status-check-box {
|
||||
|
@ -643,6 +704,12 @@ a.status__content__spoiler-link {
|
|||
left: 8px;
|
||||
}
|
||||
|
||||
&.light {
|
||||
&:before {
|
||||
border-color: transparent transparent $color5 transparent;
|
||||
}
|
||||
}
|
||||
|
||||
& > ul {
|
||||
list-style: none;
|
||||
background: $color2;
|
||||
|
@ -660,7 +727,7 @@ a.status__content__spoiler-link {
|
|||
}
|
||||
|
||||
& > .emoji-dialog {
|
||||
left: -249px;
|
||||
left: -210px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -714,15 +781,7 @@ a.status__content__spoiler-link {
|
|||
|
||||
@media screen and (min-width: 360px) {
|
||||
.columns-area {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.column:first-child, .drawer:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.column:last-child, .drawer:last-child {
|
||||
margin-right: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -730,9 +789,12 @@ a.status__content__spoiler-link {
|
|||
width: 330px;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
background: $color1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> .scrollable {
|
||||
background: $color1;
|
||||
}
|
||||
}
|
||||
|
||||
.ui {
|
||||
|
@ -764,6 +826,58 @@ a.status__content__spoiler-link {
|
|||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.column, .drawer {
|
||||
flex: 1 1 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 360px) {
|
||||
.tabs-bar {
|
||||
margin: 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.search {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
.column, .drawer {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.columns-area {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search__input, .autosuggest-textarea__textarea {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
.columns-area {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.column, .drawer {
|
||||
flex: 0 0 auto;
|
||||
padding: 10px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 2560px) {
|
||||
.columns-area {
|
||||
justify-content: center;
|
||||
|
@ -823,38 +937,6 @@ a.status__content__spoiler-link {
|
|||
}
|
||||
}
|
||||
|
||||
.column, .drawer {
|
||||
margin: 10px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
flex: 0 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.column:first-child, .drawer:first-child {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.column:last-child, .drawer:last-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
.column, .drawer {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.columns-area {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search__input, .autosuggest-textarea__textarea {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-bar {
|
||||
display: flex;
|
||||
background: lighten($color1, 8%);
|
||||
|
@ -865,17 +947,18 @@ a.status__content__spoiler-link {
|
|||
.tabs-bar__link {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
padding: 10px 5px;
|
||||
padding: 15px 10px;
|
||||
color: $color5;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
font-size:12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-bottom: 2px solid lighten($color1, 8%);
|
||||
transition: all 200ms linear;
|
||||
|
||||
.fa {
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
|
@ -889,37 +972,13 @@ a.status__content__spoiler-link {
|
|||
}
|
||||
|
||||
span {
|
||||
margin-left: 5px;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 360px) {
|
||||
.columns-area {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.tabs-bar {
|
||||
margin: 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.search {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
.columns-area {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
.tabs-bar__link {
|
||||
.fa {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline;
|
||||
}
|
||||
|
@ -1199,7 +1258,7 @@ a.status__content__spoiler-link {
|
|||
|
||||
@import 'boost';
|
||||
|
||||
button i.fa-retweet {
|
||||
button.icon-button i.fa-retweet {
|
||||
height: 19px;
|
||||
width: 22px;
|
||||
background-position: 0 0;
|
||||
|
@ -1211,7 +1270,7 @@ button i.fa-retweet {
|
|||
}
|
||||
}
|
||||
|
||||
button.active i.fa-retweet {
|
||||
button.icon-button.active i.fa-retweet {
|
||||
transition-duration: 0.9s;
|
||||
background-position: 0 100%;
|
||||
}
|
||||
|
@ -1381,12 +1440,15 @@ button.active i.fa-retweet {
|
|||
|
||||
.empty-column-indicator {
|
||||
color: lighten($color1, 20%);
|
||||
background: $color1;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
padding-top: 100px;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
cursor: default;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
align-items: center;
|
||||
|
||||
a {
|
||||
color: $color4;
|
||||
|
@ -1412,22 +1474,23 @@ button.active i.fa-retweet {
|
|||
}
|
||||
|
||||
.emoji-dialog {
|
||||
width: 280px;
|
||||
height: 220px;
|
||||
background: $color2;
|
||||
width: 245px;
|
||||
height: 270px;
|
||||
background: $color5;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
box-shadow: 0 0 15px rgba($color8, 0.4);
|
||||
box-shadow: 0 0 8px rgba($color8, 0.2);
|
||||
|
||||
.emojione {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.emoji-dialog-header {
|
||||
padding: 0 10px;
|
||||
background-color: $color3;
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
|
@ -1438,18 +1501,29 @@ button.active i.fa-retweet {
|
|||
li {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
height: 42px;
|
||||
padding: 9px 5px;
|
||||
padding: 10px 5px;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
.emoji {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
img, svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
img, svg {
|
||||
filter: grayscale(0);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: lighten($color3, 6%);
|
||||
border-bottom-color: $color4;
|
||||
|
||||
img, svg {
|
||||
filter: grayscale(0);
|
||||
|
@ -1473,7 +1547,7 @@ button.active i.fa-retweet {
|
|||
.emoji-category-header {
|
||||
box-sizing: border-box;
|
||||
overflow-y: hidden;
|
||||
padding: 8px 16px 0;
|
||||
padding: 10px 8px 10px 16px;
|
||||
display: table;
|
||||
|
||||
> * {
|
||||
|
@ -1483,10 +1557,10 @@ button.active i.fa-retweet {
|
|||
}
|
||||
|
||||
.emoji-category-title {
|
||||
font-size: 14px;
|
||||
font-family: sans-serif;
|
||||
font-weight: normal;
|
||||
color: $color1;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
color: darken($color2, 18%);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
|
@ -1526,7 +1600,7 @@ button.active i.fa-retweet {
|
|||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid $color1;
|
||||
border: 2px solid $color5;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
}
|
||||
|
@ -1534,14 +1608,20 @@ button.active i.fa-retweet {
|
|||
}
|
||||
|
||||
.emoji-search-wrapper {
|
||||
padding: 6px 16px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid lighten($color2, 4%);
|
||||
}
|
||||
|
||||
.emoji-search {
|
||||
font-size: 12px;
|
||||
padding: 6px 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
padding: 7px 9px;
|
||||
font-family: inherit;
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 1px solid #ddd;
|
||||
background: rgba($color2, 0.3);
|
||||
color: darken($color2, 18%);
|
||||
border: 1px solid $color2;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
@ -1554,11 +1634,21 @@ button.active i.fa-retweet {
|
|||
}
|
||||
|
||||
.emoji-search-wrapper + .emoji-categories-wrapper {
|
||||
top: 83px;
|
||||
top: 93px;
|
||||
}
|
||||
|
||||
.emoji-row .emoji:hover {
|
||||
background: lighten($color2, 3%);
|
||||
.emoji-row .emoji {
|
||||
img, svg {
|
||||
transition: transform 60ms ease-in-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: lighten($color2, 3%);
|
||||
|
||||
img, svg {
|
||||
transform: translateZ(0) scale(1.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emoji {
|
||||
|
@ -1915,3 +2005,41 @@ button.active i.fa-retweet {
|
|||
max-height: 80vh;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,11 +35,11 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
|
||||
def followers
|
||||
@followers = @account.followers.order('follows.created_at desc').paginate(page: params[:page], per_page: 12)
|
||||
@followers = @account.followers.order('follows.created_at desc').page(params[:page]).per(12)
|
||||
end
|
||||
|
||||
def following
|
||||
@following = @account.following.order('follows.created_at desc').paginate(page: params[:page], per_page: 12)
|
||||
@following = @account.following.order('follows.created_at desc').page(params[:page]).per(12)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -53,7 +53,7 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
|
||||
def webfinger_account_url
|
||||
webfinger_url(resource: "acct:#{@account.acct}@#{Rails.configuration.x.local_domain}")
|
||||
webfinger_url(resource: @account.to_webfinger_s)
|
||||
end
|
||||
|
||||
def check_account_suspension
|
||||
|
|
|
@ -1,51 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::AccountsController < ApplicationController
|
||||
before_action :require_admin!
|
||||
before_action :set_account, except: :index
|
||||
module Admin
|
||||
class AccountsController < BaseController
|
||||
before_action :set_account, except: :index
|
||||
|
||||
layout 'admin'
|
||||
def index
|
||||
@accounts = Account.alphabetic.page(params[:page])
|
||||
|
||||
def index
|
||||
@accounts = Account.alphabetic.paginate(page: params[:page], per_page: 40)
|
||||
@accounts = @accounts.local if params[:local].present?
|
||||
@accounts = @accounts.remote if params[:remote].present?
|
||||
@accounts = @accounts.where(domain: params[:by_domain]) if params[:by_domain].present?
|
||||
@accounts = @accounts.silenced if params[:silenced].present?
|
||||
@accounts = @accounts.recent if params[:recent].present?
|
||||
@accounts = @accounts.suspended if params[:suspended].present?
|
||||
end
|
||||
|
||||
@accounts = @accounts.local if params[:local].present?
|
||||
@accounts = @accounts.remote if params[:remote].present?
|
||||
@accounts = @accounts.where(domain: params[:by_domain]) if params[:by_domain].present?
|
||||
@accounts = @accounts.silenced if params[:silenced].present?
|
||||
@accounts = @accounts.recent if params[:recent].present?
|
||||
@accounts = @accounts.suspended if params[:suspended].present?
|
||||
end
|
||||
def show; end
|
||||
|
||||
def show; end
|
||||
def suspend
|
||||
Admin::SuspensionWorker.perform_async(@account.id)
|
||||
redirect_to admin_accounts_path
|
||||
end
|
||||
|
||||
def suspend
|
||||
Admin::SuspensionWorker.perform_async(@account.id)
|
||||
redirect_to admin_accounts_path
|
||||
end
|
||||
def unsuspend
|
||||
@account.update(suspended: false)
|
||||
redirect_to admin_accounts_path
|
||||
end
|
||||
|
||||
def unsuspend
|
||||
@account.update(suspended: false)
|
||||
redirect_to admin_accounts_path
|
||||
end
|
||||
def silence
|
||||
@account.update(silenced: true)
|
||||
redirect_to admin_accounts_path
|
||||
end
|
||||
|
||||
def silence
|
||||
@account.update(silenced: true)
|
||||
redirect_to admin_accounts_path
|
||||
end
|
||||
def unsilence
|
||||
@account.update(silenced: false)
|
||||
redirect_to admin_accounts_path
|
||||
end
|
||||
|
||||
def unsilence
|
||||
@account.update(silenced: false)
|
||||
redirect_to admin_accounts_path
|
||||
end
|
||||
private
|
||||
|
||||
private
|
||||
def set_account
|
||||
@account = Account.find(params[:id])
|
||||
end
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:id])
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.require(:account).permit(:silenced, :suspended)
|
||||
def account_params
|
||||
params.require(:account).permit(:silenced, :suspended)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
9
app/controllers/admin/base_controller.rb
Normal file
9
app/controllers/admin/base_controller.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class BaseController < ApplicationController
|
||||
before_action :require_admin!
|
||||
|
||||
layout 'admin'
|
||||
end
|
||||
end
|
|
@ -1,32 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::DomainBlocksController < ApplicationController
|
||||
before_action :require_admin!
|
||||
module Admin
|
||||
class DomainBlocksController < BaseController
|
||||
def index
|
||||
@blocks = DomainBlock.page(params[:page])
|
||||
end
|
||||
|
||||
layout 'admin'
|
||||
def new
|
||||
@domain_block = DomainBlock.new
|
||||
end
|
||||
|
||||
def index
|
||||
@blocks = DomainBlock.paginate(page: params[:page], per_page: 40)
|
||||
end
|
||||
def create
|
||||
@domain_block = DomainBlock.new(resource_params)
|
||||
|
||||
def new
|
||||
@domain_block = DomainBlock.new
|
||||
end
|
||||
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
|
||||
|
||||
def create
|
||||
@domain_block = DomainBlock.new(resource_params)
|
||||
private
|
||||
|
||||
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
|
||||
def resource_params
|
||||
params.require(:domain_block).permit(:domain, :severity)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resource_params
|
||||
params.require(:domain_block).permit(:domain, :severity)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::PubsubhubbubController < ApplicationController
|
||||
before_action :require_admin!
|
||||
|
||||
layout 'admin'
|
||||
|
||||
def index
|
||||
@subscriptions = Subscription.order('id desc').includes(:account).paginate(page: params[:page], per_page: 40)
|
||||
module Admin
|
||||
class PubsubhubbubController < BaseController
|
||||
def index
|
||||
@subscriptions = Subscription.order('id desc').includes(:account).page(params[:page])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,45 +1,44 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::ReportsController < ApplicationController
|
||||
before_action :require_admin!
|
||||
before_action :set_report, except: [:index]
|
||||
module Admin
|
||||
class ReportsController < BaseController
|
||||
before_action :set_report, except: [:index]
|
||||
|
||||
layout 'admin'
|
||||
def index
|
||||
@reports = Report.includes(:account, :target_account).order('id desc').page(params[:page])
|
||||
@reports = params[:action_taken].present? ? @reports.resolved : @reports.unresolved
|
||||
end
|
||||
|
||||
def index
|
||||
@reports = Report.includes(:account, :target_account).order('id desc').paginate(page: params[:page], per_page: 40)
|
||||
@reports = params[:action_taken].present? ? @reports.resolved : @reports.unresolved
|
||||
end
|
||||
def show
|
||||
@statuses = Status.where(id: @report.status_ids)
|
||||
end
|
||||
|
||||
def show
|
||||
@statuses = Status.where(id: @report.status_ids)
|
||||
end
|
||||
def resolve
|
||||
@report.update(action_taken: true, action_taken_by_account_id: current_account.id)
|
||||
redirect_to admin_report_path(@report)
|
||||
end
|
||||
|
||||
def resolve
|
||||
@report.update(action_taken: true, action_taken_by_account_id: current_account.id)
|
||||
redirect_to admin_report_path(@report)
|
||||
end
|
||||
def suspend
|
||||
Admin::SuspensionWorker.perform_async(@report.target_account.id)
|
||||
Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
|
||||
redirect_to admin_report_path(@report)
|
||||
end
|
||||
|
||||
def suspend
|
||||
Admin::SuspensionWorker.perform_async(@report.target_account.id)
|
||||
Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
|
||||
redirect_to admin_report_path(@report)
|
||||
end
|
||||
def silence
|
||||
@report.target_account.update(silenced: true)
|
||||
Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
|
||||
redirect_to admin_report_path(@report)
|
||||
end
|
||||
|
||||
def silence
|
||||
@report.target_account.update(silenced: true)
|
||||
Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
|
||||
redirect_to admin_report_path(@report)
|
||||
end
|
||||
def remove
|
||||
RemovalWorker.perform_async(params[:status_id])
|
||||
redirect_to admin_report_path(@report)
|
||||
end
|
||||
|
||||
def remove
|
||||
RemovalWorker.perform_async(params[:status_id])
|
||||
redirect_to admin_report_path(@report)
|
||||
end
|
||||
private
|
||||
|
||||
private
|
||||
|
||||
def set_report
|
||||
@report = Report.find(params[:id])
|
||||
def set_report
|
||||
@report = Report.find(params[:id])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,35 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::SettingsController < ApplicationController
|
||||
before_action :require_admin!
|
||||
|
||||
layout 'admin'
|
||||
|
||||
def index
|
||||
@settings = Setting.all_as_records
|
||||
end
|
||||
|
||||
def update
|
||||
@setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id])
|
||||
value = settings_params[:value]
|
||||
|
||||
# Special cases
|
||||
value = value == 'true' if @setting.var == 'open_registrations'
|
||||
|
||||
if @setting.value != value
|
||||
@setting.value = value
|
||||
@setting.save
|
||||
module Admin
|
||||
class SettingsController < BaseController
|
||||
def index
|
||||
@settings = Setting.all_as_records
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to admin_settings_path }
|
||||
format.json { respond_with_bip(@setting) }
|
||||
def update
|
||||
@setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id])
|
||||
value = settings_params[:value]
|
||||
|
||||
# Special cases
|
||||
value = value == 'true' if @setting.var == 'open_registrations'
|
||||
|
||||
if @setting.value != value
|
||||
@setting.value = value
|
||||
@setting.save
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to admin_settings_path }
|
||||
format.json { respond_with_bip(@setting) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
private
|
||||
|
||||
def settings_params
|
||||
params.require(:setting).permit(:value)
|
||||
def settings_params
|
||||
params.require(:setting).permit(:value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,7 +9,7 @@ class Api::V1::NotificationsController < ApiController
|
|||
DEFAULT_NOTIFICATIONS_LIMIT = 15
|
||||
|
||||
def index
|
||||
@notifications = Notification.where(account: current_account).browserable.paginate_by_max_id(limit_param(DEFAULT_NOTIFICATIONS_LIMIT), params[:max_id], params[:since_id])
|
||||
@notifications = Notification.where(account: current_account).browserable(exclude_types).paginate_by_max_id(limit_param(DEFAULT_NOTIFICATIONS_LIMIT), params[:max_id], params[:since_id])
|
||||
@notifications = cache_collection(@notifications, Notification)
|
||||
statuses = @notifications.select { |n| !n.target_status.nil? }.map(&:target_status)
|
||||
|
||||
|
@ -32,7 +32,13 @@ class Api::V1::NotificationsController < ApiController
|
|||
|
||||
private
|
||||
|
||||
def exclude_types
|
||||
val = params.permit(exclude_types: [])[:exclude_types] || []
|
||||
val = [val] unless val.is_a?(Enumerable)
|
||||
val
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.permit(:limit).merge(core_params)
|
||||
params.permit(:limit, exclude_types: []).merge(core_params)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,7 @@ class ApiController < ApplicationController
|
|||
protect_from_forgery with: :null_session
|
||||
|
||||
skip_before_action :verify_authenticity_token
|
||||
skip_before_action :store_current_location
|
||||
|
||||
before_action :set_rate_limit_headers
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ class RemoteFollowController < ApplicationController
|
|||
|
||||
session[:remote_follow] = @remote_follow.acct
|
||||
|
||||
redirect_to Addressable::Template.new(redirect_url_link.template).expand(uri: "#{@account.username}@#{Rails.configuration.x.local_domain}").to_s
|
||||
redirect_to Addressable::Template.new(redirect_url_link.template).expand(uri: @account.to_webfinger_s).to_s
|
||||
else
|
||||
render :new
|
||||
end
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Settings
|
||||
module Exports
|
||||
class BlockedAccountsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
export_data = Export.new(current_account.blocking).to_csv
|
||||
|
||||
respond_to do |format|
|
||||
format.csv { send_data export_data, filename: 'blocking.csv' }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Settings
|
||||
module Exports
|
||||
class FollowingAccountsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
export_data = Export.new(current_account.following).to_csv
|
||||
|
||||
respond_to do |format|
|
||||
format.csv { send_data export_data, filename: 'following.csv' }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,46 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'csv'
|
||||
|
||||
class Settings::ExportsController < ApplicationController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_account
|
||||
|
||||
def show
|
||||
@total_storage = current_account.media_attachments.sum(:file_file_size)
|
||||
@total_follows = current_account.following.count
|
||||
@total_blocks = current_account.blocking.count
|
||||
end
|
||||
|
||||
def download_following_list
|
||||
@accounts = current_account.following
|
||||
|
||||
respond_to do |format|
|
||||
format.csv { render text: accounts_list_to_csv(@accounts) }
|
||||
end
|
||||
end
|
||||
|
||||
def download_blocking_list
|
||||
@accounts = current_account.blocking
|
||||
|
||||
respond_to do |format|
|
||||
format.csv { render text: accounts_list_to_csv(@accounts) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = current_user.account
|
||||
end
|
||||
|
||||
def accounts_list_to_csv(list)
|
||||
CSV.generate do |csv|
|
||||
list.each do |account|
|
||||
csv << [(account.local? ? "#{account.username}@#{Rails.configuration.x.local_domain}" : account.acct)]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,8 +23,9 @@ class Settings::PreferencesController < ApplicationController
|
|||
}
|
||||
|
||||
current_user.settings['default_privacy'] = user_params[:setting_default_privacy]
|
||||
current_user.settings['boost_modal'] = user_params[:setting_boost_modal] == '1'
|
||||
|
||||
if current_user.update(user_params.except(:notification_emails, :interactions, :setting_default_privacy))
|
||||
if current_user.update(user_params.except(:notification_emails, :interactions, :setting_default_privacy, :setting_boost_modal))
|
||||
redirect_to settings_preferences_path, notice: I18n.t('generic.changes_saved_msg')
|
||||
else
|
||||
render action: :show
|
||||
|
@ -34,6 +35,6 @@ class Settings::PreferencesController < ApplicationController
|
|||
private
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:locale, :setting_default_privacy, notification_emails: [:follow, :follow_request, :reblog, :favourite, :mention, :digest], interactions: [:must_be_follower, :must_be_following])
|
||||
params.require(:user).permit(:locale, :setting_default_privacy, :setting_boost_modal, notification_emails: [:follow, :follow_request, :reblog, :favourite, :mention, :digest], interactions: [:must_be_follower, :must_be_following])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,7 @@ class XrdController < ApplicationController
|
|||
|
||||
def webfinger
|
||||
@account = Account.find_local!(username_from_resource)
|
||||
@canonical_account_uri = "acct:#{@account.username}@#{Rails.configuration.x.local_domain}"
|
||||
@canonical_account_uri = @account.to_webfinger_s
|
||||
@magic_key = pem_to_magic_key(@account.keypair.public_key)
|
||||
|
||||
respond_to do |format|
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AccountsHelper
|
||||
def pagination_options
|
||||
{
|
||||
previous_label: safe_join([fa_icon('chevron-left'), t('pagination.prev')], ' '),
|
||||
next_label: safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '),
|
||||
inner_window: 1,
|
||||
outer_window: 0,
|
||||
}
|
||||
end
|
||||
end
|
|
@ -1,285 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AtomBuilderHelper
|
||||
def stream_updated_at
|
||||
if @account.stream_entries.last
|
||||
(@account.updated_at > @account.stream_entries.last.created_at ? @account.updated_at : @account.stream_entries.last.created_at)
|
||||
else
|
||||
@account.updated_at
|
||||
end
|
||||
end
|
||||
|
||||
def entry(xml, is_root = false, &block)
|
||||
if is_root
|
||||
root_tag(xml, :entry, &block)
|
||||
else
|
||||
xml.entry(&block)
|
||||
end
|
||||
end
|
||||
|
||||
def feed(xml, &block)
|
||||
root_tag(xml, :feed, &block)
|
||||
end
|
||||
|
||||
def unique_id(xml, date, id, type)
|
||||
xml.id_ TagManager.instance.unique_tag(date, id, type)
|
||||
end
|
||||
|
||||
def simple_id(xml, id)
|
||||
xml.id_ id
|
||||
end
|
||||
|
||||
def published_at(xml, date)
|
||||
xml.published date.iso8601
|
||||
end
|
||||
|
||||
def updated_at(xml, date)
|
||||
xml.updated date.iso8601
|
||||
end
|
||||
|
||||
def verb(xml, verb)
|
||||
xml['activity'].send('verb', TagManager::VERBS[verb])
|
||||
end
|
||||
|
||||
def content(xml, content, warning = nil)
|
||||
xml.summary(warning) unless warning.blank?
|
||||
xml.content({ type: 'html' }, content) unless content.blank?
|
||||
end
|
||||
|
||||
def title(xml, title)
|
||||
xml.title strip_tags(title || '').truncate(80)
|
||||
end
|
||||
|
||||
def author(xml, &block)
|
||||
xml.author(&block)
|
||||
end
|
||||
|
||||
def category(xml, term)
|
||||
xml.category(term: term)
|
||||
end
|
||||
|
||||
def target(xml, &block)
|
||||
xml['activity'].object(&block)
|
||||
end
|
||||
|
||||
def object_type(xml, type)
|
||||
xml['activity'].send('object-type', TagManager::TYPES[type])
|
||||
end
|
||||
|
||||
def uri(xml, uri)
|
||||
xml.uri uri
|
||||
end
|
||||
|
||||
def name(xml, name)
|
||||
xml.name name
|
||||
end
|
||||
|
||||
def summary(xml, summary)
|
||||
xml.summary(summary) unless summary.blank?
|
||||
end
|
||||
|
||||
def subtitle(xml, subtitle)
|
||||
xml.subtitle(subtitle) unless subtitle.blank?
|
||||
end
|
||||
|
||||
def link_alternate(xml, url)
|
||||
xml.link(rel: 'alternate', type: 'text/html', href: url)
|
||||
end
|
||||
|
||||
def link_self(xml, url)
|
||||
xml.link(rel: 'self', type: 'application/atom+xml', href: url)
|
||||
end
|
||||
|
||||
def link_next(xml, url)
|
||||
xml.link(rel: 'next', type: 'application/atom+xml', href: url)
|
||||
end
|
||||
|
||||
def link_hub(xml, url)
|
||||
xml.link(rel: 'hub', href: url)
|
||||
end
|
||||
|
||||
def link_salmon(xml, url)
|
||||
xml.link(rel: 'salmon', href: url)
|
||||
end
|
||||
|
||||
def portable_contact(xml, account)
|
||||
xml['poco'].preferredUsername account.username
|
||||
xml['poco'].displayName(account.display_name) unless account.display_name.blank?
|
||||
xml['poco'].note(Formatter.instance.simplified_format(account)) unless account.note.blank?
|
||||
end
|
||||
|
||||
def in_reply_to(xml, uri, url)
|
||||
xml['thr'].send('in-reply-to', ref: uri, href: url, type: 'text/html')
|
||||
end
|
||||
|
||||
def link_mention(xml, account)
|
||||
xml.link(:rel => 'mentioned', :href => TagManager.instance.uri_for(account), 'ostatus:object-type' => TagManager::TYPES[:person])
|
||||
end
|
||||
|
||||
def link_enclosure(xml, media)
|
||||
xml.link(rel: 'enclosure', href: full_asset_url(media.file.url(:original, false)), type: media.file_content_type, length: media.file_file_size)
|
||||
end
|
||||
|
||||
def link_avatar(xml, account)
|
||||
single_link_avatar(xml, account, :original, 120)
|
||||
end
|
||||
|
||||
def link_header(xml, account)
|
||||
xml.link('rel' => 'header', 'type' => account.header_content_type, 'media:width' => 700, 'media:height' => 335, 'href' => full_asset_url(account.header.url(:original)))
|
||||
end
|
||||
|
||||
def logo(xml, url)
|
||||
xml.logo url
|
||||
end
|
||||
|
||||
def email(xml, email)
|
||||
xml.email email
|
||||
end
|
||||
|
||||
def conditionally_formatted(activity)
|
||||
if activity.is_a?(Status)
|
||||
Formatter.instance.format(activity.reblog? ? activity.reblog : activity)
|
||||
elsif activity.nil?
|
||||
nil
|
||||
else
|
||||
activity.content
|
||||
end
|
||||
end
|
||||
|
||||
def link_visibility(xml, item)
|
||||
return unless item.respond_to?(:visibility) && item.public_visibility?
|
||||
xml.link(:rel => 'mentioned', :href => TagManager::COLLECTIONS[:public], 'ostatus:object-type' => TagManager::TYPES[:collection])
|
||||
end
|
||||
|
||||
def privacy_scope(xml, level)
|
||||
xml['mastodon'].scope(level)
|
||||
end
|
||||
|
||||
def include_author(xml, account)
|
||||
simple_id xml, TagManager.instance.uri_for(account)
|
||||
object_type xml, :person
|
||||
uri xml, TagManager.instance.uri_for(account)
|
||||
name xml, account.username
|
||||
email xml, account.local? ? "#{account.acct}@#{Rails.configuration.x.local_domain}" : account.acct
|
||||
summary xml, account.note
|
||||
link_alternate xml, TagManager.instance.url_for(account)
|
||||
link_avatar xml, account
|
||||
link_header xml, account
|
||||
portable_contact xml, account
|
||||
privacy_scope xml, account.locked? ? :private : :public
|
||||
end
|
||||
|
||||
def rich_content(xml, activity)
|
||||
if activity.is_a?(Status)
|
||||
content xml, conditionally_formatted(activity), activity.spoiler_text
|
||||
else
|
||||
content xml, conditionally_formatted(activity)
|
||||
end
|
||||
end
|
||||
|
||||
def include_target(xml, target)
|
||||
simple_id xml, TagManager.instance.uri_for(target)
|
||||
|
||||
if target.object_type == :person
|
||||
include_author xml, target
|
||||
else
|
||||
object_type xml, target.object_type
|
||||
verb xml, target.verb
|
||||
title xml, target.title
|
||||
link_alternate xml, TagManager.instance.url_for(target)
|
||||
end
|
||||
|
||||
# Statuses have content and author
|
||||
return unless target.is_a?(Status)
|
||||
|
||||
rich_content xml, target
|
||||
verb xml, target.verb
|
||||
published_at xml, target.created_at
|
||||
updated_at xml, target.updated_at
|
||||
|
||||
author(xml) do
|
||||
include_author xml, target.account
|
||||
end
|
||||
|
||||
if target.reply?
|
||||
in_reply_to xml, TagManager.instance.uri_for(target.thread), TagManager.instance.url_for(target.thread)
|
||||
end
|
||||
|
||||
link_visibility xml, target
|
||||
|
||||
target.mentions.each do |mention|
|
||||
link_mention xml, mention.account
|
||||
end
|
||||
|
||||
target.media_attachments.each do |media|
|
||||
link_enclosure xml, media
|
||||
end
|
||||
|
||||
target.tags.each do |tag|
|
||||
category xml, tag.name
|
||||
end
|
||||
|
||||
category(xml, 'nsfw') if target.sensitive?
|
||||
privacy_scope(xml, target.visibility)
|
||||
end
|
||||
|
||||
def include_entry(xml, stream_entry)
|
||||
unique_id xml, stream_entry.created_at, stream_entry.activity_id, stream_entry.activity_type
|
||||
published_at xml, stream_entry.created_at
|
||||
updated_at xml, stream_entry.updated_at
|
||||
title xml, stream_entry.title
|
||||
rich_content xml, stream_entry.activity
|
||||
verb xml, stream_entry.verb
|
||||
link_self xml, account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom')
|
||||
link_alternate xml, account_stream_entry_url(stream_entry.account, stream_entry)
|
||||
object_type xml, stream_entry.object_type
|
||||
|
||||
# Comments need thread element
|
||||
if stream_entry.threaded?
|
||||
in_reply_to xml, TagManager.instance.uri_for(stream_entry.thread), TagManager.instance.url_for(stream_entry.thread)
|
||||
end
|
||||
|
||||
if stream_entry.targeted?
|
||||
target(xml) do
|
||||
include_target(xml, stream_entry.target)
|
||||
end
|
||||
end
|
||||
|
||||
link_visibility xml, stream_entry.activity
|
||||
|
||||
stream_entry.mentions.each do |mentioned|
|
||||
link_mention xml, mentioned
|
||||
end
|
||||
|
||||
return unless stream_entry.activity.is_a?(Status)
|
||||
|
||||
stream_entry.activity.media_attachments.each do |media|
|
||||
link_enclosure xml, media
|
||||
end
|
||||
|
||||
stream_entry.activity.tags.each do |tag|
|
||||
category xml, tag.name
|
||||
end
|
||||
|
||||
category(xml, 'nsfw') if stream_entry.activity.sensitive?
|
||||
privacy_scope(xml, stream_entry.activity.visibility)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def root_tag(xml, tag, &block)
|
||||
xml.send(tag, {
|
||||
'xmlns' => TagManager::XMLNS,
|
||||
'xmlns:thr' => TagManager::THR_XMLNS,
|
||||
'xmlns:activity' => TagManager::AS_XMLNS,
|
||||
'xmlns:poco' => TagManager::POCO_XMLNS,
|
||||
'xmlns:media' => TagManager::MEDIA_XMLNS,
|
||||
'xmlns:ostatus' => TagManager::OS_XMLNS,
|
||||
'xmlns:mastodon' => TagManager::MTDN_XMLNS,
|
||||
}, &block)
|
||||
end
|
||||
|
||||
def single_link_avatar(xml, account, size, px)
|
||||
xml.link('rel' => 'avatar', 'type' => account.avatar_content_type, 'media:width' => px, 'media:height' => px, 'href' => full_asset_url(account.avatar.url(size)))
|
||||
end
|
||||
end
|
|
@ -5,13 +5,16 @@ module SettingsHelper
|
|||
en: 'English',
|
||||
de: 'Deutsch',
|
||||
es: 'Español',
|
||||
eo: 'Esperanto',
|
||||
pt: 'Português',
|
||||
fr: 'Français',
|
||||
hu: 'Magyar',
|
||||
uk: 'Українська',
|
||||
'zh-CN': '简体中文',
|
||||
fi: 'Suomi',
|
||||
eo: 'Esperanto',
|
||||
ru: 'Русский',
|
||||
ja: '日本語',
|
||||
|
||||
}.freeze
|
||||
|
||||
def human_locale(locale)
|
||||
|
|
|
@ -9,10 +9,6 @@ module StreamEntriesHelper
|
|||
"@#{account.acct}#{@external_links && account.local? ? "@#{Rails.configuration.x.local_domain}" : ''}"
|
||||
end
|
||||
|
||||
def avatar_for_status_url(status)
|
||||
status.reblog? ? status.reblog.account.avatar.url(:original) : status.account.avatar.url(:original)
|
||||
end
|
||||
|
||||
def entry_classes(status, is_predecessor, is_successor, include_threads)
|
||||
classes = ['entry']
|
||||
classes << 'entry-reblog u-repost-of h-cite' if status.reblog?
|
||||
|
@ -22,18 +18,6 @@ module StreamEntriesHelper
|
|||
classes.join(' ')
|
||||
end
|
||||
|
||||
def relative_time(date)
|
||||
date < 5.days.ago ? date.strftime('%d.%m.%Y') : "#{time_ago_in_words(date)} ago"
|
||||
end
|
||||
|
||||
def reblogged_by_me_class(status)
|
||||
user_signed_in? && @reblogged.key?(status.id) ? 'reblogged' : ''
|
||||
end
|
||||
|
||||
def favourited_by_me_class(status)
|
||||
user_signed_in? && @favourited.key?(status.id) ? 'favourited' : ''
|
||||
end
|
||||
|
||||
def rtl?(text)
|
||||
return false if text.empty?
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ class AtomSerializer
|
|||
append_element(author, 'activity:object-type', TagManager::TYPES[:person])
|
||||
append_element(author, 'uri', uri)
|
||||
append_element(author, 'name', account.username)
|
||||
append_element(author, 'email', account.local? ? "#{account.acct}@#{Rails.configuration.x.local_domain}" : account.acct)
|
||||
append_element(author, 'email', account.local? ? account.local_username_and_domain : account.acct)
|
||||
append_element(author, 'summary', account.note)
|
||||
append_element(author, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(account))
|
||||
append_element(author, 'link', nil, rel: :avatar, type: account.avatar_content_type, 'media:width': 120, 'media:height': 120, href: full_asset_url(account.avatar.url(:original)))
|
||||
|
|
|
@ -66,7 +66,7 @@ class FeedManager
|
|||
timeline_key = key(:home, into_account.id)
|
||||
oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
|
||||
|
||||
from_account.statuses.select('id').where('id > ?', oldest_home_score).find_in_batches do |statuses|
|
||||
from_account.statuses.select('id').where('id > ?', oldest_home_score).reorder(nil).find_in_batches do |statuses|
|
||||
redis.pipelined do
|
||||
statuses.each do |status|
|
||||
redis.zrem(timeline_key, status.id)
|
||||
|
|
|
@ -15,7 +15,6 @@ class Formatter
|
|||
html = status.text
|
||||
html = encode(html)
|
||||
html = simple_format(html, {}, sanitize: false)
|
||||
html = html.gsub(/\n/, '')
|
||||
html = link_urls(html)
|
||||
html = link_mentions(html, status.mentions)
|
||||
html = link_hashtags(html)
|
||||
|
|
|
@ -12,12 +12,12 @@ class Account < ApplicationRecord
|
|||
validates :username, presence: true, uniqueness: { scope: :domain, case_sensitive: true }, unless: 'local?'
|
||||
|
||||
# Avatar upload
|
||||
has_attached_file :avatar, styles: { original: '120x120#' }, convert_options: { all: '-quality 80 -strip' }
|
||||
has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, convert_options: { all: '-quality 80 -strip' }
|
||||
validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES
|
||||
validates_attachment_size :avatar, less_than: 2.megabytes
|
||||
|
||||
# Header upload
|
||||
has_attached_file :header, styles: { original: '700x335#' }, convert_options: { all: '-quality 80 -strip' }
|
||||
has_attached_file :header, styles: ->(f) { header_styles(f) }, convert_options: { all: '-quality 80 -strip' }
|
||||
validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES
|
||||
validates_attachment_size :header, less_than: 2.megabytes
|
||||
|
||||
|
@ -120,6 +120,14 @@ class Account < ApplicationRecord
|
|||
local? ? username : "#{username}@#{domain}"
|
||||
end
|
||||
|
||||
def local_username_and_domain
|
||||
"#{username}@#{Rails.configuration.x.local_domain}"
|
||||
end
|
||||
|
||||
def to_webfinger_s
|
||||
"acct:#{local_username_and_domain}"
|
||||
end
|
||||
|
||||
def subscribed?
|
||||
!subscription_expires_at.blank?
|
||||
end
|
||||
|
@ -150,6 +158,22 @@ class Account < ApplicationRecord
|
|||
save!
|
||||
end
|
||||
|
||||
def avatar_original_url
|
||||
avatar.url(:original)
|
||||
end
|
||||
|
||||
def avatar_static_url
|
||||
avatar_content_type == 'image/gif' ? avatar.url(:static) : avatar_original_url
|
||||
end
|
||||
|
||||
def header_original_url
|
||||
header.url(:original)
|
||||
end
|
||||
|
||||
def header_static_url
|
||||
header_content_type == 'image/gif' ? header.url(:static) : header_original_url
|
||||
end
|
||||
|
||||
def avatar_remote_url=(url)
|
||||
parsed_url = URI.parse(url)
|
||||
|
||||
|
@ -284,6 +308,18 @@ class Account < ApplicationRecord
|
|||
def follow_mapping(query, field)
|
||||
query.pluck(field).inject({}) { |mapping, id| mapping[id] = true; mapping }
|
||||
end
|
||||
|
||||
def avatar_styles(file)
|
||||
styles = { original: '120x120#' }
|
||||
styles[:static] = { format: 'png' } if file.content_type == 'image/gif'
|
||||
styles
|
||||
end
|
||||
|
||||
def header_styles(file)
|
||||
styles = { original: '700x335#' }
|
||||
styles[:static] = { format: 'png' } if file.content_type == 'image/gif'
|
||||
styles
|
||||
end
|
||||
end
|
||||
|
||||
before_create do
|
||||
|
|
18
app/models/export.rb
Normal file
18
app/models/export.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
require 'csv'
|
||||
|
||||
class Export
|
||||
attr_reader :accounts
|
||||
|
||||
def initialize(accounts)
|
||||
@accounts = accounts
|
||||
end
|
||||
|
||||
def to_csv
|
||||
CSV.generate do |csv|
|
||||
accounts.each do |account|
|
||||
csv << [(account.local? ? account.local_username_and_domain : account.acct)]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -16,10 +16,17 @@ class Notification < ApplicationRecord
|
|||
|
||||
validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] }
|
||||
|
||||
TYPE_CLASS_MAP = {
|
||||
mention: 'Mention',
|
||||
reblog: 'Status',
|
||||
follow: 'Follow',
|
||||
follow_request: 'FollowRequest',
|
||||
favourite: 'Favourite',
|
||||
}.freeze
|
||||
|
||||
STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :media_attachments, :tags, mentions: :account]].freeze
|
||||
|
||||
scope :cache_ids, -> { select(:id, :updated_at, :activity_type, :activity_id) }
|
||||
scope :browserable, -> { where.not(activity_type: ['FollowRequest']) }
|
||||
|
||||
cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account
|
||||
|
||||
|
@ -28,12 +35,7 @@ class Notification < ApplicationRecord
|
|||
end
|
||||
|
||||
def type
|
||||
case activity_type
|
||||
when 'Status'
|
||||
:reblog
|
||||
else
|
||||
activity_type.underscore.to_sym
|
||||
end
|
||||
@type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym
|
||||
end
|
||||
|
||||
def target_status
|
||||
|
@ -50,6 +52,11 @@ class Notification < ApplicationRecord
|
|||
end
|
||||
|
||||
class << self
|
||||
def browserable(types = [])
|
||||
types.concat([:follow_request])
|
||||
where.not(activity_type: activity_types_from_types(types))
|
||||
end
|
||||
|
||||
def reload_stale_associations!(cached_items)
|
||||
account_ids = cached_items.map(&:from_account_id).uniq
|
||||
accounts = Account.where(id: account_ids).map { |a| [a.id, a] }.to_h
|
||||
|
@ -58,6 +65,12 @@ class Notification < ApplicationRecord
|
|||
item.from_account = accounts[item.from_account_id]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def activity_types_from_types(types)
|
||||
types.map { |type| TYPE_CLASS_MAP[type.to_sym] }.compact
|
||||
end
|
||||
end
|
||||
|
||||
after_initialize :set_from_account
|
||||
|
|
|
@ -26,4 +26,8 @@ class User < ApplicationRecord
|
|||
def setting_default_privacy
|
||||
settings.default_privacy || (account.locked? ? 'private' : 'public')
|
||||
end
|
||||
|
||||
def setting_boost_modal
|
||||
settings.boost_modal
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,5 +5,4 @@ class BaseService
|
|||
include ActionView::Helpers::SanitizeHelper
|
||||
|
||||
include RoutingHelper
|
||||
include AtomBuilderHelper
|
||||
end
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
.info
|
||||
= link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
|
||||
·
|
||||
= link_to t('about.other_instances'), 'https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md'
|
||||
= link_to t('about.other_instances'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md'
|
||||
·
|
||||
= link_to t('about.about_this'), about_more_path
|
||||
|
||||
|
@ -79,8 +79,8 @@
|
|||
.info
|
||||
= link_to t('about.terms'), terms_path
|
||||
·
|
||||
= link_to t('about.apps'), 'https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/Apps.md'
|
||||
= link_to t('about.apps'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md'
|
||||
·
|
||||
= link_to t('about.source_code'), 'https://github.com/tootsuite/mastodon'
|
||||
·
|
||||
= link_to t('about.other_instances'), 'https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md'
|
||||
= link_to t('about.other_instances'), 'https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md'
|
||||
|
|
|
@ -9,4 +9,4 @@
|
|||
- else
|
||||
= render partial: 'grid_card', collection: @followers, as: :account, cached: true
|
||||
|
||||
= will_paginate @followers, pagination_options
|
||||
= paginate @followers
|
||||
|
|
|
@ -9,4 +9,4 @@
|
|||
- else
|
||||
= render partial: 'grid_card', collection: @following, as: :account, cached: true
|
||||
|
||||
= will_paginate @following, pagination_options
|
||||
= paginate @following
|
||||
|
|
|
@ -31,4 +31,4 @@
|
|||
|
||||
.pagination
|
||||
- if @statuses.size == 20
|
||||
= link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), short_account_url(@account, max_id: @statuses.last.id), class: 'next_page', rel: 'next'
|
||||
= link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), short_account_url(@account, max_id: @statuses.last.id), class: 'next', rel: 'next'
|
||||
|
|
|
@ -46,4 +46,4 @@
|
|||
= table_link_to 'globe', 'Public', TagManager.instance.url_for(account)
|
||||
= table_link_to 'pencil', 'Edit', admin_account_path(account.id)
|
||||
|
||||
= will_paginate @accounts, pagination_options
|
||||
= paginate @accounts
|
||||
|
|
|
@ -13,5 +13,5 @@
|
|||
%samp= block.domain
|
||||
%td= block.severity
|
||||
|
||||
= will_paginate @blocks, pagination_options
|
||||
= paginate @blocks
|
||||
= link_to 'Add new', new_admin_domain_block_path, class: 'button'
|
||||
|
|
|
@ -26,4 +26,4 @@
|
|||
- else
|
||||
= l subscription.last_successful_delivery_at
|
||||
|
||||
= will_paginate @subscriptions, pagination_options
|
||||
= paginate @subscriptions
|
||||
|
|
|
@ -29,4 +29,4 @@
|
|||
%td= truncate(report.comment, length: 30, separator: ' ')
|
||||
%td= table_link_to 'circle', 'View', admin_report_path(report)
|
||||
|
||||
= will_paginate @reports, pagination_options
|
||||
= paginate @reports
|
||||
|
|
|
@ -4,8 +4,9 @@ attributes :id, :username, :acct, :display_name, :locked, :created_at
|
|||
|
||||
node(:note) { |account| Formatter.instance.simplified_format(account) }
|
||||
node(:url) { |account| TagManager.instance.url_for(account) }
|
||||
node(:avatar) { |account| full_asset_url(account.avatar.url(:original)) }
|
||||
node(:header) { |account| full_asset_url(account.header.url(:original)) }
|
||||
node(:followers_count) { |account| defined?(@followers_counts_map) ? (@followers_counts_map[account.id] || 0) : account.followers_count }
|
||||
node(:following_count) { |account| defined?(@following_counts_map) ? (@following_counts_map[account.id] || 0) : account.following_count }
|
||||
node(:statuses_count) { |account| defined?(@statuses_counts_map) ? (@statuses_counts_map[account.id] || 0) : account.statuses_count }
|
||||
node(:avatar) { |account| full_asset_url(account.avatar_original_url) }
|
||||
node(:avatar_static) { |account| full_asset_url(account.avatar_static_url) }
|
||||
node(:header) { |account| full_asset_url(account.header_original_url) }
|
||||
node(:header_static) { |account| full_asset_url(account.header_static_url) }
|
||||
|
||||
attributes :followers_count, :following_count, :statuses_count
|
||||
|
|
|
@ -5,6 +5,7 @@ node(:meta) do
|
|||
access_token: @token,
|
||||
locale: I18n.locale,
|
||||
me: current_account.id,
|
||||
boost_modal: current_account.user.setting_boost_modal,
|
||||
}
|
||||
end
|
||||
|
||||
|
|
9
app/views/kaminari/_next_page.html.haml
Normal file
9
app/views/kaminari/_next_page.html.haml
Normal file
|
@ -0,0 +1,9 @@
|
|||
-# Link to the "Next" page
|
||||
-# available local variables
|
||||
-# url: url to the next page
|
||||
-# current_page: a page object for the currently displayed page
|
||||
-# total_pages: total number of pages
|
||||
-# per_page: number of items to fetch per page
|
||||
-# remote: data-remote
|
||||
%span.next
|
||||
= link_to_unless current_page.last?, safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), url, rel: 'next', remote: remote
|
16
app/views/kaminari/_paginator.html.haml
Normal file
16
app/views/kaminari/_paginator.html.haml
Normal file
|
@ -0,0 +1,16 @@
|
|||
-# The container tag
|
||||
-# available local variables
|
||||
-# current_page: a page object for the currently displayed page
|
||||
-# total_pages: total number of pages
|
||||
-# per_page: number of items to fetch per page
|
||||
-# remote: data-remote
|
||||
-# paginator: the paginator that renders the pagination tags inside
|
||||
= paginator.render do
|
||||
%nav.pagination
|
||||
= prev_page_tag unless current_page.first?
|
||||
- each_page do |page|
|
||||
- if page.display_tag?
|
||||
= page_tag page
|
||||
- elsif !page.was_truncated?
|
||||
= gap_tag
|
||||
= next_page_tag unless current_page.last?
|
9
app/views/kaminari/_prev_page.html.haml
Normal file
9
app/views/kaminari/_prev_page.html.haml
Normal file
|
@ -0,0 +1,9 @@
|
|||
-# Link to the "Previous" page
|
||||
-# available local variables
|
||||
-# url: url to the previous page
|
||||
-# current_page: a page object for the currently displayed page
|
||||
-# total_pages: total number of pages
|
||||
-# per_page: number of items to fetch per page
|
||||
-# remote: data-remote
|
||||
%span.prev
|
||||
= link_to_unless current_page.first?, safe_join([fa_icon('chevron-left'), t('pagination.prev')], ' '), url, rel: 'prev', remote: remote
|
|
@ -10,8 +10,8 @@
|
|||
%tr
|
||||
%th= t('exports.follows')
|
||||
%td= @total_follows
|
||||
%td= table_link_to 'download', t('exports.csv'), follows_settings_export_path(format: :csv)
|
||||
%td= table_link_to 'download', t('exports.csv'), settings_exports_follows_path(format: :csv)
|
||||
%tr
|
||||
%th= t('exports.blocks')
|
||||
%td= @total_blocks
|
||||
%td= table_link_to 'download', t('exports.csv'), blocks_settings_export_path(format: :csv)
|
||||
%td= table_link_to 'download', t('exports.csv'), settings_exports_blocks_path(format: :csv)
|
||||
|
|
|
@ -22,5 +22,8 @@
|
|||
= ff.input :must_be_follower, as: :boolean, wrapper: :with_label
|
||||
= ff.input :must_be_following, as: :boolean, wrapper: :with_label
|
||||
|
||||
.fields-group
|
||||
= f.input :setting_boost_modal, as: :boolean, wrapper: :with_label
|
||||
|
||||
.actions
|
||||
= f.button :button, t('generic.save_changes'), type: :submit
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
.landing-strip
|
||||
= t('landing_strip_html', name: display_name(account), domain: Rails.configuration.x.local_domain, sign_up_path: new_user_registration_path)
|
||||
= t('landing_strip_html',
|
||||
name: content_tag(:span, display_name(account), class: :emojify),
|
||||
domain: Rails.configuration.x.local_domain,
|
||||
sign_up_path: new_user_registration_path)
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
= fa_icon('retweet fw')
|
||||
%span
|
||||
= link_to TagManager.instance.url_for(status.account), class: 'status__display-name muted' do
|
||||
%strong= display_name(status.account)
|
||||
%strong.emojify= display_name(status.account)
|
||||
= t('stream_entries.reblogged')
|
||||
|
||||
= render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: status.proper }
|
||||
|
|
|
@ -15,4 +15,4 @@
|
|||
|
||||
- if @statuses.size == 20
|
||||
.pagination
|
||||
= link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), tag_url(@tag, max_id: @statuses.last.id), class: 'next_page', rel: 'next'
|
||||
= link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), tag_url(@tag, max_id: @statuses.last.id), class: 'next', rel: 'next'
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<p>ようこそ<%= @resource.email %>さん</p>
|
||||
|
||||
<p>以下のリンクをクリックしてMastodonアカウントのメールアドレスを確認してください</p>
|
||||
|
||||
<p><%= link_to 'メールアドレスの確認', confirmation_url(@resource, confirmation_token: @token) %></p>
|
|
@ -0,0 +1,5 @@
|
|||
ようこそ<%= @resource.email %>さん
|
||||
|
||||
以下のリンクをクリックしてMastodonアカウントのメールアドレスを確認してください
|
||||
|
||||
<%= confirmation_url(@resource, confirmation_token: @token) %>
|
3
app/views/user_mailer/password_change.ja.html.erb
Normal file
3
app/views/user_mailer/password_change.ja.html.erb
Normal file
|
@ -0,0 +1,3 @@
|
|||
<p>こんにちは<%= @resource.email %>さん</p>
|
||||
|
||||
<p>Mastodonアカウントのパスワードが変更されました。</p>
|
3
app/views/user_mailer/password_change.ja.text.erb
Normal file
3
app/views/user_mailer/password_change.ja.text.erb
Normal file
|
@ -0,0 +1,3 @@
|
|||
こんにちは<%= @resource.email %>さん
|
||||
|
||||
Mastodonアカウントのパスワードが変更されました。
|
|
@ -0,0 +1,8 @@
|
|||
<p>こんにちは<%= @resource.email %>さん</p>
|
||||
|
||||
<p>Mastodonアカウントのパスワードの変更がリクエストされました。以下のリンクをクリックして操作を完了できます。</p>
|
||||
|
||||
<p><%= link_to 'パスワードを変更', edit_password_url(@resource, reset_password_token: @token) %></p>
|
||||
|
||||
<p>このメールに見に覚えのない場合は無視してください。</p>
|
||||
<p>上記のリンクにアクセスし、変更をしない限りパスワードは変更されません。</p>
|
|
@ -0,0 +1,8 @@
|
|||
こんにちは<%= @resource.email %>さん
|
||||
|
||||
Mastodonアカウントのパスワードの変更がリクエストされました。以下のリンクをクリックして操作を完了できます。
|
||||
|
||||
<%= edit_password_url(@resource, reset_password_token: @token) %>
|
||||
|
||||
このメールに見に覚えのない場合は無視してください。
|
||||
上記のリンクにアクセスし、変更をしない限りパスワードは変更されません。
|
|
@ -4,32 +4,41 @@ require 'csv'
|
|||
|
||||
class ImportWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'pull', retry: false
|
||||
|
||||
def perform(import_id)
|
||||
import = Import.find(import_id)
|
||||
attr_reader :import
|
||||
|
||||
case import.type
|
||||
def perform(import_id)
|
||||
@import = Import.find(import_id)
|
||||
|
||||
case @import.type
|
||||
when 'blocking'
|
||||
process_blocks(import)
|
||||
process_blocks
|
||||
when 'following'
|
||||
process_follows(import)
|
||||
process_follows
|
||||
end
|
||||
|
||||
import.destroy
|
||||
@import.destroy
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_blocks(import)
|
||||
from_account = import.account
|
||||
def from_account
|
||||
@import.account
|
||||
end
|
||||
|
||||
CSV.foreach(import.data.path) do |row|
|
||||
next if row.size != 1
|
||||
def import_contents
|
||||
Paperclip.io_adapters.for(@import.data).read
|
||||
end
|
||||
|
||||
def import_rows
|
||||
CSV.new(import_contents).reject(&:blank?)
|
||||
end
|
||||
|
||||
def process_blocks
|
||||
import_rows.each do |row|
|
||||
begin
|
||||
target_account = FollowRemoteAccountService.new.call(row[0])
|
||||
target_account = FollowRemoteAccountService.new.call(row.first)
|
||||
next if target_account.nil?
|
||||
BlockService.new.call(from_account, target_account)
|
||||
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError
|
||||
|
@ -38,14 +47,10 @@ class ImportWorker
|
|||
end
|
||||
end
|
||||
|
||||
def process_follows(import)
|
||||
from_account = import.account
|
||||
|
||||
CSV.foreach(import.data.path) do |row|
|
||||
next if row.size != 1
|
||||
|
||||
def process_follows
|
||||
import_rows.each do |row|
|
||||
begin
|
||||
FollowService.new.call(from_account, row[0])
|
||||
FollowService.new.call(from_account, row.first)
|
||||
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError
|
||||
next
|
||||
end
|
||||
|
|
|
@ -24,7 +24,9 @@ module Mastodon
|
|||
|
||||
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
|
||||
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
|
||||
config.i18n.available_locales = [:en, :de, :es, :pt, :fr, :hu, :uk, 'zh-CN', :fi, :eo]
|
||||
|
||||
config.i18n.available_locales = [:en, :de, :es, :pt, :fr, :hu, :uk, 'zh-CN', :fi, :eo, :ru, :ja]
|
||||
|
||||
config.i18n.default_locale = :en
|
||||
|
||||
# config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
lock '3.7.2'
|
||||
lock '3.8.0'
|
||||
|
||||
set :application, 'mastodon'
|
||||
set :repo_url, 'https://github.com/tootsuite/mastodon.git'
|
||||
set :branch, 'master'
|
||||
set :branch, 'skylight'
|
||||
set :rbenv_type, :user
|
||||
set :rbenv_ruby, File.read('.ruby-version').strip
|
||||
set :migration_role, :app
|
||||
|
||||
append :linked_files, '.env.production'
|
||||
append :linked_dirs, 'vendor/bundle', 'node_modules', 'public/system'
|
||||
append :linked_dirs, 'vendor/bundle', 'node_modules', 'public/system', 'tmp/cache'
|
||||
|
|
|
@ -40,7 +40,7 @@ Rails.application.configure do
|
|||
|
||||
# By default, use the lowest log level to ensure availability of diagnostic information
|
||||
# when problems arise.
|
||||
config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'debug').to_sym
|
||||
config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'info').to_sym
|
||||
|
||||
# Prepend all log lines with the following tags.
|
||||
config.log_tags = [:request_id]
|
||||
|
@ -64,7 +64,7 @@ Rails.application.configure do
|
|||
password: ENV.fetch('REDIS_PASSWORD') { false },
|
||||
db: 0,
|
||||
namespace: 'cache',
|
||||
expires_in: 20.minutes,
|
||||
expires_in: 10.minutes,
|
||||
}
|
||||
|
||||
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
|
||||
|
@ -94,12 +94,14 @@ Rails.application.configure do
|
|||
|
||||
# E-mails
|
||||
config.action_mailer.smtp_settings = {
|
||||
:port => ENV['SMTP_PORT'],
|
||||
:address => ENV['SMTP_SERVER'],
|
||||
:user_name => ENV['SMTP_LOGIN'],
|
||||
:password => ENV['SMTP_PASSWORD'],
|
||||
:domain => ENV['SMTP_DOMAIN'] || config.x.local_domain,
|
||||
:authentication => :plain,
|
||||
:port => ENV['SMTP_PORT'],
|
||||
:address => ENV['SMTP_SERVER'],
|
||||
:user_name => ENV['SMTP_LOGIN'],
|
||||
:password => ENV['SMTP_PASSWORD'],
|
||||
:domain => ENV['SMTP_DOMAIN'] || config.x.local_domain,
|
||||
:authentication => ENV['SMTP_AUTH_METHOD'] || :plain,
|
||||
:openssl_verify_mode => ENV['SMTP_OPENSSL_VERIFY_MODE'],
|
||||
:enable_starttls_auto => ENV['SMTP_ENABLE_STARTTLS_AUTO'] || true,
|
||||
}
|
||||
|
||||
config.action_mailer.delivery_method = :smtp
|
||||
|
|
|
@ -33,7 +33,7 @@ search:
|
|||
|
||||
ignore_unused:
|
||||
- 'activerecord.attributes.*'
|
||||
- '{devise,will_paginate,doorkeeper}.*'
|
||||
- '{devise,pagination,doorkeeper}.*'
|
||||
- '{datetime,time}.*'
|
||||
- 'simple_form.{yes,no}'
|
||||
- 'simple_form.{placeholders,hints,labels}.*'
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue