Compare commits

..

29 commits

Author SHA1 Message Date
Eugen Rochko 1ebcdf84b5 Merge branch 'master' into skylight 2017-04-13 22:28:33 +02:00
Eugen Rochko 791c0edfe8 Merge branch 'master' into skylight 2017-04-13 22:22:26 +02:00
Eugen Rochko 17c2ba7366 Merge branch 'master' into skylight 2017-04-13 21:55:13 +02:00
Eugen Rochko f4ab6b9f2c Merge branch 'master' into skylight 2017-04-12 21:12:07 +02:00
Eugen Rochko f63ccd8787 Change deploy 2017-04-12 18:25:34 +02:00
Eugen Rochko 7a1903cdf7 Merge branch 'master' into skylight 2017-04-12 11:28:37 +02:00
Eugen Rochko 4142036f2b Merge branch 'master' into skylight 2017-04-09 21:02:59 +02:00
Eugen Rochko 24656a9f8f Merge branch 'master' into skylight 2017-04-09 19:14:08 +02:00
Eugen Rochko 47910f57da Merge branch 'master' into skylight 2017-04-09 15:01:17 +02:00
Eugen Rochko 2a4499bb23 Merge branch 'master' into skylight 2017-04-08 22:27:03 +02:00
Eugen Rochko e258d05848 Merge branch 'master' into skylight 2017-04-08 13:27:04 +02:00
Eugen Rochko 7c81c0f7cb Merge branch 'master' into skylight 2017-04-08 13:24:25 +02:00
Eugen Rochko 1ec81ccec0 Merge branch 'master' into skylight 2017-04-07 13:09:25 +02:00
Eugen Rochko 3fa8505769 Merge branch 'master' into skylight 2017-04-07 12:25:10 +02:00
Eugen Rochko 011bcbf21f Merge branch 'feature-ox-fix' into skylight 2017-04-07 11:09:29 +02:00
Eugen Rochko 6164662e1f Force UTF8 encoding on generated XML 2017-04-07 11:05:36 +02:00
Eugen Rochko ed4d2d8a69 Merge branch 'master' into skylight 2017-04-07 05:59:59 +02:00
Eugen Rochko 453c15ab76 Merge branch 'master' into skylight 2017-04-07 00:06:58 +02:00
Eugen Rochko 2dc2003b99 Disable prepared_statements in production to allow use of pgbouncer 2017-04-06 20:50:13 +02:00
Eugen Rochko 4d1217d8f9 Merge branch 'master' into skylight 2017-04-06 20:50:05 +02:00
Eugen Rochko 0468256ab0 Merge branch 'master' into skylight 2017-04-06 02:27:36 +02:00
Eugen Rochko 634110d8bd Move ProcessingWorker/SalmonWorker into pull queue 2017-04-06 00:21:00 +02:00
Eugen Rochko 40d390e85d Merge branch 'master' into skylight 2017-04-05 21:44:22 +02:00
Eugen Rochko 31e086de0e Merge branch 'master' into skylight 2017-04-05 20:00:21 +02:00
Eugen Rochko 196e44b30b Merge branch 'master' into skylight 2017-04-05 14:29:51 +02:00
Eugen Rochko 945e5bf9cc Merge branch 'master' into skylight 2017-04-05 13:34:46 +02:00
Eugen Rochko a24d55eed8 Add sidekiq-skylight 2017-04-05 02:36:06 +02:00
Eugen Rochko 7f7a67a7c1 Merge branch 'master' into skylight 2017-04-05 02:34:54 +02:00
Eugen Rochko 300bc70123 Add Skylight 2017-04-04 22:32:22 +02:00
224 changed files with 889 additions and 4236 deletions

View file

@ -1,6 +1,12 @@
engines:
duplication:
enabled: false
enabled: true
exclude_paths:
- app/assets/javascripts/components/locales/
config:
languages:
- ruby
- javascript
rubocop:
enabled: true
eslint:

View file

@ -6,6 +6,3 @@ node_modules
storybook
neo4j
vendor/bundle
.DS_Store
*.swp
*~

View file

@ -1,7 +1,6 @@
# Service dependencies
REDIS_HOST=redis
REDIS_PORT=6379
# REDIS_DB=0
DB_HOST=db
DB_USER=postgres
DB_NAME=postgres
@ -12,10 +11,6 @@ DB_PORT=5432
LOCAL_DOMAIN=example.com
LOCAL_HTTPS=true
# Use this only if you need to run mastodon on a different domain than the one used for federation.
# Do not use this unless you know exactly what you are doing.
# WEB_DOMAIN=mastodon.example.com
# Application secrets
# Generate each with the `rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
PAPERCLIP_SECRET=
@ -46,10 +41,6 @@ SMTP_FROM_ADDRESS=notifications@example.com
#SMTP_ENABLE_STARTTLS_AUTO=true
# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files.
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system
# PAPERCLIP_ROOT_URL=/system
# Optional asset host for multi-server setups
# CDN_HOST=assets.example.com

View file

@ -1,4 +1,3 @@
# Federation
LOCAL_DOMAIN=cb6e6126.ngrok.io
LOCAL_HTTPS=true
OTP_SECRET=100c7faeef00caa29242f6b04156742bf76065771fd4117990c4282b8748ff3d99f8fdae97c982ab5bd2e6756a159121377cce4421f4a8ecd2d67bd7749a3fb4

View file

@ -8,8 +8,7 @@
"parser": "babel-eslint",
"plugins": [
"react",
"jsx-a11y"
"react"
],
"parserOptions": {
@ -44,36 +43,9 @@
"no-mixed-spaces-and-tabs": 1,
"no-nested-ternary": 1,
"no-trailing-spaces": 1,
"react/jsx-wrap-multilines": 2,
"react/wrap-multilines": 2,
"react/self-closing-comp": 2,
"react/prop-types": 2,
"react/no-multi-comp": 0,
"jsx-a11y/accessible-emoji": 1,
"jsx-a11y/anchor-has-content": 1,
"jsx-a11y/aria-activedescendant-has-tabindex": 1,
"jsx-a11y/aria-props": 1,
"jsx-a11y/aria-proptypes": 1,
"jsx-a11y/aria-role": 1,
"jsx-a11y/aria-unsupported-elements": 1,
"jsx-a11y/heading-has-content": 1,
"jsx-a11y/href-no-hash": 1,
"jsx-a11y/html-has-lang": 1,
"jsx-a11y/iframe-has-title": 1,
"jsx-a11y/img-has-alt": 1,
"jsx-a11y/img-redundant-alt": 1,
"jsx-a11y/label-has-for": 1,
"jsx-a11y/mouse-events-have-key-events": 1,
"jsx-a11y/no-access-key": 1,
"jsx-a11y/no-distracting-elements": 1,
"jsx-a11y/no-onchange": 1,
"jsx-a11y/no-redundant-roles": 1,
"jsx-a11y/onclick-has-focus": 1,
"jsx-a11y/onclick-has-role": 1,
"jsx-a11y/role-has-required-aria-props": 1,
"jsx-a11y/role-supports-aria-props": 1,
"jsx-a11y/scope": 1,
"jsx-a11y/tabindex-no-positive": 1
"react/no-multi-comp": 0
}
}

8
.gitignore vendored
View file

@ -29,16 +29,10 @@ neo4j/
# Ignore Capistrano customizations
config/deploy/*
# Ignore IDE files
.vscode/
# Ignore postgres + redis volume optionally created by docker-compose
postgres
redis
# Ignore Apple files
.DS_Store
# Ignore vim files
*~
*.swp

2
.nvmrc
View file

@ -1 +1 @@
6
6.7.0

View file

@ -5,6 +5,8 @@ notifications:
email: false
env:
matrix:
- TRAVIS_NODE_VERSION="4"
global:
- LOCAL_DOMAIN=cb6e6126.ngrok.io
- LOCAL_HTTPS=true
@ -14,7 +16,6 @@ addons:
postgresql: 9.4
rvm:
- 2.3.4
- 2.4.1
services:
@ -27,7 +28,8 @@ before_install:
- sudo apt-get -qq update
- sudo apt-get -qq install g++-4.8
install:
- nvm install
- nvm install $TRAVIS_NODE_VERSION
- npm install -g npm@3
- npm install -g yarn
- bundle install
- yarn install
@ -38,4 +40,3 @@ before_script:
script:
- bundle exec rspec
- npm test
- i18n-tasks unused

View file

@ -12,25 +12,23 @@ WORKDIR /mastodon
COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \
&& BUILD_DEPS=" \
RUN BUILD_DEPS=" \
postgresql-dev \
libxml2-dev \
libxslt-dev \
build-base" \
&& apk -U upgrade && apk add \
$BUILD_DEPS \
nodejs@edge \
nodejs-npm@edge \
nodejs \
libpq \
libxml2 \
libxslt \
ffmpeg \
file \
imagemagick@edge \
imagemagick \
&& npm install -g npm@3 && npm install -g yarn \
&& bundle install --deployment --without test development \
&& yarn --ignore-optional \
&& yarn \
&& yarn cache clean \
&& npm -g cache clean \
&& apk del $BUILD_DEPS \

18
Gemfile
View file

@ -1,13 +1,12 @@
# frozen_string_literal: true
source 'https://rubygems.org'
ruby '>= 2.3.0', '< 2.5.0'
gem 'pkg-config'
ruby '2.4.1'
gem 'rails', '~> 5.0.2'
gem 'sass-rails', '~> 5.0'
gem 'uglifier', '>= 1.3.0'
gem 'coffee-rails', '~> 4.1.0'
gem 'jquery-rails'
gem 'puma'
@ -21,7 +20,6 @@ gem 'best_in_place', '~> 3.0.1'
gem 'paperclip', '~> 5.1'
gem 'paperclip-av-transcoder'
gem 'aws-sdk', '>= 2.0'
gem 'fog'
gem 'addressable'
gem 'devise'
@ -38,13 +36,12 @@ gem 'kaminari'
gem 'link_header'
gem 'nokogiri'
gem 'oj'
gem 'ostatus2', '~> 1.1'
gem 'ostatus2'
gem 'ox'
gem 'rabl'
gem 'rack-attack'
gem 'rack-cors', require: 'rack/cors'
gem 'rack-timeout'
gem 'rails-i18n'
gem 'rails-settings-cached'
gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
gem 'rqrcode'
@ -53,16 +50,17 @@ gem 'sidekiq'
gem 'sidekiq-unique-jobs'
gem 'simple-navigation'
gem 'simple_form'
gem 'sprockets-rails', :require => 'sprockets/railtie'
gem 'statsd-instrument'
gem 'twitter-text'
gem 'tzinfo-data'
gem 'whatlanguage'
gem 'react-rails'
gem 'browserify-rails'
gem 'autoprefixer-rails'
gem 'skylight'
gem 'sidekiq-skylight'
group :development, :test do
gem 'rspec-rails'
gem 'pry-rails'
@ -72,9 +70,7 @@ group :development, :test do
end
group :test do
gem 'capybara'
gem 'faker'
gem 'microformats2'
gem 'rails-controller-testing'
gem 'rspec-sidekiq'
gem 'simplecov', require: false
@ -90,7 +86,7 @@ group :development do
gem 'bullet'
gem 'active_record_query_trace'
gem 'capistrano', '3.8.0'
gem 'capistrano'
gem 'capistrano-rails'
gem 'capistrano-rbenv'
gem 'capistrano-yarn'

View file

@ -1,7 +1,6 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (2.3.5)
actioncable (5.0.2)
actionpack (= 5.0.2)
nio4r (>= 1.2, < 3.0)
@ -42,7 +41,7 @@ GEM
tzinfo (~> 1.1)
addressable (2.5.1)
public_suffix (~> 2.0, >= 2.0.2)
airbrussh (1.2.0)
airbrussh (1.1.2)
sshkit (>= 1.6.1, != 1.7.0)
arel (7.1.4)
ast (2.3.0)
@ -100,18 +99,18 @@ GEM
sshkit (~> 1.3)
capistrano-yarn (2.0.2)
capistrano (~> 3.0)
capybara (2.13.0)
addressable
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
chunky_png (1.3.8)
climate_control (0.1.0)
cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0)
coderay (1.1.1)
coffee-rails (4.1.1)
coffee-script (>= 2.2.0)
railties (>= 4.0.0, < 5.1.x)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.12.2)
colorize (0.8.1)
concurrent-ruby (1.0.5)
connection_pool (2.2.1)
@ -146,142 +145,13 @@ GEM
thread_safe
encryptor (3.0.0)
erubis (2.7.0)
excon (0.55.0)
execjs (2.7.0)
fabrication (2.16.1)
faker (1.7.3)
i18n (~> 0.5)
fast_blank (1.0.0)
fission (0.5.0)
CFPropertyList (~> 2.2)
fog (1.38.0)
fog-aliyun (>= 0.1.0)
fog-atmos
fog-aws (>= 0.6.0)
fog-brightbox (~> 0.4)
fog-cloudatcost (~> 0.1.0)
fog-core (~> 1.32)
fog-dynect (~> 0.0.2)
fog-ecloud (~> 0.1)
fog-google (<= 0.1.0)
fog-json
fog-local
fog-openstack
fog-powerdns (>= 0.1.1)
fog-profitbricks
fog-rackspace
fog-radosgw (>= 0.0.2)
fog-riakcs
fog-sakuracloud (>= 0.0.4)
fog-serverlove
fog-softlayer
fog-storm_on_demand
fog-terremark
fog-vmfusion
fog-voxel
fog-vsphere (>= 0.4.0)
fog-xenserver
fog-xml (~> 0.1.1)
ipaddress (~> 0.5)
fog-aliyun (0.1.0)
fog-core (~> 1.27)
fog-json (~> 1.0)
ipaddress (~> 0.8)
xml-simple (~> 1.1)
fog-atmos (0.1.0)
fog-core
fog-xml
fog-aws (1.3.0)
fog-core (~> 1.38)
fog-json (~> 1.0)
fog-xml (~> 0.1)
ipaddress (~> 0.8)
fog-brightbox (0.11.0)
fog-core (~> 1.22)
fog-json
inflecto (~> 0.0.2)
fog-cloudatcost (0.1.2)
fog-core (~> 1.36)
fog-json (~> 1.0)
fog-xml (~> 0.1)
ipaddress (~> 0.8)
fog-core (1.43.0)
builder
excon (~> 0.49)
formatador (~> 0.2)
fog-dynect (0.0.3)
fog-core
fog-json
fog-xml
fog-ecloud (0.3.0)
fog-core
fog-xml
fog-google (0.1.0)
fog-core
fog-json
fog-xml
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
fog-local (0.3.1)
fog-core (~> 1.27)
fog-openstack (0.1.20)
fog-core (>= 1.40)
fog-json (>= 1.0)
ipaddress (>= 0.8)
fog-powerdns (0.1.1)
fog-core (~> 1.27)
fog-json (~> 1.0)
fog-xml (~> 0.1)
fog-profitbricks (3.0.0)
fog-core (~> 1.42)
fog-json (~> 1.0)
fog-rackspace (0.1.4)
fog-core (>= 1.35)
fog-json (>= 1.0)
fog-xml (>= 0.1)
ipaddress (>= 0.8)
fog-radosgw (0.0.5)
fog-core (>= 1.21.0)
fog-json
fog-xml (>= 0.0.1)
fog-riakcs (0.1.0)
fog-core
fog-json
fog-xml
fog-sakuracloud (1.7.5)
fog-core
fog-json
fog-serverlove (0.1.2)
fog-core
fog-json
fog-softlayer (1.1.4)
fog-core
fog-json
fog-storm_on_demand (0.1.1)
fog-core
fog-json
fog-terremark (0.1.0)
fog-core
fog-xml
fog-vmfusion (0.1.0)
fission
fog-core
fog-voxel (0.1.0)
fog-core
fog-xml
fog-vsphere (1.9.1)
fog-core
rbvmomi (~> 1.9)
fog-xenserver (0.3.0)
fog-core
fog-xml
fog-xml (0.1.3)
fog-core
nokogiri (>= 1.5.11, < 2.0.0)
font-awesome-rails (4.7.0.1)
railties (>= 3.2, < 5.1)
formatador (0.2.5)
fuubar (2.2.0)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
@ -327,8 +197,6 @@ GEM
parser (>= 2.2.3.0)
rainbow (~> 2.2)
terminal-table (>= 1.5.1)
inflecto (0.0.2)
ipaddress (0.8.3)
jmespath (1.3.1)
jquery-rails (4.3.1)
rails-dom-testing (>= 1, < 3)
@ -365,17 +233,12 @@ GEM
mail (2.6.4)
mime-types (>= 1.16, < 4)
method_source (0.8.2)
microformats2 (2.1.0)
activesupport
json
nokogiri
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mimemagic (0.3.2)
mini_portile2 (2.1.0)
minitest (5.10.1)
multi_json (1.12.1)
net-scp (1.2.1)
net-ssh (>= 2.6.5)
net-ssh (4.1.0)
@ -383,13 +246,11 @@ GEM
nokogiri (1.7.1)
mini_portile2 (~> 2.1.0)
oj (2.18.5)
openssl (2.0.3)
orm_adapter (0.5.0)
ostatus2 (1.1.0)
ostatus2 (1.0.2)
addressable (~> 2.4)
http (~> 2.0)
nokogiri (~> 1.6)
openssl (~> 2.0)
ox (2.4.11)
paperclip (5.1.0)
activemodel (>= 4.2.0)
@ -405,7 +266,6 @@ GEM
pg (0.20.0)
pghero (1.6.4)
activerecord
pkg-config (1.1.7)
powerpack (0.1.1)
pry (0.10.4)
coderay (~> 1.1.0)
@ -447,9 +307,6 @@ GEM
nokogiri (~> 1.6)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
rails-i18n (5.0.3)
i18n (~> 0.7)
railties (~> 5.0)
rails-settings-cached (0.6.5)
rails (>= 4.2.0)
rails_12factor (0.0.3)
@ -465,11 +322,6 @@ GEM
thor (>= 0.18.1, < 2.0)
rainbow (2.2.1)
rake (12.0.0)
rbvmomi (1.11.0)
builder (~> 3.0)
json (>= 1.8)
nokogiri (~> 1.5)
trollop (~> 2.1)
react-rails (1.11.0)
babel-transpiler (>= 0.7.0)
connection_pool
@ -539,6 +391,9 @@ GEM
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
redis (~> 3.2, >= 3.2.1)
sidekiq-skylight (0.2.0)
sidekiq (>= 3.3.0)
skylight (>= 0.5.2)
sidekiq-unique-jobs (5.0.0)
sidekiq (>= 4.0)
thor
@ -552,6 +407,8 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
skylight (1.2.0)
activesupport (>= 3.0.0)
slop (3.6.0)
sprockets (3.7.1)
concurrent-ruby (~> 1.0)
@ -571,7 +428,6 @@ GEM
thread (0.2.2)
thread_safe (0.3.6)
tilt (2.0.7)
trollop (2.1.2)
twitter-text (1.14.5)
unf (~> 0.1.0)
tzinfo (1.2.3)
@ -582,7 +438,7 @@ GEM
execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.3)
unf_ext (0.0.7.2)
unicode-display_width (1.1.3)
uniform_notifier (1.10.0)
warden (1.2.7)
@ -594,10 +450,6 @@ GEM
websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
whatlanguage (1.0.6)
xml-simple (1.1.5)
xpath (2.0.0)
nokogiri (~> 1.3)
PLATFORMS
ruby
@ -612,12 +464,12 @@ DEPENDENCIES
binding_of_caller
browserify-rails
bullet
capistrano (= 3.8.0)
capistrano
capistrano-faster-assets (~> 1.0)
capistrano-rails
capistrano-rbenv
capistrano-yarn
capybara
coffee-rails (~> 4.1.0)
devise
devise-two-factor
doorkeeper
@ -625,7 +477,6 @@ DEPENDENCIES
fabrication
faker
fast_blank
fog
font-awesome-rails
fuubar
goldfinger
@ -642,16 +493,14 @@ DEPENDENCIES
letter_opener_web
link_header
lograge
microformats2
nokogiri
oj
ostatus2 (~> 1.1)
ostatus2
ox
paperclip (~> 5.1)
paperclip-av-transcoder
pg
pghero
pkg-config
pry-rails
puma
rabl
@ -660,7 +509,6 @@ DEPENDENCIES
rack-timeout
rails (~> 5.0.2)
rails-controller-testing
rails-i18n
rails-settings-cached
rails_12factor
react-rails
@ -673,17 +521,17 @@ DEPENDENCIES
ruby-oembed
sass-rails (~> 5.0)
sidekiq
sidekiq-skylight
sidekiq-unique-jobs
simple-navigation
simple_form
simplecov
sprockets-rails
skylight
statsd-instrument
twitter-text
tzinfo-data
uglifier (>= 1.3.0)
webmock
whatlanguage
RUBY VERSION
ruby 2.4.1p111

View file

@ -3,4 +3,3 @@
* * * *
- [ ] I searched or browsed the repos 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).

View file

@ -48,14 +48,6 @@ If you would like, you can [support the development of this project on Patreon][
- **Deployable via Docker**
You don't need to mess with dependencies and configuration if you want to try Mastodon, if you have Docker and Docker Compose the deployment is extremely easy
## Checking out
If you want a stable release for production use, you should use tagged releases. To checkout the latest available tagged version:
git clone https://github.com/tootsuite/mastodon.git
cd mastodon
git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
## Configuration
- `LOCAL_DOMAIN` should be the domain/hostname of your instance. This is **absolutely required** as it is used for generating unique IDs for everything federation-related
@ -80,6 +72,10 @@ The project now includes a `Dockerfile` and a `docker-compose.yml` file (which r
Review the settings in `docker-compose.yml`. Note that it is not default to store the postgresql database and redis databases in a persistent storage location,
so you may need or want to adjust the settings there.
Before running the first time, you need to build the images:
docker-compose build
Then, you need to fill in the `.env.production` file:
cp .env.production.sample .env.production
@ -89,11 +85,6 @@ Do NOT change the `REDIS_*` or `DB_*` settings when running with the default doc
You will need to fill in, at least: `LOCAL_DOMAIN`, `LOCAL_HTTPS`, `PAPERCLIP_SECRET`, `SECRET_KEY_BASE`, `OTP_SECRET`, and the `SMTP_*` settings. To generate the `PAPERCLIP_SECRET`, `SECRET_KEY_BASE`, and `OTP_SECRET`, you may use:
Before running the first time, you need to build the images:
docker-compose build
docker-compose run --rm web rake secret
Do this once for each of those keys, and copy the result into the `.env.production` file in the appropriate field.

2
Vagrantfile vendored
View file

@ -43,7 +43,7 @@ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
export PATH="$HOME/.rbenv/bin:$PATH"
export PATH="$HOME/.rbenv/bin::$PATH"
eval "$(rbenv init -)"
cd /vagrant

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View file

@ -1,82 +0,0 @@
import api, { getLinks } from '../api'
import { fetchRelationships } from './accounts';
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
export const MUTES_FETCH_FAIL = 'MUTES_FETCH_FAIL';
export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
export function fetchMutes() {
return (dispatch, getState) => {
dispatch(fetchMutesRequest());
api(getState).get('/api/v1/mutes').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(fetchMutesSuccess(response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => dispatch(fetchMutesFail(error)));
};
};
export function fetchMutesRequest() {
return {
type: MUTES_FETCH_REQUEST
};
};
export function fetchMutesSuccess(accounts, next) {
return {
type: MUTES_FETCH_SUCCESS,
accounts,
next
};
};
export function fetchMutesFail(error) {
return {
type: MUTES_FETCH_FAIL,
error
};
};
export function expandMutes() {
return (dispatch, getState) => {
const url = getState().getIn(['user_lists', 'mutes', 'next']);
if (url === null) {
return;
}
dispatch(expandMutesRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(expandMutesSuccess(response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => dispatch(expandMutesFail(error)));
};
};
export function expandMutesRequest() {
return {
type: MUTES_EXPAND_REQUEST
};
};
export function expandMutesSuccess(accounts, next) {
return {
type: MUTES_EXPAND_SUCCESS,
accounts,
next
};
};
export function expandMutesFail(error) {
return {
type: MUTES_EXPAND_FAIL,
error
};
};

View file

@ -1,14 +0,0 @@
import { openModal } from './modal';
import { changeSetting, saveSettings } from './settings';
export function showOnboardingOnce() {
return (dispatch, getState) => {
const alreadySeen = getState().getIn(['settings', 'onboarded']);
if (!alreadySeen) {
dispatch(openModal('ONBOARDING'));
dispatch(changeSetting(['onboarded'], true));
dispatch(saveSettings());
}
};
};

View file

@ -10,8 +10,7 @@ const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute' }
unblock: { id: 'account.unblock', defaultMessage: 'Unblock' }
});
const buttonsStyle = {
@ -26,7 +25,6 @@ const Account = React.createClass({
me: React.PropTypes.number.isRequired,
onFollow: React.PropTypes.func.isRequired,
onBlock: React.PropTypes.func.isRequired,
onMute: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
},
@ -40,10 +38,6 @@ const Account = React.createClass({
this.props.onBlock(this.props.account);
},
handleMute () {
this.props.onMute(this.props.account);
},
render () {
const { account, me, intl } = this.props;
@ -57,14 +51,11 @@ const Account = React.createClass({
const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']);
const muting = account.getIn(['relationship', 'muting']);
if (requested) {
buttons = <IconButton disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} />
} else if (blocking) {
buttons = <IconButton active={true} icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
} else if (muting) {
buttons = <IconButton active={true} icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />;
buttons = <IconButton active={true} icon='unlock-alt' title={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
} else {
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
}

View file

@ -178,12 +178,7 @@ const AutosuggestTextarea = React.createClass({
<div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'>
{suggestions.map((suggestion, i) => (
<div
role='button'
tabIndex='0'
key={suggestion}
className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`}
onClick={this.onSuggestionClick.bind(this, suggestion)}>
<div key={suggestion} className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`} onClick={this.onSuggestionClick.bind(this, suggestion)}>
<AutosuggestAccountContainer id={suggestion} />
</div>
))}

View file

@ -9,7 +9,6 @@ const Button = React.createClass({
block: React.PropTypes.bool,
secondary: React.PropTypes.bool,
size: React.PropTypes.number,
style: React.PropTypes.object,
children: React.PropTypes.node
},

View file

@ -15,13 +15,13 @@ const ColumnBackButton = React.createClass({
mixins: [PureRenderMixin],
handleClick () {
if (window.history && window.history.length === 1) this.context.router.push("/");
if (window.history && window.history.length == 1) this.context.router.push("/");
else this.context.router.goBack();
},
render () {
return (
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button'>
<div onClick={this.handleClick} className='column-back-button'>
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</div>

View file

@ -31,7 +31,7 @@ const ColumnBackButtonSlim = React.createClass({
render () {
return (
<div style={{ position: 'relative' }}>
<div role='button' tabIndex='0' style={outerStyle} onClick={this.handleClick} className='column-back-button'>
<div style={outerStyle} onClick={this.handleClick} className='column-back-button'>
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</div>

View file

@ -46,9 +46,7 @@ const ColumnCollapsable = React.createClass({
return (
<div style={{ position: 'relative' }}>
<div role='button' tabIndex='0' title={`${title}`} style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}>
<i className={`fa fa-${icon}`} />
</div>
<div title={`${title}`} style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div>
<Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}>
{({ opacity, height }) =>

View file

@ -1,7 +1,7 @@
import { FormattedMessage } from 'react-intl';
const LoadMore = ({ onClick }) => (
<a href="#" className='load-more' role='button' onClick={onClick}>
<a href='#' className='load-more' onClick={onClick}>
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
</a>
);

View file

@ -220,7 +220,7 @@ const MediaGallery = React.createClass({
}
children = (
<div role='button' tabIndex='0' style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
<div style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
<span style={spoilerSpanStyle}>{warning}</span>
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</div>

View file

@ -6,8 +6,7 @@ const Permalink = React.createClass({
propTypes: {
href: React.PropTypes.string.isRequired,
to: React.PropTypes.string.isRequired,
children: React.PropTypes.node
to: React.PropTypes.string.isRequired
},
handleClick (e) {

View file

@ -92,14 +92,10 @@ const StatusActionBar = React.createClass({
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
}
let reblogIcon = 'retweet';
if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
else if (status.get('visibility') === 'private') reblogIcon = 'lock';
return (
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
<div style={{ float: 'left', marginRight: '18px'}}><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
<div style={{ float: 'left', marginRight: '18px'}}><IconButton disabled={status.get('visibility') === 'private' || status.get('visibility') === 'direct'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div style={{ float: 'left', marginRight: '18px'}}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div>
<div style={{ float: 'left', marginRight: '18px'}}><IconButton disabled={status.get('visibility') === 'private' || status.get('visibility') === 'direct'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={status.get('visibility') === 'direct' ? 'envelope' : (status.get('visibility') === 'private' ? 'lock' : 'retweet')} onClick={this.handleReblogClick} /></div>
<div style={{ float: 'left', marginRight: '18px'}}><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>
<div style={{ width: '18px', height: '18px', float: 'left' }}>

View file

@ -44,7 +44,6 @@ const StatusContent = React.createClass({
} else {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener');
link.setAttribute('title', link.href);
}
}
},
@ -119,7 +118,7 @@ const StatusContent = React.createClass({
return (
<div className='status__content' style={{ cursor: 'pointer' }} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<p style={{ marginBottom: hidden && status.get('mentions').size === 0 ? '0px' : '' }} >
<span dangerouslySetInnerHTML={spoilerContent} /> <a tabIndex='0' className='status__content__spoiler-link' role='button' onClick={this.handleSpoilerClick}>{toggleText}</a>
<span dangerouslySetInnerHTML={spoilerContent} /> <a className='status__content__spoiler-link' onClick={this.handleSpoilerClick}>{toggleText}</a>
</p>
{mentionsPlaceholder}

View file

@ -7,8 +7,7 @@ import { isIOS } from '../is_mobile';
const messages = defineMessages({
toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' },
toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' },
expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' },
expand_video: { id: 'video_player.video_error', defaultMessage: 'Video could not be played' }
expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' }
});
const videoStyle = {
@ -31,7 +30,7 @@ const muteStyle = {
zIndex: '5'
};
const coverStyle = {
const spoilerStyle = {
marginTop: '8px',
textAlign: 'center',
height: '100%',
@ -95,8 +94,7 @@ const VideoPlayer = React.createClass({
visible: !this.props.sensitive,
preview: true,
muted: true,
hasAudio: true,
videoError: false
hasAudio: true
};
},
@ -144,17 +142,12 @@ const VideoPlayer = React.createClass({
}
},
handleVideoError () {
this.setState({ videoError: true });
},
componentDidMount () {
if (!this.video) {
return;
}
this.video.addEventListener('loadeddata', this.handleLoadedData);
this.video.addEventListener('error', this.handleVideoError);
},
componentDidUpdate () {
@ -163,7 +156,6 @@ const VideoPlayer = React.createClass({
}
this.video.addEventListener('loadeddata', this.handleLoadedData);
this.video.addEventListener('error', this.handleVideoError);
},
componentWillUnmount () {
@ -172,7 +164,6 @@ const VideoPlayer = React.createClass({
}
this.video.removeEventListener('loadeddata', this.handleLoadedData);
this.video.removeEventListener('error', this.handleVideoError);
},
render () {
@ -203,7 +194,7 @@ const VideoPlayer = React.createClass({
if (!this.state.visible) {
if (sensitive) {
return (
<div role='button' tabIndex='0' style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
<div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
{spoilerButton}
<span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
@ -211,7 +202,7 @@ const VideoPlayer = React.createClass({
);
} else {
return (
<div role='button' tabIndex='0' style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
<div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
{spoilerButton}
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
@ -222,27 +213,19 @@ const VideoPlayer = React.createClass({
if (this.state.preview && !autoplay) {
return (
<div role='button' tabIndex='0' style={{ cursor: 'pointer', position: 'relative', marginTop: '8px', width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }} onClick={this.handleOpen}>
<div style={{ cursor: 'pointer', position: 'relative', marginTop: '8px', width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }} onClick={this.handleOpen}>
{spoilerButton}
<div style={{ position: 'absolute', top: '50%', left: '50%', fontSize: '36px', transform: 'translate(-50%, -50%)', padding: '5px', borderRadius: '100px', color: 'rgba(255, 255, 255, 0.8)' }}><i className='fa fa-play' /></div>
</div>
);
}
if (this.state.videoError) {
return (
<div style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='video-error-cover' >
<span style={spoilerSpanStyle}><FormattedMessage id='video_player.video_error' defaultMessage='Video could not be played' /></span>
</div>
);
}
return (
<div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
{spoilerButton}
{muteButton}
{expandButton}
<video role='button' tabIndex='0' ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
<video ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
</div>
);
}

View file

@ -8,7 +8,6 @@ import {
connectTimeline,
disconnectTimeline
} from '../actions/timelines';
import { showOnboardingOnce } from '../actions/onboarding';
import { updateNotifications, refreshNotifications } from '../actions/notifications';
import createBrowserHistory from 'history/lib/createBrowserHistory';
import {
@ -38,7 +37,6 @@ import FollowRequests from '../features/follow_requests';
import GenericNotFound from '../features/generic_not_found';
import FavouritedStatuses from '../features/favourited_statuses';
import Blocks from '../features/blocks';
import Mutes from '../features/mutes';
import Report from '../features/report';
import { IntlProvider, addLocaleData } from 'react-intl';
import en from 'react-intl/locale-data/en';
@ -62,8 +60,8 @@ import { hydrateStore } from '../actions/store';
import createStream from '../stream';
const store = configureStore();
const initialState = JSON.parse(document.getElementById("initial-state").textContent);
store.dispatch(hydrateStore(initialState));
store.dispatch(hydrateStore(window.INITIAL_STATE));
const browserHistory = useRouterHistory(createBrowserHistory)({
basename: '/web'
@ -96,10 +94,9 @@ const Mastodon = React.createClass({
componentDidMount() {
const { locale } = this.props;
const streamingAPIBaseURL = store.getState().getIn(['meta', 'streaming_api_base_url']);
const accessToken = store.getState().getIn(['meta', 'access_token']);
this.subscription = createStream(streamingAPIBaseURL, accessToken, 'user', {
this.subscription = createStream(accessToken, 'user', {
connected () {
store.dispatch(connectTimeline('home'));
@ -135,8 +132,6 @@ const Mastodon = React.createClass({
if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
Notification.requestPermission();
}
store.dispatch(showOnboardingOnce());
},
componentWillUnmount () {
@ -176,7 +171,6 @@ const Mastodon = React.createClass({
<Route path='follow_requests' component={FollowRequests} />
<Route path='blocks' component={Blocks} />
<Route path='mutes' component={Mutes} />
<Route path='report' component={Report} />
<Route path='*' component={GenericNotFound} />

View file

@ -43,16 +43,7 @@ const Avatar = React.createClass({
return (
<Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
{({ radius }) =>
<a
href={account.get('url')}
className='account__header__avatar'
target='_blank'
rel='noopener'
style={{ display: 'block', width: '90px', height: '90px', margin: '0 auto', marginBottom: '10px', borderRadius: `${radius}px`, overflow: 'hidden' }}
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
onFocus={this.handleMouseOver}
onBlur={this.handleMouseOut}>
<a href={account.get('url')} className='account__header__avatar' target='_blank' rel='noopener' style={{ display: 'block', width: '90px', height: '90px', margin: '0 auto', marginBottom: '10px', borderRadius: `${radius}px`, overflow: 'hidden' }} onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
<img src={account.get('avatar')} alt={account.get('acct')} style={{ display: 'block', width: '90px', height: '90px' }} />
</a>
}

View file

@ -19,7 +19,6 @@ const messages = defineMessages({
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0,
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
accessToken: state.getIn(['meta', 'access_token'])
});
@ -30,7 +29,6 @@ const CommunityTimeline = React.createClass({
propTypes: {
dispatch: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired,
streamingAPIBaseURL: React.PropTypes.string.isRequired,
accessToken: React.PropTypes.string.isRequired,
hasUnread: React.PropTypes.bool
},
@ -38,7 +36,7 @@ const CommunityTimeline = React.createClass({
mixins: [PureRenderMixin],
componentDidMount () {
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
const { dispatch, accessToken } = this.props;
dispatch(refreshTimeline('community'));
@ -46,7 +44,7 @@ const CommunityTimeline = React.createClass({
return;
}
subscription = createStream(streamingAPIBaseURL, accessToken, 'public:local', {
subscription = createStream(accessToken, 'public:local', {
connected () {
dispatch(connectTimeline('community'));

View file

@ -83,7 +83,7 @@ const PrivacyDropdown = React.createClass({
<div className='privacy-dropdown__value'><IconButton icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div>
<div className='privacy-dropdown__dropdown'>
{options.map(item =>
<div role='button' tabIndex='0' key={item.value} onClick={this.handleClick.bind(this, item.value)} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
<div key={item.value} onClick={this.handleClick.bind(this, item.value)} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
<div className='privacy-dropdown__option__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div>
<div className='privacy-dropdown__option__content'>
<strong>{item.shortText}</strong>

View file

@ -36,10 +36,6 @@ const Search = React.createClass({
}
},
noop () {
},
handleFocus () {
this.props.onShow();
},
@ -60,9 +56,9 @@ const Search = React.createClass({
onFocus={this.handleFocus}
/>
<div role='button' tabIndex='0' className='search__icon' onClick={hasValue ? this.handleClear : this.noop}>
<div className='search__icon'>
<i className={`fa fa-search ${hasValue ? '' : 'active'}`} />
<i aria-label="Clear search" className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} />
<i className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} onClick={this.handleClear} />
</div>
</div>
);

View file

@ -50,7 +50,7 @@ const Followers = React.createClass({
handleLoadMore (e) {
e.preventDefault();
this.props.dispatch(expandFollowers(Number(this.props.params.accountId)));
this.props.dispatch(expandFollowing(Number(this.props.params.accountId)));
},
render () {

View file

@ -14,7 +14,6 @@ const messages = defineMessages({
sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }
});
@ -38,7 +37,6 @@ const GettingStarted = ({ intl, me }) => {
<ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />
{followRequests}
<ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
<ColumnLink icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />
<ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
</div>

View file

@ -13,7 +13,6 @@ import createStream from '../../stream';
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'tag', 'unread']) > 0,
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
accessToken: state.getIn(['meta', 'access_token'])
});
@ -22,7 +21,6 @@ const HashtagTimeline = React.createClass({
propTypes: {
params: React.PropTypes.object.isRequired,
dispatch: React.PropTypes.func.isRequired,
streamingAPIBaseURL: React.PropTypes.string.isRequired,
accessToken: React.PropTypes.string.isRequired,
hasUnread: React.PropTypes.bool
},
@ -30,9 +28,9 @@ const HashtagTimeline = React.createClass({
mixins: [PureRenderMixin],
_subscribe (dispatch, id) {
const { streamingAPIBaseURL, accessToken } = this.props;
const { accessToken } = this.props;
this.subscription = createStream(streamingAPIBaseURL, accessToken, `hashtag&tag=${id}`, {
this.subscription = createStream(accessToken, `hashtag&tag=${id}`, {
received (data) {
switch(data.event) {

View file

@ -1,68 +0,0 @@
import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { ScrollContainer } from 'react-router-scroll';
import Column from '../ui/components/column';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import AccountContainer from '../../containers/account_container';
import { fetchMutes, expandMutes } from '../../actions/mutes';
import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
heading: { id: 'column.mutes', defaultMessage: 'Muted users' }
});
const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'mutes', 'items'])
});
const Mutes = React.createClass({
propTypes: {
params: React.PropTypes.object.isRequired,
dispatch: React.PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
componentWillMount () {
this.props.dispatch(fetchMutes());
},
handleScroll (e) {
const { scrollTop, scrollHeight, clientHeight } = e.target;
if (scrollTop === scrollHeight - clientHeight) {
this.props.dispatch(expandMutes());
}
},
render () {
const { intl, accountIds } = this.props;
if (!accountIds) {
return (
<Column>
<LoadingIndicator />
</Column>
);
}
return (
<Column icon='users' heading={intl.formatMessage(messages.heading)}>
<ColumnBackButtonSlim />
<ScrollContainer scrollKey='mutes'>
<div className='scrollable' onScroll={this.handleScroll}>
{accountIds.map(id =>
<AccountContainer key={id} id={id} />
)}
</div>
</ScrollContainer>
</Column>
);
}
});
export default connect(mapStateToProps)(injectIntl(Mutes));

View file

@ -15,7 +15,7 @@ const ClearColumnButton = React.createClass({
const { intl } = this.props;
return (
<div role='button' title={intl.formatMessage(messages.clear)} className='column-icon column-icon-clear' tabIndex='0' onClick={this.props.onClick}>
<div title={intl.formatMessage(messages.clear)} className='column-icon column-icon-clear' tabIndex='0' onClick={this.props.onClick}>
<i className='fa fa-eraser' />
</div>
);

View file

@ -27,11 +27,9 @@ const ColumnSettings = React.createClass({
propTypes: {
settings: ImmutablePropTypes.map.isRequired,
intl: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func.isRequired,
onSave: React.PropTypes.func.isRequired,
intl: React.PropTypes.shape({
formatMessage: React.PropTypes.func.isRequired
}).isRequired
},
mixins: [PureRenderMixin],

View file

@ -71,7 +71,7 @@ const Notification = React.createClass({
);
},
render () { // eslint-disable-line consistent-return
render () {
const { notification } = this.props;
const account = notification.get('account');
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');

View file

@ -14,8 +14,8 @@ const labelSpanStyle = {
marginLeft: '8px'
};
const SettingToggle = ({ settings, settingKey, label, onChange, htmlFor = '' }) => (
<label htmlFor={htmlFor} style={labelStyle}>
const SettingToggle = ({ settings, settingKey, label, onChange }) => (
<label style={labelStyle}>
<Toggle checked={settings.getIn(settingKey)} onChange={(e) => onChange(settingKey, e.target.checked)} />
<span className='setting-toggle' style={labelSpanStyle}>{label}</span>
</label>
@ -25,8 +25,7 @@ SettingToggle.propTypes = {
settings: ImmutablePropTypes.map.isRequired,
settingKey: React.PropTypes.array.isRequired,
label: React.PropTypes.node.isRequired,
onChange: React.PropTypes.func.isRequired,
htmlFor: React.PropTypes.string
onChange: React.PropTypes.func.isRequired
};
export default SettingToggle;

View file

@ -19,7 +19,6 @@ const messages = defineMessages({
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0,
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
accessToken: state.getIn(['meta', 'access_token'])
});
@ -30,7 +29,6 @@ const PublicTimeline = React.createClass({
propTypes: {
dispatch: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired,
streamingAPIBaseURL: React.PropTypes.string.isRequired,
accessToken: React.PropTypes.string.isRequired,
hasUnread: React.PropTypes.bool
},
@ -38,7 +36,7 @@ const PublicTimeline = React.createClass({
mixins: [PureRenderMixin],
componentDidMount () {
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
const { dispatch, accessToken } = this.props;
dispatch(refreshTimeline('public'));
@ -46,7 +44,7 @@ const PublicTimeline = React.createClass({
return;
}
subscription = createStream(streamingAPIBaseURL, accessToken, 'public', {
subscription = createStream(accessToken, 'public', {
connected () {
dispatch(connectTimeline('public'));

View file

@ -71,14 +71,10 @@ const ActionBar = React.createClass({
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
}
let reblogIcon = 'retweet';
if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
else if (status.get('visibility') === 'private') reblogIcon = 'lock';
return (
<div className='detailed-status__action-bar'>
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton disabled={status.get('visibility') === 'direct' || status.get('visibility') === 'private'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div>
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton disabled={status.get('visibility') === 'direct' || status.get('visibility') === 'private'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={status.get('visibility') === 'direct' ? 'envelope' : (status.get('visibility') === 'private' ? 'lock' : 'retweet')} onClick={this.handleReblogClick} /></div>
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>
<div style={{ flex: '1 1 auto', textAlign: 'center' }}><DropdownMenu size={18} icon='ellipsis-h' items={menu} direction="left" /></div>
</div>

View file

@ -64,7 +64,7 @@ const Column = React.createClass({
}
return (
<div role='section' className='column' onWheel={this.handleWheel}>
<div className='column' onWheel={this.handleWheel}>
{header}
{children}
</div>

View file

@ -25,7 +25,7 @@ const ColumnHeader = React.createClass({
}
return (
<div role='button' tabIndex='0' aria-label={type} className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick}>
<div className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick}>
{icon}
{type}
</div>

View file

@ -34,8 +34,7 @@ ColumnLink.propTypes = {
icon: React.PropTypes.string.isRequired,
text: React.PropTypes.string.isRequired,
to: React.PropTypes.string,
href: React.PropTypes.string,
method: React.PropTypes.string
href: React.PropTypes.string
};
export default ColumnLink;

View file

@ -104,8 +104,8 @@ const MediaModal = React.createClass({
leftNav = rightNav = content = '';
if (media.size > 1) {
leftNav = <div role='button' tabIndex='0' style={leftNavStyle} className='modal-container__nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
rightNav = <div role='button' tabIndex='0' style={rightNavStyle} className='modal-container__nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
leftNav = <div style={leftNavStyle} className='modal-container__nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
rightNav = <div style={rightNavStyle} className='modal-container__nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
}
if (attachment.get('type') === 'image') {

View file

@ -1,13 +1,11 @@
import PureRenderMixin from 'react-addons-pure-render-mixin';
import MediaModal from './media_modal';
import OnboardingModal from './onboarding_modal';
import VideoModal from './video_modal';
import BoostModal from './boost_modal';
import { TransitionMotion, spring } from 'react-motion';
const MODAL_COMPONENTS = {
'MEDIA': MediaModal,
'ONBOARDING': OnboardingModal,
'VIDEO': VideoModal,
'BOOST': BoostModal
};
@ -68,7 +66,7 @@ const ModalRoot = React.createClass({
return (
<div key={key}>
<div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity, transform: `translateZ(0px)` }} onClick={onClose} />
<div className='modal-root__overlay' style={{ opacity: style.opacity, transform: `translateZ(0px)` }} onClick={onClose} />
<div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
<SpecificComponent {...props} onClose={onClose} />
</div>

View file

@ -1,251 +0,0 @@
import { connect } from 'react-redux';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Permalink from '../../../components/permalink';
import { TransitionMotion, spring } from 'react-motion';
import ComposeForm from '../../compose/components/compose_form';
import Search from '../../compose/components/search';
import NavigationBar from '../../compose/components/navigation_bar';
import ColumnHeader from './column_header';
import Immutable from 'immutable';
const messages = defineMessages({
home_title: { id: 'column.home', defaultMessage: 'Home' },
notifications_title: { id: 'column.notifications', defaultMessage: 'Notifications' },
local_title: { id: 'column.community', defaultMessage: 'Local timeline' },
federated_title: { id: 'column.public', defaultMessage: 'Federated timeline' }
});
const PageOne = ({ acct, domain }) => (
<div className='onboarding-modal__page onboarding-modal__page-one'>
<div style={{ flex: '0 0 auto' }}>
<div className='onboarding-modal__page-one__elephant-friend' />
</div>
<div>
<h1><FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to Mastodon!' /></h1>
<p><FormattedMessage id='onboarding.page_one.federation' defaultMessage='Mastodon is a social network that belongs to everyone.' /></p>
<p><FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, one of many independent Mastodon instances. Your full handle is {handle}' values={{ domain, handle: <strong>{acct}@{domain}</strong> }}/></p>
</div>
</div>
);
PageOne.propTypes = {
acct: React.PropTypes.string.isRequired,
domain: React.PropTypes.string.isRequired
};
const PageTwo = () => (
<div className='onboarding-modal__page onboarding-modal__page-two'>
<div className='figure non-interactive'>
<ComposeForm
text='Awoo! #introductions'
suggestions={Immutable.List()}
mentionedDomains={[]}
onChange={() => {}}
onSubmit={() => {}}
onPaste={() => {}}
onPickEmoji={() => {}}
onChangeSpoilerText={() => {}}
onClearSuggestions={() => {}}
onFetchSuggestions={() => {}}
onSuggestionSelected={() => {}}
/>
</div>
<p><FormattedMessage id='onboarding.page_two.compose' defaultMessage='Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.' /></p>
</div>
);
const PageThree = ({ me, domain }) => (
<div className='onboarding-modal__page onboarding-modal__page-three'>
<div className='figure non-interactive'>
<Search
value=''
onChange={() => {}}
onSubmit={() => {}}
onClear={() => {}}
onShow={() => {}}
/>
<div className='pseudo-drawer'>
<NavigationBar account={me} />
</div>
</div>
<p><FormattedMessage id='onboarding.page_three.search' defaultMessage='Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.' values={{ illustration: <Permalink to='/timelines/tag/illustration' href='/tags/illustration'>#illustration</Permalink>, introductions: <Permalink to='/timelines/tag/introductions' href='/tags/introductions'>#introductions</Permalink> }}/></p>
<p><FormattedMessage id='onboarding.page_three.profile' defaultMessage='Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.' /></p>
</div>
);
PageThree.propTypes = {
me: ImmutablePropTypes.map.isRequired,
domain: React.PropTypes.string.isRequired
};
const PageFour = ({ domain, intl }) => (
<div className='onboarding-modal__page onboarding-modal__page-four'>
<div className='onboarding-modal__page-four__columns'>
<div className='row'>
<div>
<div className='figure non-interactive'><ColumnHeader icon='home' type={intl.formatMessage(messages.home_title)} /></div>
<p><FormattedMessage id='onboarding.page_four.home' defaultMessage='Home timeline shows posts from people you follow'/></p>
</div>
<div>
<div className='figure non-interactive'><ColumnHeader icon='bell' type={intl.formatMessage(messages.notifications_title)} /></div>
<p><FormattedMessage id='onboarding.page_four.notifications' defaultMessage='Notifications show when someone interacts with you' /></p>
</div>
</div>
<div className='row'>
<div>
<div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='globe' type={intl.formatMessage(messages.federated_title)} /></div>
</div>
<div>
<div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='users' type={intl.formatMessage(messages.local_title)} /></div>
</div>
</div>
<p><FormattedMessage id='onboarding.page_five.public_timelines' defaultMessage='Federated timeline lists public posts from everyone who people on {domain} follow. Local timeline is the same, but limited to people on {domain}.' values={{ domain }} /></p>
</div>
</div>
);
PageFour.propTypes = {
domain: React.PropTypes.string.isRequired,
intl: React.PropTypes.object.isRequired
};
const PageSix = ({ admin }) => {
let adminSection = '';
if (admin) {
adminSection = (
<p>
<FormattedMessage id='onboarding.page_six.admin' defaultMessage="Your instance's admin is {admin}." values={{ admin: <Permalink href={admin.get('url')} to={`/accounts/${admin.get('id')}`}>@{admin.get('acct')}</Permalink> }} />
<br />
<FormattedMessage id='onboarding.page_six.read_guidelines' defaultMessage='Please, do not forget to read the {guidelines}!' values={{ guidelines: <a href='/about/more' target='_blank'><FormattedMessage id='onboarding.page_six.guidelines' defaultMessage='community guidelines' /></a> }}/>
</p>
);
}
return (
<div className='onboarding-modal__page onboarding-modal__page-six'>
<h1><FormattedMessage id='onboarding.page_six.almost_done' defaultMessage='Almost done...' /></h1>
{adminSection}
<p><FormattedMessage id='onboarding.page_six.github' defaultMessage='Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.' values={{ github: <a href='https://github.com/tootsuite/mastodon' target='_blank' rel='noopener'>GitHub</a> }} /></p>
<p><FormattedMessage id='onboarding.page_six.apps_available' defaultMessage='There are {apps} available for iOS, Android and other platforms. And now... Bon Appetoot!' values={{ apps: <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' target='_blank' rel='noopener'><FormattedMessage id='onboarding.page_six.various_app' defaultMessage='various mobile apps' /></a> }} /></p>
</div>
);
};
PageSix.propTypes = {
admin: ImmutablePropTypes.map
};
const mapStateToProps = state => ({
me: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]),
domain: state.getIn(['meta', 'domain'])
});
const OnboardingModal = React.createClass({
propTypes: {
onClose: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired,
me: ImmutablePropTypes.map.isRequired,
domain: React.PropTypes.string.isRequired,
admin: ImmutablePropTypes.map
},
getInitialState () {
return {
currentIndex: 0
};
},
mixins: [PureRenderMixin],
handleSkip (e) {
e.preventDefault();
this.props.onClose();
},
handleDot (i, e) {
e.preventDefault();
this.setState({ currentIndex: i });
},
handleNext (maxNum, e) {
e.preventDefault();
if (this.state.currentIndex < maxNum - 1) {
this.setState({ currentIndex: this.state.currentIndex + 1 });
} else {
this.props.onClose();
}
},
render () {
const { me, admin, domain, intl } = this.props;
const pages = [
<PageOne acct={me.get('acct')} domain={domain} />,
<PageTwo />,
<PageThree me={me} domain={domain} />,
<PageFour domain={domain} intl={intl} />,
<PageSix admin={admin} />
];
const { currentIndex } = this.state;
const hasMore = currentIndex < pages.length - 1;
let nextOrDoneBtn;
if(hasMore) {
nextOrDoneBtn = <a href='#' onClick={this.handleNext.bind(null, pages.length)} className='onboarding-modal__nav onboarding-modal__next'><FormattedMessage id='onboarding.next' defaultMessage='Next' /></a>;
} else {
nextOrDoneBtn = <a href='#' onClick={this.handleNext.bind(null, pages.length)} className='onboarding-modal__nav onboarding-modal__done'><FormattedMessage id='onboarding.next' defaultMessage='Done' /></a>;
}
const styles = pages.map((page, i) => ({
key: i,
style: { opacity: spring(i === currentIndex ? 1 : 0) }
}));
return (
<div className='modal-root__modal onboarding-modal'>
<TransitionMotion styles={styles}>
{interpolatedStyles =>
<div className='onboarding-modal__pager'>
{pages.map((page, i) =>
<div key={i} style={{ opacity: interpolatedStyles[i].style.opacity, pointerEvents: i === currentIndex ? 'auto' : 'none' }}>{page}</div>
)}
</div>
}
</TransitionMotion>
<div className='onboarding-modal__paginator'>
<div>
<a href='#' className='onboarding-modal__skip' onClick={this.handleSkip}><FormattedMessage id='onboarding.skip' defaultMessage='Skip' /></a>
</div>
<div className='onboarding-modal__dots'>
{pages.map((_, i) => <div key={i} onClick={this.handleDot.bind(null, i)} className={`onboarding-modal__dot ${i === currentIndex ? 'active' : ''}`} />)}
</div>
<div>
{nextOrDoneBtn}
</div>
</div>
</div>
);
}
});
export default connect(mapStateToProps)(injectIntl(OnboardingModal));

View file

@ -24,10 +24,8 @@ const makeGetStatusIds = () => createSelector([
if (columnSettings.getIn(['regex', 'body'], '').trim().length > 0) {
try {
if (showStatus) {
const regex = new RegExp(columnSettings.getIn(['regex', 'body']).trim(), 'i');
showStatus = !regex.test(statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'unescaped_content']) : statusForId.get('unescaped_content'));
}
showStatus = showStatus && !regex.test(statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'content']) : statusForId.get('content'));
} catch(e) {
// Bad regex, don't affect filters
}

View file

@ -141,7 +141,7 @@ const UI = React.createClass({
{mountedColumns}
<NotificationsContainer />
<LoadingBarContainer className="loading-bar" />
<LoadingBarContainer style={{ backgroundColor: '#2b90d9', left: '0', top: '0' }} />
<ModalContainer />
<UploadArea active={draggingOver} />
</div>

View file

@ -14,7 +14,7 @@ Link.parseAttrs = (link, parts) => {
link = Link.parseParams(link, uriAttrs[1])
}
while(match = Link.attrPattern.exec(attrs)) { // eslint-disable-line no-cond-assign
while(match = Link.attrPattern.exec(attrs)) {
attr = match[1].toLowerCase()
value = match[4] || match[3] || match[2]

View file

@ -31,7 +31,6 @@ const en = {
"column.favourites": "Favourites",
"column.follow_requests": "Follow requests",
"column.home": "Home",
"column.mutes": "Muted users",
"column.notifications": "Notifications",
"column.public": "Federated timeline",
"compose_form.placeholder": "What is on your mind?",
@ -69,12 +68,10 @@ const en = {
"navigation_bar.follow_requests": "Follow requests",
"navigation_bar.info": "Extended information",
"navigation_bar.logout": "Logout",
"navigation_bar.mutes": "Muted users",
"navigation_bar.preferences": "Preferences",
"navigation_bar.public_timeline": "Federated timeline",
"notification.favourite": "{name} favourited your status",
"notification.follow": "{name} followed you",
"notification.mention": "{name} mentioned you",
"notification.reblog": "{name} boosted your status",
"notifications.clear_confirmation": "Are you sure you want to clear all your notifications?",
"notifications.clear": "Clear notifications",
@ -129,7 +126,6 @@ const en = {
"video_player.toggle_sound": "Toggle sound",
"video_player.toggle_visible": "Toggle visibility",
"video_player.expand": "Expand video",
"video_player.video_error": "Video could not be played",
};
export default en;

View file

@ -75,7 +75,6 @@ const fr = {
"navigation_bar.favourites": "Favoris",
"navigation_bar.info": "Plus d'informations",
"navigation_bar.logout": "Déconnexion",
"navigation_bar.mutes": "Utilisateurs muets",
"navigation_bar.follow_requests": "Demandes de suivi",
"reply_indicator.cancel": "Annuler",
"search.placeholder": "Rechercher",

View file

@ -1,125 +1,119 @@
const ja = {
"account.block": "@{name} さんをブロック",
"account.disclaimer": "このユーザーは他のインスタンスに所属しているため、数字が正確で無い場合があります。",
"account.edit_profile": "プロフィールを編集",
"account.follow": "フォロー",
"account.followers": "フォロワー",
"account.follows": "フォロー",
"account.follows_you": "フォローされています",
"column_back_button.label": "戻る",
"lightbox.close": "閉じる",
"loading_indicator.label": "読み込み中...",
"status.mention": "@{name} さんへの返信",
"status.delete": "削除",
"status.reply": "返信",
"status.reblog": "ブースト",
"status.favourite": "お気に入り",
"status.reblogged_by": "{name} さんにブーストされました",
"status.sensitive_warning": "不適切なコンテンツ",
"status.sensitive_toggle": "クリックして表示",
"status.show_more": "もっと見る",
"status.load_more": "もっと見る",
"status.show_less": "隠す",
"status.open": "Expand this status",
"status.report": "@{name} さんを通報",
"status.media_hidden": "非表示のメデイア",
"video_player.toggle_sound": "音の切り替え",
"account.mention": "@{name} さんに返信",
"account.mute": "ミュート",
"account.posts": "投稿",
"account.report": "@{name}を通報する",
"account.requested": "承認待ち",
"account.edit_profile": "プロフィールを編集",
"account.unblock": "@{name} さんのブロックを解除",
"account.unfollow": "フォロー解除",
"account.block": "@{name} さんをブロック",
"account.mute": "ミュート",
"account.unmute": "ミュート解除",
"boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。",
"column.blocks": "ブロックしたユーザー",
"column.community": "ローカルタイムライン",
"column.favourites": "お気に入り",
"column.follow_requests": "フォローリクエスト",
"account.follow": "フォロー",
"account.report": "@{name}を通報する",
"account.posts": "投稿",
"account.follows": "フォロー",
"account.followers": "フォロワー",
"account.follows_you": "フォローされています",
"account.requested": "承認待ち",
"follow_request.authorize": "許可",
"follow_request.reject": "拒否",
"getting_started.heading": "スタート",
"getting_started.about_addressing": "ドメインとユーザー名を知っているなら検索フォームに入力すればフォローできます。",
"getting_started.about_shortcuts": "対象のアカウントがあなたと同じドメインのユーザーならばユーザー名のみで検索できます。これは返信のときも一緒です。",
"getting_started.open_source_notice": "Mastodon はオープンソースソフトウェアです。誰でも GitHub{github})から開発に参加したり、問題を報告したりできます。 {apps}",
"column.home": "ホーム",
"column.mutes": "ミュートしたユーザー",
"column.notifications": "通知",
"column.community": "ローカルタイムライン",
"column.public": "連合タイムライン",
"column_back_button.label": "戻る",
"column.notifications": "通知",
"column.favourites": "お気に入り",
"tabs_bar.compose": "投稿",
"tabs_bar.home": "ホーム",
"tabs_bar.mentions": "返信",
"tabs_bar.local_timeline": "ローカル",
"tabs_bar.federated_timeline": "連合",
"tabs_bar.notifications": "通知",
"compose_form.placeholder": "今なにしてる?",
"compose_form.privacy_disclaimer": "あなたの非公開トゥートは返信先のユーザーat {domains})に公開されます。{domainsCount, plural, one {that server} other {those servers}}を信頼しますか投稿のプライバシー保護はMastodonサーバー内でのみ有効です。 もし{domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}ならばあなたの投稿のプライバシーは保護されず、ブーストされたり予期しないユーザーに見られる可能性があります。",
"compose_form.publish": "トゥート",
"compose_form.sensitive": "メディアを不適切なコンテンツとしてマークする",
"compose_form.spoiler": "テキストを隠す",
"compose_form.spoiler_placeholder": "閲覧注意",
"emoji_button.label": "絵文字を追加",
"empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",
"empty_column.hashtag": "このハッシュタグはまだ使われていません。",
"compose_form.spoiler_placeholder": "内容注意メッセージ",
"compose_form.private": "非公開にする",
"compose_form.privacy_disclaimer": "あなたの非公開トゥートは返信先のユーザーat {domains})に公開されます。{domainsCount, plural, one {that server} other {those servers}}を信頼しますか投稿のプライバシー保護はMastodonサーバー内でのみ有効です。 もし{domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}ならばあなたの投稿のプライバシーは保護されず、ブーストされたり予期しないユーザーに見られる可能性があります。",
"compose_form.unlisted": "公開タイムラインに表示しない",
"privacy.public.short": "公開",
"privacy.public.long": "公開TLに投稿する",
"privacy.unlisted.short": "未収載",
"privacy.unlisted.long": "公開TLで表示しない",
"privacy.private.short": "非公開",
"privacy.private.long": "フォロワーだけに公開",
"privacy.direct.short": "ダイレクト",
"privacy.direct.long": "含んだユーザーだけに公開",
"privacy.change": "投稿のプライバシーを変更2",
"report.heading": "新規通報",
"report.placeholder": "コメント",
"report.target": "問題のユーザー",
"report.submit": "通報する",
"navigation_bar.edit_profile": "プロフィールを編集",
"navigation_bar.preferences": "ユーザー設定",
"navigation_bar.community_timeline": "ローカルタイムライン",
"navigation_bar.public_timeline": "連合タイムライン",
"navigation_bar.logout": "ログアウト",
"navigation_bar.favourites": "お気に入り",
"navigation_bar.blocks": "ブロックしたユーザー",
"navigation_bar.info": "サーバー情報",
"reply_indicator.cancel": "キャンセル",
"search.placeholder": "検索",
"search.account": "アカウント",
"search.hashtag": "ハッシュタグ",
"search.status_by": "{uuuname}からの投稿",
"upload_area.title": "ファイルをこちらにドラッグしてください",
"upload_button.label": "メディアを追加",
"upload_form.undo": "やり直す",
"notification.follow": "{name} さんにフォローされました",
"notification.favourite": "{name} さんがあなたのトゥートをお気に入りに登録しました",
"notification.reblog": "{name} さんがあなたのトゥートをブーストしました",
"notification.mention": "{name} さんがあなたに返信しました",
"notifications.clear": "通知を片付ける",
"notifications.clear_confirmation": "通知を全部片付けます。大丈夫ですか?",
"notifications.column_settings.alert": "デスクトップ通知",
"notifications.column_settings.show": "カラムに表示",
"notifications.column_settings.follow": "新しいフォロワー",
"notifications.column_settings.favourite": "お気に入り",
"notifications.column_settings.mention": "返信",
"notifications.column_settings.reblog": "ブースト",
"notifications.column_settings.sound": "通知音を再生",
"empty_column.home": "まだ誰もフォローしていません。{public}を見に行くか、検索を使って他のユーザーを見つけましょう。",
"empty_column.home.public_timeline": "連合タイムライン",
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
"empty_column.public": "ここにはまだ何もありません!公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう!",
"follow_request.authorize": "許可",
"follow_request.reject": "拒否",
"getting_started.apps": "さまざまなアプリで利用できます。",
"getting_started.heading": "スタート",
"getting_started.open_source_notice": "Mastodon はオープンソースソフトウェアです。誰でも GitHub{github})から開発に参加したり、問題を報告したりできます。 {apps}",
"home.column_settings.advanced": "上級者向け",
"empty_column.hashtag": "このハッシュタグはまだ使っていません。",
"upload_progress.label": "アップロード中…",
"emoji_button.label": "絵文字を追加",
"home.column_settings.basic": "シンプル",
"home.column_settings.filter_regex": "正規表現でフィルター",
"home.column_settings.advanced": "エキスパート",
"home.column_settings.show_reblogs": "ブースト表示",
"home.column_settings.show_replies": "返信表示",
"home.column_settings.filter_regex": "正規表現でフィルター",
"home.settings": "カラム設定",
"lightbox.close": "閉じる",
"loading_indicator.label": "読み込み中...",
"media_gallery.toggle_visible": "表示切り替え",
"notification.settings": "カラム設定",
"missing_indicator.label": "見つかりません",
"navigation_bar.blocks": "ブロックしたユーザー",
"navigation_bar.community_timeline": "ローカルタイムライン",
"navigation_bar.edit_profile": "プロフィールを編集",
"navigation_bar.favourites": "お気に入り",
"navigation_bar.follow_requests": "フォローリクエスト",
"navigation_bar.info": "サーバー情報",
"navigation_bar.logout": "ログアウト",
"navigation_bar.mutes": "ミュートしたユーザー",
"navigation_bar.preferences": "ユーザー設定",
"navigation_bar.public_timeline": "連合タイムライン",
"notification.favourite": "{name} さんがあなたのトゥートをお気に入りに登録しました",
"notification.follow": "{name} さんにフォローされました",
"notification.mention": "{name} さんがあなたに返信しました",
"notification.reblog": "{name} さんがあなたのトゥートをブーストしました",
"notifications.clear": "通知を消去",
"notifications.clear_confirmation": "本当に通知を消去しますか?",
"notifications.column_settings.alert": "デスクトップ通知",
"notifications.column_settings.favourite": "お気に入り",
"notifications.column_settings.follow": "新しいフォロワー",
"notifications.column_settings.mention": "返信",
"notifications.column_settings.reblog": "ブースト",
"notifications.column_settings.show": "カラムに表示",
"notifications.column_settings.sound": "通知音を再生",
"notifications.settings": "カラム設定",
"privacy.change": "投稿のプライバシーを変更",
"privacy.direct.long": "メンションしたユーザーだけに公開",
"privacy.direct.short": "ダイレクト",
"privacy.private.long": "フォロワーだけに公開",
"privacy.private.short": "非公開",
"privacy.public.long": "公開TLに投稿する",
"privacy.public.short": "公開",
"privacy.unlisted.long": "公開TLで表示しない",
"privacy.unlisted.short": "未収載",
"reply_indicator.cancel": "キャンセル",
"report.heading": "新規通報",
"report.placeholder": "コメント",
"report.submit": "通報する",
"report.target": "問題のユーザー",
"search.placeholder": "検索",
"search.status_by": "{name}からの投稿",
"search_results.total": "{count} {count, plural, one {result} other {results}} 件",
"status.delete": "削除",
"status.favourite": "お気に入り",
"status.load_more": "もっと見る",
"status.media_hidden": "非表示のメデイア",
"status.mention": "@{name} さんへの返信",
"status.open": "詳細を表示",
"status.reblog": "ブースト",
"status.reblogged_by": "{name} さんにブーストされました",
"status.reply": "返信",
"status.report": "@{name} さんを通報",
"status.sensitive_toggle": "クリックして表示",
"status.sensitive_warning": "不適切なコンテンツ",
"status.show_less": "隠す",
"status.show_more": "もっと見る",
"tabs_bar.compose": "投稿",
"tabs_bar.federated_timeline": "連合",
"tabs_bar.home": "ホーム",
"tabs_bar.local_timeline": "ローカル",
"tabs_bar.notifications": "通知",
"upload_area.title": "ドラッグ&ドロップでアップロード",
"upload_button.label": "メディアを追加",
"upload_form.undo": "やり直す",
"upload_progress.label": "アップロード中…",
"video_player.expand": "動画の詳細",
"video_player.toggle_sound": "音の切り替え",
"video_player.toggle_visible": "表示切り替え",
"video_player.video_error": "動画の再生に失敗しました",
"boost_modal.combo": "次は{combo}を押せば、これをスキップできます。"
};
export default ja;

View file

@ -1,22 +1,22 @@
const nl = {
"column_back_button.label": "terug",
"lightbox.close": "Sluiten",
"loading_indicator.label": "Laden",
"status.mention": "@{name} vermelden",
"status.delete": "Verwijderen",
"status.reply": "Reageren",
"loading_indicator.label": "Laden...",
"status.mention": "Vermeld @{name}",
"status.delete": "Verwijder",
"status.reply": "Reageer",
"status.reblog": "Boost",
"status.favourite": "Favoriet",
"status.reblogged_by": "{name} boostte",
"status.sensitive_warning": "Gevoelige inhoud",
"status.sensitive_toggle": "Klik om te zien",
"video_player.toggle_sound": "Geluid in-/uitschakelen",
"account.mention": "@{name} vermelden",
"account.edit_profile": "Profiel bewerken",
"account.unblock": "@{name} deblokkeren",
"account.unfollow": "Ontvolgen",
"account.block": "@{name} blokkeren",
"account.follow": "Volgen",
"video_player.toggle_sound": "Geluid omschakelen",
"account.mention": "Vermeld @{name}",
"account.edit_profile": "Bewerk profiel",
"account.unblock": "Deblokkeer @{name}",
"account.unfollow": "Ontvolg",
"account.block": "Blokkeer @{name}",
"account.follow": "Volg",
"account.posts": "Berichten",
"account.follows": "Volgt",
"account.followers": "Volgers",
@ -25,7 +25,7 @@ const nl = {
"getting_started.heading": "Beginnen",
"getting_started.about_addressing": "Je kunt mensen volgen als je hun gebruikersnaam en het domein van hun server kent, door het e-mailachtige adres in het zoekscherm in te voeren.",
"getting_started.about_shortcuts": "Als de gezochte gebruiker op hetzelfde domein zit als jijzelf, is invoeren van de gebruikersnaam genoeg. Dat geldt ook als je mensen in de statussen wilt vermelden.",
"getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}. {apps}.",
"getting_started.open_source_notice": "Mastodon is open source software. Je kunt bijdragen of problemen melden op GitHub via {github}. {apps}.",
"column.home": "Thuis",
"column.community": "Lokale tijdlijn",
"column.public": "Federatietijdlijn",
@ -37,30 +37,30 @@ const nl = {
"tabs_bar.notifications": "Meldingen",
"compose_form.placeholder": "Waar ben je mee bezig?",
"compose_form.publish": "Toot",
"compose_form.sensitive": "Media als gevoelig markeren",
"compose_form.spoiler": "Tekst achter waarschuwing verbergen",
"compose_form.private": "Als privé markeren",
"compose_form.privacy_disclaimer": "Je besloten status wordt afgeleverd aan vermelde gebruikers op {domains}. Vertrouw je {domainsCount, plural, one {that server} andere {those servers}}? Privé plaatsen werkt alleen op Mastodon servers. Als {domains} {domainsCount, plural, een {is not a Mastodon instance} andere {are not Mastodon instances}}, dan wordt er geen indicatie gegeven dat he bericht besloten is, waardoor het kan worden geboost of op andere manier zichtbaar worden voor niet bedoelde lezers.",
"compose_form.unlisted": "Niet op openbare tijdlijnen tonen",
"navigation_bar.edit_profile": "Profiel bewerken",
"compose_form.sensitive": "Markeer media als gevoelig",
"compose_form.spoiler": "Verberg tekst achter waarschuwing",
"compose_form.private": "Mark als privé",
"compose_form.privacy_disclaimer": "Je besloten status wordt afgeleverd aan vermelde gebruikers op {domains}. Vertrouw je {domainsCount, plural, one {that server} andere {those servers}}? Privé plaatsen werkt alleen op Mastodon servers. Als {domains} {domainsCount, plural, een {is not a Mastodon instance} andere {are not Mastodon instances}}, dan wordt er geen indicatie gegeven dat he bericht besloten is, waardoor het kan worden geboost of op andere manier zichtbaar worden voor niet bedoelde lezers.",
"compose_form.unlisted": "Niet tonen op openbare tijdlijnen",
"navigation_bar.edit_profile": "Bewerk profiel",
"navigation_bar.preferences": "Voorkeuren",
"navigation_bar.community_timeline": "Lokale tijdlijn",
"navigation_bar.public_timeline": "Federatietijdlijn",
"navigation_bar.logout": "Afmelden",
"navigation_bar.logout": "Uitloggen",
"reply_indicator.cancel": "Annuleren",
"search.placeholder": "Zoeken",
"search.account": "Account",
"search.hashtag": "Hashtag",
"upload_button.label": "Media toevoegen",
"upload_button.label": "Toevoegen media",
"upload_form.undo": "Ongedaan maken",
"notification.follow": "{name} volgde jou",
"notification.favourite": "{name} markeerde je status als favoriet",
"notification.reblog": "{name} boostte je status",
"notification.mention": "{name} vermeldde jou",
"notifications.column_settings.alert": "Desktopmeldingen",
"notifications.column_settings.show": "In kolom tonen",
"notifications.column_settings.show": "Tonen in kolom",
"notifications.column_settings.follow": "Nieuwe volgers:",
"notifications.column_settings.favourite": "Favorieten:",
"notifications.column_settings.favourite": "Favoriten:",
"notifications.column_settings.mention": "Vermeldingen:",
"notifications.column_settings.reblog": "Boosts:",
};

View file

@ -1,130 +1,77 @@
const no = {
"account.block": "Blokkér @{name}",
"account.disclaimer": "Denne brukeren er fra en annen instans. Dette tallet kan være høyere.",
"account.edit_profile": "Rediger profil",
"account.follow": "Følg",
"account.followers": "Følgere",
"account.follows_you": "Følger deg",
"account.follows": "Følginger",
"account.mention": "Nevn @{name}",
"account.mute": "Demp @{name}",
"account.posts": "Poster",
"account.report": "Rapportér @{name}",
"account.requested": "Venter på godkjennelse",
"account.unblock": "Avblokker @{name}",
"account.unfollow": "Avfølg",
"account.unmute": "Avdemp @{name}",
"boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang",
"column_back_button.label": "Tilbake",
"column.blocks": "Blokkerte brukere",
"column.community": "Lokal tidslinje",
"column.favourites": "Likt",
"column.follow_requests": "Følgeforespørsler",
"column.home": "Hjem",
"column.notifications": "Varslinger",
"column.public": "Felles tidslinje",
"compose_form.placeholder": "Hva har du på hjertet?",
"compose_form.privacy_disclaimer": "Din private status vil leveres til nevnte brukere på {domains}. Stoler du på {domainsCount, plural, one {den serveren} other {de serverne}}? Synlighet fungerer kun på Mastodon-instanser. Hvis {domains} {domainsCount, plural, one {ike er en Mastodon-instans} other {ikke er Mastodon-instanser}}, vil det ikke indikeres at posten din er privat, og den kan kanskje bli fremhevd eller på annen måte bli synlig for uventede mottakere.",
"compose_form.publish": "Tut",
"compose_form.sensitive": "Merk media som følsomt",
"compose_form.spoiler_placeholder": "Innholdsadvarsel",
"compose_form.spoiler": "Skjul tekst bak advarsel",
"emoji_button.label": "Sett inn emoji",
"empty_column.community": "Den lokale tidslinjen er tom. Skriv noe offentlig for å få snøballen til å rulle!",
"empty_column.hashtag": "Det er ingenting i denne hashtagen ennå.",
"empty_column.home.public_timeline": "en offentlig tidslinje",
"empty_column.home": "Du har ikke fulgt noen ennå. Besøk {publlic} eller bruk søk for å komme i gang og møte andre brukere.",
"empty_column.notifications": "Du har ingen varsler ennå. Kommuniser med andre for å begynne samtalen.",
"empty_column.public": "Det er ingenting her! Skriv noe offentlig, eller følg brukere manuelt fra andre instanser for å fylle den opp",
"follow_request.authorize": "Autorisér",
"follow_request.reject": "Avvis",
"getting_started.apps": "Diverse apper er tilgjengelige",
"getting_started.heading": "Kom i gang",
"getting_started.open_source_notice": "Mastodon er fri programvare. Du kan bidra eller rapportere problemer på GitHub på {github}. {apps}.",
"home.column_settings.advanced": "Advansert",
"home.column_settings.basic": "Enkel",
"home.column_settings.filter_regex": "Filtrér med regulære uttrykk",
"home.column_settings.show_reblogs": "Vis fremhevinger",
"home.column_settings.show_replies": "Vis svar",
"home.settings": "Kolonneinnstillinger",
"lightbox.close": "Lukk",
"loading_indicator.label": "Laster...",
"media_gallery.toggle_visible": "Veksle synlighet",
"missing_indicator.label": "Ikke funnet",
"navigation_bar.blocks": "Blokkerte brukere",
"navigation_bar.community_timeline": "Lokal tidslinje",
"navigation_bar.edit_profile": "Rediger profil",
"navigation_bar.favourites": "Likt",
"navigation_bar.follow_requests": "Følgeforespørsler",
"navigation_bar.info": "Utvidet informasjon",
"navigation_bar.logout": "Logg ut",
"navigation_bar.preferences": "Preferanser",
"navigation_bar.public_timeline": "Felles tidslinje",
"notification.favourite": "{name} likte din status",
"notification.follow": "{name} fulgte deg",
"notification.reblog": "{name} fremhevde din status",
"notifications.clear_confirmation": "Er du sikker på at du vil fjerne alle dine varsler?",
"notifications.clear": "Fjern varsler",
"notifications.column_settings.alert": "Skrivebordsvarslinger",
"notifications.column_settings.favourite": "Likt:",
"notifications.column_settings.follow": "Nye følgere:",
"notifications.column_settings.mention": "Nevninger:",
"notifications.column_settings.reblog": "Fremhevinger:",
"notifications.column_settings.show": "Vis i kolonne",
"notifications.column_settings.sound": "Spill lyd",
"notifications.settings": "Kolonneinstillinger",
"privacy.change": "Justér synlighet",
"privacy.direct.long": "Post kun til nevnte brukere",
"privacy.direct.short": "Direkte",
"privacy.private.long": "Post kun til følgere",
"privacy.private.short": "Privat",
"privacy.public.long": "Post kun til offentlige tidslinjer",
"privacy.public.short": "Offentlig",
"privacy.unlisted.long": "Ikke vis i offentlige tidslinjer",
"privacy.unlisted.short": "Uoppført",
"reply_indicator.cancel": "Avbryt",
"report.heading": "Ny rapport",
"report.placeholder": "Tilleggskommentarer",
"report.submit": "Send inn",
"report.target": "Rapporterer",
"search_results.total": "{count} {count, plural, one {resultat} other {resultater}}",
"search.placeholder": "Søk",
"search.status_by": "Status fra {name}",
"status.delete": "Slett",
"status.favourite": "Lik",
"status.load_more": "Last mer",
"status.media_hidden": "Media skjult",
"status.mention": "Nevn @{name}",
"status.open": "Utvid denne statusen",
"status.reblog": "Fremhev",
"status.reblogged_by": "Fremhevd av {name}",
"status.delete": "Slett",
"status.reply": "Svar",
"status.report": "Rapporter @{name}",
"status.reblog": "Reblogg",
"status.favourite": "Lik",
"status.reblogged_by": "{name} reblogget",
"status.sensitive_warning": "Sensitivt innhold",
"status.sensitive_toggle": "Klikk for å vise",
"status.sensitive_warning": "Følsomt innhold",
"status.show_less": "Vis mindre",
"status.show_more": "Vis mer",
"tabs_bar.compose": "Komponer",
"tabs_bar.federated_timeline": "Felles",
"tabs_bar.home": "Hjem",
"tabs_bar.local_timeline": "Lokal",
"tabs_bar.notifications": "Varslinger",
"upload_area.title": "Dra og slipp for å laste opp",
"upload_button.label": "Legg til media",
"upload_form.undo": "Angre",
"upload_progress.label": "Laster opp...",
"status.show_less": "Vis mindre",
"status.open": "Utvid denne statusen",
"status.report": "Rapporter @{name}",
"video_player.toggle_sound": "Veksle lyd",
"video_player.toggle_visible": "Veksle synlighet",
"video_player.expand": "Utvid video",
"account.mention": "Nevn @{name}",
"account.edit_profile": "Rediger profil",
"account.unblock": "Avblokker @{name}",
"account.unfollow": "Avfølg",
"account.block": "Blokker @{name}",
"account.follow": "Følg",
"account.posts": "Poster",
"account.follows": "Følginger",
"account.followers": "Følgere",
"account.follows_you": "Folger deg",
"account.requested": "Venter på godkjennelse",
"getting_started.heading": "Kom i gang",
"getting_started.about_addressing": "Du kan følge noen hvis du vet brukernavnet deres og domenet de er på ved å skrive en e-postadresse inn i søkeskjemaet.",
"getting_started.about_shortcuts": "Hvis målbrukeren er på samme domene som deg, vil kun brukernavnet også fungere. Den samme regelen gjelder når man nevner noen i statuser.",
"getting_started.open_source_notice": "Mastodon er programvare med fri kildekode. Du kan bidra eller rapportere problemer på GitHub på {github}. {apps}.",
"column.home": "Hjem",
"column.community": "Lokal tidslinje",
"column.public": "Forent tidslinje",
"column.notifications": "Varslinger",
"column.blocks": "Blokkerte brukere",
"column.favourites": "Likt",
"tabs_bar.compose": "Komponer",
"tabs_bar.home": "Hjem",
"tabs_bar.mentions": "Nevninger",
"tabs_bar.public": "Felles tidslinje",
"tabs_bar.public": "Forent tidslinje",
"tabs_bar.notifications": "Varslinger",
"compose_form.placeholder": "Hva har du på hjertet?",
"compose_form.publish": "Tut",
"compose_form.sensitive": "Merk media som følsomt",
"compose_form.spoiler": "Skjul tekst bak advarsel",
"compose_form.private": "Merk som privat",
"compose_form.privacy_disclaimer": "Din private status vil leveres til nevnte brukere på {domains}. Stoler du på {domainsCount, plural, one {den serveren} other {de serverne}}? Synlighet fungerer kun på Mastodon-instanser. Hvis {domains} {domainsCount, plural, one {ike er en Mastodon-instans} other {ikke er Mastodon-instanser}}, vil det ikke indikeres at posten din er privat, og den kan kanskje bli reblogget eller på annen måte bli synlig for uventede mottakere.",
"compose_form.unlisted": "Ikke vis på offentlige tidslinjer",
"navigation_bar.edit_profile": "Rediger profil",
"navigation_bar.preferences": "Preferanser",
"navigation_bar.community_timeline": "Lokal tidslinje",
"navigation_bar.public_timeline": "Forent tidslinje",
"navigation_bar.logout": "Logg ut",
"navigation_bar.blocks": "Blokkerte brukere",
"navigation_bar.info": "Utvidet informasjon",
"navigation_bar.favourites": "Likt",
"reply_indicator.cancel": "Avbryt",
"search.placeholder": "Søk",
"search.account": "Konto",
"search.hashtag": "Hashtag",
"notification.mention": "{name} nevnte deg"
"upload_button.label": "Legg til media",
"upload_form.undo": "Angre",
"notification.follow": "{name} fulgte deg",
"notification.favourite": "{name} likte din status",
"notification.reblog": "{name} reblogget din status",
"notification.mention": "{name} nevnte deg",
"notifications.column_settings.alert": "Skrivebordsvarslinger",
"notifications.column_settings.show": "Vis i kolonne",
"notifications.column_settings.follow": "Nye følgere:",
"notifications.column_settings.favourite": "Likt:",
"notifications.column_settings.mention": "Nevninger:",
"notifications.column_settings.reblog": "Reblogginger:",
};
export default no;

View file

@ -2,9 +2,7 @@ const ru = {
"column_back_button.label": "Назад",
"lightbox.close": "Закрыть",
"loading_indicator.label": "Загрузка...",
"missing_indicator.label": "Не найдено",
"status.mention": "Упомянуть @{name}",
"status.media_hidden": "Медиаконтент скрыт",
"status.delete": "Удалить",
"status.reply": "Ответить",
"status.reblog": "Продвинуть",
@ -18,23 +16,18 @@ const ru = {
"status.report": "Пожаловаться",
"status.load_more": "Показать еще",
"video_player.toggle_sound": "Вкл./выкл. звук",
"video_player.toggle_visible": "Показать/скрыть",
"account.disclaimer": "Это пользователь с другого узла. Число может быть больше.",
"account.mention": "Упомянуть",
"account.edit_profile": "Изменить профиль",
"account.unblock": "Разблокировать",
"account.unfollow": "Отписаться",
"account.block": "Блокировать",
"account.mute": "Заглушить",
"account.report": "Пожаловаться",
"account.unmute": "Снять глушение",
"account.follow": "Подписаться",
"account.posts": "Посты",
"account.follows": "Подписки",
"account.followers": "Подписаны",
"account.follows_you": "Подписан(а) на Вас",
"account.requested": "Ожидает подтверждения",
"boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз",
"getting_started.heading": "Добро пожаловать",
"getting_started.about_addressing": "Вы можете подписаться на человека, зная имя пользователя и домен, на котором он находится, введя e-mail-подобный адрес в форму поиска.",
"getting_started.about_shortcuts": "Если пользователь находится на одном с Вами домене, можно использовать только имя. То же правило применимо к упоминанию пользователей в статусах.",
@ -44,16 +37,11 @@ const ru = {
"column.community": "Локальная лента",
"column.public": "Глобальная лента",
"column.notifications": "Уведомления",
"column.favourites": "Понравившееся",
"column.blocks": "Список блокировки",
"column.follow_requests": "Запросы на подписку",
"tabs_bar.compose": "Написать",
"tabs_bar.home": "Главная",
"tabs_bar.mentions": "Упоминания",
"tabs_bar.public": "Глобальная лента",
"tabs_bar.notifications": "Уведомления",
"tabs_bar.local_timeline": "Локальная",
"tabs_bar.federated_timeline": "Глобальная",
"compose_form.placeholder": "О чем Вы думаете?",
"compose_form.publish": "Трубить",
"compose_form.sensitive": "Отметить как чувствительный контент",
@ -61,7 +49,6 @@ const ru = {
"compose_form.private": "Отметить как приватное",
"compose_form.privacy_disclaimer": "Ваш приватный статус будет доставлен упомянутым пользователям на доменах {domains}. Доверяете ли вы {domainsCount, plural, one {этому серверу} other {этим серверам}}? Приватность постов работает только на узлах Mastodon. Если {domains} {domainsCount, plural, one {не является узлом Mastodon} other {не являются узлами Mastodon}}, приватность поста не будет указана, и он может оказаться продвинут или иным образом показан не обозначенным Вами пользователям.",
"compose_form.unlisted": "Не отображать в публичных лентах",
"compose_form.spoiler_placeholder": "Не для всех",
"navigation_bar.edit_profile": "Изменить профиль",
"navigation_bar.preferences": "Опции",
"navigation_bar.community_timeline": "Локальная лента",
@ -70,20 +57,12 @@ const ru = {
"navigation_bar.info": "Об узле",
"navigation_bar.favourites": "Понравившееся",
"navigation_bar.blocks": "Список блокировки",
"navigation_bar.follow_requests": "Запросы на подписку",
"reply_indicator.cancel": "Отмена",
"report.target": "Жалуемся на",
"report.heading": "Новая жалоба",
"report.placeholder": "Комментарий",
"report.submit": "Отправить",
"search.placeholder": "Поиск",
"search.account": "Аккаунт",
"search.hashtag": "Хэштег",
"search.status_by": "Статус от {name}",
"upload_area.title": "Перетащите сюда, чтобы загрузить",
"upload_button.label": "Добавить медиаконтент",
"upload_form.undo": "Отменить",
"upload_progress.label": "Загрузка...",
"notification.follow": "{name} подписался(-лась) на Вас",
"notification.favourite": "{name} понравился Ваш статус",
"notification.reblog": "{name} продвинул(а) Ваш статус",
@ -92,10 +71,9 @@ const ru = {
"home.column_settings.basic": "Основные",
"home.column_settings.advanced": "Дополнительные",
"home.column_settings.filter_regex": "Отфильтровать регулярным выражением",
"home.column_settings.show_reblogs": "Показывать продвижения",
"home.column_settings.show_replies": "Показывать продвижения",
"home.column_settings.show_replies": "Показывать ответы",
"notifications.clear": "Очистить уведомления",
"notifications.clear_confirmation": "Вы уверены, что хотите очистить все уведомления?",
"notifications.settings": "Настройки колонки",
"notifications.column_settings.alert": "Десктопные уведомления",
"notifications.column_settings.show": "Показывать в колонке",
@ -118,10 +96,6 @@ const ru = {
"privacy.private.long": "Показать только подписчикам",
"privacy.direct.short": "Направленный",
"privacy.direct.long": "Показать только упомянутым",
"emoji_button.label": "Вставить эмодзи",
"follow_request.authorize": "Авторизовать",
"follow_request.reject": "Отказать",
"media_gallery.toggle_visible": "Показать/скрыть",
};
export default ru;

View file

@ -19,27 +19,19 @@ export { localeData as localeData };
const zh_hk = {
"account.block": "封鎖 @{name}",
"account.disclaimer": "由於這個用戶在另一個服務站,實際數字會比這個更多。",
"account.edit_profile": "修改個人資料",
"account.follow": "關注",
"account.followers": "關注的人",
"account.follows_you": "關注你",
"account.follows": "正在關注",
"account.mention": "提及 @{name}",
"account.mute": "將 @{name} 靜音",
"account.posts": "文章",
"account.report": "舉報 @{name}",
"account.requested": "等候審批",
"account.unblock": "解除對 @{name} 的封鎖",
"account.unfollow": "取消關注",
"account.unmute": "取消 @{name} 的靜音",
"boost_modal.combo": "如你想在下次路過這顯示,請按{combo}",
"column_back_button.label": "返回",
"column.blocks": "封鎖用戶",
"column_back_button.label": "先前顯示",
"column.community": "本站時間軸",
"column.favourites": "喜歡的文章",
"column.follow_requests": "關注請求",
"column.home": "主頁",
"column.home": "家",
"column.notifications": "通知",
"column.public": "跨站公共時間軸",
"compose_form.placeholder": "你在想甚麼?",
@ -47,49 +39,35 @@ const zh_hk = {
"compose_form.private": "標示為「只有關注你的人能看」",
"compose_form.publish": "發文",
"compose_form.sensitive": "將媒體檔案標示為「敏感內容」",
"compose_form.spoiler_placeholder": "敏感內容",
"compose_form.spoiler": "將部份文字藏於警告訊息之後",
"compose_form.unlisted": "請勿在公共時間軸顯示",
"emoji_button.label": "加入表情符號",
"empty_column.community": "本站時間軸暫時未有內容,快貼文來搶頭香啊!",
"empty_column.hashtag": "這個標籤暫時未有內容。",
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
"empty_column.home.public_timeline": "公共時間軸",
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
"empty_column.notifications": "你沒有任何通知紀錄,快向其他用戶搭訕吧。",
"empty_column.public": "跨站公共時間軸暫時沒有內容!快寫一些公共的文章,或者關注另一些服務站的用戶吧!你和本站、友站的交流,將決定這裏出現的內容。",
"follow_request.authorize": "批准",
"follow_request.reject": "拒絕",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up.",
"getting_started.about_addressing": "只要你知道一位用戶的用戶名稱和域名,你可以用「@用戶名稱@域名」的格式在搜尋欄尋找該用戶。",
"getting_started.about_shortcuts": "只要該用戶是在你現在的服務站開立,你可以直接輸入用戶𠱷搜尋。同樣的規則適用於在文章提及別的用戶。",
"getting_started.apps": "手機或桌面應用程式",
"getting_started.heading": "開始使用",
"getting_started.open_source_notice": "Mastodon 是一個開放源碼的軟件。你可以在官方 GitHub ({github}) 貢獻或者回報問題。你亦可透過{apps}閱讀 Mastodon 上的消息。",
"home.column_settings.advanced": "進階",
"home.column_settings.basic": "基本",
"home.column_settings.filter_regex": "使用正規表達式 (regular expression) 過濾",
"home.column_settings.show_reblogs": "顯示被轉推的文章",
"home.column_settings.show_replies": "顯示回應文章",
"home.settings": "欄位設定",
"lightbox.close": "Close",
"home.column_settings.advanced": "進階",
"lightbox.close": "關閉",
"loading_indicator.label": "載入中...",
"media_gallery.toggle_visible": "打開或關上",
"missing_indicator.label": "找不到內容",
"navigation_bar.blocks": "被封鎖的用戶",
"navigation_bar.community_timeline": "本站時間軸",
"navigation_bar.edit_profile": "修改個人資料",
"navigation_bar.favourites": "喜歡的內容",
"navigation_bar.follow_requests": "關注請求",
"navigation_bar.info": "關於本服務站",
"navigation_bar.logout": "登出",
"navigation_bar.preferences": "偏好設定",
"navigation_bar.preferences": "個人設定",
"navigation_bar.public_timeline": "跨站公共時間軸",
"notification.favourite": "{name} 喜歡你的文章",
"notification.follow": "{name} 開始關注你",
"notification.follow": "{name} 開始開始你",
"notification.mention": "{name} 提及你",
"notification.reblog": "{name} 轉推你的文章",
"notifications.clear_confirmation": "你確定要清空通知紀錄嗎?",
"notifications.clear": "清空通知紀錄",
"notifications.column_settings.alert": "顯示桌面通知",
"notifications.column_settings.favourite": "喜歡你的文章:",
"notifications.column_settings.follow": "關注你:",
@ -97,26 +75,13 @@ const zh_hk = {
"notifications.column_settings.reblog": "轉推你的文章:",
"notifications.column_settings.show": "在通知欄顯示",
"notifications.column_settings.sound": "播放音效",
"notifications.settings": "欄位設定",
"privacy.change": "調整私隱設定",
"privacy.direct.long": "只有提及的用戶能看到",
"privacy.direct.short": "私人訊息",
"privacy.private.long": "只有關注你用戶能看到",
"privacy.private.short": "關注者",
"privacy.public.long": "在公共時間軸顯示",
"privacy.public.short": "公共",
"privacy.unlisted.long": "公開,但不在公共時間軸顯示",
"privacy.unlisted.short": "公開",
"reply_indicator.cancel": "取消",
"report.heading": "舉報",
"report.placeholder": "額外訊息",
"report.submit": "提交",
"report.target": "Reporting",
"search_results.total": "{count} 項結果",
"search.account": "用戶",
"search.hashtag": "標籤",
"search.placeholder": "搜尋",
"search.status_by": "按{name}搜尋文章",
"search_results.total": "{count} 項結果",
"search.status_by": "按用戶名稱搜尋文章",
"status.delete": "刪除",
"status.favourite": "喜歡",
"status.load_more": "載入更多",
@ -132,19 +97,17 @@ const zh_hk = {
"status.show_less": "減少顯示",
"status.show_more": "顯示更多",
"tabs_bar.compose": "撰寫",
"tabs_bar.federated_timeline": "跨站",
"tabs_bar.home": "主頁",
"tabs_bar.home": "家",
"tabs_bar.local_timeline": "本站",
"tabs_bar.mentions": "提及",
"tabs_bar.notifications": "通知",
"tabs_bar.public": "跨站公共時間軸",
"tabs_bar.federated_timeline": "跨站",
"upload_area.title": "將檔案拖放至此上載",
"upload_button.label": "上載媒體檔案",
"upload_form.undo": "還原",
"upload_progress.label": "上載中……",
"video_player.expand": "展開影片",
"upload_form.undo": "還原",
"video_player.toggle_sound": "開關音效",
"video_player.toggle_visible": "打開或關上",
};
export default zh_hk;

View file

@ -22,7 +22,7 @@ export default function errorsMiddleware() {
dispatch(showAlert(title, message));
} else {
console.error(action.error); // eslint-disable-line no-console
console.error(action.error);
dispatch(showAlert('Oops!', 'An unexpected error occurred.'));
}
}

View file

@ -15,10 +15,6 @@ import {
BLOCKS_FETCH_SUCCESS,
BLOCKS_EXPAND_SUCCESS
} from '../actions/blocks';
import {
MUTES_FETCH_SUCCESS,
MUTES_EXPAND_SUCCESS
} from '../actions/mutes';
import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose';
import {
REBLOG_SUCCESS,
@ -98,8 +94,6 @@ export default function accounts(state = initialState, action) {
case FOLLOW_REQUESTS_EXPAND_SUCCESS:
case BLOCKS_FETCH_SUCCESS:
case BLOCKS_EXPAND_SUCCESS:
case MUTES_FETCH_SUCCESS:
case MUTES_EXPAND_SUCCESS:
return normalizeAccounts(state, action.accounts);
case NOTIFICATIONS_REFRESH_SUCCESS:
case NOTIFICATIONS_EXPAND_SUCCESS:

View file

@ -2,7 +2,6 @@ import { STORE_HYDRATE } from '../actions/store';
import Immutable from 'immutable';
const initialState = Immutable.Map({
streaming_api_base_url: null,
access_token: null,
me: null
});

View file

@ -3,8 +3,6 @@ import { STORE_HYDRATE } from '../actions/store';
import Immutable from 'immutable';
const initialState = Immutable.Map({
onboarded: false,
home: Immutable.Map({
shows: Immutable.Map({
reblog: true,

View file

@ -48,9 +48,6 @@ const normalizeStatus = (state, status) => {
normalStatus.reblog = status.reblog.id;
}
const linebreakComplemented = status.content.replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n');
normalStatus.unescaped_content = new DOMParser().parseFromString(linebreakComplemented, 'text/html').documentElement.textContent;
return state.update(status.id, Immutable.Map(), map => map.mergeDeep(Immutable.fromJS(normalStatus)));
};

View file

@ -16,10 +16,6 @@ import {
BLOCKS_FETCH_SUCCESS,
BLOCKS_EXPAND_SUCCESS
} from '../actions/blocks';
import {
MUTES_FETCH_SUCCESS,
MUTES_EXPAND_SUCCESS
} from '../actions/mutes';
import Immutable from 'immutable';
const initialState = Immutable.Map({
@ -28,8 +24,7 @@ const initialState = Immutable.Map({
reblogged_by: Immutable.Map(),
favourited_by: Immutable.Map(),
follow_requests: Immutable.Map(),
blocks: Immutable.Map(),
mutes: Immutable.Map()
blocks: Immutable.Map()
});
const normalizeList = (state, type, id, accounts, next) => {
@ -70,10 +65,6 @@ export default function userLists(state = initialState, action) {
return state.setIn(['blocks', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
case BLOCKS_EXPAND_SUCCESS:
return state.updateIn(['blocks', 'items'], list => list.push(...action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
case MUTES_FETCH_SUCCESS:
return state.setIn(['mutes', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
case MUTES_EXPAND_SUCCESS:
return state.updateIn(['mutes', 'items'], list => list.push(...action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
default:
return state;
}

View file

@ -10,8 +10,8 @@ const createWebSocketURL = (url) => {
return a.href;
};
export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) {
const ws = new WebSocketClient(`${createWebSocketURL(streamingAPIBaseURL)}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`);
export default function getStream(accessToken, stream, { connected, received, disconnected, reconnected }) {
const ws = new WebSocketClient(`${createWebSocketURL(STREAMING_API_BASE_URL)}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`);
ws.onopen = connected;
ws.onmessage = e => received(JSON.parse(e.data));

View file

@ -64,7 +64,7 @@
p, li {
font: 16px/28px 'Montserrat', sans-serif;
font-weight: 400;
margin-bottom: 12px;
margin-bottom: 26px;
a {
color: $color4;

View file

@ -889,11 +889,6 @@ a.status__content__spoiler-link {
padding-right: 10px;
}
}
.column {
flex-grow: 1;
max-width: 600px; // This is just a guess at a sane max value
}
}
@media screen and (min-width: 2560px) {
@ -907,11 +902,6 @@ a.status__content__spoiler-link {
height: 90vh;
margin-top: 5vh;
}
.column {
flex-grow: 1;
max-width: 600px; // This is just a guess at a sane max value
}
}
.drawer__pager {
@ -942,12 +932,6 @@ a.status__content__spoiler-link {
}
}
.pseudo-drawer {
background: lighten($color1, 13%);
font-size: 13px;
text-align: left;
}
.drawer__header {
flex: 0 0 auto;
font-size: 16px;
@ -1219,10 +1203,6 @@ a.status__content__spoiler-link {
&:focus {
outline: 0;
}
@media screen and (max-width: 600px) {
font-size: 16px;
}
}
.spoiler-input__input {
@ -1287,10 +1267,6 @@ a.status__content__spoiler-link {
color: $color5;
border-bottom-color: $color4;
}
@media screen and (max-width: 600px) {
font-size: 16px;
}
}
@import 'boost';
@ -1407,7 +1383,7 @@ button.icon-button.active i.fa-retweet {
}
}
.media-spoiler, .video-error-cover {
.media-spoiler {
background: $color8;
color: $color5;
}
@ -1930,10 +1906,6 @@ button.icon-button.active i.fa-retweet {
&:focus {
background: lighten($color1, 4%);
}
@media screen and (max-width: 600px) {
font-size: 16px;
}
}
.search__icon {
@ -2034,7 +2006,6 @@ button.icon-button.active i.fa-retweet {
.modal-root__modal {
pointer-events: auto;
display: flex;
z-index: 9999;
}
.media-modal {
@ -2048,237 +2019,6 @@ button.icon-button.active i.fa-retweet {
}
}
.onboarding-modal {
background: $color2;
color: $color1;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.onboarding-modal__pager {
height: 80vh;
width: 80vw;
max-width: 500px;
max-height: 350px;
position: relative;
& > div {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 25px;
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
display: flex;
opacity: 0;
user-select: text;
}
}
@media screen and (max-width: 550px) {
.onboarding-modal {
width: 100%;
height: 100%;
border-radius: 0;
}
.onboarding-modal__pager {
width: 100%;
height: auto;
max-width: none;
max-height: none;
flex: 1 1 auto;
}
}
.onboarding-modal__paginator {
flex: 0 0 auto;
background: darken($color2, 8%);
display: flex;
padding: 25px;
& > div {
min-width: 33px;
}
a {
color: darken($color2, 34%);
text-decoration: none;
font-size: 14px;
font-weight: 500;
&:hover, &:focus, &:active {
color: darken($color2, 38%);
}
&.onboarding-modal__done, &.onboarding-modal__next {
color: $color4;
}
}
}
.onboarding-modal__dots {
flex: 1 1 auto;
display: flex;
align-items: center;
justify-content: center;
}
.onboarding-modal__dot {
width: 14px;
height: 14px;
border-radius: 14px;
background: darken($color2, 16%);
margin: 0 3px;
cursor: pointer;
&:hover {
background: darken($color2, 18%);
}
&.active {
cursor: default;
background: darken($color2, 24%);
}
}
.onboarding-modal__page {
cursor: default;
line-height: 21px;
h1 {
font-size: 18px;
font-weight: 500;
color: $color1;
margin-bottom: 20px;
}
a {
color: $color4;
&:hover, &:focus, &:active {
color: lighten($color4, 4%);
}
}
p {
font-size: 16px;
color: lighten($color1, 8%);
margin-top: 10px;
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
strong {
font-weight: 500;
background: $color1;
color: $color2;
border-radius: 4px;
font-size: 14px;
padding: 3px 6px;
}
}
}
.onboarding-modal__page-one {
display: flex;
}
.onboarding-modal__page-one__elephant-friend {
background: image-url('elephant-friend.png') no-repeat 0 0;
width: 147px;
height: 160px;
margin-right: 10px;
}
.onboarding-modal__page-two,
.onboarding-modal__page-three,
.onboarding-modal__page-four,
.onboarding-modal__page-five {
p {
text-align: left;
}
.figure {
background: darken($color1, 8%);
color: $color2;
margin-bottom: 20px;
border-radius: 4px;
padding: 10px;
text-align: center;
font-size: 14px;
box-shadow: 1px 2px 6px rgba($color8, 0.3);
.onboarding-modal__image {
border-radius: 4px;
margin-bottom: 10px;
}
&.non-interactive {
pointer-events: none;
text-align: left;
}
}
}
.onboarding-modal__page-four__columns {
.row {
display: flex;
margin-bottom: 20px;
& > div {
flex: 1 1 0;
margin: 0 10px;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
p {
text-align: center;
}
}
&:last-child {
margin-bottom: 0;
}
}
.column-header {
color: $color5;
}
}
.onboarding-modal__image {
border-radius: 8px;
width: 70vw;
max-width: 450px;
max-height: auto;
display: block;
margin: auto;
margin-bottom: 20px;
}
.onboard-sliders {
display: inline-block;
max-width: 30px;
max-height: auto;
margin-left: 10px;
}
.boost-modal {
background: lighten($color2, 8%);
color: $color1;
@ -2316,11 +2056,3 @@ button.icon-button.active i.fa-retweet {
flex: 0 0 auto;
}
}
.loading-bar {
background-color: $color4;
height: 3px;
position: absolute;
top: 0;
left: 0;
}

View file

@ -6,7 +6,3 @@
margin: 0 5px;
}
}
.recovery-codes {
list-style: none;
}

16
app/assets/stylesheets/variables.scss Executable file → Normal file
View file

@ -1,8 +1,8 @@
$color1: #282c37 !default; // darkest
$color2: #d9e1e8 !default; // lightest
$color3: #9baec8 !default; // lighter
$color4: #2b90d9 !default; // vibrant
$color5: #ffffff !default; // white
$color6: #df405a !default; // error red
$color7: #79bd9a !default; // succ green
$color8: #000000 !default; // black
$color1: #282c37; // darkest
$color2: #d9e1e8; // lightest
$color3: #9baec8; // lighter
$color4: #2b90d9; // vibrant
$color5: #ffffff; // white
$color6: #df405a; // error red
$color7: #79bd9a; // succ green
$color8: #000000; // black

View file

@ -15,26 +15,16 @@ module Admin
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id)
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg')
redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed'
else
render action: :new
end
end
def show
@domain_block = DomainBlock.find(params[:id])
end
def destroy
@domain_block = DomainBlock.find(params[:id])
UnblockDomainService.new.call(@domain_block, resource_params[:retroactive])
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg')
end
private
def resource_params
params.require(:domain_block).permit(:domain, :severity, :reject_media, :retroactive)
params.require(:domain_block).permit(:domain, :severity)
end
end
end

View file

@ -1,18 +0,0 @@
# frozen_string_literal: true
module Admin
class ReportedStatusesController < BaseController
def destroy
status = Status.find params[:id]
RemovalWorker.perform_async(status.id)
redirect_to admin_report_path(report)
end
private
def report
Report.find(params[:report_id])
end
end
end

View file

@ -5,60 +5,38 @@ module Admin
before_action :set_report, except: [:index]
def index
@reports = filtered_reports.page(params[:page])
@reports = Report.includes(:account, :target_account).order('id desc').page(params[:page])
@reports = params[:action_taken].present? ? @reports.resolved : @reports.unresolved
end
def show; end
def show
@statuses = Status.where(id: @report.status_ids)
end
def update
process_report
def resolve
@report.update(action_taken: true, action_taken_by_account_id: current_account.id)
redirect_to admin_report_path(@report)
end
def suspend
Admin::SuspensionWorker.perform_async(@report.target_account.id)
Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
redirect_to admin_report_path(@report)
end
def silence
@report.target_account.update(silenced: true)
Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
redirect_to admin_report_path(@report)
end
def remove
RemovalWorker.perform_async(params[:status_id])
redirect_to admin_report_path(@report)
end
private
def process_report
case params[:outcome].to_s
when 'resolve'
@report.update(action_taken_by_current_attributes)
when 'suspend'
Admin::SuspensionWorker.perform_async(@report.target_account.id)
resolve_all_target_account_reports
when 'silence'
@report.target_account.update(silenced: true)
resolve_all_target_account_reports
else
raise ActiveRecord::RecordNotFound
end
end
def action_taken_by_current_attributes
{ action_taken: true, action_taken_by_account_id: current_account.id }
end
def resolve_all_target_account_reports
unresolved_reports_for_target_account.update_all(
action_taken_by_current_attributes
)
end
def unresolved_reports_for_target_account
Report.where(
target_account: @report.target_account
).unresolved
end
def filtered_reports
filtering_scope.order('id desc').includes(
:account,
:target_account
)
end
def filtering_scope
params[:resolved].present? ? Report.resolved : Report.unresolved
end
def set_report
@report = Report.find(params[:id])
end

View file

@ -1,18 +0,0 @@
# frozen_string_literal: true
module Admin
class ResetsController < BaseController
before_action :set_account
def create
@account.user.send_reset_password_instructions
redirect_to admin_accounts_path
end
private
def set_account
@account = Account.find(params[:account_id])
end
end
end

View file

@ -14,7 +14,7 @@ class Api::OEmbedController < ApiController
def stream_entry_from_url(url)
params = Rails.application.routes.recognize_path(url)
raise ActiveRecord::RecordNotFound unless params[:controller] == 'stream_entries' && params[:action] == 'show'
raise ActiveRecord::NotFound unless params[:controller] == 'stream_entries' && params[:action] == 'show'
StreamEntry.find(params[:id])
end

View file

@ -30,7 +30,7 @@ class Api::PushController < ApiController
params = Rails.application.routes.recognize_path(uri.path)
domain = uri.host + (uri.port ? ":#{uri.port}" : '')
return unless TagManager.instance.web_domain?(domain) && params[:controller] == 'accounts' && params[:action] == 'show' && params[:format] == 'atom'
return unless TagManager.instance.local_domain?(domain) && params[:controller] == 'accounts' && params[:action] == 'show' && params[:format] == 'atom'
Account.find_local(params[:username])
end

View file

@ -8,9 +8,7 @@ class ApplicationController < ActionController::Base
force_ssl if: "Rails.env.production? && ENV['LOCAL_HTTPS'] == 'true'"
include Localized
helper_method :current_account
helper_method :single_user_mode?
rescue_from ActionController::RoutingError, with: :not_found
rescue_from ActiveRecord::RecordNotFound, with: :not_found
@ -71,10 +69,6 @@ class ApplicationController < ActionController::Base
end
end
def single_user_mode?
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.first
end
def current_account
@current_account ||= current_user.try(:account)
end

View file

@ -28,7 +28,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end
def check_enabled_registrations
redirect_to root_path if single_user_mode? || !Setting.open_registrations
redirect_to root_path if Rails.configuration.x.single_user_mode || !Setting.open_registrations
end
private

View file

@ -49,8 +49,7 @@ class Auth::SessionsController < Devise::SessionsController
end
def valid_otp_attempt?(user)
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
user.validate_and_consume_otp!(user_params[:otp_attempt])
end
def authenticate_with_two_factor

View file

@ -27,11 +27,7 @@ module Localized
def default_locale
ENV.fetch('DEFAULT_LOCALE') {
user_supplied_locale || I18n.default_locale
http_accept_language.compatible_language_from(I18n.available_locales) || I18n.default_locale
}
end
def user_supplied_locale
http_accept_language.language_region_compatible_from(I18n.available_locales)
end
end

View file

@ -7,14 +7,12 @@ class HomeController < ApplicationController
@body_classes = 'app-body'
@token = find_or_create_access_token.token
@web_settings = Web::Setting.find_by(user: current_user)&.data || {}
@admin = Account.find_local(Setting.site_contact_username)
@streaming_api_base_url = Rails.configuration.x.streaming_api_base_url
end
private
def authenticate_user!
redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in?
redirect_to(Rails.configuration.x.single_user_mode ? account_path(Account.first) : about_path) unless user_signed_in?
end
def find_or_create_access_token

View file

@ -19,9 +19,9 @@ class Settings::TwoFactorAuthsController < ApplicationController
def create
if current_user.validate_and_consume_otp!(confirmation_params[:code])
current_user.otp_required_for_login = true
@codes = current_user.generate_otp_backup_codes!
current_user.save!
flash[:notice] = I18n.t('two_factor_auth.enabled_success')
redirect_to settings_two_factor_auth_path, notice: I18n.t('two_factor_auth.enabled_success')
else
@confirmation = Form::TwoFactorConfirmation.new
set_qr_code
@ -30,12 +30,6 @@ class Settings::TwoFactorAuthsController < ApplicationController
end
end
def recovery_codes
@codes = current_user.generate_otp_backup_codes!
current_user.save!
flash[:notice] = I18n.t('two_factor_auth.recovery_codes_regenerated')
end
def disable
current_user.otp_required_for_login = false
current_user.save!

View file

@ -1,10 +1,9 @@
# frozen_string_literal: true
module Admin::FilterHelper
ACCOUNT_FILTERS = %i[local remote by_domain silenced suspended recent].freeze
REPORT_FILTERS = %i[resolved].freeze
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS
module Admin::AccountsHelper
def filter_params(more_params)
params.permit(:local, :remote, :by_domain, :silenced, :suspended, :recent).merge(more_params)
end
def filter_link_to(text, more_params)
new_url = filtered_url_for(more_params)
@ -17,10 +16,6 @@ module Admin::FilterHelper
private
def filter_params(more_params)
params.permit(FILTERS).merge(more_params)
end
def filter_link_class(new_url)
filtered_url_for(params) == new_url ? 'selected' : ''
end

View file

@ -4,8 +4,4 @@ module ApplicationHelper
def active_nav_class(path)
current_page?(path) ? 'active' : ''
end
def show_landing_strip?
!user_signed_in? && !single_user_mode?
end
end

View file

@ -1,19 +0,0 @@
# frozen_string_literal: true
module StyleHelper
def stylesheet_for_layout
if asset_exist? 'custom.css'
'custom'
else
'application'
end
end
def asset_exist?(path)
if Rails.configuration.assets.compile
Rails.application.precompiled_assets.include? path
else
Rails.application.assets_manifest.assets[path].present?
end
end
end

View file

@ -3,8 +3,6 @@
class AtomSerializer
include RoutingHelper
INVALID_XML_CHARS = /[^\u0009\u000a\u000d\u0020-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]/
class << self
def render(element)
document = Ox::Document.new(version: '1.0')
@ -41,7 +39,7 @@ class AtomSerializer
add_namespaces(feed)
append_element(feed, 'id', account_url(account, format: 'atom'))
append_element(feed, 'title', account.display_name.presence || account.username)
append_element(feed, 'title', account.display_name)
append_element(feed, 'subtitle', account.note)
append_element(feed, 'updated', account.updated_at.iso8601)
append_element(feed, 'logo', full_asset_url(account.avatar.url(:original)))
@ -313,15 +311,11 @@ class AtomSerializer
def append_element(parent, name, content = nil, attributes = {})
element = Ox::Element.new(name)
attributes.each { |k, v| element[k] = sanitize_str(v) }
element << sanitize_str(content) unless content.nil?
attributes.each { |k, v| element[k] = v.to_s }
element << content.to_s unless content.nil?
parent << element
end
def sanitize_str(raw_str)
raw_str.to_s.gsub(INVALID_XML_CHARS, '')
end
def add_namespaces(parent)
parent['xmlns'] = TagManager::XMLNS
parent['xmlns:thr'] = TagManager::THR_XMLNS
@ -333,8 +327,8 @@ class AtomSerializer
end
def serialize_status_attributes(entry, status)
append_element(entry, 'summary', status.spoiler_text, 'xml:lang': status.language) if status.spoiler_text?
append_element(entry, 'content', Formatter.instance.format(status.proper).to_str, type: 'html', 'xml:lang': status.language)
append_element(entry, 'summary', status.spoiler_text) if status.spoiler_text?
append_element(entry, 'content', Formatter.instance.format(status.proper).to_str, type: 'html')
status.mentions.each do |mentioned|
append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': TagManager::TYPES[:person], href: TagManager.instance.uri_for(mentioned.account))

View file

@ -15,7 +15,7 @@ class Formatter
html = status.text
html = encode(html)
html = simple_format(html, {}, sanitize: false)
html = html.delete("\n")
html = html.gsub(/\n/, '')
html = link_urls(html)
html = link_mentions(html, status.mentions)
html = link_hashtags(html)

View file

@ -56,10 +56,6 @@ class TagManager
id.start_with?("tag:#{Rails.configuration.x.local_domain}")
end
def web_domain?(domain)
domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.web_domain).zero?
end
def local_domain?(domain)
domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.local_domain).zero?
end

View file

@ -59,12 +59,7 @@ class NotificationMailer < ApplicationMailer
return if @notifications.empty?
I18n.with_locale(@me.user.locale || I18n.default_locale) do
mail to: @me.user.email,
subject: I18n.t(
:subject,
scope: [:notification_mailer, :digest],
count: @notifications.size
)
mail to: @me.user.email, subject: I18n.t('notification_mailer.digest.subject', count: @notifications.size)
end
end
end

0
app/models/.keep Normal file
View file

View file

View file

@ -3,13 +3,8 @@
class DomainBlock < ApplicationRecord
enum severity: [:silence, :suspend]
attr_accessor :retroactive
validates :domain, presence: true, uniqueness: true
has_many :accounts, foreign_key: :domain, primary_key: :domain
delegate :count, to: :accounts, prefix: true
def self.blocked?(domain)
where(domain: domain, severity: :suspend).exists?
end

View file

@ -1,15 +1,13 @@
# frozen_string_literal: true
class Import < ApplicationRecord
FILE_TYPES = ['text/plain', 'text/csv'].freeze
self.inheritance_column = false
belongs_to :account, required: true
enum type: [:following, :blocking, :muting]
validates :type, presence: true
belongs_to :account
FILE_TYPES = ['text/plain', 'text/csv'].freeze
has_attached_file :data, url: '/system/:hash.:extension', hash_secret: ENV['PAPERCLIP_SECRET']
validates_attachment_content_type :data, content_type: FILE_TYPES

View file

@ -7,8 +7,4 @@ class Report < ApplicationRecord
scope :unresolved, -> { where(action_taken: false) }
scope :resolved, -> { where(action_taken: true) }
def statuses
Status.where(id: status_ids)
end
end

View file

@ -110,10 +110,6 @@ class Status < ApplicationRecord
results
end
def non_sensitive_with_media?
!sensitive? && media_attachments.any?
end
class << self
def as_home_timeline(account)
where(account: [account] + account.following)

View file

@ -5,9 +5,7 @@ class User < ApplicationRecord
devise :registerable, :recoverable,
:rememberable, :trackable, :validatable, :confirmable,
:two_factor_authenticatable, :two_factor_backupable,
otp_secret_encryption_key: ENV['OTP_SECRET'],
otp_number_of_backup_codes: 10
:two_factor_authenticatable, otp_secret_encryption_key: ENV['OTP_SECRET']
belongs_to :account, inverse_of: :user
accepts_nested_attributes_for :account

View file

@ -3,7 +3,7 @@
class InstancePresenter
delegate(
:closed_registrations_message,
:site_contact_email,
:contact_email,
:open_registrations,
:site_description,
:site_extended_description,

View file

@ -1,86 +1,26 @@
# frozen_string_literal: true
class AccountSearchService < BaseService
attr_reader :query, :limit, :resolve, :account
def call(query, limit, resolve = false, account = nil)
@query = query
@limit = limit
@resolve = resolve
@account = account
return [] if query.blank? || query.start_with?('#')
search_service_results
end
username, domain = query.gsub(/\A@/, '').split('@')
domain = nil if TagManager.instance.local_domain?(domain)
private
def search_service_results
return [] if query_blank_or_hashtag?
if resolving_non_matching_remote_account?
[FollowRemoteAccountService.new.call("#{query_username}@#{query_domain}")]
if domain.nil?
exact_match = Account.find_local(username)
results = account.nil? ? Account.search_for(username, limit) : Account.advanced_search_for(username, account, limit)
else
search_results_and_exact_match.compact.uniq
end
exact_match = Account.find_remote(username, domain)
results = account.nil? ? Account.search_for("#{username} #{domain}", limit) : Account.advanced_search_for("#{username} #{domain}", account, limit)
end
def resolving_non_matching_remote_account?
resolve && !exact_match && !domain_is_local?
results = [exact_match] + results.reject { |a| a.id == exact_match.id } if exact_match
if resolve && !exact_match && !domain.nil?
results = [FollowRemoteAccountService.new.call("#{username}@#{domain}")]
end
def search_results_and_exact_match
[exact_match] + search_results.to_a
end
def query_blank_or_hashtag?
query.blank? || query.start_with?('#')
end
def split_query_string
@_split_query_string ||= query.gsub(/\A@/, '').split('@')
end
def query_username
@_query_username ||= split_query_string.first
end
def query_domain
@_query_domain ||= query_without_split? ? nil : split_query_string.last
end
def query_without_split?
split_query_string.size == 1
end
def domain_is_local?
@_domain_is_local ||= TagManager.instance.local_domain?(query_domain)
end
def exact_match
@_exact_match ||= Account.find_remote(query_username, query_domain)
end
def search_results
@_search_results ||= if account
advanced_search_results
else
simple_search_results
end
end
def advanced_search_results
Account.advanced_search_for(terms_for_query, account, limit)
end
def simple_search_results
Account.search_for(terms_for_query, limit)
end
def terms_for_query
if domain_is_local?
query_username
else
"#{query_username} #{query_domain}"
end
results
end
end

View file

@ -3,34 +3,12 @@
class BlockDomainService < BaseService
def call(domain_block)
if domain_block.silence?
silence_accounts!(domain_block.domain)
clear_media!(domain_block.domain) if domain_block.reject_media?
Account.where(domain: domain_block.domain).update_all(silenced: true)
else
suspend_accounts!(domain_block.domain)
end
end
private
def silence_accounts!(domain)
Account.where(domain: domain).update_all(silenced: true)
end
def clear_media!(domain)
Account.where(domain: domain).find_each do |account|
account.avatar.destroy
account.header.destroy
end
MediaAttachment.where(account: Account.where(domain: domain)).find_each do |attachment|
attachment.file.destroy
end
end
def suspend_accounts!(domain)
Account.where(domain: domain).where(suspended: false).find_each do |account|
Account.where(domain: domain_block.domain).find_each do |account|
account.subscription(api_subscription_url(account.id)).unsubscribe if account.subscribed?
SuspendAccountService.new.call(account)
end
end
end
end

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