Skip to content
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

Question being answered notification #94

Merged
merged 10 commits into from
Apr 17, 2018
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ with the current date and the next changes should go under a **[Next]** header.

* Fix course shortcode link to work in browsers besides Chrome. ([@nwalters512](https://github.com/nwalters512) in [#101](https://github.com/illinois/queue/pull/101))
* Custom error page with consistent header, footer, and button to navigate to homepage. ([@JParisFerrer](https://github.com/jparisferrer) in [#103](https://github.com/illinois/queue/pull/103))
* Add notifications for students when their question is being answered. ([@zwang180](https://github.com/zwang180) in [#94](https://github.com/illinois/queue/pull/94))

## 10 April 2018

Expand Down
53 changes: 39 additions & 14 deletions components/QuestionNotificationsToggle.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/* eslint-env browser */
import React from 'react'
import { Button } from 'reactstrap'

import React, { Fragment } from 'react'
import { Button, ButtonGroup } from 'reactstrap'
import FontAwesomeIcon from '@fortawesome/react-fontawesome'
import faBell from '@fortawesome/fontawesome-free-solid/faBell'
import faInfo from '@fortawesome/fontawesome-free-solid/faInfoCircle'

import QuestionNotificationsToggleExplanationModal from './QuestionNotificationsToggleExplanationModal'

class QuestionNotificationsToggle extends React.Component {
constructor(props) {
Expand All @@ -23,6 +25,7 @@ class QuestionNotificationsToggle extends React.Component {
supported,
enabled,
permission,
showExplanationModal: false,
}

// Sync notification setting between tabs
Expand All @@ -42,6 +45,12 @@ class QuestionNotificationsToggle extends React.Component {
this.setState({ enabled })
}

toggleExplanationModal() {
this.setState({
showExplanationModal: !this.state.showExplanationModal,
})
}

toggleNotificationsEnabled() {
const hasBeenGranted =
Notification.permission === 'denied' ||
Expand Down Expand Up @@ -92,17 +101,33 @@ class QuestionNotificationsToggle extends React.Component {
}

return (
<Button
color={color}
block
disabled={disabled}
className="mb-3 d-flex flex-row justify-content-center align-items-center"
style={{ whiteSpace: 'normal' }}
onClick={() => this.toggleNotificationsEnabled()}
>
<FontAwesomeIcon icon={faBell} className="mr-3" />
<span>{text}</span>
</Button>
<Fragment>
<ButtonGroup className="mb-3 d-block d-flex flex-row justify-content-center align-items-center">
<Button
color={color}
disabled={disabled}
id="notificationButton"
className="d-flex flex-row justify-content-center align-items-center"
style={{ whiteSpace: 'normal', flex: 1 }}
onClick={() => this.toggleNotificationsEnabled()}
>
<FontAwesomeIcon icon={faBell} className="mr-3" />
<span>{text}</span>
</Button>
<Button
color={color}
outline
className="d-flex align-self-stretch"
onClick={() => this.toggleExplanationModal()}
>
<FontAwesomeIcon icon={faInfo} />
</Button>
</ButtonGroup>
<QuestionNotificationsToggleExplanationModal
isOpen={this.state.showExplanationModal}
toggle={() => this.toggleExplanationModal()}
/>
</Fragment>
)
}
}
Expand Down
33 changes: 33 additions & 0 deletions components/QuestionNotificationsToggleExplanationModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'

const QuestionNotificationsToggleExplanationModal = props => (
<Modal isOpen={props.isOpen} toggle={props.toggle}>
<ModalHeader>Question notifications</ModalHeader>
<ModalBody>
<h5>For students</h5>
<p>
If you enable notifications, you&apos;ll get notifications when your
question is being answered.
</p>
<h5>For on-duty course staff/admins</h5>
<p>
If you enable notifications, you&apos;ll get notifications when a
student adds a new question to the queue.
</p>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={props.toggle}>
Got it
</Button>
</ModalFooter>
</Modal>
)

QuestionNotificationsToggleExplanationModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
}

export default QuestionNotificationsToggleExplanationModal
5 changes: 1 addition & 4 deletions pages/queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import Layout from '../components/Layout'
import StaffSidebar from '../components/StaffSidebar'
import QuestionPanelContainer from '../containers/QuestionPanelContainer'
import QuestionListContainer from '../containers/QuestionListContainer'
import ShowForCourseStaff from '../components/ShowForCourseStaff'
import QuestionNotificationsToggle from '../components/QuestionNotificationsToggle'

class Queue extends React.Component {
Expand Down Expand Up @@ -72,9 +71,7 @@ class Queue extends React.Component {
lg={{ size: 3 }}
className="mb-3 mb-md-0"
>
<ShowForCourseStaff queueId={this.props.queueId}>
<QuestionNotificationsToggle />
</ShowForCourseStaff>
<QuestionNotificationsToggle />
<StaffSidebar queueId={this.props.queueId} />
</Col>
<Col xs={{ size: 12 }} md={{ size: 8 }} lg={{ size: 9 }}>
Expand Down
75 changes: 53 additions & 22 deletions redux/questionNotificationMiddleware.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,64 @@
/* eslint-env browser */
import { CREATE_QUESTION } from '../constants/ActionTypes'
import { CREATE_QUESTION, UPDATE_QUESTION } from '../constants/ActionTypes'
import { baseUrl } from '../util'

function sendNotificationIfEnabled(title, options) {
if (
typeof window !== 'undefined' &&
window.localStorage &&
window.localStorage.getItem('notificationsEnabled') === 'true'
) {
if (Notification.permission === 'granted') {
const n = new Notification(title, options)
n.onclick = () => {
window.focus()
n.close()
}
}
}
}

function isOnDutyStaff(activeStaff, user) {
return Object.keys(activeStaff).some(
key => activeStaff[key].userId === user.id
)
}

export default store => next => action => {
const state = store.getState()
const { user } = state.user

// Notification for new questions added to on-duty staffs
if (action.type === CREATE_QUESTION.SUCCESS) {
const state = store.getState()
// We obviously want to avoid notifying ourselves
if (state.user.user.id !== action.question.askedById) {
if (
typeof window !== 'undefined' &&
window.localStorage &&
window.localStorage.getItem('notificationsEnabled') === 'true'
) {
if (Notification.permission === 'granted') {
const { name, location } = action.question
const { activeStaff } = state.activeStaff
// On duty staff cannot ask questions, so no need to filter by askedById
if (isOnDutyStaff(activeStaff, user)) {
const title = 'New question'
const { name, location } = action.question

const options = {
body: `Name: ${name}\nLocation: ${location}`,
icon: `${baseUrl}/static/notif_icon.png`,
}
const n = new Notification('New question', options)
n.onclick = () => {
window.focus()
n.close()
}
}
const options = {
body: `Name: ${name}\nLocation: ${location}`,
icon: `${baseUrl}/static/notif_icon.png`,
}
sendNotificationIfEnabled(title, options)
}
}
// Notification for question being answered by staff to student
// Note: Only UPDATE_QUESTION action will happen on student's side, but not UPDATE_QUESTION_ANSWERING
} else if (action.type === UPDATE_QUESTION.SUCCESS) {
const { question } = action
// If question is marked as being answered and it is the student who ask this question
const markedBeingAnswered =
!state.questions.questions[question.id].beingAnswered &&
question.beingAnswered
if (markedBeingAnswered && user.id === question.askedById) {
const name = question.answeredBy.name || question.answeredBy.netid
const title = `${name} is answering your question`

const options = {
icon: `${baseUrl}/static/notif_icon.png`,
}
sendNotificationIfEnabled(title, options)
}
}
return next(action)
}
6 changes: 6 additions & 0 deletions selectors/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,9 @@ export const isUserAnsweringQuestionForQueue = createSelector(
)
}
)

export const isUserStudent = createSelector(
[isUserAdmin, isUserCourseStaff, isUserCourseStaffForQueue],
(isAdmin, isCourseStaff, isCourseStaffForQueue) =>
!(isAdmin || isCourseStaff || isCourseStaffForQueue)
)