Merge pull request #261 from YoheiZuho/master

Master
This commit is contained in:
YoheiZuho 2019-12-25 15:22:11 +09:00 committed by GitHub
commit 235ce20a4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
167 changed files with 2280 additions and 1890 deletions

View File

View File

@ -27,10 +27,10 @@ plugins:
enabled: true enabled: true
eslint: eslint:
enabled: true enabled: true
channel: eslint-5 channel: eslint-6
rubocop: rubocop:
enabled: true enabled: true
channel: rubocop-0-71 channel: rubocop-0-76
sass-lint: sass-lint:
enabled: true enabled: true
exclude_patterns: exclude_patterns:

View File

@ -183,6 +183,11 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# LDAP_BIND_DN= # LDAP_BIND_DN=
# LDAP_PASSWORD= # LDAP_PASSWORD=
# LDAP_UID=cn # LDAP_UID=cn
# LDAP_MAIL=mail
# LDAP_SEARCH_FILTER=(|(%{uid}=%{email})(%{mail}=%{email}))
# LDAP_UID_CONVERSION_ENABLED=true
# LDAP_UID_CONVERSION_SEARCH=., -
# LDAP_UID_CONVERSION_REPLACE=_
# PAM authentication (optional) # PAM authentication (optional)
# PAM authentication uses for the email generation the "email" pam variable # PAM authentication uses for the email generation the "email" pam variable
@ -226,8 +231,8 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# Optional SAML authentication (cf. omniauth-saml) # Optional SAML authentication (cf. omniauth-saml)
# SAML_ENABLED=true # SAML_ENABLED=true
# SAML_ACS_URL= # SAML_ACS_URL=http://localhost:3000/auth/auth/saml/callback
# SAML_ISSUER=http://localhost:3000/auth/auth/saml/callback # SAML_ISSUER=https://example.com
# SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO # SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO
# SAML_IDP_CERT= # SAML_IDP_CERT=
# SAML_IDP_CERT_FINGERPRINT= # SAML_IDP_CERT_FINGERPRINT=

View File

@ -178,7 +178,11 @@ STREAMING_CLUSTER_NUM=1
# LDAP_BIND_DN= # LDAP_BIND_DN=
# LDAP_PASSWORD= # LDAP_PASSWORD=
# LDAP_UID=cn # LDAP_UID=cn
# LDAP_SEARCH_FILTER=%{uid}=%{email} # LDAP_MAIL=mail
# LDAP_SEARCH_FILTER=(|(%{uid}=%{email})(%{mail}=%{email}))
# LDAP_UID_CONVERSION_ENABLED=true
# LDAP_UID_CONVERSION_SEARCH=., -
# LDAP_UID_CONVERSION_REPLACE=_
# PAM authentication (optional) # PAM authentication (optional)
# PAM authentication uses for the email generation the "email" pam variable # PAM authentication uses for the email generation the "email" pam variable
@ -222,8 +226,8 @@ STREAMING_CLUSTER_NUM=1
# Optional SAML authentication (cf. omniauth-saml) # Optional SAML authentication (cf. omniauth-saml)
# SAML_ENABLED=true # SAML_ENABLED=true
# SAML_ACS_URL= # SAML_ACS_URL=http://localhost:3000/auth/auth/saml/callback
# SAML_ISSUER=http://localhost:3000/auth/auth/saml/callback # SAML_ISSUER=https://example.com
# SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO # SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO
# SAML_IDP_CERT= # SAML_IDP_CERT=
# SAML_IDP_CERT_FINGERPRINT= # SAML_IDP_CERT_FINGERPRINT=

3
.gitignore vendored
View File

@ -13,6 +13,7 @@
/db/*.sqlite3-journal /db/*.sqlite3-journal
# Ignore all logfiles and tempfiles. # Ignore all logfiles and tempfiles.
.eslintcache
/log/* /log/*
!/log/.keep !/log/.keep
/tmp /tmp
@ -23,6 +24,7 @@ public/packs
public/packs-test public/packs-test
.env .env
.env.production .env.production
.env.development
node_modules/ node_modules/
build/ build/
@ -60,4 +62,3 @@ ubuntu-xenial-16.04-cloudimg-console.log
# Ignore Docker option files # Ignore Docker option files
docker-compose.override.yml docker-compose.override.yml

View File

@ -71,6 +71,9 @@ Naming/MemoizedInstanceVariableName:
Rails: Rails:
Enabled: true Enabled: true
Rails/EnumHash:
Enabled: false
Rails/HasAndBelongsToMany: Rails/HasAndBelongsToMany:
Enabled: false Enabled: false
@ -102,6 +105,9 @@ Style/Documentation:
Style/DoubleNegation: Style/DoubleNegation:
Enabled: true Enabled: true
Style/FormatStringToken:
Enabled: false
Style/FrozenStringLiteralComment: Style/FrozenStringLiteralComment:
Enabled: true Enabled: true

View File

@ -3,8 +3,8 @@ FROM ubuntu:18.04 as build-dep
# Use bash for the shell # Use bash for the shell
SHELL ["bash", "-c"] SHELL ["bash", "-c"]
# Install Node # Install Node v12 (LTS)
ENV NODE_VER="12.11.1" ENV NODE_VER="12.13.1"
RUN echo "Etc/UTC" > /etc/localtime && \ RUN echo "Etc/UTC" > /etc/localtime && \
apt update && \ apt update && \
apt -y install wget python && \ apt -y install wget python && \

33
Gemfile
View File

@ -5,17 +5,18 @@ ruby '>= 2.4.0', '< 2.7.0'
gem 'pkg-config', '~> 1.4' gem 'pkg-config', '~> 1.4'
gem 'puma', '~> 4.2' gem 'puma', '~> 4.3'
gem 'rails', '~> 5.2.3' gem 'rails', '~> 5.2.4'
gem 'sprockets', '~> 3.7'
gem 'thor', '~> 0.20' gem 'thor', '~> 0.20'
gem 'hamlit-rails', '~> 0.2' gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.1' gem 'pg', '~> 1.1'
gem 'makara', '~> 0.4' gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.3' gem 'pghero', '~> 2.4'
gem 'dotenv-rails', '~> 2.7' gem 'dotenv-rails', '~> 2.7'
gem 'aws-sdk-s3', '~> 1.55', require: false gem 'aws-sdk-s3', '~> 1.59', require: false
gem 'fog-core', '<= 2.1.0' gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0' gem 'paperclip', '~> 6.0'
@ -27,7 +28,7 @@ gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.7' gem 'addressable', '~> 2.7'
gem 'bootsnap', '~> 1.4', require: false gem 'bootsnap', '~> 1.4', require: false
gem 'browser' gem 'browser'
gem 'charlock_holmes', '~> 0.7.6' gem 'charlock_holmes', '~> 0.7.7'
gem 'iso-639' gem 'iso-639'
gem 'chewy', '~> 5.1' gem 'chewy', '~> 5.1'
gem 'cld3', '~> 3.2.4' gem 'cld3', '~> 3.2.4'
@ -38,7 +39,7 @@ group :pam_authentication, optional: true do
gem 'devise_pam_authenticatable2', '~> 9.2' gem 'devise_pam_authenticatable2', '~> 9.2'
end end
gem 'net-ldap', '~> 0.10' gem 'net-ldap', '~> 0.16'
gem 'omniauth-cas', '~> 1.1' gem 'omniauth-cas', '~> 1.1'
gem 'omniauth-saml', '~> 1.10' gem 'omniauth-saml', '~> 1.10'
gem 'omniauth', '~> 1.9' gem 'omniauth', '~> 1.9'
@ -63,16 +64,16 @@ gem 'mime-types', '~> 3.3', require: 'mime/types/columnar'
gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532' gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532'
gem 'nokogiri', '~> 1.10' gem 'nokogiri', '~> 1.10'
gem 'nsa', '~> 0.2' gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.9' gem 'oj', '~> 3.10'
gem 'ostatus2', '~> 2.0' gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.11' gem 'ox', '~> 2.11'
gem 'parslet' gem 'parslet'
gem 'parallel', '~> 1.18' gem 'parallel', '~> 1.19'
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c' gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
gem 'pundit', '~> 2.1' gem 'pundit', '~> 2.1'
gem 'premailer-rails' gem 'premailer-rails'
gem 'rack-attack', '~> 6.2' gem 'rack-attack', '~> 6.2'
gem 'rack-cors', '~> 1.0', require: 'rack/cors' gem 'rack-cors', '~> 1.1', require: 'rack/cors'
gem 'rails-i18n', '~> 5.1' gem 'rails-i18n', '~> 5.1'
gem 'rails-settings-cached', '~> 0.6' gem 'rails-settings-cached', '~> 0.6'
gem 'redis', '~> 4.1', require: ['redis', 'redis/connection/hiredis'] gem 'redis', '~> 4.1', require: ['redis', 'redis/connection/hiredis']
@ -88,9 +89,9 @@ gem 'simple-navigation', '~> 4.1'
gem 'simple_form', '~> 5.0' gem 'simple_form', '~> 5.0'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.2.0' gem 'stoplight', '~> 2.2.0'
gem 'strong_migrations', '~> 0.4' gem 'strong_migrations', '~> 0.5'
gem 'tty-command', '~> 0.9', require: false gem 'tty-command', '~> 0.9', require: false
gem 'tty-prompt', '~> 0.19', require: false gem 'tty-prompt', '~> 0.20', require: false
gem 'twitter-text', '~> 1.14' gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2019' gem 'tzinfo-data', '~> 1.2019'
gem 'webpacker', '~> 4.2' gem 'webpacker', '~> 4.2'
@ -103,7 +104,7 @@ gem 'rdf-normalize', '~> 0.3'
gem 'redcarpet', "~> 3.4.0" gem 'redcarpet', "~> 3.4.0"
group :development, :test do group :development, :test do
gem 'fabrication', '~> 2.20' gem 'fabrication', '~> 2.21'
gem 'fuubar', '~> 2.5' gem 'fuubar', '~> 2.5'
gem 'i18n-tasks', '~> 0.9', require: false gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-byebug', '~> 3.7' gem 'pry-byebug', '~> 3.7'
@ -118,13 +119,13 @@ end
group :test do group :test do
gem 'capybara', '~> 3.29' gem 'capybara', '~> 3.29'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.7' gem 'faker', '~> 2.9'
gem 'microformats', '~> 4.1' gem 'microformats', '~> 4.1'
gem 'rails-controller-testing', '~> 1.0' gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0' gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.17', require: false gem 'simplecov', '~> 0.17', require: false
gem 'webmock', '~> 3.7' gem 'webmock', '~> 3.7'
gem 'parallel_tests', '~> 2.29' gem 'parallel_tests', '~> 2.30'
end end
group :development do group :development do
@ -136,8 +137,8 @@ group :development do
gem 'letter_opener', '~> 1.7' gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.3' gem 'letter_opener_web', '~> 1.3'
gem 'memory_profiler' gem 'memory_profiler'
gem 'rubocop', '~> 0.76', require: false gem 'rubocop', '~> 0.78', require: false
gem 'rubocop-rails', '~> 2.3', require: false gem 'rubocop-rails', '~> 2.4', require: false
gem 'brakeman', '~> 4.7', require: false gem 'brakeman', '~> 4.7', require: false
gem 'bundler-audit', '~> 0.6', require: false gem 'bundler-audit', '~> 0.6', require: false

View File

@ -44,25 +44,25 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (5.2.3) actioncable (5.2.4.1)
actionpack (= 5.2.3) actionpack (= 5.2.4.1)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailer (5.2.3) actionmailer (5.2.4.1)
actionpack (= 5.2.3) actionpack (= 5.2.4.1)
actionview (= 5.2.3) actionview (= 5.2.4.1)
activejob (= 5.2.3) activejob (= 5.2.4.1)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (5.2.3) actionpack (5.2.4.1)
actionview (= 5.2.3) actionview (= 5.2.4.1)
activesupport (= 5.2.3) activesupport (= 5.2.4.1)
rack (~> 2.0) rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.2.3) actionview (5.2.4.1)
activesupport (= 5.2.3) activesupport (= 5.2.4.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
@ -73,20 +73,20 @@ GEM
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.7) active_record_query_trace (1.7)
activejob (5.2.3) activejob (5.2.4.1)
activesupport (= 5.2.3) activesupport (= 5.2.4.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (5.2.3) activemodel (5.2.4.1)
activesupport (= 5.2.3) activesupport (= 5.2.4.1)
activerecord (5.2.3) activerecord (5.2.4.1)
activemodel (= 5.2.3) activemodel (= 5.2.4.1)
activesupport (= 5.2.3) activesupport (= 5.2.4.1)
arel (>= 9.0) arel (>= 9.0)
activestorage (5.2.3) activestorage (5.2.4.1)
actionpack (= 5.2.3) actionpack (= 5.2.4.1)
activerecord (= 5.2.3) activerecord (= 5.2.4.1)
marcel (~> 0.3.1) marcel (~> 0.3.1)
activesupport (5.2.3) activesupport (5.2.4.1)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
minitest (~> 5.1) minitest (~> 5.1)
@ -105,17 +105,17 @@ GEM
av (0.9.0) av (0.9.0)
cocaine (~> 0.5.3) cocaine (~> 0.5.3)
aws-eventstream (1.0.3) aws-eventstream (1.0.3)
aws-partitions (1.240.0) aws-partitions (1.251.0)
aws-sdk-core (3.78.0) aws-sdk-core (3.84.0)
aws-eventstream (~> 1.0, >= 1.0.2) aws-eventstream (~> 1.0, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0) aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-kms (1.25.0) aws-sdk-kms (1.26.0)
aws-sdk-core (~> 3, >= 3.71.0) aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.55.0) aws-sdk-s3 (1.59.0)
aws-sdk-core (~> 3, >= 3.77.0) aws-sdk-core (~> 3, >= 3.83.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.0) aws-sigv4 (1.1.0)
@ -132,9 +132,9 @@ GEM
ffi (~> 1.10.0) ffi (~> 1.10.0)
bootsnap (1.4.5) bootsnap (1.4.5)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (4.7.1) brakeman (4.7.2)
browser (2.6.1) browser (2.7.1)
builder (3.2.3) builder (3.2.4)
bullet (6.0.2) bullet (6.0.2)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
@ -168,7 +168,7 @@ GEM
xpath (~> 3.2) xpath (~> 3.2)
case_transform (0.2) case_transform (0.2)
activesupport activesupport
charlock_holmes (0.7.6) charlock_holmes (0.7.7)
chewy (5.1.0) chewy (5.1.0)
activesupport (>= 4.0) activesupport (>= 4.0)
elasticsearch (>= 2.0.0) elasticsearch (>= 2.0.0)
@ -218,7 +218,7 @@ GEM
docile (1.3.2) docile (1.3.2)
domain_name (0.5.20180417) domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.2.2) doorkeeper (5.2.3)
railties (>= 5) railties (>= 5)
dotenv (2.7.5) dotenv (2.7.5)
dotenv-rails (2.7.5) dotenv-rails (2.7.5)
@ -238,9 +238,9 @@ GEM
erubi (1.9.0) erubi (1.9.0)
et-orbi (1.1.6) et-orbi (1.1.6)
tzinfo tzinfo
excon (0.62.0) excon (0.71.0)
fabrication (2.20.2) fabrication (2.21.0)
faker (2.7.0) faker (2.9.0)
i18n (>= 1.6, < 1.8) i18n (>= 1.6, < 1.8)
faraday (0.15.4) faraday (0.15.4)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
@ -324,7 +324,7 @@ GEM
jmespath (1.4.0) jmespath (1.4.0)
json (2.2.0) json (2.2.0)
json-canonicalization (0.1.0) json-canonicalization (0.1.0)
json-ld-preloaded (3.0.4) json-ld-preloaded (3.0.6)
json-ld (~> 3.0) json-ld (~> 3.0)
multi_json (~> 1.12) multi_json (~> 1.12)
rdf (~> 3.0) rdf (~> 3.0)
@ -356,7 +356,7 @@ GEM
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
request_store (~> 1.0) request_store (~> 1.0)
loofah (2.3.1) loofah (2.4.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.7.1) mail (2.7.1)
@ -380,15 +380,15 @@ GEM
mini_portile2 (2.4.0) mini_portile2 (2.4.0)
minitest (5.13.0) minitest (5.13.0)
msgpack (1.3.1) msgpack (1.3.1)
multi_json (1.13.1) multi_json (1.14.1)
multipart-post (2.1.1) multipart-post (2.1.1)
necromancer (0.5.0) necromancer (0.5.1)
net-ldap (0.16.1) net-ldap (0.16.2)
net-scp (2.0.0) net-scp (2.0.0)
net-ssh (>= 2.6.5, < 6.0.0) net-ssh (>= 2.6.5, < 6.0.0)
net-ssh (5.2.0) net-ssh (5.2.0)
nio4r (2.5.1) nio4r (2.5.2)
nokogiri (1.10.5) nokogiri (1.10.7)
mini_portile2 (~> 2.4.0) mini_portile2 (~> 2.4.0)
nokogumbo (2.0.1) nokogumbo (2.0.1)
nokogiri (~> 1.8, >= 1.8.4) nokogiri (~> 1.8, >= 1.8.4)
@ -397,7 +397,7 @@ GEM
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5) sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0) statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.9.2) oj (3.10.0)
omniauth (1.9.0) omniauth (1.9.0)
hashie (>= 3.4.6, < 3.7.0) hashie (>= 3.4.6, < 3.7.0)
rack (>= 1.6.2, < 3) rack (>= 1.6.2, < 3)
@ -423,8 +423,8 @@ 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)
parallel (1.18.0) parallel (1.19.1)
parallel_tests (2.29.2) parallel_tests (2.30.0)
parallel parallel
parser (2.6.5.0) parser (2.6.5.0)
ast (~> 2.4.0) ast (~> 2.4.0)
@ -433,7 +433,7 @@ GEM
equatable (~> 0.6) equatable (~> 0.6)
tty-color (~> 0.5) tty-color (~> 0.5)
pg (1.1.4) pg (1.1.4)
pghero (2.3.0) pghero (2.4.1)
activerecord (>= 5) activerecord (>= 5)
pkg-config (1.4.0) pkg-config (1.4.0)
premailer (1.11.1) premailer (1.11.1)
@ -453,34 +453,34 @@ GEM
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (4.0.1) public_suffix (4.0.1)
puma (4.2.0) puma (4.3.1)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.1.0) pundit (2.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.1.6) raabro (1.1.6)
rack (2.0.7) rack (2.0.8)
rack-attack (6.2.1) rack-attack (6.2.2)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-cors (1.0.6) rack-cors (1.1.0)
rack (>= 1.6.0) rack (>= 2.0.0)
rack-protection (2.0.7) rack-protection (2.0.7)
rack rack
rack-proxy (0.6.5) rack-proxy (0.6.5)
rack rack
rack-test (1.1.0) rack-test (1.1.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rails (5.2.3) rails (5.2.4.1)
actioncable (= 5.2.3) actioncable (= 5.2.4.1)
actionmailer (= 5.2.3) actionmailer (= 5.2.4.1)
actionpack (= 5.2.3) actionpack (= 5.2.4.1)
actionview (= 5.2.3) actionview (= 5.2.4.1)
activejob (= 5.2.3) activejob (= 5.2.4.1)
activemodel (= 5.2.3) activemodel (= 5.2.4.1)
activerecord (= 5.2.3) activerecord (= 5.2.4.1)
activestorage (= 5.2.3) activestorage (= 5.2.4.1)
activesupport (= 5.2.3) activesupport (= 5.2.4.1)
bundler (>= 1.3.0) bundler (>= 1.3.0)
railties (= 5.2.3) railties (= 5.2.4.1)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.4) rails-controller-testing (1.0.4)
actionpack (>= 5.0.1.x) actionpack (>= 5.0.1.x)
@ -496,15 +496,15 @@ GEM
railties (>= 5.0, < 6) railties (>= 5.0, < 6)
rails-settings-cached (0.6.6) rails-settings-cached (0.6.6)
rails (>= 4.2.0) rails (>= 4.2.0)
railties (5.2.3) railties (5.2.4.1)
actionpack (= 5.2.3) actionpack (= 5.2.4.1)
activesupport (= 5.2.3) activesupport (= 5.2.4.1)
method_source method_source
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0) thor (>= 0.19.0, < 2.0)
rainbow (3.0.0) rainbow (3.0.0)
rake (13.0.1) rake (13.0.1)
rdf (3.0.12) rdf (3.0.13)
hamster (~> 3.0) hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.3) rdf-normalize (0.3.3)
@ -559,14 +559,14 @@ GEM
rspec-core (~> 3.0, >= 3.0.0) rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0) sidekiq (>= 2.4.0)
rspec-support (3.9.0) rspec-support (3.9.0)
rubocop (0.76.0) rubocop (0.78.0)
jaro_winkler (~> 1.5.1) jaro_winkler (~> 1.5.1)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.6) parser (>= 2.6)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7) unicode-display_width (>= 1.4.0, < 1.7)
rubocop-rails (2.3.2) rubocop-rails (2.4.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 0.72.0) rubocop (>= 0.72.0)
ruby-progressbar (1.10.1) ruby-progressbar (1.10.1)
@ -592,7 +592,7 @@ GEM
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 3) sidekiq (>= 3)
tilt (>= 1.4.0) tilt (>= 1.4.0)
sidekiq-unique-jobs (6.0.15) sidekiq-unique-jobs (6.0.18)
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 4.0, < 7.0) sidekiq (>= 4.0, < 7.0)
thor (~> 0) thor (~> 0)
@ -616,12 +616,12 @@ GEM
sshkit (1.20.0) sshkit (1.20.0)
net-scp (>= 1.1.2) net-scp (>= 1.1.2)
net-ssh (>= 2.8.0) net-ssh (>= 2.8.0)
stackprof (0.2.13) stackprof (0.2.14)
statsd-ruby (1.4.0) statsd-ruby (1.4.0)
stoplight (2.2.0) stoplight (2.2.0)
streamio-ffmpeg (3.0.2) streamio-ffmpeg (3.0.2)
multi_json (~> 1.8) multi_json (~> 1.8)
strong_migrations (0.4.2) strong_migrations (0.5.1)
activerecord (>= 5) activerecord (>= 5)
temple (0.8.1) temple (0.8.1)
terminal-table (1.8.0) terminal-table (1.8.0)
@ -635,11 +635,11 @@ GEM
tty-command (0.9.0) tty-command (0.9.0)
pastel (~> 0.7.0) pastel (~> 0.7.0)
tty-cursor (0.7.0) tty-cursor (0.7.0)
tty-prompt (0.19.0) tty-prompt (0.20.0)
necromancer (~> 0.5.0) necromancer (~> 0.5.0)
pastel (~> 0.7.0) pastel (~> 0.7.0)
tty-reader (~> 0.6.0) tty-reader (~> 0.7.0)
tty-reader (0.6.0) tty-reader (0.7.0)
tty-cursor (~> 0.7) tty-cursor (~> 0.7)
tty-screen (~> 0.7) tty-screen (~> 0.7)
wisper (~> 2.0.0) wisper (~> 2.0.0)
@ -661,17 +661,17 @@ GEM
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webpacker (4.2.0) webpacker (4.2.2)
activesupport (>= 4.2) activesupport (>= 4.2)
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 4.2) railties (>= 4.2)
webpush (0.3.8) webpush (0.3.8)
hkdf (~> 0.2) hkdf (~> 0.2)
jwt (~> 2.0) jwt (~> 2.0)
websocket-driver (0.7.0) websocket-driver (0.7.1)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3) websocket-extensions (0.1.4)
wisper (2.0.0) wisper (2.0.1)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
@ -683,7 +683,7 @@ DEPENDENCIES
active_record_query_trace (~> 1.7) active_record_query_trace (~> 1.7)
addressable (~> 2.7) addressable (~> 2.7)
annotate (~> 3.0) annotate (~> 3.0)
aws-sdk-s3 (~> 1.55) aws-sdk-s3 (~> 1.59)
better_errors (~> 2.5) better_errors (~> 2.5)
binding_of_caller (~> 0.7) binding_of_caller (~> 0.7)
blurhash (~> 0.1) blurhash (~> 0.1)
@ -697,7 +697,7 @@ DEPENDENCIES
capistrano-rbenv (~> 2.1) capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0) capistrano-yarn (~> 2.0)
capybara (~> 3.29) capybara (~> 3.29)
charlock_holmes (~> 0.7.6) charlock_holmes (~> 0.7.7)
chewy (~> 5.1) chewy (~> 5.1)
cld3 (~> 3.2.4) cld3 (~> 3.2.4)
climate_control (~> 0.2) climate_control (~> 0.2)
@ -710,8 +710,8 @@ DEPENDENCIES
discard (~> 1.1) discard (~> 1.1)
doorkeeper (~> 5.2) doorkeeper (~> 5.2)
dotenv-rails (~> 2.7) dotenv-rails (~> 2.7)
fabrication (~> 2.20) fabrication (~> 2.21)
faker (~> 2.7) faker (~> 2.9)
fast_blank (~> 1.0) fast_blank (~> 1.0)
fastimage fastimage
fog-core (<= 2.1.0) fog-core (<= 2.1.0)
@ -741,11 +741,11 @@ DEPENDENCIES
memory_profiler memory_profiler
microformats (~> 4.1) microformats (~> 4.1)
mime-types (~> 3.3) mime-types (~> 3.3)
net-ldap (~> 0.10) net-ldap (~> 0.16)
nilsimsa! nilsimsa!
nokogiri (~> 1.10) nokogiri (~> 1.10)
nsa (~> 0.2) nsa (~> 0.2)
oj (~> 3.9) oj (~> 3.10)
omniauth (~> 1.9) omniauth (~> 1.9)
omniauth-cas (~> 1.1) omniauth-cas (~> 1.1)
omniauth-saml (~> 1.10) omniauth-saml (~> 1.10)
@ -753,22 +753,22 @@ DEPENDENCIES
ox (~> 2.11) ox (~> 2.11)
paperclip (~> 6.0) paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6) paperclip-av-transcoder (~> 0.6)
parallel (~> 1.18) parallel (~> 1.19)
parallel_tests (~> 2.29) parallel_tests (~> 2.30)
parslet parslet
pg (~> 1.1) pg (~> 1.1)
pghero (~> 2.3) pghero (~> 2.4)
pkg-config (~> 1.4) pkg-config (~> 1.4)
posix-spawn! posix-spawn!
premailer-rails premailer-rails
private_address_check (~> 0.5) private_address_check (~> 0.5)
pry-byebug (~> 3.7) pry-byebug (~> 3.7)
pry-rails (~> 0.3) pry-rails (~> 0.3)
puma (~> 4.2) puma (~> 4.3)
pundit (~> 2.1) pundit (~> 2.1)
rack-attack (~> 6.2) rack-attack (~> 6.2)
rack-cors (~> 1.0) rack-cors (~> 1.1)
rails (~> 5.2.3) rails (~> 5.2.4)
rails-controller-testing (~> 1.0) rails-controller-testing (~> 1.0)
rails-i18n (~> 5.1) rails-i18n (~> 5.1)
rails-settings-cached (~> 0.6) rails-settings-cached (~> 0.6)
@ -780,8 +780,8 @@ DEPENDENCIES
rqrcode (~> 0.10) rqrcode (~> 0.10)
rspec-rails (~> 3.9) rspec-rails (~> 3.9)
rspec-sidekiq (~> 3.0) rspec-sidekiq (~> 3.0)
rubocop (~> 0.76) rubocop (~> 0.78)
rubocop-rails (~> 2.3) rubocop-rails (~> 2.4)
ruby-progressbar (~> 1.10) ruby-progressbar (~> 1.10)
sanitize (~> 5.1) sanitize (~> 5.1)
sidekiq (~> 5.2) sidekiq (~> 5.2)
@ -791,14 +791,15 @@ DEPENDENCIES
simple-navigation (~> 4.1) simple-navigation (~> 4.1)
simple_form (~> 5.0) simple_form (~> 5.0)
simplecov (~> 0.17) simplecov (~> 0.17)
sprockets (~> 3.7)
sprockets-rails (~> 3.2) sprockets-rails (~> 3.2)
stackprof stackprof
stoplight (~> 2.2.0) stoplight (~> 2.2.0)
streamio-ffmpeg (~> 3.0) streamio-ffmpeg (~> 3.0)
strong_migrations (~> 0.4) strong_migrations (~> 0.5)
thor (~> 0.20) thor (~> 0.20)
tty-command (~> 0.9) tty-command (~> 0.9)
tty-prompt (~> 0.19) tty-prompt (~> 0.20)
twitter-text (~> 1.14) twitter-text (~> 1.14)
tzinfo-data (~> 1.2019) tzinfo-data (~> 1.2019)
webmock (~> 3.7) webmock (~> 3.7)

View File

@ -72,7 +72,7 @@ Mastodon acts as an OAuth2 provider so 3rd party apps can use the REST and Strea
- **Ruby** 2.4+ - **Ruby** 2.4+
- **Node.js** 8+ - **Node.js** 8+
The repository includes deployment configurations for **Docker and docker-compose**, but also a few specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. The [**stand-alone** installation guide](https://docs.joinmastodon.org/administration/installation/) is available in the documentation. The repository includes deployment configurations for **Docker and docker-compose**, but also a few specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. The [**stand-alone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation.
A **Vagrant** configuration is included for development purposes. A **Vagrant** configuration is included for development purposes.

View File

@ -55,7 +55,8 @@ module Admin
params.permit( params.permit(
:account_id, :account_id,
:resolved, :resolved,
:target_account_id :target_account_id,
:by_target_domain
) )
end end

View File

@ -20,6 +20,10 @@ class Api::BaseController < ApplicationController
render json: { error: e.to_s }, status: 422 render json: { error: e.to_s }, status: 422
end end
rescue_from ActiveRecord::RecordNotUnique do
render json: { error: 'Duplicate record' }, status: 422
end
rescue_from ActiveRecord::RecordNotFound do rescue_from ActiveRecord::RecordNotFound do
render json: { error: 'Record not found' }, status: 404 render json: { error: 'Record not found' }, status: 404
end end

View File

@ -3,6 +3,8 @@
class Api::ProofsController < Api::BaseController class Api::ProofsController < Api::BaseController
include AccountOwnedConcern include AccountOwnedConcern
skip_before_action :require_authenticated_user!
before_action :set_provider before_action :set_provider
def index def index

View File

@ -51,6 +51,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
def data_params def data_params
return {} if params[:data].blank? return {} if params[:data].blank?
params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention, :poll]) params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
end end
end end

View File

@ -19,6 +19,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
data = { data = {
alerts: { alerts: {
follow: alerts_enabled, follow: alerts_enabled,
follow_request: false,
favourite: alerts_enabled, favourite: alerts_enabled,
reblog: alerts_enabled, reblog: alerts_enabled,
mention: alerts_enabled, mention: alerts_enabled,
@ -58,6 +59,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
end end
def data_params def data_params
@data_params ||= params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention, :poll]) @data_params ||= params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
end end
end end

View File

@ -136,6 +136,6 @@ class ApplicationController < ActionController::Base
end end
def respond_with_error(code) def respond_with_error(code)
render "errors/#{code}", layout: 'error', status: code render "errors/#{code}", layout: 'error', status: code, formats: [:html]
end end
end end

View File

@ -62,6 +62,8 @@ module AccountsHelper
def account_badge(account, all: false) def account_badge(account, all: false)
if account.bot? if account.bot?
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles') content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
elsif account.group?
content_tag(:div, content_tag(:div, t('accounts.roles.group'), class: 'account-role group'), class: 'roles')
elsif (Setting.show_staff_badge && account.user_staff?) || all elsif (Setting.show_staff_badge && account.user_staff?) || all
content_tag(:div, class: 'roles') do content_tag(:div, class: 'roles') do
if all && !account.user_staff? if all && !account.user_staff?

View File

@ -2,7 +2,7 @@
module Admin::FilterHelper module Admin::FilterHelper
ACCOUNT_FILTERS = %i(local remote by_domain active pending silenced suspended username display_name email ip staff).freeze ACCOUNT_FILTERS = %i(local remote by_domain active pending silenced suspended username display_name email ip staff).freeze
REPORT_FILTERS = %i(resolved account_id target_account_id).freeze REPORT_FILTERS = %i(resolved account_id target_account_id by_target_domain).freeze
INVITE_FILTER = %i(available expired).freeze INVITE_FILTER = %i(available expired).freeze
CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
TAGS_FILTERS = %i(directory reviewed unreviewed pending_review popular active name).freeze TAGS_FILTERS = %i(directory reviewed unreviewed pending_review popular active name).freeze

View File

@ -236,7 +236,7 @@ export function uploadCompose(files) {
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total)); dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
}, },
}).then(({ data }) => dispatch(uploadComposeSuccess(data, f))); }).then(({ data }) => dispatch(uploadComposeSuccess(data, f)));
}).catch(error => dispatch(uploadComposeFail(error, true))); }).catch(error => dispatch(uploadComposeFail(error)));
}; };
}; };
}; };
@ -267,11 +267,10 @@ export function changeUploadComposeSuccess(media) {
}; };
}; };
export function changeUploadComposeFail(error, decrement = false) { export function changeUploadComposeFail(error) {
return { return {
type: COMPOSE_UPLOAD_CHANGE_FAIL, type: COMPOSE_UPLOAD_CHANGE_FAIL,
error: error, error: error,
decrement: decrement,
skipLoading: true, skipLoading: true,
}; };
}; };

View File

@ -110,7 +110,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
const excludeTypesFromFilter = filter => { const excludeTypesFromFilter = filter => {
const allTypes = ImmutableList(['follow', 'favourite', 'reblog', 'mention', 'poll']); const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll']);
return allTypes.filterNot(item => item === filter).toJS(); return allTypes.filterNot(item => item === filter).toJS();
}; };

View File

@ -56,15 +56,21 @@ export default class ModalRoot extends React.PureComponent {
} else if (!nextProps.children) { } else if (!nextProps.children) {
this.setState({ revealed: false }); this.setState({ revealed: false });
} }
if (!nextProps.children && !!this.props.children) {
this.activeElement.focus();
this.activeElement = null;
}
} }
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
if (!this.props.children && !!prevProps.children) { if (!this.props.children && !!prevProps.children) {
this.getSiblings().forEach(sibling => sibling.removeAttribute('inert')); this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
// Because of the wicg-inert polyfill, the activeElement may not be
// immediately selectable, we have to wait for observers to run, as
// described in https://github.com/WICG/inert#performance-and-gotchas
Promise.resolve().then(() => {
this.activeElement.focus();
this.activeElement = null;
}).catch((error) => {
console.error(error);
});
} }
if (this.props.children) { if (this.props.children) {
requestAnimationFrame(() => { requestAnimationFrame(() => {

View File

@ -67,9 +67,7 @@ class Poll extends ImmutablePureComponent {
} }
} }
handleOptionChange = e => { _toggleOption = value => {
const { target: { value } } = e;
if (this.props.poll.get('multiple')) { if (this.props.poll.get('multiple')) {
const tmp = { ...this.state.selected }; const tmp = { ...this.state.selected };
if (tmp[value]) { if (tmp[value]) {
@ -83,8 +81,20 @@ class Poll extends ImmutablePureComponent {
tmp[value] = true; tmp[value] = true;
this.setState({ selected: tmp }); this.setState({ selected: tmp });
} }
}
handleOptionChange = ({ target: { value } }) => {
this._toggleOption(value);
}; };
handleOptionKeyPress = (e) => {
if (e.key === 'Enter' || e.key === ' ') {
this._toggleOption(e.target.getAttribute('data-index'));
e.stopPropagation();
e.preventDefault();
}
}
handleVote = () => { handleVote = () => {
if (this.props.disabled) { if (this.props.disabled) {
return; return;
@ -135,7 +145,17 @@ class Poll extends ImmutablePureComponent {
disabled={disabled} disabled={disabled}
/> />
{!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />} {!showResults && (
<span
className={classNames('poll__input', { checkbox: poll.get('multiple'), active })}
tabIndex='0'
role={poll.get('multiple') ? 'checkbox' : 'radio'}
onKeyPress={this.handleOptionKeyPress}
aria-checked={active}
aria-label={option.get('title')}
data-index={optionIndex}
/>
)}
{showResults && <span className='poll__number'> {showResults && <span className='poll__number'>
{!!voted && <Icon id='check' className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />} {!!voted && <Icon id='check' className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />}
{Math.round(percent)}% {Math.round(percent)}%

View File

@ -214,6 +214,23 @@ class Status extends ImmutablePureComponent {
this.props.onOpenVideo(media, startTime); this.props.onOpenVideo(media, startTime);
} }
handleHotkeyOpenMedia = e => {
const { onOpenMedia, onOpenVideo } = this.props;
const status = this._properStatus();
e.preventDefault();
if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
// TODO: toggle play/paused?
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
onOpenVideo(status.getIn(['media_attachments', 0]), 0);
} else {
onOpenMedia(status.get('media_attachments'), 0);
}
}
}
handleHotkeyReply = e => { handleHotkeyReply = e => {
e.preventDefault(); e.preventDefault();
this.props.onReply(this._properStatus(), this.context.router.history); this.props.onReply(this._properStatus(), this.context.router.history);
@ -293,6 +310,7 @@ class Status extends ImmutablePureComponent {
moveDown: this.handleHotkeyMoveDown, moveDown: this.handleHotkeyMoveDown,
toggleHidden: this.handleHotkeyToggleHidden, toggleHidden: this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive, toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia,
}; };
if (hidden) { if (hidden) {

View File

@ -181,9 +181,9 @@ class StatusActionBar extends ImmutablePureComponent {
const account = status.get('account'); const account = status.get('account');
if (relationship && relationship.get('blocking')) { if (relationship && relationship.get('blocking')) {
onBlock(status);
} else {
onUnblock(account); onUnblock(account);
} else {
onBlock(status);
} }
} }

View File

@ -238,9 +238,18 @@ class Header extends ImmutablePureComponent {
const content = { __html: account.get('note_emojified') }; const content = { __html: account.get('note_emojified') };
const displayNameHtml = { __html: account.get('display_name_html') }; const displayNameHtml = { __html: account.get('display_name_html') };
const fields = account.get('fields'); const fields = account.get('fields');
const badge = account.get('bot') ? (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>) : null;
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
let badge;
if (account.get('bot')) {
badge = (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>);
} else if (account.get('group')) {
badge = (<div className='account-role group'><FormattedMessage id='account.badges.group' defaultMessage='Group' /></div>);
} else {
badge = null;
}
return ( return (
<div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}> <div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
<div className='account__header__image'> <div className='account__header__image'>

View File

@ -13,6 +13,8 @@ const messages = defineMessages({
add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' }, add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' },
remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' }, remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' },
poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' }, poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' },
switchToMultiple: { id: 'compose_form.poll.switch_to_multiple', defaultMessage: 'Change poll to allow multiple choices' },
switchToSingle: { id: 'compose_form.poll.switch_to_single', defaultMessage: 'Change poll to allow for a single choice' },
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' }, minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' }, hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' }, days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
@ -50,6 +52,12 @@ class Option extends React.PureComponent {
e.stopPropagation(); e.stopPropagation();
}; };
handleCheckboxKeypress = e => {
if (e.key === 'Enter' || e.key === ' ') {
this.handleToggleMultiple(e);
}
}
onSuggestionsClearRequested = () => { onSuggestionsClearRequested = () => {
this.props.onClearSuggestions(); this.props.onClearSuggestions();
} }
@ -71,8 +79,11 @@ class Option extends React.PureComponent {
<span <span
className={classNames('poll__input', { checkbox: isPollMultiple })} className={classNames('poll__input', { checkbox: isPollMultiple })}
onClick={this.handleToggleMultiple} onClick={this.handleToggleMultiple}
onKeyPress={this.handleCheckboxKeypress}
role='button' role='button'
tabIndex='0' tabIndex='0'
title={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
aria-label={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
/> />
<AutosuggestInput <AutosuggestInput
@ -144,7 +155,7 @@ class PollForm extends ImmutablePureComponent {
<div className='poll__footer'> <div className='poll__footer'>
<button disabled={options.size >= 4} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button> <button disabled={options.size >= 4} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>
<select value={expiresIn} onBlur={this.handleSelectDuration}> <select value={expiresIn} onChange={this.handleSelectDuration}>
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option> <option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option> <option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
<option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option> <option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>

View File

@ -12,6 +12,7 @@ import IconButton from 'mastodon/components/icon_button';
import RelativeTimestamp from 'mastodon/components/relative_timestamp'; import RelativeTimestamp from 'mastodon/components/relative_timestamp';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import { autoPlayGif } from 'mastodon/initial_state'; import { autoPlayGif } from 'mastodon/initial_state';
import classNames from 'classnames';
const messages = defineMessages({ const messages = defineMessages({
more: { id: 'status.more', defaultMessage: 'More' }, more: { id: 'status.more', defaultMessage: 'More' },
@ -158,7 +159,7 @@ class Conversation extends ImmutablePureComponent {
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div className='conversation focusable muted' tabIndex='0'> <div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex='0'>
<div className='conversation__avatar'> <div className='conversation__avatar'>
<AvatarComposite accounts={accounts} size={48} /> <AvatarComposite accounts={accounts} size={48} />
</div> </div>
@ -166,7 +167,7 @@ class Conversation extends ImmutablePureComponent {
<div className='conversation__content'> <div className='conversation__content'>
<div className='conversation__content__info'> <div className='conversation__content__info'>
<div className='conversation__content__relative-time'> <div className='conversation__content__relative-time'>
<RelativeTimestamp timestamp={lastStatus.get('created_at')} /> {unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
</div> </div>
<div className='conversation__content__names' ref={this.setNamesRef}> <div className='conversation__content__names' ref={this.setNamesRef}>

View File

@ -56,6 +56,10 @@ class KeyboardShortcuts extends ImmutablePureComponent {
<td><kbd>enter</kbd>, <kbd>o</kbd></td> <td><kbd>enter</kbd>, <kbd>o</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open status' /></td> <td><FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open status' /></td>
</tr> </tr>
<tr>
<td><kbd>e</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.open_media' defaultMessage='to open media' /></td>
</tr>
<tr> <tr>
<td><kbd>x</kbd></td> <td><kbd>x</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td> <td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td>

View File

@ -57,6 +57,17 @@ export default class ColumnSettings extends React.PureComponent {
</div> </div>
</div> </div>
<div role='group' aria-labelledby='notifications-follow-request'>
<span id='notifications-follow-request' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow_request' defaultMessage='New follow requests:' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow_request']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow_request']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow_request']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow_request']} onChange={onChange} label={soundStr} />
</div>
</div>
<div role='group' aria-labelledby='notifications-favourite'> <div role='group' aria-labelledby='notifications-favourite'>
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span> <span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>

View File

@ -0,0 +1,59 @@
import React, { Fragment } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from 'mastodon/components/avatar';
import DisplayName from 'mastodon/components/display_name';
import Permalink from 'mastodon/components/permalink';
import IconButton from 'mastodon/components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
});
export default @injectIntl
class FollowRequest extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
onAuthorize: PropTypes.func.isRequired,
onReject: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
render () {
const { intl, hidden, account, onAuthorize, onReject } = this.props;
if (!account) {
return <div />;
}
if (hidden) {
return (
<Fragment>
{account.get('display_name')}
{account.get('username')}
</Fragment>
);
}
return (
<div className='account'>
<div className='account__wrapper'>
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
<DisplayName account={account} />
</Permalink>
<div className='account__relationship'>
<IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} />
<IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} />
</div>
</div>
</div>
);
}
}

View File

@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from 'mastodon/initial_state'; import { me } from 'mastodon/initial_state';
import StatusContainer from 'mastodon/containers/status_container'; import StatusContainer from 'mastodon/containers/status_container';
import AccountContainer from 'mastodon/containers/account_container'; import AccountContainer from 'mastodon/containers/account_container';
import FollowRequestContainer from '../containers/follow_request_container';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import Permalink from 'mastodon/components/permalink'; import Permalink from 'mastodon/components/permalink';
@ -127,7 +128,29 @@ class Notification extends ImmutablePureComponent {
</span> </span>
</div> </div>
<AccountContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} /> <AccountContainer id={account.get('id')} hidden={this.props.hidden} />
</div>
</HotKeys>
);
}
renderFollowRequest (notification, account, link) {
const { intl } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<div className='notification notification-follow-request focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow_request', defaultMessage: '{name} has requested to follow you' }, { name: account.get('acct') }), notification.get('created_at'))}>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='user' fixedWidth />
</div>
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.follow_request' defaultMessage='{name} has requested to follow you' values={{ name: link }} />
</span>
</div>
<FollowRequestContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} />
</div> </div>
</HotKeys> </HotKeys>
); );
@ -261,6 +284,8 @@ class Notification extends ImmutablePureComponent {
switch(notification.get('type')) { switch(notification.get('type')) {
case 'follow': case 'follow':
return this.renderFollow(notification, account, link); return this.renderFollow(notification, account, link);
case 'follow_request':
return this.renderFollowRequest(notification, account, link);
case 'mention': case 'mention':
return this.renderMention(notification); return this.renderMention(notification);
case 'favourite': case 'favourite':

View File

@ -0,0 +1,26 @@
import { connect } from 'react-redux';
import { makeGetAccount } from 'mastodon/selectors';
import FollowRequest from '../components/follow_request';
import { authorizeFollowRequest, rejectFollowRequest } from 'mastodon/actions/accounts';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, props) => ({
account: getAccount(state, props.id),
});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { id }) => ({
onAuthorize () {
dispatch(authorizeFollowRequest(id));
},
onReject () {
dispatch(rejectFollowRequest(id));
},
});
export default connect(makeMapStateToProps, mapDispatchToProps)(FollowRequest);

View File

@ -120,9 +120,9 @@ class ActionBar extends React.PureComponent {
const account = status.get('account'); const account = status.get('account');
if (relationship && relationship.get('blocking')) { if (relationship && relationship.get('blocking')) {
onBlock(status);
} else {
onUnblock(account); onUnblock(account);
} else {
onBlock(status);
} }
} }

View File

@ -281,6 +281,22 @@ class Status extends ImmutablePureComponent {
this.props.dispatch(openModal('VIDEO', { media, time })); this.props.dispatch(openModal('VIDEO', { media, time }));
} }
handleHotkeyOpenMedia = e => {
const status = this._properStatus();
e.preventDefault();
if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
// TODO: toggle play/paused?
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
this.handleOpenVideo(status.getIn(['media_attachments', 0]), 0);
} else {
this.handleOpenMedia(status.get('media_attachments'), 0);
}
}
}
handleMuteClick = (account) => { handleMuteClick = (account) => {
this.props.dispatch(initMuteModal(account)); this.props.dispatch(initMuteModal(account));
} }
@ -506,6 +522,7 @@ class Status extends ImmutablePureComponent {
openProfile: this.handleHotkeyOpenProfile, openProfile: this.handleHotkeyOpenProfile,
toggleHidden: this.handleHotkeyToggleHidden, toggleHidden: this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive, toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia,
}; };
return ( return (

View File

@ -4,12 +4,10 @@ import { fetchFollowRequests } from 'mastodon/actions/accounts';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { NavLink, withRouter } from 'react-router-dom'; import { NavLink, withRouter } from 'react-router-dom';
import IconWithBadge from 'mastodon/components/icon_with_badge'; import IconWithBadge from 'mastodon/components/icon_with_badge';
import { me } from 'mastodon/initial_state';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
locked: state.getIn(['accounts', me, 'locked']),
count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size, count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
}); });
@ -19,22 +17,19 @@ class FollowRequestsNavLink extends React.Component {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
locked: PropTypes.bool,
count: PropTypes.number.isRequired, count: PropTypes.number.isRequired,
}; };
componentDidMount () { componentDidMount () {
const { dispatch, locked } = this.props; const { dispatch } = this.props;
if (locked) {
dispatch(fetchFollowRequests()); dispatch(fetchFollowRequests());
} }
}
render () { render () {
const { locked, count } = this.props; const { count } = this.props;
if (!locked || count === 0) { if (count === 0) {
return null; return null;
} }

View File

@ -100,6 +100,7 @@ const keyMap = {
goToRequests: 'g r', goToRequests: 'g r',
toggleHidden: 'x', toggleHidden: 'x',
toggleSensitive: 'h', toggleSensitive: 'h',
openMedia: 'e',
}; };
class SwitchingColumnsArea extends React.PureComponent { class SwitchingColumnsArea extends React.PureComponent {

View File

@ -467,7 +467,7 @@ class Video extends React.PureComponent {
<div className='video-player__buttons-bar'> <div className='video-player__buttons-bar'>
<div className='video-player__buttons left'> <div className='video-player__buttons left'>
<button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}> <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>

View File

@ -398,6 +398,14 @@
"defaultMessage": "Favourite", "defaultMessage": "Favourite",
"id": "status.favourite" "id": "status.favourite"
}, },
{
"defaultMessage": "Bookmark",
"id": "status.bookmark"
},
{
"defaultMessage": "Remove bookmark",
"id": "status.remove_bookmark"
},
{ {
"defaultMessage": "Expand this status", "defaultMessage": "Expand this status",
"id": "status.open" "id": "status.open"
@ -437,6 +445,22 @@
{ {
"defaultMessage": "Copy link to status", "defaultMessage": "Copy link to status",
"id": "status.copy" "id": "status.copy"
},
{
"defaultMessage": "Hide everything from {domain}",
"id": "account.block_domain"
},
{
"defaultMessage": "Unhide {domain}",
"id": "account.unblock_domain"
},
{
"defaultMessage": "Unmute @{name}",
"id": "account.unmute"
},
{
"defaultMessage": "Unblock @{name}",
"id": "account.unblock"
} }
], ],
"path": "app/javascript/mastodon/components/status_action_bar.json" "path": "app/javascript/mastodon/components/status_action_bar.json"
@ -530,6 +554,14 @@
{ {
"defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"id": "confirmations.reply.message" "id": "confirmations.reply.message"
},
{
"defaultMessage": "Hide entire domain",
"id": "confirmations.domain_block.confirm"
},
{
"defaultMessage": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
"id": "confirmations.domain_block.message"
} }
], ],
"path": "app/javascript/mastodon/containers/status_container.json" "path": "app/javascript/mastodon/containers/status_container.json"
@ -797,6 +829,19 @@
], ],
"path": "app/javascript/mastodon/features/blocks/index.json" "path": "app/javascript/mastodon/features/blocks/index.json"
}, },
{
"descriptors": [
{
"defaultMessage": "Bookmarks",
"id": "column.bookmarks"
},
{
"defaultMessage": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.",
"id": "empty_column.bookmarked_statuses"
}
],
"path": "app/javascript/mastodon/features/bookmarked_statuses/index.json"
},
{ {
"descriptors": [ "descriptors": [
{ {
@ -1528,6 +1573,10 @@
"defaultMessage": "Direct messages", "defaultMessage": "Direct messages",
"id": "navigation_bar.direct" "id": "navigation_bar.direct"
}, },
{
"defaultMessage": "Bookmarks",
"id": "navigation_bar.bookmarks"
},
{ {
"defaultMessage": "Preferences", "defaultMessage": "Preferences",
"id": "navigation_bar.preferences" "id": "navigation_bar.preferences"
@ -1778,6 +1827,10 @@
"defaultMessage": "to open status", "defaultMessage": "to open status",
"id": "keyboard_shortcuts.enter" "id": "keyboard_shortcuts.enter"
}, },
{
"defaultMessage": "to open media",
"id": "keyboard_shortcuts.open_media"
},
{ {
"defaultMessage": "to show/hide text behind CW", "defaultMessage": "to show/hide text behind CW",
"id": "keyboard_shortcuts.toggle_hidden" "id": "keyboard_shortcuts.toggle_hidden"
@ -2028,6 +2081,10 @@
"defaultMessage": "New followers:", "defaultMessage": "New followers:",
"id": "notifications.column_settings.follow" "id": "notifications.column_settings.follow"
}, },
{
"defaultMessage": "New follow requests:",
"id": "notifications.column_settings.follow_request"
},
{ {
"defaultMessage": "Favourites:", "defaultMessage": "Favourites:",
"id": "notifications.column_settings.favourite" "id": "notifications.column_settings.favourite"
@ -2076,6 +2133,19 @@
], ],
"path": "app/javascript/mastodon/features/notifications/components/filter_bar.json" "path": "app/javascript/mastodon/features/notifications/components/filter_bar.json"
}, },
{
"descriptors": [
{
"defaultMessage": "Authorize",
"id": "follow_request.authorize"
},
{
"defaultMessage": "Reject",
"id": "follow_request.reject"
}
],
"path": "app/javascript/mastodon/features/notifications/components/follow_request.json"
},
{ {
"descriptors": [ "descriptors": [
{ {
@ -2097,6 +2167,10 @@
{ {
"defaultMessage": "{name} boosted your status", "defaultMessage": "{name} boosted your status",
"id": "notification.reblog" "id": "notification.reblog"
},
{
"defaultMessage": "{name} has requested to follow you",
"id": "notification.follow_request"
} }
], ],
"path": "app/javascript/mastodon/features/notifications/components/notification.json" "path": "app/javascript/mastodon/features/notifications/components/notification.json"
@ -2204,6 +2278,10 @@
"defaultMessage": "Favourite", "defaultMessage": "Favourite",
"id": "status.favourite" "id": "status.favourite"
}, },
{
"defaultMessage": "Bookmark",
"id": "status.bookmark"
},
{ {
"defaultMessage": "Mute @{name}", "defaultMessage": "Mute @{name}",
"id": "status.mute" "id": "status.mute"
@ -2251,6 +2329,22 @@
{ {
"defaultMessage": "Copy link to status", "defaultMessage": "Copy link to status",
"id": "status.copy" "id": "status.copy"
},
{
"defaultMessage": "Hide everything from {domain}",
"id": "account.block_domain"
},
{
"defaultMessage": "Unhide {domain}",
"id": "account.unblock_domain"
},
{
"defaultMessage": "Unmute @{name}",
"id": "account.unmute"
},
{
"defaultMessage": "Unblock @{name}",
"id": "account.unblock"
} }
], ],
"path": "app/javascript/mastodon/features/status/components/action_bar.json" "path": "app/javascript/mastodon/features/status/components/action_bar.json"
@ -2321,6 +2415,14 @@
{ {
"defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"id": "confirmations.reply.message" "id": "confirmations.reply.message"
},
{
"defaultMessage": "Hide entire domain",
"id": "confirmations.domain_block.confirm"
},
{
"defaultMessage": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
"id": "confirmations.domain_block.message"
} }
], ],
"path": "app/javascript/mastodon/features/status/index.json" "path": "app/javascript/mastodon/features/status/index.json"
@ -2459,6 +2561,18 @@
"defaultMessage": "A quick brown fox jumps over the lazy dog", "defaultMessage": "A quick brown fox jumps over the lazy dog",
"id": "upload_modal.description_placeholder" "id": "upload_modal.description_placeholder"
}, },
{
"defaultMessage": "Describe for people with hearing loss",
"id": "upload_form.audio_description"
},
{
"defaultMessage": "Describe for people with hearing loss or visual impairment",
"id": "upload_form.video_description"
},
{
"defaultMessage": "Describe for the visually impaired",
"id": "upload_form.description"
},
{ {
"defaultMessage": "Edit media", "defaultMessage": "Edit media",
"id": "upload_modal.edit_media" "id": "upload_modal.edit_media"
@ -2467,10 +2581,6 @@
"defaultMessage": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.", "defaultMessage": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
"id": "upload_modal.hint" "id": "upload_modal.hint"
}, },
{
"defaultMessage": "Describe for the visually impaired",
"id": "upload_form.description"
},
{ {
"defaultMessage": "Analyzing picture…", "defaultMessage": "Analyzing picture…",
"id": "upload_modal.analyzing_picture" "id": "upload_modal.analyzing_picture"
@ -2620,6 +2730,10 @@
"defaultMessage": "Favourites", "defaultMessage": "Favourites",
"id": "navigation_bar.favourites" "id": "navigation_bar.favourites"
}, },
{
"defaultMessage": "Bookmarks",
"id": "navigation_bar.bookmarks"
},
{ {
"defaultMessage": "Lists", "defaultMessage": "Lists",
"id": "navigation_bar.lists" "id": "navigation_bar.lists"

View File

@ -51,6 +51,7 @@
"bundle_modal_error.message": "Something went wrong while loading this component.", "bundle_modal_error.message": "Something went wrong while loading this component.",
"bundle_modal_error.retry": "Try again", "bundle_modal_error.retry": "Try again",
"column.blocks": "Blocked users", "column.blocks": "Blocked users",
"column.bookmarks": "Bookmarks",
"column.community": "Local timeline", "column.community": "Local timeline",
"column.direct": "Direct messages", "column.direct": "Direct messages",
"column.directory": "Browse profiles", "column.directory": "Browse profiles",
@ -138,6 +139,7 @@
"empty_column.account_timeline": "No toots here!", "empty_column.account_timeline": "No toots here!",
"empty_column.account_unavailable": "Profile unavailable", "empty_column.account_unavailable": "Profile unavailable",
"empty_column.blocks": "You haven't blocked any users yet.", "empty_column.blocks": "You haven't blocked any users yet.",
"empty_column.bookmarked_statuses": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
"empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
"empty_column.domain_blocks": "There are no hidden domains yet.", "empty_column.domain_blocks": "There are no hidden domains yet.",
@ -219,6 +221,7 @@
"keyboard_shortcuts.muted": "to open muted users list", "keyboard_shortcuts.muted": "to open muted users list",
"keyboard_shortcuts.my_profile": "to open your profile", "keyboard_shortcuts.my_profile": "to open your profile",
"keyboard_shortcuts.notifications": "to open notifications column", "keyboard_shortcuts.notifications": "to open notifications column",
"keyboard_shortcuts.open_media": "to open media",
"keyboard_shortcuts.pinned": "to open pinned toots list", "keyboard_shortcuts.pinned": "to open pinned toots list",
"keyboard_shortcuts.profile": "to open author's profile", "keyboard_shortcuts.profile": "to open author's profile",
"keyboard_shortcuts.reply": "to reply", "keyboard_shortcuts.reply": "to reply",
@ -251,6 +254,7 @@
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_notifications": "Hide notifications from this user?",
"navigation_bar.apps": "Mobile apps", "navigation_bar.apps": "Mobile apps",
"navigation_bar.blocks": "Blocked users", "navigation_bar.blocks": "Blocked users",
"navigation_bar.bookmarks": "Bookmarks",
"navigation_bar.community_timeline": "Local timeline", "navigation_bar.community_timeline": "Local timeline",
"navigation_bar.compose": "Compose new toot", "navigation_bar.compose": "Compose new toot",
"navigation_bar.direct": "Direct messages", "navigation_bar.direct": "Direct messages",
@ -273,6 +277,7 @@
"navigation_bar.security": "Security", "navigation_bar.security": "Security",
"notification.favourite": "{name} favourited your status", "notification.favourite": "{name} favourited your status",
"notification.follow": "{name} followed you", "notification.follow": "{name} followed you",
"notification.follow_request": "{name} has requested to follow you",
"notification.mention": "{name} mentioned you", "notification.mention": "{name} mentioned you",
"notification.own_poll": "Your poll has ended", "notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you have voted in has ended", "notification.poll": "A poll you have voted in has ended",
@ -285,6 +290,7 @@
"notifications.column_settings.filter_bar.category": "Quick filter bar", "notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show", "notifications.column_settings.filter_bar.show": "Show",
"notifications.column_settings.follow": "New followers:", "notifications.column_settings.follow": "New followers:",
"notifications.column_settings.follow_request": "New follow requests:",
"notifications.column_settings.mention": "Mentions:", "notifications.column_settings.mention": "Mentions:",
"notifications.column_settings.poll": "Poll results:", "notifications.column_settings.poll": "Poll results:",
"notifications.column_settings.push": "Push notifications", "notifications.column_settings.push": "Push notifications",
@ -345,6 +351,7 @@
"status.admin_account": "Open moderation interface for @{name}", "status.admin_account": "Open moderation interface for @{name}",
"status.admin_status": "Open this status in the moderation interface", "status.admin_status": "Open this status in the moderation interface",
"status.block": "Block @{name}", "status.block": "Block @{name}",
"status.bookmark": "Bookmark",
"status.cancel_reblog_private": "Unboost", "status.cancel_reblog_private": "Unboost",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "This post cannot be boosted",
"status.copy": "Copy link to status", "status.copy": "Copy link to status",
@ -369,6 +376,7 @@
"status.reblogged_by": "{name} boosted", "status.reblogged_by": "{name} boosted",
"status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.", "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.",
"status.redraft": "Delete & re-draft", "status.redraft": "Delete & re-draft",
"status.remove_bookmark": "Remove bookmark",
"status.reply": "Reply", "status.reply": "Reply",
"status.replyAll": "Reply to thread", "status.replyAll": "Reply to thread",
"status.report": "Report @{name}", "status.report": "Report @{name}",
@ -401,9 +409,11 @@
"upload_button.label": "Add media ({formats})", "upload_button.label": "Add media ({formats})",
"upload_error.limit": "File upload limit exceeded.", "upload_error.limit": "File upload limit exceeded.",
"upload_error.poll": "File upload not allowed with polls.", "upload_error.poll": "File upload not allowed with polls.",
"upload_form.audio_description": "Describe for people with hearing loss",
"upload_form.description": "Describe for the visually impaired", "upload_form.description": "Describe for the visually impaired",
"upload_form.edit": "Edit", "upload_form.edit": "Edit",
"upload_form.undo": "Delete", "upload_form.undo": "Delete",
"upload_form.video_description": "Describe for people with hearing loss or visual impairment",
"upload_modal.analyzing_picture": "Analyzing picture…", "upload_modal.analyzing_picture": "Analyzing picture…",
"upload_modal.apply": "Apply", "upload_modal.apply": "Apply",
"upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",

View File

@ -328,7 +328,7 @@ export default function compose(state = initialState, action) {
case COMPOSE_UPLOAD_SUCCESS: case COMPOSE_UPLOAD_SUCCESS:
return appendMedia(state, fromJS(action.media), action.file); return appendMedia(state, fromJS(action.media), action.file);
case COMPOSE_UPLOAD_FAIL: case COMPOSE_UPLOAD_FAIL:
return state.set('is_uploading', false).update('pending_media_attachments', n => action.decrement ? n - 1 : n); return state.set('is_uploading', false).update('pending_media_attachments', n => n - 1);
case COMPOSE_UPLOAD_UNDO: case COMPOSE_UPLOAD_UNDO:
return removeMedia(state, action.media_id); return removeMedia(state, action.media_id);
case COMPOSE_UPLOAD_PROGRESS: case COMPOSE_UPLOAD_PROGRESS:

View File

@ -13,6 +13,8 @@ import {
import { import {
ACCOUNT_BLOCK_SUCCESS, ACCOUNT_BLOCK_SUCCESS,
ACCOUNT_MUTE_SUCCESS, ACCOUNT_MUTE_SUCCESS,
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
FOLLOW_REQUEST_REJECT_SUCCESS,
} from '../actions/accounts'; } from '../actions/accounts';
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks'; import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines'; import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
@ -89,8 +91,8 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingRece
}); });
}; };
const filterNotifications = (state, accountIds) => { const filterNotifications = (state, accountIds, type) => {
const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account'))); const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account')) && (type === undefined || type === item.get('type')));
return state.update('items', helper).update('pendingItems', helper); return state.update('items', helper).update('pendingItems', helper);
}; };
@ -129,6 +131,11 @@ export default function notifications(state = initialState, action) {
return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state; return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state;
case DOMAIN_BLOCK_SUCCESS: case DOMAIN_BLOCK_SUCCESS:
return filterNotifications(state, action.accounts); return filterNotifications(state, action.accounts);
case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
case FOLLOW_REQUEST_REJECT_SUCCESS:
return filterNotifications(state, [action.id], 'follow_request');
case ACCOUNT_MUTE_SUCCESS:
return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state;
case NOTIFICATIONS_CLEAR: case NOTIFICATIONS_CLEAR:
return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false); return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false);
case TIMELINE_DELETE: case TIMELINE_DELETE:

View File

@ -6,6 +6,7 @@ const initialState = Immutable.Map({
subscription: null, subscription: null,
alerts: new Immutable.Map({ alerts: new Immutable.Map({
follow: false, follow: false,
follow_request: false,
favourite: false, favourite: false,
reblog: false, reblog: false,
mention: false, mention: false,

View File

@ -30,6 +30,7 @@ const initialState = ImmutableMap({
notifications: ImmutableMap({ notifications: ImmutableMap({
alerts: ImmutableMap({ alerts: ImmutableMap({
follow: true, follow: true,
follow_request: false,
favourite: true, favourite: true,
reblog: true, reblog: true,
mention: true, mention: true,
@ -44,6 +45,7 @@ const initialState = ImmutableMap({
shows: ImmutableMap({ shows: ImmutableMap({
follow: true, follow: true,
follow_request: false,
favourite: true, favourite: true,
reblog: true, reblog: true,
mention: true, mention: true,
@ -52,6 +54,7 @@ const initialState = ImmutableMap({
sounds: ImmutableMap({ sounds: ImmutableMap({
follow: true, follow: true,
follow_request: false,
favourite: true, favourite: true,
reblog: true, reblog: true,
mention: true, mention: true,

View File

@ -1,3 +1,6 @@
import {
NOTIFICATIONS_UPDATE,
} from '../actions/notifications';
import { import {
FOLLOWERS_FETCH_SUCCESS, FOLLOWERS_FETCH_SUCCESS,
FOLLOWERS_EXPAND_SUCCESS, FOLLOWERS_EXPAND_SUCCESS,
@ -53,6 +56,12 @@ const appendToList = (state, type, id, accounts, next) => {
}); });
}; };
const normalizeFollowRequest = (state, notification) => {
return state.updateIn(['follow_requests', 'items'], list => {
return list.filterNot(item => item === notification.account.id).unshift(notification.account.id);
});
};
export default function userLists(state = initialState, action) { export default function userLists(state = initialState, action) {
switch(action.type) { switch(action.type) {
case FOLLOWERS_FETCH_SUCCESS: case FOLLOWERS_FETCH_SUCCESS:
@ -67,6 +76,8 @@ export default function userLists(state = initialState, action) {
return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id))); return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
case FAVOURITES_FETCH_SUCCESS: case FAVOURITES_FETCH_SUCCESS:
return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id))); return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
case NOTIFICATIONS_UPDATE:
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
case FOLLOW_REQUESTS_FETCH_SUCCESS: case FOLLOW_REQUESTS_FETCH_SUCCESS:
return state.setIn(['follow_requests', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next); return state.setIn(['follow_requests', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
case FOLLOW_REQUESTS_EXPAND_SUCCESS: case FOLLOW_REQUESTS_EXPAND_SUCCESS:

View File

@ -16,6 +16,7 @@ filenames.forEach(filename => {
filtered[locale] = { filtered[locale] = {
'notification.favourite': full['notification.favourite'] || '', 'notification.favourite': full['notification.favourite'] || '',
'notification.follow': full['notification.follow'] || '', 'notification.follow': full['notification.follow'] || '',
'notification.follow_request': full['notification.follow_request'] || '',
'notification.mention': full['notification.mention'] || '', 'notification.mention': full['notification.mention'] || '',
'notification.reblog': full['notification.reblog'] || '', 'notification.reblog': full['notification.reblog'] || '',
'notification.poll': full['notification.poll'] || '', 'notification.poll': full['notification.poll'] || '',

View File

@ -1,4 +1,4 @@
import WebSocketClient from 'websocket.js'; import WebSocketClient from '@gamestdio/websocket';
const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max)); const randomIntUpTo = max => Math.floor(Math.random() * Math.floor(max));

View File

@ -45,7 +45,25 @@ const onDomainBlockSeverityChange = (target) => {
delegate(document, '#domain_block_severity', 'change', ({ target }) => onDomainBlockSeverityChange(target)); delegate(document, '#domain_block_severity', 'change', ({ target }) => onDomainBlockSeverityChange(target));
const onEnableBootstrapTimelineAccountsChange = (target) => {
const bootstrapTimelineAccountsField = document.querySelector('#form_admin_settings_bootstrap_timeline_accounts');
if (bootstrapTimelineAccountsField) {
bootstrapTimelineAccountsField.disabled = !target.checked;
if (target.checked) {
bootstrapTimelineAccountsField.parentElement.classList.remove('disabled');
} else {
bootstrapTimelineAccountsField.parentElement.classList.add('disabled');
}
}
};
delegate(document, '#form_admin_settings_enable_bootstrap_timeline_accounts', 'change', ({ target }) => onEnableBootstrapTimelineAccountsChange(target));
ready(() => { ready(() => {
const input = document.getElementById('domain_block_severity'); const domainBlockSeverityInput = document.getElementById('domain_block_severity');
if (input) onDomainBlockSeverityChange(input); if (domainBlockSeverityInput) onDomainBlockSeverityChange(domainBlockSeverityInput);
const enableBootstrapTimelineAccounts = document.getElementById('form_admin_settings_enable_bootstrap_timeline_accounts');
if (enableBootstrapTimelineAccounts) onEnableBootstrapTimelineAccountsChange(enableBootstrapTimelineAccounts);
}); });

View File

@ -181,18 +181,39 @@ $content-width: 840px;
padding-top: 30px; padding-top: 30px;
} }
&-heading {
display: flex;
padding-bottom: 40px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
margin-bottom: 40px;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
&-actions {
display: inline-flex;
& > * {
margin-left: 5px;
}
}
@media screen and (max-width: $no-columns-breakpoint) {
border-bottom: 0;
padding-bottom: 0;
}
}
h2 { h2 {
color: $secondary-text-color; color: $secondary-text-color;
font-size: 24px; font-size: 24px;
line-height: 28px; line-height: 28px;
font-weight: 400; font-weight: 400;
padding-bottom: 40px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
margin-bottom: 40px;
@media screen and (max-width: $no-columns-breakpoint) { @media screen and (max-width: $no-columns-breakpoint) {
border-bottom: 0;
padding-bottom: 0;
font-weight: 700; font-weight: 700;
} }
} }

View File

@ -2521,7 +2521,6 @@ a.account__display-name {
overflow-x: hidden; overflow-x: hidden;
flex: 1 1 auto; flex: 1 1 auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
will-change: transform; // improves perf in mobile Chrome
&.optionally-scrollable { &.optionally-scrollable {
overflow-y: auto; overflow-y: auto;
@ -6521,6 +6520,16 @@ noscript {
flex: 0 0 auto; flex: 0 0 auto;
padding: 10px; padding: 10px;
padding-top: 12px; padding-top: 12px;
position: relative;
}
&__unread {
display: inline-block;
background: $highlight-text-color;
border-radius: 50%;
width: 0.625rem;
height: 0.625rem;
margin: -.1ex .15em .1ex;
} }
&__content { &__content {
@ -6568,4 +6577,20 @@ noscript {
word-break: break-word; word-break: break-word;
} }
} }
&--unread {
background: lighten($ui-base-color, 2%);
&:focus {
background: lighten($ui-base-color, 4%);
}
.conversation__content__info {
font-weight: 700;
}
.conversation__content__relative-time {
color: $primary-text-color;
}
}
} }

View File

@ -646,7 +646,7 @@
} }
.counter { .counter {
width: 33.3%; min-width: 33.3%;
box-sizing: border-box; box-sizing: border-box;
flex: 0 0 auto; flex: 0 0 auto;
color: $darker-text-color; color: $darker-text-color;

View File

@ -91,6 +91,23 @@
border-color: $valid-value-color; border-color: $valid-value-color;
background: $valid-value-color; background: $valid-value-color;
} }
&:active,
&:focus,
&:hover {
border-width: 4px;
background: none;
}
&::-moz-focus-inner {
outline: 0 !important;
border: 0;
}
&:focus,
&:active {
outline: 0 !important;
}
} }
&__number { &__number {
@ -160,6 +177,10 @@
button, button,
select { select {
flex: 1 1 50%; flex: 1 1 50%;
&:focus {
border-color: $highlight-text-color;
}
} }
} }

View File

@ -5,7 +5,7 @@ class ActivityPub::Activity
include Redisable include Redisable
SUPPORTED_TYPES = %w(Note Question).freeze SUPPORTED_TYPES = %w(Note Question).freeze
CONVERTED_TYPES = %w(Image Audio Video Article Page).freeze CONVERTED_TYPES = %w(Image Audio Video Article Page Event).freeze
def initialize(json, account, **options) def initialize(json, account, **options)
@json = json @json = json
@ -89,7 +89,7 @@ class ActivityPub::Activity
def distribute(status) def distribute(status)
crawl_links(status) crawl_links(status)
notify_about_reblog(status) if reblog_of_local_account?(status) notify_about_reblog(status) if reblog_of_local_account?(status) && !reblog_by_following_group_account?(status)
notify_about_mentions(status) notify_about_mentions(status)
# Only continue if the status is supposed to have arrived in real-time. # Only continue if the status is supposed to have arrived in real-time.
@ -105,6 +105,10 @@ class ActivityPub::Activity
status.reblog? && status.reblog.account.local? status.reblog? && status.reblog.account.local?
end end
def reblog_by_following_group_account?(status)
status.reblog? && status.account.group? && status.reblog.account.following?(status.account)
end
def notify_about_reblog(status) def notify_about_reblog(status)
NotifyService.new.call(status.reblog.account, status) NotifyService.new.call(status.reblog.account, status)
end end

View File

@ -25,6 +25,14 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
private private
def audience_to
@object['to'] || @json['to']
end
def audience_cc
@object['cc'] || @json['cc']
end
def process_status def process_status
@tags = [] @tags = []
@mentions = [] @mentions = []
@ -75,7 +83,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end end
def process_audience def process_audience
(as_array(@object['to']) + as_array(@object['cc'])).uniq.each do |audience| (as_array(audience_to) + as_array(audience_cc)).uniq.each do |audience|
next if audience == ActivityPub::TagManager::COLLECTIONS[:public] next if audience == ActivityPub::TagManager::COLLECTIONS[:public]
# Unlike with tags, there is no point in resolving accounts we don't already # Unlike with tags, there is no point in resolving accounts we don't already
@ -149,7 +157,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
return if tag['name'].blank? return if tag['name'].blank?
Tag.find_or_create_by_names(tag['name']) do |hashtag| Tag.find_or_create_by_names(tag['name']) do |hashtag|
@tags << hashtag unless @tags.include?(hashtag) @tags << hashtag unless @tags.include?(hashtag) || !hashtag.valid?
end end
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid
nil nil
@ -159,7 +167,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
return if tag['href'].blank? return if tag['href'].blank?
account = account_from_uri(tag['href']) account = account_from_uri(tag['href'])
account = ::FetchRemoteAccountService.new.call(tag['href']) if account.nil? account = ActivityPub::FetchRemoteAccountService.new.call(tag['href']) if account.nil?
return if account.nil? return if account.nil?
@ -291,11 +299,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end end
def visibility_from_audience def visibility_from_audience
if equals_or_includes?(@object['to'], ActivityPub::TagManager::COLLECTIONS[:public]) if equals_or_includes?(audience_to, ActivityPub::TagManager::COLLECTIONS[:public])
:public :public
elsif equals_or_includes?(@object['cc'], ActivityPub::TagManager::COLLECTIONS[:public]) elsif equals_or_includes?(audience_cc, ActivityPub::TagManager::COLLECTIONS[:public])
:unlisted :unlisted
elsif equals_or_includes?(@object['to'], @account.followers_url) elsif equals_or_includes?(audience_to, @account.followers_url)
:private :private
else else
:direct :direct
@ -304,7 +312,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def audience_includes?(account) def audience_includes?(account)
uri = ActivityPub::TagManager.instance.uri_for(account) uri = ActivityPub::TagManager.instance.uri_for(account)
equals_or_includes?(@object['to'], uri) || equals_or_includes?(@object['cc'], uri) equals_or_includes?(audience_to, uri) || equals_or_includes?(audience_cc, uri)
end end
def replied_to_status def replied_to_status
@ -415,7 +423,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def addresses_local_accounts? def addresses_local_accounts?
return true if @options[:delivered_to_account_id] return true if @options[:delivered_to_account_id]
local_usernames = (as_array(@object['to']) + as_array(@object['cc'])).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) } local_usernames = (as_array(audience_to) + as_array(audience_cc)).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }
return false if local_usernames.empty? return false if local_usernames.empty?

View File

@ -35,6 +35,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
def serializable_hash(options = nil) def serializable_hash(options = nil)
named_contexts = {} named_contexts = {}
context_extensions = {} context_extensions = {}
options = serialization_options(options) options = serialization_options(options)
serialized_hash = serializer.serializable_hash(options.merge(named_contexts: named_contexts, context_extensions: context_extensions)) serialized_hash = serializer.serializable_hash(options.merge(named_contexts: named_contexts, context_extensions: context_extensions))
serialized_hash = serialized_hash.select { |k, _| options[:fields].include?(k) } if options[:fields] serialized_hash = serialized_hash.select { |k, _| options[:fields].include?(k) } if options[:fields]

View File

@ -68,10 +68,19 @@ class ActivityPub::TagManager
if status.account.silenced? if status.account.silenced?
# Only notify followers if the account is locally silenced # Only notify followers if the account is locally silenced
account_ids = status.active_mentions.pluck(:account_id) account_ids = status.active_mentions.pluck(:account_id)
to = status.account.followers.where(id: account_ids).map { |account| uri_for(account) } to = status.account.followers.where(id: account_ids).each_with_object([]) do |account, result|
to.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).map { |request| uri_for(request.account) }) result << uri_for(account)
result << account.followers_url if account.group?
end
to.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result|
result << uri_for(request.account)
result << request.account.followers_url if request.account.group?
end)
else else
status.active_mentions.map { |mention| uri_for(mention.account) } status.active_mentions.each_with_object([]) do |mention, result|
result << uri_for(mention.account)
result << mention.account.followers_url if mention.account.group?
end
end end
end end
end end
@ -97,10 +106,19 @@ class ActivityPub::TagManager
if status.account.silenced? if status.account.silenced?
# Only notify followers if the account is locally silenced # Only notify followers if the account is locally silenced
account_ids = status.active_mentions.pluck(:account_id) account_ids = status.active_mentions.pluck(:account_id)
cc.concat(status.account.followers.where(id: account_ids).map { |account| uri_for(account) }) cc.concat(status.account.followers.where(id: account_ids).each_with_object([]) do |account, result|
cc.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).map { |request| uri_for(request.account) }) result << uri_for(account)
result << account.followers_url if account.group?
end)
cc.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result|
result << uri_for(request.account)
result << request.account.followers_url if request.account.group?
end)
else else
cc.concat(status.active_mentions.map { |mention| uri_for(mention.account) }) cc.concat(status.active_mentions.each_with_object([]) do |mention, result|
result << uri_for(mention.account)
result << mention.account.followers_url if mention.account.group?
end)
end end
end end

View File

@ -44,7 +44,7 @@ class LanguageDetector
words = text.scan(RELIABLE_CHARACTERS_RE) words = text.scan(RELIABLE_CHARACTERS_RE)
if words.present? if words.present?
words.reduce(0) { |acc, elem| acc + elem.size }.to_f / text.size.to_f > 0.3 words.reduce(0) { |acc, elem| acc + elem.size }.to_f / text.size > 0.3
else else
false false
end end

View File

@ -93,6 +93,7 @@ class Account < ApplicationRecord
scope :without_silenced, -> { where(silenced_at: nil) } scope :without_silenced, -> { where(silenced_at: nil) }
scope :recent, -> { reorder(id: :desc) } scope :recent, -> { reorder(id: :desc) }
scope :bots, -> { where(actor_type: %w(Application Service)) } scope :bots, -> { where(actor_type: %w(Application Service)) }
scope :groups, -> { where(actor_type: 'Group') }
scope :alphabetic, -> { order(domain: :asc, username: :asc) } scope :alphabetic, -> { order(domain: :asc, username: :asc) }
scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') } scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) } scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
@ -153,6 +154,12 @@ class Account < ApplicationRecord
self.actor_type = ActiveModel::Type::Boolean.new.cast(val) ? 'Service' : 'Person' self.actor_type = ActiveModel::Type::Boolean.new.cast(val) ? 'Service' : 'Person'
end end
def group?
actor_type == 'Group'
end
alias group group?
def acct def acct
local? ? username : "#{username}@#{domain}" local? ? username : "#{username}@#{domain}"
end end

View File

@ -7,7 +7,7 @@
# user_id :bigint(8) # user_id :bigint(8)
# dump_file_name :string # dump_file_name :string
# dump_content_type :string # dump_content_type :string
# dump_file_size :integer # dump_file_size :bigint
# dump_updated_at :datetime # dump_updated_at :datetime
# processed :boolean default(FALSE), not null # processed :boolean default(FALSE), not null
# created_at :datetime not null # created_at :datetime not null

View File

@ -6,7 +6,7 @@ module LdapAuthenticable
class_methods do class_methods do
def authenticate_with_ldap(params = {}) def authenticate_with_ldap(params = {})
ldap = Net::LDAP.new(ldap_options) ldap = Net::LDAP.new(ldap_options)
filter = format(Devise.ldap_search_filter, uid: Devise.ldap_uid, email: params[:email]) filter = format(Devise.ldap_search_filter, uid: Devise.ldap_uid, mail: Devise.ldap_mail, email: params[:email])
if (user_info = ldap.bind_as(base: Devise.ldap_base, filter: filter, password: params[:password])) if (user_info = ldap.bind_as(base: Devise.ldap_base, filter: filter, password: params[:password]))
ldap_get_user(user_info.first) ldap_get_user(user_info.first)
@ -14,10 +14,18 @@ module LdapAuthenticable
end end
def ldap_get_user(attributes = {}) def ldap_get_user(attributes = {})
resource = joins(:account).find_by(accounts: { username: attributes[Devise.ldap_uid.to_sym].first }) safe_username = attributes[Devise.ldap_uid.to_sym].first
if Devise.ldap_uid_conversion_enabled
keys = Regexp.union(Devise.ldap_uid_conversion_search.chars)
replacement = Devise.ldap_uid_conversion_replace
safe_username = safe_username.gsub(keys, replacement)
end
resource = joins(:account).find_by(accounts: { username: safe_username })
if resource.blank? if resource.blank?
resource = new(email: attributes[:mail].first, agreement: true, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first }, admin: false, external: true, confirmed_at: Time.now.utc) resource = new(email: attributes[Devise.ldap_mail.to_sym].first, agreement: true, account_attributes: { username: safe_username }, admin: false, external: true, confirmed_at: Time.now.utc)
resource.save! resource.save!
end end

View File

@ -16,6 +16,7 @@ class Form::AdminSettings
open_deletion open_deletion
timeline_preview timeline_preview
show_staff_badge show_staff_badge
enable_bootstrap_timeline_accounts
bootstrap_timeline_accounts bootstrap_timeline_accounts
theme theme
min_invite_role min_invite_role
@ -40,6 +41,7 @@ class Form::AdminSettings
open_deletion open_deletion
timeline_preview timeline_preview
show_staff_badge show_staff_badge
enable_bootstrap_timeline_accounts
activity_api_enabled activity_api_enabled
peers_api_enabled peers_api_enabled
show_known_fediverse_at_about_page show_known_fediverse_at_about_page

View File

@ -40,7 +40,7 @@ class Form::CustomEmojiBatch
if category_id.present? if category_id.present?
CustomEmojiCategory.find(category_id) CustomEmojiCategory.find(category_id)
elsif category_name.present? elsif category_name.present?
CustomEmojiCategory.create!(name: category_name) CustomEmojiCategory.find_or_create_by!(name: category_name)
end end
end end

View File

@ -167,6 +167,18 @@ class MediaAttachment < ApplicationRecord
audio? || video? audio? || video?
end end
def variant?(other_file_name)
return true if file_file_name == other_file_name
formats = file.styles.values.map(&:format).compact
return false if formats.empty?
extension = File.extname(other_file_name)
formats.include?(extension.delete('.')) && File.basename(other_file_name, extension) == File.basename(file_file_name, File.extname(file_file_name))
end
def to_param def to_param
shortcode shortcode
end end
@ -287,7 +299,7 @@ class MediaAttachment < ApplicationRecord
width: width, width: width,
height: height, height: height,
size: "#{width}x#{height}", size: "#{width}x#{height}",
aspect: width.to_f / height.to_f, aspect: width.to_f / height,
} }
end end

View File

@ -42,7 +42,7 @@ class Notification < ApplicationRecord
validates :activity_type, inclusion: { in: TYPE_CLASS_MAP.values } validates :activity_type, inclusion: { in: TYPE_CLASS_MAP.values }
scope :browserable, ->(exclude_types = [], account_id = nil) { scope :browserable, ->(exclude_types = [], account_id = nil) {
types = TYPE_CLASS_MAP.values - activity_types_from_types(exclude_types + [:follow_request]) types = TYPE_CLASS_MAP.values - activity_types_from_types(exclude_types)
if account_id.nil? if account_id.nil?
where(activity_type: types) where(activity_type: types)
else else
@ -50,7 +50,7 @@ class Notification < ApplicationRecord
end end
} }
cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account, poll: [status: STATUS_INCLUDES] cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account, follow_request: :account, poll: [status: STATUS_INCLUDES]
def type def type
@type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym @type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym
@ -69,10 +69,6 @@ class Notification < ApplicationRecord
end end
end end
def browserable?
type != :follow_request
end
class << self class << self
def cache_ids def cache_ids
select(:id, :updated_at, :activity_type, :activity_id) select(:id, :updated_at, :activity_type, :activity_id)

View File

@ -36,7 +36,7 @@ class Poll < ApplicationRecord
scope :attached, -> { where.not(status_id: nil) } scope :attached, -> { where.not(status_id: nil) }
scope :unattached, -> { where(status_id: nil) } scope :unattached, -> { where(status_id: nil) }
before_validation :prepare_options before_validation :prepare_options, if: :local?
before_validation :prepare_votes_count before_validation :prepare_votes_count
after_initialize :prepare_cached_tallies after_initialize :prepare_cached_tallies

View File

@ -19,6 +19,8 @@ class ReportFilter
def scope_for(key, value) def scope_for(key, value)
case key.to_sym case key.to_sym
when :by_target_domain
Report.where(target_account: Account.where(domain: value))
when :resolved when :resolved
Report.resolved Report.resolved
when :account_id when :account_id

View File

@ -117,7 +117,7 @@ class Tag < ApplicationRecord
class << self class << self
def find_or_create_by_names(name_or_names) def find_or_create_by_names(name_or_names)
Array(name_or_names).map(&method(:normalize)).uniq { |str| str.mb_chars.downcase.to_s }.map do |normalized_name| Array(name_or_names).map(&method(:normalize)).uniq { |str| str.mb_chars.downcase.to_s }.map do |normalized_name|
tag = matching_name(normalized_name).first || create!(name: normalized_name) tag = matching_name(normalized_name).first || create(name: normalized_name)
yield tag if block_given? yield tag if block_given?

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class StatusRelationshipsPresenter class StatusRelationshipsPresenter
attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map,
:bookmarks_map
def initialize(statuses, current_account_id = nil, **options) def initialize(statuses, current_account_id = nil, **options)
if current_account_id.nil? if current_account_id.nil?

View File

@ -49,6 +49,8 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
'Application' 'Application'
elsif object.bot? elsif object.bot?
'Service' 'Service'
elsif object.group?
'Group'
else else
'Person' 'Person'
end end

View File

@ -3,7 +3,7 @@
class REST::AccountSerializer < ActiveModel::Serializer class REST::AccountSerializer < ActiveModel::Serializer
include RoutingHelper include RoutingHelper
attributes :id, :username, :acct, :display_name, :locked, :bot, :created_at, attributes :id, :username, :acct, :display_name, :locked, :bot, :discoverable, :group, :created_at,
:note, :url, :avatar, :avatar_static, :header, :header_static, :note, :url, :avatar, :avatar_static, :header, :header_static,
:followers_count, :following_count, :statuses_count, :last_status_at :followers_count, :following_count, :statuses_count, :last_status_at

View File

@ -95,8 +95,8 @@ class REST::StatusSerializer < ActiveModel::Serializer
end end
def bookmarked def bookmarked
if instance_options && instance_options[:bookmarks] if instance_options && instance_options[:relationships]
instance_options[:bookmarks].bookmarks_map[object.id] || false instance_options[:relationships].bookmarks_map[object.id] || false
else else
current_user.account.bookmarked?(object) current_user.account.bookmarked?(object)
end end

View File

@ -4,8 +4,8 @@ class AccountSearchService < BaseService
attr_reader :query, :limit, :offset, :options, :account attr_reader :query, :limit, :offset, :options, :account
def call(query, account = nil, options = {}) def call(query, account = nil, options = {})
@acct_hint = query.start_with?('@') @acct_hint = query&.start_with?('@')
@query = query.strip.gsub(/\A@/, '') @query = query&.strip&.gsub(/\A@/, '')
@limit = options[:limit].to_i @limit = options[:limit].to_i
@offset = options[:offset].to_i @offset = options[:offset].to_i
@options = options @options = options

View File

@ -30,7 +30,7 @@ class ActivityPub::ProcessPollService < BaseService
voters_count = @json['votersCount'] voters_count = @json['votersCount']
latest_options = items.map { |item| item['name'].presence || item['content'] } latest_options = items.map { |item| item['name'].presence || item['content'] }.compact
# If for some reasons the options were changed, it invalidates all previous # If for some reasons the options were changed, it invalidates all previous
# votes, so we need to remove them # votes, so we need to remove them

View File

@ -3,6 +3,8 @@
require 'rubygems/package' require 'rubygems/package'
class BackupService < BaseService class BackupService < BaseService
include Payloadable
attr_reader :account, :backup, :collection attr_reader :account, :backup, :collection
def call(backup) def call(backup)
@ -20,7 +22,7 @@ class BackupService < BaseService
account.statuses.with_includes.reorder(nil).find_in_batches do |statuses| account.statuses.with_includes.reorder(nil).find_in_batches do |statuses|
statuses.each do |status| statuses.each do |status|
item = serialize(status, ActivityPub::ActivitySerializer) item = serialize_payload(status, ActivityPub::ActivitySerializer, signer: @account)
item.delete(:'@context') item.delete(:'@context')
unless item[:type] == 'Announce' || item[:object][:attachment].blank? unless item[:type] == 'Announce' || item[:object][:attachment].blank?

View File

@ -5,7 +5,7 @@ class BootstrapTimelineService < BaseService
@source_account = source_account @source_account = source_account
autofollow_inviter! autofollow_inviter!
autofollow_bootstrap_timeline_accounts! autofollow_bootstrap_timeline_accounts! if Setting.enable_bootstrap_timeline_accounts
end end
private private

View File

@ -45,7 +45,7 @@ class FetchLinkCardService < BaseService
def html def html
return @html if defined?(@html) return @html if defined?(@html)
Request.new(:get, @url).perform do |res| Request.new(:get, @url).add_headers('Accept' => 'text/html').perform do |res|
if res.code == 200 && res.mime_type == 'text/html' if res.code == 200 && res.mime_type == 'text/html'
@html = res.body_with_limit @html = res.body_with_limit
@html_charset = res.charset @html_charset = res.charset

View File

@ -93,7 +93,7 @@ class FetchOEmbedService
def html def html
return @html if defined?(@html) return @html if defined?(@html)
@html = @options[:html] || Request.new(:get, @url).perform do |res| @html = @options[:html] || Request.new(:get, @url).add_headers('Accept' => 'text/html').perform do |res|
res.code != 200 || res.mime_type != 'text/html' ? nil : res.body_with_limit res.code != 200 || res.mime_type != 'text/html' ? nil : res.body_with_limit
end end
end end

View File

@ -1,17 +0,0 @@
# frozen_string_literal: true
class FetchRemoteAccountService < BaseService
def call(url, prefetched_body = nil, protocol = :ostatus)
if prefetched_body.nil?
resource_url, resource_options, protocol = FetchResourceService.new.call(url)
else
resource_url = url
resource_options = { prefetched_body: prefetched_body }
end
case protocol
when :activitypub
ActivityPub::FetchRemoteAccountService.new.call(resource_url, **resource_options)
end
end
end

View File

@ -1,17 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
class FetchRemoteStatusService < BaseService class FetchRemoteStatusService < BaseService
def call(url, prefetched_body = nil, protocol = :ostatus) def call(url, prefetched_body = nil)
if prefetched_body.nil? if prefetched_body.nil?
resource_url, resource_options, protocol = FetchResourceService.new.call(url) resource_url, resource_options = FetchResourceService.new.call(url)
else else
resource_url = url resource_url = url
resource_options = { prefetched_body: prefetched_body } resource_options = { prefetched_body: prefetched_body }
end end
case protocol ActivityPub::FetchRemoteStatusService.new.call(resource_url, **resource_options) unless resource_url.nil?
when :activitypub
ActivityPub::FetchRemoteStatusService.new.call(resource_url, **resource_options)
end
end end
end end

View File

@ -33,7 +33,7 @@ class FetchResourceService < BaseService
body = response.body_with_limit body = response.body_with_limit
json = body_to_json(body) json = body_to_json(body)
[json['id'], { prefetched_body: body, id: true }, :activitypub] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) || expected_type?(json)) [json['id'], { prefetched_body: body, id: true }] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) || expected_type?(json))
elsif !terminal elsif !terminal
link_header = response['Link'] && parse_link_header(response) link_header = response['Link'] && parse_link_header(response)

View File

@ -9,7 +9,7 @@ class NotifyService < BaseService
return if recipient.user.nil? || blocked? return if recipient.user.nil? || blocked?
create_notification! create_notification!
push_notification! if @notification.browserable? push_notification!
push_to_conversation! if direct_message? push_to_conversation! if direct_message?
send_email! if email_enabled? send_email! if email_enabled?
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid

View File

@ -19,9 +19,9 @@ class ResolveURLService < BaseService
def process_url def process_url
if equals_or_includes_any?(type, ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) if equals_or_includes_any?(type, ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES)
FetchRemoteAccountService.new.call(resource_url, body, protocol) ActivityPub::FetchRemoteAccountService.new.call(resource_url, prefetched_body: body)
elsif equals_or_includes_any?(type, ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES) elsif equals_or_includes_any?(type, ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES)
status = FetchRemoteStatusService.new.call(resource_url, body, protocol) status = FetchRemoteStatusService.new.call(resource_url, body)
authorize_with @on_behalf_of, status, :show? unless status.nil? authorize_with @on_behalf_of, status, :show? unless status.nil?
status status
elsif fetched_resource.nil? && @on_behalf_of.present? elsif fetched_resource.nil? && @on_behalf_of.present?
@ -45,12 +45,8 @@ class ResolveURLService < BaseService
fetched_resource.second[:prefetched_body] fetched_resource.second[:prefetched_body]
end end
def protocol
fetched_resource.third
end
def type def type
return json_data['type'] if protocol == :activitypub json_data['type']
end end
def json_data def json_data

View File

@ -2,7 +2,7 @@
class SearchService < BaseService class SearchService < BaseService
def call(query, account, limit, options = {}) def call(query, account, limit, options = {})
@query = query.strip @query = query&.strip
@account = account @account = account
@options = options @options = options
@limit = limit.to_i @limit = limit.to_i
@ -10,6 +10,8 @@ class SearchService < BaseService
@resolve = options[:resolve] || false @resolve = options[:resolve] || false
default_results.tap do |results| default_results.tap do |results|
next if @query.blank?
if url_query? if url_query?
results.merge!(url_resource_results) unless url_resource.nil? || (@options[:type].present? && url_resource_symbol != @options[:type].to_sym) results.merge!(url_resource_results) unless url_resource.nil? || (@options[:type].present? && url_resource_symbol != @options[:type].to_sym)
elsif @query.present? elsif @query.present?

View File

@ -19,7 +19,7 @@
.dashboard__counters__num= number_with_delimiter @blocks_count .dashboard__counters__num= number_with_delimiter @blocks_count
.dashboard__counters__label= t 'admin.instances.total_blocked_by_us' .dashboard__counters__label= t 'admin.instances.total_blocked_by_us'
%div %div
%div = link_to admin_reports_path(by_target_domain: @instance.domain) do
.dashboard__counters__num= number_with_delimiter @reports_count .dashboard__counters__num= number_with_delimiter @reports_count
.dashboard__counters__label= t 'admin.instances.total_reported' .dashboard__counters__label= t 'admin.instances.total_reported'
%div %div

View File

@ -8,13 +8,27 @@
%li= filter_link_to t('admin.reports.unresolved'), resolved: nil %li= filter_link_to t('admin.reports.unresolved'), resolved: nil
%li= filter_link_to t('admin.reports.resolved'), resolved: '1' %li= filter_link_to t('admin.reports.resolved'), resolved: '1'
= form_tag admin_reports_url, method: 'GET', class: 'simple_form' do
.fields-group
- Admin::FilterHelper::REPORT_FILTERS.each do |key|
- if params[key].present?
= hidden_field_tag key, params[key]
- %i(by_target_domain).each do |key|
.input.string.optional
= text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.reports.#{key}")
.actions
%button= t('admin.accounts.search')
= link_to t('admin.accounts.reset'), admin_reports_path, class: 'button negative'
- @reports.group_by(&:target_account_id).each do |target_account_id, reports| - @reports.group_by(&:target_account_id).each do |target_account_id, reports|
- target_account = reports.first.target_account - target_account = reports.first.target_account
.report-card .report-card
.report-card__profile .report-card__profile
= account_link_to target_account, '', size: 36, path: admin_account_path(target_account.id) = account_link_to target_account, '', size: 36, path: admin_account_path(target_account.id)
.report-card__profile__stats .report-card__profile__stats
= link_to pluralize(target_account.targeted_moderation_notes.count, t('admin.reports.account.note')), admin_account_path(target_account.id) = link_to t('admin.reports.account.notes', count: target_account.targeted_moderation_notes.count), admin_account_path(target_account.id)
%br/ %br/
- if target_account.suspended? - if target_account.suspended?
%span.red= t('admin.accounts.suspended') %span.red= t('admin.accounts.suspended')

View File

@ -4,37 +4,28 @@
- content_for :page_title do - content_for :page_title do
= t('admin.reports.report', id: @report.id) = t('admin.reports.report', id: @report.id)
%div{ style: 'overflow: hidden; margin-bottom: 20px' } - content_for :page_heading_actions do
- if @report.unresolved? - if @report.unresolved?
%div{ style: 'float: right' }
- if @report.target_account.local?
= link_to t('admin.accounts.warn'), new_admin_account_action_path(@report.target_account_id, type: 'none', report_id: @report.id), class: 'button'
= link_to t('admin.accounts.disable'), new_admin_account_action_path(@report.target_account_id, type: 'disable', report_id: @report.id), class: 'button button--destructive'
= link_to t('admin.accounts.silence'), new_admin_account_action_path(@report.target_account_id, type: 'silence', report_id: @report.id), class: 'button button--destructive'
= link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@report.target_account_id, type: 'suspend', report_id: @report.id), class: 'button button--destructive'
%div{ style: 'float: left' }
= link_to t('admin.reports.mark_as_resolved'), resolve_admin_report_path(@report), method: :post, class: 'button' = link_to t('admin.reports.mark_as_resolved'), resolve_admin_report_path(@report), method: :post, class: 'button'
- else - else
= link_to t('admin.reports.mark_as_unresolved'), reopen_admin_report_path(@report), method: :post, class: 'button' = link_to t('admin.reports.mark_as_unresolved'), reopen_admin_report_path(@report), method: :post, class: 'button'
%hr.spacer
.table-wrapper .table-wrapper
%table.table.inline-table %table.table.inline-table
%tbody %tbody
%tr %tr
%th= t('admin.reports.reported_account') %th= t('admin.reports.reported_account')
%td= admin_account_link_to @report.target_account %td= admin_account_link_to @report.target_account
%td= table_link_to 'flag', pluralize(@report.target_account.targeted_reports.count, t('admin.reports.account.report')), admin_reports_path(target_account_id: @report.target_account.id) %td= table_link_to 'flag', t('admin.reports.account.reports', count: @report.target_account.targeted_reports.count), admin_reports_path(target_account_id: @report.target_account.id)
%td= table_link_to 'file', pluralize(@report.target_account.targeted_moderation_notes.count, t('admin.reports.account.note')), admin_reports_path(target_account_id: @report.target_account.id) %td= table_link_to 'file', t('admin.reports.account.notes', count: @report.target_account.targeted_moderation_notes.count), admin_reports_path(target_account_id: @report.target_account.id)
%tr %tr
%th= t('admin.reports.reported_by') %th= t('admin.reports.reported_by')
- if @report.account.instance_actor? - if @report.account.instance_actor?
%td{ colspan: 3 }= site_hostname %td{ colspan: 3 }= site_hostname
- elsif @report.account.local? - elsif @report.account.local?
%td= admin_account_link_to @report.account %td= admin_account_link_to @report.account
%td= table_link_to 'flag', pluralize(@report.account.targeted_reports.count, t('admin.reports.account.report')), admin_reports_path(target_account_id: @report.account.id) %td= table_link_to 'flag', t('admin.reports.account.reports', count: @report.account.targeted_reports.count), admin_reports_path(target_account_id: @report.account.id)
%td= table_link_to 'file', pluralize(@report.account.targeted_moderation_notes.count, t('admin.reports.account.note')), admin_reports_path(target_account_id: @report.account.id) %td= table_link_to 'file', t('admin.reports.account.notes', count: @report.account.targeted_moderation_notes.count), admin_reports_path(target_account_id: @report.account.id)
- else - else
%td{ colspan: 3 }= @report.account.domain %td{ colspan: 3 }= @report.account.domain
%tr %tr
@ -77,6 +68,17 @@
%hr.spacer %hr.spacer
%div{ style: 'overflow: hidden; margin-bottom: 20px; clear: both' }
- if @report.unresolved?
%div{ style: 'float: right' }
- if @report.target_account.local?
= link_to t('admin.accounts.warn'), new_admin_account_action_path(@report.target_account_id, type: 'none', report_id: @report.id), class: 'button'
= link_to t('admin.accounts.disable'), new_admin_account_action_path(@report.target_account_id, type: 'disable', report_id: @report.id), class: 'button button--destructive'
= link_to t('admin.accounts.silence'), new_admin_account_action_path(@report.target_account_id, type: 'silence', report_id: @report.id), class: 'button button--destructive'
= link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@report.target_account_id, type: 'suspend', report_id: @report.id), class: 'button button--destructive'
%hr.spacer
.speech-bubble .speech-bubble
.speech-bubble__bubble= simple_format(@report.comment.presence || t('admin.reports.comment.none')) .speech-bubble__bubble= simple_format(@report.comment.presence || t('admin.reports.comment.none'))
.speech-bubble__owner .speech-bubble__owner

View File

@ -1,3 +1,6 @@
- content_for :header_tags do
= javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous'
- content_for :page_title do - content_for :page_title do
= t('admin.settings.title') = t('admin.settings.title')
@ -38,7 +41,9 @@
%hr.spacer/ %hr.spacer/
.fields-group .fields-group
= f.input :bootstrap_timeline_accounts, wrapper: :with_block_label, label: t('admin.settings.bootstrap_timeline_accounts.title'), hint: t('admin.settings.bootstrap_timeline_accounts.desc_html') = f.input :enable_bootstrap_timeline_accounts, as: :boolean, wrapper: :with_label, label: t('admin.settings.enable_bootstrap_timeline_accounts.title')
.fields-group
= f.input :bootstrap_timeline_accounts, wrapper: :with_block_label, label: t('admin.settings.bootstrap_timeline_accounts.title'), hint: t('admin.settings.bootstrap_timeline_accounts.desc_html'), disabled: !Setting.enable_bootstrap_timeline_accounts
%hr.spacer/ %hr.spacer/

View File

@ -21,8 +21,13 @@
.content-wrapper .content-wrapper
.content .content
.content-heading
%h2= yield :page_title %h2= yield :page_title
- if :page_heading_actions
.content-heading-actions
= yield :page_heading_actions
= render 'application/flashes' = render 'application/flashes'
= yield = yield

View File

@ -8,8 +8,8 @@
.filter-subset .filter-subset
%strong= t 'relationships.relationship' %strong= t 'relationships.relationship'
%ul %ul
%li= filter_link_to t('accounts.following', count: current_account.following_count), relationship: nil %li= filter_link_to t('relationships.following'), relationship: nil
%li= filter_link_to t('accounts.followers', count: current_account.followers_count), relationship: 'followed_by' %li= filter_link_to t('relationships.followers'), relationship: 'followed_by'
%li= filter_link_to t('relationships.mutual'), relationship: 'mutual' %li= filter_link_to t('relationships.mutual'), relationship: 'mutual'
.filter-subset .filter-subset

View File

@ -7,7 +7,7 @@
.fields-group .fields-group
= f.input :redirect_uri, wrapper: :with_block_label, label: t('activerecord.attributes.doorkeeper/application.redirect_uri'), hint: t('doorkeeper.applications.help.redirect_uri') = f.input :redirect_uri, wrapper: :with_block_label, label: t('activerecord.attributes.doorkeeper/application.redirect_uri'), hint: t('doorkeeper.applications.help.redirect_uri')
%p.hint= t('doorkeeper.applications.help.native_redirect_uri', native_redirect_uri: Doorkeeper.configuration.native_redirect_uri) %p.hint= t('doorkeeper.applications.help.native_redirect_uri', native_redirect_uri: content_tag(:code, Doorkeeper.configuration.native_redirect_uri)).html_safe
.field-group .field-group
.input.with_block_label .input.with_block_label

View File

@ -9,11 +9,11 @@
%td= number_to_human_size @export.total_storage %td= number_to_human_size @export.total_storage
%td %td
%tr %tr
%th= t('accounts.posts', count: @export.total_statuses) %th= t('accounts.posts_tab_heading')
%td= number_with_delimiter @export.total_statuses %td= number_with_delimiter @export.total_statuses
%td %td
%tr %tr
%th= t('exports.follows') %th= t('admin.accounts.follows')
%td= number_with_delimiter @export.total_follows %td= number_with_delimiter @export.total_follows
%td= table_link_to 'download', t('exports.csv'), settings_exports_follows_path(format: :csv) %td= table_link_to 'download', t('exports.csv'), settings_exports_follows_path(format: :csv)
%tr %tr
@ -21,7 +21,7 @@
%td= number_with_delimiter @export.total_lists %td= number_with_delimiter @export.total_lists
%td= table_link_to 'download', t('exports.csv'), settings_exports_lists_path(format: :csv) %td= table_link_to 'download', t('exports.csv'), settings_exports_lists_path(format: :csv)
%tr %tr
%th= t('accounts.followers', count: @export.total_followers) %th= t('admin.accounts.followers')
%td= number_with_delimiter @export.total_followers %td= number_with_delimiter @export.total_followers
%td %td
%tr %tr

View File

@ -4,6 +4,10 @@
= simple_form_for current_user, url: settings_preferences_notifications_path, html: { method: :put } do |f| = simple_form_for current_user, url: settings_preferences_notifications_path, html: { method: :put } do |f|
= render 'shared/error_messages', object: current_user = render 'shared/error_messages', object: current_user
%h4= t 'notifications.email_events'
%p.hint= t 'notifications.email_events_hint'
.fields-group .fields-group
= f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff| = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff|
= ff.input :follow, as: :boolean, wrapper: :with_label = ff.input :follow, as: :boolean, wrapper: :with_label
@ -21,6 +25,8 @@
= f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff| = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff|
= ff.input :digest, as: :boolean, wrapper: :with_label = ff.input :digest, as: :boolean, wrapper: :with_label
%h4= t 'notifications.other_settings'
.fields-group .fields-group
= f.simple_fields_for :interactions, hash_to_object(current_user.settings.interactions) do |ff| = f.simple_fields_for :interactions, hash_to_object(current_user.settings.interactions) do |ff|
= ff.input :must_be_follower, as: :boolean, wrapper: :with_label = ff.input :must_be_follower, as: :boolean, wrapper: :with_label

View File

@ -1,36 +1,39 @@
module.exports = (api) => { module.exports = (api) => {
const env = api.env(); const env = api.env();
const reactOptions = {
development: false,
};
const envOptions = { const envOptions = {
debug: false,
loose: true, loose: true,
modules: false, modules: false,
debug: false,
}; };
const config = { const config = {
presets: [ presets: [
'@babel/react', ['@babel/react', reactOptions],
['@babel/env', envOptions], ['@babel/env', envOptions],
], ],
plugins: [ plugins: [
'@babel/syntax-dynamic-import',
['@babel/proposal-object-rest-spread', { useBuiltIns: true }],
['@babel/proposal-decorators', { legacy: true }], ['@babel/proposal-decorators', { legacy: true }],
'@babel/proposal-class-properties', '@babel/proposal-class-properties',
['react-intl', { messagesDir: './build/messages' }], ['react-intl', { messagesDir: './build/messages' }],
'preval', 'preval',
], ],
overrides: [{ overrides: [
{
test: /tesseract\.js/, test: /tesseract\.js/,
presets: [ presets: [
['@babel/env', { ...envOptions, modules: 'commonjs' }], ['@babel/env', { ...envOptions, modules: 'commonjs' }],
], ],
}], },
],
}; };
switch (env) { switch (env) {
case 'production': case 'production':
envOptions.debug = false;
config.plugins.push(...[ config.plugins.push(...[
'lodash', 'lodash',
[ [
@ -55,11 +58,8 @@ module.exports = (api) => {
]); ]);
break; break;
case 'development': case 'development':
reactOptions.development = true;
envOptions.debug = true; envOptions.debug = true;
config.plugins.push(...[
'@babel/transform-react-jsx-source',
'@babel/transform-react-jsx-self',
]);
break; break;
case 'test': case 'test':
envOptions.modules = 'commonjs'; envOptions.modules = 'commonjs';

View File

@ -60,16 +60,15 @@ deploy.config:
- touch /app/log/production.log - touch /app/log/production.log
before_live: before_live:
web.web: web.web:
- bin/tootctl cache clear
- bundle exec rake db:migrate:setup - bundle exec rake db:migrate:setup
after_live:
worker.sidekiq:
- |- - |-
if [[ "${ES_ENABLED}" != "false" ]] if [[ "${ES_ENABLED}" != "false" ]]
then then
bin/tootctl search deploy bin/tootctl search deploy
fi fi
- bin/tootctl cache clear
after_live:
worker.sidekiq:
- bin/tootctl search deploy
web.web: web.web:

View File

@ -53,6 +53,8 @@ module Devise
@@ldap_base = nil @@ldap_base = nil
mattr_accessor :ldap_uid mattr_accessor :ldap_uid
@@ldap_uid = nil @@ldap_uid = nil
mattr_accessor :ldap_mail
@@ldap_mail = nil
mattr_accessor :ldap_bind_dn mattr_accessor :ldap_bind_dn
@@ldap_bind_dn = nil @@ldap_bind_dn = nil
mattr_accessor :ldap_password mattr_accessor :ldap_password
@ -61,6 +63,12 @@ module Devise
@@ldap_tls_no_verify = false @@ldap_tls_no_verify = false
mattr_accessor :ldap_search_filter mattr_accessor :ldap_search_filter
@@ldap_search_filter = nil @@ldap_search_filter = nil
mattr_accessor :ldap_uid_conversion_enabled
@@ldap_uid_conversion_enabled = false
mattr_accessor :ldap_uid_conversion_search
@@ldap_uid_conversion_search = nil
mattr_accessor :ldap_uid_conversion_replace
@@ldap_uid_conversion_replace = nil
class Strategies::PamAuthenticatable class Strategies::PamAuthenticatable
def valid? def valid?
@ -363,7 +371,11 @@ Devise.setup do |config|
config.ldap_bind_dn = ENV.fetch('LDAP_BIND_DN') config.ldap_bind_dn = ENV.fetch('LDAP_BIND_DN')
config.ldap_password = ENV.fetch('LDAP_PASSWORD') config.ldap_password = ENV.fetch('LDAP_PASSWORD')
config.ldap_uid = ENV.fetch('LDAP_UID', 'cn') config.ldap_uid = ENV.fetch('LDAP_UID', 'cn')
config.ldap_mail = ENV.fetch('LDAP_MAIL', 'mail')
config.ldap_tls_no_verify = ENV['LDAP_TLS_NO_VERIFY'] == 'true' config.ldap_tls_no_verify = ENV['LDAP_TLS_NO_VERIFY'] == 'true'
config.ldap_search_filter = ENV.fetch('LDAP_SEARCH_FILTER', '%{uid}=%{email}') config.ldap_search_filter = ENV.fetch('LDAP_SEARCH_FILTER', '(|(%{uid}=%{email})(%{mail}=%{email}))')
config.ldap_uid_conversion_enabled = ENV['LDAP_UID_CONVERSION_ENABLED'] == 'true'
config.ldap_uid_conversion_search = ENV.fetch('LDAP_UID_CONVERSION_SEARCH', '.,- ')
config.ldap_uid_conversion_replace = ENV.fetch('LDAP_UID_CONVERSION_REPLACE', '_')
end end
end end

View File

@ -8,8 +8,20 @@ Doorkeeper.configure do
end end
resource_owner_from_credentials do |_routes| resource_owner_from_credentials do |_routes|
if Devise.ldap_authentication
user = User.authenticate_with_ldap({ :email => request.params[:username], :password => request.params[:password] })
end
if Devise.pam_authentication
user ||= User.authenticate_with_ldap({ :email => request.params[:username], :password => request.params[:password] })
end
if user.nil?
user = User.find_by(email: request.params[:username]) user = User.find_by(email: request.params[:username])
user if !user&.otp_required_for_login? && user&.valid_password?(request.params[:password]) user = nil unless user.valid_password?(request.params[:password])
end
user if !user&.otp_required_for_login?
end end
# If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below.

View File

@ -42,7 +42,7 @@ if ENV['S3_ENABLED'] == 'true'
s3_options: { s3_options: {
signature_version: ENV.fetch('S3_SIGNATURE_VERSION') { 'v4' }, signature_version: ENV.fetch('S3_SIGNATURE_VERSION') { 'v4' },
http_open_timeout: 5, http_open_timeout: ENV.fetch('S3_OPEN_TIMEOUT'){ '5' }.to_i,
http_read_timeout: 5, http_read_timeout: 5,
http_idle_timeout: 5, http_idle_timeout: 5,
retry_limit: 0, retry_limit: 0,
@ -52,7 +52,7 @@ if ENV['S3_ENABLED'] == 'true'
if ENV.has_key?('S3_ENDPOINT') if ENV.has_key?('S3_ENDPOINT')
Paperclip::Attachment.default_options[:s3_options].merge!( Paperclip::Attachment.default_options[:s3_options].merge!(
endpoint: ENV['S3_ENDPOINT'], endpoint: ENV['S3_ENDPOINT'],
force_path_style: true force_path_style: ENV['S3_OVERRIDE_PATH_STYLE'] != 'true',
) )
Paperclip::Attachment.default_options[:url] = ':s3_path_url' Paperclip::Attachment.default_options[:url] = ':s3_path_url'
@ -89,7 +89,7 @@ else
Paperclip::Attachment.default_options.merge!( Paperclip::Attachment.default_options.merge!(
storage: :filesystem, storage: :filesystem,
use_timestamp: true, use_timestamp: true,
path: ENV.fetch('PAPERCLIP_ROOT_PATH', ':rails_root/public/system') + '/:class/:attachment/:id_partition/:style/:filename', path: File.join(ENV.fetch('PAPERCLIP_ROOT_PATH', File.join(':rails_root', 'public', 'system')), ':class', ':attachment', ':id_partition', ':style', ':filename'),
url: ENV.fetch('PAPERCLIP_ROOT_URL', '/system') + '/:class/:attachment/:id_partition/:style/:filename', url: ENV.fetch('PAPERCLIP_ROOT_URL', '/system') + '/:class/:attachment/:id_partition/:style/:filename',
) )
end end

View File

@ -124,9 +124,7 @@ ar:
email_status: حالة البريد الإلكتروني email_status: حالة البريد الإلكتروني
enable: تفعيل enable: تفعيل
enabled: مفعَّل enabled: مفعَّل
feed_url: عنوان رابط التغذية
followers: المتابِعون followers: المتابِعون
followers_url: عنوان رابط المتابِعين
follows: يتابع follows: يتابع
header: الرأسية header: الرأسية
inbox_url: رابط صندوق الوارد inbox_url: رابط صندوق الوارد
@ -154,10 +152,8 @@ ar:
no_account_selected: لم يطرأ أي تغيير على أي حساب بما أنه لم يتم اختيار أي واحد no_account_selected: لم يطرأ أي تغيير على أي حساب بما أنه لم يتم اختيار أي واحد
no_limits_imposed: مِن دون حدود مشروطة no_limits_imposed: مِن دون حدود مشروطة
not_subscribed: غير مشترك not_subscribed: غير مشترك
outbox_url: رابط صندوق الصادر
pending: في انتظار المراجعة pending: في انتظار المراجعة
perform_full_suspension: تعليق الحساب perform_full_suspension: تعليق الحساب
profile_url: رابط الصفحة التعريفية
promote: ترقية promote: ترقية
protocol: البروتوكول protocol: البروتوكول
public: عمومي public: عمومي
@ -180,7 +176,6 @@ ar:
moderator: مشرف moderator: مشرف
staff: الفريق staff: الفريق
user: مستخدِم user: مستخدِم
salmon_url: عنوان رابط سالمون Salmon
search: البحث search: البحث
shared_inbox_url: رابط الصندوق المُشترَك للبريد الوارد shared_inbox_url: رابط الصندوق المُشترَك للبريد الوارد
show: show:
@ -399,9 +394,6 @@ ar:
created_msg: تم إنشاء ملاحظة الشكوى بنجاح! created_msg: تم إنشاء ملاحظة الشكوى بنجاح!
destroyed_msg: تم حذف ملاحظة الشكوى بنجاح! destroyed_msg: تم حذف ملاحظة الشكوى بنجاح!
reports: reports:
account:
note: ملحوظة
report: تقرير
action_taken_by: تم اتخاذ الإجراء مِن طرف action_taken_by: تم اتخاذ الإجراء مِن طرف
are_you_sure: هل أنت متأكد ؟ are_you_sure: هل أنت متأكد ؟
assign_to_self: عين لي assign_to_self: عين لي

View File

@ -118,9 +118,7 @@ bn:
email_status: ইমেইলের অবস্থা email_status: ইমেইলের অবস্থা
enable: চালু করুন enable: চালু করুন
enabled: চালু করুন enabled: চালু করুন
feed_url: সম্মিলিত(feed) লিংক
followers: অনুসরকারীরা followers: অনুসরকারীরা
followers_url: অনুসরণকারীদের লিংক
follows: অনুসরণ করে follows: অনুসরণ করে
header: শিরোলেখা header: শিরোলেখা
inbox_url: চিঠি পাওয়ার বক্স লিংক inbox_url: চিঠি পাওয়ার বক্স লিংক
@ -148,10 +146,8 @@ bn:
no_account_selected: কোনও অ্যাকাউন্টই নির্বাচন করা হয়নি বলে কোনও অ্যাকাউন্ট পরিবর্তন করা হয়নি no_account_selected: কোনও অ্যাকাউন্টই নির্বাচন করা হয়নি বলে কোনও অ্যাকাউন্ট পরিবর্তন করা হয়নি
no_limits_imposed: কোন সীমা আরোপ করা নেই no_limits_imposed: কোন সীমা আরোপ করা নেই
not_subscribed: সাবস্ক্রাইব নেই not_subscribed: সাবস্ক্রাইব নেই
outbox_url: চিঠি পাঠানোর বাক্স লিংক
pending: পয্র্যবেক্ষণের অপেক্ষায় আছে pending: পয্র্যবেক্ষণের অপেক্ষায় আছে
perform_full_suspension: বাতিল করা perform_full_suspension: বাতিল করা
profile_url: প্রোফাইল URL
promote: প্রচার promote: প্রচার
protocol: প্রোটোকল protocol: প্রোটোকল
public: সর্বজনীন public: সর্বজনীন
@ -174,7 +170,6 @@ bn:
moderator: নিয়ামক moderator: নিয়ামক
staff: কর্মী staff: কর্মী
user: ব্যবহারকারী user: ব্যবহারকারী
salmon_url: সালমন URL
search: অনুসন্ধান search: অনুসন্ধান
search_same_ip: একই IP সহ অন্যান্য ব্যবহারকারীরা search_same_ip: একই IP সহ অন্যান্য ব্যবহারকারীরা
shared_inbox_url: ভাগ করা ইনবক্স URL shared_inbox_url: ভাগ করা ইনবক্স URL

View File

@ -118,9 +118,7 @@ ca:
email_status: Estat del correu electrònic email_status: Estat del correu electrònic
enable: Habilita enable: Habilita
enabled: Habilitat enabled: Habilitat
feed_url: URL del canal
followers: Seguidors followers: Seguidors
followers_url: URL dels seguidors
follows: Segueix follows: Segueix
header: Capçalera header: Capçalera
inbox_url: URL de la safata d'entrada inbox_url: URL de la safata d'entrada
@ -148,10 +146,8 @@ ca:
no_account_selected: No s'han canviat els comptes perque no s'han seleccionat no_account_selected: No s'han canviat els comptes perque no s'han seleccionat
no_limits_imposed: Sense límits imposats no_limits_imposed: Sense límits imposats
not_subscribed: No subscrit not_subscribed: No subscrit
outbox_url: URL de la bústia de sortida
pending: Revisió pendent pending: Revisió pendent
perform_full_suspension: Suspèn perform_full_suspension: Suspèn
profile_url: URL del perfil
promote: Promociona promote: Promociona
protocol: Protocol protocol: Protocol
public: Públic public: Públic
@ -174,7 +170,6 @@ ca:
moderator: Moderador moderator: Moderador
staff: Personal staff: Personal
user: Usuari user: Usuari
salmon_url: URL Salmon
search: Cerca search: Cerca
search_same_ip: Altres usuaris amb la mateixa IP search_same_ip: Altres usuaris amb la mateixa IP
shared_inbox_url: URL de la safata d'entrada compartida shared_inbox_url: URL de la safata d'entrada compartida
@ -398,9 +393,6 @@ ca:
created_msg: La nota del informe s'ha creat correctament! created_msg: La nota del informe s'ha creat correctament!
destroyed_msg: La nota del informe s'ha esborrat correctament! destroyed_msg: La nota del informe s'ha esborrat correctament!
reports: reports:
account:
note: nota
report: informe
action_taken_by: Mesures adoptades per action_taken_by: Mesures adoptades per
are_you_sure: N'estàs segur? are_you_sure: N'estàs segur?
assign_to_self: Assignar-me assign_to_self: Assignar-me

View File

@ -118,9 +118,7 @@ co:
email_status: Statutu di le-mail email_status: Statutu di le-mail
enable: Attivà enable: Attivà
enabled: Attivatu enabled: Attivatu
feed_url: URL di u flussu
followers: Abbunati followers: Abbunati
followers_url: URL di labbunati
follows: Abbunamenti follows: Abbunamenti
header: Intistatura header: Intistatura
inbox_url: URL di linbox inbox_url: URL di linbox
@ -148,10 +146,8 @@ co:
no_account_selected: Nisun contu hè statu cambiatu postu ch'ùn c'eranu micca selezziunati no_account_selected: Nisun contu hè statu cambiatu postu ch'ùn c'eranu micca selezziunati
no_limits_imposed: Nisuna limita imposta no_limits_imposed: Nisuna limita imposta
not_subscribed: Micca abbunatu not_subscribed: Micca abbunatu
outbox_url: URL di loutbox
pending: In attesa di rivista pending: In attesa di rivista
perform_full_suspension: Suspende perform_full_suspension: Suspende
profile_url: URL di u prufile
promote: Prumove promote: Prumove
protocol: Prutucollu protocol: Prutucollu
public: Pubblicu public: Pubblicu
@ -174,7 +170,6 @@ co:
moderator: Muderatore moderator: Muderatore
staff: Squadra staff: Squadra
user: Utilizatore user: Utilizatore
salmon_url: URL di Salmon
search: Cercà search: Cercà
search_same_ip: Altri utilizatori cù listessa IP search_same_ip: Altri utilizatori cù listessa IP
shared_inbox_url: URL di linbox spartuta shared_inbox_url: URL di linbox spartuta
@ -398,9 +393,6 @@ co:
created_msg: Nota di signalamentu creata! created_msg: Nota di signalamentu creata!
destroyed_msg: Nota di signalamentu sguassata! destroyed_msg: Nota di signalamentu sguassata!
reports: reports:
account:
note: nota
report: palisà
action_taken_by: Intervenzione di action_taken_by: Intervenzione di
are_you_sure: Site sicuru·a? are_you_sure: Site sicuru·a?
assign_to_self: Assignallu à mè assign_to_self: Assignallu à mè

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