-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for groups in Happychat #13520
Changes from 9 commits
5cc85a4
509556a
9ddead0
4d936b4
2ce78e6
f3ce0a9
8646dec
a126130
27face4
a74606f
9b87b9f
e74145f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/** | ||
* Internal dependencies | ||
*/ | ||
import { | ||
HAPPYCHAT_GROUP_WPCOM, | ||
HAPPYCHAT_GROUP_JPOP | ||
} from 'state/happychat/constants'; | ||
import { isJetpackSite } from 'state/sites/selectors'; | ||
|
||
/** | ||
* Get happychat group for site id | ||
* For now if the site is jetpack jpop will be set as the group otherwise WP.COM | ||
* @param {object} state Current state | ||
* @param {int} siteId The site id, if no siteId is present primary siteId will be used | ||
* @returns {string} The happychat group for the selected site | ||
*/ | ||
export const getGroupForSiteId = ( state, siteId ) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need this function? It's not imported to any other modules and it seems like it does the same thing as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, initially I thought it would make sense to separate them but for now there is no added value so I will just merge them into one. |
||
switch ( true ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mind using a standard There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure :) |
||
case isJetpackSite( state, siteId ): | ||
return HAPPYCHAT_GROUP_JPOP; | ||
|
||
default: | ||
return HAPPYCHAT_GROUP_WPCOM; | ||
} | ||
}; | ||
|
||
/** | ||
* Grab the group or groups for happychat based on siteId | ||
* @param {object} state Current state | ||
* @param {int} siteId The site id, if no siteId is present primary siteId will be used | ||
* @returns {array} of groups for site Id | ||
*/ | ||
export const getGroups = ( state, siteId ) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function has the look and feel of a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense, I think in the first implementation it did not use the isJetPack selector so it was more like a helper method not an actual selector. Moved. |
||
return [ getGroupForSiteId( state, siteId ) ]; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { expect } from 'chai'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { | ||
HAPPYCHAT_GROUP_WPCOM, | ||
HAPPYCHAT_GROUP_JPOP | ||
} from 'state/happychat/constants'; | ||
import { getGroups } from 'lib/happychat'; | ||
|
||
describe( 'groups', () => { | ||
describe( '#getGroups()', () => { | ||
it( 'should return default group for no sites', () => { | ||
const siteId = 1; | ||
const state = { | ||
sites: { | ||
items: {} | ||
} | ||
}; | ||
|
||
expect( getGroups( state, siteId ) ).to.eql( [ HAPPYCHAT_GROUP_WPCOM ] ); | ||
} ); | ||
|
||
it( 'should return default group for no siteId', () => { | ||
const siteId = undefined; | ||
const state = { | ||
sites: { | ||
items: { | ||
1: {} | ||
} | ||
} | ||
}; | ||
|
||
expect( getGroups( state, siteId ) ).to.eql( [ HAPPYCHAT_GROUP_WPCOM ] ); | ||
} ); | ||
|
||
it( 'should return JPOP group for jetpack site', () => { | ||
const siteId = 1; | ||
const state = { | ||
sites: { | ||
items: { | ||
[ siteId ]: { jetpack: true } | ||
} | ||
} | ||
}; | ||
|
||
expect( getGroups( state, siteId ) ).to.eql( [ HAPPYCHAT_GROUP_JPOP ] ); | ||
} ); | ||
} ); | ||
} ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,7 +23,9 @@ import FormButton from 'components/forms/form-button'; | |
import SitesDropdown from 'components/sites-dropdown'; | ||
import siteList from 'lib/sites-list'; | ||
import ChatClosureNotice from '../chat-closure-notice'; | ||
import QuerySites from 'components/data/query-sites'; | ||
import { getSelectedSiteId } from 'state/ui/selectors'; | ||
import { selectSiteId } from 'state/help/actions'; | ||
|
||
/** | ||
* Module variables | ||
|
@@ -109,6 +111,7 @@ export const HelpContactForm = React.createClass( { | |
setSite( siteSlug ) { | ||
const site = sites.getSite( siteSlug ); | ||
this.setState( { siteId: site.ID } ); | ||
this.props.onChangeSite( site.ID ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's some state-syncing bugs that could happen since we're storing selected site both in component state and Redux state. Namely:
So I think we should probably either drop There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I totally agree, but when I worked on this I had to use QuerySites because |
||
}, | ||
|
||
trackClickStats( selectionName, selectedOption ) { | ||
|
@@ -222,6 +225,7 @@ export const HelpContactForm = React.createClass( { | |
|
||
return ( | ||
<div className="help-contact-form"> | ||
<QuerySites allSites /> | ||
<ChatClosureNotice | ||
reason="eoy-holidays" | ||
from="2016-12-24T00:00:00Z" | ||
|
@@ -279,4 +283,12 @@ const mapStateToProps = ( state ) => { | |
}; | ||
}; | ||
|
||
export default connect( mapStateToProps )( localize( HelpContactForm ) ); | ||
const mapDispatchToProps = ( dispatch ) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI there's a shorthand for straightforward dispatch maps like this:
The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool, thanks. |
||
return { | ||
onChangeSite( siteId ) { | ||
dispatch( selectSiteId( siteId ) ); | ||
} | ||
}; | ||
}; | ||
|
||
export default connect( mapStateToProps, mapDispatchToProps )( localize( HelpContactForm ) ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -178,6 +178,7 @@ export const HAPPYCHAT_SET_MESSAGE = 'HAPPYCHAT_SET_MESSAGE'; | |
export const HAPPYCHAT_TRANSCRIPT_RECEIVE = 'HAPPYCHAT_TRANSCRIPT_RECEIVE'; | ||
export const HAPPYCHAT_TRANSCRIPT_REQUEST = 'HAPPYCHAT_TRANSCRIPT_REQUEST'; | ||
export const HELP_COURSES_RECEIVE = 'HELP_COURSES_RECEIVE'; | ||
export const HELP_SELECTED_SITE = 'HELP_SELECTED_SITE'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer this action type to be more verbose like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pinging again on this — I still think a more action-y name like ending with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Totally missed this, your suggestion makes more sense, thanks |
||
export const HELP_TICKET_CONFIGURATION_DISMISS_ERROR = 'HELP_TICKET_CONFIGURATION_DISMISS_ERROR'; | ||
export const HELP_TICKET_CONFIGURATION_REQUEST = 'HELP_TICKET_CONFIGURATION_REQUEST'; | ||
export const HELP_TICKET_CONFIGURATION_REQUEST_FAILURE = 'HELP_TICKET_CONFIGURATION_REQUEST_FAILURE'; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,7 @@ import { | |
HAPPYCHAT_SEND_MESSAGE, | ||
HAPPYCHAT_SET_MESSAGE, | ||
HAPPYCHAT_TRANSCRIPT_REQUEST, | ||
HELP_SELECTED_SITE, | ||
ROUTE_SET, | ||
|
||
COMMENTS_CHANGE_STATUS_SUCESS, | ||
|
@@ -62,6 +63,7 @@ import { | |
getCurrentUser, | ||
getCurrentUserLocale, | ||
} from 'state/current-user/selectors'; | ||
import { getGroups } from 'lib/happychat'; | ||
|
||
const debug = require( 'debug' )( 'calypso:happychat:actions' ); | ||
|
||
|
@@ -99,6 +101,7 @@ export const connectChat = ( connection, { getState, dispatch } ) => { | |
|
||
const user = getCurrentUser( state ); | ||
const locale = getCurrentUserLocale( state ); | ||
const groups = getGroups( state ); | ||
|
||
// Notify that a new connection is being established | ||
dispatch( setConnecting() ); | ||
|
@@ -129,10 +132,19 @@ export const connectChat = ( connection, { getState, dispatch } ) => { | |
|
||
return sign( { user, session_id } ); | ||
} ) | ||
.then( ( { jwt } ) => connection.open( user.ID, jwt, locale ) ) | ||
.then( ( { jwt } ) => connection.open( user.ID, jwt, locale, groups ) ) | ||
.catch( e => debug( 'failed to start happychat session', e, e.stack ) ); | ||
}; | ||
|
||
|
||
export const updateChatPreferences = ( connection, { getState }, siteId ) => { | ||
const state = getState(); | ||
const locale = getCurrentUserLocale( state ); | ||
const groups = getGroups( state, siteId ); | ||
|
||
connection.preferences( locale, groups ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should check first if there's an open connection established. When there isn't, this error can occur: This is because the We should figure out a long-term solution for all functions, but it's especially problematic in this PR because the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice catch. I guess we could use the |
||
}; | ||
|
||
export const requestTranscript = ( connection, { dispatch } ) => { | ||
debug( 'requesting current session transcript' ); | ||
|
||
|
@@ -309,6 +321,10 @@ export default function( connection = null ) { | |
case HAPPYCHAT_INITIALIZE: | ||
connectIfRecentlyActive( connection, store ); | ||
break; | ||
|
||
case HELP_SELECTED_SITE: | ||
updateChatPreferences( connection, store, action.siteId ); | ||
break; | ||
|
||
case HAPPYCHAT_SEND_USER_INFO: | ||
sendInfo( connection, store, action.siteUrl ); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/** | ||
* Internal dependencies | ||
*/ | ||
import { HELP_SELECTED_SITE } from 'state/action-types'; | ||
|
||
export const selectSiteId = ( siteId ) => ( { | ||
type: HELP_SELECTED_SITE, siteId | ||
} ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,44 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import isEmpty from 'lodash/isEmpty'; | ||
import { combineReducers } from 'redux'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm guessing this is a regression since this PR has been sitting so long, but There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed, updated. |
||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { | ||
HELP_SELECTED_SITE, | ||
SITES_RECEIVE, | ||
} from 'state/action-types'; | ||
import courses from './courses/reducer'; | ||
import { combineReducers } from 'state/utils'; | ||
import directly from './directly/reducer'; | ||
import ticket from './ticket/reducer'; | ||
import { createReducer } from 'state/utils'; | ||
|
||
const selectFirstSite = ( sites ) => { | ||
if ( ! isEmpty( sites ) ) { | ||
return sites[ 0 ].ID; | ||
} | ||
return null; | ||
}; | ||
|
||
/** | ||
* Tracks the site id for the selected site in the help/contact form | ||
* | ||
* @param {Object} state Current state | ||
* @param {Object} action Action payload | ||
* @return {Object} Updated state | ||
*/ | ||
export const selectedSiteId = createReducer( null, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This state property is being created but it's never actually used (the Happychat middleware just grabs the site ID from the action itself). Are you sure we need to keep this data in Redux? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I went with using full redux so this is used now. |
||
[ HELP_SELECTED_SITE ]: ( state, action ) => action.siteId, | ||
[ SITES_RECEIVE ]: ( state, action ) => selectFirstSite( action.sites ), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we keep this state property — are you sure this is what we want? Seems like it's possible this value will change under our feet from unrelated UI requesting site data. Maybe we should only grab the first site from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is correct, that was part of my initial hacky approach because sites-list was not fully deprecated. This is not the case anymore I removed the |
||
} ); | ||
|
||
export default combineReducers( { | ||
courses, | ||
directly, | ||
ticket, | ||
selectedSiteId, | ||
} ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/** | ||
* Internal dependencies | ||
*/ | ||
import createSelector from 'lib/create-selector'; | ||
|
||
export const getHelpSelectedSiteId = createSelector( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This selector is also unused and seems expendable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moving to redux flow makes this indispensable. |
||
state => state.help.selectedSiteId | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { expect } from 'chai'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { | ||
HELP_SELECTED_SITE | ||
} from 'state/action-types'; | ||
import { | ||
selectSiteId, | ||
} from '../actions'; | ||
|
||
describe( 'actions', () => { | ||
describe( '#selectSiteId()', () => { | ||
it( 'should return an action object', () => { | ||
const action = selectSiteId( 1 ); | ||
|
||
expect( action ).to.eql( { | ||
type: HELP_SELECTED_SITE, | ||
siteId: 1, | ||
} ); | ||
} ); | ||
} ); | ||
} ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setPreferences
seems more explicit to me — what do you think?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely ;)