@rpcbase/client 0.178.0 → 0.180.0-notifications.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/AppProvider/index.js +8 -4
  2. package/auth/components/SignOut/index.js +11 -1
  3. package/notifications/Notification/index.js +36 -0
  4. package/notifications/Notification/notification.scss +1 -0
  5. package/notifications/NotificationToast/HeaderStatus.js +93 -0
  6. package/notifications/NotificationToast/index.js +67 -0
  7. package/notifications/NotificationToast/notification-toast.scss +25 -0
  8. package/notifications/NotificationsContext/index.js +84 -0
  9. package/notifications/NotificationsContext/useNotificationsList.js +75 -0
  10. package/notifications/NotificationsList/index.js +44 -0
  11. package/notifications/NotificationsList/notifications.scss +33 -0
  12. package/notifications/NotificationsList/useLLTs.js +28 -0
  13. package/notifications/NotificationsSettingsModal/SettingsForm.js +52 -0
  14. package/notifications/NotificationsSettingsModal/index.js +48 -0
  15. package/notifications/NotificationsSettingsModal/notifications-settings.scss +1 -0
  16. package/notifications/config.js +1 -0
  17. package/notifications/index.js +4 -0
  18. package/package.json +4 -1
  19. package/rts/getUseQuery/index.js +1 -2
  20. package/rts/rts.js +2 -0
  21. package/ui/ActivityIndicator/index.js +113 -0
  22. package/ui/LottiePlayer/LottiePlayer.js +4 -0
  23. package/ui/LottiePlayer/index.js +8 -0
  24. package/ui/Modal/HashStateModal.js +32 -0
  25. package/ui/Modal/Modal.js +97 -0
  26. package/ui/Modal/ModalForm/AlertBanner.js +83 -0
  27. package/ui/Modal/ModalForm/index.js +193 -0
  28. package/ui/Modal/ModalForm/modal-form.scss +63 -0
  29. package/ui/Modal/index.js +10 -0
  30. package/ui/Modal/modal.scss +101 -0
  31. package/ui/Modal/withHashStateModal.js +24 -0
  32. package/ui/SubmitButton/index.js +32 -0
  33. package/ui/animations/checkmark.json +1 -0
  34. package/ui/springs.js +17 -0
  35. package/ui/withSuspense/index.js +40 -0
@@ -13,6 +13,7 @@ import {flagValues} from "config/flags"
13
13
 
14
14
  import {POSTHOG_KEY} from "env"
15
15
  import { useAuthRouter } from "../auth/useAuthRouter"
16
+ import { NotificationsProvider, NotificationsList } from "../notifications"
16
17
 
17
18
 
18
19
  const PostHogWrapper = ({children, ...props}) => {
@@ -56,10 +57,13 @@ const AppProvider = ({children, ...props}) => {
56
57
  return (
57
58
  <PostHogWrapper>
58
59
  <HashStateProvider>
59
- <div className={cx({"d-none": !!authComponent})}>
60
- {children}
61
- </div>
62
- {authComponent}
60
+ <NotificationsProvider>
61
+ <div className={cx({"d-none": !!authComponent})}>
62
+ {children}
63
+ </div>
64
+ {authComponent}
65
+ <NotificationsList />
66
+ </NotificationsProvider>
63
67
  </HashStateProvider>
64
68
  </PostHogWrapper>
65
69
  )
@@ -14,6 +14,7 @@ import {AccountListItem} from "../AccountsList/AccountListItem"
14
14
  import {Footer} from "../Footer"
15
15
 
16
16
  import "./sign-out.scss"
17
+ import { useNotifications } from "../../../notifications"
17
18
 
18
19
  // TODO: rts_disconnect
19
20
  // TODO: clear cache + db
@@ -26,6 +27,8 @@ export const SignOut = ({
26
27
  }) => {
27
28
  const {t} = useTranslation("rb.sign_out", {useSuspense: false})
28
29
 
30
+ const {addToast} = useNotifications()
31
+
29
32
  const [isSignedOut, setIsSignedOut] = useState(false)
30
33
 
31
34
  const [accounts, setAccounts] = useState()
@@ -44,13 +47,20 @@ export const SignOut = ({
44
47
  load()
45
48
  }, [])
46
49
 
47
-
48
50
  const onSignOut = async() => {
49
51
  // TODO: NYI handle sign out from multiple accounts here
50
52
  const res = await post("/api/v1/auth/sign_out")
51
53
  if (res.status === "ok") {
52
54
  signOut()
53
55
  setIsSignedOut(true)
56
+
57
+ const toast = addToast({
58
+ // id: `create-template-${Date.now()}`,
59
+ loading: false,
60
+ title: "Signed out",
61
+ message: "You have been signed out click here to sign back in",
62
+ })
63
+
54
64
  onSignOutSuccess()
55
65
  } else {
56
66
  throw new Error("unable to sign out")
@@ -0,0 +1,36 @@
1
+ /* @flow */
2
+ import * as React from "react"
3
+ import { useCallback } from "react";
4
+
5
+ import type {Notification as NotificationType} from "types/notification"
6
+
7
+ import "./notification.scss"
8
+
9
+ type Props = {
10
+ notification: NotificationType,
11
+ onClick?: () => void,
12
+ }
13
+
14
+ const Notification = ({ ref, notification, onClick }: Props) => {
15
+ const handleClick = useCallback(() => {
16
+ if (onClick) {
17
+ onClick();
18
+ }
19
+ }, [onClick]);
20
+
21
+ return (
22
+ <a
23
+ className={cx(["dropdown-item", notification.ack && "disabled"])}
24
+ href="#"
25
+ ref={ref}
26
+ onClick={handleClick}
27
+ >
28
+ <span>{notification.title}</span>
29
+ <p className="text-secondary mb-0" style={{whiteSpace: "initial"}}>
30
+ {notification.description}
31
+ </p>
32
+ </a>
33
+ );
34
+ }
35
+
36
+ export default Notification
@@ -0,0 +1 @@
1
+ @import "helpers";
@@ -0,0 +1,93 @@
1
+ /* @flow */
2
+ import {motion} from "framer-motion"
3
+ import {useEffect, useRef, useState} from "react"
4
+
5
+ import ActivityIndicator from "../../ui/ActivityIndicator"
6
+ import LottiePlayer from "../../ui/LottiePlayer"
7
+
8
+ import {SPRING_BOUNCY} from "../../ui/springs"
9
+
10
+ import checkmarkAnimation from "../../ui/animations/checkmark.json"
11
+
12
+ // TODO: we should have a way to unmount loader when animation complete
13
+ const HeaderStatus = ({isLoading, status}) => {
14
+ const playerRef = useRef()
15
+
16
+ const initiallyLoading = useRef(isLoading)
17
+
18
+ const [hasLoader, setHasLoader] = useState(isLoading)
19
+
20
+ const [statusAnimatedVals, setStatusAnimatedVals] = useState({})
21
+ const [loaderAnimatedVals, setLoaderAnimatedVals] = useState({})
22
+
23
+ useEffect(() => {
24
+ // set initial seeker position, TODO: this doesn't work without the animation frame
25
+ requestAnimationFrame(() => {
26
+ playerRef.current?.setSeeker(16, false)
27
+ })
28
+ }, [])
29
+
30
+ const runTransition = () => {
31
+ setStatusAnimatedVals({scale: 1, opacity: 1})
32
+ setLoaderAnimatedVals({scale: 0, opacity: 0})
33
+ requestAnimationFrame(() => {
34
+ playerRef.current?.play()
35
+ })
36
+ }
37
+
38
+ useEffect(() => {
39
+ // TODO: why was it checking on initiallyLoading.current ?
40
+ // if (!isLoading && initiallyLoading.current) {
41
+ if (!isLoading) {
42
+ runTransition()
43
+ }
44
+ }, [isLoading])
45
+
46
+ const onLoaderAnimationComplete = () => {
47
+ setHasLoader(false)
48
+ }
49
+
50
+ return (
51
+ <div style={{position: "relative", background: "red", width: 28}}>
52
+ <motion.div
53
+ className="status-icon"
54
+ style={{position: "absolute", top: -11}}
55
+ initial={{scale: 0.5, opacity: 0}}
56
+ // initial={{scale: 1, opacity: 1}}
57
+ transition={SPRING_BOUNCY}
58
+ animate={statusAnimatedVals}
59
+ >
60
+ <LottiePlayer
61
+ className={cx("", {})}
62
+ ref={playerRef}
63
+ autoplay={false}
64
+ loop={false}
65
+ speed={1}
66
+ keepLastFrame={true}
67
+ src={checkmarkAnimation}
68
+ style={{
69
+ height: `${23}px`,
70
+ width: `${23}px`,
71
+ // opacity: .15,
72
+ }}
73
+ // onEvent={this.onPlayerEvent}
74
+ />
75
+ </motion.div>
76
+
77
+ {hasLoader && (
78
+ <motion.div
79
+ style={{position: "absolute", left: 1, top: -9}}
80
+ initial={{scale: 1, opacity: 1}}
81
+ // initial={{scale: 1, opacity: 0}}
82
+ transition={SPRING_BOUNCY}
83
+ animate={loaderAnimatedVals}
84
+ onAnimationComplete={onLoaderAnimationComplete}
85
+ >
86
+ <ActivityIndicator size={19} />
87
+ </motion.div>
88
+ )}
89
+ </div>
90
+ )
91
+ }
92
+
93
+ export default HeaderStatus
@@ -0,0 +1,67 @@
1
+ /* @flow */
2
+ import assert from "assert"
3
+ import {useRef} from "react"
4
+ import Toast from "react-bootstrap/Toast"
5
+ import {CSSTransition} from "react-transition-group"
6
+
7
+ import {useNotifications} from "../NotificationsContext"
8
+
9
+ import HeaderStatus from "./HeaderStatus"
10
+
11
+ // TODO: mv to rb server
12
+ // import ack_notification from "rpc!server/notifications/ack_notification"
13
+
14
+ import "./notification-toast.scss"
15
+
16
+
17
+ const NotificationToast = ({ nodeRef, notification }) => {
18
+ const { setNotifications } = useNotifications()
19
+
20
+ const { isLoading = false, status = "success", zIndex = 999 } = notification
21
+
22
+ const onClick = () => {
23
+ console.log("onclicknotif", notification)
24
+ }
25
+
26
+ // remove notification on close
27
+ const onClose = async () => {
28
+ console.log("onClose!!!")
29
+ if (notification.id) {
30
+ console.warn("ACK NOTIFICATION not copied to rb-server yet")
31
+ // const res = await ack_notification({
32
+ // notification_id: notification.id,
33
+ // ack_at_ms: Date.now(),
34
+ // })
35
+ // assert(res.status === "ok")
36
+ }
37
+
38
+ setNotifications((current) => {
39
+ return current.filter((n) => n.id !== notification.id)
40
+ })
41
+ }
42
+
43
+ // no header when no body
44
+ const hasBody = !!notification.body
45
+
46
+ return (
47
+ <Toast
48
+ ref={nodeRef}
49
+ className={cx("notifs-item mt-2 notification-toast", {"has-no-body": !hasBody})}
50
+ style={{position: "relative", zIndex}}
51
+ onClose={onClose}
52
+ animation={false}
53
+ >
54
+ <Toast.Header className="ps-2 py-2" closeButton={true}>
55
+ {notification.icon ? <div>{"<IC>"}</div> : <HeaderStatus isLoading={isLoading} status={status} />}
56
+
57
+ <div className="me-auto">{notification.title}</div>
58
+ {notification.type === "workflow" && <small>{notification.formatted_timestamp}</small>}
59
+ {notification.type === "intent" && <small>INTENT</small>}
60
+ </Toast.Header>
61
+
62
+ {hasBody && <Toast.Body onClick={onClick}>{notification.body}</Toast.Body>}
63
+ </Toast>
64
+ )
65
+ }
66
+
67
+ export default NotificationToast
@@ -0,0 +1,25 @@
1
+ @import "helpers";
2
+
3
+ .notification-toast.toast {
4
+ max-width: 260px;
5
+
6
+ .toast-header {
7
+ color: $gray-900;
8
+ font-weight: 600;
9
+ }
10
+
11
+ &.has-no-body {
12
+ background: $white !important;
13
+
14
+ .toast-header {
15
+ background: transparent !important;
16
+ border-bottom: none;
17
+ }
18
+ }
19
+
20
+ svg {
21
+ path {
22
+ fill: $green-500 !important;
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,84 @@
1
+ /* @flow */
2
+ import {useEffect, createContext, useContext} from "react"
3
+
4
+ import {useNotificationsList} from "./useNotificationsList"
5
+
6
+ // const ICONS_MAP = {
7
+ // warning: "warning",
8
+ // error: "danger",
9
+ // }
10
+
11
+ export const NotificationsContext = createContext()
12
+
13
+ export const NotificationsProvider = ({value, children}) => {
14
+ const [notifications, setNotifications] = useNotificationsList([
15
+ // WARNING: Debug only, this breaks unit tests
16
+ {
17
+ title: "Hello world",
18
+ body: "helloooooo",
19
+ timestamp: Date.now(),
20
+ level: "error",
21
+ icon: "error",
22
+ },
23
+ ])
24
+
25
+ // A toast is displayed with notifications, but it is ephemeral and in the UI only
26
+ // they are used to provide feedback on an action that is a little longer than a simple form submit
27
+ // they are gone when the page refreshes
28
+ // toasts appear above modals
29
+ const addToast = ({
30
+ id,
31
+ title,
32
+ body,
33
+ timestamp = Date.now(),
34
+ zIndex = 2000,
35
+ isLoading = false,
36
+ }) => {
37
+ const toast = {
38
+ id: id || `notif-${Date.now().toString()}`,
39
+ title,
40
+ body,
41
+ timestamp,
42
+ zIndex,
43
+ level: "toast",
44
+ isLoading,
45
+ // icon: ICONS_MAP.error,
46
+ }
47
+
48
+ setNotifications((current) => {
49
+ const next = [...current]
50
+ next.push(toast)
51
+ return next
52
+ })
53
+
54
+ const dismiss = () => {
55
+ setNotifications((current) => {
56
+ const next = [...current]
57
+ return next.filter((notif) => notif.id !== id)
58
+ })
59
+ }
60
+
61
+ const update = (payload) => {
62
+ setNotifications((current) => {
63
+ const next = [...current]
64
+ return next.map((notif) => {
65
+ if (notif.id === id) {
66
+ // notif.isLoading = false
67
+ Object.assign(notif, payload)
68
+ }
69
+ return notif
70
+ })
71
+ })
72
+ }
73
+
74
+ return {dismiss, update}
75
+ }
76
+
77
+ return (
78
+ <NotificationsContext.Provider value={{notifications, setNotifications, addToast}}>
79
+ {children}
80
+ </NotificationsContext.Provider>
81
+ )
82
+ }
83
+
84
+ export const useNotifications = () => useContext(NotificationsContext)
@@ -0,0 +1,75 @@
1
+ /* eslint-disable */
2
+ /* @flow */
3
+ import {useState, useEffect} from "react"
4
+
5
+ import {useQuery} from "../../rts"
6
+
7
+ import {ON_NOTIFICATION_RECEIVED_EVENT} from "../config"
8
+
9
+ export const useNotificationsList = (initial = []) => {
10
+ const [allNotifications, setAllNotifications] = useState(initial)
11
+
12
+ const notificationsQuery = useQuery(
13
+ "Notification",
14
+ {
15
+ $or: [{ack_at_ms: null}, {ack_at_ms: {$exists: false}}],
16
+ },
17
+ {
18
+ projection: {
19
+ _owners: 0,
20
+ },
21
+ },
22
+ )
23
+
24
+ useEffect(() => {
25
+ if (!notificationsQuery.data) return
26
+
27
+ // console.log("NOTIFS TMP RETURN", notificationsQuery.data)
28
+ return
29
+
30
+ // console.log("got new notifs data", notificationsQuery.data)
31
+
32
+ // get all current ids
33
+ const allNotificationsIds = allNotifications.filter((n) => !!n.id).map((n) => n.id)
34
+
35
+ const insertNotifications = notificationsQuery.data.filter(
36
+ (n) => !allNotificationsIds.includes(n._id),
37
+ )
38
+
39
+ const notificationsDataIds = notificationsQuery.data.map((n) => n._id)
40
+
41
+ // remove all notifications that are from the notifications collection
42
+ const removeNotificationsIds = allNotifications
43
+ .filter((n) => !!n.id && n.col === "Notification")
44
+ .filter((n) => !notificationsDataIds.includes(n.id))
45
+ .map((n) => n.id)
46
+
47
+ // update our notifications
48
+ if (insertNotifications.length > 0 || removeNotificationsIds.length > 0) {
49
+ setAllNotifications((previous) => {
50
+ // copy and remove old notifs
51
+ const nextNotifs = [...previous].filter((n) => !removeNotificationsIds.includes(n.id))
52
+
53
+ const formattedNotifications = insertNotifications.map((n) => {
54
+ return {
55
+ id: n._id,
56
+ col: "Notification",
57
+ ...n.notification,
58
+ timestamp: n.server_timestamp_ms,
59
+ }
60
+ })
61
+
62
+ nextNotifs.unshift(...formattedNotifications)
63
+ return nextNotifs
64
+ })
65
+ }
66
+
67
+ // play animation if we added anything
68
+ if (insertNotifications.length > 0) {
69
+ const ev = new CustomEvent(ON_NOTIFICATION_RECEIVED_EVENT)
70
+ document.body.dispatchEvent(ev)
71
+ }
72
+ }, [notificationsQuery.data, allNotifications])
73
+
74
+ return [allNotifications, setAllNotifications]
75
+ }
@@ -0,0 +1,44 @@
1
+ /* @flow */
2
+ import assert from "assert"
3
+ import {useEffect, useState, createRef} from "react"
4
+ import {CSSTransition, TransitionGroup} from "react-transition-group"
5
+
6
+ import ToastContainer from "react-bootstrap/ToastContainer"
7
+
8
+ import {formatDistance} from "date-fns/formatDistance"
9
+
10
+ import {useQuery} from "@rpcbase/client/rts"
11
+
12
+ import {useNotifications} from "../NotificationsContext"
13
+ import NotificationToast from "../NotificationToast"
14
+
15
+ import useLLTs from "./useLLTs"
16
+
17
+ import "./notifications.scss"
18
+
19
+ export const NotificationsList = () => {
20
+ const {notifications, setNotifications} = useNotifications()
21
+
22
+ const llts = useLLTs()
23
+
24
+ const renderNotification = (notification, i) => {
25
+ const key = notification.id
26
+ const nodeRef = createRef()
27
+
28
+ return (
29
+ <CSSTransition key={notification.id} nodeRef={nodeRef} timeout={200} classNames="notifs-item">
30
+ <NotificationToast key={key} nodeRef={nodeRef} notification={notification} />
31
+ </CSSTransition>
32
+ )
33
+ }
34
+
35
+ return (
36
+ <ToastContainer position="bottom-end" className="pb-4 pe-3 ps-4" style={{position: "absolute", overflowY: "scroll", bottom: 0, right: 0}}>
37
+ <div className="notifications-list-wrapper">
38
+ <TransitionGroup className="transition-list">
39
+ {notifications.map(renderNotification)}
40
+ </TransitionGroup>
41
+ </div>
42
+ </ToastContainer>
43
+ )
44
+ }
@@ -0,0 +1,33 @@
1
+ @import "helpers";
2
+
3
+ $transition-duration: 200ms;
4
+
5
+ .notifications-list-wrapper {
6
+
7
+ .action-notification {
8
+ cursor: pointer;
9
+ }
10
+
11
+ .transition-list {
12
+ transition: max-height $transition-duration ease-in-out;
13
+ }
14
+
15
+ .notifs-item-enter {
16
+ opacity: 0;
17
+ }
18
+
19
+ .notifs-item-enter-active {
20
+ opacity: 1;
21
+ transition: opacity 400ms;
22
+ }
23
+
24
+ .notifs-item-exit {
25
+ opacity: 1;
26
+ }
27
+
28
+ .notifs-item-exit-active {
29
+ transform: translateX(390px);
30
+
31
+ transition: all $transition-duration ease-out;
32
+ }
33
+ }
@@ -0,0 +1,28 @@
1
+ /* @flow */
2
+ import assert from "assert"
3
+ import {useEffect, useState} from "react"
4
+
5
+ // TODO: fix ltts import to rb
6
+ // import get_llts from "rpc!server/notifications/llt/get_llts"
7
+
8
+
9
+ const useLLTs = () => {
10
+ const [llts, setLLTs] = useState([])
11
+
12
+ useEffect(() => {
13
+ const load = async() => {
14
+ // const res = await get_llts({
15
+ // env_id: envContext.envId,
16
+ // })
17
+ const res = {status: "ok", llts: []}
18
+ assert(res.status === "ok", "unable to retrieve llts")
19
+ setLLTs(res.llts)
20
+ }
21
+
22
+ load()
23
+ }, [])
24
+
25
+ return llts
26
+ }
27
+
28
+ export default useLLTs
@@ -0,0 +1,52 @@
1
+ /* @flow */
2
+ import Form from "react-bootstrap/Form"
3
+ import TimePicker from "react-time-picker"
4
+
5
+ import {useStoredValue} from "@rpcbase/client"
6
+
7
+
8
+ const TIME_FORMAT = "HH:mm"
9
+
10
+ const timePickerProps = {
11
+ format: TIME_FORMAT,
12
+ maxDetail: "minute",
13
+ renderSecondHand: false,
14
+ }
15
+
16
+ const SettingsForm = () => {
17
+ const [isEnabled, setIsEnabled] = useStoredValue("has_notifications_enabled", "yes")
18
+
19
+ const [fromTime, setFromTime] = useStoredValue("notifications_from_time", "09:00")
20
+ const [toTime, setToTime] = useStoredValue("notifications_to_time", "22:00")
21
+
22
+ const onToggleNotifications = (e) => {
23
+ setIsEnabled(e.target.checked ? "yes" : "no")
24
+ }
25
+
26
+ return (
27
+ <div className="px-2 py-3">
28
+ <div>
29
+ <div className="h6 fw-bold">Enable Notifications</div>
30
+ <Form.Check
31
+ type="switch"
32
+ id="toggle-notifications-switch"
33
+ label={`Notifications are ${isEnabled ? "enabled" : "disabled"}`}
34
+ checked={isEnabled === "yes"}
35
+ onChange={onToggleNotifications}
36
+ />
37
+ </div>
38
+
39
+ <div className="mt-3">
40
+ <div className="h6 fw-bold">Notifications are allowed:</div>
41
+ <div className="">
42
+ <span className="me-2">From:</span>
43
+ <TimePicker onChange={setFromTime} value={fromTime} {...timePickerProps} />
44
+ <span className="me-2">To:</span>
45
+ <TimePicker onChange={setToTime} value={toTime} {...timePickerProps} />
46
+ </div>
47
+ </div>
48
+ </div>
49
+ )
50
+ }
51
+
52
+ export default SettingsForm
@@ -0,0 +1,48 @@
1
+ /* @flow */
2
+ import ActivityIndicator from "@rpcbase/ui/ActivityIndicator"
3
+ import Modal, {withHashStateModal} from "@rpcbase/ui/Modal"
4
+
5
+ // import TemplatesView from "./TemplatesView"
6
+ // import {TemplateContextProvider} from "./TemplateContext"
7
+
8
+ // import DialogAnimatedPreview from "./components/DialogAnimatedPreview"
9
+ // import TogglePreviewButton from "./components/TogglePreviewButton"
10
+
11
+ import SettingsForm from "./SettingsForm"
12
+
13
+ import BellGlyph from "static/icons/notifications/bell-glyph.svg"
14
+
15
+ import "./notifications-settings.scss"
16
+
17
+
18
+ const NotificationsSettingsModal = ({onHide}) => {
19
+ const isLoading = false
20
+
21
+ return (
22
+ <Modal className="channel-templates-modal" show scrollable={false} onHide={onHide}>
23
+ <Modal.Header className="close-top" closeButton>
24
+ <BellGlyph className="me-2 align-self-start mt-1" width={22} fill={"#0d6efd"} />
25
+ <div>
26
+ <div>Notifications</div>
27
+ </div>
28
+ </Modal.Header>
29
+ <Modal.Body className="p-0" style={{maxHeight: "70vh", overflow: "visible"}}>
30
+ {isLoading && (
31
+ <div className="d-flex flex-row align-items-center">
32
+ <ActivityIndicator size={24} />
33
+ <div className="ms-2">Loading text...</div>
34
+ </div>
35
+ )}
36
+
37
+ <SettingsForm />
38
+ </Modal.Body>
39
+ <Modal.Footer className="d-flex justify-content-between">
40
+ <div>
41
+ For more info see <a href="/docs/notifications">/docs/notifications</a>
42
+ </div>
43
+ </Modal.Footer>
44
+ </Modal>
45
+ )
46
+ }
47
+
48
+ export default withHashStateModal(NotificationsSettingsModal, "showNotificationsSettingsModal")
@@ -0,0 +1 @@
1
+ export const ON_NOTIFICATION_RECEIVED_EVENT = "ON_NOTIFICATION_RECEIVED_EVENT"
@@ -0,0 +1,4 @@
1
+ export * from "./NotificationsContext"
2
+ export * from "./NotificationsList"
3
+
4
+ export * from "./config"