@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.
- package/AppProvider/index.js +8 -4
- package/auth/components/SignOut/index.js +11 -1
- package/notifications/Notification/index.js +36 -0
- package/notifications/Notification/notification.scss +1 -0
- package/notifications/NotificationToast/HeaderStatus.js +93 -0
- package/notifications/NotificationToast/index.js +67 -0
- package/notifications/NotificationToast/notification-toast.scss +25 -0
- package/notifications/NotificationsContext/index.js +84 -0
- package/notifications/NotificationsContext/useNotificationsList.js +75 -0
- package/notifications/NotificationsList/index.js +44 -0
- package/notifications/NotificationsList/notifications.scss +33 -0
- package/notifications/NotificationsList/useLLTs.js +28 -0
- package/notifications/NotificationsSettingsModal/SettingsForm.js +52 -0
- package/notifications/NotificationsSettingsModal/index.js +48 -0
- package/notifications/NotificationsSettingsModal/notifications-settings.scss +1 -0
- package/notifications/config.js +1 -0
- package/notifications/index.js +4 -0
- package/package.json +4 -1
- package/rts/getUseQuery/index.js +1 -2
- package/rts/rts.js +2 -0
- package/ui/ActivityIndicator/index.js +113 -0
- package/ui/LottiePlayer/LottiePlayer.js +4 -0
- package/ui/LottiePlayer/index.js +8 -0
- package/ui/Modal/HashStateModal.js +32 -0
- package/ui/Modal/Modal.js +97 -0
- package/ui/Modal/ModalForm/AlertBanner.js +83 -0
- package/ui/Modal/ModalForm/index.js +193 -0
- package/ui/Modal/ModalForm/modal-form.scss +63 -0
- package/ui/Modal/index.js +10 -0
- package/ui/Modal/modal.scss +101 -0
- package/ui/Modal/withHashStateModal.js +24 -0
- package/ui/SubmitButton/index.js +32 -0
- package/ui/animations/checkmark.json +1 -0
- package/ui/springs.js +17 -0
- package/ui/withSuspense/index.js +40 -0
package/AppProvider/index.js
CHANGED
|
@@ -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
|
-
<
|
|
60
|
-
{
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
@import "helpers";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ON_NOTIFICATION_RECEIVED_EVENT = "ON_NOTIFICATION_RECEIVED_EVENT"
|