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_LOGIN=
|
||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
SMTP_FROM_ADDRESS=notifications@example.com
|
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
|
# Optional asset host for multi-server setups
|
||||||
# CDN_HOST=assets.example.com
|
# 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
|
postgresql: 9.4
|
||||||
|
|
||||||
rvm:
|
rvm:
|
||||||
- 2.3.1
|
- 2.4.1
|
||||||
|
|
||||||
services:
|
services:
|
||||||
- redis-server
|
- 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" \
|
LABEL maintainer="https://github.com/tootsuite/mastodon" \
|
||||||
description="A GNU Social-compatible microblogging server"
|
description="A GNU Social-compatible microblogging server"
|
||||||
|
|
5
Gemfile
5
Gemfile
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
ruby '2.3.1'
|
ruby '2.4.1'
|
||||||
|
|
||||||
gem 'rails', '~> 5.0.2'
|
gem 'rails', '~> 5.0.2'
|
||||||
gem 'sass-rails', '~> 5.0'
|
gem 'sass-rails', '~> 5.0'
|
||||||
|
@ -32,6 +32,7 @@ gem 'htmlentities'
|
||||||
gem 'http'
|
gem 'http'
|
||||||
gem 'http_accept_language'
|
gem 'http_accept_language'
|
||||||
gem 'httplog'
|
gem 'httplog'
|
||||||
|
gem 'kaminari'
|
||||||
gem 'link_header'
|
gem 'link_header'
|
||||||
gem 'nokogiri'
|
gem 'nokogiri'
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
|
@ -52,7 +53,6 @@ gem 'simple_form'
|
||||||
gem 'statsd-instrument'
|
gem 'statsd-instrument'
|
||||||
gem 'twitter-text'
|
gem 'twitter-text'
|
||||||
gem 'tzinfo-data'
|
gem 'tzinfo-data'
|
||||||
gem 'will_paginate'
|
|
||||||
|
|
||||||
gem 'react-rails'
|
gem 'react-rails'
|
||||||
gem 'browserify-rails'
|
gem 'browserify-rails'
|
||||||
|
@ -71,6 +71,7 @@ end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem 'faker'
|
gem 'faker'
|
||||||
|
gem 'rails-controller-testing'
|
||||||
gem 'rspec-sidekiq'
|
gem 'rspec-sidekiq'
|
||||||
gem 'simplecov', require: false
|
gem 'simplecov', require: false
|
||||||
gem 'webmock'
|
gem 'webmock'
|
||||||
|
|
208
Gemfile.lock
208
Gemfile.lock
|
@ -24,7 +24,7 @@ GEM
|
||||||
erubis (~> 2.7.0)
|
erubis (~> 2.7.0)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
||||||
active_record_query_trace (1.5.3)
|
active_record_query_trace (1.5.4)
|
||||||
activejob (5.0.2)
|
activejob (5.0.2)
|
||||||
activesupport (= 5.0.2)
|
activesupport (= 5.0.2)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
|
@ -39,7 +39,7 @@ GEM
|
||||||
i18n (~> 0.7)
|
i18n (~> 0.7)
|
||||||
minitest (~> 5.1)
|
minitest (~> 5.1)
|
||||||
tzinfo (~> 1.1)
|
tzinfo (~> 1.1)
|
||||||
addressable (2.5.0)
|
addressable (2.5.1)
|
||||||
public_suffix (~> 2.0, >= 2.0.2)
|
public_suffix (~> 2.0, >= 2.0.2)
|
||||||
airbrussh (1.1.2)
|
airbrussh (1.1.2)
|
||||||
sshkit (>= 1.6.1, != 1.7.0)
|
sshkit (>= 1.6.1, != 1.7.0)
|
||||||
|
@ -47,17 +47,17 @@ GEM
|
||||||
ast (2.3.0)
|
ast (2.3.0)
|
||||||
attr_encrypted (3.0.3)
|
attr_encrypted (3.0.3)
|
||||||
encryptor (~> 3.0.0)
|
encryptor (~> 3.0.0)
|
||||||
autoprefixer-rails (6.5.0.2)
|
autoprefixer-rails (6.7.7.1)
|
||||||
execjs
|
execjs
|
||||||
av (0.9.0)
|
av (0.9.0)
|
||||||
cocaine (~> 0.5.3)
|
cocaine (~> 0.5.3)
|
||||||
aws-sdk (2.6.28)
|
aws-sdk (2.9.6)
|
||||||
aws-sdk-resources (= 2.6.28)
|
aws-sdk-resources (= 2.9.6)
|
||||||
aws-sdk-core (2.6.28)
|
aws-sdk-core (2.9.6)
|
||||||
aws-sigv4 (~> 1.0)
|
aws-sigv4 (~> 1.0)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-resources (2.6.28)
|
aws-sdk-resources (2.9.6)
|
||||||
aws-sdk-core (= 2.6.28)
|
aws-sdk-core (= 2.9.6)
|
||||||
aws-sigv4 (1.0.0)
|
aws-sigv4 (1.0.0)
|
||||||
babel-source (5.8.35)
|
babel-source (5.8.35)
|
||||||
babel-transpiler (0.7.0)
|
babel-transpiler (0.7.0)
|
||||||
|
@ -78,12 +78,11 @@ GEM
|
||||||
railties (>= 4.0.0, < 5.1)
|
railties (>= 4.0.0, < 5.1)
|
||||||
sprockets (>= 3.6.0)
|
sprockets (>= 3.6.0)
|
||||||
builder (3.2.3)
|
builder (3.2.3)
|
||||||
bullet (5.3.0)
|
bullet (5.5.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.10.0)
|
uniform_notifier (~> 1.10.0)
|
||||||
capistrano (3.7.2)
|
capistrano (3.8.0)
|
||||||
airbrussh (>= 1.0.0)
|
airbrussh (>= 1.0.0)
|
||||||
capistrano-harrow
|
|
||||||
i18n
|
i18n
|
||||||
rake (>= 10.0.0)
|
rake (>= 10.0.0)
|
||||||
sshkit (>= 1.9.0)
|
sshkit (>= 1.9.0)
|
||||||
|
@ -92,8 +91,7 @@ GEM
|
||||||
sshkit (~> 1.2)
|
sshkit (~> 1.2)
|
||||||
capistrano-faster-assets (1.0.2)
|
capistrano-faster-assets (1.0.2)
|
||||||
capistrano (>= 3.1)
|
capistrano (>= 3.1)
|
||||||
capistrano-harrow (0.5.3)
|
capistrano-rails (1.2.3)
|
||||||
capistrano-rails (1.2.2)
|
|
||||||
capistrano (~> 3.1)
|
capistrano (~> 3.1)
|
||||||
capistrano-bundler (~> 1.1)
|
capistrano-bundler (~> 1.1)
|
||||||
capistrano-rbenv (2.1.0)
|
capistrano-rbenv (2.1.0)
|
||||||
|
@ -119,7 +117,7 @@ GEM
|
||||||
crack (0.4.3)
|
crack (0.4.3)
|
||||||
safe_yaml (~> 1.0.0)
|
safe_yaml (~> 1.0.0)
|
||||||
debug_inspector (0.0.2)
|
debug_inspector (0.0.2)
|
||||||
devise (4.2.0)
|
devise (4.2.1)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0, < 5.1)
|
railties (>= 4.1.0, < 5.1)
|
||||||
|
@ -131,16 +129,16 @@ GEM
|
||||||
devise (~> 4.0)
|
devise (~> 4.0)
|
||||||
railties
|
railties
|
||||||
rotp (~> 2.0)
|
rotp (~> 2.0)
|
||||||
diff-lcs (1.2.5)
|
diff-lcs (1.3)
|
||||||
docile (1.1.5)
|
docile (1.1.5)
|
||||||
domain_name (0.5.20161129)
|
domain_name (0.5.20170404)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
doorkeeper (4.2.0)
|
doorkeeper (4.2.5)
|
||||||
railties (>= 4.2)
|
railties (>= 4.2)
|
||||||
dotenv (2.1.1)
|
dotenv (2.2.0)
|
||||||
dotenv-rails (2.1.1)
|
dotenv-rails (2.2.0)
|
||||||
dotenv (= 2.1.1)
|
dotenv (= 2.2.0)
|
||||||
railties (>= 4.0, < 5.1)
|
railties (>= 3.2, < 5.1)
|
||||||
easy_translate (0.5.0)
|
easy_translate (0.5.0)
|
||||||
json
|
json
|
||||||
thread
|
thread
|
||||||
|
@ -148,14 +146,14 @@ GEM
|
||||||
encryptor (3.0.0)
|
encryptor (3.0.0)
|
||||||
erubis (2.7.0)
|
erubis (2.7.0)
|
||||||
execjs (2.7.0)
|
execjs (2.7.0)
|
||||||
fabrication (2.15.2)
|
fabrication (2.16.1)
|
||||||
faker (1.6.6)
|
faker (1.7.3)
|
||||||
i18n (~> 0.5)
|
i18n (~> 0.5)
|
||||||
fast_blank (1.0.0)
|
fast_blank (1.0.0)
|
||||||
font-awesome-rails (4.6.3.1)
|
font-awesome-rails (4.7.0.1)
|
||||||
railties (>= 3.2, < 5.1)
|
railties (>= 3.2, < 5.1)
|
||||||
fuubar (2.1.1)
|
fuubar (2.2.0)
|
||||||
rspec (~> 3.0)
|
rspec-core (~> 3.0)
|
||||||
ruby-progressbar (~> 1.4)
|
ruby-progressbar (~> 1.4)
|
||||||
globalid (0.3.7)
|
globalid (0.3.7)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
|
@ -163,20 +161,20 @@ GEM
|
||||||
addressable (~> 2.4)
|
addressable (~> 2.4)
|
||||||
http (~> 2.0)
|
http (~> 2.0)
|
||||||
nokogiri (~> 1.6)
|
nokogiri (~> 1.6)
|
||||||
hamlit (2.7.2)
|
hamlit (2.8.1)
|
||||||
temple (~> 0.7.6)
|
temple (>= 0.8.0)
|
||||||
thor
|
thor
|
||||||
tilt
|
tilt
|
||||||
hamlit-rails (0.1.0)
|
hamlit-rails (0.2.0)
|
||||||
actionpack (>= 4.0.1)
|
actionpack (>= 4.0.1)
|
||||||
activesupport (>= 4.0.1)
|
activesupport (>= 4.0.1)
|
||||||
hamlit (>= 1.2.0)
|
hamlit (>= 1.2.0)
|
||||||
railties (>= 4.0.1)
|
railties (>= 4.0.1)
|
||||||
hashdiff (0.3.0)
|
hashdiff (0.3.2)
|
||||||
highline (1.7.8)
|
highline (1.7.8)
|
||||||
hiredis (0.6.1)
|
hiredis (0.6.1)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
http (2.1.0)
|
http (2.2.1)
|
||||||
addressable (~> 2.3)
|
addressable (~> 2.3)
|
||||||
http-cookie (~> 1.0)
|
http-cookie (~> 1.0)
|
||||||
http-form_data (~> 1.0.1)
|
http-form_data (~> 1.0.1)
|
||||||
|
@ -186,10 +184,10 @@ GEM
|
||||||
http-form_data (1.0.1)
|
http-form_data (1.0.1)
|
||||||
http_accept_language (2.1.0)
|
http_accept_language (2.1.0)
|
||||||
http_parser.rb (0.6.0)
|
http_parser.rb (0.6.0)
|
||||||
httplog (0.3.2)
|
httplog (0.99.2)
|
||||||
colorize
|
colorize
|
||||||
i18n (0.8.1)
|
i18n (0.8.1)
|
||||||
i18n-tasks (0.9.6)
|
i18n-tasks (0.9.13)
|
||||||
activesupport (>= 4.0.2)
|
activesupport (>= 4.0.2)
|
||||||
ast (>= 2.1.0)
|
ast (>= 2.1.0)
|
||||||
easy_translate (>= 0.5.0)
|
easy_translate (>= 0.5.0)
|
||||||
|
@ -197,19 +195,31 @@ GEM
|
||||||
highline (>= 1.7.3)
|
highline (>= 1.7.3)
|
||||||
i18n
|
i18n
|
||||||
parser (>= 2.2.3.0)
|
parser (>= 2.2.3.0)
|
||||||
term-ansicolor (>= 1.3.2)
|
rainbow (~> 2.2)
|
||||||
terminal-table (>= 1.5.1)
|
terminal-table (>= 1.5.1)
|
||||||
jmespath (1.3.1)
|
jmespath (1.3.1)
|
||||||
jquery-rails (4.1.1)
|
jquery-rails (4.3.1)
|
||||||
rails-dom-testing (>= 1, < 3)
|
rails-dom-testing (>= 1, < 3)
|
||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
thor (>= 0.14, < 2.0)
|
thor (>= 0.14, < 2.0)
|
||||||
json (1.8.3)
|
json (2.0.3)
|
||||||
|
kaminari (1.0.1)
|
||||||
|
activesupport (>= 4.1.0)
|
||||||
|
kaminari-actionview (= 1.0.1)
|
||||||
|
kaminari-activerecord (= 1.0.1)
|
||||||
|
kaminari-core (= 1.0.1)
|
||||||
|
kaminari-actionview (1.0.1)
|
||||||
|
actionview
|
||||||
|
kaminari-core (= 1.0.1)
|
||||||
|
kaminari-activerecord (1.0.1)
|
||||||
|
activerecord
|
||||||
|
kaminari-core (= 1.0.1)
|
||||||
|
kaminari-core (1.0.1)
|
||||||
launchy (2.4.3)
|
launchy (2.4.3)
|
||||||
addressable (~> 2.3)
|
addressable (~> 2.3)
|
||||||
letter_opener (1.4.1)
|
letter_opener (1.4.1)
|
||||||
launchy (~> 2.2)
|
launchy (~> 2.2)
|
||||||
letter_opener_web (1.3.0)
|
letter_opener_web (1.3.1)
|
||||||
actionmailer (>= 3.2)
|
actionmailer (>= 3.2)
|
||||||
letter_opener (~> 1.0)
|
letter_opener (~> 1.0)
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
|
@ -231,11 +241,11 @@ GEM
|
||||||
minitest (5.10.1)
|
minitest (5.10.1)
|
||||||
net-scp (1.2.1)
|
net-scp (1.2.1)
|
||||||
net-ssh (>= 2.6.5)
|
net-ssh (>= 2.6.5)
|
||||||
net-ssh (4.0.1)
|
net-ssh (4.1.0)
|
||||||
nio4r (2.0.0)
|
nio4r (2.0.0)
|
||||||
nokogiri (1.7.1)
|
nokogiri (1.7.1)
|
||||||
mini_portile2 (~> 2.1.0)
|
mini_portile2 (~> 2.1.0)
|
||||||
oj (2.17.3)
|
oj (2.18.5)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ostatus2 (1.0.2)
|
ostatus2 (1.0.2)
|
||||||
addressable (~> 2.4)
|
addressable (~> 2.4)
|
||||||
|
@ -251,26 +261,26 @@ GEM
|
||||||
paperclip-av-transcoder (0.6.4)
|
paperclip-av-transcoder (0.6.4)
|
||||||
av (~> 0.9.0)
|
av (~> 0.9.0)
|
||||||
paperclip (>= 2.5.2)
|
paperclip (>= 2.5.2)
|
||||||
parser (2.3.1.2)
|
parser (2.4.0.0)
|
||||||
ast (~> 2.2)
|
ast (~> 2.2)
|
||||||
pg (0.18.4)
|
pg (0.20.0)
|
||||||
pghero (1.6.2)
|
pghero (1.6.4)
|
||||||
activerecord
|
activerecord
|
||||||
powerpack (0.1.1)
|
powerpack (0.1.1)
|
||||||
pry (0.10.4)
|
pry (0.10.4)
|
||||||
coderay (~> 1.1.0)
|
coderay (~> 1.1.0)
|
||||||
method_source (~> 0.8.1)
|
method_source (~> 0.8.1)
|
||||||
slop (~> 3.4)
|
slop (~> 3.4)
|
||||||
pry-rails (0.3.4)
|
pry-rails (0.3.6)
|
||||||
pry (>= 0.9.10)
|
pry (>= 0.10.4)
|
||||||
public_suffix (2.0.4)
|
public_suffix (2.0.5)
|
||||||
puma (3.6.0)
|
puma (3.8.2)
|
||||||
rabl (0.13.1)
|
rabl (0.13.1)
|
||||||
activesupport (>= 2.3.14)
|
activesupport (>= 2.3.14)
|
||||||
rack (2.0.1)
|
rack (2.0.1)
|
||||||
rack-attack (5.0.1)
|
rack-attack (5.0.1)
|
||||||
rack
|
rack
|
||||||
rack-cors (0.4.0)
|
rack-cors (0.4.1)
|
||||||
rack-protection (1.5.3)
|
rack-protection (1.5.3)
|
||||||
rack
|
rack
|
||||||
rack-test (0.6.3)
|
rack-test (0.6.3)
|
||||||
|
@ -288,6 +298,10 @@ GEM
|
||||||
bundler (>= 1.3.0, < 2.0)
|
bundler (>= 1.3.0, < 2.0)
|
||||||
railties (= 5.0.2)
|
railties (= 5.0.2)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
|
rails-controller-testing (1.0.1)
|
||||||
|
actionpack (~> 5.x)
|
||||||
|
actionview (~> 5.x)
|
||||||
|
activesupport (~> 5.x)
|
||||||
rails-dom-testing (2.0.2)
|
rails-dom-testing (2.0.2)
|
||||||
activesupport (>= 4.2.0, < 6.0)
|
activesupport (>= 4.2.0, < 6.0)
|
||||||
nokogiri (~> 1.6)
|
nokogiri (~> 1.6)
|
||||||
|
@ -306,42 +320,37 @@ GEM
|
||||||
method_source
|
method_source
|
||||||
rake (>= 0.8.7)
|
rake (>= 0.8.7)
|
||||||
thor (>= 0.18.1, < 2.0)
|
thor (>= 0.18.1, < 2.0)
|
||||||
rainbow (2.1.0)
|
rainbow (2.2.1)
|
||||||
rake (12.0.0)
|
rake (12.0.0)
|
||||||
react-rails (1.10.0)
|
react-rails (1.11.0)
|
||||||
babel-transpiler (>= 0.7.0)
|
babel-transpiler (>= 0.7.0)
|
||||||
coffee-script-source (~> 1.8)
|
|
||||||
connection_pool
|
connection_pool
|
||||||
execjs
|
execjs
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
tilt
|
tilt
|
||||||
redis (3.3.2)
|
redis (3.3.3)
|
||||||
redis-actionpack (5.0.0)
|
redis-actionpack (5.0.1)
|
||||||
actionpack (>= 4.0.0, < 6)
|
actionpack (>= 4.0, < 6)
|
||||||
redis-rack (~> 2.0.0.pre)
|
redis-rack (>= 1, < 3)
|
||||||
redis-store (~> 1.2.0.pre)
|
redis-store (>= 1.1.0, < 1.4.0)
|
||||||
redis-activesupport (5.0.1)
|
redis-activesupport (5.0.2)
|
||||||
activesupport (>= 3, < 6)
|
activesupport (>= 3, < 6)
|
||||||
redis-store (~> 1.2.0)
|
redis-store (~> 1.3.0)
|
||||||
redis-rack (2.0.0)
|
redis-rack (2.0.1)
|
||||||
rack (~> 2.0)
|
rack (>= 2.0, < 3)
|
||||||
redis-store (~> 1.2.0)
|
redis-store (>= 1.2, < 1.4)
|
||||||
redis-rails (5.0.1)
|
redis-rails (5.0.2)
|
||||||
redis-actionpack (~> 5.0.0)
|
redis-actionpack (>= 5.0, < 6)
|
||||||
redis-activesupport (~> 5.0.0)
|
redis-activesupport (>= 5.0, < 6)
|
||||||
redis-store (~> 1.2.0)
|
redis-store (>= 1.2, < 2)
|
||||||
redis-store (1.2.0)
|
redis-store (1.3.0)
|
||||||
redis (>= 2.2)
|
redis (>= 2.2)
|
||||||
responders (2.3.0)
|
responders (2.3.0)
|
||||||
railties (>= 4.2.0, < 5.1)
|
railties (>= 4.2.0, < 5.1)
|
||||||
rotp (2.1.2)
|
rotp (2.1.2)
|
||||||
rqrcode (0.10.1)
|
rqrcode (0.10.1)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
rspec (3.5.0)
|
rspec-core (3.5.4)
|
||||||
rspec-core (~> 3.5.0)
|
|
||||||
rspec-expectations (~> 3.5.0)
|
|
||||||
rspec-mocks (~> 3.5.0)
|
|
||||||
rspec-core (3.5.2)
|
|
||||||
rspec-support (~> 3.5.0)
|
rspec-support (~> 3.5.0)
|
||||||
rspec-expectations (3.5.0)
|
rspec-expectations (3.5.0)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
|
@ -349,7 +358,7 @@ GEM
|
||||||
rspec-mocks (3.5.0)
|
rspec-mocks (3.5.0)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.5.0)
|
rspec-support (~> 3.5.0)
|
||||||
rspec-rails (3.5.1)
|
rspec-rails (3.5.2)
|
||||||
actionpack (>= 3.0)
|
actionpack (>= 3.0)
|
||||||
activesupport (>= 3.0)
|
activesupport (>= 3.0)
|
||||||
railties (>= 3.0)
|
railties (>= 3.0)
|
||||||
|
@ -357,27 +366,27 @@ GEM
|
||||||
rspec-expectations (~> 3.5.0)
|
rspec-expectations (~> 3.5.0)
|
||||||
rspec-mocks (~> 3.5.0)
|
rspec-mocks (~> 3.5.0)
|
||||||
rspec-support (~> 3.5.0)
|
rspec-support (~> 3.5.0)
|
||||||
rspec-sidekiq (2.2.0)
|
rspec-sidekiq (3.0.0)
|
||||||
rspec (~> 3.0, >= 3.0.0)
|
rspec-core (~> 3.0, >= 3.0.0)
|
||||||
sidekiq (>= 2.4.0)
|
sidekiq (>= 2.4.0)
|
||||||
rspec-support (3.5.0)
|
rspec-support (3.5.0)
|
||||||
rubocop (0.42.0)
|
rubocop (0.48.1)
|
||||||
parser (>= 2.3.1.1, < 3.0)
|
parser (>= 2.3.3.1, < 3.0)
|
||||||
powerpack (~> 0.1)
|
powerpack (~> 0.1)
|
||||||
rainbow (>= 1.99.1, < 3.0)
|
rainbow (>= 1.99.1, < 3.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (~> 1.0, >= 1.0.1)
|
unicode-display_width (~> 1.0, >= 1.0.1)
|
||||||
ruby-oembed (0.10.1)
|
ruby-oembed (0.12.0)
|
||||||
ruby-progressbar (1.8.1)
|
ruby-progressbar (1.8.1)
|
||||||
safe_yaml (1.0.4)
|
safe_yaml (1.0.4)
|
||||||
sass (3.4.22)
|
sass (3.4.23)
|
||||||
sass-rails (5.0.6)
|
sass-rails (5.0.6)
|
||||||
railties (>= 4.0.0, < 6)
|
railties (>= 4.0.0, < 6)
|
||||||
sass (~> 3.1)
|
sass (~> 3.1)
|
||||||
sprockets (>= 2.8, < 4.0)
|
sprockets (>= 2.8, < 4.0)
|
||||||
sprockets-rails (>= 2.0, < 4.0)
|
sprockets-rails (>= 2.0, < 4.0)
|
||||||
tilt (>= 1.1, < 3)
|
tilt (>= 1.1, < 3)
|
||||||
sidekiq (4.2.7)
|
sidekiq (4.2.10)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
connection_pool (~> 2.2, >= 2.2.0)
|
connection_pool (~> 2.2, >= 2.2.0)
|
||||||
rack-protection (>= 1.5.0)
|
rack-protection (>= 1.5.0)
|
||||||
|
@ -385,20 +394,20 @@ GEM
|
||||||
sidekiq-skylight (0.2.0)
|
sidekiq-skylight (0.2.0)
|
||||||
sidekiq (>= 3.3.0)
|
sidekiq (>= 3.3.0)
|
||||||
skylight (>= 0.5.2)
|
skylight (>= 0.5.2)
|
||||||
sidekiq-unique-jobs (4.0.18)
|
sidekiq-unique-jobs (5.0.0)
|
||||||
sidekiq (>= 2.6)
|
sidekiq (>= 4.0)
|
||||||
thor
|
thor
|
||||||
simple-navigation (4.0.3)
|
simple-navigation (4.0.5)
|
||||||
activesupport (>= 2.3.2)
|
activesupport (>= 2.3.2)
|
||||||
simple_form (3.2.1)
|
simple_form (3.4.0)
|
||||||
actionpack (> 4, < 5.1)
|
actionpack (> 4, < 5.1)
|
||||||
activemodel (> 4, < 5.1)
|
activemodel (> 4, < 5.1)
|
||||||
simplecov (0.12.0)
|
simplecov (0.14.1)
|
||||||
docile (~> 1.1.0)
|
docile (~> 1.1.0)
|
||||||
json (>= 1.8, < 3)
|
json (>= 1.8, < 3)
|
||||||
simplecov-html (~> 0.10.0)
|
simplecov-html (~> 0.10.0)
|
||||||
simplecov-html (0.10.0)
|
simplecov-html (0.10.0)
|
||||||
skylight (1.1.0)
|
skylight (1.2.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
slop (3.6.0)
|
slop (3.6.0)
|
||||||
sprockets (3.7.1)
|
sprockets (3.7.1)
|
||||||
|
@ -408,43 +417,39 @@ GEM
|
||||||
actionpack (>= 4.0)
|
actionpack (>= 4.0)
|
||||||
activesupport (>= 4.0)
|
activesupport (>= 4.0)
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
sshkit (1.11.5)
|
sshkit (1.13.1)
|
||||||
net-scp (>= 1.1.2)
|
net-scp (>= 1.1.2)
|
||||||
net-ssh (>= 2.8.0)
|
net-ssh (>= 2.8.0)
|
||||||
statsd-instrument (2.1.2)
|
statsd-instrument (2.1.2)
|
||||||
temple (0.7.7)
|
temple (0.8.0)
|
||||||
term-ansicolor (1.4.0)
|
terminal-table (1.7.3)
|
||||||
tins (~> 1.0)
|
unicode-display_width (~> 1.1.1)
|
||||||
terminal-table (1.7.0)
|
|
||||||
unicode-display_width (~> 1.1)
|
|
||||||
thor (0.19.4)
|
thor (0.19.4)
|
||||||
thread (0.2.2)
|
thread (0.2.2)
|
||||||
thread_safe (0.3.6)
|
thread_safe (0.3.6)
|
||||||
tilt (2.0.6)
|
tilt (2.0.7)
|
||||||
tins (1.12.0)
|
|
||||||
twitter-text (1.14.5)
|
twitter-text (1.14.5)
|
||||||
unf (~> 0.1.0)
|
unf (~> 0.1.0)
|
||||||
tzinfo (1.2.2)
|
tzinfo (1.2.3)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
tzinfo-data (1.2017.2)
|
tzinfo-data (1.2017.2)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
uglifier (3.0.1)
|
uglifier (3.2.0)
|
||||||
execjs (>= 0.3.0, < 3)
|
execjs (>= 0.3.0, < 3)
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.7.2)
|
unf_ext (0.0.7.2)
|
||||||
unicode-display_width (1.1.0)
|
unicode-display_width (1.1.3)
|
||||||
uniform_notifier (1.10.0)
|
uniform_notifier (1.10.0)
|
||||||
warden (1.2.6)
|
warden (1.2.7)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
webmock (2.1.0)
|
webmock (2.3.2)
|
||||||
addressable (>= 2.3.6)
|
addressable (>= 2.3.6)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff
|
hashdiff
|
||||||
websocket-driver (0.6.5)
|
websocket-driver (0.6.5)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.2)
|
websocket-extensions (0.1.2)
|
||||||
will_paginate (3.1.0)
|
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
@ -483,6 +488,7 @@ DEPENDENCIES
|
||||||
httplog
|
httplog
|
||||||
i18n-tasks (~> 0.9.6)
|
i18n-tasks (~> 0.9.6)
|
||||||
jquery-rails
|
jquery-rails
|
||||||
|
kaminari
|
||||||
letter_opener
|
letter_opener
|
||||||
letter_opener_web
|
letter_opener_web
|
||||||
link_header
|
link_header
|
||||||
|
@ -502,6 +508,7 @@ DEPENDENCIES
|
||||||
rack-cors
|
rack-cors
|
||||||
rack-timeout
|
rack-timeout
|
||||||
rails (~> 5.0.2)
|
rails (~> 5.0.2)
|
||||||
|
rails-controller-testing
|
||||||
rails-settings-cached
|
rails-settings-cached
|
||||||
rails_12factor
|
rails_12factor
|
||||||
react-rails
|
react-rails
|
||||||
|
@ -525,10 +532,9 @@ DEPENDENCIES
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
uglifier (>= 1.3.0)
|
uglifier (>= 1.3.0)
|
||||||
webmock
|
webmock
|
||||||
will_paginate
|
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 2.3.1p112
|
ruby 2.4.1p111
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
1.14.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
|
## Resources
|
||||||
|
|
||||||
- [List of Mastodon instances](docs/Using-Mastodon/List-of-Mastodon-instances.md)
|
- [List of Mastodon instances](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md)
|
||||||
- [Use this tool to find Twitter friends on Mastodon](https://mastodon-bridge.herokuapp.com)
|
- [Use this tool to find Twitter friends on Mastodon](https://mastodon-bridge.herokuapp.com)
|
||||||
- [API overview](docs/Using-the-API/API.md)
|
- [API overview](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md)
|
||||||
- [Frequently Asked Questions](docs/Using-Mastodon/FAQ.md)
|
- [Frequently Asked Questions](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md)
|
||||||
- [List of apps](docs/Using-Mastodon/Apps.md)
|
- [List of apps](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -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")
|
[![](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
|
docker-compose build
|
||||||
|
|
||||||
|
@ -117,25 +117,25 @@ Which will re-create the updated containers, leaving databases and data as is. D
|
||||||
|
|
||||||
## Deployment without Docker
|
## Deployment without Docker
|
||||||
|
|
||||||
Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](docs/Running-Mastodon/Production-guide.md) for examples, configuration and instructions.
|
Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Production-guide.md) for examples, configuration and instructions.
|
||||||
|
|
||||||
## Deployment on Scalingo
|
## 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)
|
[![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)
|
## Deployment on Heroku (experimental)
|
||||||
|
|
||||||
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)
|
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)
|
||||||
|
|
||||||
Mastodon can theoretically run indefinitely on a free [Heroku](https://heroku.com) app. [You can view a guide for deployment on Heroku here.](docs/Running-Mastodon/Heroku-guide.md)
|
Mastodon can 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
|
## Development with Vagrant
|
||||||
|
|
||||||
A quick way to get a development environment up and running is with Vagrant. You will need recent versions of [Vagrant](https://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) installed.
|
A quick way to get a development environment up and running is with Vagrant. You will need recent versions of [Vagrant](https://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) installed.
|
||||||
|
|
||||||
[You can find the guide for setting up a Vagrant development environment here.](docs/Running-Mastodon/Vagrant-guide.md)
|
[You can find the guide for setting up a Vagrant development environment here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Vagrant-guide.md)
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
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"
|
export PATH="$HOME/.rbenv/bin::$PATH"
|
||||||
eval "$(rbenv init -)"
|
eval "$(rbenv init -)"
|
||||||
|
|
||||||
echo "Compiling Ruby 2.3.1: warning, this takes a while!!!"
|
|
||||||
rbenv install 2.3.1
|
|
||||||
rbenv global 2.3.1
|
|
||||||
|
|
||||||
cd /vagrant
|
cd /vagrant
|
||||||
|
|
||||||
|
echo "Compiling Ruby $(cat .ruby-version): warning, this takes a while!!!"
|
||||||
|
rbenv install $(cat .ruby-version)
|
||||||
|
rbenv global $(cat .ruby-version)
|
||||||
|
|
||||||
# Configure database
|
# Configure database
|
||||||
sudo -u postgres createuser -U postgres vagrant -s
|
sudo -u postgres createuser -U postgres vagrant -s
|
||||||
sudo -u postgres createdb -U postgres mastodon_development
|
sudo -u postgres createdb -U postgres mastodon_development
|
||||||
|
|
12
app.json
12
app.json
|
@ -79,6 +79,18 @@
|
||||||
"SMTP_FROM_ADDRESS": {
|
"SMTP_FROM_ADDRESS": {
|
||||||
"description": "Address to send emails from",
|
"description": "Address to send emails from",
|
||||||
"required": false
|
"required": false
|
||||||
|
},
|
||||||
|
"SMTP_AUTH_METHOD": {
|
||||||
|
"description": "Authentication method to use with SMTP server. Default is 'plain'.",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"SMTP_OPENSSL_VERIFY_MODE": {
|
||||||
|
"description": "SMTP server certificate verification mode. Defaults is 'peer'.",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"SMTP_ENABLE_STARTTLS_AUTO": {
|
||||||
|
"description": "Enable STARTTLS if SMTP server supports it? Default is true.",
|
||||||
|
"required": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"buildpacks": [
|
"buildpacks": [
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 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() {
|
export function refreshNotifications() {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(refreshNotificationsRequest());
|
dispatch(refreshNotificationsRequest());
|
||||||
|
@ -61,6 +63,8 @@ export function refreshNotifications() {
|
||||||
params.since_id = ids.first().get('id');
|
params.since_id = ids.first().get('id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
params.exclude_types = excludeTypesFromSettings(getState());
|
||||||
|
|
||||||
api(getState).get('/api/v1/notifications', { params }).then(response => {
|
api(getState).get('/api/v1/notifications', { params }).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
|
@ -105,11 +109,11 @@ export function expandNotifications() {
|
||||||
|
|
||||||
dispatch(expandNotificationsRequest());
|
dispatch(expandNotificationsRequest());
|
||||||
|
|
||||||
api(getState).get(url, {
|
const params = {};
|
||||||
params: {
|
|
||||||
limit: 5
|
params.exclude_types = excludeTypesFromSettings(getState());
|
||||||
}
|
|
||||||
}).then(response => {
|
api(getState).get(url, params).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));
|
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import LinkHeader from 'http-link-header';
|
import LinkHeader from './link_header';
|
||||||
|
|
||||||
export const getLinks = response => {
|
export const getLinks = response => {
|
||||||
const value = response.headers.link;
|
const value = response.headers.link;
|
||||||
|
|
|
@ -65,7 +65,7 @@ const Account = React.createClass({
|
||||||
<div className='account'>
|
<div className='account'>
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
|
<Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
|
||||||
<div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={36} /></div>
|
<div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={36} /></div>
|
||||||
<DisplayName account={account} />
|
<DisplayName account={account} />
|
||||||
</Permalink>
|
</Permalink>
|
||||||
|
|
||||||
|
|
|
@ -1,103 +1,18 @@
|
||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
|
||||||
// From: http://stackoverflow.com/a/18320662
|
|
||||||
const resample = (canvas, width, height, resize_canvas) => {
|
|
||||||
let width_source = canvas.width;
|
|
||||||
let height_source = canvas.height;
|
|
||||||
width = Math.round(width);
|
|
||||||
height = Math.round(height);
|
|
||||||
|
|
||||||
let ratio_w = width_source / width;
|
|
||||||
let ratio_h = height_source / height;
|
|
||||||
let ratio_w_half = Math.ceil(ratio_w / 2);
|
|
||||||
let ratio_h_half = Math.ceil(ratio_h / 2);
|
|
||||||
|
|
||||||
let ctx = canvas.getContext("2d");
|
|
||||||
let img = ctx.getImageData(0, 0, width_source, height_source);
|
|
||||||
let img2 = ctx.createImageData(width, height);
|
|
||||||
let data = img.data;
|
|
||||||
let data2 = img2.data;
|
|
||||||
|
|
||||||
for (let j = 0; j < height; j++) {
|
|
||||||
for (let i = 0; i < width; i++) {
|
|
||||||
let x2 = (i + j * width) * 4;
|
|
||||||
let weight = 0;
|
|
||||||
let weights = 0;
|
|
||||||
let weights_alpha = 0;
|
|
||||||
let gx_r = 0;
|
|
||||||
let gx_g = 0;
|
|
||||||
let gx_b = 0;
|
|
||||||
let gx_a = 0;
|
|
||||||
let center_y = (j + 0.5) * ratio_h;
|
|
||||||
let yy_start = Math.floor(j * ratio_h);
|
|
||||||
let yy_stop = Math.ceil((j + 1) * ratio_h);
|
|
||||||
|
|
||||||
for (let yy = yy_start; yy < yy_stop; yy++) {
|
|
||||||
let dy = Math.abs(center_y - (yy + 0.5)) / ratio_h_half;
|
|
||||||
let center_x = (i + 0.5) * ratio_w;
|
|
||||||
let w0 = dy * dy; //pre-calc part of w
|
|
||||||
let xx_start = Math.floor(i * ratio_w);
|
|
||||||
let xx_stop = Math.ceil((i + 1) * ratio_w);
|
|
||||||
|
|
||||||
for (let xx = xx_start; xx < xx_stop; xx++) {
|
|
||||||
let dx = Math.abs(center_x - (xx + 0.5)) / ratio_w_half;
|
|
||||||
let w = Math.sqrt(w0 + dx * dx);
|
|
||||||
|
|
||||||
if (w >= 1) {
|
|
||||||
// pixel too far
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// hermite filter
|
|
||||||
weight = 2 * w * w * w - 3 * w * w + 1;
|
|
||||||
let pos_x = 4 * (xx + yy * width_source);
|
|
||||||
|
|
||||||
// alpha
|
|
||||||
gx_a += weight * data[pos_x + 3];
|
|
||||||
weights_alpha += weight;
|
|
||||||
|
|
||||||
// colors
|
|
||||||
if (data[pos_x + 3] < 255)
|
|
||||||
weight = weight * data[pos_x + 3] / 250;
|
|
||||||
|
|
||||||
gx_r += weight * data[pos_x];
|
|
||||||
gx_g += weight * data[pos_x + 1];
|
|
||||||
gx_b += weight * data[pos_x + 2];
|
|
||||||
weights += weight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data2[x2] = gx_r / weights;
|
|
||||||
data2[x2 + 1] = gx_g / weights;
|
|
||||||
data2[x2 + 2] = gx_b / weights;
|
|
||||||
data2[x2 + 3] = gx_a / weights_alpha;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear and resize canvas
|
|
||||||
if (resize_canvas === true) {
|
|
||||||
canvas.width = width;
|
|
||||||
canvas.height = height;
|
|
||||||
} else {
|
|
||||||
ctx.clearRect(0, 0, width_source, height_source);
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw
|
|
||||||
ctx.putImageData(img2, 0, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Avatar = React.createClass({
|
const Avatar = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
src: React.PropTypes.string.isRequired,
|
src: React.PropTypes.string.isRequired,
|
||||||
|
staticSrc: React.PropTypes.string,
|
||||||
size: React.PropTypes.number.isRequired,
|
size: React.PropTypes.number.isRequired,
|
||||||
style: React.PropTypes.object,
|
style: React.PropTypes.object,
|
||||||
animated: React.PropTypes.bool
|
animate: React.PropTypes.bool
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps () {
|
getDefaultProps () {
|
||||||
return {
|
return {
|
||||||
animated: true
|
animate: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -117,38 +32,30 @@ const Avatar = React.createClass({
|
||||||
this.setState({ hovering: false });
|
this.setState({ hovering: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
handleLoad () {
|
|
||||||
this.canvas.width = this.image.naturalWidth;
|
|
||||||
this.canvas.height = this.image.naturalHeight;
|
|
||||||
this.canvas.getContext('2d').drawImage(this.image, 0, 0);
|
|
||||||
|
|
||||||
resample(this.canvas, this.props.size * window.devicePixelRatio, this.props.size * window.devicePixelRatio, true);
|
|
||||||
},
|
|
||||||
|
|
||||||
setImageRef (c) {
|
|
||||||
this.image = c;
|
|
||||||
},
|
|
||||||
|
|
||||||
setCanvasRef (c) {
|
|
||||||
this.canvas = c;
|
|
||||||
},
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
const { src, size, staticSrc, animate } = this.props;
|
||||||
const { hovering } = this.state;
|
const { hovering } = this.state;
|
||||||
|
|
||||||
if (this.props.animated) {
|
const style = {
|
||||||
return (
|
...this.props.style,
|
||||||
<div style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px` }}>
|
width: `${size}px`,
|
||||||
<img src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ borderRadius: '4px' }} />
|
height: `${size}px`,
|
||||||
</div>
|
backgroundSize: `${size}px ${size}px`
|
||||||
);
|
};
|
||||||
|
|
||||||
|
if (hovering || animate) {
|
||||||
|
style.backgroundImage = `url(${src})`;
|
||||||
|
} else {
|
||||||
|
style.backgroundImage = `url(${staticSrc})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px`, position: 'relative' }}>
|
<div
|
||||||
<img ref={this.setImageRef} onLoad={this.handleLoad} src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ position: 'absolute', top: '0', left: '0', opacity: hovering ? '1' : '0', borderRadius: '4px' }} />
|
className='avatar'
|
||||||
<canvas ref={this.setCanvasRef} style={{ borderRadius: '4px', width: this.props.size, height: this.props.size, opacity: hovering ? '0' : '1' }} />
|
onMouseEnter={this.handleMouseEnter}
|
||||||
</div>
|
onMouseLeave={this.handleMouseLeave}
|
||||||
|
style={style}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ const IconButton = React.createClass({
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!this.props.disabled) {
|
if (!this.props.disabled) {
|
||||||
this.props.onClick();
|
this.props.onClick(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ const Status = React.createClass({
|
||||||
onOpenMedia: React.PropTypes.func,
|
onOpenMedia: React.PropTypes.func,
|
||||||
onBlock: React.PropTypes.func,
|
onBlock: React.PropTypes.func,
|
||||||
me: React.PropTypes.number,
|
me: React.PropTypes.number,
|
||||||
|
boostModal: React.PropTypes.bool,
|
||||||
muted: React.PropTypes.bool
|
muted: React.PropTypes.bool
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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' }}>
|
<a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px' }}>
|
||||||
<div className='status__avatar' style={{ position: 'absolute', left: '10px', top: '10px', width: '48px', height: '48px' }}>
|
<div className='status__avatar' style={{ position: 'absolute', left: '10px', top: '10px', width: '48px', height: '48px' }}>
|
||||||
<Avatar src={status.getIn(['account', 'avatar'])} size={48} />
|
<Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DisplayName account={status.get('account')} />
|
<DisplayName account={status.get('account')} />
|
||||||
|
|
|
@ -46,8 +46,8 @@ const StatusActionBar = React.createClass({
|
||||||
this.props.onFavourite(this.props.status);
|
this.props.onFavourite(this.props.status);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleReblogClick () {
|
handleReblogClick (e) {
|
||||||
this.props.onReblog(this.props.status);
|
this.props.onReblog(this.props.status, e);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDeleteClick () {
|
handleDeleteClick () {
|
||||||
|
|
|
@ -36,6 +36,7 @@ const StatusContent = React.createClass({
|
||||||
|
|
||||||
if (mention) {
|
if (mention) {
|
||||||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
||||||
|
link.setAttribute('title', mention.get('acct'));
|
||||||
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
||||||
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
||||||
} else if (media) {
|
} else if (media) {
|
||||||
|
@ -91,7 +92,7 @@ const StatusContent = React.createClass({
|
||||||
const { status } = this.props;
|
const { status } = this.props;
|
||||||
const { hidden } = this.state;
|
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 spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) };
|
||||||
const directionStyle = { direction: 'ltr' };
|
const directionStyle = { direction: 'ltr' };
|
||||||
|
|
||||||
|
@ -125,7 +126,7 @@ const StatusContent = React.createClass({
|
||||||
<div style={{ display: hidden ? 'none' : 'block', ...directionStyle }} dangerouslySetInnerHTML={content} />
|
<div style={{ display: hidden ? 'none' : 'block', ...directionStyle }} dangerouslySetInnerHTML={content} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else if (this.props.onClick) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='status__content'
|
className='status__content'
|
||||||
|
@ -135,6 +136,14 @@ const StatusContent = React.createClass({
|
||||||
dangerouslySetInnerHTML={content}
|
dangerouslySetInnerHTML={content}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='status__content'
|
||||||
|
style={{ ...directionStyle }}
|
||||||
|
dangerouslySetInnerHTML={content}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,9 @@ import hu from 'react-intl/locale-data/hu';
|
||||||
import uk from 'react-intl/locale-data/uk';
|
import uk from 'react-intl/locale-data/uk';
|
||||||
import fi from 'react-intl/locale-data/fi';
|
import fi from 'react-intl/locale-data/fi';
|
||||||
import eo from 'react-intl/locale-data/eo';
|
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 getMessagesForLocale from '../locales';
|
||||||
import { hydrateStore } from '../actions/store';
|
import { hydrateStore } from '../actions/store';
|
||||||
import createStream from '../stream';
|
import createStream from '../stream';
|
||||||
|
@ -60,7 +63,9 @@ const browserHistory = useRouterHistory(createBrowserHistory)({
|
||||||
basename: '/web'
|
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({
|
const Mastodon = React.createClass({
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,8 @@ const makeMapStateToProps = () => {
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
status: getStatus(state, props.id),
|
status: getStatus(state, props.id),
|
||||||
me: state.getIn(['meta', 'me'])
|
me: state.getIn(['meta', 'me']),
|
||||||
|
boostModal: state.getIn(['meta', 'boost_modal'])
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
|
@ -38,11 +39,19 @@ const mapDispatchToProps = (dispatch) => ({
|
||||||
dispatch(replyCompose(status, router));
|
dispatch(replyCompose(status, router));
|
||||||
},
|
},
|
||||||
|
|
||||||
onReblog (status) {
|
onModalReblog (status) {
|
||||||
|
dispatch(reblog(status));
|
||||||
|
},
|
||||||
|
|
||||||
|
onReblog (status, e) {
|
||||||
if (status.get('reblogged')) {
|
if (status.get('reblogged')) {
|
||||||
dispatch(unreblog(status));
|
dispatch(unreblog(status));
|
||||||
} else {
|
} else {
|
||||||
dispatch(reblog(status));
|
if (e.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 }) => (
|
const AutosuggestAccount = ({ account }) => (
|
||||||
<div style={{ overflow: 'hidden' }} className='autosuggest-account'>
|
<div style={{ overflow: 'hidden' }} className='autosuggest-account'>
|
||||||
<div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} size={18} /></div>
|
<div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={18} /></div>
|
||||||
<DisplayName account={account} />
|
<DisplayName account={account} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -83,11 +83,23 @@ const ComposeForm = React.createClass({
|
||||||
this.props.onChangeSpoilerText(e.target.value);
|
this.props.onChangeSpoilerText(e.target.value);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
// If this is the update where we've finished uploading,
|
||||||
|
// save the last caret position so we can restore it below!
|
||||||
|
if (!nextProps.is_uploading && this.props.is_uploading) {
|
||||||
|
this._restoreCaret = this.autosuggestTextarea.textarea.selectionStart;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
if (this.props.focusDate !== prevProps.focusDate) {
|
// This statement does several things:
|
||||||
// If replying to zero or one users, places the cursor at the end of the textbox.
|
// - If we're beginning a reply, and,
|
||||||
// If replying to more than one user, selects any usernames past the first;
|
// - Replying to zero or one users, places the cursor at the end of the textbox.
|
||||||
// this provides a convenient shortcut to drop everyone else from the conversation.
|
// - Replying to more than one user, selects any usernames past the first;
|
||||||
|
// this provides a convenient shortcut to drop everyone else from the conversation.
|
||||||
|
// - If we've just finished uploading an image, and have a saved caret position,
|
||||||
|
// restores the cursor to that position after the text changes!
|
||||||
|
if (this.props.focusDate !== prevProps.focusDate || (prevProps.is_uploading && !this.props.is_uploading && typeof this._restoreCaret === 'number')) {
|
||||||
let selectionEnd, selectionStart;
|
let selectionEnd, selectionStart;
|
||||||
|
|
||||||
if (this.props.preselectDate !== prevProps.preselectDate) {
|
if (this.props.preselectDate !== prevProps.preselectDate) {
|
||||||
|
@ -118,7 +130,7 @@ const ComposeForm = React.createClass({
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props;
|
const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props;
|
||||||
const disabled = this.props.is_submitting || this.props.is_uploading;
|
const disabled = this.props.is_submitting;
|
||||||
|
|
||||||
let publishText = '';
|
let publishText = '';
|
||||||
let privacyWarning = '';
|
let privacyWarning = '';
|
||||||
|
|
|
@ -46,8 +46,8 @@ const EmojiPickerDropdown = React.createClass({
|
||||||
<img draggable="false" className="emojione" alt="🙂" src="/emoji/1f602.svg" />
|
<img draggable="false" className="emojione" alt="🙂" src="/emoji/1f602.svg" />
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
|
|
||||||
<DropdownContent className='dropdown__left'>
|
<DropdownContent className='dropdown__left light'>
|
||||||
<EmojiPicker emojione={settings} onChange={this.handleChange} />
|
<EmojiPicker emojione={settings} onChange={this.handleChange} search={true} />
|
||||||
</DropdownContent>
|
</DropdownContent>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,7 +17,7 @@ const NavigationBar = React.createClass({
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div className='navigation-bar'>
|
<div className='navigation-bar'>
|
||||||
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} size={40} /></Permalink>
|
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} animate size={40} /></Permalink>
|
||||||
|
|
||||||
<div style={{ flex: '1 1 auto', marginLeft: '8px' }}>
|
<div style={{ flex: '1 1 auto', marginLeft: '8px' }}>
|
||||||
<strong style={{ fontWeight: '500', display: 'block' }}>{this.props.account.get('acct')}</strong>
|
<strong style={{ fontWeight: '500', display: 'block' }}>{this.props.account.get('acct')}</strong>
|
||||||
|
|
|
@ -50,7 +50,7 @@ const ReplyIndicator = React.createClass({
|
||||||
<div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div>
|
<div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div>
|
||||||
|
|
||||||
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}>
|
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}>
|
||||||
<div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} /></div>
|
<div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} /></div>
|
||||||
<DisplayName account={status.get('account')} />
|
<DisplayName account={status.get('account')} />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -33,7 +33,7 @@ const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
|
||||||
<div>
|
<div>
|
||||||
<div style={outerStyle}>
|
<div style={outerStyle}>
|
||||||
<Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
|
<Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
|
||||||
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={48} /></div>
|
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={48} /></div>
|
||||||
<DisplayName account={account} />
|
<DisplayName account={account} />
|
||||||
</Permalink>
|
</Permalink>
|
||||||
|
|
||||||
|
|
|
@ -4,16 +4,6 @@ const messages = defineMessages({
|
||||||
clear: { id: 'notifications.clear', defaultMessage: 'Clear notifications' }
|
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({
|
const ClearColumnButton = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
@ -25,7 +15,7 @@ const ClearColumnButton = React.createClass({
|
||||||
const { intl } = this.props;
|
const { intl } = this.props;
|
||||||
|
|
||||||
return (
|
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' />
|
<i className='fa fa-eraser' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,7 +21,7 @@ const Notification = React.createClass({
|
||||||
|
|
||||||
renderFollow (account, link) {
|
renderFollow (account, link) {
|
||||||
return (
|
return (
|
||||||
<div className='notification'>
|
<div className='notification notification-follow'>
|
||||||
<div className='notification__message'>
|
<div className='notification__message'>
|
||||||
<div style={{ position: 'absolute', 'left': '-26px'}}>
|
<div style={{ position: 'absolute', 'left': '-26px'}}>
|
||||||
<i className='fa fa-fw fa-user-plus' />
|
<i className='fa fa-fw fa-user-plus' />
|
||||||
|
@ -41,7 +41,7 @@ const Notification = React.createClass({
|
||||||
|
|
||||||
renderFavourite (notification, link) {
|
renderFavourite (notification, link) {
|
||||||
return (
|
return (
|
||||||
<div className='notification'>
|
<div className='notification notification-favourite'>
|
||||||
<div className='notification__message'>
|
<div className='notification__message'>
|
||||||
<div style={{ position: 'absolute', 'left': '-26px'}}>
|
<div style={{ position: 'absolute', 'left': '-26px'}}>
|
||||||
<i className='fa fa-fw fa-star' style={{ color: '#ca8f04' }} />
|
<i className='fa fa-fw fa-star' style={{ color: '#ca8f04' }} />
|
||||||
|
@ -57,7 +57,7 @@ const Notification = React.createClass({
|
||||||
|
|
||||||
renderReblog (notification, link) {
|
renderReblog (notification, link) {
|
||||||
return (
|
return (
|
||||||
<div className='notification'>
|
<div className='notification notification-reblog'>
|
||||||
<div className='notification__message'>
|
<div className='notification__message'>
|
||||||
<div style={{ position: 'absolute', 'left': '-26px'}}>
|
<div style={{ position: 'absolute', 'left': '-26px'}}>
|
||||||
<i className='fa fa-fw fa-retweet' />
|
<i className='fa fa-fw fa-retweet' />
|
||||||
|
@ -76,17 +76,17 @@ const Notification = React.createClass({
|
||||||
const account = notification.get('account');
|
const account = notification.get('account');
|
||||||
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
|
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
|
||||||
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
||||||
const link = <Permalink className='notification__display-name' style={linkStyle} href={account.get('url')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />;
|
const link = <Permalink className='notification__display-name' style={linkStyle} href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />;
|
||||||
|
|
||||||
switch(notification.get('type')) {
|
switch(notification.get('type')) {
|
||||||
case 'follow':
|
case 'follow':
|
||||||
return this.renderFollow(account, link);
|
return this.renderFollow(account, link);
|
||||||
case 'mention':
|
case 'mention':
|
||||||
return this.renderMention(notification);
|
return this.renderMention(notification);
|
||||||
case 'favourite':
|
case 'favourite':
|
||||||
return this.renderFavourite(notification, link);
|
return this.renderFavourite(notification, link);
|
||||||
case 'reblog':
|
case 'reblog':
|
||||||
return this.renderReblog(notification, link);
|
return this.renderReblog(notification, link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,8 +37,8 @@ const ActionBar = React.createClass({
|
||||||
this.props.onReply(this.props.status);
|
this.props.onReply(this.props.status);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleReblogClick () {
|
handleReblogClick (e) {
|
||||||
this.props.onReblog(this.props.status);
|
this.props.onReblog(this.props.status, e);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleFavouriteClick () {
|
handleFavouriteClick () {
|
||||||
|
|
|
@ -54,7 +54,7 @@ const DetailedStatus = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '14px 10px' }} className='detailed-status'>
|
<div style={{ padding: '14px 10px' }} className='detailed-status'>
|
||||||
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
|
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
|
||||||
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} size={48} /></div>
|
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /></div>
|
||||||
<DisplayName account={status.get('account')} />
|
<DisplayName account={status.get('account')} />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,8 @@ const makeMapStateToProps = () => {
|
||||||
status: getStatus(state, Number(props.params.statusId)),
|
status: getStatus(state, Number(props.params.statusId)),
|
||||||
ancestorsIds: state.getIn(['timelines', 'ancestors', Number(props.params.statusId)]),
|
ancestorsIds: state.getIn(['timelines', 'ancestors', Number(props.params.statusId)]),
|
||||||
descendantsIds: state.getIn(['timelines', 'descendants', Number(props.params.statusId)]),
|
descendantsIds: state.getIn(['timelines', 'descendants', Number(props.params.statusId)]),
|
||||||
me: state.getIn(['meta', 'me'])
|
me: state.getIn(['meta', 'me']),
|
||||||
|
boostModal: state.getIn(['meta', 'boost_modal'])
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
|
@ -55,7 +56,8 @@ const Status = React.createClass({
|
||||||
status: ImmutablePropTypes.map,
|
status: ImmutablePropTypes.map,
|
||||||
ancestorsIds: ImmutablePropTypes.list,
|
ancestorsIds: ImmutablePropTypes.list,
|
||||||
descendantsIds: ImmutablePropTypes.list,
|
descendantsIds: ImmutablePropTypes.list,
|
||||||
me: React.PropTypes.number
|
me: React.PropTypes.number,
|
||||||
|
boostModal: React.PropTypes.bool
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
@ -82,11 +84,19 @@ const Status = React.createClass({
|
||||||
this.props.dispatch(replyCompose(status, this.context.router));
|
this.props.dispatch(replyCompose(status, this.context.router));
|
||||||
},
|
},
|
||||||
|
|
||||||
handleReblogClick (status) {
|
handleModalReblog (status) {
|
||||||
|
this.props.dispatch(reblog(status));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleReblogClick (status, e) {
|
||||||
if (status.get('reblogged')) {
|
if (status.get('reblogged')) {
|
||||||
this.props.dispatch(unreblog(status));
|
this.props.dispatch(unreblog(status));
|
||||||
} else {
|
} else {
|
||||||
this.props.dispatch(reblog(status));
|
if (e.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],
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
handleHeaderClick () {
|
handleHeaderClick () {
|
||||||
let node = ReactDOM.findDOMNode(this);
|
const scrollable = ReactDOM.findDOMNode(this).querySelector('.scrollable');
|
||||||
this._interruptScrollAnimation = scrollTop(node.querySelector('.scrollable'));
|
if (!scrollable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._interruptScrollAnimation = scrollTop(scrollable);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleWheel () {
|
handleWheel () {
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
import MediaModal from './media_modal';
|
import MediaModal from './media_modal';
|
||||||
|
import BoostModal from './boost_modal';
|
||||||
import { TransitionMotion, spring } from 'react-motion';
|
import { TransitionMotion, spring } from 'react-motion';
|
||||||
|
|
||||||
const MODAL_COMPONENTS = {
|
const MODAL_COMPONENTS = {
|
||||||
'MEDIA': MediaModal
|
'MEDIA': MediaModal,
|
||||||
|
'BOOST': BoostModal
|
||||||
};
|
};
|
||||||
|
|
||||||
const ModalRoot = React.createClass({
|
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.sensitive_toggle": "Cliquer pour dévoiler",
|
||||||
"status.show_more": "Déplier",
|
"status.show_more": "Déplier",
|
||||||
"status.show_less": "Replier",
|
"status.show_less": "Replier",
|
||||||
"status.open": "Déplier ce status",
|
"status.open": "Déplier ce statut",
|
||||||
"status.report": "Signaler @{name}",
|
"status.report": "Signaler @{name}",
|
||||||
"status.load_more": "Charger plus",
|
"status.load_more": "Charger plus",
|
||||||
|
"status.media_hidden": "Média caché",
|
||||||
"video_player.toggle_sound": "Mettre/Couper le son",
|
"video_player.toggle_sound": "Mettre/Couper le son",
|
||||||
|
"video_player.toggle_visible": "Afficher/Cacher la vidéo",
|
||||||
"account.mention": "Mentionner",
|
"account.mention": "Mentionner",
|
||||||
"account.edit_profile": "Modifier le profil",
|
"account.edit_profile": "Modifier le profil",
|
||||||
"account.unblock": "Débloquer",
|
"account.unblock": "Débloquer",
|
||||||
|
@ -42,16 +44,25 @@ const fr = {
|
||||||
"column.notifications": "Notifications",
|
"column.notifications": "Notifications",
|
||||||
"column.blocks": "Utilisateurs bloqués",
|
"column.blocks": "Utilisateurs bloqués",
|
||||||
"column.favourites": "Favoris",
|
"column.favourites": "Favoris",
|
||||||
|
"column.follow_requests": "Demandes de suivi",
|
||||||
"empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateurs⋅trices pour débuter la conversation.",
|
"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.compose": "Composer",
|
||||||
"tabs_bar.home": "Accueil",
|
"tabs_bar.home": "Accueil",
|
||||||
"tabs_bar.mentions": "Mentions",
|
"tabs_bar.mentions": "Mentions",
|
||||||
"tabs_bar.public": "Fil public global",
|
"tabs_bar.public": "Fil public global",
|
||||||
"tabs_bar.notifications": "Notifications",
|
"tabs_bar.notifications": "Notifications",
|
||||||
|
"tabs_bar.local_timeline": "Fil public local",
|
||||||
|
"tabs_bar.federated_timeline": "Fil public global",
|
||||||
"compose_form.placeholder": "Qu’avez-vous en tête ?",
|
"compose_form.placeholder": "Qu’avez-vous en tête ?",
|
||||||
"compose_form.publish": "Pouet",
|
"compose_form.publish": "Pouet",
|
||||||
"compose_form.sensitive": "Marquer le média comme délicat",
|
"compose_form.sensitive": "Marquer le média comme délicat",
|
||||||
"compose_form.spoiler": "Masquer le texte derrière un avertissement",
|
"compose_form.spoiler": "Masquer le texte derrière un avertissement",
|
||||||
|
"compose_form.spoiler_placeholder": "Avertissement",
|
||||||
"compose_form.private": "Rendre privé",
|
"compose_form.private": "Rendre privé",
|
||||||
"compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodons. Si {domains} {domainsCount, plural, one {n'est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n'y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d'une autre manière à d'autres personnes imprévues",
|
"compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodons. Si {domains} {domainsCount, plural, one {n'est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n'y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d'une autre manière à d'autres personnes imprévues",
|
||||||
"compose_form.unlisted": "Ne pas afficher dans les fils publics",
|
"compose_form.unlisted": "Ne pas afficher dans les fils publics",
|
||||||
|
@ -64,23 +75,31 @@ const fr = {
|
||||||
"navigation_bar.favourites": "Favoris",
|
"navigation_bar.favourites": "Favoris",
|
||||||
"navigation_bar.info": "Plus d'informations",
|
"navigation_bar.info": "Plus d'informations",
|
||||||
"navigation_bar.logout": "Déconnexion",
|
"navigation_bar.logout": "Déconnexion",
|
||||||
|
"navigation_bar.follow_requests": "Demandes de suivi",
|
||||||
"reply_indicator.cancel": "Annuler",
|
"reply_indicator.cancel": "Annuler",
|
||||||
"search.placeholder": "Chercher",
|
"search.placeholder": "Rechercher",
|
||||||
"search.account": "Compte",
|
"search.account": "Compte",
|
||||||
"search.hashtag": "Mot-clé",
|
"search.hashtag": "Mot-clé",
|
||||||
"search_results.total": "{count} {count, plural, one {résultat} other {résultats}}",
|
"search_results.total": "{count} {count, plural, one {résultat} other {résultats}}",
|
||||||
|
"search.status_by": "Statuts de {name}",
|
||||||
"upload_button.label": "Joindre un média",
|
"upload_button.label": "Joindre un média",
|
||||||
"upload_form.undo": "Annuler",
|
"upload_form.undo": "Annuler",
|
||||||
|
"upload_progress.label": "Envoi en cours…",
|
||||||
|
"upload_area.title": "Glissez et déposez pour envoyer",
|
||||||
"notification.follow": "{name} vous suit.",
|
"notification.follow": "{name} vous suit.",
|
||||||
"notification.favourite": "{name} a ajouté à ses favoris :",
|
"notification.favourite": "{name} a ajouté à ses favoris :",
|
||||||
"notification.reblog": "{name} a partagé votre statut :",
|
"notification.reblog": "{name} a partagé votre statut :",
|
||||||
"notification.mention": "{name} vous a mentionné⋅e :",
|
"notification.mention": "{name} vous a mentionné⋅e :",
|
||||||
"notifications.column_settings.alert": "Notifications locales",
|
"notifications.column_settings.alert": "Notifications locales",
|
||||||
"notifications.column_settings.show": "Afficher dans la colonne",
|
"notifications.column_settings.show": "Afficher dans la colonne",
|
||||||
|
"notifications.column_settings.sound": "Émettre un son",
|
||||||
"notifications.column_settings.follow": "Nouveaux abonnés :",
|
"notifications.column_settings.follow": "Nouveaux abonnés :",
|
||||||
"notifications.column_settings.favourite": "Favoris :",
|
"notifications.column_settings.favourite": "Favoris :",
|
||||||
"notifications.column_settings.mention": "Mentions :",
|
"notifications.column_settings.mention": "Mentions :",
|
||||||
"notifications.column_settings.reblog": "Partages :",
|
"notifications.column_settings.reblog": "Partages :",
|
||||||
|
"notifications.clear": "Nettoyer",
|
||||||
|
"notifications.clear_confirmation": "Voulez-vous vraiment nettoyer toutes vos notifications ?",
|
||||||
|
"notifications.settings": "Paramètres de la colonne",
|
||||||
"privacy.public.short": "Public",
|
"privacy.public.short": "Public",
|
||||||
"privacy.public.long": "Afficher dans les fils publics",
|
"privacy.public.long": "Afficher dans les fils publics",
|
||||||
"privacy.unlisted.short": "Non-listé",
|
"privacy.unlisted.short": "Non-listé",
|
||||||
|
@ -90,6 +109,20 @@ const fr = {
|
||||||
"privacy.direct.short": "Direct",
|
"privacy.direct.short": "Direct",
|
||||||
"privacy.direct.long": "N’afficher que pour les personnes mentionné⋅e⋅s",
|
"privacy.direct.long": "N’afficher que pour les personnes mentionné⋅e⋅s",
|
||||||
"privacy.change": "Ajuster la confidentialité du message",
|
"privacy.change": "Ajuster la confidentialité du message",
|
||||||
|
"media_gallery.toggle_visible": "Modifier la visibilité",
|
||||||
|
"missing_indicator.label": "Non trouvé",
|
||||||
|
"follow_request.authorize": "Autoriser",
|
||||||
|
"follow_request.reject": "Rejeter",
|
||||||
|
"home.settings": "Paramètres de la colonne",
|
||||||
|
"home.column_settings.basic": "Basique",
|
||||||
|
"home.column_settings.show_reblogs": "Afficher les partages",
|
||||||
|
"home.column_settings.show_replies": "Afficher les réponses",
|
||||||
|
"home.column_settings.advanced": "Avancé",
|
||||||
|
"home.column_settings.filter_regex": "Filtrer avec une expression rationnelle",
|
||||||
|
"report.heading": "Nouveau signalement",
|
||||||
|
"report.placeholder": "Commentaires additionnels",
|
||||||
|
"report.submit": "Envoyer",
|
||||||
|
"report.target": "Signalement"
|
||||||
};
|
};
|
||||||
|
|
||||||
export default fr;
|
export default fr;
|
||||||
|
|
|
@ -7,6 +7,9 @@ import pt from './pt';
|
||||||
import uk from './uk';
|
import uk from './uk';
|
||||||
import fi from './fi';
|
import fi from './fi';
|
||||||
import eo from './eo';
|
import eo from './eo';
|
||||||
|
import ru from './ru';
|
||||||
|
import ja from './ja';
|
||||||
|
|
||||||
|
|
||||||
const locales = {
|
const locales = {
|
||||||
en,
|
en,
|
||||||
|
@ -17,7 +20,10 @@ const locales = {
|
||||||
pt,
|
pt,
|
||||||
uk,
|
uk,
|
||||||
fi,
|
fi,
|
||||||
eo
|
eo,
|
||||||
|
ru,
|
||||||
|
ja
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function getMessagesForLocale (locale) {
|
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;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.details-counters {
|
.details-counters {
|
||||||
|
@ -83,7 +84,7 @@
|
||||||
.counter {
|
.counter {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
color: $color3;
|
color: $color3;
|
||||||
padding: 0 10px;
|
padding: 5px 10px 0px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
border-right: 1px solid $color3;
|
border-right: 1px solid $color3;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
@ -148,7 +149,7 @@
|
||||||
order: 1;
|
order: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 360px) {
|
@media screen and (max-width: 480px) {
|
||||||
.details {
|
.details {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
@ -173,7 +174,7 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
a, .current, .next_page, .previous_page, .gap {
|
a, .current, .page, .gap {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: $color5;
|
color: $color5;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
@ -193,12 +194,12 @@
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.previous_page, .next_page {
|
.prev, .next {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: $color2;
|
color: $color2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.previous_page {
|
.prev {
|
||||||
float: left;
|
float: left;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
|
|
||||||
|
@ -208,7 +209,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.next_page {
|
.next {
|
||||||
float: right;
|
float: right;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
|
|
||||||
|
@ -226,11 +227,11 @@
|
||||||
@media screen and (max-width: 360px) {
|
@media screen and (max-width: 360px) {
|
||||||
padding: 30px 20px;
|
padding: 30px 20px;
|
||||||
|
|
||||||
a, .current, .next_page, .previous_page, .gap {
|
a, .current, .next, .prev, .gap {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.next_page, .previous_page {
|
.next, .prev {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,7 +1,7 @@
|
||||||
@import 'variables';
|
@import 'variables';
|
||||||
|
|
||||||
.app-body{
|
.app-body{
|
||||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.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 {
|
.icon-button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0;
|
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 {
|
.lightbox .icon-button {
|
||||||
color: $color1;
|
color: $color1;
|
||||||
}
|
}
|
||||||
|
@ -325,6 +349,43 @@ a.status__content__spoiler-link {
|
||||||
.status__display-name {
|
.status__display-name {
|
||||||
color: lighten($color1, 26%);
|
color: lighten($color1, 26%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.light {
|
||||||
|
.status__relative-time {
|
||||||
|
color: $color3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status__display-name {
|
||||||
|
color: $color1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-name {
|
||||||
|
strong {
|
||||||
|
color: $color1;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: $color3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status__content {
|
||||||
|
color: $color1;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $color4;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.status__content__spoiler-link {
|
||||||
|
color: $color5;
|
||||||
|
background: $color3;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: lighten($color3, 8%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-check-box {
|
.status-check-box {
|
||||||
|
@ -643,6 +704,12 @@ a.status__content__spoiler-link {
|
||||||
left: 8px;
|
left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.light {
|
||||||
|
&:before {
|
||||||
|
border-color: transparent transparent $color5 transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& > ul {
|
& > ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
background: $color2;
|
background: $color2;
|
||||||
|
@ -660,7 +727,7 @@ a.status__content__spoiler-link {
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .emoji-dialog {
|
& > .emoji-dialog {
|
||||||
left: -249px;
|
left: -210px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -714,15 +781,7 @@ a.status__content__spoiler-link {
|
||||||
|
|
||||||
@media screen and (min-width: 360px) {
|
@media screen and (min-width: 360px) {
|
||||||
.columns-area {
|
.columns-area {
|
||||||
margin: 0;
|
padding: 10px;
|
||||||
}
|
|
||||||
|
|
||||||
.column:first-child, .drawer:first-child {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column:last-child, .drawer:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -730,9 +789,12 @@ a.status__content__spoiler-link {
|
||||||
width: 330px;
|
width: 330px;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: $color1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
> .scrollable {
|
||||||
|
background: $color1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui {
|
.ui {
|
||||||
|
@ -764,6 +826,58 @@ a.status__content__spoiler-link {
|
||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.column, .drawer {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 360px) {
|
||||||
|
.tabs-bar {
|
||||||
|
margin: 10px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1024px) {
|
||||||
|
.column, .drawer {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columns-area {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search__input, .autosuggest-textarea__textarea {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 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) {
|
@media screen and (min-width: 2560px) {
|
||||||
.columns-area {
|
.columns-area {
|
||||||
justify-content: center;
|
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 {
|
.tabs-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: lighten($color1, 8%);
|
background: lighten($color1, 8%);
|
||||||
|
@ -865,17 +947,18 @@ a.status__content__spoiler-link {
|
||||||
.tabs-bar__link {
|
.tabs-bar__link {
|
||||||
display: block;
|
display: block;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
padding: 10px 5px;
|
padding: 15px 10px;
|
||||||
color: $color5;
|
color: $color5;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size:12px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border-bottom: 2px solid lighten($color1, 8%);
|
border-bottom: 2px solid lighten($color1, 8%);
|
||||||
transition: all 200ms linear;
|
transition: all 200ms linear;
|
||||||
|
|
||||||
.fa {
|
.fa {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
|
@ -889,37 +972,13 @@ a.status__content__spoiler-link {
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
|
margin-left: 5px;
|
||||||
display: none;
|
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) {
|
@media screen and (min-width: 600px) {
|
||||||
.tabs-bar__link {
|
.tabs-bar__link {
|
||||||
.fa {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
span {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
@ -1199,7 +1258,7 @@ a.status__content__spoiler-link {
|
||||||
|
|
||||||
@import 'boost';
|
@import 'boost';
|
||||||
|
|
||||||
button i.fa-retweet {
|
button.icon-button i.fa-retweet {
|
||||||
height: 19px;
|
height: 19px;
|
||||||
width: 22px;
|
width: 22px;
|
||||||
background-position: 0 0;
|
background-position: 0 0;
|
||||||
|
@ -1211,7 +1270,7 @@ button i.fa-retweet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button.active i.fa-retweet {
|
button.icon-button.active i.fa-retweet {
|
||||||
transition-duration: 0.9s;
|
transition-duration: 0.9s;
|
||||||
background-position: 0 100%;
|
background-position: 0 100%;
|
||||||
}
|
}
|
||||||
|
@ -1381,12 +1440,15 @@ button.active i.fa-retweet {
|
||||||
|
|
||||||
.empty-column-indicator {
|
.empty-column-indicator {
|
||||||
color: lighten($color1, 20%);
|
color: lighten($color1, 20%);
|
||||||
|
background: $color1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
padding-top: 100px;
|
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $color4;
|
color: $color4;
|
||||||
|
@ -1412,22 +1474,23 @@ button.active i.fa-retweet {
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-dialog {
|
.emoji-dialog {
|
||||||
width: 280px;
|
width: 245px;
|
||||||
height: 220px;
|
height: 270px;
|
||||||
background: $color2;
|
background: $color5;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-radius: 2px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 0 15px rgba($color8, 0.4);
|
box-shadow: 0 0 8px rgba($color8, 0.2);
|
||||||
|
|
||||||
.emojione {
|
.emojione {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-dialog-header {
|
.emoji-dialog-header {
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
background-color: $color3;
|
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -1438,18 +1501,29 @@ button.active i.fa-retweet {
|
||||||
li {
|
li {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 42px;
|
padding: 10px 5px;
|
||||||
padding: 9px 5px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
|
||||||
|
.emoji {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
img, svg {
|
img, svg {
|
||||||
width: 22px;
|
width: 18px;
|
||||||
height: 22px;
|
height: 18px;
|
||||||
filter: grayscale(100%);
|
filter: grayscale(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
img, svg {
|
||||||
|
filter: grayscale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: lighten($color3, 6%);
|
border-bottom-color: $color4;
|
||||||
|
|
||||||
img, svg {
|
img, svg {
|
||||||
filter: grayscale(0);
|
filter: grayscale(0);
|
||||||
|
@ -1473,7 +1547,7 @@ button.active i.fa-retweet {
|
||||||
.emoji-category-header {
|
.emoji-category-header {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
padding: 8px 16px 0;
|
padding: 10px 8px 10px 16px;
|
||||||
display: table;
|
display: table;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
|
@ -1483,10 +1557,10 @@ button.active i.fa-retweet {
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-category-title {
|
.emoji-category-title {
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
font-family: sans-serif;
|
text-transform: uppercase;
|
||||||
font-weight: normal;
|
font-weight: 500;
|
||||||
color: $color1;
|
color: darken($color2, 18%);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1526,7 +1600,7 @@ button.active i.fa-retweet {
|
||||||
width: 7px;
|
width: 7px;
|
||||||
height: 7px;
|
height: 7px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 2px solid $color1;
|
border: 2px solid $color5;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
left: 2px;
|
left: 2px;
|
||||||
}
|
}
|
||||||
|
@ -1534,14 +1608,20 @@ button.active i.fa-retweet {
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-search-wrapper {
|
.emoji-search-wrapper {
|
||||||
padding: 6px 16px;
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid lighten($color2, 4%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-search {
|
.emoji-search {
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
padding: 6px 4px;
|
font-weight: 400;
|
||||||
|
padding: 7px 9px;
|
||||||
|
font-family: inherit;
|
||||||
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid #ddd;
|
background: rgba($color2, 0.3);
|
||||||
|
color: darken($color2, 18%);
|
||||||
|
border: 1px solid $color2;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1554,11 +1634,21 @@ button.active i.fa-retweet {
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-search-wrapper + .emoji-categories-wrapper {
|
.emoji-search-wrapper + .emoji-categories-wrapper {
|
||||||
top: 83px;
|
top: 93px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-row .emoji:hover {
|
.emoji-row .emoji {
|
||||||
background: lighten($color2, 3%);
|
img, svg {
|
||||||
|
transition: transform 60ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: lighten($color2, 3%);
|
||||||
|
|
||||||
|
img, svg {
|
||||||
|
transform: translateZ(0) scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji {
|
.emoji {
|
||||||
|
@ -1915,3 +2005,41 @@ button.active i.fa-retweet {
|
||||||
max-height: 80vh;
|
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
|
end
|
||||||
|
|
||||||
def followers
|
def followers
|
||||||
@followers = @account.followers.order('follows.created_at desc').paginate(page: params[:page], per_page: 12)
|
@followers = @account.followers.order('follows.created_at desc').page(params[:page]).per(12)
|
||||||
end
|
end
|
||||||
|
|
||||||
def following
|
def following
|
||||||
@following = @account.following.order('follows.created_at desc').paginate(page: params[:page], per_page: 12)
|
@following = @account.following.order('follows.created_at desc').page(params[:page]).per(12)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -53,7 +53,7 @@ class AccountsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def webfinger_account_url
|
def webfinger_account_url
|
||||||
webfinger_url(resource: "acct:#{@account.acct}@#{Rails.configuration.x.local_domain}")
|
webfinger_url(resource: @account.to_webfinger_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_account_suspension
|
def check_account_suspension
|
||||||
|
|
|
@ -1,51 +1,50 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Admin::AccountsController < ApplicationController
|
module Admin
|
||||||
before_action :require_admin!
|
class AccountsController < BaseController
|
||||||
before_action :set_account, except: :index
|
before_action :set_account, except: :index
|
||||||
|
|
||||||
layout 'admin'
|
def index
|
||||||
|
@accounts = Account.alphabetic.page(params[:page])
|
||||||
|
|
||||||
def index
|
@accounts = @accounts.local if params[:local].present?
|
||||||
@accounts = Account.alphabetic.paginate(page: params[:page], per_page: 40)
|
@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?
|
def show; end
|
||||||
@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 suspend
|
||||||
|
Admin::SuspensionWorker.perform_async(@account.id)
|
||||||
|
redirect_to admin_accounts_path
|
||||||
|
end
|
||||||
|
|
||||||
def suspend
|
def unsuspend
|
||||||
Admin::SuspensionWorker.perform_async(@account.id)
|
@account.update(suspended: false)
|
||||||
redirect_to admin_accounts_path
|
redirect_to admin_accounts_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def unsuspend
|
def silence
|
||||||
@account.update(suspended: false)
|
@account.update(silenced: true)
|
||||||
redirect_to admin_accounts_path
|
redirect_to admin_accounts_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def silence
|
def unsilence
|
||||||
@account.update(silenced: true)
|
@account.update(silenced: false)
|
||||||
redirect_to admin_accounts_path
|
redirect_to admin_accounts_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def unsilence
|
private
|
||||||
@account.update(silenced: false)
|
|
||||||
redirect_to admin_accounts_path
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
def set_account
|
||||||
|
@account = Account.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
def set_account
|
def account_params
|
||||||
@account = Account.find(params[:id])
|
params.require(:account).permit(:silenced, :suspended)
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_params
|
|
||||||
params.require(:account).permit(:silenced, :suspended)
|
|
||||||
end
|
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
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Admin::DomainBlocksController < ApplicationController
|
module Admin
|
||||||
before_action :require_admin!
|
class DomainBlocksController < BaseController
|
||||||
|
def index
|
||||||
|
@blocks = DomainBlock.page(params[:page])
|
||||||
|
end
|
||||||
|
|
||||||
layout 'admin'
|
def new
|
||||||
|
@domain_block = DomainBlock.new
|
||||||
|
end
|
||||||
|
|
||||||
def index
|
def create
|
||||||
@blocks = DomainBlock.paginate(page: params[:page], per_page: 40)
|
@domain_block = DomainBlock.new(resource_params)
|
||||||
end
|
|
||||||
|
|
||||||
def new
|
if @domain_block.save
|
||||||
@domain_block = DomainBlock.new
|
DomainBlockWorker.perform_async(@domain_block.id)
|
||||||
end
|
redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed'
|
||||||
|
else
|
||||||
|
render action: :new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
private
|
||||||
@domain_block = DomainBlock.new(resource_params)
|
|
||||||
|
|
||||||
if @domain_block.save
|
def resource_params
|
||||||
DomainBlockWorker.perform_async(@domain_block.id)
|
params.require(:domain_block).permit(:domain, :severity)
|
||||||
redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed'
|
|
||||||
else
|
|
||||||
render action: :new
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def resource_params
|
|
||||||
params.require(:domain_block).permit(:domain, :severity)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Admin::PubsubhubbubController < ApplicationController
|
module Admin
|
||||||
before_action :require_admin!
|
class PubsubhubbubController < BaseController
|
||||||
|
def index
|
||||||
layout 'admin'
|
@subscriptions = Subscription.order('id desc').includes(:account).page(params[:page])
|
||||||
|
end
|
||||||
def index
|
|
||||||
@subscriptions = Subscription.order('id desc').includes(:account).paginate(page: params[:page], per_page: 40)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,45 +1,44 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Admin::ReportsController < ApplicationController
|
module Admin
|
||||||
before_action :require_admin!
|
class ReportsController < BaseController
|
||||||
before_action :set_report, except: [:index]
|
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
|
def show
|
||||||
@reports = Report.includes(:account, :target_account).order('id desc').paginate(page: params[:page], per_page: 40)
|
@statuses = Status.where(id: @report.status_ids)
|
||||||
@reports = params[:action_taken].present? ? @reports.resolved : @reports.unresolved
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def show
|
def resolve
|
||||||
@statuses = Status.where(id: @report.status_ids)
|
@report.update(action_taken: true, action_taken_by_account_id: current_account.id)
|
||||||
end
|
redirect_to admin_report_path(@report)
|
||||||
|
end
|
||||||
|
|
||||||
def resolve
|
def suspend
|
||||||
@report.update(action_taken: true, action_taken_by_account_id: current_account.id)
|
Admin::SuspensionWorker.perform_async(@report.target_account.id)
|
||||||
redirect_to admin_report_path(@report)
|
Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
|
||||||
end
|
redirect_to admin_report_path(@report)
|
||||||
|
end
|
||||||
|
|
||||||
def suspend
|
def silence
|
||||||
Admin::SuspensionWorker.perform_async(@report.target_account.id)
|
@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)
|
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)
|
redirect_to admin_report_path(@report)
|
||||||
end
|
end
|
||||||
|
|
||||||
def silence
|
def remove
|
||||||
@report.target_account.update(silenced: true)
|
RemovalWorker.perform_async(params[:status_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)
|
||||||
redirect_to admin_report_path(@report)
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def remove
|
private
|
||||||
RemovalWorker.perform_async(params[:status_id])
|
|
||||||
redirect_to admin_report_path(@report)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
def set_report
|
||||||
|
@report = Report.find(params[:id])
|
||||||
def set_report
|
end
|
||||||
@report = Report.find(params[:id])
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,35 +1,33 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Admin::SettingsController < ApplicationController
|
module Admin
|
||||||
before_action :require_admin!
|
class SettingsController < BaseController
|
||||||
|
def index
|
||||||
layout 'admin'
|
@settings = Setting.all_as_records
|
||||||
|
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
respond_to do |format|
|
def update
|
||||||
format.html { redirect_to admin_settings_path }
|
@setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id])
|
||||||
format.json { respond_with_bip(@setting) }
|
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
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def settings_params
|
def settings_params
|
||||||
params.require(:setting).permit(:value)
|
params.require(:setting).permit(:value)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,7 +9,7 @@ class Api::V1::NotificationsController < ApiController
|
||||||
DEFAULT_NOTIFICATIONS_LIMIT = 15
|
DEFAULT_NOTIFICATIONS_LIMIT = 15
|
||||||
|
|
||||||
def index
|
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)
|
@notifications = cache_collection(@notifications, Notification)
|
||||||
statuses = @notifications.select { |n| !n.target_status.nil? }.map(&:target_status)
|
statuses = @notifications.select { |n| !n.target_status.nil? }.map(&:target_status)
|
||||||
|
|
||||||
|
@ -32,7 +32,13 @@ class Api::V1::NotificationsController < ApiController
|
||||||
|
|
||||||
private
|
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)
|
def pagination_params(core_params)
|
||||||
params.permit(:limit).merge(core_params)
|
params.permit(:limit, exclude_types: []).merge(core_params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,7 @@ class ApiController < ApplicationController
|
||||||
protect_from_forgery with: :null_session
|
protect_from_forgery with: :null_session
|
||||||
|
|
||||||
skip_before_action :verify_authenticity_token
|
skip_before_action :verify_authenticity_token
|
||||||
|
skip_before_action :store_current_location
|
||||||
|
|
||||||
before_action :set_rate_limit_headers
|
before_action :set_rate_limit_headers
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ class RemoteFollowController < ApplicationController
|
||||||
|
|
||||||
session[:remote_follow] = @remote_follow.acct
|
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
|
else
|
||||||
render :new
|
render :new
|
||||||
end
|
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
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'csv'
|
|
||||||
|
|
||||||
class Settings::ExportsController < ApplicationController
|
class Settings::ExportsController < ApplicationController
|
||||||
layout 'admin'
|
layout 'admin'
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_account
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@total_storage = current_account.media_attachments.sum(:file_file_size)
|
@total_storage = current_account.media_attachments.sum(:file_file_size)
|
||||||
@total_follows = current_account.following.count
|
@total_follows = current_account.following.count
|
||||||
@total_blocks = current_account.blocking.count
|
@total_blocks = current_account.blocking.count
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -23,8 +23,9 @@ class Settings::PreferencesController < ApplicationController
|
||||||
}
|
}
|
||||||
|
|
||||||
current_user.settings['default_privacy'] = user_params[:setting_default_privacy]
|
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')
|
redirect_to settings_preferences_path, notice: I18n.t('generic.changes_saved_msg')
|
||||||
else
|
else
|
||||||
render action: :show
|
render action: :show
|
||||||
|
@ -34,6 +35,6 @@ class Settings::PreferencesController < ApplicationController
|
||||||
private
|
private
|
||||||
|
|
||||||
def user_params
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,7 +14,7 @@ class XrdController < ApplicationController
|
||||||
|
|
||||||
def webfinger
|
def webfinger
|
||||||
@account = Account.find_local!(username_from_resource)
|
@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)
|
@magic_key = pem_to_magic_key(@account.keypair.public_key)
|
||||||
|
|
||||||
respond_to do |format|
|
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',
|
en: 'English',
|
||||||
de: 'Deutsch',
|
de: 'Deutsch',
|
||||||
es: 'Español',
|
es: 'Español',
|
||||||
|
eo: 'Esperanto',
|
||||||
pt: 'Português',
|
pt: 'Português',
|
||||||
fr: 'Français',
|
fr: 'Français',
|
||||||
hu: 'Magyar',
|
hu: 'Magyar',
|
||||||
uk: 'Українська',
|
uk: 'Українська',
|
||||||
'zh-CN': '简体中文',
|
'zh-CN': '简体中文',
|
||||||
fi: 'Suomi',
|
fi: 'Suomi',
|
||||||
eo: 'Esperanto',
|
ru: 'Русский',
|
||||||
|
ja: '日本語',
|
||||||
|
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
def human_locale(locale)
|
def human_locale(locale)
|
||||||
|
|
|
@ -9,10 +9,6 @@ module StreamEntriesHelper
|
||||||
"@#{account.acct}#{@external_links && account.local? ? "@#{Rails.configuration.x.local_domain}" : ''}"
|
"@#{account.acct}#{@external_links && account.local? ? "@#{Rails.configuration.x.local_domain}" : ''}"
|
||||||
end
|
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)
|
def entry_classes(status, is_predecessor, is_successor, include_threads)
|
||||||
classes = ['entry']
|
classes = ['entry']
|
||||||
classes << 'entry-reblog u-repost-of h-cite' if status.reblog?
|
classes << 'entry-reblog u-repost-of h-cite' if status.reblog?
|
||||||
|
@ -22,18 +18,6 @@ module StreamEntriesHelper
|
||||||
classes.join(' ')
|
classes.join(' ')
|
||||||
end
|
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)
|
def rtl?(text)
|
||||||
return false if text.empty?
|
return false if text.empty?
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ class AtomSerializer
|
||||||
append_element(author, 'activity:object-type', TagManager::TYPES[:person])
|
append_element(author, 'activity:object-type', TagManager::TYPES[:person])
|
||||||
append_element(author, 'uri', uri)
|
append_element(author, 'uri', uri)
|
||||||
append_element(author, 'name', account.username)
|
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, '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: :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)))
|
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)
|
timeline_key = key(:home, into_account.id)
|
||||||
oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
|
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
|
redis.pipelined do
|
||||||
statuses.each do |status|
|
statuses.each do |status|
|
||||||
redis.zrem(timeline_key, status.id)
|
redis.zrem(timeline_key, status.id)
|
||||||
|
|
|
@ -15,7 +15,6 @@ class Formatter
|
||||||
html = status.text
|
html = status.text
|
||||||
html = encode(html)
|
html = encode(html)
|
||||||
html = simple_format(html, {}, sanitize: false)
|
html = simple_format(html, {}, sanitize: false)
|
||||||
html = html.gsub(/\n/, '')
|
|
||||||
html = link_urls(html)
|
html = link_urls(html)
|
||||||
html = link_mentions(html, status.mentions)
|
html = link_mentions(html, status.mentions)
|
||||||
html = link_hashtags(html)
|
html = link_hashtags(html)
|
||||||
|
|
|
@ -12,12 +12,12 @@ class Account < ApplicationRecord
|
||||||
validates :username, presence: true, uniqueness: { scope: :domain, case_sensitive: true }, unless: 'local?'
|
validates :username, presence: true, uniqueness: { scope: :domain, case_sensitive: true }, unless: 'local?'
|
||||||
|
|
||||||
# Avatar upload
|
# 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_content_type :avatar, content_type: IMAGE_MIME_TYPES
|
||||||
validates_attachment_size :avatar, less_than: 2.megabytes
|
validates_attachment_size :avatar, less_than: 2.megabytes
|
||||||
|
|
||||||
# Header upload
|
# 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_content_type :header, content_type: IMAGE_MIME_TYPES
|
||||||
validates_attachment_size :header, less_than: 2.megabytes
|
validates_attachment_size :header, less_than: 2.megabytes
|
||||||
|
|
||||||
|
@ -120,6 +120,14 @@ class Account < ApplicationRecord
|
||||||
local? ? username : "#{username}@#{domain}"
|
local? ? username : "#{username}@#{domain}"
|
||||||
end
|
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?
|
def subscribed?
|
||||||
!subscription_expires_at.blank?
|
!subscription_expires_at.blank?
|
||||||
end
|
end
|
||||||
|
@ -150,6 +158,22 @@ class Account < ApplicationRecord
|
||||||
save!
|
save!
|
||||||
end
|
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)
|
def avatar_remote_url=(url)
|
||||||
parsed_url = URI.parse(url)
|
parsed_url = URI.parse(url)
|
||||||
|
|
||||||
|
@ -284,6 +308,18 @@ class Account < ApplicationRecord
|
||||||
def follow_mapping(query, field)
|
def follow_mapping(query, field)
|
||||||
query.pluck(field).inject({}) { |mapping, id| mapping[id] = true; mapping }
|
query.pluck(field).inject({}) { |mapping, id| mapping[id] = true; mapping }
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
before_create do
|
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] }
|
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
|
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 :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
|
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
|
end
|
||||||
|
|
||||||
def type
|
def type
|
||||||
case activity_type
|
@type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym
|
||||||
when 'Status'
|
|
||||||
:reblog
|
|
||||||
else
|
|
||||||
activity_type.underscore.to_sym
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def target_status
|
def target_status
|
||||||
|
@ -50,6 +52,11 @@ class Notification < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
class << self
|
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)
|
def reload_stale_associations!(cached_items)
|
||||||
account_ids = cached_items.map(&:from_account_id).uniq
|
account_ids = cached_items.map(&:from_account_id).uniq
|
||||||
accounts = Account.where(id: account_ids).map { |a| [a.id, a] }.to_h
|
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]
|
item.from_account = accounts[item.from_account_id]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def activity_types_from_types(types)
|
||||||
|
types.map { |type| TYPE_CLASS_MAP[type.to_sym] }.compact
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
after_initialize :set_from_account
|
after_initialize :set_from_account
|
||||||
|
|
|
@ -26,4 +26,8 @@ class User < ApplicationRecord
|
||||||
def setting_default_privacy
|
def setting_default_privacy
|
||||||
settings.default_privacy || (account.locked? ? 'private' : 'public')
|
settings.default_privacy || (account.locked? ? 'private' : 'public')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def setting_boost_modal
|
||||||
|
settings.boost_modal
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,5 +5,4 @@ class BaseService
|
||||||
include ActionView::Helpers::SanitizeHelper
|
include ActionView::Helpers::SanitizeHelper
|
||||||
|
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
include AtomBuilderHelper
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
.info
|
.info
|
||||||
= link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
|
= 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
|
= link_to t('about.about_this'), about_more_path
|
||||||
|
|
||||||
|
@ -79,8 +79,8 @@
|
||||||
.info
|
.info
|
||||||
= link_to t('about.terms'), terms_path
|
= 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.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
|
- else
|
||||||
= render partial: 'grid_card', collection: @followers, as: :account, cached: true
|
= render partial: 'grid_card', collection: @followers, as: :account, cached: true
|
||||||
|
|
||||||
= will_paginate @followers, pagination_options
|
= paginate @followers
|
||||||
|
|
|
@ -9,4 +9,4 @@
|
||||||
- else
|
- else
|
||||||
= render partial: 'grid_card', collection: @following, as: :account, cached: true
|
= render partial: 'grid_card', collection: @following, as: :account, cached: true
|
||||||
|
|
||||||
= will_paginate @following, pagination_options
|
= paginate @following
|
||||||
|
|
|
@ -31,4 +31,4 @@
|
||||||
|
|
||||||
.pagination
|
.pagination
|
||||||
- if @statuses.size == 20
|
- 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 'globe', 'Public', TagManager.instance.url_for(account)
|
||||||
= table_link_to 'pencil', 'Edit', admin_account_path(account.id)
|
= table_link_to 'pencil', 'Edit', admin_account_path(account.id)
|
||||||
|
|
||||||
= will_paginate @accounts, pagination_options
|
= paginate @accounts
|
||||||
|
|
|
@ -13,5 +13,5 @@
|
||||||
%samp= block.domain
|
%samp= block.domain
|
||||||
%td= block.severity
|
%td= block.severity
|
||||||
|
|
||||||
= will_paginate @blocks, pagination_options
|
= paginate @blocks
|
||||||
= link_to 'Add new', new_admin_domain_block_path, class: 'button'
|
= link_to 'Add new', new_admin_domain_block_path, class: 'button'
|
||||||
|
|
|
@ -26,4 +26,4 @@
|
||||||
- else
|
- else
|
||||||
= l subscription.last_successful_delivery_at
|
= 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= truncate(report.comment, length: 30, separator: ' ')
|
||||||
%td= table_link_to 'circle', 'View', admin_report_path(report)
|
%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(:note) { |account| Formatter.instance.simplified_format(account) }
|
||||||
node(:url) { |account| TagManager.instance.url_for(account) }
|
node(:url) { |account| TagManager.instance.url_for(account) }
|
||||||
node(:avatar) { |account| full_asset_url(account.avatar.url(:original)) }
|
node(:avatar) { |account| full_asset_url(account.avatar_original_url) }
|
||||||
node(:header) { |account| full_asset_url(account.header.url(:original)) }
|
node(:avatar_static) { |account| full_asset_url(account.avatar_static_url) }
|
||||||
node(:followers_count) { |account| defined?(@followers_counts_map) ? (@followers_counts_map[account.id] || 0) : account.followers_count }
|
node(:header) { |account| full_asset_url(account.header_original_url) }
|
||||||
node(:following_count) { |account| defined?(@following_counts_map) ? (@following_counts_map[account.id] || 0) : account.following_count }
|
node(:header_static) { |account| full_asset_url(account.header_static_url) }
|
||||||
node(:statuses_count) { |account| defined?(@statuses_counts_map) ? (@statuses_counts_map[account.id] || 0) : account.statuses_count }
|
|
||||||
|
attributes :followers_count, :following_count, :statuses_count
|
||||||
|
|
|
@ -5,6 +5,7 @@ node(:meta) do
|
||||||
access_token: @token,
|
access_token: @token,
|
||||||
locale: I18n.locale,
|
locale: I18n.locale,
|
||||||
me: current_account.id,
|
me: current_account.id,
|
||||||
|
boost_modal: current_account.user.setting_boost_modal,
|
||||||
}
|
}
|
||||||
end
|
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
|
%tr
|
||||||
%th= t('exports.follows')
|
%th= t('exports.follows')
|
||||||
%td= @total_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
|
%tr
|
||||||
%th= t('exports.blocks')
|
%th= t('exports.blocks')
|
||||||
%td= @total_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_follower, as: :boolean, wrapper: :with_label
|
||||||
= ff.input :must_be_following, 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
|
.actions
|
||||||
= f.button :button, t('generic.save_changes'), type: :submit
|
= f.button :button, t('generic.save_changes'), type: :submit
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
.landing-strip
|
.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')
|
= fa_icon('retweet fw')
|
||||||
%span
|
%span
|
||||||
= link_to TagManager.instance.url_for(status.account), class: 'status__display-name muted' do
|
= 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')
|
= t('stream_entries.reblogged')
|
||||||
|
|
||||||
= render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: status.proper }
|
= render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: status.proper }
|
||||||
|
|
|
@ -15,4 +15,4 @@
|
||||||
|
|
||||||
- if @statuses.size == 20
|
- if @statuses.size == 20
|
||||||
.pagination
|
.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
|
class ImportWorker
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
|
|
||||||
sidekiq_options queue: 'pull', retry: false
|
sidekiq_options queue: 'pull', retry: false
|
||||||
|
|
||||||
def perform(import_id)
|
attr_reader :import
|
||||||
import = Import.find(import_id)
|
|
||||||
|
|
||||||
case import.type
|
def perform(import_id)
|
||||||
|
@import = Import.find(import_id)
|
||||||
|
|
||||||
|
case @import.type
|
||||||
when 'blocking'
|
when 'blocking'
|
||||||
process_blocks(import)
|
process_blocks
|
||||||
when 'following'
|
when 'following'
|
||||||
process_follows(import)
|
process_follows
|
||||||
end
|
end
|
||||||
|
|
||||||
import.destroy
|
@import.destroy
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def process_blocks(import)
|
def from_account
|
||||||
from_account = import.account
|
@import.account
|
||||||
|
end
|
||||||
|
|
||||||
CSV.foreach(import.data.path) do |row|
|
def import_contents
|
||||||
next if row.size != 1
|
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
|
begin
|
||||||
target_account = FollowRemoteAccountService.new.call(row[0])
|
target_account = FollowRemoteAccountService.new.call(row.first)
|
||||||
next if target_account.nil?
|
next if target_account.nil?
|
||||||
BlockService.new.call(from_account, target_account)
|
BlockService.new.call(from_account, target_account)
|
||||||
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError
|
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError
|
||||||
|
@ -38,14 +47,10 @@ class ImportWorker
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_follows(import)
|
def process_follows
|
||||||
from_account = import.account
|
import_rows.each do |row|
|
||||||
|
|
||||||
CSV.foreach(import.data.path) do |row|
|
|
||||||
next if row.size != 1
|
|
||||||
|
|
||||||
begin
|
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
|
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|
|
@ -24,7 +24,9 @@ module Mastodon
|
||||||
|
|
||||||
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
|
# 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.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.i18n.default_locale = :en
|
||||||
|
|
||||||
# config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
|
# 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 :application, 'mastodon'
|
||||||
set :repo_url, 'https://github.com/tootsuite/mastodon.git'
|
set :repo_url, 'https://github.com/tootsuite/mastodon.git'
|
||||||
set :branch, 'master'
|
set :branch, 'skylight'
|
||||||
set :rbenv_type, :user
|
set :rbenv_type, :user
|
||||||
set :rbenv_ruby, File.read('.ruby-version').strip
|
set :rbenv_ruby, File.read('.ruby-version').strip
|
||||||
set :migration_role, :app
|
set :migration_role, :app
|
||||||
|
|
||||||
append :linked_files, '.env.production'
|
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
|
# By default, use the lowest log level to ensure availability of diagnostic information
|
||||||
# when problems arise.
|
# 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.
|
# Prepend all log lines with the following tags.
|
||||||
config.log_tags = [:request_id]
|
config.log_tags = [:request_id]
|
||||||
|
@ -64,7 +64,7 @@ Rails.application.configure do
|
||||||
password: ENV.fetch('REDIS_PASSWORD') { false },
|
password: ENV.fetch('REDIS_PASSWORD') { false },
|
||||||
db: 0,
|
db: 0,
|
||||||
namespace: 'cache',
|
namespace: 'cache',
|
||||||
expires_in: 20.minutes,
|
expires_in: 10.minutes,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
|
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
|
||||||
|
@ -94,12 +94,14 @@ Rails.application.configure do
|
||||||
|
|
||||||
# E-mails
|
# E-mails
|
||||||
config.action_mailer.smtp_settings = {
|
config.action_mailer.smtp_settings = {
|
||||||
:port => ENV['SMTP_PORT'],
|
:port => ENV['SMTP_PORT'],
|
||||||
:address => ENV['SMTP_SERVER'],
|
:address => ENV['SMTP_SERVER'],
|
||||||
:user_name => ENV['SMTP_LOGIN'],
|
:user_name => ENV['SMTP_LOGIN'],
|
||||||
:password => ENV['SMTP_PASSWORD'],
|
:password => ENV['SMTP_PASSWORD'],
|
||||||
:domain => ENV['SMTP_DOMAIN'] || config.x.local_domain,
|
:domain => ENV['SMTP_DOMAIN'] || config.x.local_domain,
|
||||||
:authentication => :plain,
|
: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
|
config.action_mailer.delivery_method = :smtp
|
||||||
|
|
|
@ -33,7 +33,7 @@ search:
|
||||||
|
|
||||||
ignore_unused:
|
ignore_unused:
|
||||||
- 'activerecord.attributes.*'
|
- 'activerecord.attributes.*'
|
||||||
- '{devise,will_paginate,doorkeeper}.*'
|
- '{devise,pagination,doorkeeper}.*'
|
||||||
- '{datetime,time}.*'
|
- '{datetime,time}.*'
|
||||||
- 'simple_form.{yes,no}'
|
- 'simple_form.{yes,no}'
|
||||||
- 'simple_form.{placeholders,hints,labels}.*'
|
- '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