Feature(chat): Add help chat logic
This commit is contained in:
@ -1,3 +1,4 @@
|
||||
.chat-embed-help-title,
|
||||
.chat-embed-main-title {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -29,8 +29,12 @@ export default createTypes([
|
||||
'toggleMapDrawer',
|
||||
'toggleWikiDrawer',
|
||||
|
||||
// main chat
|
||||
// chat
|
||||
'openMainChat',
|
||||
'closeMainChat',
|
||||
'toggleMainChat'
|
||||
'toggleMainChat',
|
||||
|
||||
'openHelpChat',
|
||||
'closeHelpChat',
|
||||
'toggleHelpChat'
|
||||
], 'app');
|
||||
|
@ -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 }/>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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 }) => ({
|
||||
|
Reference in New Issue
Block a user