Skip to content

Commit

Permalink
[Web-App] Add novu notifications to navbar (#1015)
Browse files Browse the repository at this point in the history
* connect novu notifications to UI

* fix comments

* fix naming

* remove redundant props

* add user interactions logic to novu notification and overlay

* fix build issues
  • Loading branch information
rners01 authored Aug 31, 2023
1 parent 5412c97 commit 4aaa298
Show file tree
Hide file tree
Showing 15 changed files with 495 additions and 38 deletions.
10 changes: 7 additions & 3 deletions packages/web-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,20 @@
"@babel/runtime": "7.21.0",
"@elastic/react-search-ui": "1.17.1",
"@elastic/search-ui-app-search-connector": "1.17.1",
"@emotion/cache": "11.11.0",
"@emotion/react": "11.10.6",
"@emotion/serialize": "1.1.2",
"@emotion/styled": "11.10.6",
"@emotion/utils": "1.2.1",
"@fortawesome/fontawesome-svg-core": "6.3.0",
"@fortawesome/free-regular-svg-icons": "6.3.0",
"@fortawesome/free-solid-svg-icons": "6.3.0",
"@fortawesome/react-fontawesome": "0.2.0",
"@novu/notification-center": "0.14.0",
"@saladtechnologies/garden-components": "1.1.5",
"@novu/notification-center": "0.16.2",
"@novu/shared": "0.17.1",
"@saladtechnologies/garden-components": "1.1.8",
"@saladtechnologies/garden-fonts": "1.0.3",
"@saladtechnologies/garden-icons": "1.0.11",
"@saladtechnologies/garden-icons": "1.0.12",
"@storybook/addon-a11y": "6.5.16",
"@storybook/addon-actions": "6.5.16",
"@storybook/addon-essentials": "6.5.16",
Expand Down
4 changes: 4 additions & 0 deletions packages/web-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { MobileDevice, NotMobile } from './components'
import { config } from './config'
import { MobileRoutes } from './MobileRoutes'
import { NavigationBarContainer } from './modules/home-views'
import { NovuNotificationBanner } from './modules/notifications-views/components'
import { Routes } from './Routes'
import type { SaladTheme } from './SaladTheme'
import { getStore } from './Store'
Expand Down Expand Up @@ -117,8 +118,11 @@ export const App = withStyles(styles)(

public override render(): ReactNode {
const { classes, history } = this.props
const isAuthenticated = this.store.auth.isAuthenticated

return (
<>
{isAuthenticated && <NovuNotificationBanner />}
<MobileDevice>
<div className={classes.mobileMainWindow}>
<div className={classes.mobileNavigationContainer}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ interface NovuProviderWrapperProps {
children: ReactElement
}

const isNovuProviderEnabled = false

export const NovuProviderWrapper = observer(({ children }: NovuProviderWrapperProps): ReactElement | null => {
const store = getStore()
const { currentProfile } = store.profile
if (currentProfile?.id && isNovuProviderEnabled) {
if (currentProfile?.id) {
return (
<NovuProvider
applicationIdentifier={config.novuAppId}
Expand Down
12 changes: 10 additions & 2 deletions packages/web-app/src/modules/home-views/NavigationBarContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Avatar, AvatarDefault, BonusCard, NavigationBar } from '@saladtechnologies/garden-components'
import { Avatar, AvatarDefault, BonusCard } from '@saladtechnologies/garden-components'
import type { TargetRewardInfo } from '@saladtechnologies/garden-components/lib/components/NavigationBar/components/DesktopNavigationBar/TargetRewardStatus'
import { connect } from '../../connect'
import type { RootStore } from '../../Store'
import { NavigationBarWithNotifications } from './components/NavigationBarWithNotifications'

const mapStoreToProps = (store: RootStore): any => {
const isAuthenticated = store.auth.isAuthenticated
Expand All @@ -25,6 +26,12 @@ const mapStoreToProps = (store: RootStore): any => {
canBeRedeemed: store.balance.currentBalance >= store.rewards.selectedTargetReward?.price,
}
: null
const isNotificationsDrawerOpened = store.notifications.isNotificationsDrawerOpened
const notifications = {
isNotificationsDrawerOpened,
onOpenNotificationsDrawer: store.notifications.openNotificationsDrawer,
onCloseNotificationsDrawer: store.notifications.closeNotificationsDrawer,
}

const goToAccount = () => store.routing.push('/account/summary')
const goToSelectTargetRewardPage = () => store.routing.push('/store/select-target-reward')
Expand Down Expand Up @@ -74,7 +81,8 @@ const mapStoreToProps = (store: RootStore): any => {
startButtonToolTipError: startButton.toolTipError,
username: isAuthenticated ? store.profile.currentProfile?.username : undefined,
targetReward,
notifications,
}
}

export const NavigationBarContainer = connect(mapStoreToProps, NavigationBar)
export const NavigationBarContainer = connect(mapStoreToProps, NavigationBarWithNotifications)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useNotifications } from '@novu/notification-center'
import { NavigationBar, type NavigationBarProps } from '@saladtechnologies/garden-components'
import type { FunctionComponent } from 'react'
import { getConfiguredNovuBannerNotifications } from '../../notifications/utils'

export const NavigationBarWithNotifications: FunctionComponent<NavigationBarProps> = (props) => {
const {
notifications: novuNotifications,
unseenCount,
markNotificationAsRead,
markAllNotificationsAsSeen,
} = useNotifications()

const unreadNovuNotifications = novuNotifications?.filter((notification) => !notification.read)
const bannerNotifications = getConfiguredNovuBannerNotifications(unreadNovuNotifications, (notificationId: string) =>
markNotificationAsRead(notificationId),
)
const newsNotifications = bannerNotifications.filter((notification) => notification?.variant === 'news')
const warningsNotifications = bannerNotifications.filter((notification) => notification?.variant === 'error')
const hasUnseenNotifications = unseenCount > 0
const handleOpenNotificationsDrawer = () => {
props.notifications.onOpenNotificationsDrawer()

if (hasUnseenNotifications) {
markAllNotificationsAsSeen()
}
}

const notifications = {
...props.notifications,
news: newsNotifications,
warnings: warningsNotifications,
hasUnseenNotifications,
onOpenNotificationsDrawer: handleOpenNotificationsDrawer,
}

return <NavigationBar {...props} notifications={notifications} />
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { action } from '@storybook/addon-actions'
import { storiesOf } from '@storybook/react'
import { addStories } from '../../../../.storybook/addStories'
import { addStories } from '../../../../../.storybook/addStories'
import { NotificationToast } from './NotificationToast'

const stories = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import type { ReactNode } from 'react'
import { Component } from 'react'
import type { WithStyles } from 'react-jss'
import withStyles from 'react-jss'
import { P } from '../../../components'
import type { SaladTheme } from '../../../SaladTheme'
import { P } from '../../../../components'
import type { SaladTheme } from '../../../../SaladTheme'

const styles = (theme: SaladTheme) => ({
container: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './NotificationToast'
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useNotifications } from '@novu/notification-center'
import { NotificationBanner } from '@saladtechnologies/garden-components'
import type { FC } from 'react'
import { useEffect, useState } from 'react'
import type { WithStyles } from 'react-jss'
import withStyles from 'react-jss'
import { getConfiguredNovuBannerNotifications } from '../../../notifications/utils'

const styles = () => ({
container: {
width: 560,
position: 'fixed',
zIndex: 99,
top: 85,
left: 0,
right: 0,
margin: 'auto',
},
})

interface NovuNotificationBannerRawProps extends WithStyles<typeof styles> {}

export const NovuNotificationBannerRaw: FC<NovuNotificationBannerRawProps> = ({ classes }) => {
const { notifications, markNotificationAsRead } = useNotifications()
const [isHidden, setisHidden] = useState(false)

const unreadNovuNotifications = notifications?.filter((notification) => !notification.read)
const bannerNotifications = getConfiguredNovuBannerNotifications(
unreadNovuNotifications,
(notificationId: string) => {
markNotificationAsRead(notificationId)
setisHidden(true)
},
)
const overlayNovuNotifications = bannerNotifications?.filter((notification) => notification.overlay)
const lastOverlayNovuNotification = overlayNovuNotifications[0]

useEffect(() => {
setisHidden(false)
}, [notifications, setisHidden])

if (!lastOverlayNovuNotification || isHidden) {
return null
}

return (
<div className={classes.container}>
<NotificationBanner {...lastOverlayNovuNotification} />
</div>
)
}

export const NovuNotificationBanner = withStyles(styles)(NovuNotificationBannerRaw)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './NovuNotificationBanner'
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './NotificationToast'
export * from './NovuNotificationBanner'
16 changes: 15 additions & 1 deletion packages/web-app/src/modules/notifications/NotificationStore.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import { action, observable } from 'mobx'
import { toast } from 'react-toastify'
import type { RootStore } from '../../Store'
import { NotificationToast } from './components/NotificationToast'
import { NotificationToast } from '../notifications-views/components'
import type { NotificationMessage } from './models'

export class NotificationStore {
@observable
public isNotificationsDrawerOpened: boolean = false

constructor(private readonly store: RootStore) {}

@action.bound
openNotificationsDrawer = () => {
this.isNotificationsDrawerOpened = true
}

@action.bound
closeNotificationsDrawer = () => {
this.isNotificationsDrawerOpened = false
}

/**
* Shows a notification to the user
* @param message The notification message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export enum NotificationMessageCategory {
ReferralCodeInvalid = 'Referral Code Invalid',
ReferralCodeDoesNotExist = 'Referral Code Does Not Exist',
ReferralCodeError = 'Referral Code Error',
NovuInfo = 'Novu Info',
NovuWarning = 'Novu Warning',
}

export interface NotificationMessage {
Expand Down Expand Up @@ -52,3 +54,63 @@ export interface NotificationMessage {
*/
onClick?: () => void
}

/** A resource that represents an action that acknowledges a notification. */
export interface AcknowledgeNotificationAction {
/** The title. */
title: string
}
/** A resource that represents an action that dismisses a notification. */
export interface DismissNotificationAction {
/** The title. */
title: string
}
/** A resource that represents an action that opens a link in the default browser. */
export interface OpenLinkNotificationAction {
/** The title. */
title: string
/** The link. */
link: string
}

/** A resource that represents an in-app notification action. */
export interface NotificationAction {
action?:
| {
$case: 'acknowledge'
acknowledge: AcknowledgeNotificationAction
}
| {
$case: 'dismiss'
dismiss: DismissNotificationAction
}
| {
$case: 'openLink'
openLink: OpenLinkNotificationAction
}
}

export interface Notification {
/** The Novu resource identifier. */
novuId: string
/** The title. */
title: string
/** The body. */
body: string
/** The date and time of the notification. */
createDate: Date | undefined
/** The list of actions. */
actions: NotificationAction[]
/** A value indicating whether the notification has been acknowledged. */
acknowledged: boolean
/** identifier used to track actions from this notification. */
trackId: string
/** A value indicating whether the notification is os type. */
osNotification: boolean
/** A value indicating whether the notification is overlay type. */
overlay: boolean
/** A value indicating whether the notification has been seen. */
seen: boolean
/** A value indicating whether the notification has been read. */
read: boolean
}
Loading

0 comments on commit 4aaa298

Please sign in to comment.