Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 41 additions & 18 deletions src/components/NotificationsDropdown/NotificationsDropdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,48 @@ import EnhancedDropdown from './EnhancedDropdown'
import NotificationsBell from './NotificationsBell'


const NotificationsDropdown = (props) => {
return (
<div className="notifications-dropdown">
<EnhancedDropdown theme="UserDropdownMenu" pointerShadow noAutoclose onToggle={props.onToggle}>
<div className="dropdown-menu-header">
<NotificationsBell
hasUnread={props.hasUnread}
hasNew={props.hasNew}
onClick={props.onToggle}
/>
</div>
<div className="dropdown-menu-list">
<div className="notifications-dropdown-content">
{props.children}
class NotificationsDropdown extends React.Component {

constructor(props) {
super(props)
this.state = {
isOpen: false
}

this.toggle = this.toggle.bind(this)
}

toggle(isOpen) {
if (typeof isOpen === 'object') {
this.props.onToggle(!this.state.isOpen)
this.setState({ isOpen: !this.state.isOpen})
} else {
this.props.onToggle(isOpen)
this.setState({ isOpen })
}
}

render() {
const { hasUnread, hasNew, children } = this.props
return (
<div className="notifications-dropdown">
<EnhancedDropdown theme="UserDropdownMenu" pointerShadow noAutoclose onToggle={this.toggle}>
<div className="dropdown-menu-header">
<NotificationsBell
hasUnread={hasUnread}
hasNew={hasNew}
onClick={this.toggle}
/>
</div>
<div className="dropdown-menu-list">
<div className="notifications-dropdown-content">
{children}
</div>
</div>
</div>
</EnhancedDropdown>
</div>
)
</EnhancedDropdown>
</div>
)
}
}

NotificationsDropdown.propTypes = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import { Link } from 'react-router-dom'
import { connect } from 'react-redux'
import _ from 'lodash'
import { TransitionGroup, Transition } from 'react-transition-group'
import { getNotifications, toggleNotificationSeen, markAllNotificationsRead, toggleNotificationRead, visitNotifications,
import { getNotifications, toggleNotificationSeen, markAllNotificationsRead, markAllNotificationsSeen, toggleNotificationRead,
toggleBundledNotificationRead, viewOlderNotifications, hideOlderNotifications } from '../../routes/notifications/actions'
import {
splitNotificationsBySources,
filterReadNotifications,
filterSeenNotifications,
limitQuantityInSources,
preRenderNotifications,
} from '../../routes/notifications/helpers/notifications'
Expand All @@ -29,9 +30,9 @@ import { NOTIFICATIONS_DROPDOWN_PER_SOURCE, NOTIFICATIONS_NEW_PER_SOURCE, REFRES
import './NotificationsDropdown.scss'

const NotificationsDropdownContainerView = (props) => {
const {initialized, isLoading, lastVisited, sources, notifications, markAllNotificationsRead, toggleNotificationRead, toggleNotificationSeen,
pending, toggleBundledNotificationRead, visitNotifications, oldSourceIds, viewOlderNotifications, isDropdownMobileOpen, isDropdownWebOpen,
toggleNotificationsDropdownMobile, toggleNotificationsDropdownWeb } = props
const {initialized, isLoading, sources, notifications, markAllNotificationsRead, toggleNotificationRead, toggleNotificationSeen,
pending, toggleBundledNotificationRead, oldSourceIds, viewOlderNotifications, isDropdownMobileOpen, isDropdownWebOpen,
toggleNotificationsDropdownMobile, toggleNotificationsDropdownWeb, markAllNotificationsSeen } = props
if (!initialized && isLoading) {
return (
<NotificationsDropdown hasUnread={false}>
Expand All @@ -51,9 +52,11 @@ const NotificationsDropdownContainerView = (props) => {
}

const notReadNotifications = filterReadNotifications(notifications)
const notSeenNotifications = filterSeenNotifications(notifications)
const allNotificationsBySources = splitNotificationsBySources(sources, notReadNotifications)

const hasUnread = notReadNotifications.length > 0
const hasUnseen = notSeenNotifications.length > 0
// we have to give Dropdown component some time
// before removing notification item node from the list
// otherwise dropdown thinks we clicked outside and closes dropdown
Expand All @@ -70,7 +73,7 @@ const NotificationsDropdownContainerView = (props) => {
}, 0)
}
}
const hasNew = hasUnread && lastVisited < _.maxBy(_.map(notifications, n => new Date(n.date)))

let notificationsEmpty = (
<NotificationsEmpty>
<p className="notifications-empty-note">
Expand All @@ -92,6 +95,12 @@ const NotificationsDropdownContainerView = (props) => {
)
}

const markNotificationsSeen = (isOpen) => {
if (isOpen) {
markAllNotificationsSeen(null, notifications)
}
}

// this function checks that notification is not seen yet,
// before marking it as seen
const markNotificationSeen = (notificationId) => {
Expand All @@ -118,10 +127,10 @@ const NotificationsDropdownContainerView = (props) => {
return (
<NotificationsDropdown
hasUnread={hasUnread}
hasNew={hasNew}
hasNew={hasUnseen}
onToggle={(isOpen) => {
toggleNotificationsDropdownWeb(isOpen)
visitNotifications()
markNotificationsSeen(isOpen)
}}
>
{isDropdownWebOpen && <div>
Expand Down Expand Up @@ -192,10 +201,9 @@ const NotificationsDropdownContainerView = (props) => {
return (
<NotificationsMobilePage
hasUnread={hasUnread}
hasNew={hasNew}
hasNew={hasUnseen}
onToggle={() => {
toggleNotificationsDropdownMobile()
visitNotifications()
}}
isOpen={isDropdownMobileOpen}
>
Expand Down Expand Up @@ -253,21 +261,18 @@ class NotificationsDropdownContainer extends React.Component {
constructor(props) {
super(props)
this.state = {
lastVisited: new Date(0),
isDropdownWebOpen: false,
isDropdownMobileOpen: false,
notificationsVisited: false,
}

this.onToggleNotificationsDropdownWeb = this.onToggleNotificationsDropdownWeb.bind(this)
this.onToggleNotificationsDropdownMobile = this.onToggleNotificationsDropdownMobile.bind(this)
this.onVisitNotifications = this.onVisitNotifications.bind(this)
}

componentDidMount() {
this.props.getNotifications()
this.autoRefreshNotifications = setInterval(() => this.props.getNotifications(), REFRESH_NOTIFICATIONS_INTERVAL)
this.setState({ lastVisited: this.props.lastVisited })
}

componentWillUnmount() {
Expand All @@ -276,7 +281,6 @@ class NotificationsDropdownContainer extends React.Component {
this.onToggleNotificationsDropdownMobile(false)
this.onToggleNotificationsDropdownWeb(false)
this.props.hideOlderNotifications()
this.state.notificationsVisited && this.props.visitNotifications()
}

componentWillReceiveProps(nextProps) {
Expand All @@ -300,25 +304,15 @@ class NotificationsDropdownContainer extends React.Component {
this.setState({ isDropdownMobileOpen: !_.isUndefined(isOpen) ? isOpen : !this.state.isDropdownMobileOpen})
}

onVisitNotifications() {
this.setState({
lastVisited: _.maxBy(_.map(this.props.notifications, n => new Date(n.date))),
notificationsVisited: true
})
}

render() {
const { notifications, ...restProps } = this.props
const preRenderedNotifications = preRenderNotifications(notifications)

return (
<NotificationsDropdownContainerView
{...restProps}
notifications={preRenderedNotifications}
toggleNotificationsDropdownWeb={this.onToggleNotificationsDropdownWeb}
toggleNotificationsDropdownMobile={this.onToggleNotificationsDropdownMobile}
visitNotifications={this.onVisitNotifications}
lastVisited={this.state.lastVisited}
isDropdownMobileOpen={this.state.isDropdownMobileOpen}
isDropdownWebOpen={this.state.isDropdownWebOpen}
/>
Expand All @@ -331,9 +325,9 @@ const mapStateToProps = ({ notifications }) => notifications

const mapDispatchToProps = {
getNotifications,
visitNotifications,
toggleNotificationSeen,
markAllNotificationsRead,
markAllNotificationsSeen,
toggleNotificationRead,
toggleBundledNotificationRead,
viewOlderNotifications,
Expand Down
2 changes: 1 addition & 1 deletion src/config/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ export const LOAD_ORG_CONFIG_FAILURE = 'LOAD_ORG_CONFIG_FAILURE'
export const GET_NOTIFICATIONS_PENDING = 'GET_NOTIFICATIONS_PENDING'
export const GET_NOTIFICATIONS_SUCCESS = 'GET_NOTIFICATIONS_SUCCESS'
export const GET_NOTIFICATIONS_FAILURE = 'GET_NOTIFICATIONS_FAILURE'
export const VISIT_NOTIFICATIONS = 'VISIT_NOTIFICATIONS'
export const SET_NOTIFICATIONS_FILTER_BY = 'SET_NOTIFICATIONS_FILTER_BY'
export const MARK_ALL_NOTIFICATIONS_READ = 'MARK_ALL_NOTIFICATIONS_READ'
export const TOGGLE_NOTIFICATION_READ = 'TOGGLE_NOTIFICATION_READ'
export const TOGGLE_NOTIFICATION_SEEN = 'TOGGLE_NOTIFICATION_SEEN'
export const MARK_ALL_NOTIFICATIONS_SEEN = 'MARK_ALL_NOTIFICATIONS_SEEN'
export const VIEW_OLDER_NOTIFICATIONS_SUCCESS = 'VIEW_OLDER_NOTIFICATIONS_SUCCESS'
export const HIDE_OLDER_NOTIFICATIONS_SUCCESS = 'HIDE_OLDER_NOTIFICATIONS_SUCCESS'
export const NOTIFICATIONS_PENDING = 'NOTIFICATIONS_PENDING'
Expand Down
38 changes: 30 additions & 8 deletions src/routes/notifications/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import {
GET_NOTIFICATIONS_PENDING,
GET_NOTIFICATIONS_SUCCESS,
GET_NOTIFICATIONS_FAILURE,
VISIT_NOTIFICATIONS,
TOGGLE_NOTIFICATION_SEEN,
SET_NOTIFICATIONS_FILTER_BY,
MARK_ALL_NOTIFICATIONS_READ,
MARK_ALL_NOTIFICATIONS_SEEN,
MARK_NOTIFICATIONS_READ,
TOGGLE_NOTIFICATION_READ,
VIEW_OLDER_NOTIFICATIONS_SUCCESS,
Expand All @@ -31,10 +31,18 @@ const handleDispatchNotificationReadByType = (type, dispatch, payload, isRead) =
})
}

const handleDispatchNotificationSeenByType = (type, dispatch, payload, isSeen) => {
dispatch({
type,
payload,
isSeen
})
}

const handleDispatchNotificationRead = handleDispatchNotificationReadByType.bind(this, TOGGLE_NOTIFICATION_READ)
const handleDispatchMarkAllNotificationsRead = handleDispatchNotificationReadByType.bind(this, MARK_ALL_NOTIFICATIONS_READ)
const handleDispatchMarkNotificationsRead = handleDispatchNotificationReadByType.bind(this, MARK_NOTIFICATIONS_READ)

const handleDispatchMarkAllNotificationsSeen = handleDispatchNotificationSeenByType.bind(this, MARK_ALL_NOTIFICATIONS_SEEN)
export const getNotifications = () => (dispatch) => {
dispatch({ type: GET_NOTIFICATIONS_PENDING })
notificationsService.getNotifications().then(notifications => {
Expand All @@ -51,17 +59,31 @@ export const getNotifications = () => (dispatch) => {
})
}

export const visitNotifications = () => (dispatch) => {
dispatch({
type: VISIT_NOTIFICATIONS
})
}

export const setNotificationsFilterBy = (filterBy) => (dispatch) => dispatch({
type: SET_NOTIFICATIONS_FILTER_BY,
payload: filterBy
})

export const markAllNotificationsSeen = (sourceId, notifications = []) => (dispatch) => {
let ids = null
const sourceNfs = _.filter(notifications, n => !n.seen)
if (sourceNfs.length === 0) {
return
}
ids = _.map(sourceNfs, n => n.id).join('-')

dispatch({
type: NOTIFICATIONS_PENDING
})

handleDispatchMarkAllNotificationsSeen(dispatch, sourceId, true)
notificationsService.markNotificationsSeen(ids).catch(err => {
Alert.error(`Failed to mark notification seen. ${err.message}`)
handleDispatchMarkAllNotificationsSeen(dispatch, sourceId, false)
})

}

export const markAllNotificationsRead = (sourceId, notifications = []) => (dispatch) => {
let ids = null
if (sourceId) {
Expand Down
8 changes: 8 additions & 0 deletions src/routes/notifications/helpers/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,14 @@ export const filterNotificationsByPosts = (notifications, posts) => {
*/
export const filterReadNotifications = (notifications) => _.filter(notifications, { isRead: false })

/**
*
* @param {Array} notifications list of notifications
*
* @return {Array} list of filtered notifications
*/
export const filterSeenNotifications = (notifications) => _.filter(notifications, { seen: false })

/**
* Filter notifications that belongs to project:projectId
*
Expand Down
17 changes: 12 additions & 5 deletions src/routes/notifications/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import {
GET_NOTIFICATIONS_PENDING,
GET_NOTIFICATIONS_SUCCESS,
GET_NOTIFICATIONS_FAILURE,
VISIT_NOTIFICATIONS,
TOGGLE_NOTIFICATION_SEEN,
SET_NOTIFICATIONS_FILTER_BY,
MARK_ALL_NOTIFICATIONS_READ,
MARK_ALL_NOTIFICATIONS_SEEN,
TOGGLE_NOTIFICATION_READ,
VIEW_OLDER_NOTIFICATIONS_SUCCESS,
HIDE_OLDER_NOTIFICATIONS_SUCCESS,
Expand All @@ -26,7 +26,6 @@ const initialState = {
notifications: [],
// ids of sources that will also show old notifications
oldSourceIds: [],
lastVisited: new Date(0),
pending: false,
readers: {},
}
Expand Down Expand Up @@ -73,9 +72,6 @@ export default (state = initialState, action) => {
case GET_NOTIFICATIONS_FAILURE:
return { ...state, isLoading: false }

case VISIT_NOTIFICATIONS:
return {...state, lastVisited: _.maxBy(_.map(state.notifications, n => new Date(n.date)))}

case TOGGLE_NOTIFICATION_SEEN: {
const ids = action.payload.split('-')
const newState = {
Expand Down Expand Up @@ -109,6 +105,17 @@ export default (state = initialState, action) => {
return newState
}

case MARK_ALL_NOTIFICATIONS_SEEN: {
const newState = {
...state,
pending: false,
...getNotificationsAndFilterBy(state.notifications.map(n => (
!action.payload || n.sourcId === action.payload ? { ...n, seen: action.isSeen } : n
)), state.filterBy)
}
return newState
}

case TOGGLE_NOTIFICATION_READ: {
const newState = {
...state,
Expand Down