Skip to content
This repository has been archived by the owner on Nov 10, 2023. It is now read-only.

Commit

Permalink
feat: notification centre (#3973)
Browse files Browse the repository at this point in the history
* feat: notification `timestamp`, `read` + `action` (#3971)

* feat: notification header bell (#3974)

* feat: add notification bell in header

* fix: remove unused prop

* feat: notification centre popper (#3976)

* feat: add notification bell in header

* fix: remove unused prop

* feat: notification popper

* fix: mark notifications read on popper close

* feat: add scrollbar

* fix: chronological sort + increase button size

* fix: lint + remove padding

* fix: update type, split components + simplify sort

* fix: cleanup clickaways

* fix: add return type

* fix: use popper offset

* fix: close poppers relevant to notifications

* fix: ordering + don't read pending notifications

* fix: remove comment

* fix: clickable area of bell

* fix: use `filter`

* fix: randomise key + close notification

* fix: only show unread badge after dismissal
  • Loading branch information
iamacook authored Jun 17, 2022
1 parent 35c9c76 commit 5db53ae
Show file tree
Hide file tree
Showing 14 changed files with 507 additions and 52 deletions.
9 changes: 4 additions & 5 deletions src/assets/icons/alert.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions src/assets/icons/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 3 additions & 5 deletions src/assets/icons/error.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 4 additions & 6 deletions src/assets/icons/info.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 11 additions & 6 deletions src/components/AppLayout/Header/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { shouldSwitchWalletChain } from 'src/logic/wallets/store/selectors'
import { useSelector } from 'react-redux'
import { OVERVIEW_EVENTS } from 'src/utils/events/overview'
import Track from 'src/components/Track'
import Notifications from 'src/components/AppLayout/Header/components/Notifications'

const styles = () => ({
root: {
Expand Down Expand Up @@ -90,7 +91,8 @@ const WalletPopup = ({ anchorEl, providerDetails, classes, open, onClose }) => {
}

const Layout = ({ classes, providerDetails, providerInfo }) => {
const { clickAway, open, toggle } = useStateHandler()
const { clickAway: clickAwayNotifications, open: openNotifications, toggle: toggleNotifications } = useStateHandler()
const { clickAway: clickAwayWallet, open: openWallet, toggle: toggleWallet } = useStateHandler()
const { clickAway: clickAwayNetworks, open: openNetworks, toggle: toggleNetworks } = useStateHandler()
const isWrongChain = useSelector(shouldSwitchWalletChain)

Expand All @@ -113,25 +115,28 @@ const Layout = ({ classes, providerDetails, providerInfo }) => {
</div>
)}

<Divider />
<Notifications open={openNotifications} toggle={toggleNotifications} clickAway={clickAwayNotifications} />

<Divider />
<Provider
info={providerInfo}
open={open}
toggle={toggle}
open={openWallet}
toggle={toggleWallet}
render={(providerRef) =>
providerRef.current && (
<WalletPopup
anchorEl={providerRef.current}
providerDetails={providerDetails}
open={open}
open={openWallet}
classes={classes}
onClose={clickAway}
onClose={clickAwayWallet}
/>
)
}
/>
<Divider />

<Divider />
<NetworkSelector open={openNetworks} toggle={toggleNetworks} clickAway={clickAwayNetworks} />
</Row>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ReactElement } from 'react'
import styled from 'styled-components'
import { OptionsObject } from 'notistack'

import { NotificationsState } from 'src/logic/notifications/store/notifications'
import { black500 } from 'src/theme/variables'
import { formatTime } from 'src/utils/date'
import AlertIcon from 'src/assets/icons/alert.svg'
import CheckIcon from 'src/assets/icons/check.svg'
import ErrorIcon from 'src/assets/icons/error.svg'
import InfoIcon from 'src/assets/icons/info.svg'
import { UnreadNotificationBadge, NotificationSubtitle } from 'src/components/AppLayout/Header/components/Notifications'

const notificationIcon = {
error: ErrorIcon,
info: InfoIcon,
success: CheckIcon,
warning: AlertIcon,
}

const getNotificationIcon = (variant: OptionsObject['variant'] = 'info'): string => notificationIcon[variant]

const NoficationItem = ({ read, options, message, timestamp }: NotificationsState[number]): ReactElement => (
<Notification>
<UnreadNotificationBadge
variant="dot"
overlap="circle"
invisible={read}
anchorOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<img src={getNotificationIcon(options?.variant)} />
</UnreadNotificationBadge>
<div>
<NotificationMessage>{message}</NotificationMessage>
<br />
<NotificationSubtitle>{formatTime(timestamp)}</NotificationSubtitle>
</div>
</Notification>
)

const Notification = styled.div`
box-sizing: border-box;
display: flex;
align-items: center;
> * {
padding: 8px;
}
`

const NotificationMessage = styled.p`
all: unset;
font-weight: 400;
font-size: 16px;
line-height: 24px;
color: ${black500};
`

export default NoficationItem
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ReactElement } from 'react'
import styled from 'styled-components'

import { NotificationsState } from 'src/logic/notifications/store/notifications'
import { NOTIFICATION_LIMIT } from 'src/components/AppLayout/Header/components/Notifications'
import { StyledScrollableBar } from 'src/routes/safe/components/Transactions/TxList/styled'
import { black300 } from 'src/theme/variables'
import NotificationItem from 'src/components/AppLayout/Header/components/Notifications/NotificationItem'

const NotificationList = ({ notifications }: { notifications: NotificationsState }): ReactElement => {
if (!notifications.length) {
return <NotificationType>No notifications</NotificationType>
}

return (
<ScrollContainer $showScrollbar={notifications.length > NOTIFICATION_LIMIT}>
<NotificationType>System updates</NotificationType>
{notifications.map((notification) => (
<NotificationItem key={notification.timestamp} {...notification} />
))}
</ScrollContainer>
)
}

const ScrollContainer = styled(StyledScrollableBar)<{ $showScrollbar: boolean }>`
height: ${({ $showScrollbar: $scroll }) => ($scroll ? '500px' : 'auto')};
overflow-x: hidden;
overflow-y: auto;
width: 100%;
`

const NotificationType = styled.h4`
all: unset;
display: block;
font-weight: 400;
font-size: 14px;
color: ${black300};
margin-bottom: 12px;
`

export default NotificationList
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { NotificationsState } from 'src/logic/notifications/store/notifications'
import { getSortedNotifications } from '../'

const UNREAD_ACTION_NOTIFICATION = {
read: false,
action: true,
} as NotificationsState[number]

const UNREAD_NOTIFICATION = {
read: false,
action: false,
} as NotificationsState[number]

const READ_ACTION_NOTIFICATION = {
read: true,
action: true,
} as NotificationsState[number]

const READ_NOTIFICATION = {
read: true,
action: false,
} as NotificationsState[number]

describe('Notifications', () => {
describe('getSortedNotifications', () => {
it("should't sort correctly ordered notifications", () => {
const notifications = [
{ ...UNREAD_ACTION_NOTIFICATION, timestamp: 6 },
{ ...UNREAD_ACTION_NOTIFICATION, timestamp: 5 },
{ ...UNREAD_NOTIFICATION, timestamp: 4 },
{ ...UNREAD_NOTIFICATION, timestamp: 3 },
{ ...READ_ACTION_NOTIFICATION, timestamp: 2 },
{ ...READ_NOTIFICATION, timestamp: 1 },
]
expect(getSortedNotifications(notifications)).toEqual(notifications)
})

it('should sort the read notifications chronologically regardless of action', () => {
const notifications = [
{ ...READ_NOTIFICATION, timestamp: 1 },
{ ...READ_NOTIFICATION, timestamp: 2 },
{ ...READ_ACTION_NOTIFICATION, timestamp: 3 },
{ ...READ_NOTIFICATION, timestamp: 4 },
]

const sortedNotifications = [
{ ...READ_NOTIFICATION, timestamp: 4 },
{ ...READ_ACTION_NOTIFICATION, timestamp: 3 },
{ ...READ_NOTIFICATION, timestamp: 2 },
{ ...READ_NOTIFICATION, timestamp: 1 },
]

expect(getSortedNotifications(notifications)).toEqual(sortedNotifications)
})

it('should sort unread actionable notifications to the top', () => {
const notifications = [
{ ...READ_ACTION_NOTIFICATION, timestamp: 3 },
{ ...READ_NOTIFICATION, timestamp: 2 },
{ ...UNREAD_ACTION_NOTIFICATION, timestamp: 1 },
]

const sortedNotifications = [
{ ...UNREAD_ACTION_NOTIFICATION, timestamp: 1 },
{ ...READ_ACTION_NOTIFICATION, timestamp: 3 },
{ ...READ_NOTIFICATION, timestamp: 2 },
]
expect(getSortedNotifications(notifications)).toEqual(sortedNotifications)
})
it('should sort unread notifications to the top', () => {
const notifications = [
{ ...READ_ACTION_NOTIFICATION, timestamp: 3 },
{ ...READ_NOTIFICATION, timestamp: 3 },
{ ...UNREAD_NOTIFICATION, timestamp: 1 },
]

const sortedNotifications = [
{ ...UNREAD_NOTIFICATION, timestamp: 1 },
{ ...READ_ACTION_NOTIFICATION, timestamp: 3 },
{ ...READ_NOTIFICATION, timestamp: 3 },
]

expect(getSortedNotifications(notifications)).toEqual(sortedNotifications)
})
it('should sort actionable notifications to the top, followed by unread notifications', () => {
const notifications = [
{ ...READ_ACTION_NOTIFICATION, timestamp: 4 },
{ ...READ_NOTIFICATION, timestamp: 3 },
{ ...UNREAD_NOTIFICATION, timestamp: 2 },
{ ...UNREAD_ACTION_NOTIFICATION, timestamp: 1 },
]

const sortedNotifications = [
{ ...UNREAD_ACTION_NOTIFICATION, timestamp: 1 },
{ ...UNREAD_NOTIFICATION, timestamp: 2 },
{ ...READ_ACTION_NOTIFICATION, timestamp: 4 },
{ ...READ_NOTIFICATION, timestamp: 3 },
]

expect(getSortedNotifications(notifications)).toEqual(sortedNotifications)
})
})
})
Loading

0 comments on commit 5db53ae

Please sign in to comment.