mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-19 17:55:17 -04:00
Rework notifications (#1465)
* Rework notifications - dispatchUINotification returns a promise that contains the notification id - notifications use i18n instead of plain text Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de> * Reformat code Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
808601eaba
commit
553e9f8ead
13 changed files with 154 additions and 105 deletions
|
@ -3,6 +3,10 @@
|
||||||
"slogan": "The best platform to write and share markdown.",
|
"slogan": "The best platform to write and share markdown.",
|
||||||
"title": "Collaborative markdown notes"
|
"title": "Collaborative markdown notes"
|
||||||
},
|
},
|
||||||
|
"notificationTest": {
|
||||||
|
"title": "Test",
|
||||||
|
"content": "It works!"
|
||||||
|
},
|
||||||
"renderer": {
|
"renderer": {
|
||||||
"highlightCode": {
|
"highlightCode": {
|
||||||
"copyCode": "Copy code to clipboard"
|
"copyCode": "Copy code to clipboard"
|
||||||
|
|
|
@ -6,8 +6,7 @@
|
||||||
|
|
||||||
import { Editor, Hint, Hints, Pos } from 'codemirror'
|
import { Editor, Hint, Hints, Pos } from 'codemirror'
|
||||||
import { findWordAtCursor, generateHintListByPrefix, Hinter } from './index'
|
import { findWordAtCursor, generateHintListByPrefix, Hinter } from './index'
|
||||||
import { DEFAULT_DURATION_IN_SECONDS, dispatchUiNotification } from '../../../../redux/ui-notifications/methods'
|
import { showErrorNotification } from '../../../../redux/ui-notifications/methods'
|
||||||
import i18n from 'i18next'
|
|
||||||
|
|
||||||
type highlightJsImport = typeof import('../../../common/hljs/hljs')
|
type highlightJsImport = typeof import('../../../common/hljs/hljs')
|
||||||
|
|
||||||
|
@ -22,12 +21,7 @@ const loadHighlightJs = async (): Promise<highlightJsImport | null> => {
|
||||||
try {
|
try {
|
||||||
return await import('../../../common/hljs/hljs')
|
return await import('../../../common/hljs/hljs')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatchUiNotification(
|
showErrorNotification('common.errorWhileLoadingLibrary', { name: 'highlight.js' })(error as Error)
|
||||||
i18n.t('common.errorOccurred'),
|
|
||||||
i18n.t('common.errorWhileLoadingLibrary', { name: 'highlight.js' }),
|
|
||||||
DEFAULT_DURATION_IN_SECONDS,
|
|
||||||
'exclamation-circle'
|
|
||||||
)
|
|
||||||
console.error("can't load highlight js", error)
|
console.error("can't load highlight js", error)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { showErrorNotification } from '../../../redux/ui-notifications/methods'
|
||||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
|
|
||||||
export const PinNoteSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
|
export const PinNoteSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
|
||||||
const { t } = useTranslation()
|
useTranslation()
|
||||||
const { id } = useParams<EditorPagePathParams>()
|
const { id } = useParams<EditorPagePathParams>()
|
||||||
const history = useApplicationState((state) => state.history)
|
const history = useApplicationState((state) => state.history)
|
||||||
|
|
||||||
|
@ -28,8 +28,8 @@ export const PinNoteSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ class
|
||||||
}, [id, history])
|
}, [id, history])
|
||||||
|
|
||||||
const onPinClicked = useCallback(() => {
|
const onPinClicked = useCallback(() => {
|
||||||
toggleHistoryEntryPinning(id).catch(showErrorNotification(t('landing.history.error.updateEntry.text')))
|
toggleHistoryEntryPinning(id).catch(showErrorNotification('landing.history.error.updateEntry.text'))
|
||||||
}, [id, t])
|
}, [id])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarButton
|
<SidebarButton
|
||||||
|
|
|
@ -5,23 +5,29 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { DEFAULT_DURATION_IN_SECONDS, dispatchUiNotification } from '../../redux/ui-notifications/methods'
|
import { dispatchUiNotification } from '../../redux/ui-notifications/methods'
|
||||||
|
|
||||||
const localStorageKey = 'dontshowtestnotification'
|
const localStorageKey = 'dontshowtestnotification'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawns a notification to test the system. Only for tech demo show case.
|
||||||
|
*/
|
||||||
export const useNotificationTest = (): void => {
|
export const useNotificationTest = (): void => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (window.localStorage.getItem(localStorageKey)) {
|
if (window.localStorage.getItem(localStorageKey)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.debug('[Notifications] Dispatched test notification')
|
console.debug('[Notifications] Dispatched test notification')
|
||||||
dispatchUiNotification('Notification-Test!', 'It Works!', DEFAULT_DURATION_IN_SECONDS, 'info-circle', [
|
void dispatchUiNotification('notificationTest.title', 'notificationTest.content', {
|
||||||
{
|
icon: 'info-circle',
|
||||||
label: "Don't show again",
|
buttons: [
|
||||||
onClick: () => {
|
{
|
||||||
window.localStorage.setItem(localStorageKey, '1')
|
label: "Don't show again",
|
||||||
|
onClick: () => {
|
||||||
|
window.localStorage.setItem(localStorageKey, '1')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
])
|
})
|
||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,33 +40,23 @@ export interface HistoryEntriesProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HistoryContent: React.FC<HistoryContentProps> = ({ viewState, entries }) => {
|
export const HistoryContent: React.FC<HistoryContentProps> = ({ viewState, entries }) => {
|
||||||
const { t } = useTranslation()
|
useTranslation()
|
||||||
|
|
||||||
const [pageIndex, setPageIndex] = useState(0)
|
const [pageIndex, setPageIndex] = useState(0)
|
||||||
const [lastPageIndex, setLastPageIndex] = useState(0)
|
const [lastPageIndex, setLastPageIndex] = useState(0)
|
||||||
|
|
||||||
const onPinClick = useCallback(
|
const onPinClick = useCallback((noteId: string) => {
|
||||||
(noteId: string) => {
|
toggleHistoryEntryPinning(noteId).catch(showErrorNotification('landing.history.error.updateEntry.text'))
|
||||||
toggleHistoryEntryPinning(noteId).catch(showErrorNotification(t('landing.history.error.updateEntry.text')))
|
}, [])
|
||||||
},
|
|
||||||
[t]
|
|
||||||
)
|
|
||||||
|
|
||||||
const onDeleteClick = useCallback(
|
const onDeleteClick = useCallback((noteId: string) => {
|
||||||
(noteId: string) => {
|
deleteNote(noteId)
|
||||||
deleteNote(noteId)
|
.then(() => removeHistoryEntry(noteId))
|
||||||
.then(() => removeHistoryEntry(noteId))
|
.catch(showErrorNotification('landing.history.error.deleteNote.text'))
|
||||||
.catch(showErrorNotification(t('landing.history.error.deleteNote.text')))
|
}, [])
|
||||||
},
|
|
||||||
[t]
|
|
||||||
)
|
|
||||||
|
|
||||||
const onRemoveClick = useCallback(
|
const onRemoveClick = useCallback((noteId: string) => {
|
||||||
(noteId: string) => {
|
removeHistoryEntry(noteId).catch(showErrorNotification('landing.history.error.deleteEntry.text'))
|
||||||
removeHistoryEntry(noteId).catch(showErrorNotification(t('landing.history.error.deleteEntry.text')))
|
}, [])
|
||||||
},
|
|
||||||
[t]
|
|
||||||
)
|
|
||||||
|
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { showErrorNotification } from '../../redux/ui-notifications/methods'
|
||||||
import { useApplicationState } from '../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../hooks/common/use-application-state'
|
||||||
|
|
||||||
export const HistoryPage: React.FC = () => {
|
export const HistoryPage: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
useTranslation()
|
||||||
|
|
||||||
const allEntries = useApplicationState((state) => state.history)
|
const allEntries = useApplicationState((state) => state.history)
|
||||||
const [toolbarState, setToolbarState] = useState<HistoryToolbarState>(initToolbarState)
|
const [toolbarState, setToolbarState] = useState<HistoryToolbarState>(initToolbarState)
|
||||||
|
@ -27,8 +27,8 @@ export const HistoryPage: React.FC = () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshHistoryState().catch(showErrorNotification(t('landing.history.error.getHistory.text')))
|
refreshHistoryState().catch(showErrorNotification('landing.history.error.getHistory.text'))
|
||||||
}, [t])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
|
|
@ -21,11 +21,11 @@ export const ClearHistoryButton: React.FC = () => {
|
||||||
|
|
||||||
const onConfirm = useCallback(() => {
|
const onConfirm = useCallback(() => {
|
||||||
deleteAllHistoryEntries().catch((error) => {
|
deleteAllHistoryEntries().catch((error) => {
|
||||||
showErrorNotification(t('landing.history.error.deleteEntry.text'))(error)
|
showErrorNotification('landing.history.error.deleteEntry.text')(error)
|
||||||
refreshHistoryState().catch(showErrorNotification(t('landing.history.error.getHistory.text')))
|
refreshHistoryState().catch(showErrorNotification('landing.history.error.getHistory.text'))
|
||||||
})
|
})
|
||||||
handleClose()
|
handleClose()
|
||||||
}, [t])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
|
|
@ -116,8 +116,8 @@ export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange
|
||||||
)
|
)
|
||||||
|
|
||||||
const refreshHistory = useCallback(() => {
|
const refreshHistory = useCallback(() => {
|
||||||
refreshHistoryState().catch(showErrorNotification(t('landing.history.error.getHistory.text')))
|
refreshHistoryState().catch(showErrorNotification('landing.history.error.getHistory.text'))
|
||||||
}, [t])
|
}, [])
|
||||||
|
|
||||||
const onUploadAllToRemote = useCallback(() => {
|
const onUploadAllToRemote = useCallback(() => {
|
||||||
if (!userExists) {
|
if (!userExists) {
|
||||||
|
@ -128,7 +128,7 @@ export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange
|
||||||
.map((entry) => entry.identifier)
|
.map((entry) => entry.identifier)
|
||||||
historyEntries.forEach((entry) => (entry.origin = HistoryEntryOrigin.REMOTE))
|
historyEntries.forEach((entry) => (entry.origin = HistoryEntryOrigin.REMOTE))
|
||||||
importHistoryEntries(historyEntries).catch((error) => {
|
importHistoryEntries(historyEntries).catch((error) => {
|
||||||
showErrorNotification(t('landing.history.error.setHistory.text'))(error)
|
showErrorNotification('landing.history.error.setHistory.text')(error)
|
||||||
historyEntries.forEach((entry) => {
|
historyEntries.forEach((entry) => {
|
||||||
if (localEntries.includes(entry.identifier)) {
|
if (localEntries.includes(entry.identifier)) {
|
||||||
entry.origin = HistoryEntryOrigin.LOCAL
|
entry.origin = HistoryEntryOrigin.LOCAL
|
||||||
|
@ -137,7 +137,7 @@ export const HistoryToolbar: React.FC<HistoryToolbarProps> = ({ onSettingsChange
|
||||||
setHistoryEntries(historyEntries)
|
setHistoryEntries(historyEntries)
|
||||||
refreshHistory()
|
refreshHistory()
|
||||||
})
|
})
|
||||||
}, [userExists, historyEntries, t, refreshHistory])
|
}, [userExists, historyEntries, refreshHistory])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newState: HistoryToolbarState = {
|
const newState: HistoryToolbarState = {
|
||||||
|
|
|
@ -42,11 +42,11 @@ export const ImportHistoryButton: React.FC = () => {
|
||||||
(entries: HistoryEntry[]): void => {
|
(entries: HistoryEntry[]): void => {
|
||||||
entries.forEach((entry) => (entry.origin = userExists ? HistoryEntryOrigin.REMOTE : HistoryEntryOrigin.LOCAL))
|
entries.forEach((entry) => (entry.origin = userExists ? HistoryEntryOrigin.REMOTE : HistoryEntryOrigin.LOCAL))
|
||||||
importHistoryEntries(mergeHistoryEntries(historyState, entries)).catch((error) => {
|
importHistoryEntries(mergeHistoryEntries(historyState, entries)).catch((error) => {
|
||||||
showErrorNotification(t('landing.history.error.setHistory.text'))(error)
|
showErrorNotification('landing.history.error.setHistory.text')(error)
|
||||||
refreshHistoryState().catch(showErrorNotification(t('landing.history.error.getHistory.text')))
|
refreshHistoryState().catch(showErrorNotification('landing.history.error.getHistory.text'))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[historyState, userExists, t]
|
[historyState, userExists]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { ForkAwesomeIcon } from '../common/fork-awesome/fork-awesome-icon'
|
||||||
import { ShowIf } from '../common/show-if/show-if'
|
import { ShowIf } from '../common/show-if/show-if'
|
||||||
import { IconName } from '../common/fork-awesome/types'
|
import { IconName } from '../common/fork-awesome/types'
|
||||||
import { dismissUiNotification } from '../../redux/ui-notifications/methods'
|
import { dismissUiNotification } from '../../redux/ui-notifications/methods'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const STEPS_PER_SECOND = 10
|
const STEPS_PER_SECOND = 10
|
||||||
|
|
||||||
|
@ -19,8 +20,10 @@ export interface UiNotificationProps extends UiNotification {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UiNotificationToast: React.FC<UiNotificationProps> = ({
|
export const UiNotificationToast: React.FC<UiNotificationProps> = ({
|
||||||
title,
|
titleI18nKey,
|
||||||
content,
|
contentI18nKey,
|
||||||
|
titleI18nOptions,
|
||||||
|
contentI18nOptions,
|
||||||
date,
|
date,
|
||||||
icon,
|
icon,
|
||||||
dismissed,
|
dismissed,
|
||||||
|
@ -28,6 +31,7 @@ export const UiNotificationToast: React.FC<UiNotificationProps> = ({
|
||||||
durationInSecond,
|
durationInSecond,
|
||||||
buttons
|
buttons
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const [eta, setEta] = useState<number>()
|
const [eta, setEta] = useState<number>()
|
||||||
const interval = useRef<NodeJS.Timeout | undefined>(undefined)
|
const interval = useRef<NodeJS.Timeout | undefined>(undefined)
|
||||||
|
|
||||||
|
@ -89,15 +93,17 @@ export const UiNotificationToast: React.FC<UiNotificationProps> = ({
|
||||||
)
|
)
|
||||||
|
|
||||||
const contentDom = useMemo(() => {
|
const contentDom = useMemo(() => {
|
||||||
return content.split('\n').map((value, lineNumber) => {
|
return t(contentI18nKey, contentI18nOptions)
|
||||||
return (
|
.split('\n')
|
||||||
<Fragment key={lineNumber}>
|
.map((value, lineNumber) => {
|
||||||
{value}
|
return (
|
||||||
<br />
|
<Fragment key={lineNumber}>
|
||||||
</Fragment>
|
{value}
|
||||||
)
|
<br />
|
||||||
})
|
</Fragment>
|
||||||
}, [content])
|
)
|
||||||
|
})
|
||||||
|
}, [contentI18nKey, contentI18nOptions, t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Toast show={!dismissed && eta !== undefined} onClose={dismissThisNotification}>
|
<Toast show={!dismissed && eta !== undefined} onClose={dismissThisNotification}>
|
||||||
|
@ -106,7 +112,7 @@ export const UiNotificationToast: React.FC<UiNotificationProps> = ({
|
||||||
<ShowIf condition={!!icon}>
|
<ShowIf condition={!!icon}>
|
||||||
<ForkAwesomeIcon icon={icon as IconName} fixedWidth={true} className={'mr-1'} />
|
<ForkAwesomeIcon icon={icon as IconName} fixedWidth={true} className={'mr-1'} />
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
{title}
|
<Trans i18nKey={titleI18nKey} tOptions={titleI18nOptions} />
|
||||||
</strong>
|
</strong>
|
||||||
<small>{date.toRelative({ style: 'short' })}</small>
|
<small>{date.toRelative({ style: 'short' })}</small>
|
||||||
</Toast.Header>
|
</Toast.Header>
|
||||||
|
|
|
@ -4,40 +4,56 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import i18n from 'i18next'
|
import i18n, { TOptions } from 'i18next'
|
||||||
import { store } from '../index'
|
import { store } from '../index'
|
||||||
import {
|
import { DismissUiNotificationAction, DispatchOptions, UiNotificationActionType } from './types'
|
||||||
DismissUiNotificationAction,
|
|
||||||
DispatchUiNotificationAction,
|
|
||||||
UiNotificationActionType,
|
|
||||||
UiNotificationButton
|
|
||||||
} from './types'
|
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
import { IconName } from '../../components/common/fork-awesome/types'
|
|
||||||
|
|
||||||
export const DEFAULT_DURATION_IN_SECONDS = 10
|
export const DEFAULT_DURATION_IN_SECONDS = 10
|
||||||
|
|
||||||
export const dispatchUiNotification = (
|
/**
|
||||||
title: string,
|
* Dispatches a new UI Notification into the global application state.
|
||||||
content: string,
|
*
|
||||||
durationInSecond = DEFAULT_DURATION_IN_SECONDS,
|
* @param titleI18nKey I18n key used to show the localized title
|
||||||
icon?: IconName,
|
* @param contentI18nKey I18n key used to show the localized content
|
||||||
buttons?: UiNotificationButton[]
|
* @param icon The icon in the upper left corner
|
||||||
): void => {
|
* @param durationInSecond Show duration of the notification. If omitted then a {@link DEFAULT_DURATION_IN_SECONDS default value} will be used.
|
||||||
store.dispatch({
|
* @param buttons A array of actions that are shown in the notification
|
||||||
type: UiNotificationActionType.DISPATCH_NOTIFICATION,
|
* @param contentI18nOptions Options to configure the translation of the title. (e.g. variables)
|
||||||
notification: {
|
* @param titleI18nOptions Options to configure the translation of the content. (e.g. variables)
|
||||||
title,
|
* @return a promise that resolves as soon as the notification id available.
|
||||||
content,
|
*/
|
||||||
date: DateTime.now(),
|
export const dispatchUiNotification = async (
|
||||||
dismissed: false,
|
titleI18nKey: string,
|
||||||
icon,
|
contentI18nKey: string,
|
||||||
durationInSecond,
|
{ icon, durationInSecond, buttons, contentI18nOptions, titleI18nOptions }: Partial<DispatchOptions>
|
||||||
buttons: buttons
|
): Promise<number> => {
|
||||||
}
|
return new Promise((resolve) => {
|
||||||
} as DispatchUiNotificationAction)
|
store.dispatch({
|
||||||
|
type: UiNotificationActionType.DISPATCH_NOTIFICATION,
|
||||||
|
notificationIdCallback: (notificationId: number) => {
|
||||||
|
resolve(notificationId)
|
||||||
|
},
|
||||||
|
notification: {
|
||||||
|
titleI18nKey,
|
||||||
|
contentI18nKey,
|
||||||
|
date: DateTime.now(),
|
||||||
|
dismissed: false,
|
||||||
|
titleI18nOptions: titleI18nOptions ?? {},
|
||||||
|
contentI18nOptions: contentI18nOptions ?? {},
|
||||||
|
durationInSecond: durationInSecond ?? DEFAULT_DURATION_IN_SECONDS,
|
||||||
|
buttons: buttons ?? [],
|
||||||
|
icon: icon
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismisses a notification. It won't be removed from the global application state but hidden.
|
||||||
|
*
|
||||||
|
* @param notificationId The id of the notification to dismissed. Can be obtained from the returned promise of {@link dispatchUiNotification}
|
||||||
|
*/
|
||||||
export const dismissUiNotification = (notificationId: number): void => {
|
export const dismissUiNotification = (notificationId: number): void => {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: UiNotificationActionType.DISMISS_NOTIFICATION,
|
type: UiNotificationActionType.DISMISS_NOTIFICATION,
|
||||||
|
@ -45,9 +61,18 @@ export const dismissUiNotification = (notificationId: number): void => {
|
||||||
} as DismissUiNotificationAction)
|
} as DismissUiNotificationAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches an notification that is specialized for errors.
|
||||||
|
*
|
||||||
|
* @param messageI18nKey i18n key for the message
|
||||||
|
* @param messageI18nOptions i18n options for the message
|
||||||
|
*/
|
||||||
export const showErrorNotification =
|
export const showErrorNotification =
|
||||||
(message: string) =>
|
(messageI18nKey: string, messageI18nOptions?: TOptions | string) =>
|
||||||
(error: Error): void => {
|
(error: Error): void => {
|
||||||
console.error(message, error)
|
console.error(i18n.t(messageI18nKey, messageI18nOptions), error)
|
||||||
dispatchUiNotification(i18n.t('common.errorOccurred'), message, DEFAULT_DURATION_IN_SECONDS, 'exclamation-triangle')
|
void dispatchUiNotification('common.errorOccurred', messageI18nKey, {
|
||||||
|
contentI18nOptions: messageI18nOptions,
|
||||||
|
icon: 'exclamation-triangle'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Reducer } from 'redux'
|
import { Reducer } from 'redux'
|
||||||
import { UiNotificationActions, UiNotificationActionType, UiNotificationState } from './types'
|
import { UiNotification, UiNotificationActions, UiNotificationActionType, UiNotificationState } from './types'
|
||||||
|
|
||||||
export const UiNotificationReducer: Reducer<UiNotificationState, UiNotificationActions> = (
|
export const UiNotificationReducer: Reducer<UiNotificationState, UiNotificationActions> = (
|
||||||
state: UiNotificationState = [],
|
state: UiNotificationState = [],
|
||||||
|
@ -13,7 +13,7 @@ export const UiNotificationReducer: Reducer<UiNotificationState, UiNotificationA
|
||||||
) => {
|
) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case UiNotificationActionType.DISPATCH_NOTIFICATION:
|
case UiNotificationActionType.DISPATCH_NOTIFICATION:
|
||||||
return state.concat(action.notification)
|
return addNewNotification(state, action.notification, action.notificationIdCallback)
|
||||||
case UiNotificationActionType.DISMISS_NOTIFICATION:
|
case UiNotificationActionType.DISMISS_NOTIFICATION:
|
||||||
return dismissNotification(state, action.notificationId)
|
return dismissNotification(state, action.notificationId)
|
||||||
default:
|
default:
|
||||||
|
@ -21,6 +21,23 @@ export const UiNotificationReducer: Reducer<UiNotificationState, UiNotificationA
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link UiNotificationState notification state} by appending the given {@link UiNotification}.
|
||||||
|
* @param state The current ui notification state
|
||||||
|
* @param notification The new notification
|
||||||
|
* @param notificationIdCallback This callback is executed with the id of the new notification
|
||||||
|
* @return The new {@link UiNotificationState notification state}
|
||||||
|
*/
|
||||||
|
const addNewNotification = (
|
||||||
|
state: UiNotificationState,
|
||||||
|
notification: UiNotification,
|
||||||
|
notificationIdCallback: (notificationId: number) => void
|
||||||
|
): UiNotificationState => {
|
||||||
|
const newState = [...state, notification]
|
||||||
|
notificationIdCallback(newState.length - 1)
|
||||||
|
return newState
|
||||||
|
}
|
||||||
|
|
||||||
const dismissNotification = (
|
const dismissNotification = (
|
||||||
notificationState: UiNotificationState,
|
notificationState: UiNotificationState,
|
||||||
notificationIndex: number
|
notificationIndex: number
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import { Action } from 'redux'
|
import { Action } from 'redux'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
import { IconName } from '../../components/common/fork-awesome/types'
|
import { IconName } from '../../components/common/fork-awesome/types'
|
||||||
|
import { TOptions } from 'i18next'
|
||||||
|
|
||||||
export enum UiNotificationActionType {
|
export enum UiNotificationActionType {
|
||||||
DISPATCH_NOTIFICATION = 'notification/dispatch',
|
DISPATCH_NOTIFICATION = 'notification/dispatch',
|
||||||
|
@ -18,14 +19,19 @@ export interface UiNotificationButton {
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UiNotification {
|
export interface DispatchOptions {
|
||||||
title: string
|
titleI18nOptions: TOptions | string
|
||||||
date: DateTime
|
contentI18nOptions: TOptions | string
|
||||||
content: string
|
|
||||||
dismissed: boolean
|
|
||||||
icon?: IconName
|
|
||||||
durationInSecond: number
|
durationInSecond: number
|
||||||
buttons?: UiNotificationButton[]
|
icon?: IconName
|
||||||
|
buttons: UiNotificationButton[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UiNotification extends DispatchOptions {
|
||||||
|
titleI18nKey: string
|
||||||
|
contentI18nKey: string
|
||||||
|
date: DateTime
|
||||||
|
dismissed: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UiNotificationActions = DispatchUiNotificationAction | DismissUiNotificationAction
|
export type UiNotificationActions = DispatchUiNotificationAction | DismissUiNotificationAction
|
||||||
|
@ -33,6 +39,7 @@ export type UiNotificationActions = DispatchUiNotificationAction | DismissUiNoti
|
||||||
export interface DispatchUiNotificationAction extends Action<UiNotificationActionType> {
|
export interface DispatchUiNotificationAction extends Action<UiNotificationActionType> {
|
||||||
type: UiNotificationActionType.DISPATCH_NOTIFICATION
|
type: UiNotificationActionType.DISPATCH_NOTIFICATION
|
||||||
notification: UiNotification
|
notification: UiNotification
|
||||||
|
notificationIdCallback: (notificationId: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DismissUiNotificationAction extends Action<UiNotificationActionType> {
|
export interface DismissUiNotificationAction extends Action<UiNotificationActionType> {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue