Browse Source

Adding React.js, Redux, revamping dashboard

master
Eugen Rochko 7 years ago
parent
commit
49520d6e62
34 changed files with 297 additions and 75 deletions
  1. +1
    -0
      .gitignore
  2. +3
    -0
      Gemfile
  3. +16
    -0
      Gemfile.lock
  4. +3
    -1
      app/assets/javascripts/application.js
  5. +0
    -13
      app/assets/javascripts/channels/timeline.js
  6. +9
    -0
      app/assets/javascripts/components.js
  7. +0
    -0
      app/assets/javascripts/components/.gitkeep
  8. +18
    -0
      app/assets/javascripts/components/actions/statuses.jsx
  9. +19
    -0
      app/assets/javascripts/components/components/column.jsx
  10. +15
    -0
      app/assets/javascripts/components/components/column_header.jsx
  11. +15
    -0
      app/assets/javascripts/components/components/columns_area.jsx
  12. +16
    -0
      app/assets/javascripts/components/components/frontend.jsx
  13. +8
    -0
      app/assets/javascripts/components/components/nav_bar.jsx
  14. +19
    -0
      app/assets/javascripts/components/components/status.jsx
  15. +22
    -0
      app/assets/javascripts/components/components/status_list.jsx
  16. +40
    -0
      app/assets/javascripts/components/containers/root.jsx
  17. +10
    -0
      app/assets/javascripts/components/containers/status_list_container.jsx
  18. +6
    -0
      app/assets/javascripts/components/reducers/index.jsx
  19. +17
    -0
      app/assets/javascripts/components/reducers/statuses.jsx
  20. +6
    -0
      app/assets/javascripts/components/store/configureStore.jsx
  21. +17
    -0
      app/assets/stylesheets/application.scss
  22. +2
    -0
      app/controllers/application_controller.rb
  23. +3
    -3
      app/controllers/home_controller.rb
  24. +0
    -2
      app/controllers/settings_controller.rb
  25. +0
    -2
      app/controllers/statuses_controller.rb
  26. +1
    -1
      app/models/feed.rb
  27. +1
    -1
      app/services/fan_out_on_write_service.rb
  28. +1
    -10
      app/views/home/index.html.haml
  29. +1
    -1
      app/views/layouts/application.html.haml
  30. +0
    -39
      app/views/layouts/dashboard.html.haml
  31. +4
    -2
      config/application.rb
  32. +2
    -0
      config/environments/development.rb
  33. +2
    -0
      config/environments/production.rb
  34. +20
    -0
      package.json

+ 1
- 0
.gitignore View File

@@ -20,3 +20,4 @@ public/system
public/assets
.env
.env.*
node_modules/

+ 3
- 0
Gemfile View File

@@ -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'


+ 16
- 0
Gemfile.lock View File

@@ -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


+ 3
- 1
app/assets/javascripts/application.js View File

@@ -12,4 +12,6 @@
//
//= require jquery
//= require jquery_ujs
//= require_tree .
//= require components
//= require cable
//= require mastodon-logo

+ 0
- 13
app/assets/javascripts/channels/timeline.js View File

@@ -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));
}
});

+ 9
- 0
app/assets/javascripts/components.js View File

@@ -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
app/assets/javascripts/components/.gitkeep View File


+ 18
- 0
app/assets/javascripts/components/actions/statuses.jsx View File

@@ -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
};
}

+ 19
- 0
app/assets/javascripts/components/components/column.jsx View File

@@ -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;

+ 15
- 0
app/assets/javascripts/components/components/column_header.jsx View File

@@ -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;

+ 15
- 0
app/assets/javascripts/components/components/columns_area.jsx View File

@@ -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;

+ 16
- 0
app/assets/javascripts/components/components/frontend.jsx View File

@@ -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;

+ 8
- 0
app/assets/javascripts/components/components/nav_bar.jsx View File

@@ -0,0 +1,8 @@
const NavBar = React.createClass({

render: function() {
return <div style={{ background: '#2f3441', width: '60px', margin: '10px', marginRight: '0' }} />;
}
});

export default NavBar;

+ 19
- 0
app/assets/javascripts/components/components/status.jsx View File

@@ -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;

+ 22
- 0
app/assets/javascripts/components/components/status_list.jsx View File

@@ -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;

+ 40
- 0
app/assets/javascripts/components/containers/root.jsx View File

@@ -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;

+ 10
- 0
app/assets/javascripts/components/containers/status_list_container.jsx View File

@@ -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);

+ 6
- 0
app/assets/javascripts/components/reducers/index.jsx View File

@@ -0,0 +1,6 @@
import { combineReducers } from 'redux-immutable';
import statuses from './statuses';

export default combineReducers({
statuses
});

+ 17
- 0
app/assets/javascripts/components/reducers/statuses.jsx View File

@@ -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;
}
}

+ 6
- 0
app/assets/javascripts/components/store/configureStore.jsx View File

@@ -0,0 +1,6 @@
import { createStore } from 'redux';
import appReducer from '../reducers';

export default function configureStore(initialState) {
return createStore(appReducer, initialState);
}

+ 17
- 0
app/assets/stylesheets/application.scss View File

@@ -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 {


+ 2
- 0
app/controllers/application_controller.rb View File

@@ -12,6 +12,8 @@ class ApplicationController < ActionController::Base
end
end

helper_method :current_account

protected

def current_account


+ 3
- 3
app/controllers/home_controller.rb View File

@@ -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

+ 0
- 2
app/controllers/settings_controller.rb View File

@@ -1,6 +1,4 @@
class SettingsController < ApplicationController
layout 'dashboard'

before_action :authenticate_user!
before_action :set_account



+ 0
- 2
app/controllers/statuses_controller.rb View File

@@ -1,6 +1,4 @@
class StatusesController < ApplicationController
layout 'dashboard'

before_action :authenticate_user!

def create


+ 1
- 1
app/models/feed.rb View File

@@ -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


+ 1
- 1
app/services/fan_out_on_write_service.rb View File

@@ -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)


+ 1
- 10
app/views/home/index.html.haml View File

@@ -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
- 1
app/views/layouts/application.html.haml View File

@@ -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

+ 0
- 39
app/views/layouts/dashboard.html.haml View File

@@ -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"

+ 4
- 2
config/application.rb View File

@@ -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

+ 2
- 0
config/environments/development.rb View File

@@ -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'


+ 2
- 0
config/environments/production.rb View File

@@ -80,4 +80,6 @@ Rails.application.configure do
}

config.action_mailer.delivery_method = :smtp

config.react.variant = :production
end

+ 20
- 0
package.json View File

@@ -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…
Cancel
Save