commit
						68bdedbe5f
					
				
					 276 changed files with 4837 additions and 1332 deletions
				
			
		
							
								
								
									
										1
									
								
								.env.vagrant
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.env.vagrant
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					VAGRANT=true
 | 
				
			||||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| 
						 | 
					@ -22,3 +22,6 @@ public/assets
 | 
				
			||||||
.env.production
 | 
					.env.production
 | 
				
			||||||
node_modules/
 | 
					node_modules/
 | 
				
			||||||
neo4j/
 | 
					neo4j/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Ignore Vagrant files
 | 
				
			||||||
 | 
					.vagrant/
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -87,3 +87,4 @@ AllCops:
 | 
				
			||||||
  - 'bin/*'
 | 
					  - 'bin/*'
 | 
				
			||||||
  - 'Rakefile'
 | 
					  - 'Rakefile'
 | 
				
			||||||
  - 'node_modules/**/*'
 | 
					  - 'node_modules/**/*'
 | 
				
			||||||
 | 
					  - 'Vagrantfile'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										10
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								Gemfile
									
									
									
									
									
								
							| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
source 'https://rubygems.org'
 | 
					source 'https://rubygems.org'
 | 
				
			||||||
 | 
					ruby '2.3.1'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gem 'rails', '~> 5.0.1.0'
 | 
					gem 'rails', '~> 5.0.1.0'
 | 
				
			||||||
gem 'sass-rails', '~> 5.0'
 | 
					gem 'sass-rails', '~> 5.0'
 | 
				
			||||||
| 
						 | 
					@ -16,8 +17,9 @@ gem 'pg'
 | 
				
			||||||
gem 'pghero'
 | 
					gem 'pghero'
 | 
				
			||||||
gem 'dotenv-rails'
 | 
					gem 'dotenv-rails'
 | 
				
			||||||
gem 'font-awesome-rails'
 | 
					gem 'font-awesome-rails'
 | 
				
			||||||
 | 
					gem 'best_in_place', '~> 3.0.1'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gem 'paperclip', '~> 5.0'
 | 
					gem 'paperclip', '~> 5.1'
 | 
				
			||||||
gem 'paperclip-av-transcoder'
 | 
					gem 'paperclip-av-transcoder'
 | 
				
			||||||
gem 'aws-sdk', '>= 2.0'
 | 
					gem 'aws-sdk', '>= 2.0'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,7 +31,6 @@ gem 'link_header'
 | 
				
			||||||
gem 'ostatus2'
 | 
					gem 'ostatus2'
 | 
				
			||||||
gem 'goldfinger'
 | 
					gem 'goldfinger'
 | 
				
			||||||
gem 'devise'
 | 
					gem 'devise'
 | 
				
			||||||
gem 'rails_autolink'
 | 
					 | 
				
			||||||
gem 'doorkeeper'
 | 
					gem 'doorkeeper'
 | 
				
			||||||
gem 'rabl'
 | 
					gem 'rabl'
 | 
				
			||||||
gem 'oj'
 | 
					gem 'oj'
 | 
				
			||||||
| 
						 | 
					@ -42,9 +43,11 @@ gem 'will_paginate'
 | 
				
			||||||
gem 'rack-attack'
 | 
					gem 'rack-attack'
 | 
				
			||||||
gem 'rack-cors', require: 'rack/cors'
 | 
					gem 'rack-cors', require: 'rack/cors'
 | 
				
			||||||
gem 'sidekiq'
 | 
					gem 'sidekiq'
 | 
				
			||||||
gem 'ledermann-rails-settings'
 | 
					gem 'rails-settings-cached'
 | 
				
			||||||
gem 'pg_search'
 | 
					gem 'pg_search'
 | 
				
			||||||
gem 'simple-navigation'
 | 
					gem 'simple-navigation'
 | 
				
			||||||
 | 
					gem 'statsd-instrument'
 | 
				
			||||||
 | 
					gem 'ruby-oembed', require: 'oembed'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gem 'react-rails'
 | 
					gem 'react-rails'
 | 
				
			||||||
gem 'browserify-rails'
 | 
					gem 'browserify-rails'
 | 
				
			||||||
| 
						 | 
					@ -69,6 +72,7 @@ group :development do
 | 
				
			||||||
  gem 'better_errors'
 | 
					  gem 'better_errors'
 | 
				
			||||||
  gem 'binding_of_caller'
 | 
					  gem 'binding_of_caller'
 | 
				
			||||||
  gem 'letter_opener'
 | 
					  gem 'letter_opener'
 | 
				
			||||||
 | 
					  gem 'letter_opener_web'
 | 
				
			||||||
  gem 'bullet'
 | 
					  gem 'bullet'
 | 
				
			||||||
  gem 'active_record_query_trace'
 | 
					  gem 'active_record_query_trace'
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										34
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								Gemfile.lock
									
									
									
									
									
								
							| 
						 | 
					@ -60,6 +60,9 @@ GEM
 | 
				
			||||||
      babel-source (>= 4.0, < 6)
 | 
					      babel-source (>= 4.0, < 6)
 | 
				
			||||||
      execjs (~> 2.0)
 | 
					      execjs (~> 2.0)
 | 
				
			||||||
    bcrypt (3.1.11)
 | 
					    bcrypt (3.1.11)
 | 
				
			||||||
 | 
					    best_in_place (3.0.3)
 | 
				
			||||||
 | 
					      actionpack (>= 3.2)
 | 
				
			||||||
 | 
					      railties (>= 3.2)
 | 
				
			||||||
    better_errors (2.1.1)
 | 
					    better_errors (2.1.1)
 | 
				
			||||||
      coderay (>= 1.0.0)
 | 
					      coderay (>= 1.0.0)
 | 
				
			||||||
      erubis (>= 2.6.6)
 | 
					      erubis (>= 2.6.6)
 | 
				
			||||||
| 
						 | 
					@ -73,8 +76,7 @@ GEM
 | 
				
			||||||
    bullet (5.3.0)
 | 
					    bullet (5.3.0)
 | 
				
			||||||
      activesupport (>= 3.0.0)
 | 
					      activesupport (>= 3.0.0)
 | 
				
			||||||
      uniform_notifier (~> 1.10.0)
 | 
					      uniform_notifier (~> 1.10.0)
 | 
				
			||||||
    climate_control (0.0.3)
 | 
					    climate_control (0.1.0)
 | 
				
			||||||
      activesupport (>= 3.0)
 | 
					 | 
				
			||||||
    cocaine (0.5.8)
 | 
					    cocaine (0.5.8)
 | 
				
			||||||
      climate_control (>= 0.0.3, < 1.0)
 | 
					      climate_control (>= 0.0.3, < 1.0)
 | 
				
			||||||
    coderay (1.1.1)
 | 
					    coderay (1.1.1)
 | 
				
			||||||
| 
						 | 
					@ -86,7 +88,7 @@ GEM
 | 
				
			||||||
      execjs
 | 
					      execjs
 | 
				
			||||||
    coffee-script-source (1.10.0)
 | 
					    coffee-script-source (1.10.0)
 | 
				
			||||||
    colorize (0.8.1)
 | 
					    colorize (0.8.1)
 | 
				
			||||||
    concurrent-ruby (1.0.3)
 | 
					    concurrent-ruby (1.0.4)
 | 
				
			||||||
    connection_pool (2.2.1)
 | 
					    connection_pool (2.2.1)
 | 
				
			||||||
    crack (0.4.3)
 | 
					    crack (0.4.3)
 | 
				
			||||||
      safe_yaml (~> 1.0.0)
 | 
					      safe_yaml (~> 1.0.0)
 | 
				
			||||||
| 
						 | 
					@ -172,10 +174,12 @@ GEM
 | 
				
			||||||
    json (1.8.3)
 | 
					    json (1.8.3)
 | 
				
			||||||
    launchy (2.4.3)
 | 
					    launchy (2.4.3)
 | 
				
			||||||
      addressable (~> 2.3)
 | 
					      addressable (~> 2.3)
 | 
				
			||||||
    ledermann-rails-settings (2.4.2)
 | 
					 | 
				
			||||||
      activerecord (>= 3.1)
 | 
					 | 
				
			||||||
    letter_opener (1.4.1)
 | 
					    letter_opener (1.4.1)
 | 
				
			||||||
      launchy (~> 2.2)
 | 
					      launchy (~> 2.2)
 | 
				
			||||||
 | 
					    letter_opener_web (1.3.0)
 | 
				
			||||||
 | 
					      actionmailer (>= 3.2)
 | 
				
			||||||
 | 
					      letter_opener (~> 1.0)
 | 
				
			||||||
 | 
					      railties (>= 3.2)
 | 
				
			||||||
    link_header (0.0.8)
 | 
					    link_header (0.0.8)
 | 
				
			||||||
    lograge (0.4.1)
 | 
					    lograge (0.4.1)
 | 
				
			||||||
      actionpack (>= 4, < 5.1)
 | 
					      actionpack (>= 4, < 5.1)
 | 
				
			||||||
| 
						 | 
					@ -259,11 +263,11 @@ GEM
 | 
				
			||||||
      nokogiri (~> 1.6.0)
 | 
					      nokogiri (~> 1.6.0)
 | 
				
			||||||
    rails-html-sanitizer (1.0.3)
 | 
					    rails-html-sanitizer (1.0.3)
 | 
				
			||||||
      loofah (~> 2.0)
 | 
					      loofah (~> 2.0)
 | 
				
			||||||
 | 
					    rails-settings-cached (0.6.5)
 | 
				
			||||||
 | 
					      rails (>= 4.2.0)
 | 
				
			||||||
    rails_12factor (0.0.3)
 | 
					    rails_12factor (0.0.3)
 | 
				
			||||||
      rails_serve_static_assets
 | 
					      rails_serve_static_assets
 | 
				
			||||||
      rails_stdout_logging
 | 
					      rails_stdout_logging
 | 
				
			||||||
    rails_autolink (1.1.6)
 | 
					 | 
				
			||||||
      rails (> 3.1)
 | 
					 | 
				
			||||||
    rails_serve_static_assets (0.0.5)
 | 
					    rails_serve_static_assets (0.0.5)
 | 
				
			||||||
    rails_stdout_logging (0.0.5)
 | 
					    rails_stdout_logging (0.0.5)
 | 
				
			||||||
    railties (5.0.1)
 | 
					    railties (5.0.1)
 | 
				
			||||||
| 
						 | 
					@ -332,6 +336,7 @@ GEM
 | 
				
			||||||
      rainbow (>= 1.99.1, < 3.0)
 | 
					      rainbow (>= 1.99.1, < 3.0)
 | 
				
			||||||
      ruby-progressbar (~> 1.7)
 | 
					      ruby-progressbar (~> 1.7)
 | 
				
			||||||
      unicode-display_width (~> 1.0, >= 1.0.1)
 | 
					      unicode-display_width (~> 1.0, >= 1.0.1)
 | 
				
			||||||
 | 
					    ruby-oembed (0.10.1)
 | 
				
			||||||
    ruby-progressbar (1.8.1)
 | 
					    ruby-progressbar (1.8.1)
 | 
				
			||||||
    safe_yaml (1.0.4)
 | 
					    safe_yaml (1.0.4)
 | 
				
			||||||
    sass (3.4.22)
 | 
					    sass (3.4.22)
 | 
				
			||||||
| 
						 | 
					@ -367,6 +372,7 @@ GEM
 | 
				
			||||||
      actionpack (>= 4.0)
 | 
					      actionpack (>= 4.0)
 | 
				
			||||||
      activesupport (>= 4.0)
 | 
					      activesupport (>= 4.0)
 | 
				
			||||||
      sprockets (>= 3.0.0)
 | 
					      sprockets (>= 3.0.0)
 | 
				
			||||||
 | 
					    statsd-instrument (2.1.2)
 | 
				
			||||||
    temple (0.7.7)
 | 
					    temple (0.7.7)
 | 
				
			||||||
    term-ansicolor (1.4.0)
 | 
					    term-ansicolor (1.4.0)
 | 
				
			||||||
      tins (~> 1.0)
 | 
					      tins (~> 1.0)
 | 
				
			||||||
| 
						 | 
					@ -405,6 +411,7 @@ DEPENDENCIES
 | 
				
			||||||
  addressable
 | 
					  addressable
 | 
				
			||||||
  autoprefixer-rails
 | 
					  autoprefixer-rails
 | 
				
			||||||
  aws-sdk (>= 2.0)
 | 
					  aws-sdk (>= 2.0)
 | 
				
			||||||
 | 
					  best_in_place (~> 3.0.1)
 | 
				
			||||||
  better_errors
 | 
					  better_errors
 | 
				
			||||||
  binding_of_caller
 | 
					  binding_of_caller
 | 
				
			||||||
  browserify-rails
 | 
					  browserify-rails
 | 
				
			||||||
| 
						 | 
					@ -426,14 +433,14 @@ DEPENDENCIES
 | 
				
			||||||
  i18n-tasks (~> 0.9.6)
 | 
					  i18n-tasks (~> 0.9.6)
 | 
				
			||||||
  jbuilder (~> 2.0)
 | 
					  jbuilder (~> 2.0)
 | 
				
			||||||
  jquery-rails
 | 
					  jquery-rails
 | 
				
			||||||
  ledermann-rails-settings
 | 
					 | 
				
			||||||
  letter_opener
 | 
					  letter_opener
 | 
				
			||||||
 | 
					  letter_opener_web
 | 
				
			||||||
  link_header
 | 
					  link_header
 | 
				
			||||||
  lograge
 | 
					  lograge
 | 
				
			||||||
  nokogiri
 | 
					  nokogiri
 | 
				
			||||||
  oj
 | 
					  oj
 | 
				
			||||||
  ostatus2
 | 
					  ostatus2
 | 
				
			||||||
  paperclip (~> 5.0)
 | 
					  paperclip (~> 5.1)
 | 
				
			||||||
  paperclip-av-transcoder
 | 
					  paperclip-av-transcoder
 | 
				
			||||||
  pg
 | 
					  pg
 | 
				
			||||||
  pg_search
 | 
					  pg_search
 | 
				
			||||||
| 
						 | 
					@ -445,23 +452,28 @@ DEPENDENCIES
 | 
				
			||||||
  rack-cors
 | 
					  rack-cors
 | 
				
			||||||
  rack-timeout-puma
 | 
					  rack-timeout-puma
 | 
				
			||||||
  rails (~> 5.0.1.0)
 | 
					  rails (~> 5.0.1.0)
 | 
				
			||||||
 | 
					  rails-settings-cached
 | 
				
			||||||
  rails_12factor
 | 
					  rails_12factor
 | 
				
			||||||
  rails_autolink
 | 
					 | 
				
			||||||
  react-rails
 | 
					  react-rails
 | 
				
			||||||
  redis (~> 3.2)
 | 
					  redis (~> 3.2)
 | 
				
			||||||
  redis-rails
 | 
					  redis-rails
 | 
				
			||||||
  rspec-rails
 | 
					  rspec-rails
 | 
				
			||||||
  rspec-sidekiq
 | 
					  rspec-sidekiq
 | 
				
			||||||
  rubocop
 | 
					  rubocop
 | 
				
			||||||
 | 
					  ruby-oembed
 | 
				
			||||||
  sass-rails (~> 5.0)
 | 
					  sass-rails (~> 5.0)
 | 
				
			||||||
  sdoc (~> 0.4.0)
 | 
					  sdoc (~> 0.4.0)
 | 
				
			||||||
  sidekiq
 | 
					  sidekiq
 | 
				
			||||||
  simple-navigation
 | 
					  simple-navigation
 | 
				
			||||||
  simple_form
 | 
					  simple_form
 | 
				
			||||||
  simplecov
 | 
					  simplecov
 | 
				
			||||||
 | 
					  statsd-instrument
 | 
				
			||||||
  uglifier (>= 1.3.0)
 | 
					  uglifier (>= 1.3.0)
 | 
				
			||||||
  webmock
 | 
					  webmock
 | 
				
			||||||
  will_paginate
 | 
					  will_paginate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RUBY VERSION
 | 
				
			||||||
 | 
					   ruby 2.3.1p112
 | 
				
			||||||
 | 
					
 | 
				
			||||||
BUNDLED WITH
 | 
					BUNDLED WITH
 | 
				
			||||||
   1.13.6
 | 
					   1.13.7
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								Procfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								Procfile
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,2 @@
 | 
				
			||||||
 | 
					web: bundle exec puma -C config/puma.rb
 | 
				
			||||||
 | 
					worker: bundle exec sidekiq -q default -q mailers -q push
 | 
				
			||||||
							
								
								
									
										30
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								README.md
									
									
									
									
									
								
							| 
						 | 
					@ -1,11 +1,11 @@
 | 
				
			||||||
Mastodon
 | 
					Mastodon
 | 
				
			||||||
========
 | 
					========
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[][travis]
 | 
					[][travis]
 | 
				
			||||||
[][code_climate]
 | 
					[][code_climate]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[travis]: https://travis-ci.org/Gargron/mastodon
 | 
					[travis]: https://travis-ci.org/tootsuite/mastodon
 | 
				
			||||||
[code_climate]: https://codeclimate.com/github/Gargron/mastodon
 | 
					[code_climate]: https://codeclimate.com/github/tootsuite/mastodon
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Mastodon is a free, open-source social network server. A decentralized alternative to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly.
 | 
					Mastodon is a free, open-source social network server. A decentralized alternative to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,11 +25,11 @@ If you would like, you can [support the development of this project on Patreon][
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Resources
 | 
					## Resources
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- [List of Mastodon instances](https://github.com/Gargron/mastodon/wiki/List-of-Mastodon-instances)
 | 
					- [List of Mastodon instances](docs/Using-Mastodon/List-of-Mastodon-instances.md)
 | 
				
			||||||
- [Use this tool to find Twitter friends on Mastodon](https://mastodon-bridge.herokuapp.com)
 | 
					- [Use this tool to find Twitter friends on Mastodon](https://mastodon-bridge.herokuapp.com)
 | 
				
			||||||
- [API overview](https://github.com/Gargron/mastodon/wiki/API)
 | 
					- [API overview](docs/Using-the-API/API.md)
 | 
				
			||||||
- [How to use the API via cURL/oAuth](https://github.com/Gargron/mastodon/wiki/Testing-with-cURL)
 | 
					- [Frequently Asked Questions](docs/Using-Mastodon/FAQ.md)
 | 
				
			||||||
- [Frequently Asked Questions](https://github.com/Gargron/mastodon/wiki/FAQ)
 | 
					- [List of apps](docs/Using-Mastodon/Apps.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Features
 | 
					## Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -115,7 +115,19 @@ Which will re-create the updated containers, leaving databases and data as is. D
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Deployment without Docker
 | 
					## Deployment without Docker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](https://github.com/Gargron/mastodon/wiki/Production-guide) for examples, configuration and instructions.
 | 
					Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](docs/Running-Mastodon/Production-guide.md) for examples, configuration and instructions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Deployment on Heroku (experimental)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[](https://heroku.com/deploy)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Mastodon can theoretically run indefinitely on a free [Heroku](https://heroku.com) app. [You can view a guide for deployment on Heroku here.](docs/Running-Mastodon/Heroku.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Development with Vagrant
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A quick way to get a development environment up and running is with Vagrant. You will need recent versions of [Vagrant](https://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) installed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[You can find the guide for setting up a Vagrant development environment here.](docs/Running-Mastodon/Vagrant.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Contributing
 | 
					## Contributing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										109
									
								
								Vagrantfile
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								Vagrantfile
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,109 @@
 | 
				
			||||||
 | 
					# -*- mode: ruby -*-
 | 
				
			||||||
 | 
					# vi: set ft=ruby :
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$provision = <<SCRIPT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					cd /vagrant # This is where the host folder/repo is mounted
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Add the yarn repo + yarn repo keys
 | 
				
			||||||
 | 
					curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
 | 
				
			||||||
 | 
					sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Add repo for NodeJS
 | 
				
			||||||
 | 
					curl -sL https://deb.nodesource.com/setup_4.x | sudo bash -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Add firewall rule to redirect 80 to 3000 and save
 | 
				
			||||||
 | 
					sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 3000
 | 
				
			||||||
 | 
					echo iptables-persistent iptables-persistent/autosave_v4 boolean true | sudo debconf-set-selections
 | 
				
			||||||
 | 
					echo iptables-persistent iptables-persistent/autosave_v6 boolean true | sudo debconf-set-selections
 | 
				
			||||||
 | 
					sudo apt-get install iptables-persistent -y
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Add packages to build and run Mastodon
 | 
				
			||||||
 | 
					sudo apt-get install \
 | 
				
			||||||
 | 
					  git-core \
 | 
				
			||||||
 | 
					  g++ \
 | 
				
			||||||
 | 
					  libpq-dev \
 | 
				
			||||||
 | 
					  libxml2-dev \
 | 
				
			||||||
 | 
					  libxslt1-dev \
 | 
				
			||||||
 | 
					  imagemagick \
 | 
				
			||||||
 | 
					  nodejs \
 | 
				
			||||||
 | 
					  redis-server \
 | 
				
			||||||
 | 
					  redis-tools \
 | 
				
			||||||
 | 
					  postgresql \
 | 
				
			||||||
 | 
					  postgresql-contrib \
 | 
				
			||||||
 | 
					  yarn \
 | 
				
			||||||
 | 
					  libreadline-dev \
 | 
				
			||||||
 | 
					  -y
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Install rbenv
 | 
				
			||||||
 | 
					git clone https://github.com/rbenv/rbenv.git ~/.rbenv
 | 
				
			||||||
 | 
					cd ~/.rbenv && src/configure && make -C src
 | 
				
			||||||
 | 
					echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
 | 
				
			||||||
 | 
					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"
 | 
				
			||||||
 | 
					eval "$(rbenv init -)"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo "Compiling Ruby 2.3.1: warning, this takes a while!!!"
 | 
				
			||||||
 | 
					rbenv install 2.3.1
 | 
				
			||||||
 | 
					rbenv global 2.3.1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					cd /vagrant
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Configure database
 | 
				
			||||||
 | 
					sudo -u postgres createuser -U postgres vagrant -s
 | 
				
			||||||
 | 
					sudo -u postgres createdb -U postgres mastodon_development
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Install gems and node modules
 | 
				
			||||||
 | 
					gem install bundler
 | 
				
			||||||
 | 
					bundle install
 | 
				
			||||||
 | 
					yarn install
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Build Mastodon
 | 
				
			||||||
 | 
					bundle exec rails db:setup
 | 
				
			||||||
 | 
					bundle exec rails assets:precompile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SCRIPT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$start = <<SCRIPT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					cd /vagrant
 | 
				
			||||||
 | 
					export $(cat ".env.vagrant" | xargs)
 | 
				
			||||||
 | 
					rails s -d -b 0.0.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SCRIPT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					VAGRANTFILE_API_VERSION = "2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  config.vm.box = "ubuntu/trusty64"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  config.vm.provider :virtualbox do |vb|
 | 
				
			||||||
 | 
					    vb.name = "mastodon"
 | 
				
			||||||
 | 
					    vb.customize ["modifyvm", :id, "--memory", "1024"]
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  config.vm.hostname = "mastodon.dev"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # This uses the vagrant-hostsupdater plugin, and lets you
 | 
				
			||||||
 | 
					  # access the development site at http://mastodon.dev.
 | 
				
			||||||
 | 
					  # To install:
 | 
				
			||||||
 | 
					  #   $ vagrant plugin install hostsupdater
 | 
				
			||||||
 | 
					  if defined?(VagrantPlugins::HostsUpdater)
 | 
				
			||||||
 | 
					    config.vm.network :private_network, ip: "192.168.42.42"
 | 
				
			||||||
 | 
					    config.hostsupdater.remove_on_suspend = false
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Otherwise, you can access the site at http://localhost:3000
 | 
				
			||||||
 | 
					  config.vm.network :forwarded_port, guest: 80, host: 3000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Full provisioning script, only runs on first 'vagrant up' or with 'vagrant provision'
 | 
				
			||||||
 | 
					  config.vm.provision :shell, inline: $provision, privileged: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Start up script, runs on every 'vagrant up'
 | 
				
			||||||
 | 
					  config.vm.provision :shell, inline: $start, run: 'always', privileged: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										91
									
								
								app.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								app.json
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,91 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "Mastodon",
 | 
				
			||||||
 | 
					  "description": "A GNU Social-compatible microblogging server",
 | 
				
			||||||
 | 
					  "repository": "https://github.com/tootsuite/mastodon",
 | 
				
			||||||
 | 
					  "logo": "https://github.com/tootsuite/mastodon/raw/master/app/assets/images/logo.png",
 | 
				
			||||||
 | 
					  "env": {
 | 
				
			||||||
 | 
					    "HEROKU": {
 | 
				
			||||||
 | 
					      "description": "Leave this as true",
 | 
				
			||||||
 | 
					      "value": "true",
 | 
				
			||||||
 | 
					      "required": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "LOCAL_DOMAIN": {
 | 
				
			||||||
 | 
					      "description": "The domain that your Mastodon instance will run on (this can be appname.herokuapp.com or a custom domain)",
 | 
				
			||||||
 | 
					      "required": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "LOCAL_HTTPS": {
 | 
				
			||||||
 | 
					      "description": "Will your domain support HTTPS? (Automatic for herokuapp, requires manual configuration for custom domains)",
 | 
				
			||||||
 | 
					      "value": "false",
 | 
				
			||||||
 | 
					      "required": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "PAPERCLIP_SECRET": {
 | 
				
			||||||
 | 
					      "description": "The secret key for storing media files",
 | 
				
			||||||
 | 
					      "generator": "secret"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "SECRET_KEY_BASE": {
 | 
				
			||||||
 | 
					      "description": "The secret key base",
 | 
				
			||||||
 | 
					      "generator": "secret"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "SINGLE_USER_MODE": {
 | 
				
			||||||
 | 
					      "description": "Should the instance run in single user mode? (Disable registrations, redirect to front page)",
 | 
				
			||||||
 | 
					      "value": "false",
 | 
				
			||||||
 | 
					      "required": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "S3_ENABLED": {
 | 
				
			||||||
 | 
					      "description": "Should Mastodon use Amazon S3 for storage? This is highly recommended, as Heroku does not have persistent file storage (files will be lost).",
 | 
				
			||||||
 | 
					      "value": "true",
 | 
				
			||||||
 | 
					      "required": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "S3_BUCKET": {
 | 
				
			||||||
 | 
					      "description": "Amazon S3 Bucket",
 | 
				
			||||||
 | 
					      "required": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "S3_REGION": {
 | 
				
			||||||
 | 
					      "description": "Amazon S3 region that the bucket is located in",
 | 
				
			||||||
 | 
					      "required": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "AWS_ACCESS_KEY_ID": {
 | 
				
			||||||
 | 
					      "description": "Amazon S3 Access Key",
 | 
				
			||||||
 | 
					      "required": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "AWS_SECRET_ACCESS_KEY": {
 | 
				
			||||||
 | 
					      "description": "Amazon S3 Secret Key",
 | 
				
			||||||
 | 
					      "required": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "SMTP_SERVER": {
 | 
				
			||||||
 | 
					      "description": "Hostname for SMTP server, if you want to enable email",
 | 
				
			||||||
 | 
					      "required": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "SMTP_PORT": {
 | 
				
			||||||
 | 
					      "description": "Port for SMTP server",
 | 
				
			||||||
 | 
					      "required": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "SMTP_LOGIN": {
 | 
				
			||||||
 | 
					      "description": "Username for SMTP server",
 | 
				
			||||||
 | 
					      "required": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "SMTP_PASSWORD": {
 | 
				
			||||||
 | 
					      "description": "Password for SMTP server",
 | 
				
			||||||
 | 
					      "required": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "SMTP_DOMAIN": {
 | 
				
			||||||
 | 
					      "description": "Domain for SMTP server. Will default to instance domain if blank.",
 | 
				
			||||||
 | 
					      "required": false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "buildpacks": [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "url": "heroku/nodejs"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "url": "heroku/ruby"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "postdeploy": "bundle exec rails db:migrate && bundle exec rails db:seed"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "addons": [
 | 
				
			||||||
 | 
					    "heroku-postgresql",
 | 
				
			||||||
 | 
					    "heroku-redis"
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 874 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								app/assets/images/boost_sprite.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/assets/images/boost_sprite.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.3 KiB  | 
| 
						 | 
					@ -1,3 +1,8 @@
 | 
				
			||||||
//= require jquery
 | 
					//= require jquery
 | 
				
			||||||
//= require jquery_ujs
 | 
					//= require jquery_ujs
 | 
				
			||||||
//= require extras
 | 
					//= require extras
 | 
				
			||||||
 | 
					//= require best_in_place
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$(function () {
 | 
				
			||||||
 | 
					  $(".best_in_place").best_in_place();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,6 @@
 | 
				
			||||||
import api, { getLinks } from '../api'
 | 
					import api, { getLinks } from '../api'
 | 
				
			||||||
import Immutable from 'immutable';
 | 
					import Immutable from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ACCOUNT_SET_SELF = 'ACCOUNT_SET_SELF';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
 | 
					export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
 | 
				
			||||||
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
 | 
					export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
 | 
				
			||||||
export const ACCOUNT_FETCH_FAIL    = 'ACCOUNT_FETCH_FAIL';
 | 
					export const ACCOUNT_FETCH_FAIL    = 'ACCOUNT_FETCH_FAIL';
 | 
				
			||||||
| 
						 | 
					@ -67,13 +65,6 @@ export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
 | 
				
			||||||
export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
 | 
					export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
 | 
				
			||||||
export const FOLLOW_REQUEST_REJECT_FAIL    = 'FOLLOW_REQUEST_REJECT_FAIL';
 | 
					export const FOLLOW_REQUEST_REJECT_FAIL    = 'FOLLOW_REQUEST_REJECT_FAIL';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function setAccountSelf(account) {
 | 
					 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    type: ACCOUNT_SET_SELF,
 | 
					 | 
				
			||||||
    account
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function fetchAccount(id) {
 | 
					export function fetchAccount(id) {
 | 
				
			||||||
  return (dispatch, getState) => {
 | 
					  return (dispatch, getState) => {
 | 
				
			||||||
    dispatch(fetchAccountRequest(id));
 | 
					    dispatch(fetchAccountRequest(id));
 | 
				
			||||||
| 
						 | 
					@ -89,32 +80,39 @@ export function fetchAccount(id) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function fetchAccountTimeline(id, replace = false) {
 | 
					export function fetchAccountTimeline(id, replace = false) {
 | 
				
			||||||
  return (dispatch, getState) => {
 | 
					  return (dispatch, getState) => {
 | 
				
			||||||
    dispatch(fetchAccountTimelineRequest(id));
 | 
					    const ids      = getState().getIn(['timelines', 'accounts_timelines', id, 'items'], Immutable.List());
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const ids      = getState().getIn(['timelines', 'accounts_timelines', id], Immutable.List());
 | 
					 | 
				
			||||||
    const newestId = ids.size > 0 ? ids.first() : null;
 | 
					    const newestId = ids.size > 0 ? ids.first() : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let params = '';
 | 
					    let params = '';
 | 
				
			||||||
 | 
					    let skipLoading = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (newestId !== null && !replace) {
 | 
					    if (newestId !== null && !replace) {
 | 
				
			||||||
      params = `?since_id=${newestId}`;
 | 
					      params      = `?since_id=${newestId}`;
 | 
				
			||||||
 | 
					      skipLoading = true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dispatch(fetchAccountTimelineRequest(id, skipLoading));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    api(getState).get(`/api/v1/accounts/${id}/statuses${params}`).then(response => {
 | 
					    api(getState).get(`/api/v1/accounts/${id}/statuses${params}`).then(response => {
 | 
				
			||||||
      dispatch(fetchAccountTimelineSuccess(id, response.data, replace));
 | 
					      dispatch(fetchAccountTimelineSuccess(id, response.data, replace, skipLoading));
 | 
				
			||||||
    }).catch(error => {
 | 
					    }).catch(error => {
 | 
				
			||||||
      dispatch(fetchAccountTimelineFail(id, error));
 | 
					      dispatch(fetchAccountTimelineFail(id, error, skipLoading));
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function expandAccountTimeline(id) {
 | 
					export function expandAccountTimeline(id) {
 | 
				
			||||||
  return (dispatch, getState) => {
 | 
					  return (dispatch, getState) => {
 | 
				
			||||||
    const lastId = getState().getIn(['timelines', 'accounts_timelines', id], Immutable.List()).last();
 | 
					    const lastId = getState().getIn(['timelines', 'accounts_timelines', id, 'items'], Immutable.List()).last();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dispatch(expandAccountTimelineRequest(id));
 | 
					    dispatch(expandAccountTimelineRequest(id));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    api(getState).get(`/api/v1/accounts/${id}/statuses?max_id=${lastId}`).then(response => {
 | 
					    api(getState).get(`/api/v1/accounts/${id}/statuses`, {
 | 
				
			||||||
 | 
					      params: {
 | 
				
			||||||
 | 
					        limit: 10,
 | 
				
			||||||
 | 
					        max_id: lastId
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }).then(response => {
 | 
				
			||||||
      dispatch(expandAccountTimelineSuccess(id, response.data));
 | 
					      dispatch(expandAccountTimelineSuccess(id, response.data));
 | 
				
			||||||
    }).catch(error => {
 | 
					    }).catch(error => {
 | 
				
			||||||
      dispatch(expandAccountTimelineFail(id, error));
 | 
					      dispatch(expandAccountTimelineFail(id, error));
 | 
				
			||||||
| 
						 | 
					@ -210,27 +208,30 @@ export function unfollowAccountFail(error) {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function fetchAccountTimelineRequest(id) {
 | 
					export function fetchAccountTimelineRequest(id, skipLoading) {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    type: ACCOUNT_TIMELINE_FETCH_REQUEST,
 | 
					    type: ACCOUNT_TIMELINE_FETCH_REQUEST,
 | 
				
			||||||
    id
 | 
					    id,
 | 
				
			||||||
 | 
					    skipLoading
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function fetchAccountTimelineSuccess(id, statuses, replace) {
 | 
					export function fetchAccountTimelineSuccess(id, statuses, replace, skipLoading) {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    type: ACCOUNT_TIMELINE_FETCH_SUCCESS,
 | 
					    type: ACCOUNT_TIMELINE_FETCH_SUCCESS,
 | 
				
			||||||
    id,
 | 
					    id,
 | 
				
			||||||
    statuses,
 | 
					    statuses,
 | 
				
			||||||
    replace
 | 
					    replace,
 | 
				
			||||||
 | 
					    skipLoading
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function fetchAccountTimelineFail(id, error) {
 | 
					export function fetchAccountTimelineFail(id, error, skipLoading) {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    type: ACCOUNT_TIMELINE_FETCH_FAIL,
 | 
					    type: ACCOUNT_TIMELINE_FETCH_FAIL,
 | 
				
			||||||
    id,
 | 
					    id,
 | 
				
			||||||
    error
 | 
					    error,
 | 
				
			||||||
 | 
					    skipLoading
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -495,6 +496,10 @@ export function expandFollowingFail(id, error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function fetchRelationships(account_ids) {
 | 
					export function fetchRelationships(account_ids) {
 | 
				
			||||||
  return (dispatch, getState) => {
 | 
					  return (dispatch, getState) => {
 | 
				
			||||||
 | 
					    if (account_ids.length === 0) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dispatch(fetchRelationshipsRequest(account_ids));
 | 
					    dispatch(fetchRelationshipsRequest(account_ids));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    api(getState).get(`/api/v1/accounts/relationships?${account_ids.map(id => `id[]=${id}`).join('&')}`).then(response => {
 | 
					    api(getState).get(`/api/v1/accounts/relationships?${account_ids.map(id => `id[]=${id}`).join('&')}`).then(response => {
 | 
				
			||||||
| 
						 | 
					@ -508,21 +513,24 @@ export function fetchRelationships(account_ids) {
 | 
				
			||||||
export function fetchRelationshipsRequest(ids) {
 | 
					export function fetchRelationshipsRequest(ids) {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    type: RELATIONSHIPS_FETCH_REQUEST,
 | 
					    type: RELATIONSHIPS_FETCH_REQUEST,
 | 
				
			||||||
    ids
 | 
					    ids,
 | 
				
			||||||
 | 
					    skipLoading: true
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function fetchRelationshipsSuccess(relationships) {
 | 
					export function fetchRelationshipsSuccess(relationships) {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    type: RELATIONSHIPS_FETCH_SUCCESS,
 | 
					    type: RELATIONSHIPS_FETCH_SUCCESS,
 | 
				
			||||||
    relationships
 | 
					    relationships,
 | 
				
			||||||
 | 
					    skipLoading: true
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function fetchRelationshipsFail(error) {
 | 
					export function fetchRelationshipsFail(error) {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    type: RELATIONSHIPS_FETCH_FAIL,
 | 
					    type: RELATIONSHIPS_FETCH_FAIL,
 | 
				
			||||||
    error
 | 
					    error,
 | 
				
			||||||
 | 
					    skipLoading: true
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										47
									
								
								app/assets/javascripts/components/actions/cards.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								app/assets/javascripts/components/actions/cards.jsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,47 @@
 | 
				
			||||||
 | 
					import api from '../api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const STATUS_CARD_FETCH_REQUEST = 'STATUS_CARD_FETCH_REQUEST';
 | 
				
			||||||
 | 
					export const STATUS_CARD_FETCH_SUCCESS = 'STATUS_CARD_FETCH_SUCCESS';
 | 
				
			||||||
 | 
					export const STATUS_CARD_FETCH_FAIL    = 'STATUS_CARD_FETCH_FAIL';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function fetchStatusCard(id) {
 | 
				
			||||||
 | 
					  return (dispatch, getState) => {
 | 
				
			||||||
 | 
					    dispatch(fetchStatusCardRequest(id));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    api(getState).get(`/api/v1/statuses/${id}/card`).then(response => {
 | 
				
			||||||
 | 
					      if (!response.data.url || !response.data.title || !response.data.description) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      dispatch(fetchStatusCardSuccess(id, response.data));
 | 
				
			||||||
 | 
					    }).catch(error => {
 | 
				
			||||||
 | 
					      dispatch(fetchStatusCardFail(id, error));
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function fetchStatusCardRequest(id) {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    type: STATUS_CARD_FETCH_REQUEST,
 | 
				
			||||||
 | 
					    id,
 | 
				
			||||||
 | 
					    skipLoading: true
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function fetchStatusCardSuccess(id, card) {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    type: STATUS_CARD_FETCH_SUCCESS,
 | 
				
			||||||
 | 
					    id,
 | 
				
			||||||
 | 
					    card,
 | 
				
			||||||
 | 
					    skipLoading: true
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function fetchStatusCardFail(id, error) {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    type: STATUS_CARD_FETCH_FAIL,
 | 
				
			||||||
 | 
					    id,
 | 
				
			||||||
 | 
					    error,
 | 
				
			||||||
 | 
					    skipLoading: true
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -23,6 +23,8 @@ export const COMPOSE_MOUNT   = 'COMPOSE_MOUNT';
 | 
				
			||||||
export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
 | 
					export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
 | 
					export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
 | 
				
			||||||
 | 
					export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
 | 
				
			||||||
 | 
					export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
 | 
				
			||||||
export const COMPOSE_VISIBILITY_CHANGE  = 'COMPOSE_VISIBILITY_CHANGE';
 | 
					export const COMPOSE_VISIBILITY_CHANGE  = 'COMPOSE_VISIBILITY_CHANGE';
 | 
				
			||||||
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
 | 
					export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -68,6 +70,7 @@ export function submitCompose() {
 | 
				
			||||||
      in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
 | 
					      in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
 | 
				
			||||||
      media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')),
 | 
					      media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')),
 | 
				
			||||||
      sensitive: getState().getIn(['compose', 'sensitive']),
 | 
					      sensitive: getState().getIn(['compose', 'sensitive']),
 | 
				
			||||||
 | 
					      spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
 | 
				
			||||||
      visibility: getState().getIn(['compose', 'private']) ? 'private' : (getState().getIn(['compose', 'unlisted']) ? 'unlisted' : 'public')
 | 
					      visibility: getState().getIn(['compose', 'private']) ? 'private' : (getState().getIn(['compose', 'unlisted']) ? 'unlisted' : 'public')
 | 
				
			||||||
    }).then(function (response) {
 | 
					    }).then(function (response) {
 | 
				
			||||||
      dispatch(submitComposeSuccess({ ...response.data }));
 | 
					      dispatch(submitComposeSuccess({ ...response.data }));
 | 
				
			||||||
| 
						 | 
					@ -218,6 +221,20 @@ export function changeComposeSensitivity(checked) {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function changeComposeSpoilerness(checked) {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    type: COMPOSE_SPOILERNESS_CHANGE,
 | 
				
			||||||
 | 
					    checked
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function changeComposeSpoilerText(text) {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    type: COMPOSE_SPOILER_TEXT_CHANGE,
 | 
				
			||||||
 | 
					    text
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function changeComposeVisibility(checked) {
 | 
					export function changeComposeVisibility(checked) {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    type: COMPOSE_VISIBILITY_CHANGE,
 | 
					    type: COMPOSE_VISIBILITY_CHANGE,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										83
									
								
								app/assets/javascripts/components/actions/favourites.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								app/assets/javascripts/components/actions/favourites.jsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,83 @@
 | 
				
			||||||
 | 
					import api, { getLinks } from '../api'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST';
 | 
				
			||||||
 | 
					export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS';
 | 
				
			||||||
 | 
					export const FAVOURITED_STATUSES_FETCH_FAIL    = 'FAVOURITED_STATUSES_FETCH_FAIL';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const FAVOURITED_STATUSES_EXPAND_REQUEST = 'FAVOURITED_STATUSES_EXPAND_REQUEST';
 | 
				
			||||||
 | 
					export const FAVOURITED_STATUSES_EXPAND_SUCCESS = 'FAVOURITED_STATUSES_EXPAND_SUCCESS';
 | 
				
			||||||
 | 
					export const FAVOURITED_STATUSES_EXPAND_FAIL    = 'FAVOURITED_STATUSES_EXPAND_FAIL';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function fetchFavouritedStatuses() {
 | 
				
			||||||
 | 
					  return (dispatch, getState) => {
 | 
				
			||||||
 | 
					    dispatch(fetchFavouritedStatusesRequest());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    api(getState).get('/api/v1/favourites').then(response => {
 | 
				
			||||||
 | 
					      const next = getLinks(response).refs.find(link => link.rel === 'next');
 | 
				
			||||||
 | 
					      dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
 | 
				
			||||||
 | 
					    }).catch(error => {
 | 
				
			||||||
 | 
					      dispatch(fetchFavouritedStatusesFail(error));
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function fetchFavouritedStatusesRequest() {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    type: FAVOURITED_STATUSES_FETCH_REQUEST
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function fetchFavouritedStatusesSuccess(statuses, next) {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    type: FAVOURITED_STATUSES_FETCH_SUCCESS,
 | 
				
			||||||
 | 
					    statuses,
 | 
				
			||||||
 | 
					    next
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function fetchFavouritedStatusesFail(error) {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    type: FAVOURITED_STATUSES_FETCH_FAIL,
 | 
				
			||||||
 | 
					    error
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function expandFavouritedStatuses() {
 | 
				
			||||||
 | 
					  return (dispatch, getState) => {
 | 
				
			||||||
 | 
					    const url = getState().getIn(['status_lists', 'favourites', 'next'], null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (url === null) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dispatch(expandFavouritedStatusesRequest());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    api(getState).get(url).then(response => {
 | 
				
			||||||
 | 
					      const next = getLinks(response).refs.find(link => link.rel === 'next');
 | 
				
			||||||
 | 
					      dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));
 | 
				
			||||||
 | 
					    }).catch(error => {
 | 
				
			||||||
 | 
					      dispatch(expandFavouritedStatusesFail(error));
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function expandFavouritedStatusesRequest() {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    type: FAVOURITED_STATUSES_EXPAND_REQUEST
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function expandFavouritedStatusesSuccess(statuses, next) {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    type: FAVOURITED_STATUSES_EXPAND_SUCCESS,
 | 
				
			||||||
 | 
					    statuses,
 | 
				
			||||||
 | 
					    next
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function expandFavouritedStatusesFail(error) {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    type: FAVOURITED_STATUSES_EXPAND_FAIL,
 | 
				
			||||||
 | 
					    error
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -1,8 +0,0 @@
 | 
				
			||||||
export const ACCESS_TOKEN_SET = 'ACCESS_TOKEN_SET';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function setAccessToken(token) {
 | 
					 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    type: ACCESS_TOKEN_SET,
 | 
					 | 
				
			||||||
    token: token
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
| 
						 | 
					@ -14,8 +14,6 @@ export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
 | 
				
			||||||
export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
 | 
					export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
 | 
				
			||||||
export const NOTIFICATIONS_EXPAND_FAIL    = 'NOTIFICATIONS_EXPAND_FAIL';
 | 
					export const NOTIFICATIONS_EXPAND_FAIL    = 'NOTIFICATIONS_EXPAND_FAIL';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const NOTIFICATIONS_SETTING_CHANGE = 'NOTIFICATIONS_SETTING_CHANGE';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const fetchRelatedRelationships = (dispatch, notifications) => {
 | 
					const fetchRelatedRelationships = (dispatch, notifications) => {
 | 
				
			||||||
  const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id);
 | 
					  const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,21 +24,25 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function updateNotifications(notification, intlMessages, intlLocale) {
 | 
					export function updateNotifications(notification, intlMessages, intlLocale) {
 | 
				
			||||||
  return (dispatch, getState) => {
 | 
					  return (dispatch, getState) => {
 | 
				
			||||||
 | 
					    const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
 | 
				
			||||||
 | 
					    const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dispatch({
 | 
					    dispatch({
 | 
				
			||||||
      type: NOTIFICATIONS_UPDATE,
 | 
					      type: NOTIFICATIONS_UPDATE,
 | 
				
			||||||
      notification,
 | 
					      notification,
 | 
				
			||||||
      account: notification.account,
 | 
					      account: notification.account,
 | 
				
			||||||
      status: notification.status
 | 
					      status: notification.status,
 | 
				
			||||||
 | 
					      meta: playSound ? { sound: 'boop' } : undefined
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fetchRelatedRelationships(dispatch, [notification]);
 | 
					    fetchRelatedRelationships(dispatch, [notification]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Desktop notifications
 | 
					    // Desktop notifications
 | 
				
			||||||
    if (typeof window.Notification !== 'undefined' && getState().getIn(['notifications', 'settings', 'alerts', notification.type], false)) {
 | 
					    if (typeof window.Notification !== 'undefined' && showAlert) {
 | 
				
			||||||
      const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
 | 
					      const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
 | 
				
			||||||
      const body  = $('<p>').html(notification.status ? notification.status.content : '').text();
 | 
					      const body  = $('<p>').html(notification.status ? notification.status.content : '').text();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      new Notification(title, { body, icon: notification.account.avatar });
 | 
					      new Notification(title, { body, icon: notification.account.avatar, tag: notification.id });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -94,13 +96,17 @@ export function expandNotifications() {
 | 
				
			||||||
  return (dispatch, getState) => {
 | 
					  return (dispatch, getState) => {
 | 
				
			||||||
    const url = getState().getIn(['notifications', 'next'], null);
 | 
					    const url = getState().getIn(['notifications', 'next'], null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (url === null) {
 | 
					    if (url === null || getState().getIn(['notifications', 'isLoading'])) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dispatch(expandNotificationsRequest());
 | 
					    dispatch(expandNotificationsRequest());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    api(getState).get(url).then(response => {
 | 
					    api(getState).get(url, {
 | 
				
			||||||
 | 
					      params: {
 | 
				
			||||||
 | 
					        limit: 5
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }).then(response => {
 | 
				
			||||||
      const next = getLinks(response).refs.find(link => link.rel === 'next');
 | 
					      const next = getLinks(response).refs.find(link => link.rel === 'next');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));
 | 
					      dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));
 | 
				
			||||||
| 
						 | 
					@ -133,11 +139,3 @@ export function expandNotificationsFail(error) {
 | 
				
			||||||
    error
 | 
					    error
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					 | 
				
			||||||
export function changeNotificationsSetting(key, checked) {
 | 
					 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    type: NOTIFICATIONS_SETTING_CHANGE,
 | 
					 | 
				
			||||||
    key,
 | 
					 | 
				
			||||||
    checked
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										19
									
								
								app/assets/javascripts/components/actions/settings.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/assets/javascripts/components/actions/settings.jsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,19 @@
 | 
				
			||||||
 | 
					import axios from 'axios';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SETTING_CHANGE = 'SETTING_CHANGE';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function changeSetting(key, value) {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    type: SETTING_CHANGE,
 | 
				
			||||||
 | 
					    key,
 | 
				
			||||||
 | 
					    value
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function saveSettings() {
 | 
				
			||||||
 | 
					  return (_, getState) => {
 | 
				
			||||||
 | 
					    axios.put('/api/web/settings', {
 | 
				
			||||||
 | 
					      data: getState().get('settings').toJS()
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
import api from '../api';
 | 
					import api from '../api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { deleteFromTimelines } from './timelines';
 | 
					import { deleteFromTimelines } from './timelines';
 | 
				
			||||||
 | 
					import { fetchStatusCard } from './cards';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
 | 
					export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
 | 
				
			||||||
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
 | 
					export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
 | 
				
			||||||
| 
						 | 
					@ -14,39 +15,44 @@ export const CONTEXT_FETCH_REQUEST = 'CONTEXT_FETCH_REQUEST';
 | 
				
			||||||
export const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS';
 | 
					export const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS';
 | 
				
			||||||
export const CONTEXT_FETCH_FAIL    = 'CONTEXT_FETCH_FAIL';
 | 
					export const CONTEXT_FETCH_FAIL    = 'CONTEXT_FETCH_FAIL';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function fetchStatusRequest(id) {
 | 
					export function fetchStatusRequest(id, skipLoading) {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    type: STATUS_FETCH_REQUEST,
 | 
					    type: STATUS_FETCH_REQUEST,
 | 
				
			||||||
    id: id
 | 
					    id,
 | 
				
			||||||
 | 
					    skipLoading
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function fetchStatus(id) {
 | 
					export function fetchStatus(id) {
 | 
				
			||||||
  return (dispatch, getState) => {
 | 
					  return (dispatch, getState) => {
 | 
				
			||||||
    dispatch(fetchStatusRequest(id));
 | 
					    const skipLoading = getState().getIn(['statuses', id], null) !== null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dispatch(fetchStatusRequest(id, skipLoading));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    api(getState).get(`/api/v1/statuses/${id}`).then(response => {
 | 
					    api(getState).get(`/api/v1/statuses/${id}`).then(response => {
 | 
				
			||||||
      dispatch(fetchStatusSuccess(response.data));
 | 
					      dispatch(fetchStatusSuccess(response.data, skipLoading));
 | 
				
			||||||
      dispatch(fetchContext(id));
 | 
					      dispatch(fetchContext(id));
 | 
				
			||||||
 | 
					      dispatch(fetchStatusCard(id));
 | 
				
			||||||
    }).catch(error => {
 | 
					    }).catch(error => {
 | 
				
			||||||
      dispatch(fetchStatusFail(id, error));
 | 
					      dispatch(fetchStatusFail(id, error, skipLoading));
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function fetchStatusSuccess(status, context) {
 | 
					export function fetchStatusSuccess(status, skipLoading) {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    type: STATUS_FETCH_SUCCESS,
 | 
					    type: STATUS_FETCH_SUCCESS,
 | 
				
			||||||
    status: status,
 | 
					    status,
 | 
				
			||||||
    context: context
 | 
					    skipLoading
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function fetchStatusFail(id, error) {
 | 
					export function fetchStatusFail(id, error, skipLoading) {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    type: STATUS_FETCH_FAIL,
 | 
					    type: STATUS_FETCH_FAIL,
 | 
				
			||||||
    id: id,
 | 
					    id,
 | 
				
			||||||
    error: error
 | 
					    error,
 | 
				
			||||||
 | 
					    skipLoading
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										17
									
								
								app/assets/javascripts/components/actions/store.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/assets/javascripts/components/actions/store.jsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,17 @@
 | 
				
			||||||
 | 
					import Immutable from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const STORE_HYDRATE = 'STORE_HYDRATE';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const convertState = rawState =>
 | 
				
			||||||
 | 
					  Immutable.fromJS(rawState, (k, v) =>
 | 
				
			||||||
 | 
					    Immutable.Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x =>
 | 
				
			||||||
 | 
					      Number.isNaN(x * 1) ? x : x * 1));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function hydrateStore(rawState) {
 | 
				
			||||||
 | 
					  const state = convertState(rawState);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    type: STORE_HYDRATE,
 | 
				
			||||||
 | 
					    state
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -14,11 +14,12 @@ export const TIMELINE_EXPAND_FAIL    = 'TIMELINE_EXPAND_FAIL';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
 | 
					export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function refreshTimelineSuccess(timeline, statuses) {
 | 
					export function refreshTimelineSuccess(timeline, statuses, skipLoading) {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    type: TIMELINE_REFRESH_SUCCESS,
 | 
					    type: TIMELINE_REFRESH_SUCCESS,
 | 
				
			||||||
    timeline: timeline,
 | 
					    timeline,
 | 
				
			||||||
    statuses: statuses
 | 
					    statuses,
 | 
				
			||||||
 | 
					    skipLoading
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,55 +40,65 @@ export function deleteFromTimelines(id) {
 | 
				
			||||||
  return (dispatch, getState) => {
 | 
					  return (dispatch, getState) => {
 | 
				
			||||||
    const accountId  = getState().getIn(['statuses', id, 'account']);
 | 
					    const accountId  = getState().getIn(['statuses', id, 'account']);
 | 
				
			||||||
    const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => [status.get('id'), status.get('account')]);
 | 
					    const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => [status.get('id'), status.get('account')]);
 | 
				
			||||||
 | 
					    const reblogOf   = getState().getIn(['statuses', id, 'reblog'], null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dispatch({
 | 
					    dispatch({
 | 
				
			||||||
      type: TIMELINE_DELETE,
 | 
					      type: TIMELINE_DELETE,
 | 
				
			||||||
      id,
 | 
					      id,
 | 
				
			||||||
      accountId,
 | 
					      accountId,
 | 
				
			||||||
      references
 | 
					      references,
 | 
				
			||||||
 | 
					      reblogOf
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function refreshTimelineRequest(timeline, id) {
 | 
					export function refreshTimelineRequest(timeline, id, skipLoading) {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    type: TIMELINE_REFRESH_REQUEST,
 | 
					    type: TIMELINE_REFRESH_REQUEST,
 | 
				
			||||||
    timeline,
 | 
					    timeline,
 | 
				
			||||||
    id
 | 
					    id,
 | 
				
			||||||
 | 
					    skipLoading
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function refreshTimeline(timeline, id = null) {
 | 
					export function refreshTimeline(timeline, id = null) {
 | 
				
			||||||
  return function (dispatch, getState) {
 | 
					  return function (dispatch, getState) {
 | 
				
			||||||
    dispatch(refreshTimelineRequest(timeline, id));
 | 
					    if (getState().getIn(['timelines', timeline, 'isLoading'])) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const ids      = getState().getIn(['timelines', timeline, 'items'], Immutable.List());
 | 
					    const ids      = getState().getIn(['timelines', timeline, 'items'], Immutable.List());
 | 
				
			||||||
    const newestId = ids.size > 0 ? ids.first() : null;
 | 
					    const newestId = ids.size > 0 ? ids.first() : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let params = '';
 | 
					    let params      = '';
 | 
				
			||||||
    let path   = timeline;
 | 
					    let path        = timeline;
 | 
				
			||||||
 | 
					    let skipLoading = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (newestId !== null && getState().getIn(['timelines', timeline, 'loaded'])) {
 | 
					    if (newestId !== null && getState().getIn(['timelines', timeline, 'loaded'])) {
 | 
				
			||||||
      params = `?since_id=${newestId}`;
 | 
					      params      = `?since_id=${newestId}`;
 | 
				
			||||||
 | 
					      skipLoading = true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (id) {
 | 
					    if (id) {
 | 
				
			||||||
      path = `${path}/${id}`
 | 
					      path = `${path}/${id}`
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dispatch(refreshTimelineRequest(timeline, id, skipLoading));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    api(getState).get(`/api/v1/timelines/${path}${params}`).then(function (response) {
 | 
					    api(getState).get(`/api/v1/timelines/${path}${params}`).then(function (response) {
 | 
				
			||||||
      dispatch(refreshTimelineSuccess(timeline, response.data));
 | 
					      dispatch(refreshTimelineSuccess(timeline, response.data, skipLoading));
 | 
				
			||||||
    }).catch(function (error) {
 | 
					    }).catch(function (error) {
 | 
				
			||||||
      dispatch(refreshTimelineFail(timeline, error));
 | 
					      dispatch(refreshTimelineFail(timeline, error, skipLoading));
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function refreshTimelineFail(timeline, error) {
 | 
					export function refreshTimelineFail(timeline, error, skipLoading) {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    type: TIMELINE_REFRESH_FAIL,
 | 
					    type: TIMELINE_REFRESH_FAIL,
 | 
				
			||||||
    timeline,
 | 
					    timeline,
 | 
				
			||||||
    error
 | 
					    error,
 | 
				
			||||||
 | 
					    skipLoading
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -95,6 +106,12 @@ export function expandTimeline(timeline, id = null) {
 | 
				
			||||||
  return (dispatch, getState) => {
 | 
					  return (dispatch, getState) => {
 | 
				
			||||||
    const lastId = getState().getIn(['timelines', timeline, 'items'], Immutable.List()).last();
 | 
					    const lastId = getState().getIn(['timelines', timeline, 'items'], Immutable.List()).last();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!lastId || getState().getIn(['timelines', timeline, 'isLoading'])) {
 | 
				
			||||||
 | 
					      // If timeline is empty, don't try to load older posts since there are none
 | 
				
			||||||
 | 
					      // Also if already loading
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dispatch(expandTimelineRequest(timeline));
 | 
					    dispatch(expandTimelineRequest(timeline));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let path = timeline;
 | 
					    let path = timeline;
 | 
				
			||||||
| 
						 | 
					@ -103,7 +120,12 @@ export function expandTimeline(timeline, id = null) {
 | 
				
			||||||
      path = `${path}/${id}`
 | 
					      path = `${path}/${id}`
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    api(getState).get(`/api/v1/timelines/${path}?max_id=${lastId}`).then(response => {
 | 
					    api(getState).get(`/api/v1/timelines/${path}`, {
 | 
				
			||||||
 | 
					      params: {
 | 
				
			||||||
 | 
					        limit: 10,
 | 
				
			||||||
 | 
					        max_id: lastId
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }).then(response => {
 | 
				
			||||||
      dispatch(expandTimelineSuccess(timeline, response.data));
 | 
					      dispatch(expandTimelineSuccess(timeline, response.data));
 | 
				
			||||||
    }).catch(error => {
 | 
					    }).catch(error => {
 | 
				
			||||||
      dispatch(expandTimelineFail(timeline, error));
 | 
					      dispatch(expandTimelineFail(timeline, error));
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,9 @@ import { defineMessages, injectIntl } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
  follow: { id: 'account.follow', defaultMessage: 'Follow' },
 | 
					  follow: { id: 'account.follow', defaultMessage: 'Follow' },
 | 
				
			||||||
  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }
 | 
					  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
 | 
				
			||||||
 | 
					  requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
 | 
				
			||||||
 | 
					  unblock: { id: 'account.unblock', defaultMessage: 'Unblock' }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const outerStyle = {
 | 
					const outerStyle = {
 | 
				
			||||||
| 
						 | 
					@ -42,7 +44,9 @@ const Account = React.createClass({
 | 
				
			||||||
    account: ImmutablePropTypes.map.isRequired,
 | 
					    account: ImmutablePropTypes.map.isRequired,
 | 
				
			||||||
    me: React.PropTypes.number.isRequired,
 | 
					    me: React.PropTypes.number.isRequired,
 | 
				
			||||||
    onFollow: React.PropTypes.func.isRequired,
 | 
					    onFollow: React.PropTypes.func.isRequired,
 | 
				
			||||||
    withNote: React.PropTypes.bool
 | 
					    onBlock: React.PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    withNote: React.PropTypes.bool,
 | 
				
			||||||
 | 
					    intl: React.PropTypes.object.isRequired
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getDefaultProps () {
 | 
					  getDefaultProps () {
 | 
				
			||||||
| 
						 | 
					@ -57,6 +61,10 @@ const Account = React.createClass({
 | 
				
			||||||
    this.props.onFollow(this.props.account);
 | 
					    this.props.onFollow(this.props.account);
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleBlock () {
 | 
				
			||||||
 | 
					    this.props.onBlock(this.props.account);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { account, me, withNote, intl } = this.props;
 | 
					    const { account, me, withNote, intl } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -70,10 +78,18 @@ const Account = React.createClass({
 | 
				
			||||||
      note = <div style={noteStyle}>{account.get('note')}</div>;
 | 
					      note = <div style={noteStyle}>{account.get('note')}</div>;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (account.get('id') !== me && account.get('relationship', null) != null) {
 | 
					    if (account.get('id') !== me && account.get('relationship', null) !== null) {
 | 
				
			||||||
      const following = account.getIn(['relationship', 'following']);
 | 
					      const following = account.getIn(['relationship', 'following']);
 | 
				
			||||||
 | 
					      const requested = account.getIn(['relationship', 'requested']);
 | 
				
			||||||
 | 
					      const blocking  = account.getIn(['relationship', 'blocking']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
 | 
					      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)} 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} />;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,7 +38,8 @@ const AutosuggestTextarea = React.createClass({
 | 
				
			||||||
    onSuggestionsClearRequested: React.PropTypes.func.isRequired,
 | 
					    onSuggestionsClearRequested: React.PropTypes.func.isRequired,
 | 
				
			||||||
    onSuggestionsFetchRequested: React.PropTypes.func.isRequired,
 | 
					    onSuggestionsFetchRequested: React.PropTypes.func.isRequired,
 | 
				
			||||||
    onChange: React.PropTypes.func.isRequired,
 | 
					    onChange: React.PropTypes.func.isRequired,
 | 
				
			||||||
    onKeyUp: React.PropTypes.func
 | 
					    onKeyUp: React.PropTypes.func,
 | 
				
			||||||
 | 
					    onKeyDown: React.PropTypes.func
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getInitialState () {
 | 
					  getInitialState () {
 | 
				
			||||||
| 
						 | 
					@ -108,15 +109,28 @@ const AutosuggestTextarea = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (e.defaultPrevented || !this.props.onKeyDown) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.props.onKeyDown(e);
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onBlur () {
 | 
					  onBlur () {
 | 
				
			||||||
    this.setState({ suggestionsHidden: true });
 | 
					    // If we hide the suggestions immediately, then this will prevent the
 | 
				
			||||||
 | 
					    // onClick for the suggestions themselves from firing.
 | 
				
			||||||
 | 
					    // Setting a short window for that to take place before hiding the
 | 
				
			||||||
 | 
					    // suggestions ensures that can't happen.
 | 
				
			||||||
 | 
					    setTimeout(() => {
 | 
				
			||||||
 | 
					      this.setState({ suggestionsHidden: true });
 | 
				
			||||||
 | 
					    }, 100);
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onSuggestionClick (suggestion, e) {
 | 
					  onSuggestionClick (suggestion, e) {
 | 
				
			||||||
    e.preventDefault();
 | 
					    e.preventDefault();
 | 
				
			||||||
    this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
 | 
					    this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
 | 
				
			||||||
 | 
					    this.textarea.focus();
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentWillReceiveProps (nextProps) {
 | 
					  componentWillReceiveProps (nextProps) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,12 +8,41 @@ const Avatar = React.createClass({
 | 
				
			||||||
    style: React.PropTypes.object
 | 
					    style: React.PropTypes.object
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getInitialState () {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      hovering: false
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mixins: [PureRenderMixin],
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleMouseEnter () {
 | 
				
			||||||
 | 
					    this.setState({ hovering: true });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleMouseLeave () {
 | 
				
			||||||
 | 
					    this.setState({ hovering: false });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleLoad () {
 | 
				
			||||||
 | 
					    this.canvas.getContext('2d').drawImage(this.image, 0, 0, this.props.size, this.props.size);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setImageRef (c) {
 | 
				
			||||||
 | 
					    this.image = c;
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setCanvasRef (c) {
 | 
				
			||||||
 | 
					    this.canvas = c;
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { hovering } = this.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px` }}>
 | 
					      <div onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px`, position: 'relative' }}>
 | 
				
			||||||
        <img src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ display: 'block', borderRadius: '4px' }} />
 | 
					        <img ref={this.setImageRef} onLoad={this.handleLoad} src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ position: 'absolute', top: '0', left: '0', visibility: hovering ? 'visible' : 'hidden', borderRadius: '4px' }} />
 | 
				
			||||||
 | 
					        <canvas ref={this.setCanvasRef} width={this.props.size} height={this.props.size} style={{ borderRadius: '4px' }} />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,7 +27,7 @@ const Button = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const style = {
 | 
					    const style = {
 | 
				
			||||||
      fontFamily: 'Roboto',
 | 
					      fontFamily: 'inherit',
 | 
				
			||||||
      display: this.props.block ? 'block' : 'inline-block',
 | 
					      display: this.props.block ? 'block' : 'inline-block',
 | 
				
			||||||
      width: this.props.block ? '100%' : 'auto',
 | 
					      width: this.props.block ? '100%' : 'auto',
 | 
				
			||||||
      position: 'relative',
 | 
					      position: 'relative',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,60 @@
 | 
				
			||||||
 | 
					import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
				
			||||||
 | 
					import { Motion, spring } from 'react-motion';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const iconStyle = {
 | 
				
			||||||
 | 
					  fontSize: '16px',
 | 
				
			||||||
 | 
					  padding: '15px',
 | 
				
			||||||
 | 
					  position: 'absolute',
 | 
				
			||||||
 | 
					  right: '0',
 | 
				
			||||||
 | 
					  top: '-48px',
 | 
				
			||||||
 | 
					  cursor: 'pointer'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ColumnCollapsable = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  propTypes: {
 | 
				
			||||||
 | 
					    icon: React.PropTypes.string.isRequired,
 | 
				
			||||||
 | 
					    fullHeight: React.PropTypes.number.isRequired,
 | 
				
			||||||
 | 
					    children: React.PropTypes.node,
 | 
				
			||||||
 | 
					    onCollapse: React.PropTypes.func
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getInitialState () {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      collapsed: true
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleToggleCollapsed () {
 | 
				
			||||||
 | 
					    const currentState = this.state.collapsed;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.setState({ collapsed: !currentState });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!currentState && this.props.onCollapse) {
 | 
				
			||||||
 | 
					      this.props.onCollapse();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { icon, fullHeight, children } = this.props;
 | 
				
			||||||
 | 
					    const { collapsed } = this.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div style={{ position: 'relative' }}>
 | 
				
			||||||
 | 
					        <div style={{...iconStyle, color: collapsed ? '#9baec8' : '#fff', background: collapsed ? '#2f3441' : '#373b4a' }} 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 }) =>
 | 
				
			||||||
 | 
					            <div style={{ overflow: 'hidden', height: `${height}px`, opacity: opacity / 100 }}>
 | 
				
			||||||
 | 
					              {children}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        </Motion>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default ColumnCollapsable;
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,15 @@
 | 
				
			||||||
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
 | 
					import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const DropdownMenu = ({ icon, items, size }) => {
 | 
					const DropdownMenu = ({ icon, items, size, direction }) => {
 | 
				
			||||||
 | 
					  const directionClass = (direction == "left") ? "dropdown__left" : "dropdown__right";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Dropdown>
 | 
					    <Dropdown>
 | 
				
			||||||
      <DropdownTrigger className='icon-button' style={{ fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }}>
 | 
					      <DropdownTrigger className='icon-button' style={{ fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }}>
 | 
				
			||||||
        <i className={`fa fa-fw fa-${icon}`} style={{ verticalAlign: 'middle' }} />
 | 
					        <i className={`fa fa-fw fa-${icon}`} style={{ verticalAlign: 'middle' }} />
 | 
				
			||||||
      </DropdownTrigger>
 | 
					      </DropdownTrigger>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <DropdownContent style={{ lineHeight: '18px', textAlign: 'left' }}>
 | 
					      <DropdownContent className={directionClass} style={{ lineHeight: '18px', textAlign: 'left' }}>
 | 
				
			||||||
        <ul>
 | 
					        <ul>
 | 
				
			||||||
          {items.map(({ text, action, href = '#' }, i) => <li key={i}><a href={href} target='_blank' rel='noopener' onClick={e => {
 | 
					          {items.map(({ text, action, href = '#' }, i) => <li key={i}><a href={href} target='_blank' rel='noopener' onClick={e => {
 | 
				
			||||||
            if (typeof action === 'function') {
 | 
					            if (typeof action === 'function') {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
					import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
				
			||||||
 | 
					import { Motion, spring } from 'react-motion';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const IconButton = React.createClass({
 | 
					const IconButton = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,14 +11,16 @@ const IconButton = React.createClass({
 | 
				
			||||||
    active: React.PropTypes.bool,
 | 
					    active: React.PropTypes.bool,
 | 
				
			||||||
    style: React.PropTypes.object,
 | 
					    style: React.PropTypes.object,
 | 
				
			||||||
    activeStyle: React.PropTypes.object,
 | 
					    activeStyle: React.PropTypes.object,
 | 
				
			||||||
    disabled: React.PropTypes.bool
 | 
					    disabled: React.PropTypes.bool,
 | 
				
			||||||
 | 
					    animate: React.PropTypes.bool
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getDefaultProps () {
 | 
					  getDefaultProps () {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      size: 18,
 | 
					      size: 18,
 | 
				
			||||||
      active: false,
 | 
					      active: false,
 | 
				
			||||||
      disabled: false
 | 
					      disabled: false,
 | 
				
			||||||
 | 
					      animate: false
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,9 +52,18 @@ const IconButton = React.createClass({
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <button aria-label={this.props.title} title={this.props.title} className={`icon-button ${this.props.active ? 'active' : ''} ${this.props.disabled ? 'disabled' : ''}`} onClick={this.handleClick} style={style}>
 | 
					      <Motion defaultStyle={{ rotate: this.props.active ? -360 : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
 | 
				
			||||||
        <i className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' />
 | 
					        {({ rotate }) =>
 | 
				
			||||||
      </button>
 | 
					          <button
 | 
				
			||||||
 | 
					            aria-label={this.props.title}
 | 
				
			||||||
 | 
					            title={this.props.title}
 | 
				
			||||||
 | 
					            className={`icon-button ${this.props.active ? 'active' : ''} ${this.props.disabled ? 'disabled' : ''}`}
 | 
				
			||||||
 | 
					            onClick={this.handleClick}
 | 
				
			||||||
 | 
					            style={style}>
 | 
				
			||||||
 | 
					            <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' />
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      </Motion>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -35,7 +35,9 @@ const Lightbox = React.createClass({
 | 
				
			||||||
  propTypes: {
 | 
					  propTypes: {
 | 
				
			||||||
    isVisible: React.PropTypes.bool,
 | 
					    isVisible: React.PropTypes.bool,
 | 
				
			||||||
    onOverlayClicked: React.PropTypes.func,
 | 
					    onOverlayClicked: React.PropTypes.func,
 | 
				
			||||||
    onCloseClicked: React.PropTypes.func
 | 
					    onCloseClicked: React.PropTypes.func,
 | 
				
			||||||
 | 
					    intl: React.PropTypes.object.isRequired,
 | 
				
			||||||
 | 
					    children: React.PropTypes.node
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mixins: [PureRenderMixin],
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
| 
						 | 
					@ -57,19 +59,17 @@ const Lightbox = React.createClass({
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { intl, isVisible, onOverlayClicked, onCloseClicked, children } = this.props;
 | 
					    const { intl, isVisible, onOverlayClicked, onCloseClicked, children } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const content = isVisible ? children : <div />;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div className='lightbox' style={{...overlayStyle, display: isVisible ? 'flex' : 'none'}} onClick={onOverlayClicked}>
 | 
					      <Motion defaultStyle={{ backgroundOpacity: 0, opacity: 0, y: -400 }} style={{ backgroundOpacity: spring(isVisible ? 50 : 0), opacity: isVisible ? spring(200) : 0, y: spring(isVisible ? 0 : -400, { stiffness: 150, damping: 12 }) }}>
 | 
				
			||||||
        <Motion defaultStyle={{ y: -200 }} style={{ y: spring(isVisible ? 0 : -200) }}>
 | 
					        {({ backgroundOpacity, opacity, y }) =>
 | 
				
			||||||
          {({ y }) =>
 | 
					          <div className='lightbox' style={{...overlayStyle, background: `rgba(0, 0, 0, ${backgroundOpacity / 100})`, display: Math.floor(backgroundOpacity) === 0 ? 'none' : 'flex'}} onClick={onOverlayClicked}>
 | 
				
			||||||
            <div style={{...dialogStyle, transform: `translateY(${y}px)`}}>
 | 
					            <div style={{...dialogStyle, transform: `translateY(${y}px)`, opacity: opacity / 100 }}>
 | 
				
			||||||
              <IconButton title={intl.formatMessage({ id: 'lightbox.close', defaultMessage: 'Close' })} icon='times' onClick={onCloseClicked} size={16} style={closeStyle} />
 | 
					              <IconButton title={intl.formatMessage({ id: 'lightbox.close', defaultMessage: 'Close' })} icon='times' onClick={onCloseClicked} size={16} style={closeStyle} />
 | 
				
			||||||
              {content}
 | 
					              {children}
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          }
 | 
					          </div>
 | 
				
			||||||
        </Motion>
 | 
					        }
 | 
				
			||||||
      </div>
 | 
					      </Motion>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,17 @@
 | 
				
			||||||
import { FormattedMessage } from 'react-intl';
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const LoadingIndicator = () => {
 | 
					const style = {
 | 
				
			||||||
  const style = {
 | 
					  textAlign: 'center',
 | 
				
			||||||
    textAlign: 'center',
 | 
					  fontSize: '16px',
 | 
				
			||||||
    fontSize: '16px',
 | 
					  fontWeight: '500',
 | 
				
			||||||
    fontWeight: '500',
 | 
					  color: '#616b86',
 | 
				
			||||||
    color: '#616b86',
 | 
					  paddingTop: '120px'
 | 
				
			||||||
    paddingTop: '120px'
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return <div style={style}><FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' /></div>;
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const LoadingIndicator = () => (
 | 
				
			||||||
 | 
					  <div style={style}>
 | 
				
			||||||
 | 
					    <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default LoadingIndicator;
 | 
					export default LoadingIndicator;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,12 +1,18 @@
 | 
				
			||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
					import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
				
			||||||
import { FormattedMessage } from 'react-intl';
 | 
					import IconButton from './icon_button';
 | 
				
			||||||
 | 
					import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const messages = defineMessages({
 | 
				
			||||||
 | 
					  toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const outerStyle = {
 | 
					const outerStyle = {
 | 
				
			||||||
  marginTop: '8px',
 | 
					  marginTop: '8px',
 | 
				
			||||||
  overflow: 'hidden',
 | 
					  overflow: 'hidden',
 | 
				
			||||||
  width: '100%',
 | 
					  width: '100%',
 | 
				
			||||||
  boxSizing: 'border-box'
 | 
					  boxSizing: 'border-box',
 | 
				
			||||||
 | 
					  position: 'relative'
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const spoilerStyle = {
 | 
					const spoilerStyle = {
 | 
				
			||||||
| 
						 | 
					@ -32,11 +38,18 @@ const spoilerSubSpanStyle = {
 | 
				
			||||||
  fontWeight: '500'
 | 
					  fontWeight: '500'
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const spoilerButtonStyle = {
 | 
				
			||||||
 | 
					  position: 'absolute',
 | 
				
			||||||
 | 
					  top: '6px',
 | 
				
			||||||
 | 
					  left: '8px',
 | 
				
			||||||
 | 
					  zIndex: '100'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MediaGallery = React.createClass({
 | 
					const MediaGallery = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getInitialState () {
 | 
					  getInitialState () {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      visible: false
 | 
					      visible: !this.props.sensitive
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -59,21 +72,30 @@ const MediaGallery = React.createClass({
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleOpen () {
 | 
					  handleOpen () {
 | 
				
			||||||
    this.setState({ visible: true });
 | 
					    this.setState({ visible: !this.state.visible });
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { media, sensitive } = this.props;
 | 
					    const { media, intl, sensitive } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let children;
 | 
					    let children;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (sensitive && !this.state.visible) {
 | 
					    if (!this.state.visible) {
 | 
				
			||||||
      children = (
 | 
					      if (sensitive) {
 | 
				
			||||||
        <div style={spoilerStyle} onClick={this.handleOpen}>
 | 
					        children = (
 | 
				
			||||||
          <span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
 | 
					          <div style={spoilerStyle} onClick={this.handleOpen}>
 | 
				
			||||||
          <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
 | 
					            <span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
 | 
				
			||||||
        </div>
 | 
					            <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
 | 
				
			||||||
      );
 | 
					          </div>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        children = (
 | 
				
			||||||
 | 
					          <div style={spoilerStyle} onClick={this.handleOpen}>
 | 
				
			||||||
 | 
					            <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>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      const size = media.take(4).size;
 | 
					      const size = media.take(4).size;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -137,6 +159,9 @@ const MediaGallery = React.createClass({
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div style={{ ...outerStyle, height: `${this.props.height}px` }}>
 | 
					      <div style={{ ...outerStyle, height: `${this.props.height}px` }}>
 | 
				
			||||||
 | 
					        <div style={spoilerButtonStyle} >
 | 
				
			||||||
 | 
					          <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleOpen} />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
        {children}
 | 
					        {children}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
| 
						 | 
					@ -144,4 +169,4 @@ const MediaGallery = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default MediaGallery;
 | 
					export default injectIntl(MediaGallery);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,17 @@
 | 
				
			||||||
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const style = {
 | 
				
			||||||
 | 
					  textAlign: 'center',
 | 
				
			||||||
 | 
					  fontSize: '16px',
 | 
				
			||||||
 | 
					  fontWeight: '500',
 | 
				
			||||||
 | 
					  color: '#616b86',
 | 
				
			||||||
 | 
					  paddingTop: '120px'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const MissingIndicator = () => (
 | 
				
			||||||
 | 
					  <div style={style}>
 | 
				
			||||||
 | 
					    <FormattedMessage id='missing_indicator.label' defaultMessage='Not found' />
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default MissingIndicator;
 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,18 @@
 | 
				
			||||||
import {
 | 
					import { injectIntl, FormattedRelative } from 'react-intl';
 | 
				
			||||||
  FormattedMessage,
 | 
					 | 
				
			||||||
  FormattedDate,
 | 
					 | 
				
			||||||
  FormattedRelative
 | 
					 | 
				
			||||||
} from 'react-intl';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const RelativeTimestamp = ({ timestamp }) => {
 | 
					const RelativeTimestamp = ({ intl, timestamp }) => {
 | 
				
			||||||
  return <FormattedRelative value={new Date(timestamp)} />;
 | 
					  const date = new Date(timestamp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <time dateTime={timestamp} title={intl.formatDate(date, { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' })}>
 | 
				
			||||||
 | 
					      <FormattedRelative value={date} />
 | 
				
			||||||
 | 
					    </time>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RelativeTimestamp.propTypes = {
 | 
					RelativeTimestamp.propTypes = {
 | 
				
			||||||
 | 
					  intl: React.PropTypes.object.isRequired,
 | 
				
			||||||
  timestamp: React.PropTypes.string.isRequired
 | 
					  timestamp: React.PropTypes.string.isRequired
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default RelativeTimestamp;
 | 
					export default injectIntl(RelativeTimestamp);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,7 +49,7 @@ const StatusActionBar = React.createClass({
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleMentionClick () {
 | 
					  handleMentionClick () {
 | 
				
			||||||
    this.props.onMention(this.props.status.get('account'));
 | 
					    this.props.onMention(this.props.status.get('account'), this.context.router);
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleBlockClick () {
 | 
					  handleBlockClick () {
 | 
				
			||||||
| 
						 | 
					@ -77,10 +77,10 @@ const StatusActionBar = React.createClass({
 | 
				
			||||||
      <div style={{ marginTop: '10px', overflow: 'hidden' }}>
 | 
					      <div style={{ marginTop: '10px', overflow: 'hidden' }}>
 | 
				
			||||||
        <div style={{ float: 'left', marginRight: '18px'}}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div>
 | 
					        <div style={{ float: 'left', marginRight: '18px'}}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div>
 | 
				
			||||||
        <div style={{ float: 'left', marginRight: '18px'}}><IconButton disabled={status.get('visibility') === 'private'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={status.get('visibility') === 'private' ? 'lock' : 'retweet'} onClick={this.handleReblogClick} /></div>
 | 
					        <div style={{ float: 'left', marginRight: '18px'}}><IconButton disabled={status.get('visibility') === 'private'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={status.get('visibility') === 'private' ? 'lock' : 'retweet'} onClick={this.handleReblogClick} /></div>
 | 
				
			||||||
        <div style={{ float: 'left', marginRight: '18px'}}><IconButton active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>
 | 
					        <div style={{ float: 'left', marginRight: '18px'}}><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div style={{ width: '18px', height: '18px', float: 'left' }}>
 | 
					        <div style={{ width: '18px', height: '18px', float: 'left' }}>
 | 
				
			||||||
          <DropdownMenu items={menu} icon='ellipsis-h' size={18} />
 | 
					          <DropdownMenu items={menu} icon='ellipsis-h' size={18} direction="right" />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
					import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
				
			||||||
import emojify from '../emoji';
 | 
					import emojify from '../emoji';
 | 
				
			||||||
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StatusContent = React.createClass({
 | 
					const StatusContent = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +14,12 @@ const StatusContent = React.createClass({
 | 
				
			||||||
    onClick: React.PropTypes.func
 | 
					    onClick: React.PropTypes.func
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getInitialState () {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      hidden: true
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mixins: [PureRenderMixin],
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentDidMount () {
 | 
					  componentDidMount () {
 | 
				
			||||||
| 
						 | 
					@ -31,8 +38,6 @@ const StatusContent = React.createClass({
 | 
				
			||||||
        link.setAttribute('target', '_blank');
 | 
					        link.setAttribute('target', '_blank');
 | 
				
			||||||
        link.setAttribute('rel', 'noopener');
 | 
					        link.setAttribute('rel', 'noopener');
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					 | 
				
			||||||
      link.addEventListener('click', this.onNormalClick, false);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -52,16 +57,59 @@ const StatusContent = React.createClass({
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onNormalClick (e) {
 | 
					  handleMouseDown (e) {
 | 
				
			||||||
    e.stopPropagation();
 | 
					    this.startXY = [e.clientX, e.clientY];
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleMouseUp (e) {
 | 
				
			||||||
 | 
					    const [ startX, startY ] = this.startXY;
 | 
				
			||||||
 | 
					    const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (e.target.localName === 'a' || (e.target.parentNode && e.target.parentNode.localName === 'a')) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (deltaX + deltaY < 5 && e.button === 0) {
 | 
				
			||||||
 | 
					      this.props.onClick();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.startXY = null;
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleSpoilerClick () {
 | 
				
			||||||
 | 
					    this.setState({ hidden: !this.state.hidden });
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { status, onClick } = this.props;
 | 
					    const { status } = this.props;
 | 
				
			||||||
 | 
					    const { hidden } = this.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const content = { __html: emojify(status.get('content')) };
 | 
					    const content = { __html: emojify(status.get('content')) };
 | 
				
			||||||
 | 
					    const spoilerContent = { __html: emojify(status.get('spoiler_text', '')) };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return <div className='status__content' style={{ cursor: 'pointer' }} dangerouslySetInnerHTML={content} onClick={onClick} />;
 | 
					    if (status.get('spoiler_text').length > 0) {
 | 
				
			||||||
 | 
					      const toggleText = hidden ? <FormattedMessage id='status.show_more' defaultMessage='Show more' /> : <FormattedMessage id='status.show_less' defaultMessage='Show less' />;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <div className='status__content' style={{ cursor: 'pointer' }} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
 | 
				
			||||||
 | 
					          <p style={{ marginBottom: hidden ? '0px' : '' }} >
 | 
				
			||||||
 | 
					            <span dangerouslySetInnerHTML={spoilerContent} /> <a onClick={this.handleSpoilerClick}>{toggleText}</a>
 | 
				
			||||||
 | 
					          </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div style={{ display: hidden ? 'none' : 'block' }} dangerouslySetInnerHTML={content} />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className='status__content'
 | 
				
			||||||
 | 
					          style={{ cursor: 'pointer' }}
 | 
				
			||||||
 | 
					          onMouseDown={this.handleMouseDown}
 | 
				
			||||||
 | 
					          onMouseUp={this.handleMouseUp}
 | 
				
			||||||
 | 
					          dangerouslySetInnerHTML={content}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,8 @@ const StatusList = React.createClass({
 | 
				
			||||||
    onScrollToBottom: React.PropTypes.func,
 | 
					    onScrollToBottom: React.PropTypes.func,
 | 
				
			||||||
    onScrollToTop: React.PropTypes.func,
 | 
					    onScrollToTop: React.PropTypes.func,
 | 
				
			||||||
    onScroll: React.PropTypes.func,
 | 
					    onScroll: React.PropTypes.func,
 | 
				
			||||||
    trackScroll: React.PropTypes.bool
 | 
					    trackScroll: React.PropTypes.bool,
 | 
				
			||||||
 | 
					    isLoading: React.PropTypes.bool
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getDefaultProps () {
 | 
					  getDefaultProps () {
 | 
				
			||||||
| 
						 | 
					@ -24,10 +25,10 @@ const StatusList = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleScroll (e) {
 | 
					  handleScroll (e) {
 | 
				
			||||||
    const { scrollTop, scrollHeight, clientHeight } = e.target;
 | 
					    const { scrollTop, scrollHeight, clientHeight } = e.target;
 | 
				
			||||||
 | 
					    const offset = scrollHeight - scrollTop - clientHeight;
 | 
				
			||||||
    this._oldScrollPosition = scrollHeight - scrollTop;
 | 
					    this._oldScrollPosition = scrollHeight - scrollTop;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (scrollTop === scrollHeight - clientHeight && this.props.onScrollToBottom) {
 | 
					    if (250 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
 | 
				
			||||||
      this.props.onScrollToBottom();
 | 
					      this.props.onScrollToBottom();
 | 
				
			||||||
    } else if (scrollTop < 100 && this.props.onScrollToTop) {
 | 
					    } else if (scrollTop < 100 && this.props.onScrollToTop) {
 | 
				
			||||||
      this.props.onScrollToTop();
 | 
					      this.props.onScrollToTop();
 | 
				
			||||||
| 
						 | 
					@ -36,21 +37,37 @@ const StatusList = React.createClass({
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentDidUpdate (prevProps) {
 | 
					  componentDidMount () {
 | 
				
			||||||
    if (prevProps.statusIds.size < this.props.statusIds.size && prevProps.statusIds.first() !== this.props.statusIds.first() && this._oldScrollPosition) {
 | 
					    this.attachScrollListener();
 | 
				
			||||||
      const node = ReactDOM.findDOMNode(this);
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (node.scrollTop > 0) {
 | 
					  componentDidUpdate (prevProps) {
 | 
				
			||||||
        node.scrollTop = node.scrollHeight - this._oldScrollPosition;
 | 
					    if (this.node.scrollTop > 0 && (prevProps.statusIds.size < this.props.statusIds.size && prevProps.statusIds.first() !== this.props.statusIds.first() && !!this._oldScrollPosition)) {
 | 
				
			||||||
      }
 | 
					      this.node.scrollTop = this.node.scrollHeight - this._oldScrollPosition;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  componentWillUnmount () {
 | 
				
			||||||
 | 
					    this.detachScrollListener();
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attachScrollListener () {
 | 
				
			||||||
 | 
					    this.node.addEventListener('scroll', this.handleScroll);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  detachScrollListener () {
 | 
				
			||||||
 | 
					    this.node.removeEventListener('scroll', this.handleScroll);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setRef (c) {
 | 
				
			||||||
 | 
					    this.node = c;
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { statusIds, onScrollToBottom, trackScroll } = this.props;
 | 
					    const { statusIds, onScrollToBottom, trackScroll } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const scrollableArea = (
 | 
					    const scrollableArea = (
 | 
				
			||||||
      <div className='scrollable' onScroll={this.handleScroll}>
 | 
					      <div className='scrollable' ref={this.setRef}>
 | 
				
			||||||
        <div>
 | 
					        <div>
 | 
				
			||||||
          {statusIds.map((statusId) => {
 | 
					          {statusIds.map((statusId) => {
 | 
				
			||||||
            return <StatusContainer key={statusId} id={statusId} />;
 | 
					            return <StatusContainer key={statusId} id={statusId} />;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,8 @@ import IconButton from './icon_button';
 | 
				
			||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
					import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
  toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' }
 | 
					  toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' },
 | 
				
			||||||
 | 
					  toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const videoStyle = {
 | 
					const videoStyle = {
 | 
				
			||||||
| 
						 | 
					@ -20,7 +21,7 @@ const videoStyle = {
 | 
				
			||||||
const muteStyle = {
 | 
					const muteStyle = {
 | 
				
			||||||
  position: 'absolute',
 | 
					  position: 'absolute',
 | 
				
			||||||
  top: '10px',
 | 
					  top: '10px',
 | 
				
			||||||
  left: '10px',
 | 
					  right: '10px',
 | 
				
			||||||
  opacity: '0.8',
 | 
					  opacity: '0.8',
 | 
				
			||||||
  zIndex: '5'
 | 
					  zIndex: '5'
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -35,7 +36,8 @@ const spoilerStyle = {
 | 
				
			||||||
  display: 'flex',
 | 
					  display: 'flex',
 | 
				
			||||||
  alignItems: 'center',
 | 
					  alignItems: 'center',
 | 
				
			||||||
  justifyContent: 'center',
 | 
					  justifyContent: 'center',
 | 
				
			||||||
  flexDirection: 'column'
 | 
					  flexDirection: 'column',
 | 
				
			||||||
 | 
					  position: 'relative'
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const spoilerSpanStyle = {
 | 
					const spoilerSpanStyle = {
 | 
				
			||||||
| 
						 | 
					@ -49,6 +51,13 @@ const spoilerSubSpanStyle = {
 | 
				
			||||||
  fontWeight: '500'
 | 
					  fontWeight: '500'
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const spoilerButtonStyle = {
 | 
				
			||||||
 | 
					  position: 'absolute',
 | 
				
			||||||
 | 
					  top: '6px',
 | 
				
			||||||
 | 
					  left: '8px',
 | 
				
			||||||
 | 
					  zIndex: '100'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const VideoPlayer = React.createClass({
 | 
					const VideoPlayer = React.createClass({
 | 
				
			||||||
  propTypes: {
 | 
					  propTypes: {
 | 
				
			||||||
    media: ImmutablePropTypes.map.isRequired,
 | 
					    media: ImmutablePropTypes.map.isRequired,
 | 
				
			||||||
| 
						 | 
					@ -66,7 +75,8 @@ const VideoPlayer = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getInitialState () {
 | 
					  getInitialState () {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      visible: false,
 | 
					      visible: !this.props.sensitive,
 | 
				
			||||||
 | 
					      preview: true,
 | 
				
			||||||
      muted: true
 | 
					      muted: true
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -90,22 +100,49 @@ const VideoPlayer = React.createClass({
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleOpen () {
 | 
					  handleOpen () {
 | 
				
			||||||
    this.setState({ visible: true });
 | 
					    this.setState({ preview: !this.state.preview });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleVisibility () {
 | 
				
			||||||
 | 
					    this.setState({
 | 
				
			||||||
 | 
					      visible: !this.state.visible,
 | 
				
			||||||
 | 
					      preview: true
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { media, intl, width, height, sensitive } = this.props;
 | 
					    const { media, intl, width, height, sensitive } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (sensitive && !this.state.visible) {
 | 
					    let spoilerButton = (
 | 
				
			||||||
      return (
 | 
					      <div style={spoilerButtonStyle} >
 | 
				
			||||||
        <div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} onClick={this.handleOpen}>
 | 
					        <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} />
 | 
				
			||||||
          <span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
 | 
					      </div>
 | 
				
			||||||
          <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
 | 
					    );
 | 
				
			||||||
        </div>
 | 
					
 | 
				
			||||||
      );
 | 
					    if (!this.state.visible) {
 | 
				
			||||||
    } else if (!sensitive && !this.state.visible) {
 | 
					      if (sensitive) {
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} 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>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} onClick={this.handleOpen}>
 | 
				
			||||||
 | 
					            {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>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.state.preview) {
 | 
				
			||||||
      return (
 | 
					      return (
 | 
				
			||||||
        <div style={{ cursor: 'pointer', position: 'relative', marginTop: '8px', width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }} onClick={this.handleOpen}>
 | 
					        <div 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 style={{ position: 'absolute', top: '50%', left: '50%', fontSize: '36px', transform: 'translate(-50%, -50%)', padding: '5px', borderRadius: '100px', color: 'rgba(255, 255, 255, 0.8)' }}><i className='fa fa-play' /></div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
| 
						 | 
					@ -113,7 +150,8 @@ const VideoPlayer = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
 | 
					      <div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
 | 
				
			||||||
        <div style={muteStyle}><IconButton title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-up' : 'volume-off'} onClick={this.handleClick} /></div>
 | 
					        {spoilerButton}
 | 
				
			||||||
 | 
					        <div style={muteStyle}><IconButton title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} /></div>
 | 
				
			||||||
        <video src={media.get('url')} autoPlay='true' loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
 | 
					        <video src={media.get('url')} autoPlay='true' loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,9 @@ import { makeGetAccount } from '../selectors';
 | 
				
			||||||
import Account from '../components/account';
 | 
					import Account from '../components/account';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  followAccount,
 | 
					  followAccount,
 | 
				
			||||||
  unfollowAccount
 | 
					  unfollowAccount,
 | 
				
			||||||
 | 
					  blockAccount,
 | 
				
			||||||
 | 
					  unblockAccount
 | 
				
			||||||
} from '../actions/accounts';
 | 
					} from '../actions/accounts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const makeMapStateToProps = () => {
 | 
					const makeMapStateToProps = () => {
 | 
				
			||||||
| 
						 | 
					@ -24,6 +26,14 @@ const mapDispatchToProps = (dispatch) => ({
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      dispatch(followAccount(account.get('id')));
 | 
					      dispatch(followAccount(account.get('id')));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onBlock (account) {
 | 
				
			||||||
 | 
					    if (account.getIn(['relationship', 'blocking'])) {
 | 
				
			||||||
 | 
					      dispatch(unblockAccount(account.get('id')));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      dispatch(blockAccount(account.get('id')));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,15 +7,13 @@ import {
 | 
				
			||||||
  refreshTimeline
 | 
					  refreshTimeline
 | 
				
			||||||
} from '../actions/timelines';
 | 
					} from '../actions/timelines';
 | 
				
			||||||
import { updateNotifications } from '../actions/notifications';
 | 
					import { updateNotifications } from '../actions/notifications';
 | 
				
			||||||
import { setAccessToken } from '../actions/meta';
 | 
					 | 
				
			||||||
import { setAccountSelf } from '../actions/accounts';
 | 
					 | 
				
			||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
					 | 
				
			||||||
import createBrowserHistory from 'history/lib/createBrowserHistory';
 | 
					import createBrowserHistory from 'history/lib/createBrowserHistory';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  applyRouterMiddleware,
 | 
					  applyRouterMiddleware,
 | 
				
			||||||
  useRouterHistory,
 | 
					  useRouterHistory,
 | 
				
			||||||
  Router,
 | 
					  Router,
 | 
				
			||||||
  Route,
 | 
					  Route,
 | 
				
			||||||
 | 
					  IndexRedirect,
 | 
				
			||||||
  IndexRoute
 | 
					  IndexRoute
 | 
				
			||||||
} from 'react-router';
 | 
					} from 'react-router';
 | 
				
			||||||
import { useScroll } from 'react-router-scroll';
 | 
					import { useScroll } from 'react-router-scroll';
 | 
				
			||||||
| 
						 | 
					@ -35,6 +33,8 @@ import Favourites from '../features/favourites';
 | 
				
			||||||
import HashtagTimeline from '../features/hashtag_timeline';
 | 
					import HashtagTimeline from '../features/hashtag_timeline';
 | 
				
			||||||
import Notifications from '../features/notifications';
 | 
					import Notifications from '../features/notifications';
 | 
				
			||||||
import FollowRequests from '../features/follow_requests';
 | 
					import FollowRequests from '../features/follow_requests';
 | 
				
			||||||
 | 
					import GenericNotFound from '../features/generic_not_found';
 | 
				
			||||||
 | 
					import FavouritedStatuses from '../features/favourited_statuses';
 | 
				
			||||||
import { IntlProvider, addLocaleData } from 'react-intl';
 | 
					import { IntlProvider, addLocaleData } from 'react-intl';
 | 
				
			||||||
import en from 'react-intl/locale-data/en';
 | 
					import en from 'react-intl/locale-data/en';
 | 
				
			||||||
import de from 'react-intl/locale-data/de';
 | 
					import de from 'react-intl/locale-data/de';
 | 
				
			||||||
| 
						 | 
					@ -44,9 +44,12 @@ import pt from 'react-intl/locale-data/pt';
 | 
				
			||||||
import hu from 'react-intl/locale-data/hu';
 | 
					import hu from 'react-intl/locale-data/hu';
 | 
				
			||||||
import uk from 'react-intl/locale-data/uk';
 | 
					import uk from 'react-intl/locale-data/uk';
 | 
				
			||||||
import getMessagesForLocale from '../locales';
 | 
					import getMessagesForLocale from '../locales';
 | 
				
			||||||
 | 
					import { hydrateStore } from '../actions/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const store = configureStore();
 | 
					const store = configureStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					store.dispatch(hydrateStore(window.INITIAL_STATE));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const browserHistory = useRouterHistory(createBrowserHistory)({
 | 
					const browserHistory = useRouterHistory(createBrowserHistory)({
 | 
				
			||||||
  basename: '/web'
 | 
					  basename: '/web'
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -56,31 +59,26 @@ addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk]);
 | 
				
			||||||
const Mastodon = React.createClass({
 | 
					const Mastodon = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  propTypes: {
 | 
					  propTypes: {
 | 
				
			||||||
    token: React.PropTypes.string.isRequired,
 | 
					 | 
				
			||||||
    timelines: React.PropTypes.object,
 | 
					 | 
				
			||||||
    account: React.PropTypes.string,
 | 
					 | 
				
			||||||
    locale: React.PropTypes.string.isRequired
 | 
					    locale: React.PropTypes.string.isRequired
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mixins: [PureRenderMixin],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  componentWillMount() {
 | 
					  componentWillMount() {
 | 
				
			||||||
    const { token, account, locale } = this.props;
 | 
					    const { locale } = this.props;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    store.dispatch(setAccessToken(token));
 | 
					 | 
				
			||||||
    store.dispatch(setAccountSelf(JSON.parse(account)));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (typeof App !== 'undefined') {
 | 
					    if (typeof App !== 'undefined') {
 | 
				
			||||||
      this.subscription = App.cable.subscriptions.create('TimelineChannel', {
 | 
					      this.subscription = App.cable.subscriptions.create('TimelineChannel', {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        received (data) {
 | 
					        received (data) {
 | 
				
			||||||
          switch(data.type) {
 | 
					          switch(data.type) {
 | 
				
			||||||
            case 'update':
 | 
					          case 'update':
 | 
				
			||||||
              return store.dispatch(updateTimeline(data.timeline, JSON.parse(data.message)));
 | 
					            store.dispatch(updateTimeline(data.timeline, JSON.parse(data.message)));
 | 
				
			||||||
            case 'delete':
 | 
					            break;
 | 
				
			||||||
              return store.dispatch(deleteFromTimelines(data.id));
 | 
					          case 'delete':
 | 
				
			||||||
            case 'notification':
 | 
					            store.dispatch(deleteFromTimelines(data.id));
 | 
				
			||||||
              return store.dispatch(updateNotifications(JSON.parse(data.message), getMessagesForLocale(locale), locale));
 | 
					            break;
 | 
				
			||||||
 | 
					          case 'notification':
 | 
				
			||||||
 | 
					            store.dispatch(updateNotifications(JSON.parse(data.message), getMessagesForLocale(locale), locale));
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -107,14 +105,16 @@ const Mastodon = React.createClass({
 | 
				
			||||||
        <Provider store={store}>
 | 
					        <Provider store={store}>
 | 
				
			||||||
          <Router history={browserHistory} render={applyRouterMiddleware(useScroll())}>
 | 
					          <Router history={browserHistory} render={applyRouterMiddleware(useScroll())}>
 | 
				
			||||||
            <Route path='/' component={UI}>
 | 
					            <Route path='/' component={UI}>
 | 
				
			||||||
              <IndexRoute component={GettingStarted} />
 | 
					              <IndexRedirect to="/getting-started" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <Route path='getting-started' component={GettingStarted} />
 | 
				
			||||||
              <Route path='timelines/home' component={HomeTimeline} />
 | 
					              <Route path='timelines/home' component={HomeTimeline} />
 | 
				
			||||||
              <Route path='timelines/mentions' component={MentionsTimeline} />
 | 
					              <Route path='timelines/mentions' component={MentionsTimeline} />
 | 
				
			||||||
              <Route path='timelines/public' component={PublicTimeline} />
 | 
					              <Route path='timelines/public' component={PublicTimeline} />
 | 
				
			||||||
              <Route path='timelines/tag/:id' component={HashtagTimeline} />
 | 
					              <Route path='timelines/tag/:id' component={HashtagTimeline} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <Route path='notifications' component={Notifications} />
 | 
					              <Route path='notifications' component={Notifications} />
 | 
				
			||||||
 | 
					              <Route path='favourites' component={FavouritedStatuses} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <Route path='statuses/new' component={Compose} />
 | 
					              <Route path='statuses/new' component={Compose} />
 | 
				
			||||||
              <Route path='statuses/:statusId' component={Status} />
 | 
					              <Route path='statuses/:statusId' component={Status} />
 | 
				
			||||||
| 
						 | 
					@ -128,6 +128,7 @@ const Mastodon = React.createClass({
 | 
				
			||||||
              </Route>
 | 
					              </Route>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              <Route path='follow_requests' component={FollowRequests} />
 | 
					              <Route path='follow_requests' component={FollowRequests} />
 | 
				
			||||||
 | 
					              <Route path='*' component={GenericNotFound} />
 | 
				
			||||||
            </Route>
 | 
					            </Route>
 | 
				
			||||||
          </Router>
 | 
					          </Router>
 | 
				
			||||||
        </Provider>
 | 
					        </Provider>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,6 +15,7 @@ import { blockAccount } from '../actions/accounts';
 | 
				
			||||||
import { deleteStatus } from '../actions/statuses';
 | 
					import { deleteStatus } from '../actions/statuses';
 | 
				
			||||||
import { openMedia } from '../actions/modal';
 | 
					import { openMedia } from '../actions/modal';
 | 
				
			||||||
import { createSelector } from 'reselect'
 | 
					import { createSelector } from 'reselect'
 | 
				
			||||||
 | 
					import { isMobile } from '../is_mobile'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = (state, props) => ({
 | 
					const mapStateToProps = (state, props) => ({
 | 
				
			||||||
  statusBase: state.getIn(['statuses', props.id]),
 | 
					  statusBase: state.getIn(['statuses', props.id]),
 | 
				
			||||||
| 
						 | 
					@ -86,8 +87,11 @@ const mapDispatchToProps = (dispatch) => ({
 | 
				
			||||||
    dispatch(deleteStatus(status.get('id')));
 | 
					    dispatch(deleteStatus(status.get('id')));
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onMention (account) {
 | 
					  onMention (account, router) {
 | 
				
			||||||
    dispatch(mentionCompose(account));
 | 
					    dispatch(mentionCompose(account));
 | 
				
			||||||
 | 
					    if (isMobile(window.innerWidth)) {
 | 
				
			||||||
 | 
					      router.push('/statuses/new');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onOpenMedia (url) {
 | 
					  onOpenMedia (url) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,5 +5,5 @@ emojione.sprites      = false;
 | 
				
			||||||
emojione.imagePathPNG = '/emoji/';
 | 
					emojione.imagePathPNG = '/emoji/';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function emojify(text) {
 | 
					export default function emojify(text) {
 | 
				
			||||||
  return emojione.unicodeToImage(text);
 | 
					  return emojione.toImage(text);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -66,7 +66,7 @@ const ActionBar = React.createClass({
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div style={outerStyle}>
 | 
					      <div style={outerStyle}>
 | 
				
			||||||
        <div style={outerDropdownStyle}>
 | 
					        <div style={outerDropdownStyle}>
 | 
				
			||||||
          <DropdownMenu items={menu} icon='bars' size={24} />
 | 
					          <DropdownMenu items={menu} icon='bars' size={24} direction="right" />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div style={outerLinksStyle}>
 | 
					        <div style={outerLinksStyle}>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -71,8 +71,8 @@ const Header = React.createClass({
 | 
				
			||||||
            <span style={{ display: 'inline-block', color: '#fff', fontSize: '20px', lineHeight: '27px', fontWeight: '500' }} className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
 | 
					            <span style={{ display: 'inline-block', color: '#fff', fontSize: '20px', lineHeight: '27px', fontWeight: '500' }} className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
 | 
				
			||||||
          </a>
 | 
					          </a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <span style={{ fontSize: '14px', fontWeight: '400', display: 'block', color: '#2b90d9', marginBottom: '10px' }}>@{account.get('acct')} {lockedIcon}</span>
 | 
					          <span style={{ fontSize: '14px', fontWeight: '400', display: 'block', color: '#489fde', marginBottom: '10px' }}>@{account.get('acct')} {lockedIcon}</span>
 | 
				
			||||||
          <div style={{ color: '#616b86', fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} />
 | 
					          <div style={{ color: '#d9e1e8', fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          {info}
 | 
					          {info}
 | 
				
			||||||
          {actionBtn}
 | 
					          {actionBtn}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,6 +20,7 @@ import LoadingIndicator      from '../../components/loading_indicator';
 | 
				
			||||||
import ActionBar             from './components/action_bar';
 | 
					import ActionBar             from './components/action_bar';
 | 
				
			||||||
import Column                from '../ui/components/column';
 | 
					import Column                from '../ui/components/column';
 | 
				
			||||||
import ColumnBackButton      from '../../components/column_back_button';
 | 
					import ColumnBackButton      from '../../components/column_back_button';
 | 
				
			||||||
 | 
					import { isMobile } from '../../is_mobile'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const makeMapStateToProps = () => {
 | 
					const makeMapStateToProps = () => {
 | 
				
			||||||
  const getAccount = makeGetAccount();
 | 
					  const getAccount = makeGetAccount();
 | 
				
			||||||
| 
						 | 
					@ -34,11 +35,16 @@ const makeMapStateToProps = () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Account = React.createClass({
 | 
					const Account = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  contextTypes: {
 | 
				
			||||||
 | 
					    router: React.PropTypes.object
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  propTypes: {
 | 
					  propTypes: {
 | 
				
			||||||
    params: React.PropTypes.object.isRequired,
 | 
					    params: React.PropTypes.object.isRequired,
 | 
				
			||||||
    dispatch: React.PropTypes.func.isRequired,
 | 
					    dispatch: React.PropTypes.func.isRequired,
 | 
				
			||||||
    account: ImmutablePropTypes.map,
 | 
					    account: ImmutablePropTypes.map,
 | 
				
			||||||
    me: React.PropTypes.number.isRequired
 | 
					    me: React.PropTypes.number.isRequired,
 | 
				
			||||||
 | 
					    children: React.PropTypes.node
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mixins: [PureRenderMixin],
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
| 
						 | 
					@ -71,6 +77,9 @@ const Account = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleMention () {
 | 
					  handleMention () {
 | 
				
			||||||
    this.props.dispatch(mentionCompose(this.props.account));
 | 
					    this.props.dispatch(mentionCompose(this.props.account));
 | 
				
			||||||
 | 
					    if (isMobile(window.innerWidth)) {
 | 
				
			||||||
 | 
					      this.context.router.push('/statuses/new');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,8 @@ import StatusList from '../../components/status_list';
 | 
				
			||||||
import LoadingIndicator from '../../components/loading_indicator';
 | 
					import LoadingIndicator from '../../components/loading_indicator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = (state, props) => ({
 | 
					const mapStateToProps = (state, props) => ({
 | 
				
			||||||
  statusIds: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId)]),
 | 
					  statusIds: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'items']),
 | 
				
			||||||
 | 
					  isLoading: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'isLoading']),
 | 
				
			||||||
  me: state.getIn(['meta', 'me'])
 | 
					  me: state.getIn(['meta', 'me'])
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,7 +19,9 @@ const AccountTimeline = React.createClass({
 | 
				
			||||||
  propTypes: {
 | 
					  propTypes: {
 | 
				
			||||||
    params: React.PropTypes.object.isRequired,
 | 
					    params: React.PropTypes.object.isRequired,
 | 
				
			||||||
    dispatch: React.PropTypes.func.isRequired,
 | 
					    dispatch: React.PropTypes.func.isRequired,
 | 
				
			||||||
    statusIds: ImmutablePropTypes.list
 | 
					    statusIds: ImmutablePropTypes.list,
 | 
				
			||||||
 | 
					    isLoading: React.PropTypes.bool,
 | 
				
			||||||
 | 
					    me: React.PropTypes.number.isRequired
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mixins: [PureRenderMixin],
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
| 
						 | 
					@ -38,13 +41,13 @@ const AccountTimeline = React.createClass({
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { statusIds, me } = this.props;
 | 
					    const { statusIds, isLoading, me } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!statusIds) {
 | 
					    if (!statusIds) {
 | 
				
			||||||
      return <LoadingIndicator />;
 | 
					      return <LoadingIndicator />;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return <StatusList statusIds={statusIds} me={me} onScrollToBottom={this.handleScrollToBottom} />
 | 
					    return <StatusList statusIds={statusIds} isLoading={isLoading} me={me} onScrollToBottom={this.handleScrollToBottom} />
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,7 @@ import { Motion, spring } from 'react-motion';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
  placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
 | 
					  placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
 | 
				
			||||||
 | 
					  spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Content warning' },
 | 
				
			||||||
  publish: { id: 'compose_form.publish', defaultMessage: 'Publish' }
 | 
					  publish: { id: 'compose_form.publish', defaultMessage: 'Publish' }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,6 +26,8 @@ const ComposeForm = React.createClass({
 | 
				
			||||||
    suggestion_token: React.PropTypes.string,
 | 
					    suggestion_token: React.PropTypes.string,
 | 
				
			||||||
    suggestions: ImmutablePropTypes.list,
 | 
					    suggestions: ImmutablePropTypes.list,
 | 
				
			||||||
    sensitive: React.PropTypes.bool,
 | 
					    sensitive: React.PropTypes.bool,
 | 
				
			||||||
 | 
					    spoiler: React.PropTypes.bool,
 | 
				
			||||||
 | 
					    spoiler_text: React.PropTypes.string,
 | 
				
			||||||
    unlisted: React.PropTypes.bool,
 | 
					    unlisted: React.PropTypes.bool,
 | 
				
			||||||
    private: React.PropTypes.bool,
 | 
					    private: React.PropTypes.bool,
 | 
				
			||||||
    fileDropDate: React.PropTypes.instanceOf(Date),
 | 
					    fileDropDate: React.PropTypes.instanceOf(Date),
 | 
				
			||||||
| 
						 | 
					@ -32,6 +35,7 @@ const ComposeForm = React.createClass({
 | 
				
			||||||
    is_uploading: React.PropTypes.bool,
 | 
					    is_uploading: React.PropTypes.bool,
 | 
				
			||||||
    in_reply_to: ImmutablePropTypes.map,
 | 
					    in_reply_to: ImmutablePropTypes.map,
 | 
				
			||||||
    media_count: React.PropTypes.number,
 | 
					    media_count: React.PropTypes.number,
 | 
				
			||||||
 | 
					    me: React.PropTypes.number,
 | 
				
			||||||
    onChange: React.PropTypes.func.isRequired,
 | 
					    onChange: React.PropTypes.func.isRequired,
 | 
				
			||||||
    onSubmit: React.PropTypes.func.isRequired,
 | 
					    onSubmit: React.PropTypes.func.isRequired,
 | 
				
			||||||
    onCancelReply: React.PropTypes.func.isRequired,
 | 
					    onCancelReply: React.PropTypes.func.isRequired,
 | 
				
			||||||
| 
						 | 
					@ -39,6 +43,8 @@ const ComposeForm = React.createClass({
 | 
				
			||||||
    onFetchSuggestions: React.PropTypes.func.isRequired,
 | 
					    onFetchSuggestions: React.PropTypes.func.isRequired,
 | 
				
			||||||
    onSuggestionSelected: React.PropTypes.func.isRequired,
 | 
					    onSuggestionSelected: React.PropTypes.func.isRequired,
 | 
				
			||||||
    onChangeSensitivity: React.PropTypes.func.isRequired,
 | 
					    onChangeSensitivity: React.PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    onChangeSpoilerness: React.PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    onChangeSpoilerText: React.PropTypes.func.isRequired,
 | 
				
			||||||
    onChangeVisibility: React.PropTypes.func.isRequired,
 | 
					    onChangeVisibility: React.PropTypes.func.isRequired,
 | 
				
			||||||
    onChangeListability: React.PropTypes.func.isRequired,
 | 
					    onChangeListability: React.PropTypes.func.isRequired,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -49,7 +55,7 @@ const ComposeForm = React.createClass({
 | 
				
			||||||
    this.props.onChange(e.target.value);
 | 
					    this.props.onChange(e.target.value);
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleKeyUp (e) {
 | 
					  handleKeyDown (e) {
 | 
				
			||||||
    if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
 | 
					    if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
 | 
				
			||||||
      this.props.onSubmit();
 | 
					      this.props.onSubmit();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -76,6 +82,15 @@ const ComposeForm = React.createClass({
 | 
				
			||||||
    this.props.onChangeSensitivity(e.target.checked);
 | 
					    this.props.onChangeSensitivity(e.target.checked);
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleChangeSpoilerness (e) {
 | 
				
			||||||
 | 
					    this.props.onChangeSpoilerness(e.target.checked);
 | 
				
			||||||
 | 
					    this.props.onChangeSpoilerText('');
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleChangeSpoilerText (e) {
 | 
				
			||||||
 | 
					    this.props.onChangeSpoilerText(e.target.value);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleChangeVisibility (e) {
 | 
					  handleChangeVisibility (e) {
 | 
				
			||||||
    this.props.onChangeVisibility(e.target.checked);
 | 
					    this.props.onChangeVisibility(e.target.checked);
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -85,7 +100,14 @@ const ComposeForm = React.createClass({
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentDidUpdate (prevProps) {
 | 
					  componentDidUpdate (prevProps) {
 | 
				
			||||||
    if (prevProps.in_reply_to !== this.props.in_reply_to) {
 | 
					    if ((prevProps.in_reply_to === null && this.props.in_reply_to !== null) || (prevProps.in_reply_to !== null && this.props.in_reply_to !== null && prevProps.in_reply_to.get('id') !== this.props.in_reply_to.get('id'))) {
 | 
				
			||||||
 | 
					      // If replying to zero or one users, places the cursor at the end of the textbox.
 | 
				
			||||||
 | 
					      // If replying to more than one user, selects any usernames past the first;
 | 
				
			||||||
 | 
					      // this provides a convenient shortcut to drop everyone else from the conversation.
 | 
				
			||||||
 | 
					      const selectionStart = this.props.text.search(/\s/) + 1;
 | 
				
			||||||
 | 
					      const selectionEnd   = this.props.text.length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
 | 
				
			||||||
      this.autosuggestTextarea.textarea.focus();
 | 
					      this.autosuggestTextarea.textarea.focus();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -103,8 +125,18 @@ const ComposeForm = React.createClass({
 | 
				
			||||||
      replyArea = <ReplyIndicator status={this.props.in_reply_to} onCancel={this.props.onCancelReply} />;
 | 
					      replyArea = <ReplyIndicator status={this.props.in_reply_to} onCancel={this.props.onCancelReply} />;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let reply_to_other = !!this.props.in_reply_to && (this.props.in_reply_to.getIn(['account', 'id']) !== this.props.me);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div style={{ padding: '10px' }}>
 | 
					      <div style={{ padding: '10px' }}>
 | 
				
			||||||
 | 
					        <Motion defaultStyle={{ opacity: !this.props.spoiler ? 0 : 100, height: !this.props.spoiler ? 50 : 0 }} style={{ opacity: spring(!this.props.spoiler ? 0 : 100), height: spring(!this.props.spoiler ? 0 : 50) }}>
 | 
				
			||||||
 | 
					          {({ opacity, height }) =>
 | 
				
			||||||
 | 
					            <div className="spoiler-input" style={{ height: `${height}px`, overflow: 'hidden', opacity: opacity / 100 }}>
 | 
				
			||||||
 | 
					              <input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} type="text" className="spoiler-input__input" />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        </Motion>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {replyArea}
 | 
					        {replyArea}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <AutosuggestTextarea
 | 
					        <AutosuggestTextarea
 | 
				
			||||||
| 
						 | 
					@ -115,7 +147,7 @@ const ComposeForm = React.createClass({
 | 
				
			||||||
          value={this.props.text}
 | 
					          value={this.props.text}
 | 
				
			||||||
          onChange={this.handleChange}
 | 
					          onChange={this.handleChange}
 | 
				
			||||||
          suggestions={this.props.suggestions}
 | 
					          suggestions={this.props.suggestions}
 | 
				
			||||||
          onKeyUp={this.handleKeyUp}
 | 
					          onKeyDown={this.handleKeyDown}
 | 
				
			||||||
          onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
 | 
					          onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
 | 
				
			||||||
          onSuggestionsClearRequested={this.onSuggestionsClearRequested}
 | 
					          onSuggestionsClearRequested={this.onSuggestionsClearRequested}
 | 
				
			||||||
          onSuggestionSelected={this.onSuggestionSelected}
 | 
					          onSuggestionSelected={this.onSuggestionSelected}
 | 
				
			||||||
| 
						 | 
					@ -123,7 +155,7 @@ const ComposeForm = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div style={{ marginTop: '10px', overflow: 'hidden' }}>
 | 
					        <div style={{ marginTop: '10px', overflow: 'hidden' }}>
 | 
				
			||||||
          <div style={{ float: 'right' }}><Button text={intl.formatMessage(messages.publish)} onClick={this.handleSubmit} disabled={disabled} /></div>
 | 
					          <div style={{ float: 'right' }}><Button text={intl.formatMessage(messages.publish)} onClick={this.handleSubmit} disabled={disabled} /></div>
 | 
				
			||||||
          <div style={{ float: 'right', marginRight: '16px', lineHeight: '36px' }}><CharacterCounter max={500} text={this.props.text} /></div>
 | 
					          <div style={{ float: 'right', marginRight: '16px', lineHeight: '36px' }}><CharacterCounter max={500} text={[this.props.spoiler_text, this.props.text].join('')} /></div>
 | 
				
			||||||
          <UploadButtonContainer style={{ paddingTop: '4px' }} />
 | 
					          <UploadButtonContainer style={{ paddingTop: '4px' }} />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -132,7 +164,12 @@ const ComposeForm = React.createClass({
 | 
				
			||||||
          <span style={{ display: 'inline-block', verticalAlign: 'middle', marginBottom: '14px', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.private' defaultMessage='Mark as private' /></span>
 | 
					          <span style={{ display: 'inline-block', verticalAlign: 'middle', marginBottom: '14px', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.private' defaultMessage='Mark as private' /></span>
 | 
				
			||||||
        </label>
 | 
					        </label>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Motion defaultStyle={{ opacity: this.props.private ? 0 : 100, height: this.props.private ? 39.5 : 0 }} style={{ opacity: spring(this.props.private ? 0 : 100), height: spring(this.props.private ? 0 : 39.5) }}>
 | 
					        <label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle' }}>
 | 
				
			||||||
 | 
					          <Toggle checked={this.props.spoiler} onChange={this.handleChangeSpoilerness} />
 | 
				
			||||||
 | 
					          <span style={{ display: 'inline-block', verticalAlign: 'middle', marginBottom: '14px', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.spoiler' defaultMessage='Hide behind content warning' /></span>
 | 
				
			||||||
 | 
					        </label>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <Motion defaultStyle={{ opacity: (this.props.private || reply_to_other) ? 0 : 100, height: (this.props.private || reply_to_other) ? 39.5 : 0 }} style={{ opacity: spring((this.props.private || reply_to_other) ? 0 : 100), height: spring((this.props.private || reply_to_other) ? 0 : 39.5) }}>
 | 
				
			||||||
          {({ opacity, height }) =>
 | 
					          {({ opacity, height }) =>
 | 
				
			||||||
            <label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle', height: `${height}px`, overflow: 'hidden', opacity: opacity / 100 }}>
 | 
					            <label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle', height: `${height}px`, overflow: 'hidden', opacity: opacity / 100 }}>
 | 
				
			||||||
              <Toggle checked={this.props.unlisted} onChange={this.handleChangeListability} />
 | 
					              <Toggle checked={this.props.unlisted} onChange={this.handleChangeListability} />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,26 +1,75 @@
 | 
				
			||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
					import { Link } from 'react-router';
 | 
				
			||||||
 | 
					import { injectIntl, defineMessages } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const style = {
 | 
					const messages = defineMessages({
 | 
				
			||||||
 | 
					  start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
 | 
				
			||||||
 | 
					  public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Public timeline' },
 | 
				
			||||||
 | 
					  preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
 | 
				
			||||||
 | 
					  logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const outerStyle = {
 | 
				
			||||||
 | 
					  boxSizing: 'border-box',
 | 
				
			||||||
 | 
					  display: 'flex',
 | 
				
			||||||
 | 
					  flexDirection: 'column',
 | 
				
			||||||
 | 
					  overflowY: 'hidden'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const innerStyle = {
 | 
				
			||||||
  boxSizing: 'border-box',
 | 
					  boxSizing: 'border-box',
 | 
				
			||||||
  background: '#454b5e',
 | 
					 | 
				
			||||||
  padding: '0',
 | 
					  padding: '0',
 | 
				
			||||||
  display: 'flex',
 | 
					  display: 'flex',
 | 
				
			||||||
  flexDirection: 'column',
 | 
					  flexDirection: 'column',
 | 
				
			||||||
  overflowY: 'auto'
 | 
					  overflowY: 'auto',
 | 
				
			||||||
 | 
					  flexGrow: '1'
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Drawer = React.createClass({
 | 
					const tabStyle = {
 | 
				
			||||||
 | 
					  display: 'block',
 | 
				
			||||||
 | 
					  flex: '1 1 auto',
 | 
				
			||||||
 | 
					  padding: '15px',
 | 
				
			||||||
 | 
					  paddingBottom: '13px',
 | 
				
			||||||
 | 
					  color: '#9baec8',
 | 
				
			||||||
 | 
					  textDecoration: 'none',
 | 
				
			||||||
 | 
					  textAlign: 'center',
 | 
				
			||||||
 | 
					  fontSize: '16px',
 | 
				
			||||||
 | 
					  borderBottom: '2px solid transparent'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mixins: [PureRenderMixin],
 | 
					const tabActiveStyle = {
 | 
				
			||||||
 | 
					  color: '#2b90d9',
 | 
				
			||||||
 | 
					  borderBottom: '2px solid #2b90d9'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					const Drawer = ({ children, withHeader, intl }) => {
 | 
				
			||||||
    return (
 | 
					  let header = '';
 | 
				
			||||||
      <div className='drawer' style={style}>
 | 
					
 | 
				
			||||||
        {this.props.children}
 | 
					  if (withHeader) {
 | 
				
			||||||
 | 
					    header = (
 | 
				
			||||||
 | 
					      <div className='drawer__header'>
 | 
				
			||||||
 | 
					        <Link title={intl.formatMessage(messages.start)} style={tabStyle} to='/getting-started'><i className='fa fa-fw fa-asterisk' /></Link>
 | 
				
			||||||
 | 
					        <Link title={intl.formatMessage(messages.public)} style={tabStyle} to='/timelines/public'><i className='fa fa-fw fa-globe' /></Link>
 | 
				
			||||||
 | 
					        <a title={intl.formatMessage(messages.preferences)} style={tabStyle} href='/settings/preferences'><i className='fa fa-fw fa-cog' /></a>
 | 
				
			||||||
 | 
					        <a title={intl.formatMessage(messages.logout)} style={tabStyle} href='/auth/sign_out' data-method='delete'><i className='fa fa-fw fa-sign-out' /></a>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
});
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className='drawer' style={outerStyle}>
 | 
				
			||||||
 | 
					      {header}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default Drawer;
 | 
					      <div className='drawer__inner' style={innerStyle}>
 | 
				
			||||||
 | 
					        {children}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Drawer.propTypes = {
 | 
				
			||||||
 | 
					  withHeader: React.PropTypes.bool,
 | 
				
			||||||
 | 
					  children: React.PropTypes.node,
 | 
				
			||||||
 | 
					  intl: React.PropTypes.object
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default injectIntl(Drawer);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,12 +16,12 @@ const NavigationBar = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div style={{ padding: '10px', display: 'flex', cursor: 'default' }}>
 | 
					      <div style={{ padding: '10px', display: 'flex', flexShrink: '0', cursor: 'default' }}>
 | 
				
			||||||
        <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} size={40} /></Permalink>
 | 
					        <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} size={40} /></Permalink>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div style={{ flex: '1 1 auto', marginLeft: '8px', color: '#9baec8' }}>
 | 
					        <div style={{ flex: '1 1 auto', marginLeft: '8px', color: '#9baec8' }}>
 | 
				
			||||||
          <strong style={{ fontWeight: '500', display: 'block', color: '#fff' }}>{this.props.account.get('acct')}</strong>
 | 
					          <strong style={{ fontWeight: '500', display: 'block', color: '#fff' }}>{this.props.account.get('acct')}</strong>
 | 
				
			||||||
          <a href='/settings/profile' style={{ color: 'inherit', textDecoration: 'none' }}><FormattedMessage id='navigation_bar.settings' defaultMessage='Settings' /></a> · <Link to='/timelines/public' style={{ color: 'inherit', textDecoration: 'none' }}><FormattedMessage id='navigation_bar.public_timeline' defaultMessage='Public timeline' /></Link> · <a href='/auth/sign_out' data-method='delete' style={{ color: 'inherit', textDecoration: 'none' }}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a>
 | 
					          <a href='/settings/profile' style={{ color: 'inherit', textDecoration: 'none' }}><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,7 +38,7 @@ const inputStyle = {
 | 
				
			||||||
  border: 'none',
 | 
					  border: 'none',
 | 
				
			||||||
  padding: '10px',
 | 
					  padding: '10px',
 | 
				
			||||||
  paddingRight: '30px',
 | 
					  paddingRight: '30px',
 | 
				
			||||||
  fontFamily: 'Roboto',
 | 
					  fontFamily: 'inherit',
 | 
				
			||||||
  background: '#282c37',
 | 
					  background: '#282c37',
 | 
				
			||||||
  color: '#9baec8',
 | 
					  color: '#9baec8',
 | 
				
			||||||
  fontSize: '14px',
 | 
					  fontSize: '14px',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,9 @@ const UploadButton = React.createClass({
 | 
				
			||||||
  propTypes: {
 | 
					  propTypes: {
 | 
				
			||||||
    disabled: React.PropTypes.bool,
 | 
					    disabled: React.PropTypes.bool,
 | 
				
			||||||
    onSelectFile: React.PropTypes.func.isRequired,
 | 
					    onSelectFile: React.PropTypes.func.isRequired,
 | 
				
			||||||
    style: React.PropTypes.object
 | 
					    style: React.PropTypes.object,
 | 
				
			||||||
 | 
					    resetFileKey: React.PropTypes.number,
 | 
				
			||||||
 | 
					    intl: React.PropTypes.object.isRequired
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mixins: [PureRenderMixin],
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
| 
						 | 
					@ -31,12 +33,12 @@ const UploadButton = React.createClass({
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { intl } = this.props;
 | 
					    const { intl, resetFileKey, disabled } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div style={this.props.style}>
 | 
					      <div style={this.props.style}>
 | 
				
			||||||
        <IconButton icon='photo' title={intl.formatMessage(messages.upload)} disabled={this.props.disabled} onClick={this.handleClick} size={24} />
 | 
					        <IconButton icon='photo' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} size={24} />
 | 
				
			||||||
        <input ref={this.setRef} type='file' multiple={false} onChange={this.handleChange} disabled={this.props.disabled} style={{ display: 'none' }} />
 | 
					        <input key={resetFileKey} ref={this.setRef} type='file' multiple={false} onChange={this.handleChange} disabled={disabled} style={{ display: 'none' }} />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,15 +12,20 @@ const UploadForm = React.createClass({
 | 
				
			||||||
  propTypes: {
 | 
					  propTypes: {
 | 
				
			||||||
    media: ImmutablePropTypes.list.isRequired,
 | 
					    media: ImmutablePropTypes.list.isRequired,
 | 
				
			||||||
    is_uploading: React.PropTypes.bool,
 | 
					    is_uploading: React.PropTypes.bool,
 | 
				
			||||||
    onRemoveFile: React.PropTypes.func.isRequired
 | 
					    onRemoveFile: React.PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    intl: React.PropTypes.object.isRequired
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mixins: [PureRenderMixin],
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { intl } = this.props;
 | 
					    const { intl, media } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const uploads = this.props.media.map(attachment => (
 | 
					    if (!media.size) {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const uploads = media.map(attachment => (
 | 
				
			||||||
      <div key={attachment.get('id')} style={{ borderRadius: '4px', marginBottom: '10px' }} className='transparent-background'>
 | 
					      <div key={attachment.get('id')} style={{ borderRadius: '4px', marginBottom: '10px' }} className='transparent-background'>
 | 
				
			||||||
        <div style={{ width: '100%', height: '100px', borderRadius: '4px', background: `url(${attachment.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }}>
 | 
					        <div style={{ width: '100%', height: '100px', borderRadius: '4px', background: `url(${attachment.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }}>
 | 
				
			||||||
          <IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.props.onRemoveFile.bind(this, attachment.get('id'))} />
 | 
					          <IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.props.onRemoveFile.bind(this, attachment.get('id'))} />
 | 
				
			||||||
| 
						 | 
					@ -29,7 +34,7 @@ const UploadForm = React.createClass({
 | 
				
			||||||
    ));
 | 
					    ));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div style={{ marginBottom: '20px', padding: '10px', overflow: 'hidden' }}>
 | 
					      <div style={{ marginBottom: '20px', padding: '10px', overflow: 'hidden', flexShrink: '0' }}>
 | 
				
			||||||
        {uploads}
 | 
					        {uploads}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,8 @@ import {
 | 
				
			||||||
  fetchComposeSuggestions,
 | 
					  fetchComposeSuggestions,
 | 
				
			||||||
  selectComposeSuggestion,
 | 
					  selectComposeSuggestion,
 | 
				
			||||||
  changeComposeSensitivity,
 | 
					  changeComposeSensitivity,
 | 
				
			||||||
 | 
					  changeComposeSpoilerness,
 | 
				
			||||||
 | 
					  changeComposeSpoilerText,
 | 
				
			||||||
  changeComposeVisibility,
 | 
					  changeComposeVisibility,
 | 
				
			||||||
  changeComposeListability
 | 
					  changeComposeListability
 | 
				
			||||||
} from '../../../actions/compose';
 | 
					} from '../../../actions/compose';
 | 
				
			||||||
| 
						 | 
					@ -22,13 +24,16 @@ const makeMapStateToProps = () => {
 | 
				
			||||||
      suggestion_token: state.getIn(['compose', 'suggestion_token']),
 | 
					      suggestion_token: state.getIn(['compose', 'suggestion_token']),
 | 
				
			||||||
      suggestions: state.getIn(['compose', 'suggestions']),
 | 
					      suggestions: state.getIn(['compose', 'suggestions']),
 | 
				
			||||||
      sensitive: state.getIn(['compose', 'sensitive']),
 | 
					      sensitive: state.getIn(['compose', 'sensitive']),
 | 
				
			||||||
 | 
					      spoiler: state.getIn(['compose', 'spoiler']),
 | 
				
			||||||
 | 
					      spoiler_text: state.getIn(['compose', 'spoiler_text']),
 | 
				
			||||||
      unlisted: state.getIn(['compose', 'unlisted']),
 | 
					      unlisted: state.getIn(['compose', 'unlisted']),
 | 
				
			||||||
      private: state.getIn(['compose', 'private']),
 | 
					      private: state.getIn(['compose', 'private']),
 | 
				
			||||||
      fileDropDate: state.getIn(['compose', 'fileDropDate']),
 | 
					      fileDropDate: state.getIn(['compose', 'fileDropDate']),
 | 
				
			||||||
      is_submitting: state.getIn(['compose', 'is_submitting']),
 | 
					      is_submitting: state.getIn(['compose', 'is_submitting']),
 | 
				
			||||||
      is_uploading: state.getIn(['compose', 'is_uploading']),
 | 
					      is_uploading: state.getIn(['compose', 'is_uploading']),
 | 
				
			||||||
      in_reply_to: getStatus(state, state.getIn(['compose', 'in_reply_to'])),
 | 
					      in_reply_to: getStatus(state, state.getIn(['compose', 'in_reply_to'])),
 | 
				
			||||||
      media_count: state.getIn(['compose', 'media_attachments']).size
 | 
					      media_count: state.getIn(['compose', 'media_attachments']).size,
 | 
				
			||||||
 | 
					      me: state.getIn(['compose', 'me'])
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -65,6 +70,14 @@ const mapDispatchToProps = function (dispatch) {
 | 
				
			||||||
      dispatch(changeComposeSensitivity(checked));
 | 
					      dispatch(changeComposeSensitivity(checked));
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onChangeSpoilerness (checked) {
 | 
				
			||||||
 | 
					      dispatch(changeComposeSpoilerness(checked));
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onChangeSpoilerText (checked) {
 | 
				
			||||||
 | 
					      dispatch(changeComposeSpoilerText(checked));
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    onChangeVisibility (checked) {
 | 
					    onChangeVisibility (checked) {
 | 
				
			||||||
      dispatch(changeComposeVisibility(checked));
 | 
					      dispatch(changeComposeVisibility(checked));
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,10 @@
 | 
				
			||||||
import { connect }   from 'react-redux';
 | 
					import { connect }   from 'react-redux';
 | 
				
			||||||
import NavigationBar from '../components/navigation_bar';
 | 
					import NavigationBar from '../components/navigation_bar';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = (state, props) => ({
 | 
					const mapStateToProps = (state, props) => {
 | 
				
			||||||
  account: state.getIn(['accounts', state.getIn(['meta', 'me'])])
 | 
					  return {
 | 
				
			||||||
});
 | 
					    account: state.getIn(['accounts', state.getIn(['meta', 'me'])])
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default connect(mapStateToProps)(NavigationBar);
 | 
					export default connect(mapStateToProps)(NavigationBar);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ import { uploadCompose } from '../../../actions/compose';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = state => ({
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
  disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
 | 
					  disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
 | 
				
			||||||
 | 
					  resetFileKey: state.getIn(['compose', 'resetFileKey'])
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapDispatchToProps = dispatch => ({
 | 
					const mapDispatchToProps = dispatch => ({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,8 @@ import { mountCompose, unmountCompose } from '../../actions/compose';
 | 
				
			||||||
const Compose = React.createClass({
 | 
					const Compose = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  propTypes: {
 | 
					  propTypes: {
 | 
				
			||||||
    dispatch: React.PropTypes.func.isRequired
 | 
					    dispatch: React.PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    withHeader: React.PropTypes.bool
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mixins: [PureRenderMixin],
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
| 
						 | 
					@ -25,7 +26,7 @@ const Compose = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Drawer>
 | 
					      <Drawer withHeader={this.props.withHeader}>
 | 
				
			||||||
        <SearchContainer />
 | 
					        <SearchContainer />
 | 
				
			||||||
        <NavigationContainer />
 | 
					        <NavigationContainer />
 | 
				
			||||||
        <ComposeFormContainer />
 | 
					        <ComposeFormContainer />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,63 @@
 | 
				
			||||||
 | 
					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 { fetchFavouritedStatuses, expandFavouritedStatuses } from '../../actions/favourites';
 | 
				
			||||||
 | 
					import Column from '../ui/components/column';
 | 
				
			||||||
 | 
					import StatusList from '../../components/status_list';
 | 
				
			||||||
 | 
					import ColumnBackButton from '../public_timeline/components/column_back_button';
 | 
				
			||||||
 | 
					import { defineMessages, injectIntl } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const messages = defineMessages({
 | 
				
			||||||
 | 
					  heading: { id: 'column.favourites', defaultMessage: 'Favourites' }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
 | 
					  statusIds: state.getIn(['status_lists', 'favourites', 'items']),
 | 
				
			||||||
 | 
					  loaded: state.getIn(['status_lists', 'favourites', 'loaded']),
 | 
				
			||||||
 | 
					  me: state.getIn(['meta', 'me'])
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Favourites = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  propTypes: {
 | 
				
			||||||
 | 
					    params: React.PropTypes.object.isRequired,
 | 
				
			||||||
 | 
					    dispatch: React.PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    statusIds: ImmutablePropTypes.list.isRequired,
 | 
				
			||||||
 | 
					    loaded: React.PropTypes.bool,
 | 
				
			||||||
 | 
					    intl: React.PropTypes.object.isRequired,
 | 
				
			||||||
 | 
					    me: React.PropTypes.number.isRequired
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  componentWillMount () {
 | 
				
			||||||
 | 
					    this.props.dispatch(fetchFavouritedStatuses());
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleScrollToBottom () {
 | 
				
			||||||
 | 
					    this.props.dispatch(expandFavouritedStatuses());
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { statusIds, loaded, intl, me } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!loaded) {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        <Column>
 | 
				
			||||||
 | 
					          <LoadingIndicator />
 | 
				
			||||||
 | 
					        </Column>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <Column icon='star' heading={intl.formatMessage(messages.heading)}>
 | 
				
			||||||
 | 
					        <ColumnBackButton />
 | 
				
			||||||
 | 
					        <StatusList statusIds={statusIds} me={me} onScrollToBottom={this.handleScrollToBottom} />
 | 
				
			||||||
 | 
					      </Column>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default connect(mapStateToProps)(injectIntl(Favourites));
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					import Column from '../ui/components/column';
 | 
				
			||||||
 | 
					import MissingIndicator from '../../components/missing_indicator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const GenericNotFound = () => (
 | 
				
			||||||
 | 
					  <Column>
 | 
				
			||||||
 | 
					    <MissingIndicator />
 | 
				
			||||||
 | 
					  </Column>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default GenericNotFound;
 | 
				
			||||||
| 
						 | 
					@ -8,25 +8,16 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
  heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
 | 
					  heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
 | 
				
			||||||
  public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Public timeline' },
 | 
					  public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Public timeline' },
 | 
				
			||||||
  settings: { id: 'navigation_bar.settings', defaultMessage: 'Settings' },
 | 
					  preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
 | 
				
			||||||
  follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }
 | 
					  follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
 | 
				
			||||||
 | 
					  sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Sign out' },
 | 
				
			||||||
 | 
					  favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = state => ({
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
  me: state.getIn(['accounts', state.getIn(['meta', 'me'])])
 | 
					  me: state.getIn(['accounts', state.getIn(['meta', 'me'])])
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const hamburgerStyle = {
 | 
					 | 
				
			||||||
  background: '#373b4a',
 | 
					 | 
				
			||||||
  color: '#fff',
 | 
					 | 
				
			||||||
  fontSize: '16px',
 | 
					 | 
				
			||||||
  padding: '15px',
 | 
					 | 
				
			||||||
  position: 'absolute',
 | 
					 | 
				
			||||||
  right: '0',
 | 
					 | 
				
			||||||
  top: '-48px',
 | 
					 | 
				
			||||||
  cursor: 'default'
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const GettingStarted = ({ intl, me }) => {
 | 
					const GettingStarted = ({ intl, me }) => {
 | 
				
			||||||
  let followRequests = '';
 | 
					  let followRequests = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,19 +28,21 @@ const GettingStarted = ({ intl, me }) => {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Column icon='asterisk' heading={intl.formatMessage(messages.heading)}>
 | 
					    <Column icon='asterisk' heading={intl.formatMessage(messages.heading)}>
 | 
				
			||||||
      <div style={{ position: 'relative' }}>
 | 
					      <div style={{ position: 'relative' }}>
 | 
				
			||||||
        <div style={hamburgerStyle}><i className='fa fa-bars' /></div>
 | 
					 | 
				
			||||||
        <ColumnLink icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />
 | 
					        <ColumnLink icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />
 | 
				
			||||||
        <ColumnLink icon='cog' text={intl.formatMessage(messages.settings)} href='/settings/profile' />
 | 
					        <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
 | 
				
			||||||
 | 
					        <ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />
 | 
				
			||||||
        {followRequests}
 | 
					        {followRequests}
 | 
				
			||||||
 | 
					        <ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div className='static-content'>
 | 
					      <div className='scrollable optionally-scrollable'>
 | 
				
			||||||
        <p><FormattedMessage id='getting_started.about_addressing' defaultMessage='You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the form at the top of the sidebar.' /></p>
 | 
					        <div className='static-content getting-started'>
 | 
				
			||||||
        <p><FormattedMessage id='getting_started.about_shortcuts' defaultMessage='If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.' /></p>
 | 
					          <p><FormattedMessage id='getting_started.about_addressing' defaultMessage='You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the form at the top of the sidebar.' /></p>
 | 
				
			||||||
        <p><FormattedMessage id='getting_started.about_developer' defaultMessage='The developer of this project can be followed as Gargron@mastodon.social' /></p>
 | 
					          <p><FormattedMessage id='getting_started.about_shortcuts' defaultMessage='If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.' /></p>
 | 
				
			||||||
 | 
					          <p><FormattedMessage id='getting_started.about_developer' defaultMessage='The developer of this project can be followed as Gargron@mastodon.social' /></p>
 | 
				
			||||||
 | 
					          <p><FormattedMessage id='getting_started.open_source_notice' defaultMessage='Mastodon is open source software. You can contribute or report issues on github at {github}' values={{ github: <a style={{ color: '#616b86'}} href="https://github.com/tootsuite/mastodon">tootsuite/mastodon</a> }} /></p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div className='getting-started__illustration' />
 | 
					 | 
				
			||||||
    </Column>
 | 
					    </Column>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,68 @@
 | 
				
			||||||
 | 
					import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
				
			||||||
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
 | 
					import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					import ColumnCollapsable from '../../../components/column_collapsable';
 | 
				
			||||||
 | 
					import SettingToggle from '../../notifications/components/setting_toggle';
 | 
				
			||||||
 | 
					import SettingText from './setting_text';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const messages = defineMessages({
 | 
				
			||||||
 | 
					  filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter by regular expressions' }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const outerStyle = {
 | 
				
			||||||
 | 
					  background: '#373b4a',
 | 
				
			||||||
 | 
					  padding: '15px'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const sectionStyle = {
 | 
				
			||||||
 | 
					  cursor: 'default',
 | 
				
			||||||
 | 
					  display: 'block',
 | 
				
			||||||
 | 
					  fontWeight: '500',
 | 
				
			||||||
 | 
					  color: '#9baec8',
 | 
				
			||||||
 | 
					  marginBottom: '10px'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const rowStyle = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ColumnSettings = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  propTypes: {
 | 
				
			||||||
 | 
					    settings: ImmutablePropTypes.map.isRequired,
 | 
				
			||||||
 | 
					    onChange: React.PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    onSave: React.PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    intl: React.PropTypes.object.isRequired
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { settings, onChange, onSave, intl } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <ColumnCollapsable icon='sliders' fullHeight={209} onCollapse={onSave}>
 | 
				
			||||||
 | 
					        <div style={outerStyle}>
 | 
				
			||||||
 | 
					          <span style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div style={rowStyle}>
 | 
				
			||||||
 | 
					            <SettingToggle settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show reblogs' />} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div style={rowStyle}>
 | 
				
			||||||
 | 
					            <SettingToggle settings={settings} settingKey={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <span style={sectionStyle}><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div style={rowStyle}>
 | 
				
			||||||
 | 
					            <SettingText settings={settings} settingKey={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </ColumnCollapsable>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default injectIntl(ColumnSettings);
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,41 @@
 | 
				
			||||||
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const style = {
 | 
				
			||||||
 | 
					  display: 'block',
 | 
				
			||||||
 | 
					  fontFamily: 'inherit',
 | 
				
			||||||
 | 
					  marginBottom: '10px',
 | 
				
			||||||
 | 
					  padding: '7px 0',
 | 
				
			||||||
 | 
					  boxSizing: 'border-box',
 | 
				
			||||||
 | 
					  width: '100%'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SettingText = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  propTypes: {
 | 
				
			||||||
 | 
					    settings: ImmutablePropTypes.map.isRequired,
 | 
				
			||||||
 | 
					    settingKey: React.PropTypes.array.isRequired,
 | 
				
			||||||
 | 
					    label: React.PropTypes.string.isRequired,
 | 
				
			||||||
 | 
					    onChange: React.PropTypes.func.isRequired
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleChange (e) {
 | 
				
			||||||
 | 
					    this.props.onChange(this.props.settingKey, e.target.value)
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { settings, settingKey, label } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <input
 | 
				
			||||||
 | 
					        style={style}
 | 
				
			||||||
 | 
					        className='setting-text'
 | 
				
			||||||
 | 
					        value={settings.getIn(settingKey)}
 | 
				
			||||||
 | 
					        onChange={this.handleChange}
 | 
				
			||||||
 | 
					        placeholder={label}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default SettingText;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
 | 
					import ColumnSettings from '../components/column_settings';
 | 
				
			||||||
 | 
					import { changeSetting, saveSettings } from '../../../actions/settings';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
 | 
					  settings: state.getIn(['settings', 'home'])
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapDispatchToProps = dispatch => ({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onChange (key, checked) {
 | 
				
			||||||
 | 
					    dispatch(changeSetting(['home', ...key], checked));
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onSave () {
 | 
				
			||||||
 | 
					    dispatch(saveSettings());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,8 @@
 | 
				
			||||||
import { connect } from 'react-redux';
 | 
					 | 
				
			||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
					import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
				
			||||||
import StatusListContainer from '../ui/containers/status_list_container';
 | 
					import StatusListContainer from '../ui/containers/status_list_container';
 | 
				
			||||||
import Column from '../ui/components/column';
 | 
					import Column from '../ui/components/column';
 | 
				
			||||||
import { refreshTimeline } from '../../actions/timelines';
 | 
					 | 
				
			||||||
import { defineMessages, injectIntl } from 'react-intl';
 | 
					import { defineMessages, injectIntl } from 'react-intl';
 | 
				
			||||||
 | 
					import ColumnSettingsContainer from './containers/column_settings_container';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
  title: { id: 'column.home', defaultMessage: 'Home' }
 | 
					  title: { id: 'column.home', defaultMessage: 'Home' }
 | 
				
			||||||
| 
						 | 
					@ -12,20 +11,17 @@ const messages = defineMessages({
 | 
				
			||||||
const HomeTimeline = React.createClass({
 | 
					const HomeTimeline = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  propTypes: {
 | 
					  propTypes: {
 | 
				
			||||||
    dispatch: React.PropTypes.func.isRequired
 | 
					    intl: React.PropTypes.object.isRequired
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mixins: [PureRenderMixin],
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentWillMount () {
 | 
					 | 
				
			||||||
    this.props.dispatch(refreshTimeline('home'));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { intl } = this.props;
 | 
					    const { intl } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column icon='home' heading={intl.formatMessage(messages.title)}>
 | 
					      <Column icon='home' heading={intl.formatMessage(messages.title)}>
 | 
				
			||||||
 | 
					        <ColumnSettingsContainer />
 | 
				
			||||||
        <StatusListContainer {...this.props} type='home' />
 | 
					        <StatusListContainer {...this.props} type='home' />
 | 
				
			||||||
      </Column>
 | 
					      </Column>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
| 
						 | 
					@ -33,4 +29,4 @@ const HomeTimeline = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default connect()(injectIntl(HomeTimeline));
 | 
					export default injectIntl(HomeTimeline);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,37 +1,14 @@
 | 
				
			||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
					import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
				
			||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
import Toggle from 'react-toggle';
 | 
					 | 
				
			||||||
import { Motion, spring } from 'react-motion';
 | 
					 | 
				
			||||||
import { FormattedMessage } from 'react-intl';
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					import ColumnCollapsable from '../../../components/column_collapsable';
 | 
				
			||||||
 | 
					import SettingToggle from './setting_toggle';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const outerStyle = {
 | 
					const outerStyle = {
 | 
				
			||||||
  background: '#373b4a',
 | 
					  background: '#373b4a',
 | 
				
			||||||
  padding: '15px'
 | 
					  padding: '15px'
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const iconStyle = {
 | 
					 | 
				
			||||||
  fontSize: '16px',
 | 
					 | 
				
			||||||
  padding: '15px',
 | 
					 | 
				
			||||||
  position: 'absolute',
 | 
					 | 
				
			||||||
  right: '0',
 | 
					 | 
				
			||||||
  top: '-48px',
 | 
					 | 
				
			||||||
  cursor: 'pointer'
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const labelStyle = {
 | 
					 | 
				
			||||||
  display: 'block',
 | 
					 | 
				
			||||||
  lineHeight: '24px',
 | 
					 | 
				
			||||||
  verticalAlign: 'middle'
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const labelSpanStyle = {
 | 
					 | 
				
			||||||
  display: 'inline-block',
 | 
					 | 
				
			||||||
  verticalAlign: 'middle',
 | 
					 | 
				
			||||||
  marginBottom: '14px',
 | 
					 | 
				
			||||||
  marginLeft: '8px',
 | 
					 | 
				
			||||||
  color: '#9baec8'
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const sectionStyle = {
 | 
					const sectionStyle = {
 | 
				
			||||||
  cursor: 'default',
 | 
					  cursor: 'default',
 | 
				
			||||||
  display: 'block',
 | 
					  display: 'block',
 | 
				
			||||||
| 
						 | 
					@ -48,100 +25,55 @@ const ColumnSettings = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  propTypes: {
 | 
					  propTypes: {
 | 
				
			||||||
    settings: ImmutablePropTypes.map.isRequired,
 | 
					    settings: ImmutablePropTypes.map.isRequired,
 | 
				
			||||||
    onChange: React.PropTypes.func.isRequired
 | 
					    onChange: React.PropTypes.func.isRequired,
 | 
				
			||||||
  },
 | 
					    onSave: React.PropTypes.func.isRequired
 | 
				
			||||||
 | 
					 | 
				
			||||||
  getInitialState () {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      collapsed: true
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mixins: [PureRenderMixin],
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleToggleCollapsed () {
 | 
					 | 
				
			||||||
    this.setState({ collapsed: !this.state.collapsed });
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleChange (key, e) {
 | 
					 | 
				
			||||||
    this.props.onChange(key, e.target.checked);
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { settings }  = this.props;
 | 
					    const { settings, onChange, onSave } = this.props;
 | 
				
			||||||
    const { collapsed } = this.state;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
 | 
					    const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
 | 
				
			||||||
    const showStr  = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
 | 
					    const showStr  = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
 | 
				
			||||||
 | 
					    const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div style={{ position: 'relative' }}>
 | 
					      <ColumnCollapsable icon='sliders' fullHeight={616} onCollapse={onSave}>
 | 
				
			||||||
        <div style={{...iconStyle, color: collapsed ? '#9baec8' : '#fff', background: collapsed ? '#2f3441' : '#373b4a' }} onClick={this.handleToggleCollapsed}><i className='fa fa-sliders' /></div>
 | 
					        <div style={outerStyle}>
 | 
				
			||||||
 | 
					          <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : 458) }}>
 | 
					          <div style={rowStyle}>
 | 
				
			||||||
          {({ opacity, height }) =>
 | 
					            <SettingToggle settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} />
 | 
				
			||||||
            <div style={{ overflow: 'hidden', height: `${height}px`, opacity: opacity / 100 }}>
 | 
					            <SettingToggle settings={settings} settingKey={['shows', 'follow']} onChange={onChange} label={showStr} />
 | 
				
			||||||
              <div style={outerStyle}>
 | 
					            <SettingToggle settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} />
 | 
				
			||||||
                <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <div style={rowStyle}>
 | 
					          <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
 | 
				
			||||||
                  <label style={labelStyle}>
 | 
					 | 
				
			||||||
                    <Toggle checked={settings.getIn(['alerts', 'follow'])} onChange={this.handleChange.bind(this, ['alerts', 'follow'])} />
 | 
					 | 
				
			||||||
                    <span style={labelSpanStyle}>{alertStr}</span>
 | 
					 | 
				
			||||||
                  </label>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                  <label style={labelStyle}>
 | 
					          <div style={rowStyle}>
 | 
				
			||||||
                    <Toggle checked={settings.getIn(['shows', 'follow'])} onChange={this.handleChange.bind(this, ['shows', 'follow'])} />
 | 
					            <SettingToggle settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
 | 
				
			||||||
                    <span style={labelSpanStyle}>{showStr}</span>
 | 
					            <SettingToggle settings={settings} settingKey={['shows', 'favourite']} onChange={onChange} label={showStr} />
 | 
				
			||||||
                  </label>
 | 
					            <SettingToggle settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
 | 
				
			||||||
                </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
 | 
					          <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <div style={rowStyle}>
 | 
					          <div style={rowStyle}>
 | 
				
			||||||
                  <label style={labelStyle}>
 | 
					            <SettingToggle settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} />
 | 
				
			||||||
                    <Toggle checked={settings.getIn(['alerts', 'favourite'])} onChange={this.handleChange.bind(this, ['alerts', 'favourite'])} />
 | 
					            <SettingToggle settings={settings} settingKey={['shows', 'mention']} onChange={onChange} label={showStr} />
 | 
				
			||||||
                    <span style={labelSpanStyle}>{alertStr}</span>
 | 
					            <SettingToggle settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} />
 | 
				
			||||||
                  </label>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                  <label style={labelStyle}>
 | 
					          <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
 | 
				
			||||||
                    <Toggle checked={settings.getIn(['shows', 'favourite'])} onChange={this.handleChange.bind(this, ['shows', 'favourite'])} />
 | 
					 | 
				
			||||||
                    <span style={labelSpanStyle}>{showStr}</span>
 | 
					 | 
				
			||||||
                  </label>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
 | 
					          <div style={rowStyle}>
 | 
				
			||||||
 | 
					            <SettingToggle settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
 | 
				
			||||||
                <div style={rowStyle}>
 | 
					            <SettingToggle settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={showStr} />
 | 
				
			||||||
                  <label style={labelStyle}>
 | 
					            <SettingToggle settings={settings} settingKey={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
 | 
				
			||||||
                    <Toggle checked={settings.getIn(['alerts', 'mention'])} onChange={this.handleChange.bind(this, ['alerts', 'mention'])} />
 | 
					          </div>
 | 
				
			||||||
                    <span style={labelSpanStyle}>{alertStr}</span>
 | 
					        </div>
 | 
				
			||||||
                  </label>
 | 
					      </ColumnCollapsable>
 | 
				
			||||||
 | 
					 | 
				
			||||||
                  <label style={labelStyle}>
 | 
					 | 
				
			||||||
                    <Toggle checked={settings.getIn(['shows', 'mention'])} onChange={this.handleChange.bind(this, ['shows', 'mention'])} />
 | 
					 | 
				
			||||||
                    <span style={labelSpanStyle}>{showStr}</span>
 | 
					 | 
				
			||||||
                  </label>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <div style={rowStyle}>
 | 
					 | 
				
			||||||
                  <label style={labelStyle}>
 | 
					 | 
				
			||||||
                    <Toggle checked={settings.getIn(['alerts', 'reblog'])} onChange={this.handleChange.bind(this, ['alerts', 'reblog'])} />
 | 
					 | 
				
			||||||
                    <span style={labelSpanStyle}>{alertStr}</span>
 | 
					 | 
				
			||||||
                  </label>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                  <label style={labelStyle}>
 | 
					 | 
				
			||||||
                    <Toggle checked={settings.getIn(['shows', 'reblog'])} onChange={this.handleChange.bind(this, ['shows', 'reblog'])} />
 | 
					 | 
				
			||||||
                    <span style={labelSpanStyle}>{showStr}</span>
 | 
					 | 
				
			||||||
                  </label>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        </Motion>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,8 @@ import StatusContainer from '../../../containers/status_container';
 | 
				
			||||||
import AccountContainer from '../../../containers/account_container';
 | 
					import AccountContainer from '../../../containers/account_container';
 | 
				
			||||||
import { FormattedMessage } from 'react-intl';
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
import Permalink from '../../../components/permalink';
 | 
					import Permalink from '../../../components/permalink';
 | 
				
			||||||
 | 
					import emojify from '../../../emoji';
 | 
				
			||||||
 | 
					import escapeTextContentForBrowser from 'react/lib/escapeTextContentForBrowser';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const messageStyle = {
 | 
					const messageStyle = {
 | 
				
			||||||
  marginLeft: '68px',
 | 
					  marginLeft: '68px',
 | 
				
			||||||
| 
						 | 
					@ -71,7 +73,7 @@ const Notification = React.createClass({
 | 
				
			||||||
            <i className='fa fa-fw fa-retweet' style={{ color: '#2b90d9' }} />
 | 
					            <i className='fa fa-fw fa-retweet' style={{ color: '#2b90d9' }} />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <FormattedMessage id='notification.reblog' defaultMessage='{name} reblogged your status' values={{ name: link }} />
 | 
					          <FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <StatusContainer id={notification.get('status')} muted={true} />
 | 
					        <StatusContainer id={notification.get('status')} muted={true} />
 | 
				
			||||||
| 
						 | 
					@ -83,7 +85,8 @@ const Notification = React.createClass({
 | 
				
			||||||
    const { notification } = this.props;
 | 
					    const { notification } = this.props;
 | 
				
			||||||
    const account          = notification.get('account');
 | 
					    const account          = notification.get('account');
 | 
				
			||||||
    const displayName      = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
 | 
					    const displayName      = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
 | 
				
			||||||
    const link             = <Permalink className='notification__display-name' style={linkStyle} href={account.get('url')} to={`/accounts/${account.get('id')}`}>{displayName}</Permalink>;
 | 
					    const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
 | 
				
			||||||
 | 
					    const link             = <Permalink className='notification__display-name' style={linkStyle} href={account.get('url')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    switch(notification.get('type')) {
 | 
					    switch(notification.get('type')) {
 | 
				
			||||||
      case 'follow':
 | 
					      case 'follow':
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
 | 
					import Toggle from 'react-toggle';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const labelStyle = {
 | 
				
			||||||
 | 
					  display: 'block',
 | 
				
			||||||
 | 
					  lineHeight: '24px',
 | 
				
			||||||
 | 
					  verticalAlign: 'middle'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const labelSpanStyle = {
 | 
				
			||||||
 | 
					  display: 'inline-block',
 | 
				
			||||||
 | 
					  verticalAlign: 'middle',
 | 
				
			||||||
 | 
					  marginBottom: '14px',
 | 
				
			||||||
 | 
					  marginLeft: '8px',
 | 
				
			||||||
 | 
					  color: '#9baec8'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SettingToggle = ({ settings, settingKey, label, onChange }) => (
 | 
				
			||||||
 | 
					  <label style={labelStyle}>
 | 
				
			||||||
 | 
					    <Toggle checked={settings.getIn(settingKey)} onChange={(e) => onChange(settingKey, e.target.checked)} />
 | 
				
			||||||
 | 
					    <span style={labelSpanStyle}>{label}</span>
 | 
				
			||||||
 | 
					  </label>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SettingToggle.propTypes = {
 | 
				
			||||||
 | 
					  settings: ImmutablePropTypes.map.isRequired,
 | 
				
			||||||
 | 
					  settingKey: React.PropTypes.array.isRequired,
 | 
				
			||||||
 | 
					  label: React.PropTypes.node.isRequired,
 | 
				
			||||||
 | 
					  onChange: React.PropTypes.func.isRequired
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default SettingToggle;
 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,19 @@
 | 
				
			||||||
import { connect } from 'react-redux';
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
import ColumnSettings from '../components/column_settings';
 | 
					import ColumnSettings from '../components/column_settings';
 | 
				
			||||||
import { changeNotificationsSetting } from '../../../actions/notifications';
 | 
					import { changeSetting, saveSettings } from '../../../actions/settings';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = state => ({
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
  settings: state.getIn(['notifications', 'settings'])
 | 
					  settings: state.getIn(['settings', 'notifications'])
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapDispatchToProps = dispatch => ({
 | 
					const mapDispatchToProps = dispatch => ({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onChange (key, checked) {
 | 
					  onChange (key, checked) {
 | 
				
			||||||
    dispatch(changeNotificationsSetting(key, checked));
 | 
					    dispatch(changeSetting(['notifications', ...key], checked));
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onSave () {
 | 
				
			||||||
 | 
					    dispatch(saveSettings());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,10 +2,7 @@ import { connect } from 'react-redux';
 | 
				
			||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
					import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
				
			||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
import Column from '../ui/components/column';
 | 
					import Column from '../ui/components/column';
 | 
				
			||||||
import {
 | 
					import { expandNotifications } from '../../actions/notifications';
 | 
				
			||||||
  refreshNotifications,
 | 
					 | 
				
			||||||
  expandNotifications
 | 
					 | 
				
			||||||
} from '../../actions/notifications';
 | 
					 | 
				
			||||||
import NotificationContainer from './containers/notification_container';
 | 
					import NotificationContainer from './containers/notification_container';
 | 
				
			||||||
import { ScrollContainer } from 'react-router-scroll';
 | 
					import { ScrollContainer } from 'react-router-scroll';
 | 
				
			||||||
import { defineMessages, injectIntl } from 'react-intl';
 | 
					import { defineMessages, injectIntl } from 'react-intl';
 | 
				
			||||||
| 
						 | 
					@ -18,12 +15,13 @@ const messages = defineMessages({
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getNotifications = createSelector([
 | 
					const getNotifications = createSelector([
 | 
				
			||||||
  state => Immutable.List(state.getIn(['notifications', 'settings', 'shows']).filter(item => !item).keys()),
 | 
					  state => Immutable.List(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
 | 
				
			||||||
  state => state.getIn(['notifications', 'items'])
 | 
					  state => state.getIn(['notifications', 'items'])
 | 
				
			||||||
], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type'))));
 | 
					], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type'))));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = state => ({
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
  notifications: getNotifications(state)
 | 
					  notifications: getNotifications(state),
 | 
				
			||||||
 | 
					  isLoading: state.getIn(['notifications', 'isLoading'], true)
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Notifications = React.createClass({
 | 
					const Notifications = React.createClass({
 | 
				
			||||||
| 
						 | 
					@ -32,7 +30,8 @@ const Notifications = React.createClass({
 | 
				
			||||||
    notifications: ImmutablePropTypes.list.isRequired,
 | 
					    notifications: ImmutablePropTypes.list.isRequired,
 | 
				
			||||||
    dispatch: React.PropTypes.func.isRequired,
 | 
					    dispatch: React.PropTypes.func.isRequired,
 | 
				
			||||||
    trackScroll: React.PropTypes.bool,
 | 
					    trackScroll: React.PropTypes.bool,
 | 
				
			||||||
    intl: React.PropTypes.object.isRequired
 | 
					    intl: React.PropTypes.object.isRequired,
 | 
				
			||||||
 | 
					    isLoading: React.PropTypes.bool
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getDefaultProps () {
 | 
					  getDefaultProps () {
 | 
				
			||||||
| 
						 | 
					@ -43,15 +42,11 @@ const Notifications = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mixins: [PureRenderMixin],
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentWillMount () {
 | 
					 | 
				
			||||||
    const { dispatch } = this.props;
 | 
					 | 
				
			||||||
    dispatch(refreshNotifications());
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleScroll (e) {
 | 
					  handleScroll (e) {
 | 
				
			||||||
    const { scrollTop, scrollHeight, clientHeight } = e.target;
 | 
					    const { scrollTop, scrollHeight, clientHeight } = e.target;
 | 
				
			||||||
 | 
					    const offset = scrollHeight - scrollTop - clientHeight;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (scrollTop === scrollHeight - clientHeight) {
 | 
					    if (250 > offset && !this.props.isLoading) {
 | 
				
			||||||
      this.props.dispatch(expandNotifications());
 | 
					      this.props.dispatch(expandNotifications());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -70,6 +65,7 @@ const Notifications = React.createClass({
 | 
				
			||||||
    if (trackScroll) {
 | 
					    if (trackScroll) {
 | 
				
			||||||
      return (
 | 
					      return (
 | 
				
			||||||
        <Column icon='bell' heading={intl.formatMessage(messages.title)}>
 | 
					        <Column icon='bell' heading={intl.formatMessage(messages.title)}>
 | 
				
			||||||
 | 
					          <ColumnSettingsContainer />
 | 
				
			||||||
          <ScrollContainer scrollKey='notifications'>
 | 
					          <ScrollContainer scrollKey='notifications'>
 | 
				
			||||||
            {scrollableArea}
 | 
					            {scrollableArea}
 | 
				
			||||||
          </ScrollContainer>
 | 
					          </ScrollContainer>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -61,8 +61,8 @@ const ActionBar = React.createClass({
 | 
				
			||||||
      <div style={{ background: '#2f3441', display: 'flex', flexDirection: 'row', borderTop: '1px solid #363c4b', borderBottom: '1px solid #363c4b', padding: '10px 0' }}>
 | 
					      <div style={{ background: '#2f3441', display: 'flex', flexDirection: 'row', borderTop: '1px solid #363c4b', borderBottom: '1px solid #363c4b', padding: '10px 0' }}>
 | 
				
			||||||
        <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div>
 | 
					        <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div>
 | 
				
			||||||
        <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton disabled={status.get('visibility') === 'private'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={status.get('visibility') === 'private' ? 'lock' : 'retweet'} onClick={this.handleReblogClick} /></div>
 | 
					        <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton disabled={status.get('visibility') === 'private'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={status.get('visibility') === 'private' ? 'lock' : 'retweet'} onClick={this.handleReblogClick} /></div>
 | 
				
			||||||
        <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>
 | 
					        <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>
 | 
				
			||||||
        <div style={{ flex: '1 1 auto', textAlign: 'center' }}><DropdownMenu size={18} icon='ellipsis-h' items={menu} /></div>
 | 
					        <div style={{ flex: '1 1 auto', textAlign: 'center' }}><DropdownMenu size={18} icon='ellipsis-h' items={menu} direction="left" /></div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,100 @@
 | 
				
			||||||
 | 
					import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
				
			||||||
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const outerStyle = {
 | 
				
			||||||
 | 
					  display: 'flex',
 | 
				
			||||||
 | 
					  cursor: 'pointer',
 | 
				
			||||||
 | 
					  fontSize: '14px',
 | 
				
			||||||
 | 
					  border: '1px solid #363c4b',
 | 
				
			||||||
 | 
					  borderRadius: '4px',
 | 
				
			||||||
 | 
					  color: '#616b86',
 | 
				
			||||||
 | 
					  marginTop: '14px',
 | 
				
			||||||
 | 
					  textDecoration: 'none',
 | 
				
			||||||
 | 
					  overflow: 'hidden'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const contentStyle = {
 | 
				
			||||||
 | 
					  flex: '1 1 auto',
 | 
				
			||||||
 | 
					  padding: '8px',
 | 
				
			||||||
 | 
					  paddingLeft: '14px',
 | 
				
			||||||
 | 
					  overflow: 'hidden'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const titleStyle = {
 | 
				
			||||||
 | 
					  display: 'block',
 | 
				
			||||||
 | 
					  fontWeight: '500',
 | 
				
			||||||
 | 
					  marginBottom: '5px',
 | 
				
			||||||
 | 
					  color: '#d9e1e8',
 | 
				
			||||||
 | 
					  overflow: 'hidden',
 | 
				
			||||||
 | 
					  textOverflow: 'ellipsis',
 | 
				
			||||||
 | 
					  whiteSpace: 'nowrap'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const descriptionStyle = {
 | 
				
			||||||
 | 
					  color: '#d9e1e8'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const imageOuterStyle = {
 | 
				
			||||||
 | 
					  flex: '0 0 100px',
 | 
				
			||||||
 | 
					  background: '#373b4a'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const imageStyle = {
 | 
				
			||||||
 | 
					  display: 'block',
 | 
				
			||||||
 | 
					  width: '100%',
 | 
				
			||||||
 | 
					  height: 'auto',
 | 
				
			||||||
 | 
					  margin: '0',
 | 
				
			||||||
 | 
					  borderRadius: '4px 0 0 4px'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const hostStyle = {
 | 
				
			||||||
 | 
					  display: 'block',
 | 
				
			||||||
 | 
					  marginTop: '5px',
 | 
				
			||||||
 | 
					  fontSize: '13px'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getHostname = url => {
 | 
				
			||||||
 | 
					  const parser = document.createElement('a');
 | 
				
			||||||
 | 
					  parser.href = url;
 | 
				
			||||||
 | 
					  return parser.hostname;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Card = React.createClass({
 | 
				
			||||||
 | 
					  propTypes: {
 | 
				
			||||||
 | 
					    card: ImmutablePropTypes.map
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { card } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (card === null) {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let image = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (card.get('image')) {
 | 
				
			||||||
 | 
					      image = (
 | 
				
			||||||
 | 
					        <div style={imageOuterStyle}>
 | 
				
			||||||
 | 
					          <img src={card.get('image')} alt={card.get('title')} style={imageStyle} />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <a style={outerStyle} href={card.get('url')} className='status-card'>
 | 
				
			||||||
 | 
					        {image}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div style={contentStyle}>
 | 
				
			||||||
 | 
					          <strong style={titleStyle} title={card.get('title')}>{card.get('title')}</strong>
 | 
				
			||||||
 | 
					          <p style={descriptionStyle}>{card.get('description').substring(0, 50)}</p>
 | 
				
			||||||
 | 
					          <span style={hostStyle}>{getHostname(card.get('url'))}</span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Card;
 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,7 @@ import MediaGallery from '../../../components/media_gallery';
 | 
				
			||||||
import VideoPlayer from '../../../components/video_player';
 | 
					import VideoPlayer from '../../../components/video_player';
 | 
				
			||||||
import { Link } from 'react-router';
 | 
					import { Link } from 'react-router';
 | 
				
			||||||
import { FormattedDate, FormattedNumber } from 'react-intl';
 | 
					import { FormattedDate, FormattedNumber } from 'react-intl';
 | 
				
			||||||
 | 
					import CardContainer from '../containers/card_container';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const DetailedStatus = React.createClass({
 | 
					const DetailedStatus = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,7 +33,9 @@ const DetailedStatus = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status;
 | 
					    const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status;
 | 
				
			||||||
    let media    = '';
 | 
					
 | 
				
			||||||
 | 
					    let media           = '';
 | 
				
			||||||
 | 
					    let applicationLink = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (status.get('media_attachments').size > 0) {
 | 
					    if (status.get('media_attachments').size > 0) {
 | 
				
			||||||
      if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
 | 
					      if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
 | 
				
			||||||
| 
						 | 
					@ -40,6 +43,12 @@ const DetailedStatus = React.createClass({
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} />;
 | 
					        media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} />;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      media = <CardContainer statusId={status.get('id')} />;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (status.get('application')) {
 | 
				
			||||||
 | 
					      applicationLink = <span> · <a className='detailed-status__application' style={{ color: 'inherit' }} href={status.getIn(['application', 'website'])} target='_blank' rel='nooopener'>{status.getIn(['application', 'name'])}</a></span>;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
| 
						 | 
					@ -54,7 +63,7 @@ const DetailedStatus = React.createClass({
 | 
				
			||||||
        {media}
 | 
					        {media}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div style={{ marginTop: '15px', color: '#616b86', fontSize: '14px', lineHeight: '18px' }}>
 | 
					        <div style={{ marginTop: '15px', color: '#616b86', fontSize: '14px', lineHeight: '18px' }}>
 | 
				
			||||||
          <a className='detailed-status__datetime' style={{ color: 'inherit' }} href={status.get('url')} target='_blank' rel='noopener'><FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /></a> · <Link to={`/statuses/${status.get('id')}/reblogs`} style={{ color: 'inherit', textDecoration: 'none' }}><i className='fa fa-retweet' /><span style={{ fontWeight: '500', fontSize: '12px', marginLeft: '6px', display: 'inline-block' }}><FormattedNumber value={status.get('reblogs_count')} /></span></Link> · <Link to={`/statuses/${status.get('id')}/favourites`} style={{ color: 'inherit', textDecoration: 'none' }}><i className='fa fa-star' /><span style={{ fontWeight: '500', fontSize: '12px', marginLeft: '6px', display: 'inline-block' }}><FormattedNumber value={status.get('favourites_count')} /></span></Link>
 | 
					          <a className='detailed-status__datetime' style={{ color: 'inherit' }} href={status.get('url')} target='_blank' rel='noopener'><FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /></a>{applicationLink} · <Link to={`/statuses/${status.get('id')}/reblogs`} style={{ color: 'inherit', textDecoration: 'none' }}><i className='fa fa-retweet' /><span style={{ fontWeight: '500', fontSize: '12px', marginLeft: '6px', display: 'inline-block' }}><FormattedNumber value={status.get('reblogs_count')} /></span></Link> · <Link to={`/statuses/${status.get('id')}/favourites`} style={{ color: 'inherit', textDecoration: 'none' }}><i className='fa fa-star' /><span style={{ fontWeight: '500', fontSize: '12px', marginLeft: '6px', display: 'inline-block' }}><FormattedNumber value={status.get('favourites_count')} /></span></Link>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
 | 
					import Card from '../components/card';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapStateToProps = (state, { statusId }) => ({
 | 
				
			||||||
 | 
					  card: state.getIn(['cards', statusId], null)
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default connect(mapStateToProps)(Card);
 | 
				
			||||||
| 
						 | 
					@ -23,6 +23,7 @@ import { ScrollContainer }   from 'react-router-scroll';
 | 
				
			||||||
import ColumnBackButton      from '../../components/column_back_button';
 | 
					import ColumnBackButton      from '../../components/column_back_button';
 | 
				
			||||||
import StatusContainer       from '../../containers/status_container';
 | 
					import StatusContainer       from '../../containers/status_container';
 | 
				
			||||||
import { openMedia }         from '../../actions/modal';
 | 
					import { openMedia }         from '../../actions/modal';
 | 
				
			||||||
 | 
					import { isMobile } from '../../is_mobile'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const makeMapStateToProps = () => {
 | 
					const makeMapStateToProps = () => {
 | 
				
			||||||
  const getStatus = makeGetStatus();
 | 
					  const getStatus = makeGetStatus();
 | 
				
			||||||
| 
						 | 
					@ -47,7 +48,8 @@ const Status = React.createClass({
 | 
				
			||||||
    dispatch: React.PropTypes.func.isRequired,
 | 
					    dispatch: React.PropTypes.func.isRequired,
 | 
				
			||||||
    status: ImmutablePropTypes.map,
 | 
					    status: ImmutablePropTypes.map,
 | 
				
			||||||
    ancestorsIds: ImmutablePropTypes.list,
 | 
					    ancestorsIds: ImmutablePropTypes.list,
 | 
				
			||||||
    descendantsIds: ImmutablePropTypes.list
 | 
					    descendantsIds: ImmutablePropTypes.list,
 | 
				
			||||||
 | 
					    me: React.PropTypes.number
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mixins: [PureRenderMixin],
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
| 
						 | 
					@ -80,6 +82,10 @@ const Status = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleMentionClick (account) {
 | 
					  handleMentionClick (account) {
 | 
				
			||||||
    this.props.dispatch(mentionCompose(account));
 | 
					    this.props.dispatch(mentionCompose(account));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (isMobile(window.innerWidth)) {
 | 
				
			||||||
 | 
					      this.context.router.push('/statuses/new');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleOpenMedia (url) {
 | 
					  handleOpenMedia (url) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,10 +13,10 @@ const iconStyle = {
 | 
				
			||||||
  marginRight: '5px'
 | 
					  marginRight: '5px'
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ColumnLink = ({ icon, text, to, href }) => {
 | 
					const ColumnLink = ({ icon, text, to, href, method }) => {
 | 
				
			||||||
  if (href) {
 | 
					  if (href) {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <a href={href} style={outerStyle} className='column-link'>
 | 
					      <a href={href} style={outerStyle} className='column-link' data-method={method}>
 | 
				
			||||||
        <i className={`fa fa-fw fa-${icon}`} style={iconStyle} />
 | 
					        <i className={`fa fa-fw fa-${icon}`} style={iconStyle} />
 | 
				
			||||||
        {text}
 | 
					        {text}
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,15 +3,14 @@ import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const outerStyle = {
 | 
					const outerStyle = {
 | 
				
			||||||
  background: '#373b4a',
 | 
					  background: '#373b4a',
 | 
				
			||||||
  margin: '10px',
 | 
					 | 
				
			||||||
  flex: '0 0 auto',
 | 
					  flex: '0 0 auto',
 | 
				
			||||||
  marginBottom: '0'
 | 
					  overflowY: 'auto'
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const tabStyle = {
 | 
					const tabStyle = {
 | 
				
			||||||
  display: 'block',
 | 
					  display: 'block',
 | 
				
			||||||
  flex: '1 1 auto',
 | 
					  flex: '1 1 auto',
 | 
				
			||||||
  padding: '10px',
 | 
					  padding: '10px 5px',
 | 
				
			||||||
  color: '#fff',
 | 
					  color: '#fff',
 | 
				
			||||||
  textDecoration: 'none',
 | 
					  textDecoration: 'none',
 | 
				
			||||||
  textAlign: 'center',
 | 
					  textAlign: 'center',
 | 
				
			||||||
| 
						 | 
					@ -31,7 +30,7 @@ const TabsBar = () => {
 | 
				
			||||||
      <Link style={tabStyle} activeStyle={tabActiveStyle} to='/statuses/new'><i className='fa fa-fw fa-pencil' /> <FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></Link>
 | 
					      <Link style={tabStyle} activeStyle={tabActiveStyle} to='/statuses/new'><i className='fa fa-fw fa-pencil' /> <FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></Link>
 | 
				
			||||||
      <Link style={tabStyle} activeStyle={tabActiveStyle} to='/timelines/home'><i className='fa fa-fw fa-home' /> <FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></Link>
 | 
					      <Link style={tabStyle} activeStyle={tabActiveStyle} to='/timelines/home'><i className='fa fa-fw fa-home' /> <FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></Link>
 | 
				
			||||||
      <Link style={tabStyle} activeStyle={tabActiveStyle} to='/notifications'><i className='fa fa-fw fa-bell' /> <FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></Link>
 | 
					      <Link style={tabStyle} activeStyle={tabActiveStyle} to='/notifications'><i className='fa fa-fw fa-bell' /> <FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></Link>
 | 
				
			||||||
      <Link style={tabStyle} activeStyle={tabActiveStyle} to='/timelines/public'><i className='fa fa-fw fa-globe' /> <FormattedMessage id='tabs_bar.public' defaultMessage='Public' /></Link>
 | 
					      <Link style={{ ...tabStyle, flexGrow: '0', flexBasis: '30px' }} activeStyle={tabActiveStyle} to='/getting-started'><i className='fa fa-fw fa-bars' /></Link>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,9 @@
 | 
				
			||||||
import { connect }           from 'react-redux';
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
import { closeModal }        from '../../../actions/modal';
 | 
					import { closeModal } from '../../../actions/modal';
 | 
				
			||||||
import Lightbox              from '../../../components/lightbox';
 | 
					import Lightbox from '../../../components/lightbox';
 | 
				
			||||||
 | 
					import ImageLoader from 'react-imageloader';
 | 
				
			||||||
 | 
					import LoadingIndicator from '../../../components/loading_indicator';
 | 
				
			||||||
 | 
					import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = state => ({
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
  url: state.getIn(['modal', 'url']),
 | 
					  url: state.getIn(['modal', 'url']),
 | 
				
			||||||
| 
						 | 
					@ -23,6 +26,18 @@ const imageStyle = {
 | 
				
			||||||
  maxHeight: '80vh'
 | 
					  maxHeight: '80vh'
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const loadingStyle = {
 | 
				
			||||||
 | 
					  background: '#373b4a',
 | 
				
			||||||
 | 
					  width: '400px',
 | 
				
			||||||
 | 
					  paddingBottom: '120px'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const preloader = () => (
 | 
				
			||||||
 | 
					  <div style={loadingStyle}>
 | 
				
			||||||
 | 
					    <LoadingIndicator />
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Modal = React.createClass({
 | 
					const Modal = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  propTypes: {
 | 
					  propTypes: {
 | 
				
			||||||
| 
						 | 
					@ -32,12 +47,18 @@ const Modal = React.createClass({
 | 
				
			||||||
    onOverlayClicked: React.PropTypes.func
 | 
					    onOverlayClicked: React.PropTypes.func
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  mixins: [PureRenderMixin],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { url, ...other } = this.props;
 | 
					    const { url, ...other } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Lightbox {...other}>
 | 
					      <Lightbox {...other}>
 | 
				
			||||||
        <img src={url} style={imageStyle} />
 | 
					        <ImageLoader
 | 
				
			||||||
 | 
					          src={url}
 | 
				
			||||||
 | 
					          preloader={preloader}
 | 
				
			||||||
 | 
					          imgProps={{ style: imageStyle }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
      </Lightbox>
 | 
					      </Lightbox>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,26 +2,56 @@ import { connect } from 'react-redux';
 | 
				
			||||||
import StatusList from '../../../components/status_list';
 | 
					import StatusList from '../../../components/status_list';
 | 
				
			||||||
import { expandTimeline, scrollTopTimeline } from '../../../actions/timelines';
 | 
					import { expandTimeline, scrollTopTimeline } from '../../../actions/timelines';
 | 
				
			||||||
import Immutable from 'immutable';
 | 
					import Immutable from 'immutable';
 | 
				
			||||||
 | 
					import { createSelector } from 'reselect';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getStatusIds = createSelector([
 | 
				
			||||||
 | 
					  (state, { type }) => state.getIn(['settings', type], Immutable.Map()),
 | 
				
			||||||
 | 
					  (state, { type }) => state.getIn(['timelines', type, 'items'], Immutable.List()),
 | 
				
			||||||
 | 
					  (state)           => state.get('statuses')
 | 
				
			||||||
 | 
					], (columnSettings, statusIds, statuses) => statusIds.filter(id => {
 | 
				
			||||||
 | 
					  const statusForId = statuses.get(id);
 | 
				
			||||||
 | 
					  let showStatus    = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (columnSettings.getIn(['shows', 'reblog']) === false) {
 | 
				
			||||||
 | 
					    showStatus = showStatus && statusForId.get('reblog') === null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (columnSettings.getIn(['shows', 'reply']) === false) {
 | 
				
			||||||
 | 
					    showStatus = showStatus && statusForId.get('in_reply_to_id') === null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (columnSettings.getIn(['regex', 'body'], '').trim().length > 0) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const regex = new RegExp(columnSettings.getIn(['regex', 'body']).trim(), 'i');
 | 
				
			||||||
 | 
					      showStatus = showStatus && !regex.test(statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'content']) : statusForId.get('content'));
 | 
				
			||||||
 | 
					    } catch(e) {
 | 
				
			||||||
 | 
					      // Bad regex, don't affect filters
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return showStatus;
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = (state, props) => ({
 | 
					const mapStateToProps = (state, props) => ({
 | 
				
			||||||
  statusIds: state.getIn(['timelines', props.type, 'items'], Immutable.List())
 | 
					  statusIds: getStatusIds(state, props),
 | 
				
			||||||
 | 
					  isLoading: state.getIn(['timelines', props.type, 'isLoading'], true)
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapDispatchToProps = function (dispatch, props) {
 | 
					const mapDispatchToProps = (dispatch, { type, id }) => ({
 | 
				
			||||||
  return {
 | 
					 | 
				
			||||||
    onScrollToBottom () {
 | 
					 | 
				
			||||||
      dispatch(scrollTopTimeline(props.type, false));
 | 
					 | 
				
			||||||
      dispatch(expandTimeline(props.type, props.id));
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    onScrollToTop () {
 | 
					  onScrollToBottom () {
 | 
				
			||||||
      dispatch(scrollTopTimeline(props.type, true));
 | 
					    dispatch(scrollTopTimeline(type, false));
 | 
				
			||||||
    },
 | 
					    dispatch(expandTimeline(type, id));
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    onScroll () {
 | 
					  onScrollToTop () {
 | 
				
			||||||
      dispatch(scrollTopTimeline(props.type, false));
 | 
					    dispatch(scrollTopTimeline(type, true));
 | 
				
			||||||
    }
 | 
					  },
 | 
				
			||||||
  };
 | 
					
 | 
				
			||||||
};
 | 
					  onScroll () {
 | 
				
			||||||
 | 
					    dispatch(scrollTopTimeline(type, false));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default connect(mapStateToProps, mapDispatchToProps)(StatusList);
 | 
					export default connect(mapStateToProps, mapDispatchToProps)(StatusList);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,12 +8,20 @@ import Compose from '../compose';
 | 
				
			||||||
import TabsBar from './components/tabs_bar';
 | 
					import TabsBar from './components/tabs_bar';
 | 
				
			||||||
import ModalContainer from './containers/modal_container';
 | 
					import ModalContainer from './containers/modal_container';
 | 
				
			||||||
import Notifications from '../notifications';
 | 
					import Notifications from '../notifications';
 | 
				
			||||||
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
 | 
					import { isMobile } from '../../is_mobile';
 | 
				
			||||||
import { debounce } from 'react-decoration';
 | 
					import { debounce } from 'react-decoration';
 | 
				
			||||||
import { uploadCompose } from '../../actions/compose';
 | 
					import { uploadCompose } from '../../actions/compose';
 | 
				
			||||||
import { connect } from 'react-redux';
 | 
					import { refreshTimeline } from '../../actions/timelines';
 | 
				
			||||||
 | 
					import { refreshNotifications } from '../../actions/notifications';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const UI = React.createClass({
 | 
					const UI = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  propTypes: {
 | 
				
			||||||
 | 
					    dispatch: React.PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    children: React.PropTypes.node
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getInitialState () {
 | 
					  getInitialState () {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      width: window.innerWidth
 | 
					      width: window.innerWidth
 | 
				
			||||||
| 
						 | 
					@ -41,7 +49,7 @@ const UI = React.createClass({
 | 
				
			||||||
  handleDrop (e) {
 | 
					  handleDrop (e) {
 | 
				
			||||||
    e.preventDefault();
 | 
					    e.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (e.dataTransfer) {
 | 
					    if (e.dataTransfer && e.dataTransfer.files.length === 1) {
 | 
				
			||||||
      this.props.dispatch(uploadCompose(e.dataTransfer.files));
 | 
					      this.props.dispatch(uploadCompose(e.dataTransfer.files));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -50,6 +58,9 @@ const UI = React.createClass({
 | 
				
			||||||
    window.addEventListener('resize', this.handleResize, { passive: true });
 | 
					    window.addEventListener('resize', this.handleResize, { passive: true });
 | 
				
			||||||
    window.addEventListener('dragover', this.handleDragOver);
 | 
					    window.addEventListener('dragover', this.handleDragOver);
 | 
				
			||||||
    window.addEventListener('drop', this.handleDrop);
 | 
					    window.addEventListener('drop', this.handleDrop);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.props.dispatch(refreshTimeline('home'));
 | 
				
			||||||
 | 
					    this.props.dispatch(refreshNotifications());
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentWillUnmount () {
 | 
					  componentWillUnmount () {
 | 
				
			||||||
| 
						 | 
					@ -59,11 +70,9 @@ const UI = React.createClass({
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const layoutBreakpoint = 1024;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mountedColumns;
 | 
					    let mountedColumns;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.state.width <= layoutBreakpoint) {
 | 
					    if (isMobile(this.state.width)) {
 | 
				
			||||||
      mountedColumns = (
 | 
					      mountedColumns = (
 | 
				
			||||||
        <ColumnsArea>
 | 
					        <ColumnsArea>
 | 
				
			||||||
          {this.props.children}
 | 
					          {this.props.children}
 | 
				
			||||||
| 
						 | 
					@ -72,7 +81,7 @@ const UI = React.createClass({
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      mountedColumns = (
 | 
					      mountedColumns = (
 | 
				
			||||||
        <ColumnsArea>
 | 
					        <ColumnsArea>
 | 
				
			||||||
          <Compose />
 | 
					          <Compose withHeader={true} />
 | 
				
			||||||
          <HomeTimeline trackScroll={false} />
 | 
					          <HomeTimeline trackScroll={false} />
 | 
				
			||||||
          <Notifications trackScroll={false} />
 | 
					          <Notifications trackScroll={false} />
 | 
				
			||||||
          {this.props.children}
 | 
					          {this.props.children}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										5
									
								
								app/assets/javascripts/components/is_mobile.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/assets/javascripts/components/is_mobile.jsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					const LAYOUT_BREAKPOINT = 1024;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isMobile(width) {
 | 
				
			||||||
 | 
					  return width <= LAYOUT_BREAKPOINT;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,9 @@ const en = {
 | 
				
			||||||
  "status.reblog": "Teilen",
 | 
					  "status.reblog": "Teilen",
 | 
				
			||||||
  "status.favourite": "Favorisieren",
 | 
					  "status.favourite": "Favorisieren",
 | 
				
			||||||
  "status.reblogged_by": "{name} teilte",
 | 
					  "status.reblogged_by": "{name} teilte",
 | 
				
			||||||
 | 
					  "status.sensitive_warning": "Sensible Inhalte",
 | 
				
			||||||
 | 
					  "status.sensitive_toggle": "Klicken um zu zeigen",
 | 
				
			||||||
 | 
					  "status.open": "Öffnen",
 | 
				
			||||||
  "video_player.toggle_sound": "Ton umschalten",
 | 
					  "video_player.toggle_sound": "Ton umschalten",
 | 
				
			||||||
  "account.mention": "Erwähnen",
 | 
					  "account.mention": "Erwähnen",
 | 
				
			||||||
  "account.edit_profile": "Profil bearbeiten",
 | 
					  "account.edit_profile": "Profil bearbeiten",
 | 
				
			||||||
| 
						 | 
					@ -19,14 +22,17 @@ const en = {
 | 
				
			||||||
  "account.follows": "Folgt",
 | 
					  "account.follows": "Folgt",
 | 
				
			||||||
  "account.followers": "Folger",
 | 
					  "account.followers": "Folger",
 | 
				
			||||||
  "account.follows_you": "Folgt dir",
 | 
					  "account.follows_you": "Folgt dir",
 | 
				
			||||||
 | 
					  "account.requested": "Warte auf Erlaubnis",
 | 
				
			||||||
  "getting_started.heading": "Erste Schritte",
 | 
					  "getting_started.heading": "Erste Schritte",
 | 
				
			||||||
  "getting_started.about_addressing": "Du kannst Leuten folgen, falls du ihren Nutzernamen und ihre Domain kennst, in dem du eine e-mail-artige Addresse in das Suchfeld oben an der Seite eingibst.",
 | 
					  "getting_started.about_addressing": "Du kannst Leuten folgen, falls du ihren Nutzernamen und ihre Domain kennst, in dem du eine e-mail-artige Addresse in das Suchfeld oben an der Seite eingibst.",
 | 
				
			||||||
  "getting_started.about_shortcuts": "Falls der Zielnutzer an derselben Domain ist wie du, funktioniert der Nutzername auch alleine. Das gilt auch für Erwähnungen in Beiträgen.",
 | 
					  "getting_started.about_shortcuts": "Falls der Zielnutzer an derselben Domain ist wie du, funktioniert der Nutzername auch alleine. Das gilt auch für Erwähnungen in Beiträgen.",
 | 
				
			||||||
  "getting_started.about_developer": "Der Entwickler des Projekts kann unter Gargron@mastodon.social gefunden werden",
 | 
					  "getting_started.about_developer": "Der Entwickler des Projekts kann unter Gargron@mastodon.social gefunden werden",
 | 
				
			||||||
 | 
					  "getting_started.open_source_notice": "Mastodon ist quelloffene Software. Du kannst auf {github} dazu beitragen oder Probleme melden.",
 | 
				
			||||||
  "column.home": "Home",
 | 
					  "column.home": "Home",
 | 
				
			||||||
  "column.mentions": "Erwähnungen",
 | 
					  "column.mentions": "Erwähnungen",
 | 
				
			||||||
  "column.public": "Gesamtes Bekanntes Netz",
 | 
					  "column.public": "Gesamtes Bekanntes Netz",
 | 
				
			||||||
  "column.notifications": "Mitteilungen",
 | 
					  "column.notifications": "Mitteilungen",
 | 
				
			||||||
 | 
					  "column.follow_requests": "Folgeanfragen",
 | 
				
			||||||
  "tabs_bar.compose": "Schreiben",
 | 
					  "tabs_bar.compose": "Schreiben",
 | 
				
			||||||
  "tabs_bar.home": "Home",
 | 
					  "tabs_bar.home": "Home",
 | 
				
			||||||
  "tabs_bar.mentions": "Erwähnungen",
 | 
					  "tabs_bar.mentions": "Erwähnungen",
 | 
				
			||||||
| 
						 | 
					@ -36,9 +42,12 @@ const en = {
 | 
				
			||||||
  "compose_form.publish": "Veröffentlichen",
 | 
					  "compose_form.publish": "Veröffentlichen",
 | 
				
			||||||
  "compose_form.sensitive": "Medien als sensitiv markieren",
 | 
					  "compose_form.sensitive": "Medien als sensitiv markieren",
 | 
				
			||||||
  "compose_form.unlisted": "Öffentlich nicht auflisten",
 | 
					  "compose_form.unlisted": "Öffentlich nicht auflisten",
 | 
				
			||||||
  "navigation_bar.settings": "Einstellungen",
 | 
					  "compose_form.private": "Als privat markieren",
 | 
				
			||||||
 | 
					  "navigation_bar.edit_profile": "Profil bearbeiten",
 | 
				
			||||||
 | 
					  "navigation_bar.preferences": "Einstellungen",
 | 
				
			||||||
  "navigation_bar.public_timeline": "Öffentlich",
 | 
					  "navigation_bar.public_timeline": "Öffentlich",
 | 
				
			||||||
  "navigation_bar.logout": "Abmelden",
 | 
					  "navigation_bar.logout": "Abmelden",
 | 
				
			||||||
 | 
					  "navigation_bar.follow_requests": "Folgeanfragen",
 | 
				
			||||||
  "reply_indicator.cancel": "Abbrechen",
 | 
					  "reply_indicator.cancel": "Abbrechen",
 | 
				
			||||||
  "search.placeholder": "Suche",
 | 
					  "search.placeholder": "Suche",
 | 
				
			||||||
  "search.account": "Konto",
 | 
					  "search.account": "Konto",
 | 
				
			||||||
| 
						 | 
					@ -48,7 +57,21 @@ const en = {
 | 
				
			||||||
  "notification.follow": "{name} folgt dir",
 | 
					  "notification.follow": "{name} folgt dir",
 | 
				
			||||||
  "notification.favourite": "{name} favorisierte deinen Status",
 | 
					  "notification.favourite": "{name} favorisierte deinen Status",
 | 
				
			||||||
  "notification.reblog": "{name} teilte deinen Status",
 | 
					  "notification.reblog": "{name} teilte deinen Status",
 | 
				
			||||||
  "notification.mention": "{name} erwähnte dich"
 | 
					  "notification.mention": "{name} erwähnte dich",
 | 
				
			||||||
 | 
					  "notifications.column_settings.alert": "Desktop-Benachrichtigunen",
 | 
				
			||||||
 | 
					  "notifications.column_settings.show": "In der Spalte anzeigen",
 | 
				
			||||||
 | 
					  "notifications.column_settings.follow": "Neue Folger:",
 | 
				
			||||||
 | 
					  "notifications.column_settings.favourite": "Favorisierungen:",
 | 
				
			||||||
 | 
					  "notifications.column_settings.mention": "Erwähnungen:",
 | 
				
			||||||
 | 
					  "notifications.column_settings.reblog": "Geteilte Beiträge:",
 | 
				
			||||||
 | 
					  "follow_request.authorize": "Erlauben",
 | 
				
			||||||
 | 
					  "follow_request.reject": "Ablehnen",
 | 
				
			||||||
 | 
					  "home.column_settings.basic": "Einfach",
 | 
				
			||||||
 | 
					  "home.column_settings.advanced": "Fortgeschritten",
 | 
				
			||||||
 | 
					  "home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen",
 | 
				
			||||||
 | 
					  "home.column_settings.show_replies": "Antworten anzeigen",
 | 
				
			||||||
 | 
					  "home.column_settings.filter_regex": "Filter durch reguläre Ausdrücke",
 | 
				
			||||||
 | 
					  "missing_indicator.label": "Nicht gefunden"
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default en;
 | 
					export default en;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,7 +17,6 @@ const en = {
 | 
				
			||||||
  "account.unfollow": "Unfollow",
 | 
					  "account.unfollow": "Unfollow",
 | 
				
			||||||
  "account.block": "Block",
 | 
					  "account.block": "Block",
 | 
				
			||||||
  "account.follow": "Follow",
 | 
					  "account.follow": "Follow",
 | 
				
			||||||
  "account.block": "Block",
 | 
					 | 
				
			||||||
  "account.posts": "Posts",
 | 
					  "account.posts": "Posts",
 | 
				
			||||||
  "account.follows": "Follows",
 | 
					  "account.follows": "Follows",
 | 
				
			||||||
  "account.followers": "Followers",
 | 
					  "account.followers": "Followers",
 | 
				
			||||||
| 
						 | 
					@ -27,6 +26,7 @@ const en = {
 | 
				
			||||||
  "getting_started.about_addressing": "You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the search form.",
 | 
					  "getting_started.about_addressing": "You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the search form.",
 | 
				
			||||||
  "getting_started.about_shortcuts": "If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.",
 | 
					  "getting_started.about_shortcuts": "If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.",
 | 
				
			||||||
  "getting_started.about_developer": "The developer of this project can be followed as Gargron@mastodon.social",
 | 
					  "getting_started.about_developer": "The developer of this project can be followed as Gargron@mastodon.social",
 | 
				
			||||||
 | 
					  "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on github at {github}.",
 | 
				
			||||||
  "column.home": "Home",
 | 
					  "column.home": "Home",
 | 
				
			||||||
  "column.mentions": "Mentions",
 | 
					  "column.mentions": "Mentions",
 | 
				
			||||||
  "column.public": "Public",
 | 
					  "column.public": "Public",
 | 
				
			||||||
| 
						 | 
					@ -40,7 +40,9 @@ const en = {
 | 
				
			||||||
  "compose_form.publish": "Toot",
 | 
					  "compose_form.publish": "Toot",
 | 
				
			||||||
  "compose_form.sensitive": "Mark media as sensitive",
 | 
					  "compose_form.sensitive": "Mark media as sensitive",
 | 
				
			||||||
  "compose_form.private": "Mark as private",
 | 
					  "compose_form.private": "Mark as private",
 | 
				
			||||||
  "navigation_bar.settings": "Settings",
 | 
					  "compose_form.unlisted": "Do not display in public timeline",
 | 
				
			||||||
 | 
					  "navigation_bar.edit_profile": "Edit profile",
 | 
				
			||||||
 | 
					  "navigation_bar.preferences": "Preferences",
 | 
				
			||||||
  "navigation_bar.public_timeline": "Public timeline",
 | 
					  "navigation_bar.public_timeline": "Public timeline",
 | 
				
			||||||
  "navigation_bar.logout": "Logout",
 | 
					  "navigation_bar.logout": "Logout",
 | 
				
			||||||
  "reply_indicator.cancel": "Cancel",
 | 
					  "reply_indicator.cancel": "Cancel",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,7 +37,8 @@ const es = {
 | 
				
			||||||
  "compose_form.publish": "Publicar",
 | 
					  "compose_form.publish": "Publicar",
 | 
				
			||||||
  "compose_form.sensitive": "Marcar el contenido como sensible",
 | 
					  "compose_form.sensitive": "Marcar el contenido como sensible",
 | 
				
			||||||
  "compose_form.unlisted": "Privado",
 | 
					  "compose_form.unlisted": "Privado",
 | 
				
			||||||
  "navigation_bar.settings": "Ajustes",
 | 
					  "navigation_bar.edit_profile": "Editar perfil",
 | 
				
			||||||
 | 
					  "navigation_bar.preferences": "Preferencias",
 | 
				
			||||||
  "navigation_bar.public_timeline": "Público",
 | 
					  "navigation_bar.public_timeline": "Público",
 | 
				
			||||||
  "navigation_bar.logout": "Cerrar sesión",
 | 
					  "navigation_bar.logout": "Cerrar sesión",
 | 
				
			||||||
  "reply_indicator.cancel": "Cancelar",
 | 
					  "reply_indicator.cancel": "Cancelar",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,7 +38,8 @@ const fr = {
 | 
				
			||||||
  "compose_form.publish": "Pouet",
 | 
					  "compose_form.publish": "Pouet",
 | 
				
			||||||
  "compose_form.sensitive": "Marquer le contenu comme délicat",
 | 
					  "compose_form.sensitive": "Marquer le contenu comme délicat",
 | 
				
			||||||
  "compose_form.unlisted": "Ne pas apparaître dans le fil public",
 | 
					  "compose_form.unlisted": "Ne pas apparaître dans le fil public",
 | 
				
			||||||
  "navigation_bar.settings": "Paramètres",
 | 
					  "navigation_bar.edit_profile": "Modifier le profil",
 | 
				
			||||||
 | 
					  "navigation_bar.preferences": "Préférences",
 | 
				
			||||||
  "navigation_bar.public_timeline": "Public",
 | 
					  "navigation_bar.public_timeline": "Public",
 | 
				
			||||||
  "navigation_bar.logout": "Déconnexion",
 | 
					  "navigation_bar.logout": "Déconnexion",
 | 
				
			||||||
  "reply_indicator.cancel": "Annuler",
 | 
					  "reply_indicator.cancel": "Annuler",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,7 +38,8 @@ const hu = {
 | 
				
			||||||
  "compose_form.publish": "Tülk!",
 | 
					  "compose_form.publish": "Tülk!",
 | 
				
			||||||
  "compose_form.sensitive": "Tartalom érzékenynek jelölése",
 | 
					  "compose_form.sensitive": "Tartalom érzékenynek jelölése",
 | 
				
			||||||
  "compose_form.unlisted": "Listázatlan mód",
 | 
					  "compose_form.unlisted": "Listázatlan mód",
 | 
				
			||||||
  "navigation_bar.settings": "Beállítások",
 | 
					  "navigation_bar.edit_profile": "Profil szerkesztése",
 | 
				
			||||||
 | 
					  "navigation_bar.preferences": "Beállítások",
 | 
				
			||||||
  "navigation_bar.public_timeline": "Nyilvános időfolyam",
 | 
					  "navigation_bar.public_timeline": "Nyilvános időfolyam",
 | 
				
			||||||
  "navigation_bar.logout": "Kijelentkezés",
 | 
					  "navigation_bar.logout": "Kijelentkezés",
 | 
				
			||||||
  "reply_indicator.cancel": "Mégsem",
 | 
					  "reply_indicator.cancel": "Mégsem",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,7 +36,8 @@ const pt = {
 | 
				
			||||||
  "compose_form.publish": "Publicar",
 | 
					  "compose_form.publish": "Publicar",
 | 
				
			||||||
  "compose_form.sensitive": "Marcar conteúdo como sensível",
 | 
					  "compose_form.sensitive": "Marcar conteúdo como sensível",
 | 
				
			||||||
  "compose_form.unlisted": "Modo não-listado",
 | 
					  "compose_form.unlisted": "Modo não-listado",
 | 
				
			||||||
  "navigation_bar.settings": "Configurações",
 | 
					  "navigation_bar.edit_profile": "Editar perfil",
 | 
				
			||||||
 | 
					  "navigation_bar.preferences": "Preferências",
 | 
				
			||||||
  "navigation_bar.public_timeline": "Timeline Pública",
 | 
					  "navigation_bar.public_timeline": "Timeline Pública",
 | 
				
			||||||
  "navigation_bar.logout": "Logout",
 | 
					  "navigation_bar.logout": "Logout",
 | 
				
			||||||
  "reply_indicator.cancel": "Cancelar",
 | 
					  "reply_indicator.cancel": "Cancelar",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,7 +38,8 @@ const uk = {
 | 
				
			||||||
  "compose_form.publish": "Дмухнути",
 | 
					  "compose_form.publish": "Дмухнути",
 | 
				
			||||||
  "compose_form.sensitive": "Непристойний зміст",
 | 
					  "compose_form.sensitive": "Непристойний зміст",
 | 
				
			||||||
  "compose_form.unlisted": "Таємний режим",
 | 
					  "compose_form.unlisted": "Таємний режим",
 | 
				
			||||||
  "navigation_bar.settings": "Налаштування",
 | 
					  "navigation_bar.edit_profile": "Редагувати профіль",
 | 
				
			||||||
 | 
					  "navigation_bar.preferences": "Налаштування",
 | 
				
			||||||
  "navigation_bar.public_timeline": "Публічна стіна",
 | 
					  "navigation_bar.public_timeline": "Публічна стіна",
 | 
				
			||||||
  "navigation_bar.logout": "Вийти",
 | 
					  "navigation_bar.logout": "Вийти",
 | 
				
			||||||
  "reply_indicator.cancel": "Відмінити",
 | 
					  "reply_indicator.cancel": "Відмінити",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,7 @@ export default function errorsMiddleware() {
 | 
				
			||||||
          dispatch(showAlert(title, message));
 | 
					          dispatch(showAlert(title, message));
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          console.error(action.error);
 | 
					          console.error(action.error);
 | 
				
			||||||
          dispatch(showAlert('Oops!', 'An unexpected error occurred. Inspect the console for more details'));
 | 
					          dispatch(showAlert('Oops!', 'An unexpected error occurred.'));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										25
									
								
								app/assets/javascripts/components/middleware/loading_bar.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/assets/javascripts/components/middleware/loading_bar.jsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,25 @@
 | 
				
			||||||
 | 
					import { showLoading, hideLoading } from 'react-redux-loading-bar';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const defaultTypeSuffixes = ['PENDING', 'FULFILLED', 'REJECTED'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function loadingBarMiddleware(config = {}) {
 | 
				
			||||||
 | 
					  const promiseTypeSuffixes = config.promiseTypeSuffixes || defaultTypeSuffixes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return ({ dispatch }) => next => (action) => {
 | 
				
			||||||
 | 
					    if (action.type && !action.skipLoading) {
 | 
				
			||||||
 | 
					      const [PENDING, FULFILLED, REJECTED] = promiseTypeSuffixes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const isPending = new RegExp(`${PENDING}$`, 'g');
 | 
				
			||||||
 | 
					      const isFulfilled = new RegExp(`${FULFILLED}$`, 'g');
 | 
				
			||||||
 | 
					      const isRejected = new RegExp(`${REJECTED}$`, 'g');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (action.type.match(isPending)) {
 | 
				
			||||||
 | 
					        dispatch(showLoading());
 | 
				
			||||||
 | 
					      } else if (action.type.match(isFulfilled) || action.type.match(isRejected)) {
 | 
				
			||||||
 | 
					        dispatch(hideLoading());
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return next(action);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,4 @@
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ACCOUNT_SET_SELF,
 | 
					 | 
				
			||||||
  ACCOUNT_FETCH_SUCCESS,
 | 
					  ACCOUNT_FETCH_SUCCESS,
 | 
				
			||||||
  FOLLOWERS_FETCH_SUCCESS,
 | 
					  FOLLOWERS_FETCH_SUCCESS,
 | 
				
			||||||
  FOLLOWERS_EXPAND_SUCCESS,
 | 
					  FOLLOWERS_EXPAND_SUCCESS,
 | 
				
			||||||
| 
						 | 
					@ -7,7 +6,9 @@ import {
 | 
				
			||||||
  FOLLOWING_EXPAND_SUCCESS,
 | 
					  FOLLOWING_EXPAND_SUCCESS,
 | 
				
			||||||
  ACCOUNT_TIMELINE_FETCH_SUCCESS,
 | 
					  ACCOUNT_TIMELINE_FETCH_SUCCESS,
 | 
				
			||||||
  ACCOUNT_TIMELINE_EXPAND_SUCCESS,
 | 
					  ACCOUNT_TIMELINE_EXPAND_SUCCESS,
 | 
				
			||||||
  FOLLOW_REQUESTS_FETCH_SUCCESS
 | 
					  FOLLOW_REQUESTS_FETCH_SUCCESS,
 | 
				
			||||||
 | 
					  ACCOUNT_FOLLOW_SUCCESS,
 | 
				
			||||||
 | 
					  ACCOUNT_UNFOLLOW_SUCCESS
 | 
				
			||||||
} from '../actions/accounts';
 | 
					} from '../actions/accounts';
 | 
				
			||||||
import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose';
 | 
					import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
| 
						 | 
					@ -33,6 +34,11 @@ import {
 | 
				
			||||||
  NOTIFICATIONS_REFRESH_SUCCESS,
 | 
					  NOTIFICATIONS_REFRESH_SUCCESS,
 | 
				
			||||||
  NOTIFICATIONS_EXPAND_SUCCESS
 | 
					  NOTIFICATIONS_EXPAND_SUCCESS
 | 
				
			||||||
} from '../actions/notifications';
 | 
					} from '../actions/notifications';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  FAVOURITED_STATUSES_FETCH_SUCCESS,
 | 
				
			||||||
 | 
					  FAVOURITED_STATUSES_EXPAND_SUCCESS
 | 
				
			||||||
 | 
					} from '../actions/favourites';
 | 
				
			||||||
 | 
					import { STORE_HYDRATE } from '../actions/store';
 | 
				
			||||||
import Immutable from 'immutable';
 | 
					import Immutable from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const normalizeAccount = (state, account) => state.set(account.id, Immutable.fromJS(account));
 | 
					const normalizeAccount = (state, account) => state.set(account.id, Immutable.fromJS(account));
 | 
				
			||||||
| 
						 | 
					@ -67,38 +73,45 @@ const initialState = Immutable.Map();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function accounts(state = initialState, action) {
 | 
					export default function accounts(state = initialState, action) {
 | 
				
			||||||
  switch(action.type) {
 | 
					  switch(action.type) {
 | 
				
			||||||
    case ACCOUNT_SET_SELF:
 | 
					  case STORE_HYDRATE:
 | 
				
			||||||
    case ACCOUNT_FETCH_SUCCESS:
 | 
					    return state.merge(action.state.get('accounts'));
 | 
				
			||||||
    case NOTIFICATIONS_UPDATE:
 | 
					  case ACCOUNT_FETCH_SUCCESS:
 | 
				
			||||||
      return normalizeAccount(state, action.account);
 | 
					  case NOTIFICATIONS_UPDATE:
 | 
				
			||||||
    case FOLLOWERS_FETCH_SUCCESS:
 | 
					    return normalizeAccount(state, action.account);
 | 
				
			||||||
    case FOLLOWERS_EXPAND_SUCCESS:
 | 
					  case FOLLOWERS_FETCH_SUCCESS:
 | 
				
			||||||
    case FOLLOWING_FETCH_SUCCESS:
 | 
					  case FOLLOWERS_EXPAND_SUCCESS:
 | 
				
			||||||
    case FOLLOWING_EXPAND_SUCCESS:
 | 
					  case FOLLOWING_FETCH_SUCCESS:
 | 
				
			||||||
    case REBLOGS_FETCH_SUCCESS:
 | 
					  case FOLLOWING_EXPAND_SUCCESS:
 | 
				
			||||||
    case FAVOURITES_FETCH_SUCCESS:
 | 
					  case REBLOGS_FETCH_SUCCESS:
 | 
				
			||||||
    case COMPOSE_SUGGESTIONS_READY:
 | 
					  case FAVOURITES_FETCH_SUCCESS:
 | 
				
			||||||
    case SEARCH_SUGGESTIONS_READY:
 | 
					  case COMPOSE_SUGGESTIONS_READY:
 | 
				
			||||||
    case FOLLOW_REQUESTS_FETCH_SUCCESS:
 | 
					  case SEARCH_SUGGESTIONS_READY:
 | 
				
			||||||
      return normalizeAccounts(state, action.accounts);
 | 
					  case FOLLOW_REQUESTS_FETCH_SUCCESS:
 | 
				
			||||||
    case NOTIFICATIONS_REFRESH_SUCCESS:
 | 
					    return normalizeAccounts(state, action.accounts);
 | 
				
			||||||
    case NOTIFICATIONS_EXPAND_SUCCESS:
 | 
					  case NOTIFICATIONS_REFRESH_SUCCESS:
 | 
				
			||||||
      return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses);
 | 
					  case NOTIFICATIONS_EXPAND_SUCCESS:
 | 
				
			||||||
    case TIMELINE_REFRESH_SUCCESS:
 | 
					    return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses);
 | 
				
			||||||
    case TIMELINE_EXPAND_SUCCESS:
 | 
					  case TIMELINE_REFRESH_SUCCESS:
 | 
				
			||||||
    case ACCOUNT_TIMELINE_FETCH_SUCCESS:
 | 
					  case TIMELINE_EXPAND_SUCCESS:
 | 
				
			||||||
    case ACCOUNT_TIMELINE_EXPAND_SUCCESS:
 | 
					  case ACCOUNT_TIMELINE_FETCH_SUCCESS:
 | 
				
			||||||
    case CONTEXT_FETCH_SUCCESS:
 | 
					  case ACCOUNT_TIMELINE_EXPAND_SUCCESS:
 | 
				
			||||||
      return normalizeAccountsFromStatuses(state, action.statuses);
 | 
					  case CONTEXT_FETCH_SUCCESS:
 | 
				
			||||||
    case REBLOG_SUCCESS:
 | 
					  case FAVOURITED_STATUSES_FETCH_SUCCESS:
 | 
				
			||||||
    case FAVOURITE_SUCCESS:
 | 
					  case FAVOURITED_STATUSES_EXPAND_SUCCESS:
 | 
				
			||||||
    case UNREBLOG_SUCCESS:
 | 
					    return normalizeAccountsFromStatuses(state, action.statuses);
 | 
				
			||||||
    case UNFAVOURITE_SUCCESS:
 | 
					  case REBLOG_SUCCESS:
 | 
				
			||||||
      return normalizeAccountFromStatus(state, action.response);
 | 
					  case FAVOURITE_SUCCESS:
 | 
				
			||||||
    case TIMELINE_UPDATE:
 | 
					  case UNREBLOG_SUCCESS:
 | 
				
			||||||
    case STATUS_FETCH_SUCCESS:
 | 
					  case UNFAVOURITE_SUCCESS:
 | 
				
			||||||
      return normalizeAccountFromStatus(state, action.status);
 | 
					    return normalizeAccountFromStatus(state, action.response);
 | 
				
			||||||
    default:
 | 
					  case TIMELINE_UPDATE:
 | 
				
			||||||
      return state;
 | 
					  case STATUS_FETCH_SUCCESS:
 | 
				
			||||||
 | 
					    return normalizeAccountFromStatus(state, action.status);
 | 
				
			||||||
 | 
					  case ACCOUNT_FOLLOW_SUCCESS:
 | 
				
			||||||
 | 
					    return state.updateIn([action.relationship.id, 'followers_count'], num => num + 1);
 | 
				
			||||||
 | 
					  case ACCOUNT_UNFOLLOW_SUCCESS:
 | 
				
			||||||
 | 
					    return state.updateIn([action.relationship.id, 'followers_count'], num => Math.max(0, num - 1));
 | 
				
			||||||
 | 
					  default:
 | 
				
			||||||
 | 
					    return state;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										14
									
								
								app/assets/javascripts/components/reducers/cards.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								app/assets/javascripts/components/reducers/cards.jsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,14 @@
 | 
				
			||||||
 | 
					import { STATUS_CARD_FETCH_SUCCESS } from '../actions/cards';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Immutable from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const initialState = Immutable.Map();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function cards(state = initialState, action) {
 | 
				
			||||||
 | 
					  switch(action.type) {
 | 
				
			||||||
 | 
					  case STATUS_CARD_FETCH_SUCCESS:
 | 
				
			||||||
 | 
					    return state.set(action.id, Immutable.fromJS(action.card));
 | 
				
			||||||
 | 
					  default:
 | 
				
			||||||
 | 
					    return state;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -17,16 +17,20 @@ import {
 | 
				
			||||||
  COMPOSE_SUGGESTIONS_READY,
 | 
					  COMPOSE_SUGGESTIONS_READY,
 | 
				
			||||||
  COMPOSE_SUGGESTION_SELECT,
 | 
					  COMPOSE_SUGGESTION_SELECT,
 | 
				
			||||||
  COMPOSE_SENSITIVITY_CHANGE,
 | 
					  COMPOSE_SENSITIVITY_CHANGE,
 | 
				
			||||||
 | 
					  COMPOSE_SPOILERNESS_CHANGE,
 | 
				
			||||||
 | 
					  COMPOSE_SPOILER_TEXT_CHANGE,
 | 
				
			||||||
  COMPOSE_VISIBILITY_CHANGE,
 | 
					  COMPOSE_VISIBILITY_CHANGE,
 | 
				
			||||||
  COMPOSE_LISTABILITY_CHANGE
 | 
					  COMPOSE_LISTABILITY_CHANGE
 | 
				
			||||||
} from '../actions/compose';
 | 
					} from '../actions/compose';
 | 
				
			||||||
import { TIMELINE_DELETE } from '../actions/timelines';
 | 
					import { TIMELINE_DELETE } from '../actions/timelines';
 | 
				
			||||||
import { ACCOUNT_SET_SELF } from '../actions/accounts';
 | 
					import { STORE_HYDRATE } from '../actions/store';
 | 
				
			||||||
import Immutable from 'immutable';
 | 
					import Immutable from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const initialState = Immutable.Map({
 | 
					const initialState = Immutable.Map({
 | 
				
			||||||
  mounted: false,
 | 
					  mounted: false,
 | 
				
			||||||
  sensitive: false,
 | 
					  sensitive: false,
 | 
				
			||||||
 | 
					  spoiler: false,
 | 
				
			||||||
 | 
					  spoiler_text: '',
 | 
				
			||||||
  unlisted: false,
 | 
					  unlisted: false,
 | 
				
			||||||
  private: false,
 | 
					  private: false,
 | 
				
			||||||
  text: '',
 | 
					  text: '',
 | 
				
			||||||
| 
						 | 
					@ -38,7 +42,8 @@ const initialState = Immutable.Map({
 | 
				
			||||||
  media_attachments: Immutable.List(),
 | 
					  media_attachments: Immutable.List(),
 | 
				
			||||||
  suggestion_token: null,
 | 
					  suggestion_token: null,
 | 
				
			||||||
  suggestions: Immutable.List(),
 | 
					  suggestions: Immutable.List(),
 | 
				
			||||||
  me: null
 | 
					  me: null,
 | 
				
			||||||
 | 
					  resetFileKey: Math.floor((Math.random() * 0x10000))
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function statusToTextMentions(state, status) {
 | 
					function statusToTextMentions(state, status) {
 | 
				
			||||||
| 
						 | 
					@ -55,6 +60,8 @@ function statusToTextMentions(state, status) {
 | 
				
			||||||
function clearAll(state) {
 | 
					function clearAll(state) {
 | 
				
			||||||
  return state.withMutations(map => {
 | 
					  return state.withMutations(map => {
 | 
				
			||||||
    map.set('text', '');
 | 
					    map.set('text', '');
 | 
				
			||||||
 | 
					    map.set('spoiler', false);
 | 
				
			||||||
 | 
					    map.set('spoiler_text', '');
 | 
				
			||||||
    map.set('is_submitting', false);
 | 
					    map.set('is_submitting', false);
 | 
				
			||||||
    map.set('in_reply_to', null);
 | 
					    map.set('in_reply_to', null);
 | 
				
			||||||
    map.update('media_attachments', list => list.clear());
 | 
					    map.update('media_attachments', list => list.clear());
 | 
				
			||||||
| 
						 | 
					@ -65,6 +72,7 @@ function appendMedia(state, media) {
 | 
				
			||||||
  return state.withMutations(map => {
 | 
					  return state.withMutations(map => {
 | 
				
			||||||
    map.update('media_attachments', list => list.push(media));
 | 
					    map.update('media_attachments', list => list.push(media));
 | 
				
			||||||
    map.set('is_uploading', false);
 | 
					    map.set('is_uploading', false);
 | 
				
			||||||
 | 
					    map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
 | 
				
			||||||
    map.update('text', oldText => `${oldText} ${media.get('text_url')}`.trim());
 | 
					    map.update('text', oldText => `${oldText} ${media.get('text_url')}`.trim());
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -80,7 +88,7 @@ function removeMedia(state, mediaId) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const insertSuggestion = (state, position, token, completion) => {
 | 
					const insertSuggestion = (state, position, token, completion) => {
 | 
				
			||||||
  return state.withMutations(map => {
 | 
					  return state.withMutations(map => {
 | 
				
			||||||
    map.update('text', oldText => `${oldText.slice(0, position)}${completion}${oldText.slice(position + token.length)}`);
 | 
					    map.update('text', oldText => `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`);
 | 
				
			||||||
    map.set('suggestion_token', null);
 | 
					    map.set('suggestion_token', null);
 | 
				
			||||||
    map.update('suggestions', Immutable.List(), list => list.clear());
 | 
					    map.update('suggestions', Immutable.List(), list => list.clear());
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
| 
						 | 
					@ -88,64 +96,68 @@ const insertSuggestion = (state, position, token, completion) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function compose(state = initialState, action) {
 | 
					export default function compose(state = initialState, action) {
 | 
				
			||||||
  switch(action.type) {
 | 
					  switch(action.type) {
 | 
				
			||||||
    case COMPOSE_MOUNT:
 | 
					  case STORE_HYDRATE:
 | 
				
			||||||
      return state.set('mounted', true);
 | 
					    return state.merge(action.state.get('compose'));
 | 
				
			||||||
    case COMPOSE_UNMOUNT:
 | 
					  case COMPOSE_MOUNT:
 | 
				
			||||||
      return state.set('mounted', false);
 | 
					    return state.set('mounted', true);
 | 
				
			||||||
    case COMPOSE_SENSITIVITY_CHANGE:
 | 
					  case COMPOSE_UNMOUNT:
 | 
				
			||||||
      return state.set('sensitive', action.checked);
 | 
					    return state.set('mounted', false);
 | 
				
			||||||
    case COMPOSE_VISIBILITY_CHANGE:
 | 
					  case COMPOSE_SENSITIVITY_CHANGE:
 | 
				
			||||||
      return state.set('private', action.checked);
 | 
					    return state.set('sensitive', action.checked);
 | 
				
			||||||
    case COMPOSE_LISTABILITY_CHANGE:
 | 
					  case COMPOSE_SPOILERNESS_CHANGE:
 | 
				
			||||||
      return state.set('unlisted', action.checked);      
 | 
					    return (action.checked ? state : state.set('spoiler_text', '')).set('spoiler', action.checked);
 | 
				
			||||||
    case COMPOSE_CHANGE:
 | 
					  case COMPOSE_SPOILER_TEXT_CHANGE:
 | 
				
			||||||
      return state.set('text', action.text);
 | 
					    return state.set('spoiler_text', action.text);
 | 
				
			||||||
    case COMPOSE_REPLY:
 | 
					  case COMPOSE_VISIBILITY_CHANGE:
 | 
				
			||||||
      return state.withMutations(map => {
 | 
					    return state.set('private', action.checked);
 | 
				
			||||||
        map.set('in_reply_to', action.status.get('id'));
 | 
					  case COMPOSE_LISTABILITY_CHANGE:
 | 
				
			||||||
        map.set('text', statusToTextMentions(state, action.status));
 | 
					    return state.set('unlisted', action.checked);
 | 
				
			||||||
      });
 | 
					  case COMPOSE_CHANGE:
 | 
				
			||||||
    case COMPOSE_REPLY_CANCEL:
 | 
					    return state.set('text', action.text);
 | 
				
			||||||
      return state.withMutations(map => {
 | 
					  case COMPOSE_REPLY:
 | 
				
			||||||
        map.set('in_reply_to', null);
 | 
					    return state.withMutations(map => {
 | 
				
			||||||
        map.set('text', '');
 | 
					      map.set('in_reply_to', action.status.get('id'));
 | 
				
			||||||
      });
 | 
					      map.set('text', statusToTextMentions(state, action.status));
 | 
				
			||||||
    case COMPOSE_SUBMIT_REQUEST:
 | 
					    });
 | 
				
			||||||
      return state.set('is_submitting', true);
 | 
					  case COMPOSE_REPLY_CANCEL:
 | 
				
			||||||
    case COMPOSE_SUBMIT_SUCCESS:
 | 
					    return state.withMutations(map => {
 | 
				
			||||||
      return clearAll(state);
 | 
					      map.set('in_reply_to', null);
 | 
				
			||||||
    case COMPOSE_SUBMIT_FAIL:
 | 
					      map.set('text', '');
 | 
				
			||||||
      return state.set('is_submitting', false);
 | 
					    });
 | 
				
			||||||
    case COMPOSE_UPLOAD_REQUEST:
 | 
					  case COMPOSE_SUBMIT_REQUEST:
 | 
				
			||||||
      return state.withMutations(map => {
 | 
					    return state.set('is_submitting', true);
 | 
				
			||||||
        map.set('is_uploading', true);
 | 
					  case COMPOSE_SUBMIT_SUCCESS:
 | 
				
			||||||
        map.set('fileDropDate', new Date());
 | 
					    return clearAll(state);
 | 
				
			||||||
      });
 | 
					  case COMPOSE_SUBMIT_FAIL:
 | 
				
			||||||
    case COMPOSE_UPLOAD_SUCCESS:
 | 
					    return state.set('is_submitting', false);
 | 
				
			||||||
      return appendMedia(state, Immutable.fromJS(action.media));
 | 
					  case COMPOSE_UPLOAD_REQUEST:
 | 
				
			||||||
    case COMPOSE_UPLOAD_FAIL:
 | 
					    return state.withMutations(map => {
 | 
				
			||||||
      return state.set('is_uploading', false);
 | 
					      map.set('is_uploading', true);
 | 
				
			||||||
    case COMPOSE_UPLOAD_UNDO:
 | 
					      map.set('fileDropDate', new Date());
 | 
				
			||||||
      return removeMedia(state, action.media_id);
 | 
					    });
 | 
				
			||||||
    case COMPOSE_UPLOAD_PROGRESS:
 | 
					  case COMPOSE_UPLOAD_SUCCESS:
 | 
				
			||||||
      return state.set('progress', Math.round((action.loaded / action.total) * 100));
 | 
					    return appendMedia(state, Immutable.fromJS(action.media));
 | 
				
			||||||
    case COMPOSE_MENTION:
 | 
					  case COMPOSE_UPLOAD_FAIL:
 | 
				
			||||||
      return state.update('text', text => `${text}@${action.account.get('acct')} `);
 | 
					    return state.set('is_uploading', false);
 | 
				
			||||||
    case COMPOSE_SUGGESTIONS_CLEAR:
 | 
					  case COMPOSE_UPLOAD_UNDO:
 | 
				
			||||||
      return state.update('suggestions', Immutable.List(), list => list.clear()).set('suggestion_token', null);
 | 
					    return removeMedia(state, action.media_id);
 | 
				
			||||||
    case COMPOSE_SUGGESTIONS_READY:
 | 
					  case COMPOSE_UPLOAD_PROGRESS:
 | 
				
			||||||
      return state.set('suggestions', Immutable.List(action.accounts.map(item => item.id))).set('suggestion_token', action.token);
 | 
					    return state.set('progress', Math.round((action.loaded / action.total) * 100));
 | 
				
			||||||
    case COMPOSE_SUGGESTION_SELECT:
 | 
					  case COMPOSE_MENTION:
 | 
				
			||||||
      return insertSuggestion(state, action.position, action.token, action.completion);
 | 
					    return state.update('text', text => `${text}@${action.account.get('acct')} `);
 | 
				
			||||||
    case TIMELINE_DELETE:
 | 
					  case COMPOSE_SUGGESTIONS_CLEAR:
 | 
				
			||||||
      if (action.id === state.get('in_reply_to')) {
 | 
					    return state.update('suggestions', Immutable.List(), list => list.clear()).set('suggestion_token', null);
 | 
				
			||||||
        return state.set('in_reply_to', null);
 | 
					  case COMPOSE_SUGGESTIONS_READY:
 | 
				
			||||||
      } else {
 | 
					    return state.set('suggestions', Immutable.List(action.accounts.map(item => item.id))).set('suggestion_token', action.token);
 | 
				
			||||||
        return state;
 | 
					  case COMPOSE_SUGGESTION_SELECT:
 | 
				
			||||||
      }
 | 
					    return insertSuggestion(state, action.position, action.token, action.completion);
 | 
				
			||||||
    case ACCOUNT_SET_SELF:
 | 
					  case TIMELINE_DELETE:
 | 
				
			||||||
      return state.set('me', action.account.id).set('private', action.account.locked);
 | 
					    if (action.id === state.get('in_reply_to')) {
 | 
				
			||||||
    default:
 | 
					      return state.set('in_reply_to', null);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
      return state;
 | 
					      return state;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  default:
 | 
				
			||||||
 | 
					    return state;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,9 @@ import statuses from './statuses';
 | 
				
			||||||
import relationships from './relationships';
 | 
					import relationships from './relationships';
 | 
				
			||||||
import search from './search';
 | 
					import search from './search';
 | 
				
			||||||
import notifications from './notifications';
 | 
					import notifications from './notifications';
 | 
				
			||||||
 | 
					import settings from './settings';
 | 
				
			||||||
 | 
					import status_lists from './status_lists';
 | 
				
			||||||
 | 
					import cards from './cards';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default combineReducers({
 | 
					export default combineReducers({
 | 
				
			||||||
  timelines,
 | 
					  timelines,
 | 
				
			||||||
| 
						 | 
					@ -20,9 +23,12 @@ export default combineReducers({
 | 
				
			||||||
  loadingBar: loadingBarReducer,
 | 
					  loadingBar: loadingBarReducer,
 | 
				
			||||||
  modal,
 | 
					  modal,
 | 
				
			||||||
  user_lists,
 | 
					  user_lists,
 | 
				
			||||||
 | 
					  status_lists,
 | 
				
			||||||
  accounts,
 | 
					  accounts,
 | 
				
			||||||
  statuses,
 | 
					  statuses,
 | 
				
			||||||
  relationships,
 | 
					  relationships,
 | 
				
			||||||
  search,
 | 
					  search,
 | 
				
			||||||
  notifications
 | 
					  notifications,
 | 
				
			||||||
 | 
					  settings,
 | 
				
			||||||
 | 
					  cards
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,16 +1,16 @@
 | 
				
			||||||
import { ACCESS_TOKEN_SET } from '../actions/meta';
 | 
					import { STORE_HYDRATE } from '../actions/store';
 | 
				
			||||||
import { ACCOUNT_SET_SELF } from '../actions/accounts';
 | 
					 | 
				
			||||||
import Immutable from 'immutable';
 | 
					import Immutable from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const initialState = Immutable.Map();
 | 
					const initialState = Immutable.Map({
 | 
				
			||||||
 | 
					  access_token: null,
 | 
				
			||||||
 | 
					  me: null
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function meta(state = initialState, action) {
 | 
					export default function meta(state = initialState, action) {
 | 
				
			||||||
  switch(action.type) {
 | 
					  switch(action.type) {
 | 
				
			||||||
    case ACCESS_TOKEN_SET:
 | 
					  case STORE_HYDRATE:
 | 
				
			||||||
      return state.set('access_token', action.token);
 | 
					    return state.merge(action.state.get('meta'));
 | 
				
			||||||
    case ACCOUNT_SET_SELF:
 | 
					  default:
 | 
				
			||||||
      return state.set('me', action.account.id);
 | 
					    return state;
 | 
				
			||||||
    default:
 | 
					 | 
				
			||||||
      return state;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,14 +8,14 @@ const initialState = Immutable.Map({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function modal(state = initialState, action) {
 | 
					export default function modal(state = initialState, action) {
 | 
				
			||||||
  switch(action.type) {
 | 
					  switch(action.type) {
 | 
				
			||||||
    case MEDIA_OPEN:
 | 
					  case MEDIA_OPEN:
 | 
				
			||||||
      return state.withMutations(map => {
 | 
					    return state.withMutations(map => {
 | 
				
			||||||
        map.set('url', action.url);
 | 
					      map.set('url', action.url);
 | 
				
			||||||
        map.set('open', true);
 | 
					      map.set('open', true);
 | 
				
			||||||
      });
 | 
					    });
 | 
				
			||||||
    case MODAL_CLOSE:
 | 
					  case MODAL_CLOSE:
 | 
				
			||||||
      return state.set('open', false);
 | 
					    return state.set('open', false);
 | 
				
			||||||
    default:
 | 
					  default:
 | 
				
			||||||
      return state;
 | 
					    return state;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,10 @@ import {
 | 
				
			||||||
  NOTIFICATIONS_UPDATE,
 | 
					  NOTIFICATIONS_UPDATE,
 | 
				
			||||||
  NOTIFICATIONS_REFRESH_SUCCESS,
 | 
					  NOTIFICATIONS_REFRESH_SUCCESS,
 | 
				
			||||||
  NOTIFICATIONS_EXPAND_SUCCESS,
 | 
					  NOTIFICATIONS_EXPAND_SUCCESS,
 | 
				
			||||||
  NOTIFICATIONS_SETTING_CHANGE
 | 
					  NOTIFICATIONS_REFRESH_REQUEST,
 | 
				
			||||||
 | 
					  NOTIFICATIONS_EXPAND_REQUEST,
 | 
				
			||||||
 | 
					  NOTIFICATIONS_REFRESH_FAIL,
 | 
				
			||||||
 | 
					  NOTIFICATIONS_EXPAND_FAIL
 | 
				
			||||||
} from '../actions/notifications';
 | 
					} from '../actions/notifications';
 | 
				
			||||||
import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts';
 | 
					import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts';
 | 
				
			||||||
import Immutable from 'immutable';
 | 
					import Immutable from 'immutable';
 | 
				
			||||||
| 
						 | 
					@ -11,22 +14,7 @@ const initialState = Immutable.Map({
 | 
				
			||||||
  items: Immutable.List(),
 | 
					  items: Immutable.List(),
 | 
				
			||||||
  next: null,
 | 
					  next: null,
 | 
				
			||||||
  loaded: false,
 | 
					  loaded: false,
 | 
				
			||||||
 | 
					  isLoading: true
 | 
				
			||||||
  settings: Immutable.Map({
 | 
					 | 
				
			||||||
    alerts: Immutable.Map({
 | 
					 | 
				
			||||||
      follow: true,
 | 
					 | 
				
			||||||
      favourite: true,
 | 
					 | 
				
			||||||
      reblog: true,
 | 
					 | 
				
			||||||
      mention: true
 | 
					 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    shows: Immutable.Map({
 | 
					 | 
				
			||||||
      follow: true,
 | 
					 | 
				
			||||||
      favourite: true,
 | 
					 | 
				
			||||||
      reblog: true,
 | 
					 | 
				
			||||||
      mention: true
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const notificationToMap = notification => Immutable.Map({
 | 
					const notificationToMap = notification => Immutable.Map({
 | 
				
			||||||
| 
						 | 
					@ -48,7 +36,11 @@ const normalizeNotifications = (state, notifications, next) => {
 | 
				
			||||||
    items = items.set(i, notificationToMap(n));
 | 
					    items = items.set(i, notificationToMap(n));
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return state.update('items', list => loaded ? list.unshift(...items) : list.push(...items)).set('next', next).set('loaded', true);
 | 
					  return state
 | 
				
			||||||
 | 
					    .update('items', list => loaded ? list.unshift(...items) : list.push(...items))
 | 
				
			||||||
 | 
					    .set('next', next)
 | 
				
			||||||
 | 
					    .set('loaded', true)
 | 
				
			||||||
 | 
					    .set('isLoading', false);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const appendNormalizedNotifications = (state, notifications, next) => {
 | 
					const appendNormalizedNotifications = (state, notifications, next) => {
 | 
				
			||||||
| 
						 | 
					@ -58,7 +50,10 @@ const appendNormalizedNotifications = (state, notifications, next) => {
 | 
				
			||||||
    items = items.set(i, notificationToMap(n));
 | 
					    items = items.set(i, notificationToMap(n));
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return state.update('items', list => list.push(...items)).set('next', next);
 | 
					  return state
 | 
				
			||||||
 | 
					    .update('items', list => list.push(...items))
 | 
				
			||||||
 | 
					    .set('next', next)
 | 
				
			||||||
 | 
					    .set('isLoading', false);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const filterNotifications = (state, relationship) => {
 | 
					const filterNotifications = (state, relationship) => {
 | 
				
			||||||
| 
						 | 
					@ -67,17 +62,20 @@ const filterNotifications = (state, relationship) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function notifications(state = initialState, action) {
 | 
					export default function notifications(state = initialState, action) {
 | 
				
			||||||
  switch(action.type) {
 | 
					  switch(action.type) {
 | 
				
			||||||
    case NOTIFICATIONS_UPDATE:
 | 
					  case NOTIFICATIONS_REFRESH_REQUEST:
 | 
				
			||||||
      return normalizeNotification(state, action.notification);
 | 
					  case NOTIFICATIONS_EXPAND_REQUEST:
 | 
				
			||||||
    case NOTIFICATIONS_REFRESH_SUCCESS:
 | 
					  case NOTIFICATIONS_REFRESH_FAIL:
 | 
				
			||||||
      return normalizeNotifications(state, action.notifications, action.next);
 | 
					  case NOTIFICATIONS_EXPAND_FAIL:
 | 
				
			||||||
    case NOTIFICATIONS_EXPAND_SUCCESS:
 | 
					    return state.set('isLoading', true);
 | 
				
			||||||
      return appendNormalizedNotifications(state, action.notifications, action.next);
 | 
					  case NOTIFICATIONS_UPDATE:
 | 
				
			||||||
    case ACCOUNT_BLOCK_SUCCESS:
 | 
					    return normalizeNotification(state, action.notification);
 | 
				
			||||||
      return filterNotifications(state, action.relationship);
 | 
					  case NOTIFICATIONS_REFRESH_SUCCESS:
 | 
				
			||||||
    case NOTIFICATIONS_SETTING_CHANGE:
 | 
					    return normalizeNotifications(state, action.notifications, action.next);
 | 
				
			||||||
      return state.setIn(['settings', ...action.key], action.checked);
 | 
					  case NOTIFICATIONS_EXPAND_SUCCESS:
 | 
				
			||||||
    default:
 | 
					    return appendNormalizedNotifications(state, action.notifications, action.next);
 | 
				
			||||||
      return state;
 | 
					  case ACCOUNT_BLOCK_SUCCESS:
 | 
				
			||||||
 | 
					    return filterNotifications(state, action.relationship);
 | 
				
			||||||
 | 
					  default:
 | 
				
			||||||
 | 
					    return state;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,7 @@ const normalizeSuggestions = (state, value, accounts) => {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (value.indexOf('@') === -1) {
 | 
					  if (value.indexOf('@') === -1 && value.indexOf(' ') === -1) {
 | 
				
			||||||
    newSuggestions.push({
 | 
					    newSuggestions.push({
 | 
				
			||||||
      title: 'hashtag',
 | 
					      title: 'hashtag',
 | 
				
			||||||
      items: [
 | 
					      items: [
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										46
									
								
								app/assets/javascripts/components/reducers/settings.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								app/assets/javascripts/components/reducers/settings.jsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,46 @@
 | 
				
			||||||
 | 
					import { SETTING_CHANGE } from '../actions/settings';
 | 
				
			||||||
 | 
					import { STORE_HYDRATE } from '../actions/store';
 | 
				
			||||||
 | 
					import Immutable from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const initialState = Immutable.Map({
 | 
				
			||||||
 | 
					  home: Immutable.Map({
 | 
				
			||||||
 | 
					    shows: Immutable.Map({
 | 
				
			||||||
 | 
					      reblog: true,
 | 
				
			||||||
 | 
					      reply: true
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  notifications: Immutable.Map({
 | 
				
			||||||
 | 
					    alerts: Immutable.Map({
 | 
				
			||||||
 | 
					      follow: true,
 | 
				
			||||||
 | 
					      favourite: true,
 | 
				
			||||||
 | 
					      reblog: true,
 | 
				
			||||||
 | 
					      mention: true
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    shows: Immutable.Map({
 | 
				
			||||||
 | 
					      follow: true,
 | 
				
			||||||
 | 
					      favourite: true,
 | 
				
			||||||
 | 
					      reblog: true,
 | 
				
			||||||
 | 
					      mention: true
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    sounds: Immutable.Map({
 | 
				
			||||||
 | 
					      follow: true,
 | 
				
			||||||
 | 
					      favourite: true,
 | 
				
			||||||
 | 
					      reblog: true,
 | 
				
			||||||
 | 
					      mention: true
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function settings(state = initialState, action) {
 | 
				
			||||||
 | 
					  switch(action.type) {
 | 
				
			||||||
 | 
					  case STORE_HYDRATE:
 | 
				
			||||||
 | 
					    return state.mergeDeep(action.state.get('settings'));
 | 
				
			||||||
 | 
					  case SETTING_CHANGE:
 | 
				
			||||||
 | 
					    return state.setIn(action.key, action.value);
 | 
				
			||||||
 | 
					  default:
 | 
				
			||||||
 | 
					    return state;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										39
									
								
								app/assets/javascripts/components/reducers/status_lists.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								app/assets/javascripts/components/reducers/status_lists.jsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,39 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  FAVOURITED_STATUSES_FETCH_SUCCESS,
 | 
				
			||||||
 | 
					  FAVOURITED_STATUSES_EXPAND_SUCCESS
 | 
				
			||||||
 | 
					} from '../actions/favourites';
 | 
				
			||||||
 | 
					import Immutable from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const initialState = Immutable.Map({
 | 
				
			||||||
 | 
					  favourites: Immutable.Map({
 | 
				
			||||||
 | 
					    next: null,
 | 
				
			||||||
 | 
					    loaded: false,
 | 
				
			||||||
 | 
					    items: Immutable.List()
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const normalizeList = (state, listType, statuses, next) => {
 | 
				
			||||||
 | 
					  return state.update(listType, listMap => listMap.withMutations(map => {
 | 
				
			||||||
 | 
					    map.set('next', next);
 | 
				
			||||||
 | 
					    map.set('loaded', true);
 | 
				
			||||||
 | 
					    map.set('items', Immutable.List(statuses.map(item => item.id)));
 | 
				
			||||||
 | 
					  }));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const appendToList = (state, listType, statuses, next) => {
 | 
				
			||||||
 | 
					  return state.update(listType, listMap => listMap.withMutations(map => {
 | 
				
			||||||
 | 
					    map.set('next', next);
 | 
				
			||||||
 | 
					    map.set('items', map.get('items').push(...statuses.map(item => item.id)));
 | 
				
			||||||
 | 
					  }));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function statusLists(state = initialState, action) {
 | 
				
			||||||
 | 
					  switch(action.type) {
 | 
				
			||||||
 | 
					  case FAVOURITED_STATUSES_FETCH_SUCCESS:
 | 
				
			||||||
 | 
					    return normalizeList(state, 'favourites', action.statuses, action.next);
 | 
				
			||||||
 | 
					  case FAVOURITED_STATUSES_EXPAND_SUCCESS:
 | 
				
			||||||
 | 
					    return appendToList(state, 'favourites', action.statuses, action.next);
 | 
				
			||||||
 | 
					  default:
 | 
				
			||||||
 | 
					    return state;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -28,6 +28,10 @@ import {
 | 
				
			||||||
  NOTIFICATIONS_REFRESH_SUCCESS,
 | 
					  NOTIFICATIONS_REFRESH_SUCCESS,
 | 
				
			||||||
  NOTIFICATIONS_EXPAND_SUCCESS
 | 
					  NOTIFICATIONS_EXPAND_SUCCESS
 | 
				
			||||||
} from '../actions/notifications';
 | 
					} from '../actions/notifications';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  FAVOURITED_STATUSES_FETCH_SUCCESS,
 | 
				
			||||||
 | 
					  FAVOURITED_STATUSES_EXPAND_SUCCESS
 | 
				
			||||||
 | 
					} from '../actions/favourites';
 | 
				
			||||||
import Immutable from 'immutable';
 | 
					import Immutable from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const normalizeStatus = (state, status) => {
 | 
					const normalizeStatus = (state, status) => {
 | 
				
			||||||
| 
						 | 
					@ -77,36 +81,38 @@ const initialState = Immutable.Map();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function statuses(state = initialState, action) {
 | 
					export default function statuses(state = initialState, action) {
 | 
				
			||||||
  switch(action.type) {
 | 
					  switch(action.type) {
 | 
				
			||||||
    case TIMELINE_UPDATE:
 | 
					  case TIMELINE_UPDATE:
 | 
				
			||||||
    case STATUS_FETCH_SUCCESS:
 | 
					  case STATUS_FETCH_SUCCESS:
 | 
				
			||||||
    case NOTIFICATIONS_UPDATE:
 | 
					  case NOTIFICATIONS_UPDATE:
 | 
				
			||||||
      return normalizeStatus(state, action.status);
 | 
					    return normalizeStatus(state, action.status);
 | 
				
			||||||
    case REBLOG_SUCCESS:
 | 
					  case REBLOG_SUCCESS:
 | 
				
			||||||
    case UNREBLOG_SUCCESS:
 | 
					  case UNREBLOG_SUCCESS:
 | 
				
			||||||
    case FAVOURITE_SUCCESS:
 | 
					  case FAVOURITE_SUCCESS:
 | 
				
			||||||
    case UNFAVOURITE_SUCCESS:
 | 
					  case UNFAVOURITE_SUCCESS:
 | 
				
			||||||
      return normalizeStatus(state, action.response);
 | 
					    return normalizeStatus(state, action.response);
 | 
				
			||||||
    case FAVOURITE_REQUEST:
 | 
					  case FAVOURITE_REQUEST:
 | 
				
			||||||
      return state.setIn([action.status.get('id'), 'favourited'], true);
 | 
					    return state.setIn([action.status.get('id'), 'favourited'], true);
 | 
				
			||||||
    case FAVOURITE_FAIL:
 | 
					  case FAVOURITE_FAIL:
 | 
				
			||||||
      return state.setIn([action.status.get('id'), 'favourited'], false);
 | 
					    return state.setIn([action.status.get('id'), 'favourited'], false);
 | 
				
			||||||
    case REBLOG_REQUEST:
 | 
					  case REBLOG_REQUEST:
 | 
				
			||||||
      return state.setIn([action.status.get('id'), 'reblogged'], true);
 | 
					    return state.setIn([action.status.get('id'), 'reblogged'], true);
 | 
				
			||||||
    case REBLOG_FAIL:
 | 
					  case REBLOG_FAIL:
 | 
				
			||||||
      return state.setIn([action.status.get('id'), 'reblogged'], false);
 | 
					    return state.setIn([action.status.get('id'), 'reblogged'], false);
 | 
				
			||||||
    case TIMELINE_REFRESH_SUCCESS:
 | 
					  case TIMELINE_REFRESH_SUCCESS:
 | 
				
			||||||
    case TIMELINE_EXPAND_SUCCESS:
 | 
					  case TIMELINE_EXPAND_SUCCESS:
 | 
				
			||||||
    case ACCOUNT_TIMELINE_FETCH_SUCCESS:
 | 
					  case ACCOUNT_TIMELINE_FETCH_SUCCESS:
 | 
				
			||||||
    case ACCOUNT_TIMELINE_EXPAND_SUCCESS:
 | 
					  case ACCOUNT_TIMELINE_EXPAND_SUCCESS:
 | 
				
			||||||
    case CONTEXT_FETCH_SUCCESS:
 | 
					  case CONTEXT_FETCH_SUCCESS:
 | 
				
			||||||
    case NOTIFICATIONS_REFRESH_SUCCESS:
 | 
					  case NOTIFICATIONS_REFRESH_SUCCESS:
 | 
				
			||||||
    case NOTIFICATIONS_EXPAND_SUCCESS:
 | 
					  case NOTIFICATIONS_EXPAND_SUCCESS:
 | 
				
			||||||
      return normalizeStatuses(state, action.statuses);
 | 
					  case FAVOURITED_STATUSES_FETCH_SUCCESS:
 | 
				
			||||||
    case TIMELINE_DELETE:
 | 
					  case FAVOURITED_STATUSES_EXPAND_SUCCESS:
 | 
				
			||||||
      return deleteStatus(state, action.id, action.references);
 | 
					    return normalizeStatuses(state, action.statuses);
 | 
				
			||||||
    case ACCOUNT_BLOCK_SUCCESS:
 | 
					  case TIMELINE_DELETE:
 | 
				
			||||||
      return filterStatuses(state, action.relationship);
 | 
					    return deleteStatus(state, action.id, action.references);
 | 
				
			||||||
    default:
 | 
					  case ACCOUNT_BLOCK_SUCCESS:
 | 
				
			||||||
      return state;
 | 
					    return filterStatuses(state, action.relationship);
 | 
				
			||||||
 | 
					  default:
 | 
				
			||||||
 | 
					    return state;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,12 @@
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  TIMELINE_REFRESH_REQUEST,
 | 
					  TIMELINE_REFRESH_REQUEST,
 | 
				
			||||||
  TIMELINE_REFRESH_SUCCESS,
 | 
					  TIMELINE_REFRESH_SUCCESS,
 | 
				
			||||||
 | 
					  TIMELINE_REFRESH_FAIL,
 | 
				
			||||||
  TIMELINE_UPDATE,
 | 
					  TIMELINE_UPDATE,
 | 
				
			||||||
  TIMELINE_DELETE,
 | 
					  TIMELINE_DELETE,
 | 
				
			||||||
  TIMELINE_EXPAND_SUCCESS,
 | 
					  TIMELINE_EXPAND_SUCCESS,
 | 
				
			||||||
 | 
					  TIMELINE_EXPAND_REQUEST,
 | 
				
			||||||
 | 
					  TIMELINE_EXPAND_FAIL,
 | 
				
			||||||
  TIMELINE_SCROLL_TOP
 | 
					  TIMELINE_SCROLL_TOP
 | 
				
			||||||
} from '../actions/timelines';
 | 
					} from '../actions/timelines';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
| 
						 | 
					@ -13,37 +16,43 @@ import {
 | 
				
			||||||
  UNFAVOURITE_SUCCESS
 | 
					  UNFAVOURITE_SUCCESS
 | 
				
			||||||
} from '../actions/interactions';
 | 
					} from '../actions/interactions';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ACCOUNT_FETCH_SUCCESS,
 | 
					  ACCOUNT_TIMELINE_FETCH_REQUEST,
 | 
				
			||||||
  ACCOUNT_TIMELINE_FETCH_SUCCESS,
 | 
					  ACCOUNT_TIMELINE_FETCH_SUCCESS,
 | 
				
			||||||
 | 
					  ACCOUNT_TIMELINE_FETCH_FAIL,
 | 
				
			||||||
 | 
					  ACCOUNT_TIMELINE_EXPAND_REQUEST,
 | 
				
			||||||
  ACCOUNT_TIMELINE_EXPAND_SUCCESS,
 | 
					  ACCOUNT_TIMELINE_EXPAND_SUCCESS,
 | 
				
			||||||
 | 
					  ACCOUNT_TIMELINE_EXPAND_FAIL,
 | 
				
			||||||
  ACCOUNT_BLOCK_SUCCESS
 | 
					  ACCOUNT_BLOCK_SUCCESS
 | 
				
			||||||
} from '../actions/accounts';
 | 
					} from '../actions/accounts';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  STATUS_FETCH_SUCCESS,
 | 
					 | 
				
			||||||
  CONTEXT_FETCH_SUCCESS
 | 
					  CONTEXT_FETCH_SUCCESS
 | 
				
			||||||
} from '../actions/statuses';
 | 
					} from '../actions/statuses';
 | 
				
			||||||
import Immutable from 'immutable';
 | 
					import Immutable from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const initialState = Immutable.Map({
 | 
					const initialState = Immutable.Map({
 | 
				
			||||||
  home: Immutable.Map({
 | 
					  home: Immutable.Map({
 | 
				
			||||||
 | 
					    isLoading: false,
 | 
				
			||||||
    loaded: false,
 | 
					    loaded: false,
 | 
				
			||||||
    top: true,
 | 
					    top: true,
 | 
				
			||||||
    items: Immutable.List()
 | 
					    items: Immutable.List()
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mentions: Immutable.Map({
 | 
					  mentions: Immutable.Map({
 | 
				
			||||||
 | 
					    isLoading: false,
 | 
				
			||||||
    loaded: false,
 | 
					    loaded: false,
 | 
				
			||||||
    top: true,
 | 
					    top: true,
 | 
				
			||||||
    items: Immutable.List()
 | 
					    items: Immutable.List()
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public: Immutable.Map({
 | 
					  public: Immutable.Map({
 | 
				
			||||||
 | 
					    isLoading: false,
 | 
				
			||||||
    loaded: false,
 | 
					    loaded: false,
 | 
				
			||||||
    top: true,
 | 
					    top: true,
 | 
				
			||||||
    items: Immutable.List()
 | 
					    items: Immutable.List()
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  tag: Immutable.Map({
 | 
					  tag: Immutable.Map({
 | 
				
			||||||
 | 
					    isLoading: false,
 | 
				
			||||||
    id: null,
 | 
					    id: null,
 | 
				
			||||||
    loaded: false,
 | 
					    loaded: false,
 | 
				
			||||||
    top: true,
 | 
					    top: true,
 | 
				
			||||||
| 
						 | 
					@ -82,6 +91,7 @@ const normalizeTimeline = (state, timeline, statuses, replace = false) => {
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  state = state.setIn([timeline, 'loaded'], true);
 | 
					  state = state.setIn([timeline, 'loaded'], true);
 | 
				
			||||||
 | 
					  state = state.setIn([timeline, 'isLoading'], false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return state.updateIn([timeline, 'items'], Immutable.List(), list => (loaded ? list.unshift(...ids) : ids));
 | 
					  return state.updateIn([timeline, 'items'], Immutable.List(), list => (loaded ? list.unshift(...ids) : ids));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -94,6 +104,8 @@ const appendNormalizedTimeline = (state, timeline, statuses) => {
 | 
				
			||||||
    moreIds = moreIds.set(i, status.get('id'));
 | 
					    moreIds = moreIds.set(i, status.get('id'));
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  state = state.setIn([timeline, 'isLoading'], false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return state.updateIn([timeline, 'items'], Immutable.List(), list => list.push(...moreIds));
 | 
					  return state.updateIn([timeline, 'items'], Immutable.List(), list => list.push(...moreIds));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -105,7 +117,10 @@ const normalizeAccountTimeline = (state, accountId, statuses, replace = false) =
 | 
				
			||||||
    ids   = ids.set(i, status.get('id'));
 | 
					    ids   = ids.set(i, status.get('id'));
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => (replace ? ids : list.unshift(...ids)));
 | 
					  return state.updateIn(['accounts_timelines', accountId], Immutable.Map(), map => map
 | 
				
			||||||
 | 
					    .set('isLoading', false)
 | 
				
			||||||
 | 
					    .set('loaded', true)
 | 
				
			||||||
 | 
					    .update('items', Immutable.List(), list => (replace ? ids : list.unshift(...ids))));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const appendNormalizedAccountTimeline = (state, accountId, statuses) => {
 | 
					const appendNormalizedAccountTimeline = (state, accountId, statuses) => {
 | 
				
			||||||
| 
						 | 
					@ -116,7 +131,9 @@ const appendNormalizedAccountTimeline = (state, accountId, statuses) => {
 | 
				
			||||||
    moreIds = moreIds.set(i, status.get('id'));
 | 
					    moreIds = moreIds.set(i, status.get('id'));
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => list.push(...moreIds));
 | 
					  return state.updateIn(['accounts_timelines', accountId], Immutable.Map(), map => map
 | 
				
			||||||
 | 
					    .set('isLoading', false)
 | 
				
			||||||
 | 
					    .update('items', list => list.push(...moreIds)));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const updateTimeline = (state, timeline, status, references) => {
 | 
					const updateTimeline = (state, timeline, status, references) => {
 | 
				
			||||||
| 
						 | 
					@ -145,14 +162,19 @@ const updateTimeline = (state, timeline, status, references) => {
 | 
				
			||||||
  return state;
 | 
					  return state;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const deleteStatus = (state, id, accountId, references) => {
 | 
					const deleteStatus = (state, id, accountId, references, reblogOf) => {
 | 
				
			||||||
 | 
					  if (reblogOf) {
 | 
				
			||||||
 | 
					    // If we are deleting a reblog, just replace reblog with its original
 | 
				
			||||||
 | 
					    return state.updateIn(['home', 'items'], list => list.map(item => item === id ? reblogOf : item));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Remove references from timelines
 | 
					  // Remove references from timelines
 | 
				
			||||||
  ['home', 'mentions', 'public', 'tag'].forEach(function (timeline) {
 | 
					  ['home', 'mentions', 'public', 'tag'].forEach(function (timeline) {
 | 
				
			||||||
    state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id));
 | 
					    state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id));
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Remove references from account timelines
 | 
					  // Remove references from account timelines
 | 
				
			||||||
  state = state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => list.filterNot(item => item === id));
 | 
					  state = state.updateIn(['accounts_timelines', accountId, 'items'], Immutable.List([]), list => list.filterNot(item => item === id));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Remove references from context
 | 
					  // Remove references from context
 | 
				
			||||||
  state.getIn(['descendants', id], Immutable.List()).forEach(descendantId => {
 | 
					  state.getIn(['descendants', id], Immutable.List()).forEach(descendantId => {
 | 
				
			||||||
| 
						 | 
					@ -202,8 +224,11 @@ const resetTimeline = (state, timeline, id) => {
 | 
				
			||||||
  if (timeline === 'tag' && state.getIn([timeline, 'id']) !== id) {
 | 
					  if (timeline === 'tag' && state.getIn([timeline, 'id']) !== id) {
 | 
				
			||||||
    state = state.update(timeline, map => map
 | 
					    state = state.update(timeline, map => map
 | 
				
			||||||
        .set('id', id)
 | 
					        .set('id', id)
 | 
				
			||||||
 | 
					        .set('isLoading', true)
 | 
				
			||||||
        .set('loaded', false)
 | 
					        .set('loaded', false)
 | 
				
			||||||
        .update('items', list => list.clear()));
 | 
					        .update('items', list => list.clear()));
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    state = state.setIn([timeline, 'isLoading'], true);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return state;
 | 
					  return state;
 | 
				
			||||||
| 
						 | 
					@ -211,27 +236,37 @@ const resetTimeline = (state, timeline, id) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function timelines(state = initialState, action) {
 | 
					export default function timelines(state = initialState, action) {
 | 
				
			||||||
  switch(action.type) {
 | 
					  switch(action.type) {
 | 
				
			||||||
    case TIMELINE_REFRESH_REQUEST:
 | 
					  case TIMELINE_REFRESH_REQUEST:
 | 
				
			||||||
      return resetTimeline(state, action.timeline, action.id);
 | 
					  case TIMELINE_EXPAND_REQUEST:
 | 
				
			||||||
    case TIMELINE_REFRESH_SUCCESS:
 | 
					    return resetTimeline(state, action.timeline, action.id);
 | 
				
			||||||
      return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses));
 | 
					  case TIMELINE_REFRESH_FAIL:
 | 
				
			||||||
    case TIMELINE_EXPAND_SUCCESS:
 | 
					  case TIMELINE_EXPAND_FAIL:
 | 
				
			||||||
      return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses));
 | 
					    return state.setIn([action.timeline, 'isLoading'], false);
 | 
				
			||||||
    case TIMELINE_UPDATE:
 | 
					  case TIMELINE_REFRESH_SUCCESS:
 | 
				
			||||||
      return updateTimeline(state, action.timeline, Immutable.fromJS(action.status), action.references);
 | 
					    return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses));
 | 
				
			||||||
    case TIMELINE_DELETE:
 | 
					  case TIMELINE_EXPAND_SUCCESS:
 | 
				
			||||||
      return deleteStatus(state, action.id, action.accountId, action.references);
 | 
					    return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses));
 | 
				
			||||||
    case CONTEXT_FETCH_SUCCESS:
 | 
					  case TIMELINE_UPDATE:
 | 
				
			||||||
      return normalizeContext(state, action.id, Immutable.fromJS(action.ancestors), Immutable.fromJS(action.descendants));
 | 
					    return updateTimeline(state, action.timeline, Immutable.fromJS(action.status), action.references);
 | 
				
			||||||
    case ACCOUNT_TIMELINE_FETCH_SUCCESS:
 | 
					  case TIMELINE_DELETE:
 | 
				
			||||||
      return normalizeAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.replace);
 | 
					    return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
 | 
				
			||||||
    case ACCOUNT_TIMELINE_EXPAND_SUCCESS:
 | 
					  case CONTEXT_FETCH_SUCCESS:
 | 
				
			||||||
      return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses));
 | 
					    return normalizeContext(state, action.id, Immutable.fromJS(action.ancestors), Immutable.fromJS(action.descendants));
 | 
				
			||||||
    case ACCOUNT_BLOCK_SUCCESS:
 | 
					  case ACCOUNT_TIMELINE_FETCH_REQUEST:
 | 
				
			||||||
      return filterTimelines(state, action.relationship, action.statuses);
 | 
					  case ACCOUNT_TIMELINE_EXPAND_REQUEST:
 | 
				
			||||||
    case TIMELINE_SCROLL_TOP:
 | 
					    return state.updateIn(['accounts_timelines', action.id], Immutable.Map(), map => map.set('isLoading', true));
 | 
				
			||||||
      return state.setIn([action.timeline, 'top'], action.top);
 | 
					  case ACCOUNT_TIMELINE_FETCH_FAIL:
 | 
				
			||||||
    default:
 | 
					  case ACCOUNT_TIMELINE_EXPAND_FAIL:
 | 
				
			||||||
      return state;
 | 
					    return state.updateIn(['accounts_timelines', action.id], Immutable.Map(), map => map.set('isLoading', false));
 | 
				
			||||||
 | 
					  case ACCOUNT_TIMELINE_FETCH_SUCCESS:
 | 
				
			||||||
 | 
					    return normalizeAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.replace);
 | 
				
			||||||
 | 
					  case ACCOUNT_TIMELINE_EXPAND_SUCCESS:
 | 
				
			||||||
 | 
					    return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses));
 | 
				
			||||||
 | 
					  case ACCOUNT_BLOCK_SUCCESS:
 | 
				
			||||||
 | 
					    return filterTimelines(state, action.relationship, action.statuses);
 | 
				
			||||||
 | 
					  case TIMELINE_SCROLL_TOP:
 | 
				
			||||||
 | 
					    return state.setIn([action.timeline, 'top'], action.top);
 | 
				
			||||||
 | 
					  default:
 | 
				
			||||||
 | 
					    return state;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
		Reference in a new issue