diff --git a/components/toast.js b/components/toast.js
index 936c0750..c96f6e31 100644
--- a/components/toast.js
+++ b/components/toast.js
@@ -12,53 +12,63 @@ export const ToastProvider = ({ children }) => {
const router = useRouter()
const [toasts, setToasts] = useState([])
const toastId = useRef(0)
+
const dispatchToast = useCallback((toastConfig) => {
toastConfig = {
...toastConfig,
id: toastId.current++
}
setToasts(toasts => [...toasts, toastConfig])
+ return () => removeToast(toastConfig)
}, [])
- const removeToast = useCallback(id => {
- setToasts(toasts => toasts.filter(toast => toast.id !== id))
+ const removeToast = useCallback(({ id, onCancel, tag }) => {
+ setToasts(toasts => toasts.filter(toast => {
+ if (tag && !onCancel) {
+ // if tag onCancel is not set, toast did show X for closing.
+ // if additionally tag is set, we close all toasts with same tag.
+ return toast.tag !== tag
+ }
+ return toast.id !== id
+ }))
}, [])
const toaster = useMemo(() => ({
- success: (body, delay = 5000) => {
- dispatchToast({
+ success: (body, options) => {
+ const toast = {
body,
variant: 'success',
autohide: true,
- delay
- })
+ delay: 5000,
+ ...options
+ }
+ return dispatchToast(toast)
},
- warning: (body, delay = 5000) => {
- dispatchToast({
+ warning: (body, options) => {
+ const toast = {
body,
variant: 'warning',
autohide: true,
- delay
- })
+ delay: 5000,
+ ...options
+ }
+ return dispatchToast(toast)
},
- danger: (body, onCloseCallback) => {
- const id = toastId.current
- dispatchToast({
- id,
+ danger: (body, options) => {
+ const toast = {
body,
variant: 'danger',
autohide: false,
- onCloseCallback
- })
- return {
- removeToast: () => removeToast(id)
+ ...options
}
+ return dispatchToast(toast)
}
}), [dispatchToast, removeToast])
- // Clear all toasts on page navigation
+ // Only clear toasts with no cancel function on page navigation
+ // since navigation should not interfere with being able to cancel an action.
useEffect(() => {
- const handleRouteChangeStart = () => setToasts([])
+ const handleRouteChangeStart = () => setToasts(toasts => toasts.filter(({ onCancel }) => onCancel), [])
router.events.on('routeChangeStart', handleRouteChangeStart)
return () => {
@@ -66,31 +76,63 @@ export const ToastProvider = ({ children }) => {
}
}, [router])
+ // this function merges toasts with the same tag into one toast.
+ // for example: 3x 'zap pending' -> '(3) zap pending'
+ const tagReducer = (toasts, toast) => {
+ const { tag } = toast
+
+ // has tag?
+ if (!tag) return [...toasts, toast]
+
+ // existing tag?
+ const idx = toasts.findIndex(toast => toast.tag === tag)
+ if (idx === -1) return [...toasts, toast]
+
+ // merge toasts with same tag
+ const prevToast = toasts[idx]
+ let { rawBody, body, amount } = prevToast
+ rawBody ??= body
+ amount = amount ? amount + 1 : 2
+ body = `(${amount}) ${rawBody}`
+ return [
+ ...toasts.slice(0, idx),
+ { ...toast, rawBody, amount, body },
+ ...toasts.slice(idx + 1)
+ ]
+ }
+
+ // only show toast with highest ID of each tag
+ const visibleToasts = toasts.reduce(tagReducer, [])
+
return (
- {toasts.map(toast => (
- removeToast(toast.id)}
- >
-
-
-
{toast.body}
-
-
-
-
- ))}
+ {visibleToasts.map(toast => {
+ const textStyle = toast.variant === 'warning' ? 'text-dark' : ''
+ return (
+ removeToast(toast.id)}
+ >
+
+
+
{toast.body}
+
+
+
+
+ )
+ })}
{children}
diff --git a/components/toast.module.css b/components/toast.module.css
index 6d09ad7b..c66d1363 100644
--- a/components/toast.module.css
+++ b/components/toast.module.css
@@ -21,6 +21,13 @@
border-color: var(--bs-warning-border-subtle);
}
+.toastCancel {
+ font-style: italic;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+}
+
.toastClose {
color: #fff;
font-family: "lightning";
diff --git a/components/use-crossposter.js b/components/use-crossposter.js
index ede0f45d..552bf50d 100644
--- a/components/use-crossposter.js
+++ b/components/use-crossposter.js
@@ -27,7 +27,7 @@ export default function useCrossposter () {
const relayError = (failedRelays) => {
return new Promise(resolve => {
- const { removeToast } = toast.danger(
+ const removeToast = toast.danger(
<>
Crossposting failed for {failedRelays.join(', ')}