Feature(chat): Add help chat logic
This commit is contained in:
@ -1,3 +1,4 @@
|
|||||||
|
.chat-embed-help-title,
|
||||||
.chat-embed-main-title {
|
.chat-embed-main-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@ -1,20 +1,34 @@
|
|||||||
import { Observable } from 'rx';
|
import { Subject, Observable } from 'rx';
|
||||||
import Chat from 'gitter-sidecar';
|
import Chat from 'gitter-sidecar';
|
||||||
import types from '../../common/app/redux/types';
|
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 div = document.createElement('div');
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
const actionBar = document.querySelector(
|
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'));
|
span.appendChild(document.createTextNode(title));
|
||||||
div.className = 'chat-embed-main-title';
|
div.className = `chat-embed-${type}-title`;
|
||||||
div.appendChild(span);
|
div.appendChild(span);
|
||||||
actionBar.insertBefore(div, actionBar.firstChild);
|
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;
|
let mainChatTitleAdded = false;
|
||||||
const mainChatContainer = document.createElement('aside');
|
const mainChatContainer = document.createElement('aside');
|
||||||
mainChatContainer.id = 'chat-embed-main';
|
mainChatContainer.id = 'chat-embed-main';
|
||||||
@ -26,7 +40,7 @@ export default function gitterSaga(actions$, getState, { document }) {
|
|||||||
targetElement: mainChatContainer
|
targetElement: mainChatContainer
|
||||||
});
|
});
|
||||||
|
|
||||||
const mainChatToggle$ = Observable.fromEventPattern(
|
const toggle$ = Observable.fromEventPattern(
|
||||||
h => mainChatContainer.addEventListener('gitter-chat-toggle', h),
|
h => mainChatContainer.addEventListener('gitter-chat-toggle', h),
|
||||||
h => mainChatContainer.removeEventListener('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;
|
const { isMainChatOpen } = getState().app;
|
||||||
if (!mainChatTitleAdded) {
|
if (!mainChatTitleAdded) {
|
||||||
mainChatTitleAdded = true;
|
mainChatTitleAdded = true;
|
||||||
createHeader(document);
|
createHeader('freecodecamp', 'Free Code Camp\'s Main Chat', document);
|
||||||
}
|
}
|
||||||
if (isMainChatOpen === e.detail.state) {
|
if (isMainChatOpen === e.detail.state) {
|
||||||
return null;
|
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(
|
return Observable.merge(
|
||||||
mainChatToggle$,
|
mainChatToggle$,
|
||||||
|
helpToggleProxy,
|
||||||
actions$
|
actions$
|
||||||
.filter(({ type }) => (
|
.filter(({ type }) => (
|
||||||
type === types.openMainChat ||
|
type === types.openMainChat ||
|
||||||
type === types.closeMainChat ||
|
type === types.closeMainChat ||
|
||||||
type === types.toggleMainChat
|
type === types.toggleMainChat ||
|
||||||
|
type === types.toggleHelpChat
|
||||||
))
|
))
|
||||||
.map(() => {
|
.map(({ type }) => {
|
||||||
const { isMainChatOpen } = getState().app;
|
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);
|
mainChat.toggleChat(isMainChatOpen);
|
||||||
if (!isMainChatOpen) {
|
shouldBlur = !isMainChatOpen;
|
||||||
|
if (!shouldBlur) {
|
||||||
document.activeElement.blur();
|
document.activeElement.blur();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -54,8 +54,9 @@ export const createErrorObservable = error => Observable.just({
|
|||||||
|
|
||||||
// drawers
|
// drawers
|
||||||
export const toggleMapDrawer = createAction(types.toggleMapDrawer);
|
export const toggleMapDrawer = createAction(types.toggleMapDrawer);
|
||||||
export const toggleWikiDrawer = createAction(types.toggleWikiDrawer);
|
|
||||||
|
|
||||||
// chat
|
|
||||||
export const toggleMainChat = createAction(types.toggleMainChat);
|
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);
|
export const toggleNightMode = createAction(types.toggleNightMode);
|
||||||
|
@ -9,7 +9,8 @@ const initialState = {
|
|||||||
csrfToken: '',
|
csrfToken: '',
|
||||||
windowHeight: 0,
|
windowHeight: 0,
|
||||||
navHeight: 0,
|
navHeight: 0,
|
||||||
isMainChatOpen: false
|
isMainChatOpen: false,
|
||||||
|
isHelpChatOpen: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handleActions(
|
export default handleActions(
|
||||||
@ -50,6 +51,18 @@ export default handleActions(
|
|||||||
...state,
|
...state,
|
||||||
isMainChatOpen: !state.isMainChatOpen
|
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 }) => ({
|
[types.delayedRedirect]: (state, { payload }) => ({
|
||||||
...state,
|
...state,
|
||||||
delayedRedirect: payload
|
delayedRedirect: payload
|
||||||
|
@ -29,8 +29,12 @@ export default createTypes([
|
|||||||
'toggleMapDrawer',
|
'toggleMapDrawer',
|
||||||
'toggleWikiDrawer',
|
'toggleWikiDrawer',
|
||||||
|
|
||||||
// main chat
|
// chat
|
||||||
'openMainChat',
|
'openMainChat',
|
||||||
'closeMainChat',
|
'closeMainChat',
|
||||||
'toggleMainChat'
|
'toggleMainChat',
|
||||||
|
|
||||||
|
'openHelpChat',
|
||||||
|
'closeHelpChat',
|
||||||
|
'toggleHelpChat'
|
||||||
], 'app');
|
], 'app');
|
||||||
|
@ -2,7 +2,6 @@ import React, { PropTypes } from 'react';
|
|||||||
import ReactDom from 'react-dom';
|
import ReactDom from 'react-dom';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import PureComponent from 'react-pure-render/component';
|
import PureComponent from 'react-pure-render/component';
|
||||||
import { Col, Row } from 'react-bootstrap';
|
import { Col, Row } from 'react-bootstrap';
|
||||||
|
|
||||||
@ -12,8 +11,14 @@ import ToolPanel from './Tool-Panel.jsx';
|
|||||||
import { challengeSelector } from '../../redux/selectors';
|
import { challengeSelector } from '../../redux/selectors';
|
||||||
import { updateHint, executeChallenge } from '../../redux/actions';
|
import { updateHint, executeChallenge } from '../../redux/actions';
|
||||||
import { makeToast } from '../../../../toasts/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(
|
const mapStateToProps = createSelector(
|
||||||
challengeSelector,
|
challengeSelector,
|
||||||
state => state.app.windowHeight,
|
state => state.app.windowHeight,
|
||||||
@ -53,7 +58,8 @@ export class SidePanel extends PureComponent {
|
|||||||
output: PropTypes.string,
|
output: PropTypes.string,
|
||||||
hints: PropTypes.string,
|
hints: PropTypes.string,
|
||||||
updateHint: PropTypes.func,
|
updateHint: PropTypes.func,
|
||||||
makeToast: PropTypes.func
|
makeToast: PropTypes.func,
|
||||||
|
toggleHelpChat: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
renderDescription(description = [ 'Happy Coding!' ], descriptionRegex) {
|
renderDescription(description = [ 'Happy Coding!' ], descriptionRegex) {
|
||||||
@ -92,7 +98,8 @@ export class SidePanel extends PureComponent {
|
|||||||
hint,
|
hint,
|
||||||
executeChallenge,
|
executeChallenge,
|
||||||
updateHint,
|
updateHint,
|
||||||
makeToast
|
makeToast,
|
||||||
|
toggleHelpChat
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const style = {
|
const style = {
|
||||||
overflowX: 'hidden',
|
overflowX: 'hidden',
|
||||||
@ -124,6 +131,7 @@ export class SidePanel extends PureComponent {
|
|||||||
executeChallenge={ executeChallenge }
|
executeChallenge={ executeChallenge }
|
||||||
hint={ hint }
|
hint={ hint }
|
||||||
makeToast={ makeToast }
|
makeToast={ makeToast }
|
||||||
|
toggleHelpChat={ toggleHelpChat }
|
||||||
updateHint={ updateHint }
|
updateHint={ updateHint }
|
||||||
/>
|
/>
|
||||||
<Output output={ output }/>
|
<Output output={ output }/>
|
||||||
|
@ -13,7 +13,8 @@ export default class ToolPanel extends PureComponent {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
executeChallenge: PropTypes.func,
|
executeChallenge: PropTypes.func,
|
||||||
updateHint: PropTypes.func,
|
updateHint: PropTypes.func,
|
||||||
hint: PropTypes.string
|
hint: PropTypes.string,
|
||||||
|
toggleHelpChat: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
makeHint() {
|
makeHint() {
|
||||||
@ -50,7 +51,11 @@ export default class ToolPanel extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { hint, executeChallenge } = this.props;
|
const {
|
||||||
|
hint,
|
||||||
|
executeChallenge,
|
||||||
|
toggleHelpChat
|
||||||
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{ this.renderHint(hint, this.makeHint) }
|
{ this.renderHint(hint, this.makeHint) }
|
||||||
@ -79,6 +84,7 @@ export default class ToolPanel extends PureComponent {
|
|||||||
bsSize='large'
|
bsSize='large'
|
||||||
bsStyle='primary'
|
bsStyle='primary'
|
||||||
componentClass='label'
|
componentClass='label'
|
||||||
|
onClick={ toggleHelpChat }
|
||||||
>
|
>
|
||||||
Help
|
Help
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -15,7 +15,12 @@ import {
|
|||||||
simpleProject,
|
simpleProject,
|
||||||
frontEndProject
|
frontEndProject
|
||||||
} from '../../../../utils/challengeTypes';
|
} from '../../../../utils/challengeTypes';
|
||||||
|
import { toggleHelpChat } from '../../../../redux/actions';
|
||||||
|
|
||||||
|
const bindableActions = {
|
||||||
|
submitChallenge,
|
||||||
|
toggleHelpChat
|
||||||
|
};
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
challengeSelector,
|
challengeSelector,
|
||||||
state => state.app.isSignedIn,
|
state => state.app.isSignedIn,
|
||||||
@ -37,7 +42,8 @@ export class ToolPanel extends PureComponent {
|
|||||||
isSignedIn: PropTypes.bool,
|
isSignedIn: PropTypes.bool,
|
||||||
isSimple: PropTypes.bool,
|
isSimple: PropTypes.bool,
|
||||||
isFrontEnd: PropTypes.bool,
|
isFrontEnd: PropTypes.bool,
|
||||||
isSubmitting: PropTypes.bool
|
isSubmitting: PropTypes.bool,
|
||||||
|
toggleHelpChat: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
renderSubmitButton(isSignedIn, submitChallenge) {
|
renderSubmitButton(isSignedIn, submitChallenge) {
|
||||||
@ -62,7 +68,8 @@ export class ToolPanel extends PureComponent {
|
|||||||
isSimple,
|
isSimple,
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
submitChallenge
|
submitChallenge,
|
||||||
|
toggleHelpChat
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const FormElement = isFrontEnd ? FrontEndForm : BackEndForm;
|
const FormElement = isFrontEnd ? FrontEndForm : BackEndForm;
|
||||||
@ -79,6 +86,7 @@ export class ToolPanel extends PureComponent {
|
|||||||
bsStyle='primary'
|
bsStyle='primary'
|
||||||
className='btn-primary-ghost btn-big'
|
className='btn-primary-ghost btn-big'
|
||||||
componentClass='div'
|
componentClass='div'
|
||||||
|
onClick={ toggleHelpChat }
|
||||||
>
|
>
|
||||||
Help
|
Help
|
||||||
</Button>
|
</Button>
|
||||||
@ -97,5 +105,5 @@ export class ToolPanel extends PureComponent {
|
|||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
{ submitChallenge }
|
bindableActions
|
||||||
)(ToolPanel);
|
)(ToolPanel);
|
||||||
|
@ -42,6 +42,7 @@ const initialUiState = {
|
|||||||
const initialState = {
|
const initialState = {
|
||||||
id: '',
|
id: '',
|
||||||
challenge: '',
|
challenge: '',
|
||||||
|
helpChatRoom: 'Help',
|
||||||
// old code storage key
|
// old code storage key
|
||||||
legacyKey: '',
|
legacyKey: '',
|
||||||
files: {},
|
files: {},
|
||||||
@ -68,6 +69,7 @@ const mainReducer = handleActions(
|
|||||||
challenge: challenge.dashedName,
|
challenge: challenge.dashedName,
|
||||||
key: getFileKey(challenge),
|
key: getFileKey(challenge),
|
||||||
tests: createTests(challenge),
|
tests: createTests(challenge),
|
||||||
|
helpChatRoom: challenge.helpRoom || 'Help',
|
||||||
numOfHints: Array.isArray(challenge.hints) ? challenge.hints.length : 0
|
numOfHints: Array.isArray(challenge.hints) ? challenge.hints.length : 0
|
||||||
}),
|
}),
|
||||||
[types.updateTests]: (state, { payload: tests }) => ({
|
[types.updateTests]: (state, { payload: tests }) => ({
|
||||||
|
Reference in New Issue
Block a user