mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-22 01:36: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 type { TOptions } from 'i18next'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
import type { PropsWithChildren } from 'react'
|
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 { ExclamationTriangle as IconExclamationTriangle } from 'react-bootstrap-icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { v4 as uuid } from 'uuid'
|
import { v4 as uuid } from 'uuid'
|
||||||
|
import { useMap } from 'react-use'
|
||||||
|
|
||||||
const log = new Logger('Notifications')
|
const log = new Logger('Notifications')
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@ interface UiNotificationContext {
|
||||||
showErrorNotification: (messageI18nKey: string, messageI18nOptions?: TOptions) => (error: Error) => void
|
showErrorNotification: (messageI18nKey: string, messageI18nOptions?: TOptions) => (error: Error) => void
|
||||||
|
|
||||||
dismissNotification: (notificationUuid: string) => 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 }) => {
|
export const UiNotificationBoundary: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [uiNotifications, setUiNotifications] = useState<UiNotification[]>([])
|
const [uiNotifications, { set: setUiNotification, remove: removeUiNotification, get: getUiNotification }] = useMap<
|
||||||
|
Record<string, UiNotification>
|
||||||
|
>({})
|
||||||
|
|
||||||
const dispatchUiNotification = useCallback(
|
const dispatchUiNotification = useCallback(
|
||||||
(
|
(
|
||||||
|
@ -58,23 +62,21 @@ export const UiNotificationBoundary: React.FC<PropsWithChildren> = ({ children }
|
||||||
contentI18nKey: string,
|
contentI18nKey: string,
|
||||||
{ icon, durationInSecond, buttons, contentI18nOptions, titleI18nOptions }: Partial<DispatchOptions>
|
{ icon, durationInSecond, buttons, contentI18nOptions, titleI18nOptions }: Partial<DispatchOptions>
|
||||||
) => {
|
) => {
|
||||||
setUiNotifications((oldState) => [
|
const notificationUuid = uuid()
|
||||||
...oldState,
|
setUiNotification(notificationUuid, {
|
||||||
{
|
titleI18nKey,
|
||||||
titleI18nKey,
|
contentI18nKey,
|
||||||
contentI18nKey,
|
createdAtTimestamp: DateTime.now().toSeconds(),
|
||||||
createdAtTimestamp: DateTime.now().toSeconds(),
|
dismissed: false,
|
||||||
dismissed: false,
|
titleI18nOptions: titleI18nOptions ?? {},
|
||||||
titleI18nOptions: titleI18nOptions ?? {},
|
contentI18nOptions: contentI18nOptions ?? {},
|
||||||
contentI18nOptions: contentI18nOptions ?? {},
|
durationInSecond: durationInSecond ?? DEFAULT_DURATION_IN_SECONDS,
|
||||||
durationInSecond: durationInSecond ?? DEFAULT_DURATION_IN_SECONDS,
|
buttons: buttons ?? [],
|
||||||
buttons: buttons ?? [],
|
icon: icon,
|
||||||
icon: icon,
|
uuid: notificationUuid
|
||||||
uuid: uuid()
|
})
|
||||||
}
|
|
||||||
])
|
|
||||||
},
|
},
|
||||||
[]
|
[setUiNotification]
|
||||||
)
|
)
|
||||||
|
|
||||||
const showErrorNotification = useCallback(
|
const showErrorNotification = useCallback(
|
||||||
|
@ -89,24 +91,35 @@ export const UiNotificationBoundary: React.FC<PropsWithChildren> = ({ children }
|
||||||
[dispatchUiNotification, t]
|
[dispatchUiNotification, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
const dismissNotification = useCallback((notificationUuid: string): void => {
|
const dismissNotification = useCallback(
|
||||||
setUiNotifications((old) => {
|
(notificationUuid: string): void => {
|
||||||
const found = old.find((notification) => notification.uuid === notificationUuid)
|
const entry = getUiNotification(notificationUuid)
|
||||||
if (found === undefined) {
|
if (!entry) {
|
||||||
return old
|
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(() => {
|
const context = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
dispatchUiNotification: dispatchUiNotification,
|
dispatchUiNotification: dispatchUiNotification,
|
||||||
showErrorNotification: showErrorNotification,
|
showErrorNotification: showErrorNotification,
|
||||||
dismissNotification: dismissNotification
|
dismissNotification: dismissNotification,
|
||||||
|
pruneNotification: pruneNotification
|
||||||
}
|
}
|
||||||
}, [dismissNotification, dispatchUiNotification, showErrorNotification])
|
}, [dismissNotification, dispatchUiNotification, showErrorNotification, pruneNotification])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<uiNotificationContext.Provider value={context}>
|
<uiNotificationContext.Provider value={context}>
|
||||||
<UiNotifications notifications={uiNotifications} />
|
<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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -31,13 +31,18 @@ export interface UiNotificationProps {
|
||||||
export const UiNotificationToast: React.FC<UiNotificationProps> = ({ notification }) => {
|
export const UiNotificationToast: React.FC<UiNotificationProps> = ({ notification }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [remainingSteps, setRemainingSteps] = useState<number>(() => notification.durationInSecond * STEPS_PER_SECOND)
|
const [remainingSteps, setRemainingSteps] = useState<number>(() => notification.durationInSecond * STEPS_PER_SECOND)
|
||||||
const { dismissNotification } = useUiNotifications()
|
const { dismissNotification, pruneNotification } = useUiNotifications()
|
||||||
|
|
||||||
const dismissNow = useCallback(() => {
|
const dismissNow = useCallback(() => {
|
||||||
log.debug(`Dismiss notification ${notification.uuid} immediately`)
|
log.debug(`Dismiss notification ${notification.uuid} immediately`)
|
||||||
setRemainingSteps(0)
|
setRemainingSteps(0)
|
||||||
}, [notification.uuid])
|
}, [notification.uuid])
|
||||||
|
|
||||||
|
const prune = useCallback(() => {
|
||||||
|
log.debug(`Prune notification ${notification.uuid} from state`)
|
||||||
|
pruneNotification(notification.uuid)
|
||||||
|
}, [pruneNotification, notification.uuid])
|
||||||
|
|
||||||
useEffectOnce(() => {
|
useEffectOnce(() => {
|
||||||
log.debug(`Show notification ${notification.uuid}`)
|
log.debug(`Show notification ${notification.uuid}`)
|
||||||
})
|
})
|
||||||
|
@ -97,6 +102,7 @@ export const UiNotificationToast: React.FC<UiNotificationProps> = ({ notificatio
|
||||||
className={styles.toast}
|
className={styles.toast}
|
||||||
show={!notification.dismissed}
|
show={!notification.dismissed}
|
||||||
onClose={dismissNow}
|
onClose={dismissNow}
|
||||||
|
onExited={prune}
|
||||||
{...cypressId('notification-toast')}>
|
{...cypressId('notification-toast')}>
|
||||||
<Toast.Header>
|
<Toast.Header>
|
||||||
<strong className='me-auto'>
|
<strong className='me-auto'>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { UiNotificationToast } from './ui-notification-toast'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
|
|
||||||
export interface UiNotificationsProps {
|
export interface UiNotificationsProps {
|
||||||
notifications: UiNotification[]
|
notifications: Record<string, UiNotification>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -19,7 +19,7 @@ export interface UiNotificationsProps {
|
||||||
*/
|
*/
|
||||||
export const UiNotifications: React.FC<UiNotificationsProps> = ({ notifications }) => {
|
export const UiNotifications: React.FC<UiNotificationsProps> = ({ notifications }) => {
|
||||||
const notificationElements = useMemo(() => {
|
const notificationElements = useMemo(() => {
|
||||||
return notifications
|
return Object.values(notifications)
|
||||||
.sort((a, b) => b.createdAtTimestamp - a.createdAtTimestamp)
|
.sort((a, b) => b.createdAtTimestamp - a.createdAtTimestamp)
|
||||||
.map((notification) => <UiNotificationToast key={notification.uuid} notification={notification} />)
|
.map((notification) => <UiNotificationToast key={notification.uuid} notification={notification} />)
|
||||||
}, [notifications])
|
}, [notifications])
|
||||||
|
|
|
@ -14,6 +14,7 @@ export const mockUiNotifications = () => {
|
||||||
jest.spyOn(useUiNotificationsModule, 'useUiNotifications').mockReturnValue({
|
jest.spyOn(useUiNotificationsModule, 'useUiNotifications').mockReturnValue({
|
||||||
showErrorNotification: jest.fn(),
|
showErrorNotification: jest.fn(),
|
||||||
dismissNotification: jest.fn(),
|
dismissNotification: jest.fn(),
|
||||||
dispatchUiNotification: jest.fn()
|
dispatchUiNotification: jest.fn(),
|
||||||
|
pruneNotification: jest.fn()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue