Compare commits
29 commits
Author | SHA1 | Date | |
---|---|---|---|
1ebcdf84b5 | |||
791c0edfe8 | |||
17c2ba7366 | |||
f4ab6b9f2c | |||
f63ccd8787 | |||
7a1903cdf7 | |||
4142036f2b | |||
24656a9f8f | |||
47910f57da | |||
2a4499bb23 | |||
e258d05848 | |||
7c81c0f7cb | |||
1ec81ccec0 | |||
3fa8505769 | |||
011bcbf21f | |||
6164662e1f | |||
ed4d2d8a69 | |||
453c15ab76 | |||
2dc2003b99 | |||
4d1217d8f9 | |||
0468256ab0 | |||
634110d8bd | |||
40d390e85d | |||
31e086de0e | |||
196e44b30b | |||
945e5bf9cc | |||
a24d55eed8 | |||
7f7a67a7c1 | |||
300bc70123 |
|
@ -1,6 +1,12 @@
|
|||
engines:
|
||||
duplication:
|
||||
enabled: false
|
||||
enabled: true
|
||||
exclude_paths:
|
||||
- app/assets/javascripts/components/locales/
|
||||
config:
|
||||
languages:
|
||||
- ruby
|
||||
- javascript
|
||||
rubocop:
|
||||
enabled: true
|
||||
eslint:
|
||||
|
|
|
@ -6,6 +6,3 @@ node_modules
|
|||
storybook
|
||||
neo4j
|
||||
vendor/bundle
|
||||
.DS_Store
|
||||
*.swp
|
||||
*~
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# Service dependencies
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
# REDIS_DB=0
|
||||
DB_HOST=db
|
||||
DB_USER=postgres
|
||||
DB_NAME=postgres
|
||||
|
@ -12,10 +11,6 @@ DB_PORT=5432
|
|||
LOCAL_DOMAIN=example.com
|
||||
LOCAL_HTTPS=true
|
||||
|
||||
# Use this only if you need to run mastodon on a different domain than the one used for federation.
|
||||
# Do not use this unless you know exactly what you are doing.
|
||||
# WEB_DOMAIN=mastodon.example.com
|
||||
|
||||
# Application secrets
|
||||
# Generate each with the `rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
|
||||
PAPERCLIP_SECRET=
|
||||
|
@ -46,10 +41,6 @@ SMTP_FROM_ADDRESS=notifications@example.com
|
|||
#SMTP_ENABLE_STARTTLS_AUTO=true
|
||||
|
||||
|
||||
# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files.
|
||||
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system
|
||||
# PAPERCLIP_ROOT_URL=/system
|
||||
|
||||
# Optional asset host for multi-server setups
|
||||
# CDN_HOST=assets.example.com
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# Federation
|
||||
LOCAL_DOMAIN=cb6e6126.ngrok.io
|
||||
LOCAL_HTTPS=true
|
||||
OTP_SECRET=100c7faeef00caa29242f6b04156742bf76065771fd4117990c4282b8748ff3d99f8fdae97c982ab5bd2e6756a159121377cce4421f4a8ecd2d67bd7749a3fb4
|
||||
|
|
34
.eslintrc
34
.eslintrc
|
@ -8,8 +8,7 @@
|
|||
"parser": "babel-eslint",
|
||||
|
||||
"plugins": [
|
||||
"react",
|
||||
"jsx-a11y"
|
||||
"react"
|
||||
],
|
||||
|
||||
"parserOptions": {
|
||||
|
@ -44,36 +43,9 @@
|
|||
"no-mixed-spaces-and-tabs": 1,
|
||||
"no-nested-ternary": 1,
|
||||
"no-trailing-spaces": 1,
|
||||
|
||||
"react/jsx-wrap-multilines": 2,
|
||||
"react/wrap-multilines": 2,
|
||||
"react/self-closing-comp": 2,
|
||||
"react/prop-types": 2,
|
||||
"react/no-multi-comp": 0,
|
||||
|
||||
"jsx-a11y/accessible-emoji": 1,
|
||||
"jsx-a11y/anchor-has-content": 1,
|
||||
"jsx-a11y/aria-activedescendant-has-tabindex": 1,
|
||||
"jsx-a11y/aria-props": 1,
|
||||
"jsx-a11y/aria-proptypes": 1,
|
||||
"jsx-a11y/aria-role": 1,
|
||||
"jsx-a11y/aria-unsupported-elements": 1,
|
||||
"jsx-a11y/heading-has-content": 1,
|
||||
"jsx-a11y/href-no-hash": 1,
|
||||
"jsx-a11y/html-has-lang": 1,
|
||||
"jsx-a11y/iframe-has-title": 1,
|
||||
"jsx-a11y/img-has-alt": 1,
|
||||
"jsx-a11y/img-redundant-alt": 1,
|
||||
"jsx-a11y/label-has-for": 1,
|
||||
"jsx-a11y/mouse-events-have-key-events": 1,
|
||||
"jsx-a11y/no-access-key": 1,
|
||||
"jsx-a11y/no-distracting-elements": 1,
|
||||
"jsx-a11y/no-onchange": 1,
|
||||
"jsx-a11y/no-redundant-roles": 1,
|
||||
"jsx-a11y/onclick-has-focus": 1,
|
||||
"jsx-a11y/onclick-has-role": 1,
|
||||
"jsx-a11y/role-has-required-aria-props": 1,
|
||||
"jsx-a11y/role-supports-aria-props": 1,
|
||||
"jsx-a11y/scope": 1,
|
||||
"jsx-a11y/tabindex-no-positive": 1
|
||||
"react/no-multi-comp": 0
|
||||
}
|
||||
}
|
||||
|
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -29,16 +29,10 @@ neo4j/
|
|||
# Ignore Capistrano customizations
|
||||
config/deploy/*
|
||||
|
||||
|
||||
# Ignore IDE files
|
||||
.vscode/
|
||||
|
||||
# Ignore postgres + redis volume optionally created by docker-compose
|
||||
postgres
|
||||
redis
|
||||
|
||||
# Ignore Apple files
|
||||
.DS_Store
|
||||
|
||||
# Ignore vim files
|
||||
*~
|
||||
*.swp
|
||||
|
|
|
@ -5,6 +5,8 @@ notifications:
|
|||
email: false
|
||||
|
||||
env:
|
||||
matrix:
|
||||
- TRAVIS_NODE_VERSION="4"
|
||||
global:
|
||||
- LOCAL_DOMAIN=cb6e6126.ngrok.io
|
||||
- LOCAL_HTTPS=true
|
||||
|
@ -14,7 +16,6 @@ addons:
|
|||
postgresql: 9.4
|
||||
|
||||
rvm:
|
||||
- 2.3.4
|
||||
- 2.4.1
|
||||
|
||||
services:
|
||||
|
@ -27,7 +28,8 @@ before_install:
|
|||
- sudo apt-get -qq update
|
||||
- sudo apt-get -qq install g++-4.8
|
||||
install:
|
||||
- nvm install
|
||||
- nvm install $TRAVIS_NODE_VERSION
|
||||
- npm install -g npm@3
|
||||
- npm install -g yarn
|
||||
- bundle install
|
||||
- yarn install
|
||||
|
@ -38,4 +40,3 @@ before_script:
|
|||
script:
|
||||
- bundle exec rspec
|
||||
- npm test
|
||||
- i18n-tasks unused
|
||||
|
|
10
Dockerfile
10
Dockerfile
|
@ -12,25 +12,23 @@ WORKDIR /mastodon
|
|||
|
||||
COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
|
||||
|
||||
RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \
|
||||
&& BUILD_DEPS=" \
|
||||
RUN BUILD_DEPS=" \
|
||||
postgresql-dev \
|
||||
libxml2-dev \
|
||||
libxslt-dev \
|
||||
build-base" \
|
||||
&& apk -U upgrade && apk add \
|
||||
$BUILD_DEPS \
|
||||
nodejs@edge \
|
||||
nodejs-npm@edge \
|
||||
nodejs \
|
||||
libpq \
|
||||
libxml2 \
|
||||
libxslt \
|
||||
ffmpeg \
|
||||
file \
|
||||
imagemagick@edge \
|
||||
imagemagick \
|
||||
&& npm install -g npm@3 && npm install -g yarn \
|
||||
&& bundle install --deployment --without test development \
|
||||
&& yarn --ignore-optional \
|
||||
&& yarn \
|
||||
&& yarn cache clean \
|
||||
&& npm -g cache clean \
|
||||
&& apk del $BUILD_DEPS \
|
||||
|
|
18
Gemfile
18
Gemfile
|
@ -1,13 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
ruby '>= 2.3.0', '< 2.5.0'
|
||||
|
||||
gem 'pkg-config'
|
||||
ruby '2.4.1'
|
||||
|
||||
gem 'rails', '~> 5.0.2'
|
||||
gem 'sass-rails', '~> 5.0'
|
||||
gem 'uglifier', '>= 1.3.0'
|
||||
gem 'coffee-rails', '~> 4.1.0'
|
||||
gem 'jquery-rails'
|
||||
gem 'puma'
|
||||
|
||||
|
@ -21,7 +20,6 @@ gem 'best_in_place', '~> 3.0.1'
|
|||
gem 'paperclip', '~> 5.1'
|
||||
gem 'paperclip-av-transcoder'
|
||||
gem 'aws-sdk', '>= 2.0'
|
||||
gem 'fog'
|
||||
|
||||
gem 'addressable'
|
||||
gem 'devise'
|
||||
|
@ -38,13 +36,12 @@ gem 'kaminari'
|
|||
gem 'link_header'
|
||||
gem 'nokogiri'
|
||||
gem 'oj'
|
||||
gem 'ostatus2', '~> 1.1'
|
||||
gem 'ostatus2'
|
||||
gem 'ox'
|
||||
gem 'rabl'
|
||||
gem 'rack-attack'
|
||||
gem 'rack-cors', require: 'rack/cors'
|
||||
gem 'rack-timeout'
|
||||
gem 'rails-i18n'
|
||||
gem 'rails-settings-cached'
|
||||
gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
|
||||
gem 'rqrcode'
|
||||
|
@ -53,16 +50,17 @@ gem 'sidekiq'
|
|||
gem 'sidekiq-unique-jobs'
|
||||
gem 'simple-navigation'
|
||||
gem 'simple_form'
|
||||
gem 'sprockets-rails', :require => 'sprockets/railtie'
|
||||
gem 'statsd-instrument'
|
||||
gem 'twitter-text'
|
||||
gem 'tzinfo-data'
|
||||
gem 'whatlanguage'
|
||||
|
||||
gem 'react-rails'
|
||||
gem 'browserify-rails'
|
||||
gem 'autoprefixer-rails'
|
||||
|
||||
gem 'skylight'
|
||||
gem 'sidekiq-skylight'
|
||||
|
||||
group :development, :test do
|
||||
gem 'rspec-rails'
|
||||
gem 'pry-rails'
|
||||
|
@ -72,9 +70,7 @@ group :development, :test do
|
|||
end
|
||||
|
||||
group :test do
|
||||
gem 'capybara'
|
||||
gem 'faker'
|
||||
gem 'microformats2'
|
||||
gem 'rails-controller-testing'
|
||||
gem 'rspec-sidekiq'
|
||||
gem 'simplecov', require: false
|
||||
|
@ -90,7 +86,7 @@ group :development do
|
|||
gem 'bullet'
|
||||
gem 'active_record_query_trace'
|
||||
|
||||
gem 'capistrano', '3.8.0'
|
||||
gem 'capistrano'
|
||||
gem 'capistrano-rails'
|
||||
gem 'capistrano-rbenv'
|
||||
gem 'capistrano-yarn'
|
||||
|
|
192
Gemfile.lock
192
Gemfile.lock
|
@ -1,7 +1,6 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (2.3.5)
|
||||
actioncable (5.0.2)
|
||||
actionpack (= 5.0.2)
|
||||
nio4r (>= 1.2, < 3.0)
|
||||
|
@ -42,7 +41,7 @@ GEM
|
|||
tzinfo (~> 1.1)
|
||||
addressable (2.5.1)
|
||||
public_suffix (~> 2.0, >= 2.0.2)
|
||||
airbrussh (1.2.0)
|
||||
airbrussh (1.1.2)
|
||||
sshkit (>= 1.6.1, != 1.7.0)
|
||||
arel (7.1.4)
|
||||
ast (2.3.0)
|
||||
|
@ -100,18 +99,18 @@ GEM
|
|||
sshkit (~> 1.3)
|
||||
capistrano-yarn (2.0.2)
|
||||
capistrano (~> 3.0)
|
||||
capybara (2.13.0)
|
||||
addressable
|
||||
mime-types (>= 1.16)
|
||||
nokogiri (>= 1.3.3)
|
||||
rack (>= 1.0.0)
|
||||
rack-test (>= 0.5.4)
|
||||
xpath (~> 2.0)
|
||||
chunky_png (1.3.8)
|
||||
climate_control (0.1.0)
|
||||
cocaine (0.5.8)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
coderay (1.1.1)
|
||||
coffee-rails (4.1.1)
|
||||
coffee-script (>= 2.2.0)
|
||||
railties (>= 4.0.0, < 5.1.x)
|
||||
coffee-script (2.4.1)
|
||||
coffee-script-source
|
||||
execjs
|
||||
coffee-script-source (1.12.2)
|
||||
colorize (0.8.1)
|
||||
concurrent-ruby (1.0.5)
|
||||
connection_pool (2.2.1)
|
||||
|
@ -146,142 +145,13 @@ GEM
|
|||
thread_safe
|
||||
encryptor (3.0.0)
|
||||
erubis (2.7.0)
|
||||
excon (0.55.0)
|
||||
execjs (2.7.0)
|
||||
fabrication (2.16.1)
|
||||
faker (1.7.3)
|
||||
i18n (~> 0.5)
|
||||
fast_blank (1.0.0)
|
||||
fission (0.5.0)
|
||||
CFPropertyList (~> 2.2)
|
||||
fog (1.38.0)
|
||||
fog-aliyun (>= 0.1.0)
|
||||
fog-atmos
|
||||
fog-aws (>= 0.6.0)
|
||||
fog-brightbox (~> 0.4)
|
||||
fog-cloudatcost (~> 0.1.0)
|
||||
fog-core (~> 1.32)
|
||||
fog-dynect (~> 0.0.2)
|
||||
fog-ecloud (~> 0.1)
|
||||
fog-google (<= 0.1.0)
|
||||
fog-json
|
||||
fog-local
|
||||
fog-openstack
|
||||
fog-powerdns (>= 0.1.1)
|
||||
fog-profitbricks
|
||||
fog-rackspace
|
||||
fog-radosgw (>= 0.0.2)
|
||||
fog-riakcs
|
||||
fog-sakuracloud (>= 0.0.4)
|
||||
fog-serverlove
|
||||
fog-softlayer
|
||||
fog-storm_on_demand
|
||||
fog-terremark
|
||||
fog-vmfusion
|
||||
fog-voxel
|
||||
fog-vsphere (>= 0.4.0)
|
||||
fog-xenserver
|
||||
fog-xml (~> 0.1.1)
|
||||
ipaddress (~> 0.5)
|
||||
fog-aliyun (0.1.0)
|
||||
fog-core (~> 1.27)
|
||||
fog-json (~> 1.0)
|
||||
ipaddress (~> 0.8)
|
||||
xml-simple (~> 1.1)
|
||||
fog-atmos (0.1.0)
|
||||
fog-core
|
||||
fog-xml
|
||||
fog-aws (1.3.0)
|
||||
fog-core (~> 1.38)
|
||||
fog-json (~> 1.0)
|
||||
fog-xml (~> 0.1)
|
||||
ipaddress (~> 0.8)
|
||||
fog-brightbox (0.11.0)
|
||||
fog-core (~> 1.22)
|
||||
fog-json
|
||||
inflecto (~> 0.0.2)
|
||||
fog-cloudatcost (0.1.2)
|
||||
fog-core (~> 1.36)
|
||||
fog-json (~> 1.0)
|
||||
fog-xml (~> 0.1)
|
||||
ipaddress (~> 0.8)
|
||||
fog-core (1.43.0)
|
||||
builder
|
||||
excon (~> 0.49)
|
||||
formatador (~> 0.2)
|
||||
fog-dynect (0.0.3)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-xml
|
||||
fog-ecloud (0.3.0)
|
||||
fog-core
|
||||
fog-xml
|
||||
fog-google (0.1.0)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-xml
|
||||
fog-json (1.0.2)
|
||||
fog-core (~> 1.0)
|
||||
multi_json (~> 1.10)
|
||||
fog-local (0.3.1)
|
||||
fog-core (~> 1.27)
|
||||
fog-openstack (0.1.20)
|
||||
fog-core (>= 1.40)
|
||||
fog-json (>= 1.0)
|
||||
ipaddress (>= 0.8)
|
||||
fog-powerdns (0.1.1)
|
||||
fog-core (~> 1.27)
|
||||
fog-json (~> 1.0)
|
||||
fog-xml (~> 0.1)
|
||||
fog-profitbricks (3.0.0)
|
||||
fog-core (~> 1.42)
|
||||
fog-json (~> 1.0)
|
||||
fog-rackspace (0.1.4)
|
||||
fog-core (>= 1.35)
|
||||
fog-json (>= 1.0)
|
||||
fog-xml (>= 0.1)
|
||||
ipaddress (>= 0.8)
|
||||
fog-radosgw (0.0.5)
|
||||
fog-core (>= 1.21.0)
|
||||
fog-json
|
||||
fog-xml (>= 0.0.1)
|
||||
fog-riakcs (0.1.0)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-xml
|
||||
fog-sakuracloud (1.7.5)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-serverlove (0.1.2)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-softlayer (1.1.4)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-storm_on_demand (0.1.1)
|
||||
fog-core
|
||||
fog-json
|
||||
fog-terremark (0.1.0)
|
||||
fog-core
|
||||
fog-xml
|
||||
fog-vmfusion (0.1.0)
|
||||
fission
|
||||
fog-core
|
||||
fog-voxel (0.1.0)
|
||||
fog-core
|
||||
fog-xml
|
||||
fog-vsphere (1.9.1)
|
||||
fog-core
|
||||
rbvmomi (~> 1.9)
|
||||
fog-xenserver (0.3.0)
|
||||
fog-core
|
||||
fog-xml
|
||||
fog-xml (0.1.3)
|
||||
fog-core
|
||||
nokogiri (>= 1.5.11, < 2.0.0)
|
||||
font-awesome-rails (4.7.0.1)
|
||||
railties (>= 3.2, < 5.1)
|
||||
formatador (0.2.5)
|
||||
fuubar (2.2.0)
|
||||
rspec-core (~> 3.0)
|
||||
ruby-progressbar (~> 1.4)
|
||||
|
@ -327,8 +197,6 @@ GEM
|
|||
parser (>= 2.2.3.0)
|
||||
rainbow (~> 2.2)
|
||||
terminal-table (>= 1.5.1)
|
||||
inflecto (0.0.2)
|
||||
ipaddress (0.8.3)
|
||||
jmespath (1.3.1)
|
||||
jquery-rails (4.3.1)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
|
@ -365,17 +233,12 @@ GEM
|
|||
mail (2.6.4)
|
||||
mime-types (>= 1.16, < 4)
|
||||
method_source (0.8.2)
|
||||
microformats2 (2.1.0)
|
||||
activesupport
|
||||
json
|
||||
nokogiri
|
||||
mime-types (3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2016.0521)
|
||||
mimemagic (0.3.2)
|
||||
mini_portile2 (2.1.0)
|
||||
minitest (5.10.1)
|
||||
multi_json (1.12.1)
|
||||
net-scp (1.2.1)
|
||||
net-ssh (>= 2.6.5)
|
||||
net-ssh (4.1.0)
|
||||
|
@ -383,13 +246,11 @@ GEM
|
|||
nokogiri (1.7.1)
|
||||
mini_portile2 (~> 2.1.0)
|
||||
oj (2.18.5)
|
||||
openssl (2.0.3)
|
||||
orm_adapter (0.5.0)
|
||||
ostatus2 (1.1.0)
|
||||
ostatus2 (1.0.2)
|
||||
addressable (~> 2.4)
|
||||
http (~> 2.0)
|
||||
nokogiri (~> 1.6)
|
||||
openssl (~> 2.0)
|
||||
ox (2.4.11)
|
||||
paperclip (5.1.0)
|
||||
activemodel (>= 4.2.0)
|
||||
|
@ -405,7 +266,6 @@ GEM
|
|||
pg (0.20.0)
|
||||
pghero (1.6.4)
|
||||
activerecord
|
||||
pkg-config (1.1.7)
|
||||
powerpack (0.1.1)
|
||||
pry (0.10.4)
|
||||
coderay (~> 1.1.0)
|
||||
|
@ -447,9 +307,6 @@ GEM
|
|||
nokogiri (~> 1.6)
|
||||
rails-html-sanitizer (1.0.3)
|
||||
loofah (~> 2.0)
|
||||
rails-i18n (5.0.3)
|
||||
i18n (~> 0.7)
|
||||
railties (~> 5.0)
|
||||
rails-settings-cached (0.6.5)
|
||||
rails (>= 4.2.0)
|
||||
rails_12factor (0.0.3)
|
||||
|
@ -465,11 +322,6 @@ GEM
|
|||
thor (>= 0.18.1, < 2.0)
|
||||
rainbow (2.2.1)
|
||||
rake (12.0.0)
|
||||
rbvmomi (1.11.0)
|
||||
builder (~> 3.0)
|
||||
json (>= 1.8)
|
||||
nokogiri (~> 1.5)
|
||||
trollop (~> 2.1)
|
||||
react-rails (1.11.0)
|
||||
babel-transpiler (>= 0.7.0)
|
||||
connection_pool
|
||||
|
@ -539,6 +391,9 @@ GEM
|
|||
connection_pool (~> 2.2, >= 2.2.0)
|
||||
rack-protection (>= 1.5.0)
|
||||
redis (~> 3.2, >= 3.2.1)
|
||||
sidekiq-skylight (0.2.0)
|
||||
sidekiq (>= 3.3.0)
|
||||
skylight (>= 0.5.2)
|
||||
sidekiq-unique-jobs (5.0.0)
|
||||
sidekiq (>= 4.0)
|
||||
thor
|
||||
|
@ -552,6 +407,8 @@ GEM
|
|||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.0)
|
||||
skylight (1.2.0)
|
||||
activesupport (>= 3.0.0)
|
||||
slop (3.6.0)
|
||||
sprockets (3.7.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
|
@ -571,7 +428,6 @@ GEM
|
|||
thread (0.2.2)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.7)
|
||||
trollop (2.1.2)
|
||||
twitter-text (1.14.5)
|
||||
unf (~> 0.1.0)
|
||||
tzinfo (1.2.3)
|
||||
|
@ -582,7 +438,7 @@ GEM
|
|||
execjs (>= 0.3.0, < 3)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.3)
|
||||
unf_ext (0.0.7.2)
|
||||
unicode-display_width (1.1.3)
|
||||
uniform_notifier (1.10.0)
|
||||
warden (1.2.7)
|
||||
|
@ -594,10 +450,6 @@ GEM
|
|||
websocket-driver (0.6.5)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.2)
|
||||
whatlanguage (1.0.6)
|
||||
xml-simple (1.1.5)
|
||||
xpath (2.0.0)
|
||||
nokogiri (~> 1.3)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
@ -612,12 +464,12 @@ DEPENDENCIES
|
|||
binding_of_caller
|
||||
browserify-rails
|
||||
bullet
|
||||
capistrano (= 3.8.0)
|
||||
capistrano
|
||||
capistrano-faster-assets (~> 1.0)
|
||||
capistrano-rails
|
||||
capistrano-rbenv
|
||||
capistrano-yarn
|
||||
capybara
|
||||
coffee-rails (~> 4.1.0)
|
||||
devise
|
||||
devise-two-factor
|
||||
doorkeeper
|
||||
|
@ -625,7 +477,6 @@ DEPENDENCIES
|
|||
fabrication
|
||||
faker
|
||||
fast_blank
|
||||
fog
|
||||
font-awesome-rails
|
||||
fuubar
|
||||
goldfinger
|
||||
|
@ -642,16 +493,14 @@ DEPENDENCIES
|
|||
letter_opener_web
|
||||
link_header
|
||||
lograge
|
||||
microformats2
|
||||
nokogiri
|
||||
oj
|
||||
ostatus2 (~> 1.1)
|
||||
ostatus2
|
||||
ox
|
||||
paperclip (~> 5.1)
|
||||
paperclip-av-transcoder
|
||||
pg
|
||||
pghero
|
||||
pkg-config
|
||||
pry-rails
|
||||
puma
|
||||
rabl
|
||||
|
@ -660,7 +509,6 @@ DEPENDENCIES
|
|||
rack-timeout
|
||||
rails (~> 5.0.2)
|
||||
rails-controller-testing
|
||||
rails-i18n
|
||||
rails-settings-cached
|
||||
rails_12factor
|
||||
react-rails
|
||||
|
@ -673,17 +521,17 @@ DEPENDENCIES
|
|||
ruby-oembed
|
||||
sass-rails (~> 5.0)
|
||||
sidekiq
|
||||
sidekiq-skylight
|
||||
sidekiq-unique-jobs
|
||||
simple-navigation
|
||||
simple_form
|
||||
simplecov
|
||||
sprockets-rails
|
||||
skylight
|
||||
statsd-instrument
|
||||
twitter-text
|
||||
tzinfo-data
|
||||
uglifier (>= 1.3.0)
|
||||
webmock
|
||||
whatlanguage
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.4.1p111
|
||||
|
|
|
@ -3,4 +3,3 @@
|
|||
* * * *
|
||||
|
||||
- [ ] I searched or browsed the repo’s other issues to ensure this is not a duplicate.
|
||||
- [ ] This bug happens on a [tagged release](https://github.com/tootsuite/mastodon/releases) and not on `master` (If you're a user, don't worry about this).
|
||||
|
|
17
README.md
17
README.md
|
@ -48,14 +48,6 @@ If you would like, you can [support the development of this project on Patreon][
|
|||
- **Deployable via Docker**
|
||||
You don't need to mess with dependencies and configuration if you want to try Mastodon, if you have Docker and Docker Compose the deployment is extremely easy
|
||||
|
||||
## Checking out
|
||||
|
||||
If you want a stable release for production use, you should use tagged releases. To checkout the latest available tagged version:
|
||||
|
||||
git clone https://github.com/tootsuite/mastodon.git
|
||||
cd mastodon
|
||||
git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
## Configuration
|
||||
|
||||
- `LOCAL_DOMAIN` should be the domain/hostname of your instance. This is **absolutely required** as it is used for generating unique IDs for everything federation-related
|
||||
|
@ -80,6 +72,10 @@ The project now includes a `Dockerfile` and a `docker-compose.yml` file (which r
|
|||
Review the settings in `docker-compose.yml`. Note that it is not default to store the postgresql database and redis databases in a persistent storage location,
|
||||
so you may need or want to adjust the settings there.
|
||||
|
||||
Before running the first time, you need to build the images:
|
||||
|
||||
docker-compose build
|
||||
|
||||
Then, you need to fill in the `.env.production` file:
|
||||
|
||||
cp .env.production.sample .env.production
|
||||
|
@ -89,11 +85,6 @@ Do NOT change the `REDIS_*` or `DB_*` settings when running with the default doc
|
|||
|
||||
You will need to fill in, at least: `LOCAL_DOMAIN`, `LOCAL_HTTPS`, `PAPERCLIP_SECRET`, `SECRET_KEY_BASE`, `OTP_SECRET`, and the `SMTP_*` settings. To generate the `PAPERCLIP_SECRET`, `SECRET_KEY_BASE`, and `OTP_SECRET`, you may use:
|
||||
|
||||
Before running the first time, you need to build the images:
|
||||
|
||||
docker-compose build
|
||||
|
||||
|
||||
docker-compose run --rm web rake secret
|
||||
|
||||
Do this once for each of those keys, and copy the result into the `.env.production` file in the appropriate field.
|
||||
|
|
2
Vagrantfile
vendored
2
Vagrantfile
vendored
|
@ -43,7 +43,7 @@ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
|
|||
|
||||
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
|
||||
|
||||
export PATH="$HOME/.rbenv/bin:$PATH"
|
||||
export PATH="$HOME/.rbenv/bin::$PATH"
|
||||
eval "$(rbenv init -)"
|
||||
|
||||
cd /vagrant
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 24 KiB |
|
@ -1,82 +0,0 @@
|
|||
import api, { getLinks } from '../api'
|
||||
import { fetchRelationships } from './accounts';
|
||||
|
||||
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
|
||||
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
|
||||
export const MUTES_FETCH_FAIL = 'MUTES_FETCH_FAIL';
|
||||
|
||||
export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
|
||||
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
|
||||
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
|
||||
|
||||
export function fetchMutes() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(fetchMutesRequest());
|
||||
|
||||
api(getState).get('/api/v1/mutes').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(fetchMutesSuccess(response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
}).catch(error => dispatch(fetchMutesFail(error)));
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchMutesRequest() {
|
||||
return {
|
||||
type: MUTES_FETCH_REQUEST
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchMutesSuccess(accounts, next) {
|
||||
return {
|
||||
type: MUTES_FETCH_SUCCESS,
|
||||
accounts,
|
||||
next
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchMutesFail(error) {
|
||||
return {
|
||||
type: MUTES_FETCH_FAIL,
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
export function expandMutes() {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['user_lists', 'mutes', 'next']);
|
||||
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandMutesRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(expandMutesSuccess(response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
}).catch(error => dispatch(expandMutesFail(error)));
|
||||
};
|
||||
};
|
||||
|
||||
export function expandMutesRequest() {
|
||||
return {
|
||||
type: MUTES_EXPAND_REQUEST
|
||||
};
|
||||
};
|
||||
|
||||
export function expandMutesSuccess(accounts, next) {
|
||||
return {
|
||||
type: MUTES_EXPAND_SUCCESS,
|
||||
accounts,
|
||||
next
|
||||
};
|
||||
};
|
||||
|
||||
export function expandMutesFail(error) {
|
||||
return {
|
||||
type: MUTES_EXPAND_FAIL,
|
||||
error
|
||||
};
|
||||
};
|
|
@ -1,14 +0,0 @@
|
|||
import { openModal } from './modal';
|
||||
import { changeSetting, saveSettings } from './settings';
|
||||
|
||||
export function showOnboardingOnce() {
|
||||
return (dispatch, getState) => {
|
||||
const alreadySeen = getState().getIn(['settings', 'onboarded']);
|
||||
|
||||
if (!alreadySeen) {
|
||||
dispatch(openModal('ONBOARDING'));
|
||||
dispatch(changeSetting(['onboarded'], true));
|
||||
dispatch(saveSettings());
|
||||
}
|
||||
};
|
||||
};
|
|
@ -10,8 +10,7 @@ const messages = defineMessages({
|
|||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock' },
|
||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute' }
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock' }
|
||||
});
|
||||
|
||||
const buttonsStyle = {
|
||||
|
@ -26,7 +25,6 @@ const Account = React.createClass({
|
|||
me: React.PropTypes.number.isRequired,
|
||||
onFollow: React.PropTypes.func.isRequired,
|
||||
onBlock: React.PropTypes.func.isRequired,
|
||||
onMute: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
|
@ -40,10 +38,6 @@ const Account = React.createClass({
|
|||
this.props.onBlock(this.props.account);
|
||||
},
|
||||
|
||||
handleMute () {
|
||||
this.props.onMute(this.props.account);
|
||||
},
|
||||
|
||||
render () {
|
||||
const { account, me, intl } = this.props;
|
||||
|
||||
|
@ -57,14 +51,11 @@ const Account = React.createClass({
|
|||
const following = account.getIn(['relationship', 'following']);
|
||||
const requested = account.getIn(['relationship', 'requested']);
|
||||
const blocking = account.getIn(['relationship', 'blocking']);
|
||||
const muting = account.getIn(['relationship', 'muting']);
|
||||
|
||||
if (requested) {
|
||||
buttons = <IconButton disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} />
|
||||
} else if (blocking) {
|
||||
buttons = <IconButton active={true} icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
|
||||
} else if (muting) {
|
||||
buttons = <IconButton active={true} icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />;
|
||||
buttons = <IconButton active={true} icon='unlock-alt' title={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
|
||||
} else {
|
||||
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
||||
}
|
||||
|
|
|
@ -178,12 +178,7 @@ const AutosuggestTextarea = React.createClass({
|
|||
|
||||
<div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'>
|
||||
{suggestions.map((suggestion, i) => (
|
||||
<div
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
key={suggestion}
|
||||
className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`}
|
||||
onClick={this.onSuggestionClick.bind(this, suggestion)}>
|
||||
<div key={suggestion} className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`} onClick={this.onSuggestionClick.bind(this, suggestion)}>
|
||||
<AutosuggestAccountContainer id={suggestion} />
|
||||
</div>
|
||||
))}
|
||||
|
|
|
@ -9,7 +9,6 @@ const Button = React.createClass({
|
|||
block: React.PropTypes.bool,
|
||||
secondary: React.PropTypes.bool,
|
||||
size: React.PropTypes.number,
|
||||
style: React.PropTypes.object,
|
||||
children: React.PropTypes.node
|
||||
},
|
||||
|
||||
|
|
|
@ -15,13 +15,13 @@ const ColumnBackButton = React.createClass({
|
|||
mixins: [PureRenderMixin],
|
||||
|
||||
handleClick () {
|
||||
if (window.history && window.history.length === 1) this.context.router.push("/");
|
||||
if (window.history && window.history.length == 1) this.context.router.push("/");
|
||||
else this.context.router.goBack();
|
||||
},
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button'>
|
||||
<div onClick={this.handleClick} className='column-back-button'>
|
||||
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</div>
|
||||
|
|
|
@ -31,7 +31,7 @@ const ColumnBackButtonSlim = React.createClass({
|
|||
render () {
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div role='button' tabIndex='0' style={outerStyle} onClick={this.handleClick} className='column-back-button'>
|
||||
<div style={outerStyle} onClick={this.handleClick} className='column-back-button'>
|
||||
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||
</div>
|
||||
|
|
|
@ -46,9 +46,7 @@ const ColumnCollapsable = React.createClass({
|
|||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div role='button' tabIndex='0' title={`${title}`} style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}>
|
||||
<i className={`fa fa-${icon}`} />
|
||||
</div>
|
||||
<div title={`${title}`} style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div>
|
||||
|
||||
<Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}>
|
||||
{({ opacity, height }) =>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const LoadMore = ({ onClick }) => (
|
||||
<a href="#" className='load-more' role='button' onClick={onClick}>
|
||||
<a href='#' className='load-more' onClick={onClick}>
|
||||
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
|
||||
</a>
|
||||
);
|
||||
|
|
|
@ -220,7 +220,7 @@ const MediaGallery = React.createClass({
|
|||
}
|
||||
|
||||
children = (
|
||||
<div role='button' tabIndex='0' style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
|
||||
<div style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
|
||||
<span style={spoilerSpanStyle}>{warning}</span>
|
||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
</div>
|
||||
|
|
|
@ -6,8 +6,7 @@ const Permalink = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
href: React.PropTypes.string.isRequired,
|
||||
to: React.PropTypes.string.isRequired,
|
||||
children: React.PropTypes.node
|
||||
to: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
handleClick (e) {
|
||||
|
|
|
@ -92,14 +92,10 @@ const StatusActionBar = React.createClass({
|
|||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
||||
}
|
||||
|
||||
let reblogIcon = 'retweet';
|
||||
if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
|
||||
else if (status.get('visibility') === 'private') reblogIcon = 'lock';
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
|
||||
<div style={{ float: 'left', marginRight: '18px'}}><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
|
||||
<div style={{ float: 'left', marginRight: '18px'}}><IconButton disabled={status.get('visibility') === 'private' || status.get('visibility') === 'direct'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
|
||||
<div style={{ float: 'left', marginRight: '18px'}}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div>
|
||||
<div style={{ float: 'left', marginRight: '18px'}}><IconButton disabled={status.get('visibility') === 'private' || status.get('visibility') === 'direct'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={status.get('visibility') === 'direct' ? 'envelope' : (status.get('visibility') === 'private' ? 'lock' : 'retweet')} onClick={this.handleReblogClick} /></div>
|
||||
<div style={{ float: 'left', marginRight: '18px'}}><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>
|
||||
|
||||
<div style={{ width: '18px', height: '18px', float: 'left' }}>
|
||||
|
|
|
@ -44,7 +44,6 @@ const StatusContent = React.createClass({
|
|||
} else {
|
||||
link.setAttribute('target', '_blank');
|
||||
link.setAttribute('rel', 'noopener');
|
||||
link.setAttribute('title', link.href);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -119,7 +118,7 @@ const StatusContent = React.createClass({
|
|||
return (
|
||||
<div className='status__content' style={{ cursor: 'pointer' }} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
||||
<p style={{ marginBottom: hidden && status.get('mentions').size === 0 ? '0px' : '' }} >
|
||||
<span dangerouslySetInnerHTML={spoilerContent} /> <a tabIndex='0' className='status__content__spoiler-link' role='button' onClick={this.handleSpoilerClick}>{toggleText}</a>
|
||||
<span dangerouslySetInnerHTML={spoilerContent} /> <a className='status__content__spoiler-link' onClick={this.handleSpoilerClick}>{toggleText}</a>
|
||||
</p>
|
||||
|
||||
{mentionsPlaceholder}
|
||||
|
|
|
@ -7,8 +7,7 @@ import { isIOS } from '../is_mobile';
|
|||
const messages = defineMessages({
|
||||
toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' },
|
||||
toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' },
|
||||
expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' },
|
||||
expand_video: { id: 'video_player.video_error', defaultMessage: 'Video could not be played' }
|
||||
expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' }
|
||||
});
|
||||
|
||||
const videoStyle = {
|
||||
|
@ -31,7 +30,7 @@ const muteStyle = {
|
|||
zIndex: '5'
|
||||
};
|
||||
|
||||
const coverStyle = {
|
||||
const spoilerStyle = {
|
||||
marginTop: '8px',
|
||||
textAlign: 'center',
|
||||
height: '100%',
|
||||
|
@ -95,8 +94,7 @@ const VideoPlayer = React.createClass({
|
|||
visible: !this.props.sensitive,
|
||||
preview: true,
|
||||
muted: true,
|
||||
hasAudio: true,
|
||||
videoError: false
|
||||
hasAudio: true
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -144,17 +142,12 @@ const VideoPlayer = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
handleVideoError () {
|
||||
this.setState({ videoError: true });
|
||||
},
|
||||
|
||||
componentDidMount () {
|
||||
if (!this.video) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.video.addEventListener('loadeddata', this.handleLoadedData);
|
||||
this.video.addEventListener('error', this.handleVideoError);
|
||||
},
|
||||
|
||||
componentDidUpdate () {
|
||||
|
@ -163,7 +156,6 @@ const VideoPlayer = React.createClass({
|
|||
}
|
||||
|
||||
this.video.addEventListener('loadeddata', this.handleLoadedData);
|
||||
this.video.addEventListener('error', this.handleVideoError);
|
||||
},
|
||||
|
||||
componentWillUnmount () {
|
||||
|
@ -172,7 +164,6 @@ const VideoPlayer = React.createClass({
|
|||
}
|
||||
|
||||
this.video.removeEventListener('loadeddata', this.handleLoadedData);
|
||||
this.video.removeEventListener('error', this.handleVideoError);
|
||||
},
|
||||
|
||||
render () {
|
||||
|
@ -203,7 +194,7 @@ const VideoPlayer = React.createClass({
|
|||
if (!this.state.visible) {
|
||||
if (sensitive) {
|
||||
return (
|
||||
<div role='button' tabIndex='0' style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||
<div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||
{spoilerButton}
|
||||
<span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
|
||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
|
@ -211,7 +202,7 @@ const VideoPlayer = React.createClass({
|
|||
);
|
||||
} else {
|
||||
return (
|
||||
<div role='button' tabIndex='0' style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||
<div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||
{spoilerButton}
|
||||
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
|
||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||
|
@ -222,27 +213,19 @@ const VideoPlayer = React.createClass({
|
|||
|
||||
if (this.state.preview && !autoplay) {
|
||||
return (
|
||||
<div role='button' tabIndex='0' style={{ cursor: 'pointer', position: 'relative', marginTop: '8px', width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }} onClick={this.handleOpen}>
|
||||
<div style={{ cursor: 'pointer', position: 'relative', marginTop: '8px', width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }} onClick={this.handleOpen}>
|
||||
{spoilerButton}
|
||||
<div style={{ position: 'absolute', top: '50%', left: '50%', fontSize: '36px', transform: 'translate(-50%, -50%)', padding: '5px', borderRadius: '100px', color: 'rgba(255, 255, 255, 0.8)' }}><i className='fa fa-play' /></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.videoError) {
|
||||
return (
|
||||
<div style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='video-error-cover' >
|
||||
<span style={spoilerSpanStyle}><FormattedMessage id='video_player.video_error' defaultMessage='Video could not be played' /></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
|
||||
{spoilerButton}
|
||||
{muteButton}
|
||||
{expandButton}
|
||||
<video role='button' tabIndex='0' ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
|
||||
<video ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
connectTimeline,
|
||||
disconnectTimeline
|
||||
} from '../actions/timelines';
|
||||
import { showOnboardingOnce } from '../actions/onboarding';
|
||||
import { updateNotifications, refreshNotifications } from '../actions/notifications';
|
||||
import createBrowserHistory from 'history/lib/createBrowserHistory';
|
||||
import {
|
||||
|
@ -38,7 +37,6 @@ import FollowRequests from '../features/follow_requests';
|
|||
import GenericNotFound from '../features/generic_not_found';
|
||||
import FavouritedStatuses from '../features/favourited_statuses';
|
||||
import Blocks from '../features/blocks';
|
||||
import Mutes from '../features/mutes';
|
||||
import Report from '../features/report';
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
import en from 'react-intl/locale-data/en';
|
||||
|
@ -62,8 +60,8 @@ import { hydrateStore } from '../actions/store';
|
|||
import createStream from '../stream';
|
||||
|
||||
const store = configureStore();
|
||||
const initialState = JSON.parse(document.getElementById("initial-state").textContent);
|
||||
store.dispatch(hydrateStore(initialState));
|
||||
|
||||
store.dispatch(hydrateStore(window.INITIAL_STATE));
|
||||
|
||||
const browserHistory = useRouterHistory(createBrowserHistory)({
|
||||
basename: '/web'
|
||||
|
@ -96,10 +94,9 @@ const Mastodon = React.createClass({
|
|||
|
||||
componentDidMount() {
|
||||
const { locale } = this.props;
|
||||
const streamingAPIBaseURL = store.getState().getIn(['meta', 'streaming_api_base_url']);
|
||||
const accessToken = store.getState().getIn(['meta', 'access_token']);
|
||||
|
||||
this.subscription = createStream(streamingAPIBaseURL, accessToken, 'user', {
|
||||
this.subscription = createStream(accessToken, 'user', {
|
||||
|
||||
connected () {
|
||||
store.dispatch(connectTimeline('home'));
|
||||
|
@ -135,8 +132,6 @@ const Mastodon = React.createClass({
|
|||
if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
|
||||
Notification.requestPermission();
|
||||
}
|
||||
|
||||
store.dispatch(showOnboardingOnce());
|
||||
},
|
||||
|
||||
componentWillUnmount () {
|
||||
|
@ -176,7 +171,6 @@ const Mastodon = React.createClass({
|
|||
|
||||
<Route path='follow_requests' component={FollowRequests} />
|
||||
<Route path='blocks' component={Blocks} />
|
||||
<Route path='mutes' component={Mutes} />
|
||||
<Route path='report' component={Report} />
|
||||
|
||||
<Route path='*' component={GenericNotFound} />
|
||||
|
|
|
@ -43,16 +43,7 @@ const Avatar = React.createClass({
|
|||
return (
|
||||
<Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
|
||||
{({ radius }) =>
|
||||
<a
|
||||
href={account.get('url')}
|
||||
className='account__header__avatar'
|
||||
target='_blank'
|
||||
rel='noopener'
|
||||
style={{ display: 'block', width: '90px', height: '90px', margin: '0 auto', marginBottom: '10px', borderRadius: `${radius}px`, overflow: 'hidden' }}
|
||||
onMouseOver={this.handleMouseOver}
|
||||
onMouseOut={this.handleMouseOut}
|
||||
onFocus={this.handleMouseOver}
|
||||
onBlur={this.handleMouseOut}>
|
||||
<a href={account.get('url')} className='account__header__avatar' target='_blank' rel='noopener' style={{ display: 'block', width: '90px', height: '90px', margin: '0 auto', marginBottom: '10px', borderRadius: `${radius}px`, overflow: 'hidden' }} onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
|
||||
<img src={account.get('avatar')} alt={account.get('acct')} style={{ display: 'block', width: '90px', height: '90px' }} />
|
||||
</a>
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ const messages = defineMessages({
|
|||
|
||||
const mapStateToProps = state => ({
|
||||
hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0,
|
||||
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
||||
accessToken: state.getIn(['meta', 'access_token'])
|
||||
});
|
||||
|
||||
|
@ -30,7 +29,6 @@ const CommunityTimeline = React.createClass({
|
|||
propTypes: {
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
streamingAPIBaseURL: React.PropTypes.string.isRequired,
|
||||
accessToken: React.PropTypes.string.isRequired,
|
||||
hasUnread: React.PropTypes.bool
|
||||
},
|
||||
|
@ -38,7 +36,7 @@ const CommunityTimeline = React.createClass({
|
|||
mixins: [PureRenderMixin],
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
|
||||
const { dispatch, accessToken } = this.props;
|
||||
|
||||
dispatch(refreshTimeline('community'));
|
||||
|
||||
|
@ -46,7 +44,7 @@ const CommunityTimeline = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
subscription = createStream(streamingAPIBaseURL, accessToken, 'public:local', {
|
||||
subscription = createStream(accessToken, 'public:local', {
|
||||
|
||||
connected () {
|
||||
dispatch(connectTimeline('community'));
|
||||
|
|
|
@ -92,7 +92,7 @@ const ComposeForm = React.createClass({
|
|||
},
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
// This statement does several things:
|
||||
// This statement does several things:
|
||||
// - If we're beginning a reply, and,
|
||||
// - Replying to zero or one users, places the cursor at the end of the textbox.
|
||||
// - Replying to more than one user, selects any usernames past the first;
|
||||
|
|
|
@ -83,7 +83,7 @@ const PrivacyDropdown = React.createClass({
|
|||
<div className='privacy-dropdown__value'><IconButton icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div>
|
||||
<div className='privacy-dropdown__dropdown'>
|
||||
{options.map(item =>
|
||||
<div role='button' tabIndex='0' key={item.value} onClick={this.handleClick.bind(this, item.value)} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
|
||||
<div key={item.value} onClick={this.handleClick.bind(this, item.value)} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
|
||||
<div className='privacy-dropdown__option__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div>
|
||||
<div className='privacy-dropdown__option__content'>
|
||||
<strong>{item.shortText}</strong>
|
||||
|
|
|
@ -36,10 +36,6 @@ const Search = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
noop () {
|
||||
|
||||
},
|
||||
|
||||
handleFocus () {
|
||||
this.props.onShow();
|
||||
},
|
||||
|
@ -60,9 +56,9 @@ const Search = React.createClass({
|
|||
onFocus={this.handleFocus}
|
||||
/>
|
||||
|
||||
<div role='button' tabIndex='0' className='search__icon' onClick={hasValue ? this.handleClear : this.noop}>
|
||||
<div className='search__icon'>
|
||||
<i className={`fa fa-search ${hasValue ? '' : 'active'}`} />
|
||||
<i aria-label="Clear search" className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} />
|
||||
<i className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} onClick={this.handleClear} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -50,7 +50,7 @@ const Followers = React.createClass({
|
|||
|
||||
handleLoadMore (e) {
|
||||
e.preventDefault();
|
||||
this.props.dispatch(expandFollowers(Number(this.props.params.accountId)));
|
||||
this.props.dispatch(expandFollowing(Number(this.props.params.accountId)));
|
||||
},
|
||||
|
||||
render () {
|
||||
|
|
|
@ -14,7 +14,6 @@ const messages = defineMessages({
|
|||
sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
||||
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }
|
||||
});
|
||||
|
||||
|
@ -38,7 +37,6 @@ const GettingStarted = ({ intl, me }) => {
|
|||
<ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />
|
||||
{followRequests}
|
||||
<ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
|
||||
<ColumnLink icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />
|
||||
<ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
|
||||
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
|
||||
</div>
|
||||
|
|
|
@ -13,7 +13,6 @@ import createStream from '../../stream';
|
|||
|
||||
const mapStateToProps = state => ({
|
||||
hasUnread: state.getIn(['timelines', 'tag', 'unread']) > 0,
|
||||
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
||||
accessToken: state.getIn(['meta', 'access_token'])
|
||||
});
|
||||
|
||||
|
@ -22,7 +21,6 @@ const HashtagTimeline = React.createClass({
|
|||
propTypes: {
|
||||
params: React.PropTypes.object.isRequired,
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
streamingAPIBaseURL: React.PropTypes.string.isRequired,
|
||||
accessToken: React.PropTypes.string.isRequired,
|
||||
hasUnread: React.PropTypes.bool
|
||||
},
|
||||
|
@ -30,9 +28,9 @@ const HashtagTimeline = React.createClass({
|
|||
mixins: [PureRenderMixin],
|
||||
|
||||
_subscribe (dispatch, id) {
|
||||
const { streamingAPIBaseURL, accessToken } = this.props;
|
||||
const { accessToken } = this.props;
|
||||
|
||||
this.subscription = createStream(streamingAPIBaseURL, accessToken, `hashtag&tag=${id}`, {
|
||||
this.subscription = createStream(accessToken, `hashtag&tag=${id}`, {
|
||||
|
||||
received (data) {
|
||||
switch(data.event) {
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import { ScrollContainer } from 'react-router-scroll';
|
||||
import Column from '../ui/components/column';
|
||||
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
||||
import AccountContainer from '../../containers/account_container';
|
||||
import { fetchMutes, expandMutes } from '../../actions/mutes';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.mutes', defaultMessage: 'Muted users' }
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'mutes', 'items'])
|
||||
});
|
||||
|
||||
const Mutes = React.createClass({
|
||||
propTypes: {
|
||||
params: React.PropTypes.object.isRequired,
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchMutes());
|
||||
},
|
||||
|
||||
handleScroll (e) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
|
||||
if (scrollTop === scrollHeight - clientHeight) {
|
||||
this.props.dispatch(expandMutes());
|
||||
}
|
||||
},
|
||||
|
||||
render () {
|
||||
const { intl, accountIds } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column icon='users' heading={intl.formatMessage(messages.heading)}>
|
||||
<ColumnBackButtonSlim />
|
||||
<ScrollContainer scrollKey='mutes'>
|
||||
<div className='scrollable' onScroll={this.handleScroll}>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} />
|
||||
)}
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(Mutes));
|
|
@ -15,7 +15,7 @@ const ClearColumnButton = React.createClass({
|
|||
const { intl } = this.props;
|
||||
|
||||
return (
|
||||
<div role='button' title={intl.formatMessage(messages.clear)} className='column-icon column-icon-clear' tabIndex='0' onClick={this.props.onClick}>
|
||||
<div title={intl.formatMessage(messages.clear)} className='column-icon column-icon-clear' tabIndex='0' onClick={this.props.onClick}>
|
||||
<i className='fa fa-eraser' />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -27,11 +27,9 @@ const ColumnSettings = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
onSave: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.shape({
|
||||
formatMessage: React.PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
|
|
|
@ -71,7 +71,7 @@ const Notification = React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
render () { // eslint-disable-line consistent-return
|
||||
render () {
|
||||
const { notification } = this.props;
|
||||
const account = notification.get('account');
|
||||
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
|
||||
|
|
|
@ -14,8 +14,8 @@ const labelSpanStyle = {
|
|||
marginLeft: '8px'
|
||||
};
|
||||
|
||||
const SettingToggle = ({ settings, settingKey, label, onChange, htmlFor = '' }) => (
|
||||
<label htmlFor={htmlFor} style={labelStyle}>
|
||||
const SettingToggle = ({ settings, settingKey, label, onChange }) => (
|
||||
<label style={labelStyle}>
|
||||
<Toggle checked={settings.getIn(settingKey)} onChange={(e) => onChange(settingKey, e.target.checked)} />
|
||||
<span className='setting-toggle' style={labelSpanStyle}>{label}</span>
|
||||
</label>
|
||||
|
@ -25,8 +25,7 @@ SettingToggle.propTypes = {
|
|||
settings: ImmutablePropTypes.map.isRequired,
|
||||
settingKey: React.PropTypes.array.isRequired,
|
||||
label: React.PropTypes.node.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
htmlFor: React.PropTypes.string
|
||||
onChange: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SettingToggle;
|
||||
|
|
|
@ -19,7 +19,6 @@ const messages = defineMessages({
|
|||
|
||||
const mapStateToProps = state => ({
|
||||
hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0,
|
||||
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
||||
accessToken: state.getIn(['meta', 'access_token'])
|
||||
});
|
||||
|
||||
|
@ -30,7 +29,6 @@ const PublicTimeline = React.createClass({
|
|||
propTypes: {
|
||||
dispatch: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
streamingAPIBaseURL: React.PropTypes.string.isRequired,
|
||||
accessToken: React.PropTypes.string.isRequired,
|
||||
hasUnread: React.PropTypes.bool
|
||||
},
|
||||
|
@ -38,7 +36,7 @@ const PublicTimeline = React.createClass({
|
|||
mixins: [PureRenderMixin],
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
|
||||
const { dispatch, accessToken } = this.props;
|
||||
|
||||
dispatch(refreshTimeline('public'));
|
||||
|
||||
|
@ -46,7 +44,7 @@ const PublicTimeline = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
subscription = createStream(streamingAPIBaseURL, accessToken, 'public', {
|
||||
subscription = createStream(accessToken, 'public', {
|
||||
|
||||
connected () {
|
||||
dispatch(connectTimeline('public'));
|
||||
|
|
|
@ -71,14 +71,10 @@ const ActionBar = React.createClass({
|
|||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
||||
}
|
||||
|
||||
let reblogIcon = 'retweet';
|
||||
if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
|
||||
else if (status.get('visibility') === 'private') reblogIcon = 'lock';
|
||||
|
||||
return (
|
||||
<div className='detailed-status__action-bar'>
|
||||
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
|
||||
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton disabled={status.get('visibility') === 'direct' || status.get('visibility') === 'private'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
|
||||
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div>
|
||||
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton disabled={status.get('visibility') === 'direct' || status.get('visibility') === 'private'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={status.get('visibility') === 'direct' ? 'envelope' : (status.get('visibility') === 'private' ? 'lock' : 'retweet')} onClick={this.handleReblogClick} /></div>
|
||||
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>
|
||||
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><DropdownMenu size={18} icon='ellipsis-h' items={menu} direction="left" /></div>
|
||||
</div>
|
||||
|
|
|
@ -64,7 +64,7 @@ const Column = React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div role='section' className='column' onWheel={this.handleWheel}>
|
||||
<div className='column' onWheel={this.handleWheel}>
|
||||
{header}
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
@ -25,7 +25,7 @@ const ColumnHeader = React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div role='button' tabIndex='0' aria-label={type} className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick}>
|
||||
<div className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick}>
|
||||
{icon}
|
||||
{type}
|
||||
</div>
|
||||
|
|
|
@ -34,8 +34,7 @@ ColumnLink.propTypes = {
|
|||
icon: React.PropTypes.string.isRequired,
|
||||
text: React.PropTypes.string.isRequired,
|
||||
to: React.PropTypes.string,
|
||||
href: React.PropTypes.string,
|
||||
method: React.PropTypes.string
|
||||
href: React.PropTypes.string
|
||||
};
|
||||
|
||||
export default ColumnLink;
|
||||
|
|
|
@ -104,8 +104,8 @@ const MediaModal = React.createClass({
|
|||
leftNav = rightNav = content = '';
|
||||
|
||||
if (media.size > 1) {
|
||||
leftNav = <div role='button' tabIndex='0' style={leftNavStyle} className='modal-container__nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
|
||||
rightNav = <div role='button' tabIndex='0' style={rightNavStyle} className='modal-container__nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
|
||||
leftNav = <div style={leftNavStyle} className='modal-container__nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
|
||||
rightNav = <div style={rightNavStyle} className='modal-container__nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
|
||||
}
|
||||
|
||||
if (attachment.get('type') === 'image') {
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import MediaModal from './media_modal';
|
||||
import OnboardingModal from './onboarding_modal';
|
||||
import VideoModal from './video_modal';
|
||||
import BoostModal from './boost_modal';
|
||||
import { TransitionMotion, spring } from 'react-motion';
|
||||
|
||||
const MODAL_COMPONENTS = {
|
||||
'MEDIA': MediaModal,
|
||||
'ONBOARDING': OnboardingModal,
|
||||
'VIDEO': VideoModal,
|
||||
'BOOST': BoostModal
|
||||
};
|
||||
|
@ -68,7 +66,7 @@ const ModalRoot = React.createClass({
|
|||
|
||||
return (
|
||||
<div key={key}>
|
||||
<div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity, transform: `translateZ(0px)` }} onClick={onClose} />
|
||||
<div className='modal-root__overlay' style={{ opacity: style.opacity, transform: `translateZ(0px)` }} onClick={onClose} />
|
||||
<div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
|
||||
<SpecificComponent {...props} onClose={onClose} />
|
||||
</div>
|
||||
|
|
|
@ -1,251 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import Permalink from '../../../components/permalink';
|
||||
import { TransitionMotion, spring } from 'react-motion';
|
||||
import ComposeForm from '../../compose/components/compose_form';
|
||||
import Search from '../../compose/components/search';
|
||||
import NavigationBar from '../../compose/components/navigation_bar';
|
||||
import ColumnHeader from './column_header';
|
||||
import Immutable from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
home_title: { id: 'column.home', defaultMessage: 'Home' },
|
||||
notifications_title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
||||
local_title: { id: 'column.community', defaultMessage: 'Local timeline' },
|
||||
federated_title: { id: 'column.public', defaultMessage: 'Federated timeline' }
|
||||
});
|
||||
|
||||
const PageOne = ({ acct, domain }) => (
|
||||
<div className='onboarding-modal__page onboarding-modal__page-one'>
|
||||
<div style={{ flex: '0 0 auto' }}>
|
||||
<div className='onboarding-modal__page-one__elephant-friend' />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1><FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to Mastodon!' /></h1>
|
||||
<p><FormattedMessage id='onboarding.page_one.federation' defaultMessage='Mastodon is a social network that belongs to everyone.' /></p>
|
||||
<p><FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, one of many independent Mastodon instances. Your full handle is {handle}' values={{ domain, handle: <strong>{acct}@{domain}</strong> }}/></p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
PageOne.propTypes = {
|
||||
acct: React.PropTypes.string.isRequired,
|
||||
domain: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
const PageTwo = () => (
|
||||
<div className='onboarding-modal__page onboarding-modal__page-two'>
|
||||
<div className='figure non-interactive'>
|
||||
<ComposeForm
|
||||
text='Awoo! #introductions'
|
||||
suggestions={Immutable.List()}
|
||||
mentionedDomains={[]}
|
||||
onChange={() => {}}
|
||||
onSubmit={() => {}}
|
||||
onPaste={() => {}}
|
||||
onPickEmoji={() => {}}
|
||||
onChangeSpoilerText={() => {}}
|
||||
onClearSuggestions={() => {}}
|
||||
onFetchSuggestions={() => {}}
|
||||
onSuggestionSelected={() => {}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p><FormattedMessage id='onboarding.page_two.compose' defaultMessage='Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.' /></p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const PageThree = ({ me, domain }) => (
|
||||
<div className='onboarding-modal__page onboarding-modal__page-three'>
|
||||
<div className='figure non-interactive'>
|
||||
<Search
|
||||
value=''
|
||||
onChange={() => {}}
|
||||
onSubmit={() => {}}
|
||||
onClear={() => {}}
|
||||
onShow={() => {}}
|
||||
/>
|
||||
|
||||
<div className='pseudo-drawer'>
|
||||
<NavigationBar account={me} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p><FormattedMessage id='onboarding.page_three.search' defaultMessage='Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.' values={{ illustration: <Permalink to='/timelines/tag/illustration' href='/tags/illustration'>#illustration</Permalink>, introductions: <Permalink to='/timelines/tag/introductions' href='/tags/introductions'>#introductions</Permalink> }}/></p>
|
||||
<p><FormattedMessage id='onboarding.page_three.profile' defaultMessage='Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.' /></p>
|
||||
</div>
|
||||
);
|
||||
|
||||
PageThree.propTypes = {
|
||||
me: ImmutablePropTypes.map.isRequired,
|
||||
domain: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
const PageFour = ({ domain, intl }) => (
|
||||
<div className='onboarding-modal__page onboarding-modal__page-four'>
|
||||
<div className='onboarding-modal__page-four__columns'>
|
||||
<div className='row'>
|
||||
<div>
|
||||
<div className='figure non-interactive'><ColumnHeader icon='home' type={intl.formatMessage(messages.home_title)} /></div>
|
||||
<p><FormattedMessage id='onboarding.page_four.home' defaultMessage='Home timeline shows posts from people you follow'/></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className='figure non-interactive'><ColumnHeader icon='bell' type={intl.formatMessage(messages.notifications_title)} /></div>
|
||||
<p><FormattedMessage id='onboarding.page_four.notifications' defaultMessage='Notifications show when someone interacts with you' /></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='row'>
|
||||
<div>
|
||||
<div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='globe' type={intl.formatMessage(messages.federated_title)} /></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='users' type={intl.formatMessage(messages.local_title)} /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p><FormattedMessage id='onboarding.page_five.public_timelines' defaultMessage='Federated timeline lists public posts from everyone who people on {domain} follow. Local timeline is the same, but limited to people on {domain}.' values={{ domain }} /></p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
PageFour.propTypes = {
|
||||
domain: React.PropTypes.string.isRequired,
|
||||
intl: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const PageSix = ({ admin }) => {
|
||||
let adminSection = '';
|
||||
|
||||
if (admin) {
|
||||
adminSection = (
|
||||
<p>
|
||||
<FormattedMessage id='onboarding.page_six.admin' defaultMessage="Your instance's admin is {admin}." values={{ admin: <Permalink href={admin.get('url')} to={`/accounts/${admin.get('id')}`}>@{admin.get('acct')}</Permalink> }} />
|
||||
<br />
|
||||
<FormattedMessage id='onboarding.page_six.read_guidelines' defaultMessage='Please, do not forget to read the {guidelines}!' values={{ guidelines: <a href='/about/more' target='_blank'><FormattedMessage id='onboarding.page_six.guidelines' defaultMessage='community guidelines' /></a> }}/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='onboarding-modal__page onboarding-modal__page-six'>
|
||||
<h1><FormattedMessage id='onboarding.page_six.almost_done' defaultMessage='Almost done...' /></h1>
|
||||
{adminSection}
|
||||
<p><FormattedMessage id='onboarding.page_six.github' defaultMessage='Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.' values={{ github: <a href='https://github.com/tootsuite/mastodon' target='_blank' rel='noopener'>GitHub</a> }} /></p>
|
||||
<p><FormattedMessage id='onboarding.page_six.apps_available' defaultMessage='There are {apps} available for iOS, Android and other platforms. And now... Bon Appetoot!' values={{ apps: <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' target='_blank' rel='noopener'><FormattedMessage id='onboarding.page_six.various_app' defaultMessage='various mobile apps' /></a> }} /></p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PageSix.propTypes = {
|
||||
admin: ImmutablePropTypes.map
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
me: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
|
||||
admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]),
|
||||
domain: state.getIn(['meta', 'domain'])
|
||||
});
|
||||
|
||||
const OnboardingModal = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
onClose: React.PropTypes.func.isRequired,
|
||||
intl: React.PropTypes.object.isRequired,
|
||||
me: ImmutablePropTypes.map.isRequired,
|
||||
domain: React.PropTypes.string.isRequired,
|
||||
admin: ImmutablePropTypes.map
|
||||
},
|
||||
|
||||
getInitialState () {
|
||||
return {
|
||||
currentIndex: 0
|
||||
};
|
||||
},
|
||||
|
||||
mixins: [PureRenderMixin],
|
||||
|
||||
handleSkip (e) {
|
||||
e.preventDefault();
|
||||
this.props.onClose();
|
||||
},
|
||||
|
||||
handleDot (i, e) {
|
||||
e.preventDefault();
|
||||
this.setState({ currentIndex: i });
|
||||
},
|
||||
|
||||
handleNext (maxNum, e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.currentIndex < maxNum - 1) {
|
||||
this.setState({ currentIndex: this.state.currentIndex + 1 });
|
||||
} else {
|
||||
this.props.onClose();
|
||||
}
|
||||
},
|
||||
|
||||
render () {
|
||||
const { me, admin, domain, intl } = this.props;
|
||||
|
||||
const pages = [
|
||||
<PageOne acct={me.get('acct')} domain={domain} />,
|
||||
<PageTwo />,
|
||||
<PageThree me={me} domain={domain} />,
|
||||
<PageFour domain={domain} intl={intl} />,
|
||||
<PageSix admin={admin} />
|
||||
];
|
||||
|
||||
const { currentIndex } = this.state;
|
||||
const hasMore = currentIndex < pages.length - 1;
|
||||
|
||||
let nextOrDoneBtn;
|
||||
|
||||
if(hasMore) {
|
||||
nextOrDoneBtn = <a href='#' onClick={this.handleNext.bind(null, pages.length)} className='onboarding-modal__nav onboarding-modal__next'><FormattedMessage id='onboarding.next' defaultMessage='Next' /></a>;
|
||||
} else {
|
||||
nextOrDoneBtn = <a href='#' onClick={this.handleNext.bind(null, pages.length)} className='onboarding-modal__nav onboarding-modal__done'><FormattedMessage id='onboarding.next' defaultMessage='Done' /></a>;
|
||||
}
|
||||
|
||||
const styles = pages.map((page, i) => ({
|
||||
key: i,
|
||||
style: { opacity: spring(i === currentIndex ? 1 : 0) }
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal onboarding-modal'>
|
||||
<TransitionMotion styles={styles}>
|
||||
{interpolatedStyles =>
|
||||
<div className='onboarding-modal__pager'>
|
||||
{pages.map((page, i) =>
|
||||
<div key={i} style={{ opacity: interpolatedStyles[i].style.opacity, pointerEvents: i === currentIndex ? 'auto' : 'none' }}>{page}</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
</TransitionMotion>
|
||||
|
||||
<div className='onboarding-modal__paginator'>
|
||||
<div>
|
||||
<a href='#' className='onboarding-modal__skip' onClick={this.handleSkip}><FormattedMessage id='onboarding.skip' defaultMessage='Skip' /></a>
|
||||
</div>
|
||||
|
||||
<div className='onboarding-modal__dots'>
|
||||
{pages.map((_, i) => <div key={i} onClick={this.handleDot.bind(null, i)} className={`onboarding-modal__dot ${i === currentIndex ? 'active' : ''}`} />)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{nextOrDoneBtn}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(OnboardingModal));
|
|
@ -24,10 +24,8 @@ const makeGetStatusIds = () => createSelector([
|
|||
|
||||
if (columnSettings.getIn(['regex', 'body'], '').trim().length > 0) {
|
||||
try {
|
||||
if (showStatus) {
|
||||
const regex = new RegExp(columnSettings.getIn(['regex', 'body']).trim(), 'i');
|
||||
showStatus = !regex.test(statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'unescaped_content']) : statusForId.get('unescaped_content'));
|
||||
}
|
||||
const regex = new RegExp(columnSettings.getIn(['regex', 'body']).trim(), 'i');
|
||||
showStatus = showStatus && !regex.test(statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'content']) : statusForId.get('content'));
|
||||
} catch(e) {
|
||||
// Bad regex, don't affect filters
|
||||
}
|
||||
|
|
|
@ -141,7 +141,7 @@ const UI = React.createClass({
|
|||
{mountedColumns}
|
||||
|
||||
<NotificationsContainer />
|
||||
<LoadingBarContainer className="loading-bar" />
|
||||
<LoadingBarContainer style={{ backgroundColor: '#2b90d9', left: '0', top: '0' }} />
|
||||
<ModalContainer />
|
||||
<UploadArea active={draggingOver} />
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@ Link.parseAttrs = (link, parts) => {
|
|||
link = Link.parseParams(link, uriAttrs[1])
|
||||
}
|
||||
|
||||
while(match = Link.attrPattern.exec(attrs)) { // eslint-disable-line no-cond-assign
|
||||
while(match = Link.attrPattern.exec(attrs)) {
|
||||
attr = match[1].toLowerCase()
|
||||
value = match[4] || match[3] || match[2]
|
||||
|
||||
|
|
|
@ -31,7 +31,6 @@ const en = {
|
|||
"column.favourites": "Favourites",
|
||||
"column.follow_requests": "Follow requests",
|
||||
"column.home": "Home",
|
||||
"column.mutes": "Muted users",
|
||||
"column.notifications": "Notifications",
|
||||
"column.public": "Federated timeline",
|
||||
"compose_form.placeholder": "What is on your mind?",
|
||||
|
@ -69,12 +68,10 @@ const en = {
|
|||
"navigation_bar.follow_requests": "Follow requests",
|
||||
"navigation_bar.info": "Extended information",
|
||||
"navigation_bar.logout": "Logout",
|
||||
"navigation_bar.mutes": "Muted users",
|
||||
"navigation_bar.preferences": "Preferences",
|
||||
"navigation_bar.public_timeline": "Federated timeline",
|
||||
"notification.favourite": "{name} favourited your status",
|
||||
"notification.follow": "{name} followed you",
|
||||
"notification.mention": "{name} mentioned you",
|
||||
"notification.reblog": "{name} boosted your status",
|
||||
"notifications.clear_confirmation": "Are you sure you want to clear all your notifications?",
|
||||
"notifications.clear": "Clear notifications",
|
||||
|
@ -129,7 +126,6 @@ const en = {
|
|||
"video_player.toggle_sound": "Toggle sound",
|
||||
"video_player.toggle_visible": "Toggle visibility",
|
||||
"video_player.expand": "Expand video",
|
||||
"video_player.video_error": "Video could not be played",
|
||||
};
|
||||
|
||||
export default en;
|
||||
|
|
|
@ -73,7 +73,7 @@ const es = {
|
|||
"notifications.column_settings.mention": "Menciones:",
|
||||
"notifications.column_settings.reblog": "Retoots:",
|
||||
"emoji_button.label": "Insertar emoji",
|
||||
"privacy.public.short": "Público",
|
||||
"privacy.public.short": "Público",
|
||||
"privacy.public.long": "Mostrar en la historia federada",
|
||||
"privacy.unlisted.short": "Sin federar",
|
||||
"privacy.unlisted.long": "No mostrar en la historia federada",
|
||||
|
|
|
@ -75,7 +75,6 @@ const fr = {
|
|||
"navigation_bar.favourites": "Favoris",
|
||||
"navigation_bar.info": "Plus d'informations",
|
||||
"navigation_bar.logout": "Déconnexion",
|
||||
"navigation_bar.mutes": "Utilisateurs muets",
|
||||
"navigation_bar.follow_requests": "Demandes de suivi",
|
||||
"reply_indicator.cancel": "Annuler",
|
||||
"search.placeholder": "Rechercher",
|
||||
|
|
|
@ -1,125 +1,119 @@
|
|||
const ja = {
|
||||
"account.block": "@{name} さんをブロック",
|
||||
"account.disclaimer": "このユーザーは他のインスタンスに所属しているため、数字が正確で無い場合があります。",
|
||||
"account.edit_profile": "プロフィールを編集",
|
||||
"account.follow": "フォロー",
|
||||
"account.followers": "フォロワー",
|
||||
"account.follows": "フォロー",
|
||||
"account.follows_you": "フォローされています",
|
||||
"column_back_button.label": "戻る",
|
||||
"lightbox.close": "閉じる",
|
||||
"loading_indicator.label": "読み込み中...",
|
||||
"status.mention": "@{name} さんへの返信",
|
||||
"status.delete": "削除",
|
||||
"status.reply": "返信",
|
||||
"status.reblog": "ブースト",
|
||||
"status.favourite": "お気に入り",
|
||||
"status.reblogged_by": "{name} さんにブーストされました",
|
||||
"status.sensitive_warning": "不適切なコンテンツ",
|
||||
"status.sensitive_toggle": "クリックして表示",
|
||||
"status.show_more": "もっと見る",
|
||||
"status.load_more": "もっと見る",
|
||||
"status.show_less": "隠す",
|
||||
"status.open": "Expand this status",
|
||||
"status.report": "@{name} さんを通報",
|
||||
"status.media_hidden": "非表示のメデイア",
|
||||
"video_player.toggle_sound": "音の切り替え",
|
||||
"account.mention": "@{name} さんに返信",
|
||||
"account.mute": "ミュート",
|
||||
"account.posts": "投稿",
|
||||
"account.report": "@{name}を通報する",
|
||||
"account.requested": "承認待ち",
|
||||
"account.edit_profile": "プロフィールを編集",
|
||||
"account.unblock": "@{name} さんのブロックを解除",
|
||||
"account.unfollow": "フォロー解除",
|
||||
"account.block": "@{name} さんをブロック",
|
||||
"account.mute": "ミュート",
|
||||
"account.unmute": "ミュート解除",
|
||||
"boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。",
|
||||
"column.blocks": "ブロックしたユーザー",
|
||||
"column.community": "ローカルタイムライン",
|
||||
"column.favourites": "お気に入り",
|
||||
"column.follow_requests": "フォローリクエスト",
|
||||
"account.follow": "フォロー",
|
||||
"account.report": "@{name}を通報する",
|
||||
"account.posts": "投稿",
|
||||
"account.follows": "フォロー",
|
||||
"account.followers": "フォロワー",
|
||||
"account.follows_you": "フォローされています",
|
||||
"account.requested": "承認待ち",
|
||||
"follow_request.authorize": "許可",
|
||||
"follow_request.reject": "拒否",
|
||||
"getting_started.heading": "スタート",
|
||||
"getting_started.about_addressing": "ドメインとユーザー名を知っているなら検索フォームに入力すればフォローできます。",
|
||||
"getting_started.about_shortcuts": "対象のアカウントがあなたと同じドメインのユーザーならばユーザー名のみで検索できます。これは返信のときも一緒です。",
|
||||
"getting_started.open_source_notice": "Mastodon はオープンソースソフトウェアです。誰でも GitHub({github})から開発に参加したり、問題を報告したりできます。 {apps}",
|
||||
"column.home": "ホーム",
|
||||
"column.mutes": "ミュートしたユーザー",
|
||||
"column.notifications": "通知",
|
||||
"column.community": "ローカルタイムライン",
|
||||
"column.public": "連合タイムライン",
|
||||
"column_back_button.label": "戻る",
|
||||
"column.notifications": "通知",
|
||||
"column.favourites": "お気に入り",
|
||||
"tabs_bar.compose": "投稿",
|
||||
"tabs_bar.home": "ホーム",
|
||||
"tabs_bar.mentions": "返信",
|
||||
"tabs_bar.local_timeline": "ローカル",
|
||||
"tabs_bar.federated_timeline": "連合",
|
||||
"tabs_bar.notifications": "通知",
|
||||
"compose_form.placeholder": "今なにしてる?",
|
||||
"compose_form.privacy_disclaimer": "あなたの非公開トゥートは返信先のユーザー(at {domains})に公開されます。{domainsCount, plural, one {that server} other {those servers}}を信頼しますか?投稿のプライバシー保護はMastodonサーバー内でのみ有効です。 もし{domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}ならばあなたの投稿のプライバシーは保護されず、ブーストされたり予期しないユーザーに見られる可能性があります。",
|
||||
"compose_form.publish": "トゥート",
|
||||
"compose_form.sensitive": "メディアを不適切なコンテンツとしてマークする",
|
||||
"compose_form.spoiler": "テキストを隠す",
|
||||
"compose_form.spoiler_placeholder": "閲覧注意",
|
||||
"emoji_button.label": "絵文字を追加",
|
||||
"empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",
|
||||
"empty_column.hashtag": "このハッシュタグはまだ使われていません。",
|
||||
"compose_form.spoiler_placeholder": "内容注意メッセージ",
|
||||
"compose_form.private": "非公開にする",
|
||||
"compose_form.privacy_disclaimer": "あなたの非公開トゥートは返信先のユーザー(at {domains})に公開されます。{domainsCount, plural, one {that server} other {those servers}}を信頼しますか?投稿のプライバシー保護はMastodonサーバー内でのみ有効です。 もし{domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}ならばあなたの投稿のプライバシーは保護されず、ブーストされたり予期しないユーザーに見られる可能性があります。",
|
||||
"compose_form.unlisted": "公開タイムラインに表示しない",
|
||||
"privacy.public.short": "公開",
|
||||
"privacy.public.long": "公開TLに投稿する",
|
||||
"privacy.unlisted.short": "未収載",
|
||||
"privacy.unlisted.long": "公開TLで表示しない",
|
||||
"privacy.private.short": "非公開",
|
||||
"privacy.private.long": "フォロワーだけに公開",
|
||||
"privacy.direct.short": "ダイレクト",
|
||||
"privacy.direct.long": "含んだユーザーだけに公開",
|
||||
"privacy.change": "投稿のプライバシーを変更2",
|
||||
"report.heading": "新規通報",
|
||||
"report.placeholder": "コメント",
|
||||
"report.target": "問題のユーザー",
|
||||
"report.submit": "通報する",
|
||||
"navigation_bar.edit_profile": "プロフィールを編集",
|
||||
"navigation_bar.preferences": "ユーザー設定",
|
||||
"navigation_bar.community_timeline": "ローカルタイムライン",
|
||||
"navigation_bar.public_timeline": "連合タイムライン",
|
||||
"navigation_bar.logout": "ログアウト",
|
||||
"navigation_bar.favourites": "お気に入り",
|
||||
"navigation_bar.blocks": "ブロックしたユーザー",
|
||||
"navigation_bar.info": "サーバー情報",
|
||||
"reply_indicator.cancel": "キャンセル",
|
||||
"search.placeholder": "検索",
|
||||
"search.account": "アカウント",
|
||||
"search.hashtag": "ハッシュタグ",
|
||||
"search.status_by": "{uuuname}からの投稿",
|
||||
"upload_area.title": "ファイルをこちらにドラッグしてください",
|
||||
"upload_button.label": "メディアを追加",
|
||||
"upload_form.undo": "やり直す",
|
||||
"notification.follow": "{name} さんにフォローされました",
|
||||
"notification.favourite": "{name} さんがあなたのトゥートをお気に入りに登録しました",
|
||||
"notification.reblog": "{name} さんがあなたのトゥートをブーストしました",
|
||||
"notification.mention": "{name} さんがあなたに返信しました",
|
||||
"notifications.clear": "通知を片付ける",
|
||||
"notifications.clear_confirmation": "通知を全部片付けます。大丈夫ですか?",
|
||||
"notifications.column_settings.alert": "デスクトップ通知",
|
||||
"notifications.column_settings.show": "カラムに表示",
|
||||
"notifications.column_settings.follow": "新しいフォロワー",
|
||||
"notifications.column_settings.favourite": "お気に入り",
|
||||
"notifications.column_settings.mention": "返信",
|
||||
"notifications.column_settings.reblog": "ブースト",
|
||||
"notifications.column_settings.sound": "通知音を再生",
|
||||
"empty_column.home": "まだ誰もフォローしていません。{public}を見に行くか、検索を使って他のユーザーを見つけましょう。",
|
||||
"empty_column.home.public_timeline": "連合タイムライン",
|
||||
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
|
||||
"empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!",
|
||||
"follow_request.authorize": "許可",
|
||||
"follow_request.reject": "拒否",
|
||||
"getting_started.apps": "さまざまなアプリで利用できます。",
|
||||
"getting_started.heading": "スタート",
|
||||
"getting_started.open_source_notice": "Mastodon はオープンソースソフトウェアです。誰でも GitHub({github})から開発に参加したり、問題を報告したりできます。 {apps}",
|
||||
"home.column_settings.advanced": "上級者向け",
|
||||
"empty_column.hashtag": "このハッシュタグはまだ使っていません。",
|
||||
"upload_progress.label": "アップロード中…",
|
||||
"emoji_button.label": "絵文字を追加",
|
||||
"home.column_settings.basic": "シンプル",
|
||||
"home.column_settings.filter_regex": "正規表現でフィルター",
|
||||
"home.column_settings.advanced": "エキスパート",
|
||||
"home.column_settings.show_reblogs": "ブースト表示",
|
||||
"home.column_settings.show_replies": "返信表示",
|
||||
"home.column_settings.filter_regex": "正規表現でフィルター",
|
||||
"home.settings": "カラム設定",
|
||||
"lightbox.close": "閉じる",
|
||||
"loading_indicator.label": "読み込み中...",
|
||||
"media_gallery.toggle_visible": "表示切り替え",
|
||||
"notification.settings": "カラム設定",
|
||||
"missing_indicator.label": "見つかりません",
|
||||
"navigation_bar.blocks": "ブロックしたユーザー",
|
||||
"navigation_bar.community_timeline": "ローカルタイムライン",
|
||||
"navigation_bar.edit_profile": "プロフィールを編集",
|
||||
"navigation_bar.favourites": "お気に入り",
|
||||
"navigation_bar.follow_requests": "フォローリクエスト",
|
||||
"navigation_bar.info": "サーバー情報",
|
||||
"navigation_bar.logout": "ログアウト",
|
||||
"navigation_bar.mutes": "ミュートしたユーザー",
|
||||
"navigation_bar.preferences": "ユーザー設定",
|
||||
"navigation_bar.public_timeline": "連合タイムライン",
|
||||
"notification.favourite": "{name} さんがあなたのトゥートをお気に入りに登録しました",
|
||||
"notification.follow": "{name} さんにフォローされました",
|
||||
"notification.mention": "{name} さんがあなたに返信しました",
|
||||
"notification.reblog": "{name} さんがあなたのトゥートをブーストしました",
|
||||
"notifications.clear": "通知を消去",
|
||||
"notifications.clear_confirmation": "本当に通知を消去しますか?",
|
||||
"notifications.column_settings.alert": "デスクトップ通知",
|
||||
"notifications.column_settings.favourite": "お気に入り",
|
||||
"notifications.column_settings.follow": "新しいフォロワー",
|
||||
"notifications.column_settings.mention": "返信",
|
||||
"notifications.column_settings.reblog": "ブースト",
|
||||
"notifications.column_settings.show": "カラムに表示",
|
||||
"notifications.column_settings.sound": "通知音を再生",
|
||||
"notifications.settings": "カラム設定",
|
||||
"privacy.change": "投稿のプライバシーを変更",
|
||||
"privacy.direct.long": "メンションしたユーザーだけに公開",
|
||||
"privacy.direct.short": "ダイレクト",
|
||||
"privacy.private.long": "フォロワーだけに公開",
|
||||
"privacy.private.short": "非公開",
|
||||
"privacy.public.long": "公開TLに投稿する",
|
||||
"privacy.public.short": "公開",
|
||||
"privacy.unlisted.long": "公開TLで表示しない",
|
||||
"privacy.unlisted.short": "未収載",
|
||||
"reply_indicator.cancel": "キャンセル",
|
||||
"report.heading": "新規通報",
|
||||
"report.placeholder": "コメント",
|
||||
"report.submit": "通報する",
|
||||
"report.target": "問題のユーザー",
|
||||
"search.placeholder": "検索",
|
||||
"search.status_by": "{name}からの投稿",
|
||||
"search_results.total": "{count} {count, plural, one {result} other {results}} 件",
|
||||
"status.delete": "削除",
|
||||
"status.favourite": "お気に入り",
|
||||
"status.load_more": "もっと見る",
|
||||
"status.media_hidden": "非表示のメデイア",
|
||||
"status.mention": "@{name} さんへの返信",
|
||||
"status.open": "詳細を表示",
|
||||
"status.reblog": "ブースト",
|
||||
"status.reblogged_by": "{name} さんにブーストされました",
|
||||
"status.reply": "返信",
|
||||
"status.report": "@{name} さんを通報",
|
||||
"status.sensitive_toggle": "クリックして表示",
|
||||
"status.sensitive_warning": "不適切なコンテンツ",
|
||||
"status.show_less": "隠す",
|
||||
"status.show_more": "もっと見る",
|
||||
"tabs_bar.compose": "投稿",
|
||||
"tabs_bar.federated_timeline": "連合",
|
||||
"tabs_bar.home": "ホーム",
|
||||
"tabs_bar.local_timeline": "ローカル",
|
||||
"tabs_bar.notifications": "通知",
|
||||
"upload_area.title": "ドラッグ&ドロップでアップロード",
|
||||
"upload_button.label": "メディアを追加",
|
||||
"upload_form.undo": "やり直す",
|
||||
"upload_progress.label": "アップロード中…",
|
||||
"video_player.expand": "動画の詳細",
|
||||
"video_player.toggle_sound": "音の切り替え",
|
||||
"video_player.toggle_visible": "表示切り替え",
|
||||
"video_player.video_error": "動画の再生に失敗しました",
|
||||
"boost_modal.combo": "次は{combo}を押せば、これをスキップできます。"
|
||||
};
|
||||
|
||||
export default ja;
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
const nl = {
|
||||
"column_back_button.label": "terug",
|
||||
"lightbox.close": "Sluiten",
|
||||
"loading_indicator.label": "Laden…",
|
||||
"status.mention": "@{name} vermelden",
|
||||
"status.delete": "Verwijderen",
|
||||
"status.reply": "Reageren",
|
||||
"loading_indicator.label": "Laden...",
|
||||
"status.mention": "Vermeld @{name}",
|
||||
"status.delete": "Verwijder",
|
||||
"status.reply": "Reageer",
|
||||
"status.reblog": "Boost",
|
||||
"status.favourite": "Favoriet",
|
||||
"status.reblogged_by": "{name} boostte",
|
||||
"status.sensitive_warning": "Gevoelige inhoud",
|
||||
"status.sensitive_toggle": "Klik om te zien",
|
||||
"video_player.toggle_sound": "Geluid in-/uitschakelen",
|
||||
"account.mention": "@{name} vermelden",
|
||||
"account.edit_profile": "Profiel bewerken",
|
||||
"account.unblock": "@{name} deblokkeren",
|
||||
"account.unfollow": "Ontvolgen",
|
||||
"account.block": "@{name} blokkeren",
|
||||
"account.follow": "Volgen",
|
||||
"video_player.toggle_sound": "Geluid omschakelen",
|
||||
"account.mention": "Vermeld @{name}",
|
||||
"account.edit_profile": "Bewerk profiel",
|
||||
"account.unblock": "Deblokkeer @{name}",
|
||||
"account.unfollow": "Ontvolg",
|
||||
"account.block": "Blokkeer @{name}",
|
||||
"account.follow": "Volg",
|
||||
"account.posts": "Berichten",
|
||||
"account.follows": "Volgt",
|
||||
"account.followers": "Volgers",
|
||||
|
@ -25,7 +25,7 @@ const nl = {
|
|||
"getting_started.heading": "Beginnen",
|
||||
"getting_started.about_addressing": "Je kunt mensen volgen als je hun gebruikersnaam en het domein van hun server kent, door het e-mailachtige adres in het zoekscherm in te voeren.",
|
||||
"getting_started.about_shortcuts": "Als de gezochte gebruiker op hetzelfde domein zit als jijzelf, is invoeren van de gebruikersnaam genoeg. Dat geldt ook als je mensen in de statussen wilt vermelden.",
|
||||
"getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}. {apps}.",
|
||||
"getting_started.open_source_notice": "Mastodon is open source software. Je kunt bijdragen of problemen melden op GitHub via {github}. {apps}.",
|
||||
"column.home": "Thuis",
|
||||
"column.community": "Lokale tijdlijn",
|
||||
"column.public": "Federatietijdlijn",
|
||||
|
@ -37,30 +37,30 @@ const nl = {
|
|||
"tabs_bar.notifications": "Meldingen",
|
||||
"compose_form.placeholder": "Waar ben je mee bezig?",
|
||||
"compose_form.publish": "Toot",
|
||||
"compose_form.sensitive": "Media als gevoelig markeren",
|
||||
"compose_form.spoiler": "Tekst achter waarschuwing verbergen",
|
||||
"compose_form.private": "Als privé markeren",
|
||||
"compose_form.privacy_disclaimer": "Je besloten status wordt afgeleverd aan vermelde gebruikers op {domains}. Vertrouw je {domainsCount, plural, one {that server} andere {those servers}}? Privé plaatsen werkt alleen op Mastodon servers. Als {domains} {domainsCount, plural, een {is not a Mastodon instance} andere {are not Mastodon instances}}, dan wordt er geen indicatie gegeven dat he bericht besloten is, waardoor het kan worden geboost of op andere manier zichtbaar worden voor niet bedoelde lezers.",
|
||||
"compose_form.unlisted": "Niet op openbare tijdlijnen tonen",
|
||||
"navigation_bar.edit_profile": "Profiel bewerken",
|
||||
"compose_form.sensitive": "Markeer media als gevoelig",
|
||||
"compose_form.spoiler": "Verberg tekst achter waarschuwing",
|
||||
"compose_form.private": "Mark als privé",
|
||||
"compose_form.privacy_disclaimer": "Je besloten status wordt afgeleverd aan vermelde gebruikers op {domains}. Vertrouw je {domainsCount, plural, one {that server} andere {those servers}}? Privé plaatsen werkt alleen op Mastodon servers. Als {domains} {domainsCount, plural, een {is not a Mastodon instance} andere {are not Mastodon instances}}, dan wordt er geen indicatie gegeven dat he bericht besloten is, waardoor het kan worden geboost of op andere manier zichtbaar worden voor niet bedoelde lezers.",
|
||||
"compose_form.unlisted": "Niet tonen op openbare tijdlijnen",
|
||||
"navigation_bar.edit_profile": "Bewerk profiel",
|
||||
"navigation_bar.preferences": "Voorkeuren",
|
||||
"navigation_bar.community_timeline": "Lokale tijdlijn",
|
||||
"navigation_bar.public_timeline": "Federatietijdlijn",
|
||||
"navigation_bar.logout": "Afmelden",
|
||||
"navigation_bar.logout": "Uitloggen",
|
||||
"reply_indicator.cancel": "Annuleren",
|
||||
"search.placeholder": "Zoeken",
|
||||
"search.account": "Account",
|
||||
"search.hashtag": "Hashtag",
|
||||
"upload_button.label": "Media toevoegen",
|
||||
"upload_button.label": "Toevoegen media",
|
||||
"upload_form.undo": "Ongedaan maken",
|
||||
"notification.follow": "{name} volgde jou",
|
||||
"notification.favourite": "{name} markeerde je status als favoriet",
|
||||
"notification.reblog": "{name} boostte je status",
|
||||
"notification.mention": "{name} vermeldde jou",
|
||||
"notifications.column_settings.alert": "Desktopmeldingen",
|
||||
"notifications.column_settings.show": "In kolom tonen",
|
||||
"notifications.column_settings.show": "Tonen in kolom",
|
||||
"notifications.column_settings.follow": "Nieuwe volgers:",
|
||||
"notifications.column_settings.favourite": "Favorieten:",
|
||||
"notifications.column_settings.favourite": "Favoriten:",
|
||||
"notifications.column_settings.mention": "Vermeldingen:",
|
||||
"notifications.column_settings.reblog": "Boosts:",
|
||||
};
|
||||
|
|
|
@ -1,130 +1,77 @@
|
|||
const no = {
|
||||
"account.block": "Blokkér @{name}",
|
||||
"account.disclaimer": "Denne brukeren er fra en annen instans. Dette tallet kan være høyere.",
|
||||
"account.edit_profile": "Rediger profil",
|
||||
"account.follow": "Følg",
|
||||
"account.followers": "Følgere",
|
||||
"account.follows_you": "Følger deg",
|
||||
"account.follows": "Følginger",
|
||||
"account.mention": "Nevn @{name}",
|
||||
"account.mute": "Demp @{name}",
|
||||
"account.posts": "Poster",
|
||||
"account.report": "Rapportér @{name}",
|
||||
"account.requested": "Venter på godkjennelse",
|
||||
"account.unblock": "Avblokker @{name}",
|
||||
"account.unfollow": "Avfølg",
|
||||
"account.unmute": "Avdemp @{name}",
|
||||
"boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang",
|
||||
"column_back_button.label": "Tilbake",
|
||||
"column.blocks": "Blokkerte brukere",
|
||||
"column.community": "Lokal tidslinje",
|
||||
"column.favourites": "Likt",
|
||||
"column.follow_requests": "Følgeforespørsler",
|
||||
"column.home": "Hjem",
|
||||
"column.notifications": "Varslinger",
|
||||
"column.public": "Felles tidslinje",
|
||||
"compose_form.placeholder": "Hva har du på hjertet?",
|
||||
"compose_form.privacy_disclaimer": "Din private status vil leveres til nevnte brukere på {domains}. Stoler du på {domainsCount, plural, one {den serveren} other {de serverne}}? Synlighet fungerer kun på Mastodon-instanser. Hvis {domains} {domainsCount, plural, one {ike er en Mastodon-instans} other {ikke er Mastodon-instanser}}, vil det ikke indikeres at posten din er privat, og den kan kanskje bli fremhevd eller på annen måte bli synlig for uventede mottakere.",
|
||||
"compose_form.publish": "Tut",
|
||||
"compose_form.sensitive": "Merk media som følsomt",
|
||||
"compose_form.spoiler_placeholder": "Innholdsadvarsel",
|
||||
"compose_form.spoiler": "Skjul tekst bak advarsel",
|
||||
"emoji_button.label": "Sett inn emoji",
|
||||
"empty_column.community": "Den lokale tidslinjen er tom. Skriv noe offentlig for å få snøballen til å rulle!",
|
||||
"empty_column.hashtag": "Det er ingenting i denne hashtagen ennå.",
|
||||
"empty_column.home.public_timeline": "en offentlig tidslinje",
|
||||
"empty_column.home": "Du har ikke fulgt noen ennå. Besøk {publlic} eller bruk søk for å komme i gang og møte andre brukere.",
|
||||
"empty_column.notifications": "Du har ingen varsler ennå. Kommuniser med andre for å begynne samtalen.",
|
||||
"empty_column.public": "Det er ingenting her! Skriv noe offentlig, eller følg brukere manuelt fra andre instanser for å fylle den opp",
|
||||
"follow_request.authorize": "Autorisér",
|
||||
"follow_request.reject": "Avvis",
|
||||
"getting_started.apps": "Diverse apper er tilgjengelige",
|
||||
"getting_started.heading": "Kom i gang",
|
||||
"getting_started.open_source_notice": "Mastodon er fri programvare. Du kan bidra eller rapportere problemer på GitHub på {github}. {apps}.",
|
||||
"home.column_settings.advanced": "Advansert",
|
||||
"home.column_settings.basic": "Enkel",
|
||||
"home.column_settings.filter_regex": "Filtrér med regulære uttrykk",
|
||||
"home.column_settings.show_reblogs": "Vis fremhevinger",
|
||||
"home.column_settings.show_replies": "Vis svar",
|
||||
"home.settings": "Kolonneinnstillinger",
|
||||
"lightbox.close": "Lukk",
|
||||
"loading_indicator.label": "Laster...",
|
||||
"media_gallery.toggle_visible": "Veksle synlighet",
|
||||
"missing_indicator.label": "Ikke funnet",
|
||||
"navigation_bar.blocks": "Blokkerte brukere",
|
||||
"navigation_bar.community_timeline": "Lokal tidslinje",
|
||||
"navigation_bar.edit_profile": "Rediger profil",
|
||||
"navigation_bar.favourites": "Likt",
|
||||
"navigation_bar.follow_requests": "Følgeforespørsler",
|
||||
"navigation_bar.info": "Utvidet informasjon",
|
||||
"navigation_bar.logout": "Logg ut",
|
||||
"navigation_bar.preferences": "Preferanser",
|
||||
"navigation_bar.public_timeline": "Felles tidslinje",
|
||||
"notification.favourite": "{name} likte din status",
|
||||
"notification.follow": "{name} fulgte deg",
|
||||
"notification.reblog": "{name} fremhevde din status",
|
||||
"notifications.clear_confirmation": "Er du sikker på at du vil fjerne alle dine varsler?",
|
||||
"notifications.clear": "Fjern varsler",
|
||||
"notifications.column_settings.alert": "Skrivebordsvarslinger",
|
||||
"notifications.column_settings.favourite": "Likt:",
|
||||
"notifications.column_settings.follow": "Nye følgere:",
|
||||
"notifications.column_settings.mention": "Nevninger:",
|
||||
"notifications.column_settings.reblog": "Fremhevinger:",
|
||||
"notifications.column_settings.show": "Vis i kolonne",
|
||||
"notifications.column_settings.sound": "Spill lyd",
|
||||
"notifications.settings": "Kolonneinstillinger",
|
||||
"privacy.change": "Justér synlighet",
|
||||
"privacy.direct.long": "Post kun til nevnte brukere",
|
||||
"privacy.direct.short": "Direkte",
|
||||
"privacy.private.long": "Post kun til følgere",
|
||||
"privacy.private.short": "Privat",
|
||||
"privacy.public.long": "Post kun til offentlige tidslinjer",
|
||||
"privacy.public.short": "Offentlig",
|
||||
"privacy.unlisted.long": "Ikke vis i offentlige tidslinjer",
|
||||
"privacy.unlisted.short": "Uoppført",
|
||||
"reply_indicator.cancel": "Avbryt",
|
||||
"report.heading": "Ny rapport",
|
||||
"report.placeholder": "Tilleggskommentarer",
|
||||
"report.submit": "Send inn",
|
||||
"report.target": "Rapporterer",
|
||||
"search_results.total": "{count} {count, plural, one {resultat} other {resultater}}",
|
||||
"search.placeholder": "Søk",
|
||||
"search.status_by": "Status fra {name}",
|
||||
"status.delete": "Slett",
|
||||
"status.favourite": "Lik",
|
||||
"status.load_more": "Last mer",
|
||||
"status.media_hidden": "Media skjult",
|
||||
"status.mention": "Nevn @{name}",
|
||||
"status.open": "Utvid denne statusen",
|
||||
"status.reblog": "Fremhev",
|
||||
"status.reblogged_by": "Fremhevd av {name}",
|
||||
"status.delete": "Slett",
|
||||
"status.reply": "Svar",
|
||||
"status.report": "Rapporter @{name}",
|
||||
"status.reblog": "Reblogg",
|
||||
"status.favourite": "Lik",
|
||||
"status.reblogged_by": "{name} reblogget",
|
||||
"status.sensitive_warning": "Sensitivt innhold",
|
||||
"status.sensitive_toggle": "Klikk for å vise",
|
||||
"status.sensitive_warning": "Følsomt innhold",
|
||||
"status.show_less": "Vis mindre",
|
||||
"status.show_more": "Vis mer",
|
||||
"tabs_bar.compose": "Komponer",
|
||||
"tabs_bar.federated_timeline": "Felles",
|
||||
"tabs_bar.home": "Hjem",
|
||||
"tabs_bar.local_timeline": "Lokal",
|
||||
"tabs_bar.notifications": "Varslinger",
|
||||
"upload_area.title": "Dra og slipp for å laste opp",
|
||||
"upload_button.label": "Legg til media",
|
||||
"upload_form.undo": "Angre",
|
||||
"upload_progress.label": "Laster opp...",
|
||||
"status.show_less": "Vis mindre",
|
||||
"status.open": "Utvid denne statusen",
|
||||
"status.report": "Rapporter @{name}",
|
||||
"video_player.toggle_sound": "Veksle lyd",
|
||||
"video_player.toggle_visible": "Veksle synlighet",
|
||||
"video_player.expand": "Utvid video",
|
||||
"account.mention": "Nevn @{name}",
|
||||
"account.edit_profile": "Rediger profil",
|
||||
"account.unblock": "Avblokker @{name}",
|
||||
"account.unfollow": "Avfølg",
|
||||
"account.block": "Blokker @{name}",
|
||||
"account.follow": "Følg",
|
||||
"account.posts": "Poster",
|
||||
"account.follows": "Følginger",
|
||||
"account.followers": "Følgere",
|
||||
"account.follows_you": "Folger deg",
|
||||
"account.requested": "Venter på godkjennelse",
|
||||
"getting_started.heading": "Kom i gang",
|
||||
"getting_started.about_addressing": "Du kan følge noen hvis du vet brukernavnet deres og domenet de er på ved å skrive en e-postadresse inn i søkeskjemaet.",
|
||||
"getting_started.about_shortcuts": "Hvis målbrukeren er på samme domene som deg, vil kun brukernavnet også fungere. Den samme regelen gjelder når man nevner noen i statuser.",
|
||||
"getting_started.open_source_notice": "Mastodon er programvare med fri kildekode. Du kan bidra eller rapportere problemer på GitHub på {github}. {apps}.",
|
||||
"column.home": "Hjem",
|
||||
"column.community": "Lokal tidslinje",
|
||||
"column.public": "Forent tidslinje",
|
||||
"column.notifications": "Varslinger",
|
||||
"column.blocks": "Blokkerte brukere",
|
||||
"column.favourites": "Likt",
|
||||
"tabs_bar.compose": "Komponer",
|
||||
"tabs_bar.home": "Hjem",
|
||||
"tabs_bar.mentions": "Nevninger",
|
||||
"tabs_bar.public": "Felles tidslinje",
|
||||
"tabs_bar.public": "Forent tidslinje",
|
||||
"tabs_bar.notifications": "Varslinger",
|
||||
"compose_form.placeholder": "Hva har du på hjertet?",
|
||||
"compose_form.publish": "Tut",
|
||||
"compose_form.sensitive": "Merk media som følsomt",
|
||||
"compose_form.spoiler": "Skjul tekst bak advarsel",
|
||||
"compose_form.private": "Merk som privat",
|
||||
"compose_form.privacy_disclaimer": "Din private status vil leveres til nevnte brukere på {domains}. Stoler du på {domainsCount, plural, one {den serveren} other {de serverne}}? Synlighet fungerer kun på Mastodon-instanser. Hvis {domains} {domainsCount, plural, one {ike er en Mastodon-instans} other {ikke er Mastodon-instanser}}, vil det ikke indikeres at posten din er privat, og den kan kanskje bli reblogget eller på annen måte bli synlig for uventede mottakere.",
|
||||
"compose_form.unlisted": "Ikke vis på offentlige tidslinjer",
|
||||
"navigation_bar.edit_profile": "Rediger profil",
|
||||
"navigation_bar.preferences": "Preferanser",
|
||||
"navigation_bar.community_timeline": "Lokal tidslinje",
|
||||
"navigation_bar.public_timeline": "Forent tidslinje",
|
||||
"navigation_bar.logout": "Logg ut",
|
||||
"navigation_bar.blocks": "Blokkerte brukere",
|
||||
"navigation_bar.info": "Utvidet informasjon",
|
||||
"navigation_bar.favourites": "Likt",
|
||||
"reply_indicator.cancel": "Avbryt",
|
||||
"search.placeholder": "Søk",
|
||||
"search.account": "Konto",
|
||||
"search.hashtag": "Hashtag",
|
||||
"notification.mention": "{name} nevnte deg"
|
||||
"upload_button.label": "Legg til media",
|
||||
"upload_form.undo": "Angre",
|
||||
"notification.follow": "{name} fulgte deg",
|
||||
"notification.favourite": "{name} likte din status",
|
||||
"notification.reblog": "{name} reblogget din status",
|
||||
"notification.mention": "{name} nevnte deg",
|
||||
"notifications.column_settings.alert": "Skrivebordsvarslinger",
|
||||
"notifications.column_settings.show": "Vis i kolonne",
|
||||
"notifications.column_settings.follow": "Nye følgere:",
|
||||
"notifications.column_settings.favourite": "Likt:",
|
||||
"notifications.column_settings.mention": "Nevninger:",
|
||||
"notifications.column_settings.reblog": "Reblogginger:",
|
||||
};
|
||||
|
||||
export default no;
|
||||
|
|
|
@ -2,9 +2,7 @@ const ru = {
|
|||
"column_back_button.label": "Назад",
|
||||
"lightbox.close": "Закрыть",
|
||||
"loading_indicator.label": "Загрузка...",
|
||||
"missing_indicator.label": "Не найдено",
|
||||
"status.mention": "Упомянуть @{name}",
|
||||
"status.media_hidden": "Медиаконтент скрыт",
|
||||
"status.delete": "Удалить",
|
||||
"status.reply": "Ответить",
|
||||
"status.reblog": "Продвинуть",
|
||||
|
@ -16,25 +14,20 @@ const ru = {
|
|||
"status.show_less": "Свернуть",
|
||||
"status.open": "Развернуть статус",
|
||||
"status.report": "Пожаловаться",
|
||||
"status.load_more": "Показать еще",
|
||||
"status.load_more": "Показать еще",
|
||||
"video_player.toggle_sound": "Вкл./выкл. звук",
|
||||
"video_player.toggle_visible": "Показать/скрыть",
|
||||
"account.disclaimer": "Это пользователь с другого узла. Число может быть больше.",
|
||||
"account.mention": "Упомянуть",
|
||||
"account.edit_profile": "Изменить профиль",
|
||||
"account.unblock": "Разблокировать",
|
||||
"account.unfollow": "Отписаться",
|
||||
"account.block": "Блокировать",
|
||||
"account.mute": "Заглушить",
|
||||
"account.report": "Пожаловаться",
|
||||
"account.unmute": "Снять глушение",
|
||||
"account.follow": "Подписаться",
|
||||
"account.posts": "Посты",
|
||||
"account.follows": "Подписки",
|
||||
"account.followers": "Подписаны",
|
||||
"account.follows_you": "Подписан(а) на Вас",
|
||||
"account.requested": "Ожидает подтверждения",
|
||||
"boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз",
|
||||
"getting_started.heading": "Добро пожаловать",
|
||||
"getting_started.about_addressing": "Вы можете подписаться на человека, зная имя пользователя и домен, на котором он находится, введя e-mail-подобный адрес в форму поиска.",
|
||||
"getting_started.about_shortcuts": "Если пользователь находится на одном с Вами домене, можно использовать только имя. То же правило применимо к упоминанию пользователей в статусах.",
|
||||
|
@ -44,16 +37,11 @@ const ru = {
|
|||
"column.community": "Локальная лента",
|
||||
"column.public": "Глобальная лента",
|
||||
"column.notifications": "Уведомления",
|
||||
"column.favourites": "Понравившееся",
|
||||
"column.blocks": "Список блокировки",
|
||||
"column.follow_requests": "Запросы на подписку",
|
||||
"tabs_bar.compose": "Написать",
|
||||
"tabs_bar.home": "Главная",
|
||||
"tabs_bar.mentions": "Упоминания",
|
||||
"tabs_bar.public": "Глобальная лента",
|
||||
"tabs_bar.notifications": "Уведомления",
|
||||
"tabs_bar.local_timeline": "Локальная",
|
||||
"tabs_bar.federated_timeline": "Глобальная",
|
||||
"compose_form.placeholder": "О чем Вы думаете?",
|
||||
"compose_form.publish": "Трубить",
|
||||
"compose_form.sensitive": "Отметить как чувствительный контент",
|
||||
|
@ -61,7 +49,6 @@ const ru = {
|
|||
"compose_form.private": "Отметить как приватное",
|
||||
"compose_form.privacy_disclaimer": "Ваш приватный статус будет доставлен упомянутым пользователям на доменах {domains}. Доверяете ли вы {domainsCount, plural, one {этому серверу} other {этим серверам}}? Приватность постов работает только на узлах Mastodon. Если {domains} {domainsCount, plural, one {не является узлом Mastodon} other {не являются узлами Mastodon}}, приватность поста не будет указана, и он может оказаться продвинут или иным образом показан не обозначенным Вами пользователям.",
|
||||
"compose_form.unlisted": "Не отображать в публичных лентах",
|
||||
"compose_form.spoiler_placeholder": "Не для всех",
|
||||
"navigation_bar.edit_profile": "Изменить профиль",
|
||||
"navigation_bar.preferences": "Опции",
|
||||
"navigation_bar.community_timeline": "Локальная лента",
|
||||
|
@ -70,20 +57,12 @@ const ru = {
|
|||
"navigation_bar.info": "Об узле",
|
||||
"navigation_bar.favourites": "Понравившееся",
|
||||
"navigation_bar.blocks": "Список блокировки",
|
||||
"navigation_bar.follow_requests": "Запросы на подписку",
|
||||
"reply_indicator.cancel": "Отмена",
|
||||
"report.target": "Жалуемся на",
|
||||
"report.heading": "Новая жалоба",
|
||||
"report.placeholder": "Комментарий",
|
||||
"report.submit": "Отправить",
|
||||
"search.placeholder": "Поиск",
|
||||
"search.account": "Аккаунт",
|
||||
"search.hashtag": "Хэштег",
|
||||
"search.status_by": "Статус от {name}",
|
||||
"upload_area.title": "Перетащите сюда, чтобы загрузить",
|
||||
"upload_button.label": "Добавить медиаконтент",
|
||||
"upload_form.undo": "Отменить",
|
||||
"upload_progress.label": "Загрузка...",
|
||||
"notification.follow": "{name} подписался(-лась) на Вас",
|
||||
"notification.favourite": "{name} понравился Ваш статус",
|
||||
"notification.reblog": "{name} продвинул(а) Ваш статус",
|
||||
|
@ -92,10 +71,9 @@ const ru = {
|
|||
"home.column_settings.basic": "Основные",
|
||||
"home.column_settings.advanced": "Дополнительные",
|
||||
"home.column_settings.filter_regex": "Отфильтровать регулярным выражением",
|
||||
"home.column_settings.show_reblogs": "Показывать продвижения",
|
||||
"home.column_settings.show_replies": "Показывать продвижения",
|
||||
"home.column_settings.show_replies": "Показывать ответы",
|
||||
"notifications.clear": "Очистить уведомления",
|
||||
"notifications.clear_confirmation": "Вы уверены, что хотите очистить все уведомления?",
|
||||
"notifications.settings": "Настройки колонки",
|
||||
"notifications.column_settings.alert": "Десктопные уведомления",
|
||||
"notifications.column_settings.show": "Показывать в колонке",
|
||||
|
@ -118,10 +96,6 @@ const ru = {
|
|||
"privacy.private.long": "Показать только подписчикам",
|
||||
"privacy.direct.short": "Направленный",
|
||||
"privacy.direct.long": "Показать только упомянутым",
|
||||
"emoji_button.label": "Вставить эмодзи",
|
||||
"follow_request.authorize": "Авторизовать",
|
||||
"follow_request.reject": "Отказать",
|
||||
"media_gallery.toggle_visible": "Показать/скрыть",
|
||||
};
|
||||
|
||||
export default ru;
|
||||
|
|
|
@ -19,27 +19,19 @@ export { localeData as localeData };
|
|||
|
||||
const zh_hk = {
|
||||
"account.block": "封鎖 @{name}",
|
||||
"account.disclaimer": "由於這個用戶在另一個服務站,實際數字會比這個更多。",
|
||||
"account.edit_profile": "修改個人資料",
|
||||
"account.follow": "關注",
|
||||
"account.followers": "關注的人",
|
||||
"account.follows_you": "關注你",
|
||||
"account.follows": "正在關注",
|
||||
"account.mention": "提及 @{name}",
|
||||
"account.mute": "將 @{name} 靜音",
|
||||
"account.posts": "文章",
|
||||
"account.report": "舉報 @{name}",
|
||||
"account.requested": "等候審批",
|
||||
"account.unblock": "解除對 @{name} 的封鎖",
|
||||
"account.unfollow": "取消關注",
|
||||
"account.unmute": "取消 @{name} 的靜音",
|
||||
"boost_modal.combo": "如你想在下次路過這顯示,請按{combo},",
|
||||
"column_back_button.label": "返回",
|
||||
"column.blocks": "封鎖用戶",
|
||||
"column_back_button.label": "先前顯示",
|
||||
"column.community": "本站時間軸",
|
||||
"column.favourites": "喜歡的文章",
|
||||
"column.follow_requests": "關注請求",
|
||||
"column.home": "主頁",
|
||||
"column.home": "家",
|
||||
"column.notifications": "通知",
|
||||
"column.public": "跨站公共時間軸",
|
||||
"compose_form.placeholder": "你在想甚麼?",
|
||||
|
@ -47,49 +39,35 @@ const zh_hk = {
|
|||
"compose_form.private": "標示為「只有關注你的人能看」",
|
||||
"compose_form.publish": "發文",
|
||||
"compose_form.sensitive": "將媒體檔案標示為「敏感內容」",
|
||||
"compose_form.spoiler_placeholder": "敏感內容",
|
||||
"compose_form.spoiler": "將部份文字藏於警告訊息之後",
|
||||
"compose_form.unlisted": "請勿在公共時間軸顯示",
|
||||
"emoji_button.label": "加入表情符號",
|
||||
"empty_column.community": "本站時間軸暫時未有內容,快貼文來搶頭香啊!",
|
||||
"empty_column.hashtag": "這個標籤暫時未有內容。",
|
||||
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
|
||||
"empty_column.home.public_timeline": "公共時間軸",
|
||||
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
|
||||
"empty_column.notifications": "你沒有任何通知紀錄,快向其他用戶搭訕吧。",
|
||||
"empty_column.public": "跨站公共時間軸暫時沒有內容!快寫一些公共的文章,或者關注另一些服務站的用戶吧!你和本站、友站的交流,將決定這裏出現的內容。",
|
||||
"follow_request.authorize": "批准",
|
||||
"follow_request.reject": "拒絕",
|
||||
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
|
||||
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up.",
|
||||
"getting_started.about_addressing": "只要你知道一位用戶的用戶名稱和域名,你可以用「@用戶名稱@域名」的格式在搜尋欄尋找該用戶。",
|
||||
"getting_started.about_shortcuts": "只要該用戶是在你現在的服務站開立,你可以直接輸入用戶𠱷搜尋。同樣的規則適用於在文章提及別的用戶。",
|
||||
"getting_started.apps": "手機或桌面應用程式",
|
||||
"getting_started.heading": "開始使用",
|
||||
"getting_started.open_source_notice": "Mastodon 是一個開放源碼的軟件。你可以在官方 GitHub ({github}) 貢獻或者回報問題。你亦可透過{apps}閱讀 Mastodon 上的消息。",
|
||||
"home.column_settings.advanced": "進階",
|
||||
"home.column_settings.basic": "基本",
|
||||
"home.column_settings.filter_regex": "使用正規表達式 (regular expression) 過濾",
|
||||
"home.column_settings.show_reblogs": "顯示被轉推的文章",
|
||||
"home.column_settings.show_replies": "顯示回應文章",
|
||||
"home.settings": "欄位設定",
|
||||
"lightbox.close": "Close",
|
||||
"home.column_settings.advanced": "進階",
|
||||
"lightbox.close": "關閉",
|
||||
"loading_indicator.label": "載入中...",
|
||||
"media_gallery.toggle_visible": "打開或關上",
|
||||
"missing_indicator.label": "找不到內容",
|
||||
"navigation_bar.blocks": "被封鎖的用戶",
|
||||
"navigation_bar.community_timeline": "本站時間軸",
|
||||
"navigation_bar.edit_profile": "修改個人資料",
|
||||
"navigation_bar.favourites": "喜歡的內容",
|
||||
"navigation_bar.follow_requests": "關注請求",
|
||||
"navigation_bar.info": "關於本服務站",
|
||||
"navigation_bar.logout": "登出",
|
||||
"navigation_bar.preferences": "偏好設定",
|
||||
"navigation_bar.preferences": "個人設定",
|
||||
"navigation_bar.public_timeline": "跨站公共時間軸",
|
||||
"notification.favourite": "{name} 喜歡你的文章",
|
||||
"notification.follow": "{name} 開始關注你",
|
||||
"notification.follow": "{name} 開始開始你",
|
||||
"notification.mention": "{name} 提及你",
|
||||
"notification.reblog": "{name} 轉推你的文章",
|
||||
"notifications.clear_confirmation": "你確定要清空通知紀錄嗎?",
|
||||
"notifications.clear": "清空通知紀錄",
|
||||
"notifications.column_settings.alert": "顯示桌面通知",
|
||||
"notifications.column_settings.favourite": "喜歡你的文章:",
|
||||
"notifications.column_settings.follow": "關注你:",
|
||||
|
@ -97,26 +75,13 @@ const zh_hk = {
|
|||
"notifications.column_settings.reblog": "轉推你的文章:",
|
||||
"notifications.column_settings.show": "在通知欄顯示",
|
||||
"notifications.column_settings.sound": "播放音效",
|
||||
"notifications.settings": "欄位設定",
|
||||
"privacy.change": "調整私隱設定",
|
||||
"privacy.direct.long": "只有提及的用戶能看到",
|
||||
"privacy.direct.short": "私人訊息",
|
||||
"privacy.private.long": "只有關注你用戶能看到",
|
||||
"privacy.private.short": "關注者",
|
||||
"privacy.public.long": "在公共時間軸顯示",
|
||||
"privacy.public.short": "公共",
|
||||
"privacy.unlisted.long": "公開,但不在公共時間軸顯示",
|
||||
"privacy.unlisted.short": "公開",
|
||||
"reply_indicator.cancel": "取消",
|
||||
"report.heading": "舉報",
|
||||
"report.placeholder": "額外訊息",
|
||||
"report.submit": "提交",
|
||||
"report.target": "Reporting",
|
||||
"search_results.total": "{count} 項結果",
|
||||
"search.account": "用戶",
|
||||
"search.hashtag": "標籤",
|
||||
"search.placeholder": "搜尋",
|
||||
"search.status_by": "按{name}搜尋文章",
|
||||
"search_results.total": "{count} 項結果",
|
||||
"search.status_by": "按用戶名稱搜尋文章",
|
||||
"status.delete": "刪除",
|
||||
"status.favourite": "喜歡",
|
||||
"status.load_more": "載入更多",
|
||||
|
@ -132,19 +97,17 @@ const zh_hk = {
|
|||
"status.show_less": "減少顯示",
|
||||
"status.show_more": "顯示更多",
|
||||
"tabs_bar.compose": "撰寫",
|
||||
"tabs_bar.federated_timeline": "跨站",
|
||||
"tabs_bar.home": "主頁",
|
||||
"tabs_bar.home": "家",
|
||||
"tabs_bar.local_timeline": "本站",
|
||||
"tabs_bar.mentions": "提及",
|
||||
"tabs_bar.notifications": "通知",
|
||||
"tabs_bar.public": "跨站公共時間軸",
|
||||
"tabs_bar.federated_timeline": "跨站",
|
||||
"upload_area.title": "將檔案拖放至此上載",
|
||||
"upload_button.label": "上載媒體檔案",
|
||||
"upload_form.undo": "還原",
|
||||
"upload_progress.label": "上載中……",
|
||||
"video_player.expand": "展開影片",
|
||||
"upload_form.undo": "還原",
|
||||
"video_player.toggle_sound": "開關音效",
|
||||
"video_player.toggle_visible": "打開或關上",
|
||||
};
|
||||
|
||||
export default zh_hk;
|
||||
|
|
|
@ -22,7 +22,7 @@ export default function errorsMiddleware() {
|
|||
|
||||
dispatch(showAlert(title, message));
|
||||
} else {
|
||||
console.error(action.error); // eslint-disable-line no-console
|
||||
console.error(action.error);
|
||||
dispatch(showAlert('Oops!', 'An unexpected error occurred.'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,10 +15,6 @@ import {
|
|||
BLOCKS_FETCH_SUCCESS,
|
||||
BLOCKS_EXPAND_SUCCESS
|
||||
} from '../actions/blocks';
|
||||
import {
|
||||
MUTES_FETCH_SUCCESS,
|
||||
MUTES_EXPAND_SUCCESS
|
||||
} from '../actions/mutes';
|
||||
import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose';
|
||||
import {
|
||||
REBLOG_SUCCESS,
|
||||
|
@ -98,8 +94,6 @@ export default function accounts(state = initialState, action) {
|
|||
case FOLLOW_REQUESTS_EXPAND_SUCCESS:
|
||||
case BLOCKS_FETCH_SUCCESS:
|
||||
case BLOCKS_EXPAND_SUCCESS:
|
||||
case MUTES_FETCH_SUCCESS:
|
||||
case MUTES_EXPAND_SUCCESS:
|
||||
return normalizeAccounts(state, action.accounts);
|
||||
case NOTIFICATIONS_REFRESH_SUCCESS:
|
||||
case NOTIFICATIONS_EXPAND_SUCCESS:
|
||||
|
|
|
@ -9,17 +9,17 @@ const initialState = Immutable.List([]);
|
|||
|
||||
export default function alerts(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case ALERT_SHOW:
|
||||
return state.push(Immutable.Map({
|
||||
key: state.size > 0 ? state.last().get('key') + 1 : 0,
|
||||
title: action.title,
|
||||
message: action.message
|
||||
}));
|
||||
case ALERT_DISMISS:
|
||||
return state.filterNot(item => item.get('key') === action.alert.key);
|
||||
case ALERT_CLEAR:
|
||||
return state.clear();
|
||||
default:
|
||||
return state;
|
||||
case ALERT_SHOW:
|
||||
return state.push(Immutable.Map({
|
||||
key: state.size > 0 ? state.last().get('key') + 1 : 0,
|
||||
title: action.title,
|
||||
message: action.message
|
||||
}));
|
||||
case ALERT_DISMISS:
|
||||
return state.filterNot(item => item.get('key') === action.alert.key);
|
||||
case ALERT_CLEAR:
|
||||
return state.clear();
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,7 +2,6 @@ import { STORE_HYDRATE } from '../actions/store';
|
|||
import Immutable from 'immutable';
|
||||
|
||||
const initialState = Immutable.Map({
|
||||
streaming_api_base_url: null,
|
||||
access_token: null,
|
||||
me: null
|
||||
});
|
||||
|
|
|
@ -3,8 +3,6 @@ import { STORE_HYDRATE } from '../actions/store';
|
|||
import Immutable from 'immutable';
|
||||
|
||||
const initialState = Immutable.Map({
|
||||
onboarded: false,
|
||||
|
||||
home: Immutable.Map({
|
||||
shows: Immutable.Map({
|
||||
reblog: true,
|
||||
|
|
|
@ -48,9 +48,6 @@ const normalizeStatus = (state, status) => {
|
|||
normalStatus.reblog = status.reblog.id;
|
||||
}
|
||||
|
||||
const linebreakComplemented = status.content.replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
||||
normalStatus.unescaped_content = new DOMParser().parseFromString(linebreakComplemented, 'text/html').documentElement.textContent;
|
||||
|
||||
return state.update(status.id, Immutable.Map(), map => map.mergeDeep(Immutable.fromJS(normalStatus)));
|
||||
};
|
||||
|
||||
|
|
|
@ -16,10 +16,6 @@ import {
|
|||
BLOCKS_FETCH_SUCCESS,
|
||||
BLOCKS_EXPAND_SUCCESS
|
||||
} from '../actions/blocks';
|
||||
import {
|
||||
MUTES_FETCH_SUCCESS,
|
||||
MUTES_EXPAND_SUCCESS
|
||||
} from '../actions/mutes';
|
||||
import Immutable from 'immutable';
|
||||
|
||||
const initialState = Immutable.Map({
|
||||
|
@ -28,8 +24,7 @@ const initialState = Immutable.Map({
|
|||
reblogged_by: Immutable.Map(),
|
||||
favourited_by: Immutable.Map(),
|
||||
follow_requests: Immutable.Map(),
|
||||
blocks: Immutable.Map(),
|
||||
mutes: Immutable.Map()
|
||||
blocks: Immutable.Map()
|
||||
});
|
||||
|
||||
const normalizeList = (state, type, id, accounts, next) => {
|
||||
|
@ -70,10 +65,6 @@ export default function userLists(state = initialState, action) {
|
|||
return state.setIn(['blocks', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
|
||||
case BLOCKS_EXPAND_SUCCESS:
|
||||
return state.updateIn(['blocks', 'items'], list => list.push(...action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
|
||||
case MUTES_FETCH_SUCCESS:
|
||||
return state.setIn(['mutes', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
||||
case MUTES_EXPAND_SUCCESS:
|
||||
return state.updateIn(['mutes', 'items'], list => list.push(...action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@ const createWebSocketURL = (url) => {
|
|||
return a.href;
|
||||
};
|
||||
|
||||
export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) {
|
||||
const ws = new WebSocketClient(`${createWebSocketURL(streamingAPIBaseURL)}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`);
|
||||
export default function getStream(accessToken, stream, { connected, received, disconnected, reconnected }) {
|
||||
const ws = new WebSocketClient(`${createWebSocketURL(STREAMING_API_BASE_URL)}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`);
|
||||
|
||||
ws.onopen = connected;
|
||||
ws.onmessage = e => received(JSON.parse(e.data));
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
p, li {
|
||||
font: 16px/28px 'Montserrat', sans-serif;
|
||||
font-weight: 400;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 26px;
|
||||
|
||||
a {
|
||||
color: $color4;
|
||||
|
@ -352,7 +352,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 625px) {
|
||||
.mascot {
|
||||
display: none;
|
||||
|
|
|
@ -889,11 +889,6 @@ a.status__content__spoiler-link {
|
|||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.column {
|
||||
flex-grow: 1;
|
||||
max-width: 600px; // This is just a guess at a sane max value
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 2560px) {
|
||||
|
@ -907,11 +902,6 @@ a.status__content__spoiler-link {
|
|||
height: 90vh;
|
||||
margin-top: 5vh;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex-grow: 1;
|
||||
max-width: 600px; // This is just a guess at a sane max value
|
||||
}
|
||||
}
|
||||
|
||||
.drawer__pager {
|
||||
|
@ -942,12 +932,6 @@ a.status__content__spoiler-link {
|
|||
}
|
||||
}
|
||||
|
||||
.pseudo-drawer {
|
||||
background: lighten($color1, 13%);
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.drawer__header {
|
||||
flex: 0 0 auto;
|
||||
font-size: 16px;
|
||||
|
@ -1219,10 +1203,6 @@ a.status__content__spoiler-link {
|
|||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.spoiler-input__input {
|
||||
|
@ -1287,10 +1267,6 @@ a.status__content__spoiler-link {
|
|||
color: $color5;
|
||||
border-bottom-color: $color4;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@import 'boost';
|
||||
|
@ -1407,7 +1383,7 @@ button.icon-button.active i.fa-retweet {
|
|||
}
|
||||
}
|
||||
|
||||
.media-spoiler, .video-error-cover {
|
||||
.media-spoiler {
|
||||
background: $color8;
|
||||
color: $color5;
|
||||
}
|
||||
|
@ -1930,10 +1906,6 @@ button.icon-button.active i.fa-retweet {
|
|||
&:focus {
|
||||
background: lighten($color1, 4%);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.search__icon {
|
||||
|
@ -2034,7 +2006,6 @@ button.icon-button.active i.fa-retweet {
|
|||
.modal-root__modal {
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.media-modal {
|
||||
|
@ -2048,237 +2019,6 @@ button.icon-button.active i.fa-retweet {
|
|||
}
|
||||
}
|
||||
|
||||
.onboarding-modal {
|
||||
background: $color2;
|
||||
color: $color1;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.onboarding-modal__pager {
|
||||
height: 80vh;
|
||||
width: 80vw;
|
||||
max-width: 500px;
|
||||
max-height: 350px;
|
||||
position: relative;
|
||||
|
||||
& > div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 25px;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
opacity: 0;
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
.onboarding-modal {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.onboarding-modal__pager {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding-modal__paginator {
|
||||
flex: 0 0 auto;
|
||||
background: darken($color2, 8%);
|
||||
display: flex;
|
||||
padding: 25px;
|
||||
|
||||
& > div {
|
||||
min-width: 33px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: darken($color2, 34%);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover, &:focus, &:active {
|
||||
color: darken($color2, 38%);
|
||||
}
|
||||
|
||||
&.onboarding-modal__done, &.onboarding-modal__next {
|
||||
color: $color4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding-modal__dots {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.onboarding-modal__dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 14px;
|
||||
background: darken($color2, 16%);
|
||||
margin: 0 3px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: darken($color2, 18%);
|
||||
}
|
||||
|
||||
&.active {
|
||||
cursor: default;
|
||||
background: darken($color2, 24%);
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding-modal__page {
|
||||
cursor: default;
|
||||
line-height: 21px;
|
||||
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: $color1;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $color4;
|
||||
|
||||
&:hover, &:focus, &:active {
|
||||
color: lighten($color4, 4%);
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
color: lighten($color1, 8%);
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 500;
|
||||
background: $color1;
|
||||
color: $color2;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding-modal__page-one {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.onboarding-modal__page-one__elephant-friend {
|
||||
background: image-url('elephant-friend.png') no-repeat 0 0;
|
||||
width: 147px;
|
||||
height: 160px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.onboarding-modal__page-two,
|
||||
.onboarding-modal__page-three,
|
||||
.onboarding-modal__page-four,
|
||||
.onboarding-modal__page-five {
|
||||
p {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.figure {
|
||||
background: darken($color1, 8%);
|
||||
color: $color2;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
box-shadow: 1px 2px 6px rgba($color8, 0.3);
|
||||
|
||||
.onboarding-modal__image {
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&.non-interactive {
|
||||
pointer-events: none;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding-modal__page-four__columns {
|
||||
.row {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
|
||||
& > div {
|
||||
flex: 1 1 0;
|
||||
margin: 0 10px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.column-header {
|
||||
color: $color5;
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding-modal__image {
|
||||
border-radius: 8px;
|
||||
width: 70vw;
|
||||
max-width: 450px;
|
||||
max-height: auto;
|
||||
display: block;
|
||||
margin: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.onboard-sliders {
|
||||
display: inline-block;
|
||||
max-width: 30px;
|
||||
max-height: auto;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.boost-modal {
|
||||
background: lighten($color2, 8%);
|
||||
color: $color1;
|
||||
|
@ -2316,11 +2056,3 @@ button.icon-button.active i.fa-retweet {
|
|||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-bar {
|
||||
background-color: $color4;
|
||||
height: 3px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,3 @@
|
|||
margin: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.recovery-codes {
|
||||
list-style: none;
|
||||
}
|
||||
|
|
16
app/assets/stylesheets/variables.scss
Executable file → Normal file
16
app/assets/stylesheets/variables.scss
Executable file → Normal file
|
@ -1,8 +1,8 @@
|
|||
$color1: #282c37 !default; // darkest
|
||||
$color2: #d9e1e8 !default; // lightest
|
||||
$color3: #9baec8 !default; // lighter
|
||||
$color4: #2b90d9 !default; // vibrant
|
||||
$color5: #ffffff !default; // white
|
||||
$color6: #df405a !default; // error red
|
||||
$color7: #79bd9a !default; // succ green
|
||||
$color8: #000000 !default; // black
|
||||
$color1: #282c37; // darkest
|
||||
$color2: #d9e1e8; // lightest
|
||||
$color3: #9baec8; // lighter
|
||||
$color4: #2b90d9; // vibrant
|
||||
$color5: #ffffff; // white
|
||||
$color6: #df405a; // error red
|
||||
$color7: #79bd9a; // succ green
|
||||
$color8: #000000; // black
|
||||
|
|
|
@ -15,26 +15,16 @@ module Admin
|
|||
|
||||
if @domain_block.save
|
||||
DomainBlockWorker.perform_async(@domain_block.id)
|
||||
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg')
|
||||
redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed'
|
||||
else
|
||||
render action: :new
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@domain_block = DomainBlock.find(params[:id])
|
||||
end
|
||||
|
||||
def destroy
|
||||
@domain_block = DomainBlock.find(params[:id])
|
||||
UnblockDomainService.new.call(@domain_block, resource_params[:retroactive])
|
||||
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resource_params
|
||||
params.require(:domain_block).permit(:domain, :severity, :reject_media, :retroactive)
|
||||
params.require(:domain_block).permit(:domain, :severity)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class ReportedStatusesController < BaseController
|
||||
def destroy
|
||||
status = Status.find params[:id]
|
||||
|
||||
RemovalWorker.perform_async(status.id)
|
||||
redirect_to admin_report_path(report)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def report
|
||||
Report.find(params[:report_id])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,60 +5,38 @@ module Admin
|
|||
before_action :set_report, except: [:index]
|
||||
|
||||
def index
|
||||
@reports = filtered_reports.page(params[:page])
|
||||
@reports = Report.includes(:account, :target_account).order('id desc').page(params[:page])
|
||||
@reports = params[:action_taken].present? ? @reports.resolved : @reports.unresolved
|
||||
end
|
||||
|
||||
def show; end
|
||||
def show
|
||||
@statuses = Status.where(id: @report.status_ids)
|
||||
end
|
||||
|
||||
def update
|
||||
process_report
|
||||
def resolve
|
||||
@report.update(action_taken: true, action_taken_by_account_id: current_account.id)
|
||||
redirect_to admin_report_path(@report)
|
||||
end
|
||||
|
||||
def suspend
|
||||
Admin::SuspensionWorker.perform_async(@report.target_account.id)
|
||||
Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
|
||||
redirect_to admin_report_path(@report)
|
||||
end
|
||||
|
||||
def silence
|
||||
@report.target_account.update(silenced: true)
|
||||
Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
|
||||
redirect_to admin_report_path(@report)
|
||||
end
|
||||
|
||||
def remove
|
||||
RemovalWorker.perform_async(params[:status_id])
|
||||
redirect_to admin_report_path(@report)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_report
|
||||
case params[:outcome].to_s
|
||||
when 'resolve'
|
||||
@report.update(action_taken_by_current_attributes)
|
||||
when 'suspend'
|
||||
Admin::SuspensionWorker.perform_async(@report.target_account.id)
|
||||
resolve_all_target_account_reports
|
||||
when 'silence'
|
||||
@report.target_account.update(silenced: true)
|
||||
resolve_all_target_account_reports
|
||||
else
|
||||
raise ActiveRecord::RecordNotFound
|
||||
end
|
||||
end
|
||||
|
||||
def action_taken_by_current_attributes
|
||||
{ action_taken: true, action_taken_by_account_id: current_account.id }
|
||||
end
|
||||
|
||||
def resolve_all_target_account_reports
|
||||
unresolved_reports_for_target_account.update_all(
|
||||
action_taken_by_current_attributes
|
||||
)
|
||||
end
|
||||
|
||||
def unresolved_reports_for_target_account
|
||||
Report.where(
|
||||
target_account: @report.target_account
|
||||
).unresolved
|
||||
end
|
||||
|
||||
def filtered_reports
|
||||
filtering_scope.order('id desc').includes(
|
||||
:account,
|
||||
:target_account
|
||||
)
|
||||
end
|
||||
|
||||
def filtering_scope
|
||||
params[:resolved].present? ? Report.resolved : Report.unresolved
|
||||
end
|
||||
|
||||
def set_report
|
||||
@report = Report.find(params[:id])
|
||||
end
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class ResetsController < BaseController
|
||||
before_action :set_account
|
||||
|
||||
def create
|
||||
@account.user.send_reset_password_instructions
|
||||
redirect_to admin_accounts_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -14,7 +14,7 @@ class Api::OEmbedController < ApiController
|
|||
def stream_entry_from_url(url)
|
||||
params = Rails.application.routes.recognize_path(url)
|
||||
|
||||
raise ActiveRecord::RecordNotFound unless params[:controller] == 'stream_entries' && params[:action] == 'show'
|
||||
raise ActiveRecord::NotFound unless params[:controller] == 'stream_entries' && params[:action] == 'show'
|
||||
|
||||
StreamEntry.find(params[:id])
|
||||
end
|
||||
|
|
|
@ -30,7 +30,7 @@ class Api::PushController < ApiController
|
|||
params = Rails.application.routes.recognize_path(uri.path)
|
||||
domain = uri.host + (uri.port ? ":#{uri.port}" : '')
|
||||
|
||||
return unless TagManager.instance.web_domain?(domain) && params[:controller] == 'accounts' && params[:action] == 'show' && params[:format] == 'atom'
|
||||
return unless TagManager.instance.local_domain?(domain) && params[:controller] == 'accounts' && params[:action] == 'show' && params[:format] == 'atom'
|
||||
|
||||
Account.find_local(params[:username])
|
||||
end
|
||||
|
|
|
@ -8,9 +8,7 @@ class ApplicationController < ActionController::Base
|
|||
force_ssl if: "Rails.env.production? && ENV['LOCAL_HTTPS'] == 'true'"
|
||||
|
||||
include Localized
|
||||
|
||||
helper_method :current_account
|
||||
helper_method :single_user_mode?
|
||||
|
||||
rescue_from ActionController::RoutingError, with: :not_found
|
||||
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
||||
|
@ -71,10 +69,6 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
end
|
||||
|
||||
def single_user_mode?
|
||||
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.first
|
||||
end
|
||||
|
||||
def current_account
|
||||
@current_account ||= current_user.try(:account)
|
||||
end
|
||||
|
|
|
@ -28,7 +28,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
end
|
||||
|
||||
def check_enabled_registrations
|
||||
redirect_to root_path if single_user_mode? || !Setting.open_registrations
|
||||
redirect_to root_path if Rails.configuration.x.single_user_mode || !Setting.open_registrations
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -49,8 +49,7 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
end
|
||||
|
||||
def valid_otp_attempt?(user)
|
||||
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
|
||||
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
|
||||
user.validate_and_consume_otp!(user_params[:otp_attempt])
|
||||
end
|
||||
|
||||
def authenticate_with_two_factor
|
||||
|
|
|
@ -27,11 +27,7 @@ module Localized
|
|||
|
||||
def default_locale
|
||||
ENV.fetch('DEFAULT_LOCALE') {
|
||||
user_supplied_locale || I18n.default_locale
|
||||
http_accept_language.compatible_language_from(I18n.available_locales) || I18n.default_locale
|
||||
}
|
||||
end
|
||||
|
||||
def user_supplied_locale
|
||||
http_accept_language.language_region_compatible_from(I18n.available_locales)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,17 +4,15 @@ class HomeController < ApplicationController
|
|||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
@body_classes = 'app-body'
|
||||
@token = find_or_create_access_token.token
|
||||
@web_settings = Web::Setting.find_by(user: current_user)&.data || {}
|
||||
@admin = Account.find_local(Setting.site_contact_username)
|
||||
@streaming_api_base_url = Rails.configuration.x.streaming_api_base_url
|
||||
@body_classes = 'app-body'
|
||||
@token = find_or_create_access_token.token
|
||||
@web_settings = Web::Setting.find_by(user: current_user)&.data || {}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authenticate_user!
|
||||
redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in?
|
||||
redirect_to(Rails.configuration.x.single_user_mode ? account_path(Account.first) : about_path) unless user_signed_in?
|
||||
end
|
||||
|
||||
def find_or_create_access_token
|
||||
|
|
|
@ -19,9 +19,9 @@ class Settings::TwoFactorAuthsController < ApplicationController
|
|||
def create
|
||||
if current_user.validate_and_consume_otp!(confirmation_params[:code])
|
||||
current_user.otp_required_for_login = true
|
||||
@codes = current_user.generate_otp_backup_codes!
|
||||
current_user.save!
|
||||
flash[:notice] = I18n.t('two_factor_auth.enabled_success')
|
||||
|
||||
redirect_to settings_two_factor_auth_path, notice: I18n.t('two_factor_auth.enabled_success')
|
||||
else
|
||||
@confirmation = Form::TwoFactorConfirmation.new
|
||||
set_qr_code
|
||||
|
@ -30,12 +30,6 @@ class Settings::TwoFactorAuthsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def recovery_codes
|
||||
@codes = current_user.generate_otp_backup_codes!
|
||||
current_user.save!
|
||||
flash[:notice] = I18n.t('two_factor_auth.recovery_codes_regenerated')
|
||||
end
|
||||
|
||||
def disable
|
||||
current_user.otp_required_for_login = false
|
||||
current_user.save!
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin::FilterHelper
|
||||
ACCOUNT_FILTERS = %i[local remote by_domain silenced suspended recent].freeze
|
||||
REPORT_FILTERS = %i[resolved].freeze
|
||||
|
||||
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS
|
||||
module Admin::AccountsHelper
|
||||
def filter_params(more_params)
|
||||
params.permit(:local, :remote, :by_domain, :silenced, :suspended, :recent).merge(more_params)
|
||||
end
|
||||
|
||||
def filter_link_to(text, more_params)
|
||||
new_url = filtered_url_for(more_params)
|
||||
|
@ -17,10 +16,6 @@ module Admin::FilterHelper
|
|||
|
||||
private
|
||||
|
||||
def filter_params(more_params)
|
||||
params.permit(FILTERS).merge(more_params)
|
||||
end
|
||||
|
||||
def filter_link_class(new_url)
|
||||
filtered_url_for(params) == new_url ? 'selected' : ''
|
||||
end
|
|
@ -4,8 +4,4 @@ module ApplicationHelper
|
|||
def active_nav_class(path)
|
||||
current_page?(path) ? 'active' : ''
|
||||
end
|
||||
|
||||
def show_landing_strip?
|
||||
!user_signed_in? && !single_user_mode?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module StyleHelper
|
||||
def stylesheet_for_layout
|
||||
if asset_exist? 'custom.css'
|
||||
'custom'
|
||||
else
|
||||
'application'
|
||||
end
|
||||
end
|
||||
|
||||
def asset_exist?(path)
|
||||
if Rails.configuration.assets.compile
|
||||
Rails.application.precompiled_assets.include? path
|
||||
else
|
||||
Rails.application.assets_manifest.assets[path].present?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,8 +3,6 @@
|
|||
class AtomSerializer
|
||||
include RoutingHelper
|
||||
|
||||
INVALID_XML_CHARS = /[^\u0009\u000a\u000d\u0020-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]/
|
||||
|
||||
class << self
|
||||
def render(element)
|
||||
document = Ox::Document.new(version: '1.0')
|
||||
|
@ -41,7 +39,7 @@ class AtomSerializer
|
|||
add_namespaces(feed)
|
||||
|
||||
append_element(feed, 'id', account_url(account, format: 'atom'))
|
||||
append_element(feed, 'title', account.display_name.presence || account.username)
|
||||
append_element(feed, 'title', account.display_name)
|
||||
append_element(feed, 'subtitle', account.note)
|
||||
append_element(feed, 'updated', account.updated_at.iso8601)
|
||||
append_element(feed, 'logo', full_asset_url(account.avatar.url(:original)))
|
||||
|
@ -313,15 +311,11 @@ class AtomSerializer
|
|||
|
||||
def append_element(parent, name, content = nil, attributes = {})
|
||||
element = Ox::Element.new(name)
|
||||
attributes.each { |k, v| element[k] = sanitize_str(v) }
|
||||
element << sanitize_str(content) unless content.nil?
|
||||
attributes.each { |k, v| element[k] = v.to_s }
|
||||
element << content.to_s unless content.nil?
|
||||
parent << element
|
||||
end
|
||||
|
||||
def sanitize_str(raw_str)
|
||||
raw_str.to_s.gsub(INVALID_XML_CHARS, '')
|
||||
end
|
||||
|
||||
def add_namespaces(parent)
|
||||
parent['xmlns'] = TagManager::XMLNS
|
||||
parent['xmlns:thr'] = TagManager::THR_XMLNS
|
||||
|
@ -333,8 +327,8 @@ class AtomSerializer
|
|||
end
|
||||
|
||||
def serialize_status_attributes(entry, status)
|
||||
append_element(entry, 'summary', status.spoiler_text, 'xml:lang': status.language) if status.spoiler_text?
|
||||
append_element(entry, 'content', Formatter.instance.format(status.proper).to_str, type: 'html', 'xml:lang': status.language)
|
||||
append_element(entry, 'summary', status.spoiler_text) if status.spoiler_text?
|
||||
append_element(entry, 'content', Formatter.instance.format(status.proper).to_str, type: 'html')
|
||||
|
||||
status.mentions.each do |mentioned|
|
||||
append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': TagManager::TYPES[:person], href: TagManager.instance.uri_for(mentioned.account))
|
||||
|
|
|
@ -15,7 +15,7 @@ class Formatter
|
|||
html = status.text
|
||||
html = encode(html)
|
||||
html = simple_format(html, {}, sanitize: false)
|
||||
html = html.delete("\n")
|
||||
html = html.gsub(/\n/, '')
|
||||
html = link_urls(html)
|
||||
html = link_mentions(html, status.mentions)
|
||||
html = link_hashtags(html)
|
||||
|
|
|
@ -56,10 +56,6 @@ class TagManager
|
|||
id.start_with?("tag:#{Rails.configuration.x.local_domain}")
|
||||
end
|
||||
|
||||
def web_domain?(domain)
|
||||
domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.web_domain).zero?
|
||||
end
|
||||
|
||||
def local_domain?(domain)
|
||||
domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.local_domain).zero?
|
||||
end
|
||||
|
|
|
@ -59,12 +59,7 @@ class NotificationMailer < ApplicationMailer
|
|||
return if @notifications.empty?
|
||||
|
||||
I18n.with_locale(@me.user.locale || I18n.default_locale) do
|
||||
mail to: @me.user.email,
|
||||
subject: I18n.t(
|
||||
:subject,
|
||||
scope: [:notification_mailer, :digest],
|
||||
count: @notifications.size
|
||||
)
|
||||
mail to: @me.user.email, subject: I18n.t('notification_mailer.digest.subject', count: @notifications.size)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
0
app/models/.keep
Normal file
0
app/models/.keep
Normal file
0
app/models/concerns/.keep
Normal file
0
app/models/concerns/.keep
Normal file
|
@ -3,13 +3,8 @@
|
|||
class DomainBlock < ApplicationRecord
|
||||
enum severity: [:silence, :suspend]
|
||||
|
||||
attr_accessor :retroactive
|
||||
|
||||
validates :domain, presence: true, uniqueness: true
|
||||
|
||||
has_many :accounts, foreign_key: :domain, primary_key: :domain
|
||||
delegate :count, to: :accounts, prefix: true
|
||||
|
||||
def self.blocked?(domain)
|
||||
where(domain: domain, severity: :suspend).exists?
|
||||
end
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Import < ApplicationRecord
|
||||
FILE_TYPES = ['text/plain', 'text/csv'].freeze
|
||||
|
||||
self.inheritance_column = false
|
||||
|
||||
belongs_to :account, required: true
|
||||
|
||||
enum type: [:following, :blocking, :muting]
|
||||
|
||||
validates :type, presence: true
|
||||
belongs_to :account
|
||||
|
||||
FILE_TYPES = ['text/plain', 'text/csv'].freeze
|
||||
|
||||
has_attached_file :data, url: '/system/:hash.:extension', hash_secret: ENV['PAPERCLIP_SECRET']
|
||||
validates_attachment_content_type :data, content_type: FILE_TYPES
|
||||
|
|
|
@ -7,8 +7,4 @@ class Report < ApplicationRecord
|
|||
|
||||
scope :unresolved, -> { where(action_taken: false) }
|
||||
scope :resolved, -> { where(action_taken: true) }
|
||||
|
||||
def statuses
|
||||
Status.where(id: status_ids)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -110,10 +110,6 @@ class Status < ApplicationRecord
|
|||
results
|
||||
end
|
||||
|
||||
def non_sensitive_with_media?
|
||||
!sensitive? && media_attachments.any?
|
||||
end
|
||||
|
||||
class << self
|
||||
def as_home_timeline(account)
|
||||
where(account: [account] + account.following)
|
||||
|
|
|
@ -5,9 +5,7 @@ class User < ApplicationRecord
|
|||
|
||||
devise :registerable, :recoverable,
|
||||
:rememberable, :trackable, :validatable, :confirmable,
|
||||
:two_factor_authenticatable, :two_factor_backupable,
|
||||
otp_secret_encryption_key: ENV['OTP_SECRET'],
|
||||
otp_number_of_backup_codes: 10
|
||||
:two_factor_authenticatable, otp_secret_encryption_key: ENV['OTP_SECRET']
|
||||
|
||||
belongs_to :account, inverse_of: :user
|
||||
accepts_nested_attributes_for :account
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue