Skip to content

Commit

Permalink
Basic redux integration for thread component
Browse files Browse the repository at this point in the history
  • Loading branch information
DmitryTsepelev committed Jul 8, 2018
1 parent dcd97a9 commit 89e5033
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 66 deletions.
5 changes: 5 additions & 0 deletions web/app/components/thread/getCollapsedComments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LS_COLLAPSE_KEY } from 'common/constants';

const getCollapsedComments = () => JSON.parse(localStorage.getItem(LS_COLLAPSE_KEY) || '[]');

export default getCollapsedComments;
4 changes: 4 additions & 0 deletions web/app/components/thread/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
import { collapsedThreads } from './thread.reducers';

export const threadReducers = { collapsedThreads };

export { default } from './thread';
5 changes: 5 additions & 0 deletions web/app/components/thread/saveCollapsedComments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LS_COLLAPSE_KEY } from 'common/constants';

const saveCollapsedComments = comments => localStorage.setItem(LS_COLLAPSE_KEY, JSON.stringify(comments));

export default saveCollapsedComments;
6 changes: 6 additions & 0 deletions web/app/components/thread/thread.actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const THREAD_SET_COLLAPSE = 'THREAD/COLLAPSE_SET';
export const setCollapse = (comment, collapsed) => ({
type: THREAD_SET_COLLAPSE,
comment,
collapsed,
});
14 changes: 14 additions & 0 deletions web/app/components/thread/thread.getters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import store from 'common/store';

export const getThreadIsCollapsed = (state, comment) => {
let collapsed = state.collapsedThreads[comment.id];

if (collapsed !== null && collapsed !== undefined) {
return collapsed;
}

const config = store.get('config') || {};
const score = comment.score || 0;

return score <= config.critical_score;
};
95 changes: 32 additions & 63 deletions web/app/components/thread/thread.jsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,41 @@
/** @jsx h */
import { h, Component } from 'preact';

import { LS_COLLAPSE_KEY } from 'common/constants';
import { siteId, url } from 'common/settings';
import store from 'common/store';
import { connect } from 'preact-redux';

import Comment from 'components/comment';
import { setCollapse } from './thread.actions';
import { getThreadIsCollapsed } from './thread.getters';

export default class Thread extends Component {
class Thread extends Component {
constructor(props) {
super(props);

if (this.props.data && this.props.data.comment) {
this.updateCollapsedState(this.props.data.comment);
}

this.onCollapseToggle = this.onCollapseToggle.bind(this);
}

componentWillReceiveProps(nextProps) {
if (nextProps.data && nextProps.data.comment) {
this.updateCollapsedState(nextProps.data.comment);
}
}

updateCollapsedState(comment) {
const config = store.get('config') || {};
const score = comment.score || 0;

this.lsCollapsedID = `${siteId}_${url}_${comment.id}`;

this.state = {
collapsed:
(!this.state.isCollapsedChanged && score <= config.critical_score) ||
getCollapsedComments().includes(this.lsCollapsedID),
isCollapsedChanged: true,
};
}

onCollapseToggle() {
const collapsed = !this.state.collapsed;

this.setState({ collapsed: !this.state.collapsed });

let collapsedComments = getCollapsedComments();

if (collapsed) {
if (!collapsedComments.includes(this.lsCollapsedID)) {
collapsedComments = collapsedComments.concat(this.lsCollapsedID);
}
} else {
collapsedComments = collapsedComments.filter(id => id !== this.lsCollapsedID);
}

saveCollapsedComments(collapsedComments);
this.props.setCollapse(this.props.data.comment, !this.props.collapsed);
}

render(props, { collapsed }) {
render(props) {
const {
collapsed,
data: { comment, replies = [] },
mods = {},
} = props;

let threadComments = null;
if (!collapsed && !!replies.length) {
threadComments = replies.map(thread => (
<Thread
key={thread.comment.id}
data={thread}
mods={{ level: mods.level < 5 ? mods.level + 1 : mods.level }}
onReply={props.onReply}
onEdit={props.onEdit}
/>
));
}

return (
<div
className={b('thread', props)}
Expand All @@ -76,26 +50,21 @@ export default class Thread extends Component {
onCollapseToggle={this.onCollapseToggle}
/>

{!collapsed &&
!!replies.length &&
replies.map(thread => (
<Thread
key={thread.comment.id}
data={thread}
mods={{ level: mods.level < 5 ? mods.level + 1 : mods.level }}
onReply={props.onReply}
onEdit={props.onEdit}
/>
))}
{threadComments}
</div>
);
}
}

function getCollapsedComments() {
return JSON.parse(localStorage.getItem(LS_COLLAPSE_KEY) || '[]');
}
const mapStateToProps = (state, props) => ({
collapsed: getThreadIsCollapsed(state, props.data.comment),
});

function saveCollapsedComments(comments) {
localStorage.setItem(LS_COLLAPSE_KEY, JSON.stringify(comments));
}
const mapDispatchToProps = { setCollapse };

const enhance = connect(
mapStateToProps,
mapDispatchToProps
);

export default enhance(Thread);
43 changes: 43 additions & 0 deletions web/app/components/thread/thread.reducers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { THREAD_SET_COLLAPSE } from './thread.actions';
import getCollapsedComments from './getCollapsedComments';
import saveCollapsedComments from './saveCollapsedComments';
import { siteId, url } from 'common/settings';

const collapsedCommentIds = getCollapsedComments()
.map(comment => comment.split('_'))
.filter(components => components[0] === siteId && components[1] === url)
.map(component => component[2]);

const initialState = collapsedCommentIds.reduce((acc, id) => ({ ...acc, [id]: true }), {});

const persistCollapsedComments = (comment, collapsed) => {
const lsCollapsedID = `${siteId}_${url}_${comment.id}`;
let collapsedComments = getCollapsedComments();

if (collapsed) {
collapsedComments = [...new Set(collapsedComments.concat(lsCollapsedID))];
} else {
collapsedComments = collapsedComments.filter(id => id !== lsCollapsedID);
}

saveCollapsedComments(collapsedComments);
};

export const collapsedThreads = (state = initialState, action) => {
switch (action.type) {
case THREAD_SET_COLLAPSE: {
const currentCollapsed = state[action.comment.id];

if (action.collapsed !== currentCollapsed) {
persistCollapsedComments(action.comment, action.collapsed);
}

return {
...state,
[action.comment.id]: action.collapsed,
};
}
default:
return state;
}
};
32 changes: 32 additions & 0 deletions web/app/components/thread/thread.reducers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { setCollapse } from './thread.actions';
import { collapsedThreads } from './thread.reducers';
import { getThreadIsCollapsed } from './thread.getters';
import getCollapsedComments from './getCollapsedComments';

describe('collapsedThreads', () => {
const comment = { id: 1 };

it('should set collapsed to true', () => {
const collapsed = true;
const action = setCollapse(comment, collapsed);

const newState = {
collapsedThreads: collapsedThreads({}, action),
};

expect(getThreadIsCollapsed(newState, comment)).toEqual(collapsed);
expect(getCollapsedComments().length).toEqual(1);
});

it('should set collapsed to false', () => {
const collapsed = false;
const action = setCollapse(comment, collapsed);

const newState = {
collapsedThreads: collapsedThreads({}, action),
};

expect(getThreadIsCollapsed(newState, comment)).toEqual(collapsed);
expect(getCollapsedComments().length).toEqual(0);
});
});
10 changes: 9 additions & 1 deletion web/app/remark.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import loadPolyfills from 'common/polyfills';

import { h, render } from 'preact';
import Root from './components/root';
import { Provider } from 'preact-redux';
import store from './store';
// eslint-disable-next-line no-unused-vars
import ListComments from './components/list-comments'; // TODO: temp solution for extracting styles

Expand All @@ -17,6 +19,12 @@ loadPolyfills().then(() => {
}
});

const Main = () => (
<Provider store={store}>
<Root />
</Provider>
);

function init() {
const node = document.getElementById(NODE_ID);

Expand All @@ -25,5 +33,5 @@ function init() {
return;
}

render(<Root />, node.parentElement, node);
render(<Main />, node.parentElement, node);
}
12 changes: 12 additions & 0 deletions web/app/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createStore } from 'redux';
import { combineReducers } from 'redux';

import { threadReducers } from './components/thread';

const rootReducer = combineReducers({
...threadReducers,
});

const store = createStore(rootReducer);

export default store;
11 changes: 9 additions & 2 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,25 @@
"cross-env": "^5.2.0",
"css-loader": "^0.28.11",
"eslint": "^4.19.1",
"eslint-plugin-jsx-a11y": "^6.1.0",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-jsx-a11y": "^6.1.0",
"eslint-plugin-prettier": "^2.6.1",
"eslint-plugin-react": "^7.10.0",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^0.11.1",
"html-webpack-plugin": "^2.30.1",
"husky": "^0.14.3",
"jest": "^23.1.0",
"jest-localstorage-mock": "^2.2.0",
"lint-staged": "^7.2.0",
"node-sass": "sass/node-sass#v5",
"postcss-csso": "^2.0.0",
"postcss-loader": "^2.1.5",
"postcss-url": "^6.3.1",
"postcss-wrap": "0.0.4",
"preact-redux": "^2.0.3",
"prettier": "^1.13.7",
"redux": "^4.0.0",
"sass-loader": "^6.0.7",
"style-loader": "^0.19.1",
"webpack": "^3.12.0",
Expand All @@ -69,11 +72,15 @@
"^.+\\.jsx?$": "<rootDir>/fileTransformer.js"
},
"setupFiles": [
"<rootDir>/injectGlobalVariable.js"
"<rootDir>/injectGlobalVariable.js",
"jest-localstorage-mock"
],
"moduleDirectories": [
"node_modules",
"<rootDir>/app"
],
"testMatch": [
"<rootDir>/**/*.test.js"
]
}
}

0 comments on commit 89e5033

Please sign in to comment.