Merge branch 'master' into skylight
commit
9a97beb3a5
|
@ -1,12 +1,6 @@
|
||||||
engines:
|
engines:
|
||||||
duplication:
|
duplication:
|
||||||
enabled: true
|
enabled: false
|
||||||
exclude_paths:
|
|
||||||
- app/assets/javascripts/components/locales/
|
|
||||||
config:
|
|
||||||
languages:
|
|
||||||
- ruby
|
|
||||||
- javascript
|
|
||||||
rubocop:
|
rubocop:
|
||||||
enabled: true
|
enabled: true
|
||||||
eslint:
|
eslint:
|
||||||
|
|
|
@ -6,3 +6,6 @@ node_modules
|
||||||
storybook
|
storybook
|
||||||
neo4j
|
neo4j
|
||||||
vendor/bundle
|
vendor/bundle
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# Service dependencies
|
# Service dependencies
|
||||||
REDIS_HOST=redis
|
REDIS_HOST=redis
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
# REDIS_DB=0
|
||||||
DB_HOST=db
|
DB_HOST=db
|
||||||
DB_USER=postgres
|
DB_USER=postgres
|
||||||
DB_NAME=postgres
|
DB_NAME=postgres
|
||||||
|
@ -11,6 +12,10 @@ DB_PORT=5432
|
||||||
LOCAL_DOMAIN=example.com
|
LOCAL_DOMAIN=example.com
|
||||||
LOCAL_HTTPS=true
|
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
|
# Application secrets
|
||||||
# Generate each with the `rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
|
# Generate each with the `rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
|
||||||
PAPERCLIP_SECRET=
|
PAPERCLIP_SECRET=
|
||||||
|
@ -41,6 +46,10 @@ SMTP_FROM_ADDRESS=notifications@example.com
|
||||||
#SMTP_ENABLE_STARTTLS_AUTO=true
|
#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
|
# Optional asset host for multi-server setups
|
||||||
# CDN_HOST=assets.example.com
|
# CDN_HOST=assets.example.com
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
# Federation
|
# Federation
|
||||||
LOCAL_DOMAIN=cb6e6126.ngrok.io
|
LOCAL_DOMAIN=cb6e6126.ngrok.io
|
||||||
LOCAL_HTTPS=true
|
LOCAL_HTTPS=true
|
||||||
|
OTP_SECRET=100c7faeef00caa29242f6b04156742bf76065771fd4117990c4282b8748ff3d99f8fdae97c982ab5bd2e6756a159121377cce4421f4a8ecd2d67bd7749a3fb4
|
||||||
|
|
34
.eslintrc
34
.eslintrc
|
@ -8,7 +8,8 @@
|
||||||
"parser": "babel-eslint",
|
"parser": "babel-eslint",
|
||||||
|
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"react"
|
"react",
|
||||||
|
"jsx-a11y"
|
||||||
],
|
],
|
||||||
|
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
|
@ -43,9 +44,36 @@
|
||||||
"no-mixed-spaces-and-tabs": 1,
|
"no-mixed-spaces-and-tabs": 1,
|
||||||
"no-nested-ternary": 1,
|
"no-nested-ternary": 1,
|
||||||
"no-trailing-spaces": 1,
|
"no-trailing-spaces": 1,
|
||||||
"react/wrap-multilines": 2,
|
|
||||||
|
"react/jsx-wrap-multilines": 2,
|
||||||
"react/self-closing-comp": 2,
|
"react/self-closing-comp": 2,
|
||||||
"react/prop-types": 2,
|
"react/prop-types": 2,
|
||||||
"react/no-multi-comp": 0
|
"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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,10 +29,16 @@ neo4j/
|
||||||
# Ignore Capistrano customizations
|
# Ignore Capistrano customizations
|
||||||
config/deploy/*
|
config/deploy/*
|
||||||
|
|
||||||
|
|
||||||
# Ignore IDE files
|
# Ignore IDE files
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
# Ignore postgres + redis volume optionally created by docker-compose
|
# Ignore postgres + redis volume optionally created by docker-compose
|
||||||
postgres
|
postgres
|
||||||
redis
|
redis
|
||||||
|
|
||||||
|
# Ignore Apple files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Ignore vim files
|
||||||
|
*~
|
||||||
|
*.swp
|
||||||
|
|
|
@ -5,8 +5,6 @@ notifications:
|
||||||
email: false
|
email: false
|
||||||
|
|
||||||
env:
|
env:
|
||||||
matrix:
|
|
||||||
- TRAVIS_NODE_VERSION="4"
|
|
||||||
global:
|
global:
|
||||||
- LOCAL_DOMAIN=cb6e6126.ngrok.io
|
- LOCAL_DOMAIN=cb6e6126.ngrok.io
|
||||||
- LOCAL_HTTPS=true
|
- LOCAL_HTTPS=true
|
||||||
|
@ -16,6 +14,7 @@ addons:
|
||||||
postgresql: 9.4
|
postgresql: 9.4
|
||||||
|
|
||||||
rvm:
|
rvm:
|
||||||
|
- 2.3.4
|
||||||
- 2.4.1
|
- 2.4.1
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
@ -28,8 +27,7 @@ before_install:
|
||||||
- sudo apt-get -qq update
|
- sudo apt-get -qq update
|
||||||
- sudo apt-get -qq install g++-4.8
|
- sudo apt-get -qq install g++-4.8
|
||||||
install:
|
install:
|
||||||
- nvm install $TRAVIS_NODE_VERSION
|
- nvm install
|
||||||
- npm install -g npm@3
|
|
||||||
- npm install -g yarn
|
- npm install -g yarn
|
||||||
- bundle install
|
- bundle install
|
||||||
- yarn install
|
- yarn install
|
||||||
|
@ -40,3 +38,4 @@ before_script:
|
||||||
script:
|
script:
|
||||||
- bundle exec rspec
|
- bundle exec rspec
|
||||||
- npm test
|
- npm test
|
||||||
|
- i18n-tasks unused
|
||||||
|
|
10
Dockerfile
10
Dockerfile
|
@ -12,23 +12,25 @@ WORKDIR /mastodon
|
||||||
|
|
||||||
COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
|
COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
|
||||||
|
|
||||||
RUN BUILD_DEPS=" \
|
RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \
|
||||||
|
&& BUILD_DEPS=" \
|
||||||
postgresql-dev \
|
postgresql-dev \
|
||||||
libxml2-dev \
|
libxml2-dev \
|
||||||
libxslt-dev \
|
libxslt-dev \
|
||||||
build-base" \
|
build-base" \
|
||||||
&& apk -U upgrade && apk add \
|
&& apk -U upgrade && apk add \
|
||||||
$BUILD_DEPS \
|
$BUILD_DEPS \
|
||||||
nodejs \
|
nodejs@edge \
|
||||||
|
nodejs-npm@edge \
|
||||||
libpq \
|
libpq \
|
||||||
libxml2 \
|
libxml2 \
|
||||||
libxslt \
|
libxslt \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
file \
|
file \
|
||||||
imagemagick \
|
imagemagick@edge \
|
||||||
&& npm install -g npm@3 && npm install -g yarn \
|
&& npm install -g npm@3 && npm install -g yarn \
|
||||||
&& bundle install --deployment --without test development \
|
&& bundle install --deployment --without test development \
|
||||||
&& yarn \
|
&& yarn --ignore-optional \
|
||||||
&& yarn cache clean \
|
&& yarn cache clean \
|
||||||
&& npm -g cache clean \
|
&& npm -g cache clean \
|
||||||
&& apk del $BUILD_DEPS \
|
&& apk del $BUILD_DEPS \
|
||||||
|
|
14
Gemfile
14
Gemfile
|
@ -1,12 +1,13 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
ruby '2.4.1'
|
ruby '>= 2.3.0', '< 2.5.0'
|
||||||
|
|
||||||
|
gem 'pkg-config'
|
||||||
|
|
||||||
gem 'rails', '~> 5.0.2'
|
gem 'rails', '~> 5.0.2'
|
||||||
gem 'sass-rails', '~> 5.0'
|
gem 'sass-rails', '~> 5.0'
|
||||||
gem 'uglifier', '>= 1.3.0'
|
gem 'uglifier', '>= 1.3.0'
|
||||||
gem 'coffee-rails', '~> 4.1.0'
|
|
||||||
gem 'jquery-rails'
|
gem 'jquery-rails'
|
||||||
gem 'puma'
|
gem 'puma'
|
||||||
|
|
||||||
|
@ -36,12 +37,13 @@ gem 'kaminari'
|
||||||
gem 'link_header'
|
gem 'link_header'
|
||||||
gem 'nokogiri'
|
gem 'nokogiri'
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
gem 'ostatus2'
|
gem 'ostatus2', '~> 1.1'
|
||||||
gem 'ox'
|
gem 'ox'
|
||||||
gem 'rabl'
|
gem 'rabl'
|
||||||
gem 'rack-attack'
|
gem 'rack-attack'
|
||||||
gem 'rack-cors', require: 'rack/cors'
|
gem 'rack-cors', require: 'rack/cors'
|
||||||
gem 'rack-timeout'
|
gem 'rack-timeout'
|
||||||
|
gem 'rails-i18n'
|
||||||
gem 'rails-settings-cached'
|
gem 'rails-settings-cached'
|
||||||
gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
|
gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
|
||||||
gem 'rqrcode'
|
gem 'rqrcode'
|
||||||
|
@ -50,9 +52,11 @@ gem 'sidekiq'
|
||||||
gem 'sidekiq-unique-jobs'
|
gem 'sidekiq-unique-jobs'
|
||||||
gem 'simple-navigation'
|
gem 'simple-navigation'
|
||||||
gem 'simple_form'
|
gem 'simple_form'
|
||||||
|
gem 'sprockets-rails', :require => 'sprockets/railtie'
|
||||||
gem 'statsd-instrument'
|
gem 'statsd-instrument'
|
||||||
gem 'twitter-text'
|
gem 'twitter-text'
|
||||||
gem 'tzinfo-data'
|
gem 'tzinfo-data'
|
||||||
|
gem 'whatlanguage'
|
||||||
|
|
||||||
gem 'react-rails'
|
gem 'react-rails'
|
||||||
gem 'browserify-rails'
|
gem 'browserify-rails'
|
||||||
|
@ -70,7 +74,9 @@ group :development, :test do
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
|
gem 'capybara'
|
||||||
gem 'faker'
|
gem 'faker'
|
||||||
|
gem 'microformats2'
|
||||||
gem 'rails-controller-testing'
|
gem 'rails-controller-testing'
|
||||||
gem 'rspec-sidekiq'
|
gem 'rspec-sidekiq'
|
||||||
gem 'simplecov', require: false
|
gem 'simplecov', require: false
|
||||||
|
@ -86,7 +92,7 @@ group :development do
|
||||||
gem 'bullet'
|
gem 'bullet'
|
||||||
gem 'active_record_query_trace'
|
gem 'active_record_query_trace'
|
||||||
|
|
||||||
gem 'capistrano'
|
gem 'capistrano', '3.8.0'
|
||||||
gem 'capistrano-rails'
|
gem 'capistrano-rails'
|
||||||
gem 'capistrano-rbenv'
|
gem 'capistrano-rbenv'
|
||||||
gem 'capistrano-yarn'
|
gem 'capistrano-yarn'
|
||||||
|
|
44
Gemfile.lock
44
Gemfile.lock
|
@ -41,7 +41,7 @@ GEM
|
||||||
tzinfo (~> 1.1)
|
tzinfo (~> 1.1)
|
||||||
addressable (2.5.1)
|
addressable (2.5.1)
|
||||||
public_suffix (~> 2.0, >= 2.0.2)
|
public_suffix (~> 2.0, >= 2.0.2)
|
||||||
airbrussh (1.1.2)
|
airbrussh (1.2.0)
|
||||||
sshkit (>= 1.6.1, != 1.7.0)
|
sshkit (>= 1.6.1, != 1.7.0)
|
||||||
arel (7.1.4)
|
arel (7.1.4)
|
||||||
ast (2.3.0)
|
ast (2.3.0)
|
||||||
|
@ -99,18 +99,18 @@ GEM
|
||||||
sshkit (~> 1.3)
|
sshkit (~> 1.3)
|
||||||
capistrano-yarn (2.0.2)
|
capistrano-yarn (2.0.2)
|
||||||
capistrano (~> 3.0)
|
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)
|
chunky_png (1.3.8)
|
||||||
climate_control (0.1.0)
|
climate_control (0.1.0)
|
||||||
cocaine (0.5.8)
|
cocaine (0.5.8)
|
||||||
climate_control (>= 0.0.3, < 1.0)
|
climate_control (>= 0.0.3, < 1.0)
|
||||||
coderay (1.1.1)
|
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)
|
colorize (0.8.1)
|
||||||
concurrent-ruby (1.0.5)
|
concurrent-ruby (1.0.5)
|
||||||
connection_pool (2.2.1)
|
connection_pool (2.2.1)
|
||||||
|
@ -233,6 +233,10 @@ GEM
|
||||||
mail (2.6.4)
|
mail (2.6.4)
|
||||||
mime-types (>= 1.16, < 4)
|
mime-types (>= 1.16, < 4)
|
||||||
method_source (0.8.2)
|
method_source (0.8.2)
|
||||||
|
microformats2 (2.1.0)
|
||||||
|
activesupport
|
||||||
|
json
|
||||||
|
nokogiri
|
||||||
mime-types (3.1)
|
mime-types (3.1)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2016.0521)
|
mime-types-data (3.2016.0521)
|
||||||
|
@ -246,11 +250,13 @@ GEM
|
||||||
nokogiri (1.7.1)
|
nokogiri (1.7.1)
|
||||||
mini_portile2 (~> 2.1.0)
|
mini_portile2 (~> 2.1.0)
|
||||||
oj (2.18.5)
|
oj (2.18.5)
|
||||||
|
openssl (2.0.3)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ostatus2 (1.0.2)
|
ostatus2 (1.1.0)
|
||||||
addressable (~> 2.4)
|
addressable (~> 2.4)
|
||||||
http (~> 2.0)
|
http (~> 2.0)
|
||||||
nokogiri (~> 1.6)
|
nokogiri (~> 1.6)
|
||||||
|
openssl (~> 2.0)
|
||||||
ox (2.4.11)
|
ox (2.4.11)
|
||||||
paperclip (5.1.0)
|
paperclip (5.1.0)
|
||||||
activemodel (>= 4.2.0)
|
activemodel (>= 4.2.0)
|
||||||
|
@ -266,6 +272,7 @@ GEM
|
||||||
pg (0.20.0)
|
pg (0.20.0)
|
||||||
pghero (1.6.4)
|
pghero (1.6.4)
|
||||||
activerecord
|
activerecord
|
||||||
|
pkg-config (1.1.7)
|
||||||
powerpack (0.1.1)
|
powerpack (0.1.1)
|
||||||
pry (0.10.4)
|
pry (0.10.4)
|
||||||
coderay (~> 1.1.0)
|
coderay (~> 1.1.0)
|
||||||
|
@ -307,6 +314,9 @@ GEM
|
||||||
nokogiri (~> 1.6)
|
nokogiri (~> 1.6)
|
||||||
rails-html-sanitizer (1.0.3)
|
rails-html-sanitizer (1.0.3)
|
||||||
loofah (~> 2.0)
|
loofah (~> 2.0)
|
||||||
|
rails-i18n (5.0.3)
|
||||||
|
i18n (~> 0.7)
|
||||||
|
railties (~> 5.0)
|
||||||
rails-settings-cached (0.6.5)
|
rails-settings-cached (0.6.5)
|
||||||
rails (>= 4.2.0)
|
rails (>= 4.2.0)
|
||||||
rails_12factor (0.0.3)
|
rails_12factor (0.0.3)
|
||||||
|
@ -438,7 +448,7 @@ GEM
|
||||||
execjs (>= 0.3.0, < 3)
|
execjs (>= 0.3.0, < 3)
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.7.2)
|
unf_ext (0.0.7.3)
|
||||||
unicode-display_width (1.1.3)
|
unicode-display_width (1.1.3)
|
||||||
uniform_notifier (1.10.0)
|
uniform_notifier (1.10.0)
|
||||||
warden (1.2.7)
|
warden (1.2.7)
|
||||||
|
@ -450,6 +460,9 @@ GEM
|
||||||
websocket-driver (0.6.5)
|
websocket-driver (0.6.5)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.2)
|
websocket-extensions (0.1.2)
|
||||||
|
whatlanguage (1.0.6)
|
||||||
|
xpath (2.0.0)
|
||||||
|
nokogiri (~> 1.3)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
@ -464,12 +477,12 @@ DEPENDENCIES
|
||||||
binding_of_caller
|
binding_of_caller
|
||||||
browserify-rails
|
browserify-rails
|
||||||
bullet
|
bullet
|
||||||
capistrano
|
capistrano (= 3.8.0)
|
||||||
capistrano-faster-assets (~> 1.0)
|
capistrano-faster-assets (~> 1.0)
|
||||||
capistrano-rails
|
capistrano-rails
|
||||||
capistrano-rbenv
|
capistrano-rbenv
|
||||||
capistrano-yarn
|
capistrano-yarn
|
||||||
coffee-rails (~> 4.1.0)
|
capybara
|
||||||
devise
|
devise
|
||||||
devise-two-factor
|
devise-two-factor
|
||||||
doorkeeper
|
doorkeeper
|
||||||
|
@ -493,14 +506,16 @@ DEPENDENCIES
|
||||||
letter_opener_web
|
letter_opener_web
|
||||||
link_header
|
link_header
|
||||||
lograge
|
lograge
|
||||||
|
microformats2
|
||||||
nokogiri
|
nokogiri
|
||||||
oj
|
oj
|
||||||
ostatus2
|
ostatus2 (~> 1.1)
|
||||||
ox
|
ox
|
||||||
paperclip (~> 5.1)
|
paperclip (~> 5.1)
|
||||||
paperclip-av-transcoder
|
paperclip-av-transcoder
|
||||||
pg
|
pg
|
||||||
pghero
|
pghero
|
||||||
|
pkg-config
|
||||||
pry-rails
|
pry-rails
|
||||||
puma
|
puma
|
||||||
rabl
|
rabl
|
||||||
|
@ -509,6 +524,7 @@ DEPENDENCIES
|
||||||
rack-timeout
|
rack-timeout
|
||||||
rails (~> 5.0.2)
|
rails (~> 5.0.2)
|
||||||
rails-controller-testing
|
rails-controller-testing
|
||||||
|
rails-i18n
|
||||||
rails-settings-cached
|
rails-settings-cached
|
||||||
rails_12factor
|
rails_12factor
|
||||||
react-rails
|
react-rails
|
||||||
|
@ -527,11 +543,13 @@ DEPENDENCIES
|
||||||
simple_form
|
simple_form
|
||||||
simplecov
|
simplecov
|
||||||
skylight
|
skylight
|
||||||
|
sprockets-rails
|
||||||
statsd-instrument
|
statsd-instrument
|
||||||
twitter-text
|
twitter-text
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
uglifier (>= 1.3.0)
|
uglifier (>= 1.3.0)
|
||||||
webmock
|
webmock
|
||||||
|
whatlanguage
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 2.4.1p111
|
ruby 2.4.1p111
|
||||||
|
|
|
@ -3,3 +3,4 @@
|
||||||
* * * *
|
* * * *
|
||||||
|
|
||||||
- [ ] I searched or browsed the repo’s other issues to ensure this is not a duplicate.
|
- [ ] 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,6 +48,14 @@ If you would like, you can [support the development of this project on Patreon][
|
||||||
- **Deployable via Docker**
|
- **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
|
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
|
## 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
|
- `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
|
||||||
|
@ -72,10 +80,6 @@ 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,
|
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.
|
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:
|
Then, you need to fill in the `.env.production` file:
|
||||||
|
|
||||||
cp .env.production.sample .env.production
|
cp .env.production.sample .env.production
|
||||||
|
@ -85,6 +89,11 @@ 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:
|
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
|
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.
|
Do this once for each of those keys, and copy the result into the `.env.production` file in the appropriate field.
|
||||||
|
|
|
@ -43,7 +43,7 @@ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
|
||||||
|
|
||||||
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
|
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
|
||||||
|
|
||||||
export PATH="$HOME/.rbenv/bin::$PATH"
|
export PATH="$HOME/.rbenv/bin:$PATH"
|
||||||
eval "$(rbenv init -)"
|
eval "$(rbenv init -)"
|
||||||
|
|
||||||
cd /vagrant
|
cd /vagrant
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
|
@ -0,0 +1,82 @@
|
||||||
|
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
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,14 @@
|
||||||
|
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,7 +10,8 @@ const messages = defineMessages({
|
||||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
||||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock' }
|
unblock: { id: 'account.unblock', defaultMessage: 'Unblock' },
|
||||||
|
unmute: { id: 'account.unmute', defaultMessage: 'Unmute' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttonsStyle = {
|
const buttonsStyle = {
|
||||||
|
@ -25,6 +26,7 @@ const Account = React.createClass({
|
||||||
me: React.PropTypes.number.isRequired,
|
me: React.PropTypes.number.isRequired,
|
||||||
onFollow: React.PropTypes.func.isRequired,
|
onFollow: React.PropTypes.func.isRequired,
|
||||||
onBlock: React.PropTypes.func.isRequired,
|
onBlock: React.PropTypes.func.isRequired,
|
||||||
|
onMute: React.PropTypes.func.isRequired,
|
||||||
intl: React.PropTypes.object.isRequired
|
intl: React.PropTypes.object.isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -38,6 +40,10 @@ const Account = React.createClass({
|
||||||
this.props.onBlock(this.props.account);
|
this.props.onBlock(this.props.account);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleMute () {
|
||||||
|
this.props.onMute(this.props.account);
|
||||||
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, me, intl } = this.props;
|
const { account, me, intl } = this.props;
|
||||||
|
|
||||||
|
@ -51,11 +57,14 @@ const Account = React.createClass({
|
||||||
const following = account.getIn(['relationship', 'following']);
|
const following = account.getIn(['relationship', 'following']);
|
||||||
const requested = account.getIn(['relationship', 'requested']);
|
const requested = account.getIn(['relationship', 'requested']);
|
||||||
const blocking = account.getIn(['relationship', 'blocking']);
|
const blocking = account.getIn(['relationship', 'blocking']);
|
||||||
|
const muting = account.getIn(['relationship', 'muting']);
|
||||||
|
|
||||||
if (requested) {
|
if (requested) {
|
||||||
buttons = <IconButton disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} />
|
buttons = <IconButton disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} />
|
||||||
} else if (blocking) {
|
} else if (blocking) {
|
||||||
buttons = <IconButton active={true} icon='unlock-alt' title={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
|
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} />;
|
||||||
} else {
|
} else {
|
||||||
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -178,7 +178,12 @@ const AutosuggestTextarea = React.createClass({
|
||||||
|
|
||||||
<div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'>
|
<div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'>
|
||||||
{suggestions.map((suggestion, i) => (
|
{suggestions.map((suggestion, i) => (
|
||||||
<div key={suggestion} className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`} onClick={this.onSuggestionClick.bind(this, suggestion)}>
|
<div
|
||||||
|
role='button'
|
||||||
|
tabIndex='0'
|
||||||
|
key={suggestion}
|
||||||
|
className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`}
|
||||||
|
onClick={this.onSuggestionClick.bind(this, suggestion)}>
|
||||||
<AutosuggestAccountContainer id={suggestion} />
|
<AutosuggestAccountContainer id={suggestion} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -9,6 +9,7 @@ const Button = React.createClass({
|
||||||
block: React.PropTypes.bool,
|
block: React.PropTypes.bool,
|
||||||
secondary: React.PropTypes.bool,
|
secondary: React.PropTypes.bool,
|
||||||
size: React.PropTypes.number,
|
size: React.PropTypes.number,
|
||||||
|
style: React.PropTypes.object,
|
||||||
children: React.PropTypes.node
|
children: React.PropTypes.node
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -15,13 +15,13 @@ const ColumnBackButton = React.createClass({
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
handleClick () {
|
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();
|
else this.context.router.goBack();
|
||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div onClick={this.handleClick} className='column-back-button'>
|
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button'>
|
||||||
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
||||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -31,7 +31,7 @@ const ColumnBackButtonSlim = React.createClass({
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<div style={outerStyle} onClick={this.handleClick} className='column-back-button'>
|
<div role='button' tabIndex='0' style={outerStyle} onClick={this.handleClick} className='column-back-button'>
|
||||||
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
||||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -46,7 +46,9 @@ const ColumnCollapsable = React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<div title={`${title}`} style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div>
|
<div role='button' tabIndex='0' title={`${title}`} style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}>
|
||||||
|
<i className={`fa fa-${icon}`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}>
|
<Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}>
|
||||||
{({ opacity, height }) =>
|
{({ opacity, height }) =>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const LoadMore = ({ onClick }) => (
|
const LoadMore = ({ onClick }) => (
|
||||||
<a href='#' className='load-more' onClick={onClick}>
|
<a href="#" className='load-more' role='button' onClick={onClick}>
|
||||||
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
|
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
|
@ -220,7 +220,7 @@ const MediaGallery = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
children = (
|
children = (
|
||||||
<div style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
|
<div role='button' tabIndex='0' style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
|
||||||
<span style={spoilerSpanStyle}>{warning}</span>
|
<span style={spoilerSpanStyle}>{warning}</span>
|
||||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,8 @@ const Permalink = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
href: React.PropTypes.string.isRequired,
|
href: React.PropTypes.string.isRequired,
|
||||||
to: React.PropTypes.string.isRequired
|
to: React.PropTypes.string.isRequired,
|
||||||
|
children: React.PropTypes.node
|
||||||
},
|
},
|
||||||
|
|
||||||
handleClick (e) {
|
handleClick (e) {
|
||||||
|
|
|
@ -92,10 +92,14 @@ const StatusActionBar = React.createClass({
|
||||||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let reblogIcon = 'retweet';
|
||||||
|
if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
|
||||||
|
else if (status.get('visibility') === 'private') reblogIcon = 'lock';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
|
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
|
||||||
<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 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={status.get('visibility') === 'direct' ? 'envelope' : (status.get('visibility') === 'private' ? 'lock' : 'retweet')} onClick={this.handleReblogClick} /></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 animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></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' }}>
|
<div style={{ width: '18px', height: '18px', float: 'left' }}>
|
||||||
|
|
|
@ -44,6 +44,7 @@ const StatusContent = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
link.setAttribute('target', '_blank');
|
link.setAttribute('target', '_blank');
|
||||||
link.setAttribute('rel', 'noopener');
|
link.setAttribute('rel', 'noopener');
|
||||||
|
link.setAttribute('title', link.href);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -118,7 +119,7 @@ const StatusContent = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className='status__content' style={{ cursor: 'pointer' }} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
<div className='status__content' style={{ cursor: 'pointer' }} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
||||||
<p style={{ marginBottom: hidden && status.get('mentions').size === 0 ? '0px' : '' }} >
|
<p style={{ marginBottom: hidden && status.get('mentions').size === 0 ? '0px' : '' }} >
|
||||||
<span dangerouslySetInnerHTML={spoilerContent} /> <a className='status__content__spoiler-link' onClick={this.handleSpoilerClick}>{toggleText}</a>
|
<span dangerouslySetInnerHTML={spoilerContent} /> <a tabIndex='0' className='status__content__spoiler-link' role='button' onClick={this.handleSpoilerClick}>{toggleText}</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{mentionsPlaceholder}
|
{mentionsPlaceholder}
|
||||||
|
|
|
@ -7,7 +7,8 @@ import { isIOS } from '../is_mobile';
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' },
|
toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' },
|
||||||
toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' },
|
toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' },
|
||||||
expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' }
|
expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' },
|
||||||
|
expand_video: { id: 'video_player.video_error', defaultMessage: 'Video could not be played' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const videoStyle = {
|
const videoStyle = {
|
||||||
|
@ -30,7 +31,7 @@ const muteStyle = {
|
||||||
zIndex: '5'
|
zIndex: '5'
|
||||||
};
|
};
|
||||||
|
|
||||||
const spoilerStyle = {
|
const coverStyle = {
|
||||||
marginTop: '8px',
|
marginTop: '8px',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
@ -94,7 +95,8 @@ const VideoPlayer = React.createClass({
|
||||||
visible: !this.props.sensitive,
|
visible: !this.props.sensitive,
|
||||||
preview: true,
|
preview: true,
|
||||||
muted: true,
|
muted: true,
|
||||||
hasAudio: true
|
hasAudio: true,
|
||||||
|
videoError: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -142,12 +144,17 @@ const VideoPlayer = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleVideoError () {
|
||||||
|
this.setState({ videoError: true });
|
||||||
|
},
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
if (!this.video) {
|
if (!this.video) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.video.addEventListener('loadeddata', this.handleLoadedData);
|
this.video.addEventListener('loadeddata', this.handleLoadedData);
|
||||||
|
this.video.addEventListener('error', this.handleVideoError);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate () {
|
componentDidUpdate () {
|
||||||
|
@ -156,6 +163,7 @@ const VideoPlayer = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
this.video.addEventListener('loadeddata', this.handleLoadedData);
|
this.video.addEventListener('loadeddata', this.handleLoadedData);
|
||||||
|
this.video.addEventListener('error', this.handleVideoError);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
|
@ -164,6 +172,7 @@ const VideoPlayer = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
this.video.removeEventListener('loadeddata', this.handleLoadedData);
|
this.video.removeEventListener('loadeddata', this.handleLoadedData);
|
||||||
|
this.video.removeEventListener('error', this.handleVideoError);
|
||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
@ -194,7 +203,7 @@ const VideoPlayer = React.createClass({
|
||||||
if (!this.state.visible) {
|
if (!this.state.visible) {
|
||||||
if (sensitive) {
|
if (sensitive) {
|
||||||
return (
|
return (
|
||||||
<div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
<div role='button' tabIndex='0' style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||||
{spoilerButton}
|
{spoilerButton}
|
||||||
<span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
|
<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>
|
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||||
|
@ -202,7 +211,7 @@ const VideoPlayer = React.createClass({
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
<div role='button' tabIndex='0' style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||||
{spoilerButton}
|
{spoilerButton}
|
||||||
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
|
<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>
|
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||||
|
@ -213,19 +222,27 @@ const VideoPlayer = React.createClass({
|
||||||
|
|
||||||
if (this.state.preview && !autoplay) {
|
if (this.state.preview && !autoplay) {
|
||||||
return (
|
return (
|
||||||
<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}>
|
<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}>
|
||||||
{spoilerButton}
|
{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 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>
|
</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 (
|
return (
|
||||||
<div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
|
<div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
|
||||||
{spoilerButton}
|
{spoilerButton}
|
||||||
{muteButton}
|
{muteButton}
|
||||||
{expandButton}
|
{expandButton}
|
||||||
<video ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
|
<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} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
connectTimeline,
|
connectTimeline,
|
||||||
disconnectTimeline
|
disconnectTimeline
|
||||||
} from '../actions/timelines';
|
} from '../actions/timelines';
|
||||||
|
import { showOnboardingOnce } from '../actions/onboarding';
|
||||||
import { updateNotifications, refreshNotifications } from '../actions/notifications';
|
import { updateNotifications, refreshNotifications } from '../actions/notifications';
|
||||||
import createBrowserHistory from 'history/lib/createBrowserHistory';
|
import createBrowserHistory from 'history/lib/createBrowserHistory';
|
||||||
import {
|
import {
|
||||||
|
@ -37,6 +38,7 @@ import FollowRequests from '../features/follow_requests';
|
||||||
import GenericNotFound from '../features/generic_not_found';
|
import GenericNotFound from '../features/generic_not_found';
|
||||||
import FavouritedStatuses from '../features/favourited_statuses';
|
import FavouritedStatuses from '../features/favourited_statuses';
|
||||||
import Blocks from '../features/blocks';
|
import Blocks from '../features/blocks';
|
||||||
|
import Mutes from '../features/mutes';
|
||||||
import Report from '../features/report';
|
import Report from '../features/report';
|
||||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||||
import en from 'react-intl/locale-data/en';
|
import en from 'react-intl/locale-data/en';
|
||||||
|
@ -60,8 +62,8 @@ import { hydrateStore } from '../actions/store';
|
||||||
import createStream from '../stream';
|
import createStream from '../stream';
|
||||||
|
|
||||||
const store = configureStore();
|
const store = configureStore();
|
||||||
|
const initialState = JSON.parse(document.getElementById("initial-state").textContent);
|
||||||
store.dispatch(hydrateStore(window.INITIAL_STATE));
|
store.dispatch(hydrateStore(initialState));
|
||||||
|
|
||||||
const browserHistory = useRouterHistory(createBrowserHistory)({
|
const browserHistory = useRouterHistory(createBrowserHistory)({
|
||||||
basename: '/web'
|
basename: '/web'
|
||||||
|
@ -94,9 +96,10 @@ const Mastodon = React.createClass({
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { locale } = this.props;
|
const { locale } = this.props;
|
||||||
|
const streamingAPIBaseURL = store.getState().getIn(['meta', 'streaming_api_base_url']);
|
||||||
const accessToken = store.getState().getIn(['meta', 'access_token']);
|
const accessToken = store.getState().getIn(['meta', 'access_token']);
|
||||||
|
|
||||||
this.subscription = createStream(accessToken, 'user', {
|
this.subscription = createStream(streamingAPIBaseURL, accessToken, 'user', {
|
||||||
|
|
||||||
connected () {
|
connected () {
|
||||||
store.dispatch(connectTimeline('home'));
|
store.dispatch(connectTimeline('home'));
|
||||||
|
@ -132,6 +135,8 @@ const Mastodon = React.createClass({
|
||||||
if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
|
if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
|
||||||
Notification.requestPermission();
|
Notification.requestPermission();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
store.dispatch(showOnboardingOnce());
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
|
@ -171,6 +176,7 @@ const Mastodon = React.createClass({
|
||||||
|
|
||||||
<Route path='follow_requests' component={FollowRequests} />
|
<Route path='follow_requests' component={FollowRequests} />
|
||||||
<Route path='blocks' component={Blocks} />
|
<Route path='blocks' component={Blocks} />
|
||||||
|
<Route path='mutes' component={Mutes} />
|
||||||
<Route path='report' component={Report} />
|
<Route path='report' component={Report} />
|
||||||
|
|
||||||
<Route path='*' component={GenericNotFound} />
|
<Route path='*' component={GenericNotFound} />
|
||||||
|
|
|
@ -43,7 +43,16 @@ const Avatar = React.createClass({
|
||||||
return (
|
return (
|
||||||
<Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
|
<Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
|
||||||
{({ radius }) =>
|
{({ 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}>
|
<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}>
|
||||||
<img src={account.get('avatar')} alt={account.get('acct')} style={{ display: 'block', width: '90px', height: '90px' }} />
|
<img src={account.get('avatar')} alt={account.get('acct')} style={{ display: 'block', width: '90px', height: '90px' }} />
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0,
|
hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0,
|
||||||
|
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
||||||
accessToken: state.getIn(['meta', 'access_token'])
|
accessToken: state.getIn(['meta', 'access_token'])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@ const CommunityTimeline = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
dispatch: React.PropTypes.func.isRequired,
|
dispatch: React.PropTypes.func.isRequired,
|
||||||
intl: React.PropTypes.object.isRequired,
|
intl: React.PropTypes.object.isRequired,
|
||||||
|
streamingAPIBaseURL: React.PropTypes.string.isRequired,
|
||||||
accessToken: React.PropTypes.string.isRequired,
|
accessToken: React.PropTypes.string.isRequired,
|
||||||
hasUnread: React.PropTypes.bool
|
hasUnread: React.PropTypes.bool
|
||||||
},
|
},
|
||||||
|
@ -36,7 +38,7 @@ const CommunityTimeline = React.createClass({
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch, accessToken } = this.props;
|
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
|
||||||
|
|
||||||
dispatch(refreshTimeline('community'));
|
dispatch(refreshTimeline('community'));
|
||||||
|
|
||||||
|
@ -44,7 +46,7 @@ const CommunityTimeline = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription = createStream(accessToken, 'public:local', {
|
subscription = createStream(streamingAPIBaseURL, accessToken, 'public:local', {
|
||||||
|
|
||||||
connected () {
|
connected () {
|
||||||
dispatch(connectTimeline('community'));
|
dispatch(connectTimeline('community'));
|
||||||
|
|
|
@ -92,7 +92,7 @@ const ComposeForm = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
// This statement does several things:
|
// This statement does several things:
|
||||||
// - If we're beginning a reply, and,
|
// - If we're beginning a reply, and,
|
||||||
// - Replying to zero or one users, places the cursor at the end of the textbox.
|
// - 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;
|
// - 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__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'>
|
<div className='privacy-dropdown__dropdown'>
|
||||||
{options.map(item =>
|
{options.map(item =>
|
||||||
<div key={item.value} onClick={this.handleClick.bind(this, item.value)} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
|
<div role='button' tabIndex='0' 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__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div>
|
||||||
<div className='privacy-dropdown__option__content'>
|
<div className='privacy-dropdown__option__content'>
|
||||||
<strong>{item.shortText}</strong>
|
<strong>{item.shortText}</strong>
|
||||||
|
|
|
@ -36,6 +36,10 @@ const Search = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
noop () {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
handleFocus () {
|
handleFocus () {
|
||||||
this.props.onShow();
|
this.props.onShow();
|
||||||
},
|
},
|
||||||
|
@ -56,9 +60,9 @@ const Search = React.createClass({
|
||||||
onFocus={this.handleFocus}
|
onFocus={this.handleFocus}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='search__icon'>
|
<div role='button' tabIndex='0' className='search__icon' onClick={hasValue ? this.handleClear : this.noop}>
|
||||||
<i className={`fa fa-search ${hasValue ? '' : 'active'}`} />
|
<i className={`fa fa-search ${hasValue ? '' : 'active'}`} />
|
||||||
<i className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} onClick={this.handleClear} />
|
<i aria-label="Clear search" className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -50,7 +50,7 @@ const Followers = React.createClass({
|
||||||
|
|
||||||
handleLoadMore (e) {
|
handleLoadMore (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.dispatch(expandFollowing(Number(this.props.params.accountId)));
|
this.props.dispatch(expandFollowers(Number(this.props.params.accountId)));
|
||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
|
|
@ -14,6 +14,7 @@ const messages = defineMessages({
|
||||||
sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
||||||
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||||
|
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||||
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }
|
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,6 +38,7 @@ const GettingStarted = ({ intl, me }) => {
|
||||||
<ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />
|
<ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />
|
||||||
{followRequests}
|
{followRequests}
|
||||||
<ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
|
<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='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' />
|
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,6 +13,7 @@ import createStream from '../../stream';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
hasUnread: state.getIn(['timelines', 'tag', 'unread']) > 0,
|
hasUnread: state.getIn(['timelines', 'tag', 'unread']) > 0,
|
||||||
|
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
||||||
accessToken: state.getIn(['meta', 'access_token'])
|
accessToken: state.getIn(['meta', 'access_token'])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -21,6 +22,7 @@ const HashtagTimeline = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
params: React.PropTypes.object.isRequired,
|
params: React.PropTypes.object.isRequired,
|
||||||
dispatch: React.PropTypes.func.isRequired,
|
dispatch: React.PropTypes.func.isRequired,
|
||||||
|
streamingAPIBaseURL: React.PropTypes.string.isRequired,
|
||||||
accessToken: React.PropTypes.string.isRequired,
|
accessToken: React.PropTypes.string.isRequired,
|
||||||
hasUnread: React.PropTypes.bool
|
hasUnread: React.PropTypes.bool
|
||||||
},
|
},
|
||||||
|
@ -28,9 +30,9 @@ const HashtagTimeline = React.createClass({
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
_subscribe (dispatch, id) {
|
_subscribe (dispatch, id) {
|
||||||
const { accessToken } = this.props;
|
const { streamingAPIBaseURL, accessToken } = this.props;
|
||||||
|
|
||||||
this.subscription = createStream(accessToken, `hashtag&tag=${id}`, {
|
this.subscription = createStream(streamingAPIBaseURL, accessToken, `hashtag&tag=${id}`, {
|
||||||
|
|
||||||
received (data) {
|
received (data) {
|
||||||
switch(data.event) {
|
switch(data.event) {
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
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;
|
const { intl } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div title={intl.formatMessage(messages.clear)} className='column-icon column-icon-clear' tabIndex='0' onClick={this.props.onClick}>
|
<div role='button' title={intl.formatMessage(messages.clear)} className='column-icon column-icon-clear' tabIndex='0' onClick={this.props.onClick}>
|
||||||
<i className='fa fa-eraser' />
|
<i className='fa fa-eraser' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -27,9 +27,11 @@ const ColumnSettings = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
settings: ImmutablePropTypes.map.isRequired,
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
intl: React.PropTypes.object.isRequired,
|
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: React.PropTypes.func.isRequired,
|
||||||
onSave: React.PropTypes.func.isRequired,
|
onSave: React.PropTypes.func.isRequired,
|
||||||
|
intl: React.PropTypes.shape({
|
||||||
|
formatMessage: React.PropTypes.func.isRequired
|
||||||
|
}).isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
|
|
@ -71,7 +71,7 @@ const Notification = React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () { // eslint-disable-line consistent-return
|
||||||
const { notification } = this.props;
|
const { notification } = this.props;
|
||||||
const account = notification.get('account');
|
const account = notification.get('account');
|
||||||
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
|
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
|
||||||
|
|
|
@ -14,8 +14,8 @@ const labelSpanStyle = {
|
||||||
marginLeft: '8px'
|
marginLeft: '8px'
|
||||||
};
|
};
|
||||||
|
|
||||||
const SettingToggle = ({ settings, settingKey, label, onChange }) => (
|
const SettingToggle = ({ settings, settingKey, label, onChange, htmlFor = '' }) => (
|
||||||
<label style={labelStyle}>
|
<label htmlFor={htmlFor} style={labelStyle}>
|
||||||
<Toggle checked={settings.getIn(settingKey)} onChange={(e) => onChange(settingKey, e.target.checked)} />
|
<Toggle checked={settings.getIn(settingKey)} onChange={(e) => onChange(settingKey, e.target.checked)} />
|
||||||
<span className='setting-toggle' style={labelSpanStyle}>{label}</span>
|
<span className='setting-toggle' style={labelSpanStyle}>{label}</span>
|
||||||
</label>
|
</label>
|
||||||
|
@ -25,7 +25,8 @@ SettingToggle.propTypes = {
|
||||||
settings: ImmutablePropTypes.map.isRequired,
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
settingKey: React.PropTypes.array.isRequired,
|
settingKey: React.PropTypes.array.isRequired,
|
||||||
label: React.PropTypes.node.isRequired,
|
label: React.PropTypes.node.isRequired,
|
||||||
onChange: React.PropTypes.func.isRequired
|
onChange: React.PropTypes.func.isRequired,
|
||||||
|
htmlFor: React.PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SettingToggle;
|
export default SettingToggle;
|
||||||
|
|
|
@ -19,6 +19,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0,
|
hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0,
|
||||||
|
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
||||||
accessToken: state.getIn(['meta', 'access_token'])
|
accessToken: state.getIn(['meta', 'access_token'])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@ const PublicTimeline = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
dispatch: React.PropTypes.func.isRequired,
|
dispatch: React.PropTypes.func.isRequired,
|
||||||
intl: React.PropTypes.object.isRequired,
|
intl: React.PropTypes.object.isRequired,
|
||||||
|
streamingAPIBaseURL: React.PropTypes.string.isRequired,
|
||||||
accessToken: React.PropTypes.string.isRequired,
|
accessToken: React.PropTypes.string.isRequired,
|
||||||
hasUnread: React.PropTypes.bool
|
hasUnread: React.PropTypes.bool
|
||||||
},
|
},
|
||||||
|
@ -36,7 +38,7 @@ const PublicTimeline = React.createClass({
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { dispatch, accessToken } = this.props;
|
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
|
||||||
|
|
||||||
dispatch(refreshTimeline('public'));
|
dispatch(refreshTimeline('public'));
|
||||||
|
|
||||||
|
@ -44,7 +46,7 @@ const PublicTimeline = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription = createStream(accessToken, 'public', {
|
subscription = createStream(streamingAPIBaseURL, accessToken, 'public', {
|
||||||
|
|
||||||
connected () {
|
connected () {
|
||||||
dispatch(connectTimeline('public'));
|
dispatch(connectTimeline('public'));
|
||||||
|
|
|
@ -71,10 +71,14 @@ const ActionBar = React.createClass({
|
||||||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let reblogIcon = 'retweet';
|
||||||
|
if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
|
||||||
|
else if (status.get('visibility') === 'private') reblogIcon = 'lock';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='detailed-status__action-bar'>
|
<div className='detailed-status__action-bar'>
|
||||||
<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 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={status.get('visibility') === 'direct' ? 'envelope' : (status.get('visibility') === 'private' ? 'lock' : 'retweet')} onClick={this.handleReblogClick} /></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 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' }}><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 style={{ flex: '1 1 auto', textAlign: 'center' }}><DropdownMenu size={18} icon='ellipsis-h' items={menu} direction="left" /></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -64,7 +64,7 @@ const Column = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='column' onWheel={this.handleWheel}>
|
<div role='section' className='column' onWheel={this.handleWheel}>
|
||||||
{header}
|
{header}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,7 +25,7 @@ const ColumnHeader = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick}>
|
<div role='button' tabIndex='0' aria-label={type} className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick}>
|
||||||
{icon}
|
{icon}
|
||||||
{type}
|
{type}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -34,7 +34,8 @@ ColumnLink.propTypes = {
|
||||||
icon: React.PropTypes.string.isRequired,
|
icon: React.PropTypes.string.isRequired,
|
||||||
text: React.PropTypes.string.isRequired,
|
text: React.PropTypes.string.isRequired,
|
||||||
to: React.PropTypes.string,
|
to: React.PropTypes.string,
|
||||||
href: React.PropTypes.string
|
href: React.PropTypes.string,
|
||||||
|
method: React.PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ColumnLink;
|
export default ColumnLink;
|
||||||
|
|
|
@ -104,8 +104,8 @@ const MediaModal = React.createClass({
|
||||||
leftNav = rightNav = content = '';
|
leftNav = rightNav = content = '';
|
||||||
|
|
||||||
if (media.size > 1) {
|
if (media.size > 1) {
|
||||||
leftNav = <div style={leftNavStyle} className='modal-container__nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
|
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 style={rightNavStyle} className='modal-container__nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachment.get('type') === 'image') {
|
if (attachment.get('type') === 'image') {
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
import MediaModal from './media_modal';
|
import MediaModal from './media_modal';
|
||||||
|
import OnboardingModal from './onboarding_modal';
|
||||||
import VideoModal from './video_modal';
|
import VideoModal from './video_modal';
|
||||||
import BoostModal from './boost_modal';
|
import BoostModal from './boost_modal';
|
||||||
import { TransitionMotion, spring } from 'react-motion';
|
import { TransitionMotion, spring } from 'react-motion';
|
||||||
|
|
||||||
const MODAL_COMPONENTS = {
|
const MODAL_COMPONENTS = {
|
||||||
'MEDIA': MediaModal,
|
'MEDIA': MediaModal,
|
||||||
|
'ONBOARDING': OnboardingModal,
|
||||||
'VIDEO': VideoModal,
|
'VIDEO': VideoModal,
|
||||||
'BOOST': BoostModal
|
'BOOST': BoostModal
|
||||||
};
|
};
|
||||||
|
@ -66,7 +68,7 @@ const ModalRoot = React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key}>
|
<div key={key}>
|
||||||
<div className='modal-root__overlay' style={{ opacity: style.opacity, transform: `translateZ(0px)` }} onClick={onClose} />
|
<div role='presentation' 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})` }}>
|
<div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
|
||||||
<SpecificComponent {...props} onClose={onClose} />
|
<SpecificComponent {...props} onClose={onClose} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,251 @@
|
||||||
|
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,8 +24,10 @@ const makeGetStatusIds = () => createSelector([
|
||||||
|
|
||||||
if (columnSettings.getIn(['regex', 'body'], '').trim().length > 0) {
|
if (columnSettings.getIn(['regex', 'body'], '').trim().length > 0) {
|
||||||
try {
|
try {
|
||||||
const regex = new RegExp(columnSettings.getIn(['regex', 'body']).trim(), 'i');
|
if (showStatus) {
|
||||||
showStatus = showStatus && !regex.test(statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'content']) : statusForId.get('content'));
|
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'));
|
||||||
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
// Bad regex, don't affect filters
|
// Bad regex, don't affect filters
|
||||||
}
|
}
|
||||||
|
|
|
@ -141,7 +141,7 @@ const UI = React.createClass({
|
||||||
{mountedColumns}
|
{mountedColumns}
|
||||||
|
|
||||||
<NotificationsContainer />
|
<NotificationsContainer />
|
||||||
<LoadingBarContainer style={{ backgroundColor: '#2b90d9', left: '0', top: '0' }} />
|
<LoadingBarContainer className="loading-bar" />
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<UploadArea active={draggingOver} />
|
<UploadArea active={draggingOver} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,7 +14,7 @@ Link.parseAttrs = (link, parts) => {
|
||||||
link = Link.parseParams(link, uriAttrs[1])
|
link = Link.parseParams(link, uriAttrs[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
while(match = Link.attrPattern.exec(attrs)) {
|
while(match = Link.attrPattern.exec(attrs)) { // eslint-disable-line no-cond-assign
|
||||||
attr = match[1].toLowerCase()
|
attr = match[1].toLowerCase()
|
||||||
value = match[4] || match[3] || match[2]
|
value = match[4] || match[3] || match[2]
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ const en = {
|
||||||
"column.favourites": "Favourites",
|
"column.favourites": "Favourites",
|
||||||
"column.follow_requests": "Follow requests",
|
"column.follow_requests": "Follow requests",
|
||||||
"column.home": "Home",
|
"column.home": "Home",
|
||||||
|
"column.mutes": "Muted users",
|
||||||
"column.notifications": "Notifications",
|
"column.notifications": "Notifications",
|
||||||
"column.public": "Federated timeline",
|
"column.public": "Federated timeline",
|
||||||
"compose_form.placeholder": "What is on your mind?",
|
"compose_form.placeholder": "What is on your mind?",
|
||||||
|
@ -68,10 +69,12 @@ const en = {
|
||||||
"navigation_bar.follow_requests": "Follow requests",
|
"navigation_bar.follow_requests": "Follow requests",
|
||||||
"navigation_bar.info": "Extended information",
|
"navigation_bar.info": "Extended information",
|
||||||
"navigation_bar.logout": "Logout",
|
"navigation_bar.logout": "Logout",
|
||||||
|
"navigation_bar.mutes": "Muted users",
|
||||||
"navigation_bar.preferences": "Preferences",
|
"navigation_bar.preferences": "Preferences",
|
||||||
"navigation_bar.public_timeline": "Federated timeline",
|
"navigation_bar.public_timeline": "Federated timeline",
|
||||||
"notification.favourite": "{name} favourited your status",
|
"notification.favourite": "{name} favourited your status",
|
||||||
"notification.follow": "{name} followed you",
|
"notification.follow": "{name} followed you",
|
||||||
|
"notification.mention": "{name} mentioned you",
|
||||||
"notification.reblog": "{name} boosted your status",
|
"notification.reblog": "{name} boosted your status",
|
||||||
"notifications.clear_confirmation": "Are you sure you want to clear all your notifications?",
|
"notifications.clear_confirmation": "Are you sure you want to clear all your notifications?",
|
||||||
"notifications.clear": "Clear notifications",
|
"notifications.clear": "Clear notifications",
|
||||||
|
@ -126,6 +129,7 @@ const en = {
|
||||||
"video_player.toggle_sound": "Toggle sound",
|
"video_player.toggle_sound": "Toggle sound",
|
||||||
"video_player.toggle_visible": "Toggle visibility",
|
"video_player.toggle_visible": "Toggle visibility",
|
||||||
"video_player.expand": "Expand video",
|
"video_player.expand": "Expand video",
|
||||||
|
"video_player.video_error": "Video could not be played",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default en;
|
export default en;
|
||||||
|
|
|
@ -73,7 +73,7 @@ const es = {
|
||||||
"notifications.column_settings.mention": "Menciones:",
|
"notifications.column_settings.mention": "Menciones:",
|
||||||
"notifications.column_settings.reblog": "Retoots:",
|
"notifications.column_settings.reblog": "Retoots:",
|
||||||
"emoji_button.label": "Insertar emoji",
|
"emoji_button.label": "Insertar emoji",
|
||||||
"privacy.public.short": "Público",
|
"privacy.public.short": "Público",
|
||||||
"privacy.public.long": "Mostrar en la historia federada",
|
"privacy.public.long": "Mostrar en la historia federada",
|
||||||
"privacy.unlisted.short": "Sin federar",
|
"privacy.unlisted.short": "Sin federar",
|
||||||
"privacy.unlisted.long": "No mostrar en la historia federada",
|
"privacy.unlisted.long": "No mostrar en la historia federada",
|
||||||
|
|
|
@ -75,6 +75,7 @@ const fr = {
|
||||||
"navigation_bar.favourites": "Favoris",
|
"navigation_bar.favourites": "Favoris",
|
||||||
"navigation_bar.info": "Plus d'informations",
|
"navigation_bar.info": "Plus d'informations",
|
||||||
"navigation_bar.logout": "Déconnexion",
|
"navigation_bar.logout": "Déconnexion",
|
||||||
|
"navigation_bar.mutes": "Utilisateurs muets",
|
||||||
"navigation_bar.follow_requests": "Demandes de suivi",
|
"navigation_bar.follow_requests": "Demandes de suivi",
|
||||||
"reply_indicator.cancel": "Annuler",
|
"reply_indicator.cancel": "Annuler",
|
||||||
"search.placeholder": "Rechercher",
|
"search.placeholder": "Rechercher",
|
||||||
|
|
|
@ -1,119 +1,125 @@
|
||||||
const ja = {
|
const ja = {
|
||||||
"column_back_button.label": "戻る",
|
"account.block": "@{name} さんをブロック",
|
||||||
"lightbox.close": "閉じる",
|
"account.disclaimer": "このユーザーは他のインスタンスに所属しているため、数字が正確で無い場合があります。",
|
||||||
"loading_indicator.label": "読み込み中...",
|
|
||||||
"status.mention": "@{name} さんへの返信",
|
|
||||||
"status.delete": "削除",
|
|
||||||
"status.reply": "返信",
|
|
||||||
"status.reblog": "ブースト",
|
|
||||||
"status.favourite": "お気に入り",
|
|
||||||
"status.reblogged_by": "{name} さんにブーストされました",
|
|
||||||
"status.sensitive_warning": "不適切なコンテンツ",
|
|
||||||
"status.sensitive_toggle": "クリックして表示",
|
|
||||||
"status.show_more": "もっと見る",
|
|
||||||
"status.load_more": "もっと見る",
|
|
||||||
"status.show_less": "隠す",
|
|
||||||
"status.open": "Expand this status",
|
|
||||||
"status.report": "@{name} さんを通報",
|
|
||||||
"status.media_hidden": "非表示のメデイア",
|
|
||||||
"video_player.toggle_sound": "音の切り替え",
|
|
||||||
"account.mention": "@{name} さんに返信",
|
|
||||||
"account.edit_profile": "プロフィールを編集",
|
"account.edit_profile": "プロフィールを編集",
|
||||||
|
"account.follow": "フォロー",
|
||||||
|
"account.followers": "フォロワー",
|
||||||
|
"account.follows": "フォロー",
|
||||||
|
"account.follows_you": "フォローされています",
|
||||||
|
"account.mention": "@{name} さんに返信",
|
||||||
|
"account.mute": "ミュート",
|
||||||
|
"account.posts": "投稿",
|
||||||
|
"account.report": "@{name}を通報する",
|
||||||
|
"account.requested": "承認待ち",
|
||||||
"account.unblock": "@{name} さんのブロックを解除",
|
"account.unblock": "@{name} さんのブロックを解除",
|
||||||
"account.unfollow": "フォロー解除",
|
"account.unfollow": "フォロー解除",
|
||||||
"account.block": "@{name} さんをブロック",
|
|
||||||
"account.mute": "ミュート",
|
|
||||||
"account.unmute": "ミュート解除",
|
"account.unmute": "ミュート解除",
|
||||||
"account.follow": "フォロー",
|
"boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。",
|
||||||
"account.report": "@{name}を通報する",
|
"column.blocks": "ブロックしたユーザー",
|
||||||
"account.posts": "投稿",
|
|
||||||
"account.follows": "フォロー",
|
|
||||||
"account.followers": "フォロワー",
|
|
||||||
"account.follows_you": "フォローされています",
|
|
||||||
"account.requested": "承認待ち",
|
|
||||||
"follow_request.authorize": "許可",
|
|
||||||
"follow_request.reject": "拒否",
|
|
||||||
"getting_started.heading": "スタート",
|
|
||||||
"getting_started.about_addressing": "ドメインとユーザー名を知っているなら検索フォームに入力すればフォローできます。",
|
|
||||||
"getting_started.about_shortcuts": "対象のアカウントがあなたと同じドメインのユーザーならばユーザー名のみで検索できます。これは返信のときも一緒です。",
|
|
||||||
"getting_started.open_source_notice": "Mastodon はオープンソースソフトウェアです。誰でも GitHub({github})から開発に参加したり、問題を報告したりできます。 {apps}",
|
|
||||||
"column.home": "ホーム",
|
|
||||||
"column.community": "ローカルタイムライン",
|
"column.community": "ローカルタイムライン",
|
||||||
"column.public": "連合タイムライン",
|
|
||||||
"column.notifications": "通知",
|
|
||||||
"column.favourites": "お気に入り",
|
"column.favourites": "お気に入り",
|
||||||
"tabs_bar.compose": "投稿",
|
"column.follow_requests": "フォローリクエスト",
|
||||||
"tabs_bar.home": "ホーム",
|
"column.home": "ホーム",
|
||||||
"tabs_bar.mentions": "返信",
|
"column.mutes": "ミュートしたユーザー",
|
||||||
"tabs_bar.local_timeline": "ローカル",
|
"column.notifications": "通知",
|
||||||
"tabs_bar.federated_timeline": "連合",
|
"column.public": "連合タイムライン",
|
||||||
"tabs_bar.notifications": "通知",
|
"column_back_button.label": "戻る",
|
||||||
"compose_form.placeholder": "今なにしてる?",
|
"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.publish": "トゥート",
|
||||||
"compose_form.sensitive": "メディアを不適切なコンテンツとしてマークする",
|
"compose_form.sensitive": "メディアを不適切なコンテンツとしてマークする",
|
||||||
"compose_form.spoiler": "テキストを隠す",
|
"compose_form.spoiler": "テキストを隠す",
|
||||||
"compose_form.spoiler_placeholder": "内容注意メッセージ",
|
"compose_form.spoiler_placeholder": "閲覧注意",
|
||||||
"compose_form.private": "非公開にする",
|
"emoji_button.label": "絵文字を追加",
|
||||||
"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}}ならばあなたの投稿のプライバシーは保護されず、ブーストされたり予期しないユーザーに見られる可能性があります。",
|
"empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",
|
||||||
"compose_form.unlisted": "公開タイムラインに表示しない",
|
"empty_column.hashtag": "このハッシュタグはまだ使われていません。",
|
||||||
"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}を見に行くか、検索を使って他のユーザーを見つけましょう。",
|
||||||
"empty_column.home.public_timeline": "連合タイムライン",
|
"empty_column.home.public_timeline": "連合タイムライン",
|
||||||
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
|
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
|
||||||
"empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!",
|
"empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!",
|
||||||
"empty_column.hashtag": "このハッシュタグはまだ使っていません。",
|
"follow_request.authorize": "許可",
|
||||||
"upload_progress.label": "アップロード中…",
|
"follow_request.reject": "拒否",
|
||||||
"emoji_button.label": "絵文字を追加",
|
"getting_started.apps": "さまざまなアプリで利用できます。",
|
||||||
|
"getting_started.heading": "スタート",
|
||||||
|
"getting_started.open_source_notice": "Mastodon はオープンソースソフトウェアです。誰でも GitHub({github})から開発に参加したり、問題を報告したりできます。 {apps}",
|
||||||
|
"home.column_settings.advanced": "上級者向け",
|
||||||
"home.column_settings.basic": "シンプル",
|
"home.column_settings.basic": "シンプル",
|
||||||
"home.column_settings.advanced": "エキスパート",
|
"home.column_settings.filter_regex": "正規表現でフィルター",
|
||||||
"home.column_settings.show_reblogs": "ブースト表示",
|
"home.column_settings.show_reblogs": "ブースト表示",
|
||||||
"home.column_settings.show_replies": "返信表示",
|
"home.column_settings.show_replies": "返信表示",
|
||||||
"home.column_settings.filter_regex": "正規表現でフィルター",
|
|
||||||
"home.settings": "カラム設定",
|
"home.settings": "カラム設定",
|
||||||
"notification.settings": "カラム設定",
|
"lightbox.close": "閉じる",
|
||||||
|
"loading_indicator.label": "読み込み中...",
|
||||||
|
"media_gallery.toggle_visible": "表示切り替え",
|
||||||
"missing_indicator.label": "見つかりません",
|
"missing_indicator.label": "見つかりません",
|
||||||
"boost_modal.combo": "次は{combo}を押せば、これをスキップできます。"
|
"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": "動画の再生に失敗しました",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ja;
|
export default ja;
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
const nl = {
|
const nl = {
|
||||||
"column_back_button.label": "terug",
|
"column_back_button.label": "terug",
|
||||||
"lightbox.close": "Sluiten",
|
"lightbox.close": "Sluiten",
|
||||||
"loading_indicator.label": "Laden...",
|
"loading_indicator.label": "Laden…",
|
||||||
"status.mention": "Vermeld @{name}",
|
"status.mention": "@{name} vermelden",
|
||||||
"status.delete": "Verwijder",
|
"status.delete": "Verwijderen",
|
||||||
"status.reply": "Reageer",
|
"status.reply": "Reageren",
|
||||||
"status.reblog": "Boost",
|
"status.reblog": "Boost",
|
||||||
"status.favourite": "Favoriet",
|
"status.favourite": "Favoriet",
|
||||||
"status.reblogged_by": "{name} boostte",
|
"status.reblogged_by": "{name} boostte",
|
||||||
"status.sensitive_warning": "Gevoelige inhoud",
|
"status.sensitive_warning": "Gevoelige inhoud",
|
||||||
"status.sensitive_toggle": "Klik om te zien",
|
"status.sensitive_toggle": "Klik om te zien",
|
||||||
"video_player.toggle_sound": "Geluid omschakelen",
|
"video_player.toggle_sound": "Geluid in-/uitschakelen",
|
||||||
"account.mention": "Vermeld @{name}",
|
"account.mention": "@{name} vermelden",
|
||||||
"account.edit_profile": "Bewerk profiel",
|
"account.edit_profile": "Profiel bewerken",
|
||||||
"account.unblock": "Deblokkeer @{name}",
|
"account.unblock": "@{name} deblokkeren",
|
||||||
"account.unfollow": "Ontvolg",
|
"account.unfollow": "Ontvolgen",
|
||||||
"account.block": "Blokkeer @{name}",
|
"account.block": "@{name} blokkeren",
|
||||||
"account.follow": "Volg",
|
"account.follow": "Volgen",
|
||||||
"account.posts": "Berichten",
|
"account.posts": "Berichten",
|
||||||
"account.follows": "Volgt",
|
"account.follows": "Volgt",
|
||||||
"account.followers": "Volgers",
|
"account.followers": "Volgers",
|
||||||
|
@ -25,7 +25,7 @@ const nl = {
|
||||||
"getting_started.heading": "Beginnen",
|
"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_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.about_shortcuts": "Als de gezochte gebruiker op hetzelfde domein zit als jijzelf, is invoeren van de gebruikersnaam genoeg. Dat geldt ook als je mensen in de statussen wilt vermelden.",
|
||||||
"getting_started.open_source_notice": "Mastodon is open source software. Je kunt bijdragen of problemen melden op GitHub via {github}. {apps}.",
|
"getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}. {apps}.",
|
||||||
"column.home": "Thuis",
|
"column.home": "Thuis",
|
||||||
"column.community": "Lokale tijdlijn",
|
"column.community": "Lokale tijdlijn",
|
||||||
"column.public": "Federatietijdlijn",
|
"column.public": "Federatietijdlijn",
|
||||||
|
@ -37,30 +37,30 @@ const nl = {
|
||||||
"tabs_bar.notifications": "Meldingen",
|
"tabs_bar.notifications": "Meldingen",
|
||||||
"compose_form.placeholder": "Waar ben je mee bezig?",
|
"compose_form.placeholder": "Waar ben je mee bezig?",
|
||||||
"compose_form.publish": "Toot",
|
"compose_form.publish": "Toot",
|
||||||
"compose_form.sensitive": "Markeer media als gevoelig",
|
"compose_form.sensitive": "Media als gevoelig markeren",
|
||||||
"compose_form.spoiler": "Verberg tekst achter waarschuwing",
|
"compose_form.spoiler": "Tekst achter waarschuwing verbergen",
|
||||||
"compose_form.private": "Mark als privé",
|
"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.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",
|
"compose_form.unlisted": "Niet op openbare tijdlijnen tonen",
|
||||||
"navigation_bar.edit_profile": "Bewerk profiel",
|
"navigation_bar.edit_profile": "Profiel bewerken",
|
||||||
"navigation_bar.preferences": "Voorkeuren",
|
"navigation_bar.preferences": "Voorkeuren",
|
||||||
"navigation_bar.community_timeline": "Lokale tijdlijn",
|
"navigation_bar.community_timeline": "Lokale tijdlijn",
|
||||||
"navigation_bar.public_timeline": "Federatietijdlijn",
|
"navigation_bar.public_timeline": "Federatietijdlijn",
|
||||||
"navigation_bar.logout": "Uitloggen",
|
"navigation_bar.logout": "Afmelden",
|
||||||
"reply_indicator.cancel": "Annuleren",
|
"reply_indicator.cancel": "Annuleren",
|
||||||
"search.placeholder": "Zoeken",
|
"search.placeholder": "Zoeken",
|
||||||
"search.account": "Account",
|
"search.account": "Account",
|
||||||
"search.hashtag": "Hashtag",
|
"search.hashtag": "Hashtag",
|
||||||
"upload_button.label": "Toevoegen media",
|
"upload_button.label": "Media toevoegen",
|
||||||
"upload_form.undo": "Ongedaan maken",
|
"upload_form.undo": "Ongedaan maken",
|
||||||
"notification.follow": "{name} volgde jou",
|
"notification.follow": "{name} volgde jou",
|
||||||
"notification.favourite": "{name} markeerde je status als favoriet",
|
"notification.favourite": "{name} markeerde je status als favoriet",
|
||||||
"notification.reblog": "{name} boostte je status",
|
"notification.reblog": "{name} boostte je status",
|
||||||
"notification.mention": "{name} vermeldde jou",
|
"notification.mention": "{name} vermeldde jou",
|
||||||
"notifications.column_settings.alert": "Desktopmeldingen",
|
"notifications.column_settings.alert": "Desktopmeldingen",
|
||||||
"notifications.column_settings.show": "Tonen in kolom",
|
"notifications.column_settings.show": "In kolom tonen",
|
||||||
"notifications.column_settings.follow": "Nieuwe volgers:",
|
"notifications.column_settings.follow": "Nieuwe volgers:",
|
||||||
"notifications.column_settings.favourite": "Favoriten:",
|
"notifications.column_settings.favourite": "Favorieten:",
|
||||||
"notifications.column_settings.mention": "Vermeldingen:",
|
"notifications.column_settings.mention": "Vermeldingen:",
|
||||||
"notifications.column_settings.reblog": "Boosts:",
|
"notifications.column_settings.reblog": "Boosts:",
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,77 +1,130 @@
|
||||||
const no = {
|
const no = {
|
||||||
"column_back_button.label": "Tilbake",
|
"account.block": "Blokkér @{name}",
|
||||||
"lightbox.close": "Lukk",
|
"account.disclaimer": "Denne brukeren er fra en annen instans. Dette tallet kan være høyere.",
|
||||||
"loading_indicator.label": "Laster...",
|
|
||||||
"status.mention": "Nevn @{name}",
|
|
||||||
"status.delete": "Slett",
|
|
||||||
"status.reply": "Svar",
|
|
||||||
"status.reblog": "Reblogg",
|
|
||||||
"status.favourite": "Lik",
|
|
||||||
"status.reblogged_by": "{name} reblogget",
|
|
||||||
"status.sensitive_warning": "Sensitivt innhold",
|
|
||||||
"status.sensitive_toggle": "Klikk for å vise",
|
|
||||||
"status.show_more": "Vis mer",
|
|
||||||
"status.show_less": "Vis mindre",
|
|
||||||
"status.open": "Utvid denne statusen",
|
|
||||||
"status.report": "Rapporter @{name}",
|
|
||||||
"video_player.toggle_sound": "Veksle lyd",
|
|
||||||
"account.mention": "Nevn @{name}",
|
|
||||||
"account.edit_profile": "Rediger profil",
|
"account.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.unblock": "Avblokker @{name}",
|
||||||
"account.unfollow": "Avfølg",
|
"account.unfollow": "Avfølg",
|
||||||
"account.block": "Blokker @{name}",
|
"account.unmute": "Avdemp @{name}",
|
||||||
"account.follow": "Følg",
|
"boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang",
|
||||||
"account.posts": "Poster",
|
"column_back_button.label": "Tilbake",
|
||||||
"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.blocks": "Blokkerte brukere",
|
||||||
|
"column.community": "Lokal tidslinje",
|
||||||
"column.favourites": "Likt",
|
"column.favourites": "Likt",
|
||||||
"tabs_bar.compose": "Komponer",
|
"column.follow_requests": "Følgeforespørsler",
|
||||||
"tabs_bar.home": "Hjem",
|
"column.home": "Hjem",
|
||||||
"tabs_bar.mentions": "Nevninger",
|
"column.notifications": "Varslinger",
|
||||||
"tabs_bar.public": "Forent tidslinje",
|
"column.public": "Felles tidslinje",
|
||||||
"tabs_bar.notifications": "Varslinger",
|
|
||||||
"compose_form.placeholder": "Hva har du på hjertet?",
|
"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.publish": "Tut",
|
||||||
"compose_form.sensitive": "Merk media som følsomt",
|
"compose_form.sensitive": "Merk media som følsomt",
|
||||||
|
"compose_form.spoiler_placeholder": "Innholdsadvarsel",
|
||||||
"compose_form.spoiler": "Skjul tekst bak advarsel",
|
"compose_form.spoiler": "Skjul tekst bak advarsel",
|
||||||
"compose_form.private": "Merk som privat",
|
"emoji_button.label": "Sett inn emoji",
|
||||||
"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.",
|
"empty_column.community": "Den lokale tidslinjen er tom. Skriv noe offentlig for å få snøballen til å rulle!",
|
||||||
"compose_form.unlisted": "Ikke vis på offentlige tidslinjer",
|
"empty_column.hashtag": "Det er ingenting i denne hashtagen ennå.",
|
||||||
"navigation_bar.edit_profile": "Rediger profil",
|
"empty_column.home.public_timeline": "en offentlig tidslinje",
|
||||||
"navigation_bar.preferences": "Preferanser",
|
"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.",
|
||||||
"navigation_bar.community_timeline": "Lokal tidslinje",
|
"empty_column.notifications": "Du har ingen varsler ennå. Kommuniser med andre for å begynne samtalen.",
|
||||||
"navigation_bar.public_timeline": "Forent tidslinje",
|
"empty_column.public": "Det er ingenting her! Skriv noe offentlig, eller følg brukere manuelt fra andre instanser for å fylle den opp",
|
||||||
"navigation_bar.logout": "Logg ut",
|
"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.blocks": "Blokkerte brukere",
|
||||||
"navigation_bar.info": "Utvidet informasjon",
|
"navigation_bar.community_timeline": "Lokal tidslinje",
|
||||||
|
"navigation_bar.edit_profile": "Rediger profil",
|
||||||
"navigation_bar.favourites": "Likt",
|
"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",
|
"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.placeholder": "Søk",
|
||||||
"search.account": "Konto",
|
"search.status_by": "Status fra {name}",
|
||||||
"search.hashtag": "Hashtag",
|
"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.reply": "Svar",
|
||||||
|
"status.report": "Rapporter @{name}",
|
||||||
|
"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_button.label": "Legg til media",
|
||||||
"upload_form.undo": "Angre",
|
"upload_form.undo": "Angre",
|
||||||
"notification.follow": "{name} fulgte deg",
|
"upload_progress.label": "Laster opp...",
|
||||||
"notification.favourite": "{name} likte din status",
|
"video_player.toggle_sound": "Veksle lyd",
|
||||||
"notification.reblog": "{name} reblogget din status",
|
"video_player.toggle_visible": "Veksle synlighet",
|
||||||
"notification.mention": "{name} nevnte deg",
|
"video_player.expand": "Utvid video",
|
||||||
"notifications.column_settings.alert": "Skrivebordsvarslinger",
|
"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.",
|
||||||
"notifications.column_settings.show": "Vis i kolonne",
|
"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.",
|
||||||
"notifications.column_settings.follow": "Nye følgere:",
|
"tabs_bar.mentions": "Nevninger",
|
||||||
"notifications.column_settings.favourite": "Likt:",
|
"tabs_bar.public": "Felles tidslinje",
|
||||||
"notifications.column_settings.mention": "Nevninger:",
|
"compose_form.private": "Merk som privat",
|
||||||
"notifications.column_settings.reblog": "Reblogginger:",
|
"compose_form.unlisted": "Ikke vis på offentlige tidslinjer",
|
||||||
|
"search.account": "Konto",
|
||||||
|
"search.hashtag": "Hashtag",
|
||||||
|
"notification.mention": "{name} nevnte deg"
|
||||||
};
|
};
|
||||||
|
|
||||||
export default no;
|
export default no;
|
||||||
|
|
|
@ -2,7 +2,9 @@ const ru = {
|
||||||
"column_back_button.label": "Назад",
|
"column_back_button.label": "Назад",
|
||||||
"lightbox.close": "Закрыть",
|
"lightbox.close": "Закрыть",
|
||||||
"loading_indicator.label": "Загрузка...",
|
"loading_indicator.label": "Загрузка...",
|
||||||
|
"missing_indicator.label": "Не найдено",
|
||||||
"status.mention": "Упомянуть @{name}",
|
"status.mention": "Упомянуть @{name}",
|
||||||
|
"status.media_hidden": "Медиаконтент скрыт",
|
||||||
"status.delete": "Удалить",
|
"status.delete": "Удалить",
|
||||||
"status.reply": "Ответить",
|
"status.reply": "Ответить",
|
||||||
"status.reblog": "Продвинуть",
|
"status.reblog": "Продвинуть",
|
||||||
|
@ -14,20 +16,25 @@ const ru = {
|
||||||
"status.show_less": "Свернуть",
|
"status.show_less": "Свернуть",
|
||||||
"status.open": "Развернуть статус",
|
"status.open": "Развернуть статус",
|
||||||
"status.report": "Пожаловаться",
|
"status.report": "Пожаловаться",
|
||||||
"status.load_more": "Показать еще",
|
"status.load_more": "Показать еще",
|
||||||
"video_player.toggle_sound": "Вкл./выкл. звук",
|
"video_player.toggle_sound": "Вкл./выкл. звук",
|
||||||
|
"video_player.toggle_visible": "Показать/скрыть",
|
||||||
|
"account.disclaimer": "Это пользователь с другого узла. Число может быть больше.",
|
||||||
"account.mention": "Упомянуть",
|
"account.mention": "Упомянуть",
|
||||||
"account.edit_profile": "Изменить профиль",
|
"account.edit_profile": "Изменить профиль",
|
||||||
"account.unblock": "Разблокировать",
|
"account.unblock": "Разблокировать",
|
||||||
"account.unfollow": "Отписаться",
|
"account.unfollow": "Отписаться",
|
||||||
"account.block": "Блокировать",
|
"account.block": "Блокировать",
|
||||||
"account.mute": "Заглушить",
|
"account.mute": "Заглушить",
|
||||||
|
"account.report": "Пожаловаться",
|
||||||
|
"account.unmute": "Снять глушение",
|
||||||
"account.follow": "Подписаться",
|
"account.follow": "Подписаться",
|
||||||
"account.posts": "Посты",
|
"account.posts": "Посты",
|
||||||
"account.follows": "Подписки",
|
"account.follows": "Подписки",
|
||||||
"account.followers": "Подписаны",
|
"account.followers": "Подписаны",
|
||||||
"account.follows_you": "Подписан(а) на Вас",
|
"account.follows_you": "Подписан(а) на Вас",
|
||||||
"account.requested": "Ожидает подтверждения",
|
"account.requested": "Ожидает подтверждения",
|
||||||
|
"boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз",
|
||||||
"getting_started.heading": "Добро пожаловать",
|
"getting_started.heading": "Добро пожаловать",
|
||||||
"getting_started.about_addressing": "Вы можете подписаться на человека, зная имя пользователя и домен, на котором он находится, введя e-mail-подобный адрес в форму поиска.",
|
"getting_started.about_addressing": "Вы можете подписаться на человека, зная имя пользователя и домен, на котором он находится, введя e-mail-подобный адрес в форму поиска.",
|
||||||
"getting_started.about_shortcuts": "Если пользователь находится на одном с Вами домене, можно использовать только имя. То же правило применимо к упоминанию пользователей в статусах.",
|
"getting_started.about_shortcuts": "Если пользователь находится на одном с Вами домене, можно использовать только имя. То же правило применимо к упоминанию пользователей в статусах.",
|
||||||
|
@ -37,11 +44,16 @@ const ru = {
|
||||||
"column.community": "Локальная лента",
|
"column.community": "Локальная лента",
|
||||||
"column.public": "Глобальная лента",
|
"column.public": "Глобальная лента",
|
||||||
"column.notifications": "Уведомления",
|
"column.notifications": "Уведомления",
|
||||||
|
"column.favourites": "Понравившееся",
|
||||||
|
"column.blocks": "Список блокировки",
|
||||||
|
"column.follow_requests": "Запросы на подписку",
|
||||||
"tabs_bar.compose": "Написать",
|
"tabs_bar.compose": "Написать",
|
||||||
"tabs_bar.home": "Главная",
|
"tabs_bar.home": "Главная",
|
||||||
"tabs_bar.mentions": "Упоминания",
|
"tabs_bar.mentions": "Упоминания",
|
||||||
"tabs_bar.public": "Глобальная лента",
|
"tabs_bar.public": "Глобальная лента",
|
||||||
"tabs_bar.notifications": "Уведомления",
|
"tabs_bar.notifications": "Уведомления",
|
||||||
|
"tabs_bar.local_timeline": "Локальная",
|
||||||
|
"tabs_bar.federated_timeline": "Глобальная",
|
||||||
"compose_form.placeholder": "О чем Вы думаете?",
|
"compose_form.placeholder": "О чем Вы думаете?",
|
||||||
"compose_form.publish": "Трубить",
|
"compose_form.publish": "Трубить",
|
||||||
"compose_form.sensitive": "Отметить как чувствительный контент",
|
"compose_form.sensitive": "Отметить как чувствительный контент",
|
||||||
|
@ -49,6 +61,7 @@ const ru = {
|
||||||
"compose_form.private": "Отметить как приватное",
|
"compose_form.private": "Отметить как приватное",
|
||||||
"compose_form.privacy_disclaimer": "Ваш приватный статус будет доставлен упомянутым пользователям на доменах {domains}. Доверяете ли вы {domainsCount, plural, one {этому серверу} other {этим серверам}}? Приватность постов работает только на узлах Mastodon. Если {domains} {domainsCount, plural, one {не является узлом Mastodon} other {не являются узлами Mastodon}}, приватность поста не будет указана, и он может оказаться продвинут или иным образом показан не обозначенным Вами пользователям.",
|
"compose_form.privacy_disclaimer": "Ваш приватный статус будет доставлен упомянутым пользователям на доменах {domains}. Доверяете ли вы {domainsCount, plural, one {этому серверу} other {этим серверам}}? Приватность постов работает только на узлах Mastodon. Если {domains} {domainsCount, plural, one {не является узлом Mastodon} other {не являются узлами Mastodon}}, приватность поста не будет указана, и он может оказаться продвинут или иным образом показан не обозначенным Вами пользователям.",
|
||||||
"compose_form.unlisted": "Не отображать в публичных лентах",
|
"compose_form.unlisted": "Не отображать в публичных лентах",
|
||||||
|
"compose_form.spoiler_placeholder": "Не для всех",
|
||||||
"navigation_bar.edit_profile": "Изменить профиль",
|
"navigation_bar.edit_profile": "Изменить профиль",
|
||||||
"navigation_bar.preferences": "Опции",
|
"navigation_bar.preferences": "Опции",
|
||||||
"navigation_bar.community_timeline": "Локальная лента",
|
"navigation_bar.community_timeline": "Локальная лента",
|
||||||
|
@ -57,12 +70,20 @@ const ru = {
|
||||||
"navigation_bar.info": "Об узле",
|
"navigation_bar.info": "Об узле",
|
||||||
"navigation_bar.favourites": "Понравившееся",
|
"navigation_bar.favourites": "Понравившееся",
|
||||||
"navigation_bar.blocks": "Список блокировки",
|
"navigation_bar.blocks": "Список блокировки",
|
||||||
|
"navigation_bar.follow_requests": "Запросы на подписку",
|
||||||
"reply_indicator.cancel": "Отмена",
|
"reply_indicator.cancel": "Отмена",
|
||||||
|
"report.target": "Жалуемся на",
|
||||||
|
"report.heading": "Новая жалоба",
|
||||||
|
"report.placeholder": "Комментарий",
|
||||||
|
"report.submit": "Отправить",
|
||||||
"search.placeholder": "Поиск",
|
"search.placeholder": "Поиск",
|
||||||
"search.account": "Аккаунт",
|
"search.account": "Аккаунт",
|
||||||
"search.hashtag": "Хэштег",
|
"search.hashtag": "Хэштег",
|
||||||
|
"search.status_by": "Статус от {name}",
|
||||||
|
"upload_area.title": "Перетащите сюда, чтобы загрузить",
|
||||||
"upload_button.label": "Добавить медиаконтент",
|
"upload_button.label": "Добавить медиаконтент",
|
||||||
"upload_form.undo": "Отменить",
|
"upload_form.undo": "Отменить",
|
||||||
|
"upload_progress.label": "Загрузка...",
|
||||||
"notification.follow": "{name} подписался(-лась) на Вас",
|
"notification.follow": "{name} подписался(-лась) на Вас",
|
||||||
"notification.favourite": "{name} понравился Ваш статус",
|
"notification.favourite": "{name} понравился Ваш статус",
|
||||||
"notification.reblog": "{name} продвинул(а) Ваш статус",
|
"notification.reblog": "{name} продвинул(а) Ваш статус",
|
||||||
|
@ -71,9 +92,10 @@ const ru = {
|
||||||
"home.column_settings.basic": "Основные",
|
"home.column_settings.basic": "Основные",
|
||||||
"home.column_settings.advanced": "Дополнительные",
|
"home.column_settings.advanced": "Дополнительные",
|
||||||
"home.column_settings.filter_regex": "Отфильтровать регулярным выражением",
|
"home.column_settings.filter_regex": "Отфильтровать регулярным выражением",
|
||||||
"home.column_settings.show_replies": "Показывать продвижения",
|
"home.column_settings.show_reblogs": "Показывать продвижения",
|
||||||
"home.column_settings.show_replies": "Показывать ответы",
|
"home.column_settings.show_replies": "Показывать ответы",
|
||||||
"notifications.clear": "Очистить уведомления",
|
"notifications.clear": "Очистить уведомления",
|
||||||
|
"notifications.clear_confirmation": "Вы уверены, что хотите очистить все уведомления?",
|
||||||
"notifications.settings": "Настройки колонки",
|
"notifications.settings": "Настройки колонки",
|
||||||
"notifications.column_settings.alert": "Десктопные уведомления",
|
"notifications.column_settings.alert": "Десктопные уведомления",
|
||||||
"notifications.column_settings.show": "Показывать в колонке",
|
"notifications.column_settings.show": "Показывать в колонке",
|
||||||
|
@ -96,6 +118,10 @@ const ru = {
|
||||||
"privacy.private.long": "Показать только подписчикам",
|
"privacy.private.long": "Показать только подписчикам",
|
||||||
"privacy.direct.short": "Направленный",
|
"privacy.direct.short": "Направленный",
|
||||||
"privacy.direct.long": "Показать только упомянутым",
|
"privacy.direct.long": "Показать только упомянутым",
|
||||||
|
"emoji_button.label": "Вставить эмодзи",
|
||||||
|
"follow_request.authorize": "Авторизовать",
|
||||||
|
"follow_request.reject": "Отказать",
|
||||||
|
"media_gallery.toggle_visible": "Показать/скрыть",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ru;
|
export default ru;
|
||||||
|
|
|
@ -19,19 +19,27 @@ export { localeData as localeData };
|
||||||
|
|
||||||
const zh_hk = {
|
const zh_hk = {
|
||||||
"account.block": "封鎖 @{name}",
|
"account.block": "封鎖 @{name}",
|
||||||
|
"account.disclaimer": "由於這個用戶在另一個服務站,實際數字會比這個更多。",
|
||||||
"account.edit_profile": "修改個人資料",
|
"account.edit_profile": "修改個人資料",
|
||||||
"account.follow": "關注",
|
"account.follow": "關注",
|
||||||
"account.followers": "關注的人",
|
"account.followers": "關注的人",
|
||||||
"account.follows_you": "關注你",
|
"account.follows_you": "關注你",
|
||||||
"account.follows": "正在關注",
|
"account.follows": "正在關注",
|
||||||
"account.mention": "提及 @{name}",
|
"account.mention": "提及 @{name}",
|
||||||
|
"account.mute": "將 @{name} 靜音",
|
||||||
"account.posts": "文章",
|
"account.posts": "文章",
|
||||||
|
"account.report": "舉報 @{name}",
|
||||||
"account.requested": "等候審批",
|
"account.requested": "等候審批",
|
||||||
"account.unblock": "解除對 @{name} 的封鎖",
|
"account.unblock": "解除對 @{name} 的封鎖",
|
||||||
"account.unfollow": "取消關注",
|
"account.unfollow": "取消關注",
|
||||||
"column_back_button.label": "先前顯示",
|
"account.unmute": "取消 @{name} 的靜音",
|
||||||
|
"boost_modal.combo": "如你想在下次路過這顯示,請按{combo},",
|
||||||
|
"column_back_button.label": "返回",
|
||||||
|
"column.blocks": "封鎖用戶",
|
||||||
"column.community": "本站時間軸",
|
"column.community": "本站時間軸",
|
||||||
"column.home": "家",
|
"column.favourites": "喜歡的文章",
|
||||||
|
"column.follow_requests": "關注請求",
|
||||||
|
"column.home": "主頁",
|
||||||
"column.notifications": "通知",
|
"column.notifications": "通知",
|
||||||
"column.public": "跨站公共時間軸",
|
"column.public": "跨站公共時間軸",
|
||||||
"compose_form.placeholder": "你在想甚麼?",
|
"compose_form.placeholder": "你在想甚麼?",
|
||||||
|
@ -39,35 +47,49 @@ const zh_hk = {
|
||||||
"compose_form.private": "標示為「只有關注你的人能看」",
|
"compose_form.private": "標示為「只有關注你的人能看」",
|
||||||
"compose_form.publish": "發文",
|
"compose_form.publish": "發文",
|
||||||
"compose_form.sensitive": "將媒體檔案標示為「敏感內容」",
|
"compose_form.sensitive": "將媒體檔案標示為「敏感內容」",
|
||||||
|
"compose_form.spoiler_placeholder": "敏感內容",
|
||||||
"compose_form.spoiler": "將部份文字藏於警告訊息之後",
|
"compose_form.spoiler": "將部份文字藏於警告訊息之後",
|
||||||
"compose_form.unlisted": "請勿在公共時間軸顯示",
|
"compose_form.unlisted": "請勿在公共時間軸顯示",
|
||||||
|
"emoji_button.label": "加入表情符號",
|
||||||
"empty_column.community": "本站時間軸暫時未有內容,快貼文來搶頭香啊!",
|
"empty_column.community": "本站時間軸暫時未有內容,快貼文來搶頭香啊!",
|
||||||
"empty_column.hashtag": "這個標籤暫時未有內容。",
|
"empty_column.hashtag": "這個標籤暫時未有內容。",
|
||||||
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
|
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
|
||||||
"empty_column.home.public_timeline": "公共時間軸",
|
"empty_column.home.public_timeline": "公共時間軸",
|
||||||
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
|
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
|
||||||
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up.",
|
"empty_column.notifications": "你沒有任何通知紀錄,快向其他用戶搭訕吧。",
|
||||||
|
"empty_column.public": "跨站公共時間軸暫時沒有內容!快寫一些公共的文章,或者關注另一些服務站的用戶吧!你和本站、友站的交流,將決定這裏出現的內容。",
|
||||||
|
"follow_request.authorize": "批准",
|
||||||
|
"follow_request.reject": "拒絕",
|
||||||
"getting_started.about_addressing": "只要你知道一位用戶的用戶名稱和域名,你可以用「@用戶名稱@域名」的格式在搜尋欄尋找該用戶。",
|
"getting_started.about_addressing": "只要你知道一位用戶的用戶名稱和域名,你可以用「@用戶名稱@域名」的格式在搜尋欄尋找該用戶。",
|
||||||
"getting_started.about_shortcuts": "只要該用戶是在你現在的服務站開立,你可以直接輸入用戶𠱷搜尋。同樣的規則適用於在文章提及別的用戶。",
|
"getting_started.about_shortcuts": "只要該用戶是在你現在的服務站開立,你可以直接輸入用戶𠱷搜尋。同樣的規則適用於在文章提及別的用戶。",
|
||||||
"getting_started.apps": "手機或桌面應用程式",
|
"getting_started.apps": "手機或桌面應用程式",
|
||||||
"getting_started.heading": "開始使用",
|
"getting_started.heading": "開始使用",
|
||||||
"getting_started.open_source_notice": "Mastodon 是一個開放源碼的軟件。你可以在官方 GitHub ({github}) 貢獻或者回報問題。你亦可透過{apps}閱讀 Mastodon 上的消息。",
|
"getting_started.open_source_notice": "Mastodon 是一個開放源碼的軟件。你可以在官方 GitHub ({github}) 貢獻或者回報問題。你亦可透過{apps}閱讀 Mastodon 上的消息。",
|
||||||
|
"home.column_settings.advanced": "進階",
|
||||||
"home.column_settings.basic": "基本",
|
"home.column_settings.basic": "基本",
|
||||||
|
"home.column_settings.filter_regex": "使用正規表達式 (regular expression) 過濾",
|
||||||
"home.column_settings.show_reblogs": "顯示被轉推的文章",
|
"home.column_settings.show_reblogs": "顯示被轉推的文章",
|
||||||
"home.column_settings.show_replies": "顯示回應文章",
|
"home.column_settings.show_replies": "顯示回應文章",
|
||||||
"home.column_settings.advanced": "進階",
|
"home.settings": "欄位設定",
|
||||||
"lightbox.close": "關閉",
|
"lightbox.close": "Close",
|
||||||
"loading_indicator.label": "載入中...",
|
"loading_indicator.label": "載入中...",
|
||||||
|
"media_gallery.toggle_visible": "打開或關上",
|
||||||
"missing_indicator.label": "找不到內容",
|
"missing_indicator.label": "找不到內容",
|
||||||
|
"navigation_bar.blocks": "被封鎖的用戶",
|
||||||
"navigation_bar.community_timeline": "本站時間軸",
|
"navigation_bar.community_timeline": "本站時間軸",
|
||||||
"navigation_bar.edit_profile": "修改個人資料",
|
"navigation_bar.edit_profile": "修改個人資料",
|
||||||
|
"navigation_bar.favourites": "喜歡的內容",
|
||||||
|
"navigation_bar.follow_requests": "關注請求",
|
||||||
|
"navigation_bar.info": "關於本服務站",
|
||||||
"navigation_bar.logout": "登出",
|
"navigation_bar.logout": "登出",
|
||||||
"navigation_bar.preferences": "個人設定",
|
"navigation_bar.preferences": "偏好設定",
|
||||||
"navigation_bar.public_timeline": "跨站公共時間軸",
|
"navigation_bar.public_timeline": "跨站公共時間軸",
|
||||||
"notification.favourite": "{name} 喜歡你的文章",
|
"notification.favourite": "{name} 喜歡你的文章",
|
||||||
"notification.follow": "{name} 開始開始你",
|
"notification.follow": "{name} 開始關注你",
|
||||||
"notification.mention": "{name} 提及你",
|
"notification.mention": "{name} 提及你",
|
||||||
"notification.reblog": "{name} 轉推你的文章",
|
"notification.reblog": "{name} 轉推你的文章",
|
||||||
|
"notifications.clear_confirmation": "你確定要清空通知紀錄嗎?",
|
||||||
|
"notifications.clear": "清空通知紀錄",
|
||||||
"notifications.column_settings.alert": "顯示桌面通知",
|
"notifications.column_settings.alert": "顯示桌面通知",
|
||||||
"notifications.column_settings.favourite": "喜歡你的文章:",
|
"notifications.column_settings.favourite": "喜歡你的文章:",
|
||||||
"notifications.column_settings.follow": "關注你:",
|
"notifications.column_settings.follow": "關注你:",
|
||||||
|
@ -75,13 +97,26 @@ const zh_hk = {
|
||||||
"notifications.column_settings.reblog": "轉推你的文章:",
|
"notifications.column_settings.reblog": "轉推你的文章:",
|
||||||
"notifications.column_settings.show": "在通知欄顯示",
|
"notifications.column_settings.show": "在通知欄顯示",
|
||||||
"notifications.column_settings.sound": "播放音效",
|
"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": "取消",
|
"reply_indicator.cancel": "取消",
|
||||||
|
"report.heading": "舉報",
|
||||||
|
"report.placeholder": "額外訊息",
|
||||||
|
"report.submit": "提交",
|
||||||
"report.target": "Reporting",
|
"report.target": "Reporting",
|
||||||
|
"search_results.total": "{count} 項結果",
|
||||||
"search.account": "用戶",
|
"search.account": "用戶",
|
||||||
"search.hashtag": "標籤",
|
"search.hashtag": "標籤",
|
||||||
"search.placeholder": "搜尋",
|
"search.placeholder": "搜尋",
|
||||||
"search_results.total": "{count} 項結果",
|
"search.status_by": "按{name}搜尋文章",
|
||||||
"search.status_by": "按用戶名稱搜尋文章",
|
|
||||||
"status.delete": "刪除",
|
"status.delete": "刪除",
|
||||||
"status.favourite": "喜歡",
|
"status.favourite": "喜歡",
|
||||||
"status.load_more": "載入更多",
|
"status.load_more": "載入更多",
|
||||||
|
@ -97,17 +132,19 @@ const zh_hk = {
|
||||||
"status.show_less": "減少顯示",
|
"status.show_less": "減少顯示",
|
||||||
"status.show_more": "顯示更多",
|
"status.show_more": "顯示更多",
|
||||||
"tabs_bar.compose": "撰寫",
|
"tabs_bar.compose": "撰寫",
|
||||||
"tabs_bar.home": "家",
|
"tabs_bar.federated_timeline": "跨站",
|
||||||
|
"tabs_bar.home": "主頁",
|
||||||
"tabs_bar.local_timeline": "本站",
|
"tabs_bar.local_timeline": "本站",
|
||||||
"tabs_bar.mentions": "提及",
|
"tabs_bar.mentions": "提及",
|
||||||
"tabs_bar.notifications": "通知",
|
"tabs_bar.notifications": "通知",
|
||||||
"tabs_bar.public": "跨站公共時間軸",
|
"tabs_bar.public": "跨站公共時間軸",
|
||||||
"tabs_bar.federated_timeline": "跨站",
|
|
||||||
"upload_area.title": "將檔案拖放至此上載",
|
"upload_area.title": "將檔案拖放至此上載",
|
||||||
"upload_button.label": "上載媒體檔案",
|
"upload_button.label": "上載媒體檔案",
|
||||||
"upload_progress.label": "上載中……",
|
|
||||||
"upload_form.undo": "還原",
|
"upload_form.undo": "還原",
|
||||||
|
"upload_progress.label": "上載中……",
|
||||||
|
"video_player.expand": "展開影片",
|
||||||
"video_player.toggle_sound": "開關音效",
|
"video_player.toggle_sound": "開關音效",
|
||||||
|
"video_player.toggle_visible": "打開或關上",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default zh_hk;
|
export default zh_hk;
|
||||||
|
|
|
@ -22,7 +22,7 @@ export default function errorsMiddleware() {
|
||||||
|
|
||||||
dispatch(showAlert(title, message));
|
dispatch(showAlert(title, message));
|
||||||
} else {
|
} else {
|
||||||
console.error(action.error);
|
console.error(action.error); // eslint-disable-line no-console
|
||||||
dispatch(showAlert('Oops!', 'An unexpected error occurred.'));
|
dispatch(showAlert('Oops!', 'An unexpected error occurred.'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,10 @@ import {
|
||||||
BLOCKS_FETCH_SUCCESS,
|
BLOCKS_FETCH_SUCCESS,
|
||||||
BLOCKS_EXPAND_SUCCESS
|
BLOCKS_EXPAND_SUCCESS
|
||||||
} from '../actions/blocks';
|
} from '../actions/blocks';
|
||||||
|
import {
|
||||||
|
MUTES_FETCH_SUCCESS,
|
||||||
|
MUTES_EXPAND_SUCCESS
|
||||||
|
} from '../actions/mutes';
|
||||||
import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose';
|
import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose';
|
||||||
import {
|
import {
|
||||||
REBLOG_SUCCESS,
|
REBLOG_SUCCESS,
|
||||||
|
@ -94,6 +98,8 @@ export default function accounts(state = initialState, action) {
|
||||||
case FOLLOW_REQUESTS_EXPAND_SUCCESS:
|
case FOLLOW_REQUESTS_EXPAND_SUCCESS:
|
||||||
case BLOCKS_FETCH_SUCCESS:
|
case BLOCKS_FETCH_SUCCESS:
|
||||||
case BLOCKS_EXPAND_SUCCESS:
|
case BLOCKS_EXPAND_SUCCESS:
|
||||||
|
case MUTES_FETCH_SUCCESS:
|
||||||
|
case MUTES_EXPAND_SUCCESS:
|
||||||
return normalizeAccounts(state, action.accounts);
|
return normalizeAccounts(state, action.accounts);
|
||||||
case NOTIFICATIONS_REFRESH_SUCCESS:
|
case NOTIFICATIONS_REFRESH_SUCCESS:
|
||||||
case NOTIFICATIONS_EXPAND_SUCCESS:
|
case NOTIFICATIONS_EXPAND_SUCCESS:
|
||||||
|
|
|
@ -9,17 +9,17 @@ const initialState = Immutable.List([]);
|
||||||
|
|
||||||
export default function alerts(state = initialState, action) {
|
export default function alerts(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case ALERT_SHOW:
|
case ALERT_SHOW:
|
||||||
return state.push(Immutable.Map({
|
return state.push(Immutable.Map({
|
||||||
key: state.size > 0 ? state.last().get('key') + 1 : 0,
|
key: state.size > 0 ? state.last().get('key') + 1 : 0,
|
||||||
title: action.title,
|
title: action.title,
|
||||||
message: action.message
|
message: action.message
|
||||||
}));
|
}));
|
||||||
case ALERT_DISMISS:
|
case ALERT_DISMISS:
|
||||||
return state.filterNot(item => item.get('key') === action.alert.key);
|
return state.filterNot(item => item.get('key') === action.alert.key);
|
||||||
case ALERT_CLEAR:
|
case ALERT_CLEAR:
|
||||||
return state.clear();
|
return state.clear();
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { STORE_HYDRATE } from '../actions/store';
|
||||||
import Immutable from 'immutable';
|
import Immutable from 'immutable';
|
||||||
|
|
||||||
const initialState = Immutable.Map({
|
const initialState = Immutable.Map({
|
||||||
|
streaming_api_base_url: null,
|
||||||
access_token: null,
|
access_token: null,
|
||||||
me: null
|
me: null
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { STORE_HYDRATE } from '../actions/store';
|
||||||
import Immutable from 'immutable';
|
import Immutable from 'immutable';
|
||||||
|
|
||||||
const initialState = Immutable.Map({
|
const initialState = Immutable.Map({
|
||||||
|
onboarded: false,
|
||||||
|
|
||||||
home: Immutable.Map({
|
home: Immutable.Map({
|
||||||
shows: Immutable.Map({
|
shows: Immutable.Map({
|
||||||
reblog: true,
|
reblog: true,
|
||||||
|
|
|
@ -48,6 +48,9 @@ const normalizeStatus = (state, status) => {
|
||||||
normalStatus.reblog = status.reblog.id;
|
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)));
|
return state.update(status.id, Immutable.Map(), map => map.mergeDeep(Immutable.fromJS(normalStatus)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,10 @@ import {
|
||||||
BLOCKS_FETCH_SUCCESS,
|
BLOCKS_FETCH_SUCCESS,
|
||||||
BLOCKS_EXPAND_SUCCESS
|
BLOCKS_EXPAND_SUCCESS
|
||||||
} from '../actions/blocks';
|
} from '../actions/blocks';
|
||||||
|
import {
|
||||||
|
MUTES_FETCH_SUCCESS,
|
||||||
|
MUTES_EXPAND_SUCCESS
|
||||||
|
} from '../actions/mutes';
|
||||||
import Immutable from 'immutable';
|
import Immutable from 'immutable';
|
||||||
|
|
||||||
const initialState = Immutable.Map({
|
const initialState = Immutable.Map({
|
||||||
|
@ -24,7 +28,8 @@ const initialState = Immutable.Map({
|
||||||
reblogged_by: Immutable.Map(),
|
reblogged_by: Immutable.Map(),
|
||||||
favourited_by: Immutable.Map(),
|
favourited_by: Immutable.Map(),
|
||||||
follow_requests: Immutable.Map(),
|
follow_requests: Immutable.Map(),
|
||||||
blocks: Immutable.Map()
|
blocks: Immutable.Map(),
|
||||||
|
mutes: Immutable.Map()
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizeList = (state, type, id, accounts, next) => {
|
const normalizeList = (state, type, id, accounts, next) => {
|
||||||
|
@ -65,6 +70,10 @@ 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);
|
return state.setIn(['blocks', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
|
||||||
case BLOCKS_EXPAND_SUCCESS:
|
case BLOCKS_EXPAND_SUCCESS:
|
||||||
return state.updateIn(['blocks', 'items'], list => list.push(...action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
|
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:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,8 @@ const createWebSocketURL = (url) => {
|
||||||
return a.href;
|
return a.href;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function getStream(accessToken, stream, { connected, received, disconnected, reconnected }) {
|
export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) {
|
||||||
const ws = new WebSocketClient(`${createWebSocketURL(STREAMING_API_BASE_URL)}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`);
|
const ws = new WebSocketClient(`${createWebSocketURL(streamingAPIBaseURL)}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`);
|
||||||
|
|
||||||
ws.onopen = connected;
|
ws.onopen = connected;
|
||||||
ws.onmessage = e => received(JSON.parse(e.data));
|
ws.onmessage = e => received(JSON.parse(e.data));
|
||||||
|
|
|
@ -64,7 +64,7 @@
|
||||||
p, li {
|
p, li {
|
||||||
font: 16px/28px 'Montserrat', sans-serif;
|
font: 16px/28px 'Montserrat', sans-serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
margin-bottom: 26px;
|
margin-bottom: 12px;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $color4;
|
color: $color4;
|
||||||
|
@ -352,7 +352,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 625px) {
|
@media screen and (max-width: 625px) {
|
||||||
.mascot {
|
.mascot {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -932,6 +932,12 @@ a.status__content__spoiler-link {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pseudo-drawer {
|
||||||
|
background: lighten($color1, 13%);
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.drawer__header {
|
.drawer__header {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
@ -1203,6 +1209,10 @@ a.status__content__spoiler-link {
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.spoiler-input__input {
|
.spoiler-input__input {
|
||||||
|
@ -1267,6 +1277,10 @@ a.status__content__spoiler-link {
|
||||||
color: $color5;
|
color: $color5;
|
||||||
border-bottom-color: $color4;
|
border-bottom-color: $color4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@import 'boost';
|
@import 'boost';
|
||||||
|
@ -1383,7 +1397,7 @@ button.icon-button.active i.fa-retweet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-spoiler {
|
.media-spoiler, .video-error-cover {
|
||||||
background: $color8;
|
background: $color8;
|
||||||
color: $color5;
|
color: $color5;
|
||||||
}
|
}
|
||||||
|
@ -1906,6 +1920,10 @@ button.icon-button.active i.fa-retweet {
|
||||||
&:focus {
|
&:focus {
|
||||||
background: lighten($color1, 4%);
|
background: lighten($color1, 4%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search__icon {
|
.search__icon {
|
||||||
|
@ -2006,6 +2024,7 @@ button.icon-button.active i.fa-retweet {
|
||||||
.modal-root__modal {
|
.modal-root__modal {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-modal {
|
.media-modal {
|
||||||
|
@ -2019,6 +2038,237 @@ 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 {
|
.boost-modal {
|
||||||
background: lighten($color2, 8%);
|
background: lighten($color2, 8%);
|
||||||
color: $color1;
|
color: $color1;
|
||||||
|
@ -2056,3 +2306,11 @@ button.icon-button.active i.fa-retweet {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-bar {
|
||||||
|
background-color: $color4;
|
||||||
|
height: 3px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
|
@ -6,3 +6,7 @@
|
||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recovery-codes {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
$color1: #282c37; // darkest
|
$color1: #282c37 !default; // darkest
|
||||||
$color2: #d9e1e8; // lightest
|
$color2: #d9e1e8 !default; // lightest
|
||||||
$color3: #9baec8; // lighter
|
$color3: #9baec8 !default; // lighter
|
||||||
$color4: #2b90d9; // vibrant
|
$color4: #2b90d9 !default; // vibrant
|
||||||
$color5: #ffffff; // white
|
$color5: #ffffff !default; // white
|
||||||
$color6: #df405a; // error red
|
$color6: #df405a !default; // error red
|
||||||
$color7: #79bd9a; // succ green
|
$color7: #79bd9a !default; // succ green
|
||||||
$color8: #000000; // black
|
$color8: #000000 !default; // black
|
||||||
|
|
|
@ -15,16 +15,26 @@ module Admin
|
||||||
|
|
||||||
if @domain_block.save
|
if @domain_block.save
|
||||||
DomainBlockWorker.perform_async(@domain_block.id)
|
DomainBlockWorker.perform_async(@domain_block.id)
|
||||||
redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed'
|
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg')
|
||||||
else
|
else
|
||||||
render action: :new
|
render action: :new
|
||||||
end
|
end
|
||||||
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
|
private
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params.require(:domain_block).permit(:domain, :severity)
|
params.require(:domain_block).permit(:domain, :severity, :reject_media, :retroactive)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# 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,38 +5,60 @@ module Admin
|
||||||
before_action :set_report, except: [:index]
|
before_action :set_report, except: [:index]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@reports = Report.includes(:account, :target_account).order('id desc').page(params[:page])
|
@reports = filtered_reports.page(params[:page])
|
||||||
@reports = params[:action_taken].present? ? @reports.resolved : @reports.unresolved
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show; end
|
||||||
@statuses = Status.where(id: @report.status_ids)
|
|
||||||
end
|
|
||||||
|
|
||||||
def resolve
|
def update
|
||||||
@report.update(action_taken: true, action_taken_by_account_id: current_account.id)
|
process_report
|
||||||
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)
|
redirect_to admin_report_path(@report)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
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
|
def set_report
|
||||||
@report = Report.find(params[:id])
|
@report = Report.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# 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)
|
def stream_entry_from_url(url)
|
||||||
params = Rails.application.routes.recognize_path(url)
|
params = Rails.application.routes.recognize_path(url)
|
||||||
|
|
||||||
raise ActiveRecord::NotFound unless params[:controller] == 'stream_entries' && params[:action] == 'show'
|
raise ActiveRecord::RecordNotFound unless params[:controller] == 'stream_entries' && params[:action] == 'show'
|
||||||
|
|
||||||
StreamEntry.find(params[:id])
|
StreamEntry.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
|
@ -30,7 +30,7 @@ class Api::PushController < ApiController
|
||||||
params = Rails.application.routes.recognize_path(uri.path)
|
params = Rails.application.routes.recognize_path(uri.path)
|
||||||
domain = uri.host + (uri.port ? ":#{uri.port}" : '')
|
domain = uri.host + (uri.port ? ":#{uri.port}" : '')
|
||||||
|
|
||||||
return unless TagManager.instance.local_domain?(domain) && params[:controller] == 'accounts' && params[:action] == 'show' && params[:format] == 'atom'
|
return unless TagManager.instance.web_domain?(domain) && params[:controller] == 'accounts' && params[:action] == 'show' && params[:format] == 'atom'
|
||||||
|
|
||||||
Account.find_local(params[:username])
|
Account.find_local(params[:username])
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,7 +8,9 @@ class ApplicationController < ActionController::Base
|
||||||
force_ssl if: "Rails.env.production? && ENV['LOCAL_HTTPS'] == 'true'"
|
force_ssl if: "Rails.env.production? && ENV['LOCAL_HTTPS'] == 'true'"
|
||||||
|
|
||||||
include Localized
|
include Localized
|
||||||
|
|
||||||
helper_method :current_account
|
helper_method :current_account
|
||||||
|
helper_method :single_user_mode?
|
||||||
|
|
||||||
rescue_from ActionController::RoutingError, with: :not_found
|
rescue_from ActionController::RoutingError, with: :not_found
|
||||||
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
||||||
|
@ -69,6 +71,10 @@ class ApplicationController < ActionController::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def single_user_mode?
|
||||||
|
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.first
|
||||||
|
end
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
@current_account ||= current_user.try(:account)
|
@current_account ||= current_user.try(:account)
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,7 +28,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_enabled_registrations
|
def check_enabled_registrations
|
||||||
redirect_to root_path if Rails.configuration.x.single_user_mode || !Setting.open_registrations
|
redirect_to root_path if single_user_mode? || !Setting.open_registrations
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -49,7 +49,8 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_otp_attempt?(user)
|
def valid_otp_attempt?(user)
|
||||||
user.validate_and_consume_otp!(user_params[:otp_attempt])
|
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
|
||||||
|
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
|
||||||
end
|
end
|
||||||
|
|
||||||
def authenticate_with_two_factor
|
def authenticate_with_two_factor
|
||||||
|
|
|
@ -27,7 +27,11 @@ module Localized
|
||||||
|
|
||||||
def default_locale
|
def default_locale
|
||||||
ENV.fetch('DEFAULT_LOCALE') {
|
ENV.fetch('DEFAULT_LOCALE') {
|
||||||
http_accept_language.compatible_language_from(I18n.available_locales) || I18n.default_locale
|
user_supplied_locale || I18n.default_locale
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def user_supplied_locale
|
||||||
|
http_accept_language.language_region_compatible_from(I18n.available_locales)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,15 +4,17 @@ class HomeController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@body_classes = 'app-body'
|
@body_classes = 'app-body'
|
||||||
@token = find_or_create_access_token.token
|
@token = find_or_create_access_token.token
|
||||||
@web_settings = Web::Setting.find_by(user: current_user)&.data || {}
|
@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
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def authenticate_user!
|
def authenticate_user!
|
||||||
redirect_to(Rails.configuration.x.single_user_mode ? account_path(Account.first) : about_path) unless user_signed_in?
|
redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in?
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_access_token
|
def find_or_create_access_token
|
||||||
|
|
|
@ -19,9 +19,9 @@ class Settings::TwoFactorAuthsController < ApplicationController
|
||||||
def create
|
def create
|
||||||
if current_user.validate_and_consume_otp!(confirmation_params[:code])
|
if current_user.validate_and_consume_otp!(confirmation_params[:code])
|
||||||
current_user.otp_required_for_login = true
|
current_user.otp_required_for_login = true
|
||||||
|
@codes = current_user.generate_otp_backup_codes!
|
||||||
current_user.save!
|
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
|
else
|
||||||
@confirmation = Form::TwoFactorConfirmation.new
|
@confirmation = Form::TwoFactorConfirmation.new
|
||||||
set_qr_code
|
set_qr_code
|
||||||
|
@ -30,6 +30,12 @@ class Settings::TwoFactorAuthsController < ApplicationController
|
||||||
end
|
end
|
||||||
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
|
def disable
|
||||||
current_user.otp_required_for_login = false
|
current_user.otp_required_for_login = false
|
||||||
current_user.save!
|
current_user.save!
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Admin::AccountsHelper
|
module Admin::FilterHelper
|
||||||
def filter_params(more_params)
|
ACCOUNT_FILTERS = %i[local remote by_domain silenced suspended recent].freeze
|
||||||
params.permit(:local, :remote, :by_domain, :silenced, :suspended, :recent).merge(more_params)
|
REPORT_FILTERS = %i[resolved].freeze
|
||||||
end
|
|
||||||
|
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS
|
||||||
|
|
||||||
def filter_link_to(text, more_params)
|
def filter_link_to(text, more_params)
|
||||||
new_url = filtered_url_for(more_params)
|
new_url = filtered_url_for(more_params)
|
||||||
|
@ -16,6 +17,10 @@ module Admin::AccountsHelper
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def filter_params(more_params)
|
||||||
|
params.permit(FILTERS).merge(more_params)
|
||||||
|
end
|
||||||
|
|
||||||
def filter_link_class(new_url)
|
def filter_link_class(new_url)
|
||||||
filtered_url_for(params) == new_url ? 'selected' : ''
|
filtered_url_for(params) == new_url ? 'selected' : ''
|
||||||
end
|
end
|
|
@ -4,4 +4,8 @@ module ApplicationHelper
|
||||||
def active_nav_class(path)
|
def active_nav_class(path)
|
||||||
current_page?(path) ? 'active' : ''
|
current_page?(path) ? 'active' : ''
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show_landing_strip?
|
||||||
|
!user_signed_in? && !single_user_mode?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
# 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,6 +3,8 @@
|
||||||
class AtomSerializer
|
class AtomSerializer
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
|
INVALID_XML_CHARS = /[^\u0009\u000a\u000d\u0020-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]/
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def render(element)
|
def render(element)
|
||||||
document = Ox::Document.new(version: '1.0')
|
document = Ox::Document.new(version: '1.0')
|
||||||
|
@ -39,7 +41,7 @@ class AtomSerializer
|
||||||
add_namespaces(feed)
|
add_namespaces(feed)
|
||||||
|
|
||||||
append_element(feed, 'id', account_url(account, format: 'atom'))
|
append_element(feed, 'id', account_url(account, format: 'atom'))
|
||||||
append_element(feed, 'title', account.display_name)
|
append_element(feed, 'title', account.display_name.presence || account.username)
|
||||||
append_element(feed, 'subtitle', account.note)
|
append_element(feed, 'subtitle', account.note)
|
||||||
append_element(feed, 'updated', account.updated_at.iso8601)
|
append_element(feed, 'updated', account.updated_at.iso8601)
|
||||||
append_element(feed, 'logo', full_asset_url(account.avatar.url(:original)))
|
append_element(feed, 'logo', full_asset_url(account.avatar.url(:original)))
|
||||||
|
@ -311,11 +313,15 @@ class AtomSerializer
|
||||||
|
|
||||||
def append_element(parent, name, content = nil, attributes = {})
|
def append_element(parent, name, content = nil, attributes = {})
|
||||||
element = Ox::Element.new(name)
|
element = Ox::Element.new(name)
|
||||||
attributes.each { |k, v| element[k] = v.to_s }
|
attributes.each { |k, v| element[k] = sanitize_str(v) }
|
||||||
element << content.to_s unless content.nil?
|
element << sanitize_str(content) unless content.nil?
|
||||||
parent << element
|
parent << element
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sanitize_str(raw_str)
|
||||||
|
raw_str.to_s.gsub(INVALID_XML_CHARS, '')
|
||||||
|
end
|
||||||
|
|
||||||
def add_namespaces(parent)
|
def add_namespaces(parent)
|
||||||
parent['xmlns'] = TagManager::XMLNS
|
parent['xmlns'] = TagManager::XMLNS
|
||||||
parent['xmlns:thr'] = TagManager::THR_XMLNS
|
parent['xmlns:thr'] = TagManager::THR_XMLNS
|
||||||
|
@ -327,8 +333,8 @@ class AtomSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def serialize_status_attributes(entry, status)
|
def serialize_status_attributes(entry, status)
|
||||||
append_element(entry, 'summary', status.spoiler_text) if status.spoiler_text?
|
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')
|
append_element(entry, 'content', Formatter.instance.format(status.proper).to_str, type: 'html', 'xml:lang': status.language)
|
||||||
|
|
||||||
status.mentions.each do |mentioned|
|
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))
|
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 = status.text
|
||||||
html = encode(html)
|
html = encode(html)
|
||||||
html = simple_format(html, {}, sanitize: false)
|
html = simple_format(html, {}, sanitize: false)
|
||||||
html = html.gsub(/\n/, '')
|
html = html.delete("\n")
|
||||||
html = link_urls(html)
|
html = link_urls(html)
|
||||||
html = link_mentions(html, status.mentions)
|
html = link_mentions(html, status.mentions)
|
||||||
html = link_hashtags(html)
|
html = link_hashtags(html)
|
||||||
|
|
|
@ -56,6 +56,10 @@ class TagManager
|
||||||
id.start_with?("tag:#{Rails.configuration.x.local_domain}")
|
id.start_with?("tag:#{Rails.configuration.x.local_domain}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def web_domain?(domain)
|
||||||
|
domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.web_domain).zero?
|
||||||
|
end
|
||||||
|
|
||||||
def local_domain?(domain)
|
def local_domain?(domain)
|
||||||
domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.local_domain).zero?
|
domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.local_domain).zero?
|
||||||
end
|
end
|
||||||
|
|
|
@ -59,7 +59,12 @@ class NotificationMailer < ApplicationMailer
|
||||||
return if @notifications.empty?
|
return if @notifications.empty?
|
||||||
|
|
||||||
I18n.with_locale(@me.user.locale || I18n.default_locale) do
|
I18n.with_locale(@me.user.locale || I18n.default_locale) do
|
||||||
mail to: @me.user.email, subject: I18n.t('notification_mailer.digest.subject', count: @notifications.size)
|
mail to: @me.user.email,
|
||||||
|
subject: I18n.t(
|
||||||
|
:subject,
|
||||||
|
scope: [:notification_mailer, :digest],
|
||||||
|
count: @notifications.size
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,8 +3,13 @@
|
||||||
class DomainBlock < ApplicationRecord
|
class DomainBlock < ApplicationRecord
|
||||||
enum severity: [:silence, :suspend]
|
enum severity: [:silence, :suspend]
|
||||||
|
|
||||||
|
attr_accessor :retroactive
|
||||||
|
|
||||||
validates :domain, presence: true, uniqueness: true
|
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)
|
def self.blocked?(domain)
|
||||||
where(domain: domain, severity: :suspend).exists?
|
where(domain: domain, severity: :suspend).exists?
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Import < ApplicationRecord
|
class Import < ApplicationRecord
|
||||||
|
FILE_TYPES = ['text/plain', 'text/csv'].freeze
|
||||||
|
|
||||||
self.inheritance_column = false
|
self.inheritance_column = false
|
||||||
|
|
||||||
|
belongs_to :account, required: true
|
||||||
|
|
||||||
enum type: [:following, :blocking, :muting]
|
enum type: [:following, :blocking, :muting]
|
||||||
|
|
||||||
belongs_to :account
|
validates :type, presence: true
|
||||||
|
|
||||||
FILE_TYPES = ['text/plain', 'text/csv'].freeze
|
|
||||||
|
|
||||||
has_attached_file :data, url: '/system/:hash.:extension', hash_secret: ENV['PAPERCLIP_SECRET']
|
has_attached_file :data, url: '/system/:hash.:extension', hash_secret: ENV['PAPERCLIP_SECRET']
|
||||||
validates_attachment_content_type :data, content_type: FILE_TYPES
|
validates_attachment_content_type :data, content_type: FILE_TYPES
|
||||||
|
|
|
@ -7,4 +7,8 @@ class Report < ApplicationRecord
|
||||||
|
|
||||||
scope :unresolved, -> { where(action_taken: false) }
|
scope :unresolved, -> { where(action_taken: false) }
|
||||||
scope :resolved, -> { where(action_taken: true) }
|
scope :resolved, -> { where(action_taken: true) }
|
||||||
|
|
||||||
|
def statuses
|
||||||
|
Status.where(id: status_ids)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -110,6 +110,10 @@ class Status < ApplicationRecord
|
||||||
results
|
results
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def non_sensitive_with_media?
|
||||||
|
!sensitive? && media_attachments.any?
|
||||||
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def as_home_timeline(account)
|
def as_home_timeline(account)
|
||||||
where(account: [account] + account.following)
|
where(account: [account] + account.following)
|
||||||
|
|
|
@ -5,7 +5,9 @@ class User < ApplicationRecord
|
||||||
|
|
||||||
devise :registerable, :recoverable,
|
devise :registerable, :recoverable,
|
||||||
:rememberable, :trackable, :validatable, :confirmable,
|
:rememberable, :trackable, :validatable, :confirmable,
|
||||||
:two_factor_authenticatable, otp_secret_encryption_key: ENV['OTP_SECRET']
|
:two_factor_authenticatable, :two_factor_backupable,
|
||||||
|
otp_secret_encryption_key: ENV['OTP_SECRET'],
|
||||||
|
otp_number_of_backup_codes: 10
|
||||||
|
|
||||||
belongs_to :account, inverse_of: :user
|
belongs_to :account, inverse_of: :user
|
||||||
accepts_nested_attributes_for :account
|
accepts_nested_attributes_for :account
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue