mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-29 06:15:29 -04:00
Add toasts (#1073)
Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
parent
0b4a0afa16
commit
a86789dbef
11 changed files with 320 additions and 13 deletions
|
@ -32,6 +32,8 @@ import { Splitter } from './splitter/splitter'
|
|||
import { DualScrollState, ScrollState } from './synced-scroll/scroll-props'
|
||||
import { RendererType } from '../render-page/rendering-message'
|
||||
import { useEditorModeFromUrl } from './hooks/useEditorModeFromUrl'
|
||||
import { UiNotifications } from '../notifications/ui-notifications'
|
||||
import { useNotificationTest } from './use-notification-test'
|
||||
|
||||
export interface EditorPagePathParams {
|
||||
id: string
|
||||
|
@ -82,8 +84,11 @@ export const EditorPage: React.FC = () => {
|
|||
scrollSource.current = ScrollSource.EDITOR
|
||||
}, [])
|
||||
|
||||
useNotificationTest()
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<UiNotifications/>
|
||||
<MotdBanner/>
|
||||
<div className={ 'd-flex flex-column vh-100' }>
|
||||
<AppBar mode={ AppBarMode.EDITOR }/>
|
||||
|
|
24
src/components/editor-page/use-notification-test.tsx
Normal file
24
src/components/editor-page/use-notification-test.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { DEFAULT_DURATION_IN_SECONDS, dispatchUiNotification } from '../../redux/ui-notifications/methods'
|
||||
|
||||
const localStorageKey = 'dontshowtestnotification'
|
||||
|
||||
export const useNotificationTest = (): void => {
|
||||
useEffect(() => {
|
||||
if (window.localStorage.getItem(localStorageKey)) {
|
||||
return
|
||||
}
|
||||
console.debug('[Notifications] Dispatched test notification')
|
||||
dispatchUiNotification('Notification-Test!', 'It Works!', DEFAULT_DURATION_IN_SECONDS, 'info-circle', [{
|
||||
label: 'Don\'t show again', onClick: () => {
|
||||
window.localStorage.setItem(localStorageKey, '1')
|
||||
}
|
||||
}])
|
||||
}, [])
|
||||
}
|
|
@ -4,26 +4,30 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import React, { Fragment } from 'react'
|
||||
import { Container } from 'react-bootstrap'
|
||||
import { useDocumentTitle } from '../../hooks/common/use-document-title'
|
||||
import { MotdBanner } from '../common/motd-banner/motd-banner'
|
||||
import { Footer } from './footer/footer'
|
||||
import { HeaderBar } from './navigation/header-bar/header-bar'
|
||||
import { UiNotifications } from '../notifications/ui-notifications'
|
||||
|
||||
export const LandingLayout: React.FC = ({ children }) => {
|
||||
useDocumentTitle()
|
||||
|
||||
return (
|
||||
<Container className="text-light d-flex flex-column mvh-100">
|
||||
<MotdBanner/>
|
||||
<HeaderBar/>
|
||||
<div className={ 'd-flex flex-column justify-content-between flex-fill text-center' }>
|
||||
<main>
|
||||
{ children }
|
||||
</main>
|
||||
<Footer/>
|
||||
</div>
|
||||
</Container>
|
||||
<Fragment>
|
||||
<UiNotifications/>
|
||||
<Container className="text-light d-flex flex-column mvh-100">
|
||||
<MotdBanner/>
|
||||
<HeaderBar/>
|
||||
<div className={ 'd-flex flex-column justify-content-between flex-fill text-center' }>
|
||||
<main>
|
||||
{ children }
|
||||
</main>
|
||||
<Footer/>
|
||||
</div>
|
||||
</Container>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
|
22
src/components/notifications/notifications.scss
Normal file
22
src/components/notifications/notifications.scss
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
.notifications-area {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
z-index: 2000;
|
||||
|
||||
.toast {
|
||||
min-width: 250px;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
.progress {
|
||||
border-radius: 0;
|
||||
height: 0.25rem;
|
||||
}
|
||||
}
|
92
src/components/notifications/ui-notification-toast.tsx
Normal file
92
src/components/notifications/ui-notification-toast.tsx
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Button, ProgressBar, Toast } from 'react-bootstrap'
|
||||
import { UiNotification } from '../../redux/ui-notifications/types'
|
||||
import { ForkAwesomeIcon } from '../common/fork-awesome/fork-awesome-icon'
|
||||
import { ShowIf } from '../common/show-if/show-if'
|
||||
import { IconName } from '../common/fork-awesome/types'
|
||||
import { dismissUiNotification } from '../../redux/ui-notifications/methods'
|
||||
|
||||
const STEPS_PER_SECOND = 10
|
||||
|
||||
export interface UiNotificationProps extends UiNotification {
|
||||
notificationId: number
|
||||
}
|
||||
|
||||
export const UiNotificationToast: React.FC<UiNotificationProps> = ({ title, content, date, icon, dismissed, notificationId, durationInSecond, buttons }) => {
|
||||
const [eta, setEta] = useState<number>()
|
||||
const interval = useRef<NodeJS.Timeout | undefined>(undefined)
|
||||
|
||||
const deleteInterval = useCallback(() => {
|
||||
if (interval.current) {
|
||||
clearInterval(interval.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const dismissThisNotification = useCallback(() => {
|
||||
console.debug(`[Notifications] Dismissed notification ${ notificationId }`)
|
||||
dismissUiNotification(notificationId)
|
||||
}, [notificationId])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (dismissed || !!interval.current) {
|
||||
return
|
||||
}
|
||||
console.debug(`[Notifications] Show notification ${ notificationId }`)
|
||||
setEta(durationInSecond * STEPS_PER_SECOND)
|
||||
interval.current = setInterval(() => setEta((lastETA) => {
|
||||
if (lastETA === undefined) {
|
||||
return
|
||||
} else if (lastETA <= 0) {
|
||||
return 0
|
||||
} else {
|
||||
return lastETA - 1
|
||||
}
|
||||
}), 1000 / STEPS_PER_SECOND)
|
||||
return () => {
|
||||
deleteInterval()
|
||||
}
|
||||
}, [deleteInterval, dismissThisNotification, dismissed, durationInSecond, notificationId])
|
||||
|
||||
useEffect(() => {
|
||||
if (eta === 0) {
|
||||
dismissThisNotification()
|
||||
}
|
||||
}, [dismissThisNotification, eta])
|
||||
|
||||
const buttonsDom = useMemo(() => buttons?.map(button => {
|
||||
const buttonClick = () => {
|
||||
button.onClick()
|
||||
dismissThisNotification()
|
||||
}
|
||||
return <Button key={ button.label } size={ 'sm' } onClick={ buttonClick }
|
||||
variant={ 'link' }>{ button.label }</Button>
|
||||
}
|
||||
), [buttons, dismissThisNotification])
|
||||
|
||||
return (
|
||||
<Toast show={ !dismissed && eta !== undefined } onClose={ dismissThisNotification }>
|
||||
<Toast.Header>
|
||||
<strong className="mr-auto">
|
||||
<ShowIf condition={ !!icon }>
|
||||
<ForkAwesomeIcon icon={ icon as IconName } fixedWidth={ true } className={ 'mr-1' }/>
|
||||
</ShowIf>
|
||||
{ title }
|
||||
</strong>
|
||||
<small>{ date.toRelative({ style: 'short' }) }</small>
|
||||
</Toast.Header>
|
||||
<Toast.Body>{ content }</Toast.Body>
|
||||
<ProgressBar variant={ 'info' } now={ eta } max={ durationInSecond * STEPS_PER_SECOND } min={ 0 }/>
|
||||
<div>
|
||||
{
|
||||
buttonsDom
|
||||
}
|
||||
</div>
|
||||
</Toast>
|
||||
)
|
||||
}
|
34
src/components/notifications/ui-notifications.tsx
Normal file
34
src/components/notifications/ui-notifications.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { UiNotificationToast } from './ui-notification-toast'
|
||||
import './notifications.scss'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { ApplicationState } from '../../redux'
|
||||
import equal from 'fast-deep-equal'
|
||||
|
||||
export const UiNotifications: React.FC = () => {
|
||||
const notifications = useSelector((state: ApplicationState) => state.uiNotifications, equal)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ 'notifications-area' }
|
||||
aria-live="polite"
|
||||
aria-atomic="true">
|
||||
{
|
||||
notifications.map((notification, notificationIndex) =>
|
||||
<UiNotificationToast
|
||||
key={ notificationIndex }
|
||||
notificationId={ notificationIndex }
|
||||
{ ...notification }/>)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue