mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-21 17:26:29 -05:00
enhancement(notifications): remove from state after dismissal
This commit changes the implementation from an array to a Map, as that makes accessing a notification for dismissal or removal more performant. Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
ea57f5bb3b
commit
909f45bc3f
4 changed files with 54 additions and 34 deletions
|
@ -10,10 +10,11 @@ import { UiNotifications } from './ui-notifications'
|
|||
import type { TOptions } from 'i18next'
|
||||
import { DateTime } from 'luxon'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'
|
||||
import React, { createContext, useCallback, useContext, useMemo } from 'react'
|
||||
import { ExclamationTriangle as IconExclamationTriangle } from 'react-bootstrap-icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { useMap } from 'react-use'
|
||||
|
||||
const log = new Logger('Notifications')
|
||||
|
||||
|
@ -27,6 +28,7 @@ interface UiNotificationContext {
|
|||
showErrorNotification: (messageI18nKey: string, messageI18nOptions?: TOptions) => (error: Error) => void
|
||||
|
||||
dismissNotification: (notificationUuid: string) => void
|
||||
pruneNotification: (notificationUuid: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -50,7 +52,9 @@ const uiNotificationContext = createContext<UiNotificationContext | undefined>(u
|
|||
*/
|
||||
export const UiNotificationBoundary: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const { t } = useTranslation()
|
||||
const [uiNotifications, setUiNotifications] = useState<UiNotification[]>([])
|
||||
const [uiNotifications, { set: setUiNotification, remove: removeUiNotification, get: getUiNotification }] = useMap<
|
||||
Record<string, UiNotification>
|
||||
>({})
|
||||
|
||||
const dispatchUiNotification = useCallback(
|
||||
(
|
||||
|
@ -58,9 +62,8 @@ export const UiNotificationBoundary: React.FC<PropsWithChildren> = ({ children }
|
|||
contentI18nKey: string,
|
||||
{ icon, durationInSecond, buttons, contentI18nOptions, titleI18nOptions }: Partial<DispatchOptions>
|
||||
) => {
|
||||
setUiNotifications((oldState) => [
|
||||
...oldState,
|
||||
{
|
||||
const notificationUuid = uuid()
|
||||
setUiNotification(notificationUuid, {
|
||||
titleI18nKey,
|
||||
contentI18nKey,
|
||||
createdAtTimestamp: DateTime.now().toSeconds(),
|
||||
|
@ -70,11 +73,10 @@ export const UiNotificationBoundary: React.FC<PropsWithChildren> = ({ children }
|
|||
durationInSecond: durationInSecond ?? DEFAULT_DURATION_IN_SECONDS,
|
||||
buttons: buttons ?? [],
|
||||
icon: icon,
|
||||
uuid: uuid()
|
||||
}
|
||||
])
|
||||
uuid: notificationUuid
|
||||
})
|
||||
},
|
||||
[]
|
||||
[setUiNotification]
|
||||
)
|
||||
|
||||
const showErrorNotification = useCallback(
|
||||
|
@ -89,24 +91,35 @@ export const UiNotificationBoundary: React.FC<PropsWithChildren> = ({ children }
|
|||
[dispatchUiNotification, t]
|
||||
)
|
||||
|
||||
const dismissNotification = useCallback((notificationUuid: string): void => {
|
||||
setUiNotifications((old) => {
|
||||
const found = old.find((notification) => notification.uuid === notificationUuid)
|
||||
if (found === undefined) {
|
||||
return old
|
||||
const dismissNotification = useCallback(
|
||||
(notificationUuid: string): void => {
|
||||
const entry = getUiNotification(notificationUuid)
|
||||
if (!entry) {
|
||||
return
|
||||
}
|
||||
return old.filter((value) => value.uuid !== notificationUuid).concat({ ...found, dismissed: true })
|
||||
})
|
||||
}, [])
|
||||
setUiNotification(notificationUuid, { ...entry, dismissed: true })
|
||||
},
|
||||
[setUiNotification, getUiNotification]
|
||||
)
|
||||
|
||||
const pruneNotification = useCallback(
|
||||
(notificationUuid: string): void => {
|
||||
if (!uiNotifications[notificationUuid]) {
|
||||
return
|
||||
}
|
||||
removeUiNotification(notificationUuid)
|
||||
},
|
||||
[uiNotifications, removeUiNotification]
|
||||
)
|
||||
|
||||
const context = useMemo(() => {
|
||||
return {
|
||||
dispatchUiNotification: dispatchUiNotification,
|
||||
showErrorNotification: showErrorNotification,
|
||||
dismissNotification: dismissNotification
|
||||
dismissNotification: dismissNotification,
|
||||
pruneNotification: pruneNotification
|
||||
}
|
||||
}, [dismissNotification, dispatchUiNotification, showErrorNotification])
|
||||
|
||||
}, [dismissNotification, dispatchUiNotification, showErrorNotification, pruneNotification])
|
||||
return (
|
||||
<uiNotificationContext.Provider value={context}>
|
||||
<UiNotifications notifications={uiNotifications} />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -31,13 +31,18 @@ export interface UiNotificationProps {
|
|||
export const UiNotificationToast: React.FC<UiNotificationProps> = ({ notification }) => {
|
||||
const { t } = useTranslation()
|
||||
const [remainingSteps, setRemainingSteps] = useState<number>(() => notification.durationInSecond * STEPS_PER_SECOND)
|
||||
const { dismissNotification } = useUiNotifications()
|
||||
const { dismissNotification, pruneNotification } = useUiNotifications()
|
||||
|
||||
const dismissNow = useCallback(() => {
|
||||
log.debug(`Dismiss notification ${notification.uuid} immediately`)
|
||||
setRemainingSteps(0)
|
||||
}, [notification.uuid])
|
||||
|
||||
const prune = useCallback(() => {
|
||||
log.debug(`Prune notification ${notification.uuid} from state`)
|
||||
pruneNotification(notification.uuid)
|
||||
}, [pruneNotification, notification.uuid])
|
||||
|
||||
useEffectOnce(() => {
|
||||
log.debug(`Show notification ${notification.uuid}`)
|
||||
})
|
||||
|
@ -97,6 +102,7 @@ export const UiNotificationToast: React.FC<UiNotificationProps> = ({ notificatio
|
|||
className={styles.toast}
|
||||
show={!notification.dismissed}
|
||||
onClose={dismissNow}
|
||||
onExited={prune}
|
||||
{...cypressId('notification-toast')}>
|
||||
<Toast.Header>
|
||||
<strong className='me-auto'>
|
||||
|
|
|
@ -9,7 +9,7 @@ import { UiNotificationToast } from './ui-notification-toast'
|
|||
import React, { useMemo } from 'react'
|
||||
|
||||
export interface UiNotificationsProps {
|
||||
notifications: UiNotification[]
|
||||
notifications: Record<string, UiNotification>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -19,7 +19,7 @@ export interface UiNotificationsProps {
|
|||
*/
|
||||
export const UiNotifications: React.FC<UiNotificationsProps> = ({ notifications }) => {
|
||||
const notificationElements = useMemo(() => {
|
||||
return notifications
|
||||
return Object.values(notifications)
|
||||
.sort((a, b) => b.createdAtTimestamp - a.createdAtTimestamp)
|
||||
.map((notification) => <UiNotificationToast key={notification.uuid} notification={notification} />)
|
||||
}, [notifications])
|
||||
|
|
|
@ -14,6 +14,7 @@ export const mockUiNotifications = () => {
|
|||
jest.spyOn(useUiNotificationsModule, 'useUiNotifications').mockReturnValue({
|
||||
showErrorNotification: jest.fn(),
|
||||
dismissNotification: jest.fn(),
|
||||
dispatchUiNotification: jest.fn()
|
||||
dispatchUiNotification: jest.fn(),
|
||||
pruneNotification: jest.fn()
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue