Feature(chat): Add help chat logic

This commit is contained in:
Berkeley Martinez
2016-07-11 17:44:50 -07:00
parent d918f02906
commit efcfaf0391
9 changed files with 188 additions and 28 deletions

View File

@ -1,3 +1,4 @@
.chat-embed-help-title,
.chat-embed-main-title {
display: flex;
flex-grow: 1;

View File

@ -1,20 +1,34 @@
import { Observable } from 'rx';
import { Subject, Observable } from 'rx';
import Chat from 'gitter-sidecar';
import types from '../../common/app/redux/types';
import {
openHelpChat,
closeHelpChat,
toggleMainChat
} from '../../common/app/redux/actions';
function createHeader(document) {
export function createHeader(room, title, document) {
const type = room === 'freecodecamp' ? 'main' : 'help';
const div = document.createElement('div');
const span = document.createElement('span');
const actionBar = document.querySelector(
'#chat-embed-main > .gitter-chat-embed-action-bar'
`#chat-embed-${type}> .gitter-chat-embed-action-bar`
);
span.appendChild(document.createTextNode('Free Code Camp\'s Main Chat'));
div.className = 'chat-embed-main-title';
span.appendChild(document.createTextNode(title));
div.className = `chat-embed-${type}-title`;
div.appendChild(span);
actionBar.insertBefore(div, actionBar.firstChild);
}
export default function gitterSaga(actions$, getState, { document }) {
export function createHelpContainer(document) {
const container = document.createElement('aside');
container.id = 'chat-embed-help';
container.className = 'gitter-chat-embed is-collapsed';
document.body.appendChild(container);
return container;
}
export function createMainChat(getState, document) {
let mainChatTitleAdded = false;
const mainChatContainer = document.createElement('aside');
mainChatContainer.id = 'chat-embed-main';
@ -26,7 +40,7 @@ export default function gitterSaga(actions$, getState, { document }) {
targetElement: mainChatContainer
});
const mainChatToggle$ = Observable.fromEventPattern(
const toggle$ = Observable.fromEventPattern(
h => mainChatContainer.addEventListener('gitter-chat-toggle', h),
h => mainChatContainer.removeEventListener('gitter-chat-toggle', h)
)
@ -34,25 +48,128 @@ export default function gitterSaga(actions$, getState, { document }) {
const { isMainChatOpen } = getState().app;
if (!mainChatTitleAdded) {
mainChatTitleAdded = true;
createHeader(document);
createHeader('freecodecamp', 'Free Code Camp\'s Main Chat', document);
}
if (isMainChatOpen === e.detail.state) {
return null;
}
return { type: types.toggleMainChat };
return toggleMainChat();
});
return {
mainChat,
toggle$
};
}
// only one help room may be alive at once
export function createHelpChat(room, container, proxy, document) {
const title = room.replace(/([A-Z])/g, ' $1');
let isTitleAdded = false;
const chat = new Chat({
room: `freecodecamp/${room}`,
activationElement: false,
targetElement: container
});
// return subscription to toggle stream
// dispose when rooms switch
const subscription = Observable.fromEventPattern(
h => container.addEventListener('gitter-chat-toggle', h),
h => container.removeEventListener('gitter-chat-toggle', h)
)
.map(e => {
if (!isTitleAdded) {
isTitleAdded = true;
createHeader(room, title, document);
}
const gitterState = e.detail.state;
return gitterState ? openHelpChat() : closeHelpChat();
})
// use subject proxy to dispatch actions
.subscribe(proxy);
return { chat, subscription };
}
export const cache = {};
export function toggleHelpChat(isOpen, room, proxy, document) {
// check is container is already created
if (!cache['container']) {
cache['container'] = createHelpContainer(document);
}
const { container } = cache;
if (!cache['chat']) {
const {
chat,
subscription
} = createHelpChat(room, container, proxy, document);
cache.chat = chat;
// make sure we clear out old subscription
if (cache.subscription && cache.subscription.dispose) {
cache.subscription.dispose();
}
cache.subscription = subscription;
cache.currentRoom = room;
}
// have we switched rooms?
if (!cache.currentRoom === room) {
// room has changed, if chat object exist, destroy it
// and end subscription to toggle
try {
cache.chat.destroy();
cache.subscription.dispose();
// chat and subscription may not exist at first so we catch errors here
} catch (err) {
console.error(err);
}
// create new chat room and cache
const {
chat,
subscription
} = createHelpChat(room, container, proxy, document);
cache.chat = chat;
cache.subscription = subscription;
cache.currentRoom = room;
}
// all goes well pull chat object from cache
const { chat } = cache;
chat.toggleChat(isOpen);
}
export default function gitterSaga(actions$, getState, { document }) {
const helpToggleProxy = new Subject();
const {
mainChat,
toggle$: mainChatToggle$
} = createMainChat(getState, document);
return Observable.merge(
mainChatToggle$,
helpToggleProxy,
actions$
.filter(({ type }) => (
type === types.openMainChat ||
type === types.closeMainChat ||
type === types.toggleMainChat
type === types.toggleMainChat ||
type === types.toggleHelpChat
))
.map(() => {
const { isMainChatOpen } = getState().app;
.map(({ type }) => {
const state = getState();
let shouldBlur = false;
if (type === types.toggleHelpChat) {
const {
app: { isHelpChatOpen },
challengesApp: { helpChatRoom }
} = state;
shouldBlur = !isHelpChatOpen;
toggleHelpChat(
isHelpChatOpen,
helpChatRoom,
helpToggleProxy,
document
);
}
const { isMainChatOpen } = state.app;
mainChat.toggleChat(isMainChatOpen);
if (!isMainChatOpen) {
shouldBlur = !isMainChatOpen;
if (!shouldBlur) {
document.activeElement.blur();
}
return null;

View File

@ -54,8 +54,9 @@ export const createErrorObservable = error => Observable.just({
// drawers
export const toggleMapDrawer = createAction(types.toggleMapDrawer);
export const toggleWikiDrawer = createAction(types.toggleWikiDrawer);
// chat
export const toggleMainChat = createAction(types.toggleMainChat);
export const toggleHelpChat = createAction(types.toggleHelpChat);
export const openHelpChat = createAction(types.openHelpChat);
export const closeHelpChat = createAction(types.closeHelpChat);
export const toggleNightMode = createAction(types.toggleNightMode);

View File

@ -9,7 +9,8 @@ const initialState = {
csrfToken: '',
windowHeight: 0,
navHeight: 0,
isMainChatOpen: false
isMainChatOpen: false,
isHelpChatOpen: false
};
export default handleActions(
@ -50,6 +51,18 @@ export default handleActions(
...state,
isMainChatOpen: !state.isMainChatOpen
}),
[types.toggleHelpChat]: state => ({
...state,
isHelpChatOpen: !state.isHelpChatOpen
}),
[types.openHelpChat]: state => ({
...state,
isHelpChatOpen: true
}),
[types.closeHelpChat]: state => ({
...state,
isHelpChatOpen: false
}),
[types.delayedRedirect]: (state, { payload }) => ({
...state,
delayedRedirect: payload

View File

@ -29,8 +29,12 @@ export default createTypes([
'toggleMapDrawer',
'toggleWikiDrawer',
// main chat
// chat
'openMainChat',
'closeMainChat',
'toggleMainChat'
'toggleMainChat',
'openHelpChat',
'closeHelpChat',
'toggleHelpChat'
], 'app');

View File

@ -2,7 +2,6 @@ import React, { PropTypes } from 'react';
import ReactDom from 'react-dom';
import { createSelector } from 'reselect';
import { connect } from 'react-redux';
import PureComponent from 'react-pure-render/component';
import { Col, Row } from 'react-bootstrap';
@ -12,8 +11,14 @@ import ToolPanel from './Tool-Panel.jsx';
import { challengeSelector } from '../../redux/selectors';
import { updateHint, executeChallenge } from '../../redux/actions';
import { makeToast } from '../../../../toasts/redux/actions';
import { toggleHelpChat } from '../../../../redux/actions';
const bindableActions = { makeToast, executeChallenge, updateHint };
const bindableActions = {
makeToast,
executeChallenge,
updateHint,
toggleHelpChat
};
const mapStateToProps = createSelector(
challengeSelector,
state => state.app.windowHeight,
@ -53,7 +58,8 @@ export class SidePanel extends PureComponent {
output: PropTypes.string,
hints: PropTypes.string,
updateHint: PropTypes.func,
makeToast: PropTypes.func
makeToast: PropTypes.func,
toggleHelpChat: PropTypes.func
};
renderDescription(description = [ 'Happy Coding!' ], descriptionRegex) {
@ -92,7 +98,8 @@ export class SidePanel extends PureComponent {
hint,
executeChallenge,
updateHint,
makeToast
makeToast,
toggleHelpChat
} = this.props;
const style = {
overflowX: 'hidden',
@ -124,6 +131,7 @@ export class SidePanel extends PureComponent {
executeChallenge={ executeChallenge }
hint={ hint }
makeToast={ makeToast }
toggleHelpChat={ toggleHelpChat }
updateHint={ updateHint }
/>
<Output output={ output }/>

View File

@ -13,7 +13,8 @@ export default class ToolPanel extends PureComponent {
static propTypes = {
executeChallenge: PropTypes.func,
updateHint: PropTypes.func,
hint: PropTypes.string
hint: PropTypes.string,
toggleHelpChat: PropTypes.func
};
makeHint() {
@ -50,7 +51,11 @@ export default class ToolPanel extends PureComponent {
}
render() {
const { hint, executeChallenge } = this.props;
const {
hint,
executeChallenge,
toggleHelpChat
} = this.props;
return (
<div>
{ this.renderHint(hint, this.makeHint) }
@ -79,6 +84,7 @@ export default class ToolPanel extends PureComponent {
bsSize='large'
bsStyle='primary'
componentClass='label'
onClick={ toggleHelpChat }
>
Help
</Button>

View File

@ -15,7 +15,12 @@ import {
simpleProject,
frontEndProject
} from '../../../../utils/challengeTypes';
import { toggleHelpChat } from '../../../../redux/actions';
const bindableActions = {
submitChallenge,
toggleHelpChat
};
const mapStateToProps = createSelector(
challengeSelector,
state => state.app.isSignedIn,
@ -37,7 +42,8 @@ export class ToolPanel extends PureComponent {
isSignedIn: PropTypes.bool,
isSimple: PropTypes.bool,
isFrontEnd: PropTypes.bool,
isSubmitting: PropTypes.bool
isSubmitting: PropTypes.bool,
toggleHelpChat: PropTypes.func
};
renderSubmitButton(isSignedIn, submitChallenge) {
@ -62,7 +68,8 @@ export class ToolPanel extends PureComponent {
isSimple,
isSignedIn,
isSubmitting,
submitChallenge
submitChallenge,
toggleHelpChat
} = this.props;
const FormElement = isFrontEnd ? FrontEndForm : BackEndForm;
@ -79,6 +86,7 @@ export class ToolPanel extends PureComponent {
bsStyle='primary'
className='btn-primary-ghost btn-big'
componentClass='div'
onClick={ toggleHelpChat }
>
Help
</Button>
@ -97,5 +105,5 @@ export class ToolPanel extends PureComponent {
export default connect(
mapStateToProps,
{ submitChallenge }
bindableActions
)(ToolPanel);

View File

@ -42,6 +42,7 @@ const initialUiState = {
const initialState = {
id: '',
challenge: '',
helpChatRoom: 'Help',
// old code storage key
legacyKey: '',
files: {},
@ -68,6 +69,7 @@ const mainReducer = handleActions(
challenge: challenge.dashedName,
key: getFileKey(challenge),
tests: createTests(challenge),
helpChatRoom: challenge.helpRoom || 'Help',
numOfHints: Array.isArray(challenge.hints) ? challenge.hints.length : 0
}),
[types.updateTests]: (state, { payload: tests }) => ({