mirror of https://github.com/mastodon/mastodon
				
				
				
			Adding React.js, Redux, revamping dashboard
							parent
							
								
									68c93f8b85
								
							
						
					
					
						commit
						49520d6e62
					
				@ -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));
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
@ -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');
 | 
			
		||||
@ -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
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -0,0 +1,8 @@
 | 
			
		||||
const NavBar = React.createClass({
 | 
			
		||||
 | 
			
		||||
  render: function() {
 | 
			
		||||
    return <div style={{ background: '#2f3441', width: '60px', margin: '10px', marginRight: '0' }} />;
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default NavBar;
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -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;
 | 
			
		||||
@ -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);
 | 
			
		||||
@ -0,0 +1,6 @@
 | 
			
		||||
import { combineReducers } from 'redux-immutable';
 | 
			
		||||
import statuses            from './statuses';
 | 
			
		||||
 | 
			
		||||
export default combineReducers({
 | 
			
		||||
  statuses
 | 
			
		||||
});
 | 
			
		||||
@ -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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,6 @@
 | 
			
		||||
import { createStore } from 'redux';
 | 
			
		||||
import appReducer from '../reducers';
 | 
			
		||||
 | 
			
		||||
export default function configureStore(initialState) {
 | 
			
		||||
  return createStore(appReducer, initialState);
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -1,39 +0,0 @@
 | 
			
		||||
- content_for :content do
 | 
			
		||||
  .dashboard-wrapper
 | 
			
		||||
    .dashboard__sidebar
 | 
			
		||||
      .dashboard__top-bar.alternate
 | 
			
		||||
         
 | 
			
		||||
      .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"
 | 
			
		||||
@ -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"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue