about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2016-08-24 17:56:44 +0200
committerEugen Rochko <eugen@zeonfederated.com>2016-08-24 17:56:44 +0200
commit49520d6e627e49a1f9f1b8cfa9b323450307fcc6 (patch)
tree49b3f093b1791bfa9434a4507d45944342d2dc43
parent68c93f8b859617fb6bb2dc5cf6c5f9a6362bf6a8 (diff)
Adding React.js, Redux, revamping dashboard
-rw-r--r--.gitignore1
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.lock16
-rw-r--r--app/assets/javascripts/application.js4
-rw-r--r--app/assets/javascripts/channels/timeline.js13
-rw-r--r--app/assets/javascripts/components.js9
-rw-r--r--app/assets/javascripts/components/.gitkeep0
-rw-r--r--app/assets/javascripts/components/actions/statuses.jsx18
-rw-r--r--app/assets/javascripts/components/components/column.jsx19
-rw-r--r--app/assets/javascripts/components/components/column_header.jsx15
-rw-r--r--app/assets/javascripts/components/components/columns_area.jsx15
-rw-r--r--app/assets/javascripts/components/components/frontend.jsx16
-rw-r--r--app/assets/javascripts/components/components/nav_bar.jsx8
-rw-r--r--app/assets/javascripts/components/components/status.jsx19
-rw-r--r--app/assets/javascripts/components/components/status_list.jsx22
-rw-r--r--app/assets/javascripts/components/containers/root.jsx40
-rw-r--r--app/assets/javascripts/components/containers/status_list_container.jsx10
-rw-r--r--app/assets/javascripts/components/reducers/index.jsx6
-rw-r--r--app/assets/javascripts/components/reducers/statuses.jsx17
-rw-r--r--app/assets/javascripts/components/store/configureStore.jsx6
-rw-r--r--app/assets/stylesheets/application.scss17
-rw-r--r--app/controllers/application_controller.rb2
-rw-r--r--app/controllers/home_controller.rb6
-rw-r--r--app/controllers/settings_controller.rb2
-rw-r--r--app/controllers/statuses_controller.rb2
-rw-r--r--app/models/feed.rb2
-rw-r--r--app/services/fan_out_on_write_service.rb2
-rw-r--r--app/views/home/index.html.haml11
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/layouts/dashboard.html.haml39
-rw-r--r--config/application.rb6
-rw-r--r--config/environments/development.rb2
-rw-r--r--config/environments/production.rb2
-rw-r--r--package.json20
34 files changed, 297 insertions, 75 deletions
diff --git a/.gitignore b/.gitignore
index a289a4983..340657ee9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,4 @@ public/system
 public/assets
 .env
 .env.*
+node_modules/
diff --git a/Gemfile b/Gemfile
index 6b6ded4d8..c4f4480f1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -38,6 +38,9 @@ gem 'rack-attack'
 gem 'sidekiq'
 gem 'sinatra', require: nil, github: 'sinatra'
 
+gem 'react-rails'
+gem 'browserify-rails'
+
 group :development, :test do
   gem 'rspec-rails'
   gem 'pry-rails'
diff --git a/Gemfile.lock b/Gemfile.lock
index 19b9b7fb6..919a40a56 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -53,6 +53,10 @@ GEM
     addressable (2.4.0)
     arel (7.1.1)
     ast (2.3.0)
+    babel-source (5.8.35)
+    babel-transpiler (0.7.0)
+      babel-source (>= 4.0, < 6)
+      execjs (~> 2.0)
     bcrypt (3.1.11)
     better_errors (2.1.1)
       coderay (>= 1.0.0)
@@ -60,6 +64,9 @@ GEM
       rack (>= 0.9.0)
     binding_of_caller (0.7.2)
       debug_inspector (>= 0.0.1)
+    browserify-rails (3.1.0)
+      railties (>= 4.0.0, < 5.1)
+      sprockets (>= 3.5.2)
     builder (3.2.2)
     bullet (5.3.0)
       activesupport (>= 3.0.0)
@@ -245,6 +252,13 @@ GEM
     rake (11.2.2)
     rdoc (4.2.2)
       json (~> 1.4)
+    react-rails (1.8.2)
+      babel-transpiler (>= 0.7.0)
+      coffee-script-source (~> 1.8)
+      connection_pool
+      execjs
+      railties (>= 3.2)
+      tilt
     redis (3.3.1)
     ref (2.0.0)
     responders (2.3.0)
@@ -348,6 +362,7 @@ DEPENDENCIES
   addressable
   better_errors
   binding_of_caller
+  browserify-rails
   bullet
   coffee-rails (~> 4.1.0)
   devise
@@ -380,6 +395,7 @@ DEPENDENCIES
   rails (= 5.0.0.1)
   rails_12factor
   rails_autolink
+  react-rails
   redis (~> 3.2)
   rspec-rails
   rspec-sidekiq
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 646c5aba4..b9d77b07f 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -12,4 +12,6 @@
 //
 //= require jquery
 //= require jquery_ujs
-//= require_tree .
+//= require components
+//= require cable
+//= require mastodon-logo
diff --git a/app/assets/javascripts/channels/timeline.js b/app/assets/javascripts/channels/timeline.js
deleted file mode 100644
index ca7c50d12..000000000
--- a/app/assets/javascripts/channels/timeline.js
+++ /dev/null
@@ -1,13 +0,0 @@
-App.timeline = App.cable.subscriptions.create("TimelineChannel", {
-  connected: function() {
-    console.log('Connected');
-  },
-
-  disconnected: function() {
-    console.log('Disconnected');
-  },
-
-  received: function(data) {
-    console.log(JSON.parse(data.message));
-  }
-});
diff --git a/app/assets/javascripts/components.js b/app/assets/javascripts/components.js
new file mode 100644
index 000000000..d4d9b97e4
--- /dev/null
+++ b/app/assets/javascripts/components.js
@@ -0,0 +1,9 @@
+//= require_self
+//= require react_ujs
+
+window.React    = require('react');
+window.ReactDOM = require('react-dom');
+
+//= require_tree ./components
+
+window.Root = require('./components/containers/root');
diff --git a/app/assets/javascripts/components/.gitkeep b/app/assets/javascripts/components/.gitkeep
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/app/assets/javascripts/components/.gitkeep
diff --git a/app/assets/javascripts/components/actions/statuses.jsx b/app/assets/javascripts/components/actions/statuses.jsx
new file mode 100644
index 000000000..21821b8ba
--- /dev/null
+++ b/app/assets/javascripts/components/actions/statuses.jsx
@@ -0,0 +1,18 @@
+export const SET_TIMELINE = 'SET_TIMELINE';
+export const ADD_STATUS   = 'ADD_STATUS';
+
+export function setTimeline(timeline, statuses) {
+  return {
+    type: SET_TIMELINE,
+    timeline: timeline,
+    statuses: statuses
+  };
+}
+
+export function addStatus(timeline, status) {
+  return {
+    type: ADD_STATUS,
+    timeline: timeline,
+    status: status
+  };
+}
diff --git a/app/assets/javascripts/components/components/column.jsx b/app/assets/javascripts/components/components/column.jsx
new file mode 100644
index 000000000..c585b6b0b
--- /dev/null
+++ b/app/assets/javascripts/components/components/column.jsx
@@ -0,0 +1,19 @@
+import StatusListContainer from '../containers/status_list_container';
+import ColumnHeader        from './column_header';
+
+const Column = React.createClass({
+  propTypes: {
+    type: React.PropTypes.string
+  },
+
+  render: function() {
+    return (
+      <div style={{ width: '350px', flex: '0 0 auto', background: '#282c37', margin: '10px', marginRight: '0', display: 'flex', flexDirection: 'column' }}>
+        <ColumnHeader type={this.props.type} />
+        <StatusListContainer type={this.props.type} />
+      </div>
+    );
+  }
+});
+
+export default Column;
diff --git a/app/assets/javascripts/components/components/column_header.jsx b/app/assets/javascripts/components/components/column_header.jsx
new file mode 100644
index 000000000..e2f7d7c1c
--- /dev/null
+++ b/app/assets/javascripts/components/components/column_header.jsx
@@ -0,0 +1,15 @@
+const ColumnHeader = React.createClass({
+  propTypes: {
+    type: React.PropTypes.string
+  },
+
+  render: function() {
+    return (
+      <div style={{ padding: '15px', fontSize: '16px', background: '#2f3441', flex: '0 0 auto' }}>
+        {this.props.type}
+      </div>
+    );
+  }
+});
+
+export default ColumnHeader;
diff --git a/app/assets/javascripts/components/components/columns_area.jsx b/app/assets/javascripts/components/components/columns_area.jsx
new file mode 100644
index 000000000..1c46f722d
--- /dev/null
+++ b/app/assets/javascripts/components/components/columns_area.jsx
@@ -0,0 +1,15 @@
+import Column from './column';
+
+const ColumnsArea = React.createClass({
+
+  render: function() {
+    return (
+      <div style={{ display: 'flex', flexDirection: 'row', flex: '1' }}>
+        <Column type='home' />
+        <Column type='mentions' />
+      </div>
+    );
+  }
+});
+
+export default ColumnsArea;
diff --git a/app/assets/javascripts/components/components/frontend.jsx b/app/assets/javascripts/components/components/frontend.jsx
new file mode 100644
index 000000000..6f9c46fa9
--- /dev/null
+++ b/app/assets/javascripts/components/components/frontend.jsx
@@ -0,0 +1,16 @@
+import NavBar      from './nav_bar';
+import ColumnsArea from './columns_area';
+
+const Frontend = React.createClass({
+
+  render: function() {
+    return (
+      <div style={{ flex: '0 0 auto', display: 'flex', width: '100%', height: '100%', background: '#1a1c23' }}>
+        <NavBar />
+        <ColumnsArea />
+      </div>
+    );
+  }
+});
+
+export default Frontend;
diff --git a/app/assets/javascripts/components/components/nav_bar.jsx b/app/assets/javascripts/components/components/nav_bar.jsx
new file mode 100644
index 000000000..1ece3cc34
--- /dev/null
+++ b/app/assets/javascripts/components/components/nav_bar.jsx
@@ -0,0 +1,8 @@
+const NavBar = React.createClass({
+
+  render: function() {
+    return <div style={{ background: '#2f3441', width: '60px', margin: '10px', marginRight: '0' }} />;
+  }
+});
+
+export default NavBar;
diff --git a/app/assets/javascripts/components/components/status.jsx b/app/assets/javascripts/components/components/status.jsx
new file mode 100644
index 000000000..9bbb02077
--- /dev/null
+++ b/app/assets/javascripts/components/components/status.jsx
@@ -0,0 +1,19 @@
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+const Status = React.createClass({
+  propTypes: {
+    status: ImmutablePropTypes.map.isRequired
+  },
+
+  render: function() {
+    console.log(this.props.status.toJS());
+
+    return (
+      <div style={{ height: '100px' }}>
+        {this.props.status.getIn(['account', 'username'])}: {this.props.status.get('content')}
+      </div>
+    );
+  }
+});
+
+export default Status;
diff --git a/app/assets/javascripts/components/components/status_list.jsx b/app/assets/javascripts/components/components/status_list.jsx
new file mode 100644
index 000000000..c986c773b
--- /dev/null
+++ b/app/assets/javascripts/components/components/status_list.jsx
@@ -0,0 +1,22 @@
+import Status             from './status';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+const StatusList = React.createClass({
+  propTypes: {
+    statuses: ImmutablePropTypes.list.isRequired
+  },
+
+  render: function() {
+    return (
+      <div style={{ overflowY: 'scroll', flex: '1 1 auto' }}>
+        <div>
+          {this.props.statuses.map((status) => {
+            return <Status key={status.get('id')} status={status} />;
+          })}
+        </div>
+      </div>
+    );
+  }
+});
+
+export default StatusList;
diff --git a/app/assets/javascripts/components/containers/root.jsx b/app/assets/javascripts/components/containers/root.jsx
new file mode 100644
index 000000000..7da984d89
--- /dev/null
+++ b/app/assets/javascripts/components/containers/root.jsx
@@ -0,0 +1,40 @@
+import { Provider }               from 'react-redux';
+import configureStore             from '../store/configureStore';
+import Frontend                   from '../components/frontend';
+import { setTimeline, addStatus } from '../actions/statuses';
+
+const store = configureStore();
+
+const Root = React.createClass({
+
+  componentWillMount() {
+    for (var timelineType in this.props.timelines) {
+      if (this.props.timelines.hasOwnProperty(timelineType)) {
+        store.dispatch(setTimeline(timelineType, JSON.parse(this.props.timelines[timelineType])));
+      }
+    }
+
+    if (typeof App !== 'undefined') {
+      App.timeline = App.cable.subscriptions.create("TimelineChannel", {
+        connected: function() {},
+
+        disconnected: function() {},
+
+        received: function(data) {
+          return store.dispatch(addStatus(data.timeline, JSON.parse(data.message)));
+        }
+      });
+    }
+  },
+
+  render() {
+    return (
+      <Provider store={store}>
+        <Frontend />
+      </Provider>
+    );
+  }
+
+});
+
+export default Root;
diff --git a/app/assets/javascripts/components/containers/status_list_container.jsx b/app/assets/javascripts/components/containers/status_list_container.jsx
new file mode 100644
index 000000000..c2e55db66
--- /dev/null
+++ b/app/assets/javascripts/components/containers/status_list_container.jsx
@@ -0,0 +1,10 @@
+import { connect } from 'react-redux';
+import StatusList  from '../components/status_list';
+
+const mapStateToProps = function (state, props) {
+  return {
+    statuses: state.getIn(['statuses', props.type])
+  };
+};
+
+export default connect(mapStateToProps)(StatusList);
diff --git a/app/assets/javascripts/components/reducers/index.jsx b/app/assets/javascripts/components/reducers/index.jsx
new file mode 100644
index 000000000..c7e858f38
--- /dev/null
+++ b/app/assets/javascripts/components/reducers/index.jsx
@@ -0,0 +1,6 @@
+import { combineReducers } from 'redux-immutable';
+import statuses            from './statuses';
+
+export default combineReducers({
+  statuses
+});
diff --git a/app/assets/javascripts/components/reducers/statuses.jsx b/app/assets/javascripts/components/reducers/statuses.jsx
new file mode 100644
index 000000000..d69d66328
--- /dev/null
+++ b/app/assets/javascripts/components/reducers/statuses.jsx
@@ -0,0 +1,17 @@
+import { SET_TIMELINE, ADD_STATUS } from '../actions/statuses';
+import Immutable                    from 'immutable';
+
+const initialState = Immutable.Map();
+
+export default function statuses(state = initialState, action) {
+  switch(action.type) {
+    case SET_TIMELINE:
+      return state.set(action.timeline, Immutable.fromJS(action.statuses));
+    case ADD_STATUS:
+      return state.update(action.timeline, function (list) {
+        list.unshift(Immutable.fromJS(action.status));
+      });
+    default:
+      return state;
+  }
+}
diff --git a/app/assets/javascripts/components/store/configureStore.jsx b/app/assets/javascripts/components/store/configureStore.jsx
new file mode 100644
index 000000000..bb5d664d0
--- /dev/null
+++ b/app/assets/javascripts/components/store/configureStore.jsx
@@ -0,0 +1,6 @@
+import { createStore } from 'redux';
+import appReducer from '../reducers';
+
+export default function configureStore(initialState) {
+  return createStore(appReducer, initialState);
+}
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 1f35053a1..668b6f90d 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -67,6 +67,23 @@ body {
   font-weight: 400;
   color: #fff;
   padding-bottom: 140px;
+  text-rendering: optimizelegibility;
+  font-feature-settings: "kern";
+
+  &.app-body {
+    position: fixed;
+    width: 100%;
+    height: 100%;
+    padding: 0;
+  }
+}
+
+.app-holder {
+  display: flex;
+  width: 100%;
+  height: 100%;
+  align-items: center;
+  justify-content: center;
 }
 
 .container {
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index c190abdf2..399faa21e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -12,6 +12,8 @@ class ApplicationController < ActionController::Base
     end
   end
 
+  helper_method :current_account
+
   protected
 
   def current_account
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 9b0b36a86..57973ba49 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -1,9 +1,9 @@
 class HomeController < ApplicationController
-  layout 'dashboard'
-
   before_action :authenticate_user!
 
   def index
-    @timeline = Feed.new(:home, current_user.account).get(10, params[:max_id])
+    @body_classes = 'app-body'
+    @home         = Feed.new(:home, current_user.account).get(20)
+    @mentions     = Feed.new(:mentions, current_user.account).get(20)
   end
 end
diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb
index cb4688292..f6ba958fb 100644
--- a/app/controllers/settings_controller.rb
+++ b/app/controllers/settings_controller.rb
@@ -1,6 +1,4 @@
 class SettingsController < ApplicationController
-  layout 'dashboard'
-
   before_action :authenticate_user!
   before_action :set_account
 
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index 99f3e5079..d6779e0a7 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -1,6 +1,4 @@
 class StatusesController < ApplicationController
-  layout 'dashboard'
-
   before_action :authenticate_user!
 
   def create
diff --git a/app/models/feed.rb b/app/models/feed.rb
index 1d6c2cfbf..e7574956e 100644
--- a/app/models/feed.rb
+++ b/app/models/feed.rb
@@ -4,7 +4,7 @@ class Feed
     @account = account
   end
 
-  def get(limit, max_id)
+  def get(limit, max_id = nil)
     max_id     = '+inf' if max_id.nil?
     unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", '-inf', limit: [0, limit])
     status_map = Hash.new
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index dc030736b..6e96fa75b 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -31,7 +31,7 @@ class FanOutOnWriteService < BaseService
   def push(type, receiver, status)
     redis.zadd(FeedManager.key(type, receiver.id), status.id, status.id)
     trim(type, receiver)
-    ActionCable.server.broadcast("timeline:#{receiver.id}", message: inline_render(receiver, status))
+    ActionCable.server.broadcast("timeline:#{receiver.id}", timeline: type, message: inline_render(receiver, status))
   end
 
   def trim(type, receiver)
diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml
index a663bf37b..941397384 100644
--- a/app/views/home/index.html.haml
+++ b/app/views/home/index.html.haml
@@ -1,10 +1 @@
-= simple_form_for Status.new, url: statuses_path, method: :post do |f|
-  = f.input :text, required: true, autofocus: true, label: false, placeholder: 'What are you up to?'
-
-  .form-actions
-    = f.button :submit, 'Post update'
-
-- content_for :raw_content do
-  .activity-stream.activity-stream-embedded
-    - @timeline.each do |status|
-      = render partial: 'stream_entries/status', locals: { status: status }
+= react_component 'Root', { timelines: { home: render(file: 'api/statuses/home', locals: { statuses: @home }, formats: :json), mentions: render(file: 'api/statuses/mentions', locals: { statuses: @mentions }, formats: :json) }}, class: 'app-holder', prerender: false
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index afd29b04a..1746d9964 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -9,5 +9,5 @@
     = javascript_include_tag 'application'
     = csrf_meta_tags
     = yield :header_tags
-  %body
+  %body{ class: @body_classes }
     = content_for?(:content) ? yield(:content) : yield
diff --git a/app/views/layouts/dashboard.html.haml b/app/views/layouts/dashboard.html.haml
deleted file mode 100644
index 7336cdcc4..000000000
--- a/app/views/layouts/dashboard.html.haml
+++ /dev/null
@@ -1,39 +0,0 @@
-- content_for :content do
-  .dashboard-wrapper
-    .dashboard__sidebar
-      .dashboard__top-bar.alternate
-        &nbsp;
-      .dashboard__current-user
-        = link_to account_path(current_user.account) do
-          = image_tag current_user.account.avatar.url(:medium), class: 'dashboard__current-user__avatar'
-          %strong.dashboard__current-user__display-name= display_name(current_user.account)
-          %span.dashboard__current-user__username= "@#{current_user.account.username}"
-      %ul
-        %li{ class: active_nav_class(root_path) }
-          = link_to root_path do
-            = fa_icon 'home'
-            Home
-        %li{ class: active_nav_class(oauth_authorized_applications_path) }
-          = link_to oauth_authorized_applications_path do
-            = fa_icon 'shield'
-            Authorized apps
-        %li{ class: active_nav_class(settings_path) }
-          = link_to settings_path do
-            = fa_icon 'user'
-            Edit profile
-
-    .dashboard__content
-      .dashboard__top-bar
-        = content_for?(:page_title) ? yield(:page_title) : 'Mastodon'
-        %ul
-          %li= link_to fa_icon('gear'), edit_registration_path(current_user), title: 'Change password'
-          %li= link_to fa_icon('sign-out'), destroy_user_session_path, method: :delete, title: 'Sign out'
-
-      .dashboard__content__content= yield
-
-      = yield(:raw_content)
-
-  .footer
-    .domain= Rails.configuration.x.local_domain
-
-= render template: "layouts/application"
diff --git a/config/application.rb b/config/application.rb
index c3ea5851a..81205de32 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -28,12 +28,14 @@ module Mastodon
     config.active_job.queue_adapter = :sidekiq
 
     config.to_prepare do
-      Doorkeeper::ApplicationsController.layout           'dashboard'
-      Doorkeeper::AuthorizedApplicationsController.layout 'dashboard'
+      # Doorkeeper::ApplicationsController.layout           'dashboard'
+      # Doorkeeper::AuthorizedApplicationsController.layout 'dashboard'
       Doorkeeper::AuthorizationsController.layout         'auth'
     end
 
     config.middleware.use Rack::Attack
     config.middleware.use Rack::Deflater
+
+    config.browserify_rails.commandline_options = "--transform [ babelify --presets [ es2015 react ] ] --extension=\".jsx\""
   end
 end
diff --git a/config/environments/development.rb b/config/environments/development.rb
index ba0af1f57..c51d98543 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -63,6 +63,8 @@ Rails.application.configure do
     Bullet.bullet_logger = true
     Bullet.rails_logger = true
   end
+
+  config.react.variant = :development
 end
 
 require 'sidekiq/testing'
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 09b77654f..0a56d4fe0 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -80,4 +80,6 @@ Rails.application.configure do
   }
 
   config.action_mailer.delivery_method = :smtp
+
+  config.react.variant = :production
 end
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..7ba01ecfd
--- /dev/null
+++ b/package.json
@@ -0,0 +1,20 @@
+{
+  "name": "mastodon",
+  "devDependencies": {
+    "babel-preset-es2015": "^6.13.2",
+    "babel-preset-react": "^6.11.1",
+    "babelify": "^7.3.0",
+    "browserify": "^13.1.0",
+    "browserify-incremental": "^3.1.1",
+    "react": "^15.3.0",
+    "react-dom": "^15.3.0",
+    "redux-devtools": "^3.3.1"
+  },
+  "dependencies": {
+    "immutable": "^3.8.1",
+    "react-immutable-proptypes": "^2.1.0",
+    "react-redux": "^4.4.5",
+    "redux": "^3.5.2",
+    "redux-immutable": "^3.0.8"
+  }
+}