From 0410fc9ce605f1495870eaee1715a33e706cf58b Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Tue, 30 Apr 2024 11:49:36 +0300 Subject: [PATCH 1/3] feat: allow setting notification read from web/snackbar when opening link from snackbar or web notification, the notification can be automatically set as read like in the notifications page Signed-off-by: Heikki Hellgren --- .changeset/ten-penguins-roll.md | 5 ++++ plugins/notifications/api-report.md | 6 ++++- .../NotificationsSideBarItem.tsx | 23 +++++++++++++++++-- .../src/hooks/useWebNotifications.ts | 23 ++++++++++++++++--- 4 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 .changeset/ten-penguins-roll.md diff --git a/.changeset/ten-penguins-roll.md b/.changeset/ten-penguins-roll.md new file mode 100644 index 0000000000..60e0ae773a --- /dev/null +++ b/.changeset/ten-penguins-roll.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications': patch +--- + +Allow setting notification as read when opening snackbar or web notification link diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md index 9240bdb1c2..2547204c7b 100644 --- a/plugins/notifications/api-report.md +++ b/plugins/notifications/api-report.md @@ -103,6 +103,7 @@ export const NotificationsSidebarItem: (props?: { titleCounterEnabled?: boolean; snackbarEnabled?: boolean; snackbarAutoHideDuration?: number | null; + markAsReadOnLinkOpen?: boolean; className?: string; icon?: IconComponent; text?: string; @@ -182,7 +183,10 @@ export function useTitleCounter(): { }; // @public (undocumented) -export function useWebNotifications(enabled: boolean): { +export function useWebNotifications( + enabled: boolean, + markAsReadOnLinkOpen: boolean, +): { sendWebNotification: (options: { id: string; title: string; diff --git a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx index 7b55c63747..6e1f7c7d56 100644 --- a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx +++ b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx @@ -84,6 +84,7 @@ export const NotificationsSidebarItem = (props?: { titleCounterEnabled?: boolean; snackbarEnabled?: boolean; snackbarAutoHideDuration?: number | null; + markAsReadOnLinkOpen?: boolean; className?: string; icon?: IconComponent; text?: string; @@ -95,6 +96,7 @@ export const NotificationsSidebarItem = (props?: { titleCounterEnabled = true, snackbarEnabled = true, snackbarAutoHideDuration = 10000, + markAsReadOnLinkOpen = false, icon = NotificationsIcon, text = 'Notifications', ...restProps @@ -103,6 +105,7 @@ export const NotificationsSidebarItem = (props?: { titleCounterEnabled: true, snackbarEnabled: true, snackbarAutoHideDuration: 10000, + markAsReadOnLinkOpen: false, }; const { loading, error, value, retry } = useNotificationsApi(api => @@ -114,7 +117,10 @@ export const NotificationsSidebarItem = (props?: { const notificationsRoute = useRouteRef(rootRouteRef); // TODO: Do we want to add long polling in case signals are not available const { lastSignal } = useSignal('notifications'); - const { sendWebNotification } = useWebNotifications(webNotificationsEnabled); + const { sendWebNotification } = useWebNotifications( + webNotificationsEnabled, + markAsReadOnLinkOpen, + ); const [refresh, setRefresh] = React.useState(false); const { setNotificationCount } = useTitleCounter(); @@ -126,6 +132,19 @@ export const NotificationsSidebarItem = (props?: { component={Link} to={notification.payload.link ?? notificationsRoute()} onClick={() => { + if (markAsReadOnLinkOpen) { + notificationsApi + .updateNotifications({ + ids: [notification.id], + read: true, + }) + .catch(() => { + alertApi.post({ + message: 'Failed to mark notification as read', + severity: 'error', + }); + }); + } closeSnackbar(snackBarId); }} > @@ -156,7 +175,7 @@ export const NotificationsSidebarItem = (props?: { return { action }; }, - [notificationsRoute, notificationsApi, alertApi], + [notificationsRoute, markAsReadOnLinkOpen, notificationsApi, alertApi], ); useEffect(() => { diff --git a/plugins/notifications/src/hooks/useWebNotifications.ts b/plugins/notifications/src/hooks/useWebNotifications.ts index 728d992130..92a0a9b806 100644 --- a/plugins/notifications/src/hooks/useWebNotifications.ts +++ b/plugins/notifications/src/hooks/useWebNotifications.ts @@ -15,14 +15,19 @@ */ import { useCallback, useEffect, useState } from 'react'; import { rootRouteRef } from '../routes'; -import { useRouteRef } from '@backstage/core-plugin-api'; +import { useApi, useRouteRef } from '@backstage/core-plugin-api'; import { useNavigate } from 'react-router-dom'; +import { notificationsApiRef } from '../api'; /** @public */ -export function useWebNotifications(enabled: boolean) { +export function useWebNotifications( + enabled: boolean, + markAsReadOnLinkOpen: boolean, +) { const [webNotificationPermission, setWebNotificationPermission] = useState('default'); const notificationsRoute = useRouteRef(rootRouteRef); + const notificationsApi = useApi(notificationsApiRef); const navigate = useNavigate(); useEffect(() => { @@ -57,6 +62,12 @@ export function useWebNotifications(enabled: boolean) { event.preventDefault(); if (options.link) { window.open(options.link, '_blank'); + if (markAsReadOnLinkOpen) { + notificationsApi.updateNotifications({ + ids: [options.id], + read: true, + }); + } } else { navigate(notificationsRoute()); } @@ -65,7 +76,13 @@ export function useWebNotifications(enabled: boolean) { return notification; }, - [webNotificationPermission, navigate, notificationsRoute], + [ + webNotificationPermission, + markAsReadOnLinkOpen, + notificationsApi, + navigate, + notificationsRoute, + ], ); return { sendWebNotification }; From a880e941aaf1382189169b3416df51ec4b2c0b6c Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Thu, 30 May 2024 15:45:38 +0300 Subject: [PATCH 2/3] fix: change notification hooks to internal Signed-off-by: Heikki Hellgren --- plugins/notifications/api-report.md | 18 ------------------ .../notifications/src/hooks/useTitleCounter.ts | 2 +- .../src/hooks/useWebNotifications.ts | 2 +- plugins/notifications/src/index.ts | 2 +- 4 files changed, 3 insertions(+), 21 deletions(-) diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md index 2547204c7b..c431664883 100644 --- a/plugins/notifications/api-report.md +++ b/plugins/notifications/api-report.md @@ -177,23 +177,5 @@ export function useNotificationsApi( value: T; }; -// @public (undocumented) -export function useTitleCounter(): { - setNotificationCount: (newCount: number) => void; -}; - -// @public (undocumented) -export function useWebNotifications( - enabled: boolean, - markAsReadOnLinkOpen: boolean, -): { - sendWebNotification: (options: { - id: string; - title: string; - description: string; - link?: string; - }) => Notification | null; -}; - // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/notifications/src/hooks/useTitleCounter.ts b/plugins/notifications/src/hooks/useTitleCounter.ts index 0793cd58d6..399bfb84f4 100644 --- a/plugins/notifications/src/hooks/useTitleCounter.ts +++ b/plugins/notifications/src/hooks/useTitleCounter.ts @@ -25,7 +25,7 @@ const throttledSetTitle = throttle((shownTitle: string) => { document.title = shownTitle; }, 100); -/** @public */ +/** @internal */ export function useTitleCounter() { const [title, setTitle] = useState(document.title); const [count, setCount] = useState(0); diff --git a/plugins/notifications/src/hooks/useWebNotifications.ts b/plugins/notifications/src/hooks/useWebNotifications.ts index 92a0a9b806..3fbd3c5496 100644 --- a/plugins/notifications/src/hooks/useWebNotifications.ts +++ b/plugins/notifications/src/hooks/useWebNotifications.ts @@ -19,7 +19,7 @@ import { useApi, useRouteRef } from '@backstage/core-plugin-api'; import { useNavigate } from 'react-router-dom'; import { notificationsApiRef } from '../api'; -/** @public */ +/** @internal */ export function useWebNotifications( enabled: boolean, markAsReadOnLinkOpen: boolean, diff --git a/plugins/notifications/src/index.ts b/plugins/notifications/src/index.ts index bb5d76a0cf..25565f1ae1 100644 --- a/plugins/notifications/src/index.ts +++ b/plugins/notifications/src/index.ts @@ -15,5 +15,5 @@ */ export { notificationsPlugin, NotificationsPage } from './plugin'; export * from './api'; -export * from './hooks'; +export { useNotificationsApi } from './hooks'; export * from './components'; From aa2d3e302acff82c078dcc40dd75fe17144fc5ef Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Thu, 30 May 2024 21:14:02 +0300 Subject: [PATCH 3/3] feat: make default functionality to mark notification read when opening link Signed-off-by: Heikki Hellgren --- .changeset/ten-penguins-roll.md | 4 +-- plugins/notifications/api-report.md | 1 - .../NotificationsSideBarItem.tsx | 13 ++++----- .../src/hooks/useWebNotifications.ts | 29 ++++++------------- 4 files changed, 17 insertions(+), 30 deletions(-) diff --git a/.changeset/ten-penguins-roll.md b/.changeset/ten-penguins-roll.md index 60e0ae773a..152bb1524b 100644 --- a/.changeset/ten-penguins-roll.md +++ b/.changeset/ten-penguins-roll.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-notifications': patch +'@backstage/plugin-notifications': minor --- -Allow setting notification as read when opening snackbar or web notification link +By default, set notification as read when opening snackbar or web notification link diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md index c431664883..5e06ae2616 100644 --- a/plugins/notifications/api-report.md +++ b/plugins/notifications/api-report.md @@ -103,7 +103,6 @@ export const NotificationsSidebarItem: (props?: { titleCounterEnabled?: boolean; snackbarEnabled?: boolean; snackbarAutoHideDuration?: number | null; - markAsReadOnLinkOpen?: boolean; className?: string; icon?: IconComponent; text?: string; diff --git a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx index 6e1f7c7d56..457893b016 100644 --- a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx +++ b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx @@ -84,7 +84,6 @@ export const NotificationsSidebarItem = (props?: { titleCounterEnabled?: boolean; snackbarEnabled?: boolean; snackbarAutoHideDuration?: number | null; - markAsReadOnLinkOpen?: boolean; className?: string; icon?: IconComponent; text?: string; @@ -96,7 +95,6 @@ export const NotificationsSidebarItem = (props?: { titleCounterEnabled = true, snackbarEnabled = true, snackbarAutoHideDuration = 10000, - markAsReadOnLinkOpen = false, icon = NotificationsIcon, text = 'Notifications', ...restProps @@ -105,7 +103,6 @@ export const NotificationsSidebarItem = (props?: { titleCounterEnabled: true, snackbarEnabled: true, snackbarAutoHideDuration: 10000, - markAsReadOnLinkOpen: false, }; const { loading, error, value, retry } = useNotificationsApi(api => @@ -117,9 +114,8 @@ export const NotificationsSidebarItem = (props?: { const notificationsRoute = useRouteRef(rootRouteRef); // TODO: Do we want to add long polling in case signals are not available const { lastSignal } = useSignal('notifications'); - const { sendWebNotification } = useWebNotifications( + const { sendWebNotification, requestUserPermission } = useWebNotifications( webNotificationsEnabled, - markAsReadOnLinkOpen, ); const [refresh, setRefresh] = React.useState(false); const { setNotificationCount } = useTitleCounter(); @@ -132,7 +128,7 @@ export const NotificationsSidebarItem = (props?: { component={Link} to={notification.payload.link ?? notificationsRoute()} onClick={() => { - if (markAsReadOnLinkOpen) { + if (notification.payload.link) { notificationsApi .updateNotifications({ ids: [notification.id], @@ -175,7 +171,7 @@ export const NotificationsSidebarItem = (props?: { return { action }; }, - [notificationsRoute, markAsReadOnLinkOpen, notificationsApi, alertApi], + [notificationsRoute, notificationsApi, alertApi], ); useEffect(() => { @@ -278,6 +274,9 @@ export const NotificationsSidebarItem = (props?: { )} { + requestUserPermission(); + }} hasNotifications={!error && !!unreadCount} text={text} icon={icon} diff --git a/plugins/notifications/src/hooks/useWebNotifications.ts b/plugins/notifications/src/hooks/useWebNotifications.ts index 3fbd3c5496..ec120a614d 100644 --- a/plugins/notifications/src/hooks/useWebNotifications.ts +++ b/plugins/notifications/src/hooks/useWebNotifications.ts @@ -13,24 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useState } from 'react'; import { rootRouteRef } from '../routes'; import { useApi, useRouteRef } from '@backstage/core-plugin-api'; import { useNavigate } from 'react-router-dom'; import { notificationsApiRef } from '../api'; /** @internal */ -export function useWebNotifications( - enabled: boolean, - markAsReadOnLinkOpen: boolean, -) { +export function useWebNotifications(enabled: boolean) { const [webNotificationPermission, setWebNotificationPermission] = useState('default'); const notificationsRoute = useRouteRef(rootRouteRef); const notificationsApi = useApi(notificationsApiRef); const navigate = useNavigate(); - useEffect(() => { + const requestUserPermission = useCallback(() => { if ( enabled && 'Notification' in window && @@ -62,12 +59,10 @@ export function useWebNotifications( event.preventDefault(); if (options.link) { window.open(options.link, '_blank'); - if (markAsReadOnLinkOpen) { - notificationsApi.updateNotifications({ - ids: [options.id], - read: true, - }); - } + notificationsApi.updateNotifications({ + ids: [options.id], + read: true, + }); } else { navigate(notificationsRoute()); } @@ -76,14 +71,8 @@ export function useWebNotifications( return notification; }, - [ - webNotificationPermission, - markAsReadOnLinkOpen, - notificationsApi, - navigate, - notificationsRoute, - ], + [webNotificationPermission, notificationsApi, navigate, notificationsRoute], ); - return { sendWebNotification }; + return { sendWebNotification, requestUserPermission }; }