feat: add support for unread count in document title

Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
Heikki Hellgren
2024-01-26 13:16:30 +02:00
parent fdf03a6997
commit dced4bbc96
7 changed files with 197 additions and 35 deletions
+13
View File
@@ -121,5 +121,18 @@ export function useNotificationsApi<T>(
value: T;
};
// @public (undocumented)
export function useTitleCounter(): {
setNotificationCount: (newCount: number) => void;
};
// @public (undocumented)
export function useWebNotifications(): {
sendWebNotification: (options: {
title: string;
description: string;
}) => Notification | null;
};
// (No @packageDocumentation comment for this package)
```
+31
View File
@@ -0,0 +1,31 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface Config {
/** @visibility frontend */
notifications?: {
/**
* Enable or disable the Web Notification API for notifications, defaults to false
* @visibility frontend
*/
enableWebNotifications?: boolean;
/**
* Enable or disable the title override to show notification count, defaults to true
* @visibility frontend
*/
enableTitleCounter?: boolean;
};
}
+4 -2
View File
@@ -51,6 +51,8 @@
"msw": "^1.0.0"
},
"files": [
"dist"
]
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts"
}
@@ -13,36 +13,38 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { useNotificationsApi } from '../../hooks';
import { SidebarItem } from '@backstage/core-components';
import NotificationsIcon from '@material-ui/icons/Notifications';
import { useRouteRef } from '@backstage/core-plugin-api';
import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
import { rootRouteRef } from '../../routes';
import { useSignal } from '@backstage/plugin-signals-react';
import { useWebNotifications } from '../../hooks/useWebNotifications';
import { useTitleCounter } from '../../hooks/useTitleCounter';
/** @public */
export const NotificationsSidebarItem = () => {
const { loading, error, value, retry } = useNotificationsApi(api =>
api.getStatus(),
);
const config = useApi(configApiRef);
const [unreadCount, setUnreadCount] = React.useState(0);
const [webNotificationPermission, setWebNotificationPermission] =
React.useState('default');
const notificationsRoute = useRouteRef(rootRouteRef);
const { lastSignal } = useSignal('notifications');
const [webNotifications, setWebNotifications] = React.useState<
Notification[]
>([]);
const { sendWebNotification } = useWebNotifications();
const [refresh, setRefresh] = React.useState(false);
useEffect(() => {
if ('Notification' in window && webNotificationPermission === 'default') {
window.Notification.requestPermission().then(permission => {
setWebNotificationPermission(permission);
});
}
}, [webNotificationPermission]);
const webNotificationsEnabled = useMemo(
() =>
config.getOptionalBoolean('notifications.enableWebNotifications') ??
false,
[config],
);
const titleCounterEnabled = useMemo(
() => config.getOptionalString('notifications.enableTitleCounter') ?? true,
[config],
);
const { setNotificationCount } = useTitleCounter();
useEffect(() => {
if (refresh) {
@@ -54,37 +56,35 @@ export const NotificationsSidebarItem = () => {
useEffect(() => {
if (lastSignal && lastSignal.action === 'refresh') {
if (
webNotificationPermission === 'granted' &&
webNotificationsEnabled &&
'title' in lastSignal &&
'description' in lastSignal &&
'link' in lastSignal
) {
const notification = new Notification(lastSignal.title as string, {
body: lastSignal.description as string,
const notification = sendWebNotification({
title: lastSignal.title as string,
description: lastSignal.description as string,
});
notification.onclick = function (event) {
event.preventDefault();
notification.close();
window.open(lastSignal.link as string, '_blank');
};
setWebNotifications(prev => [...prev, notification]);
if (notification) {
notification.onclick = event => {
event.preventDefault();
notification.close();
window.open(lastSignal.link as string, '_blank');
};
}
}
setRefresh(true);
}
}, [lastSignal, webNotificationPermission]);
}, [lastSignal, sendWebNotification, webNotificationsEnabled]);
useEffect(() => {
if (!loading && !error && value) {
setUnreadCount(value.unread);
if (titleCounterEnabled) {
setNotificationCount(value.unread);
}
}
}, [loading, error, value]);
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
webNotifications.forEach(n => n.close());
setWebNotifications([]);
}
});
}, [loading, error, value, titleCounterEnabled, setNotificationCount]);
// TODO: Figure out if the count can be added to hasNotifications
return (
+2
View File
@@ -14,3 +14,5 @@
* limitations under the License.
*/
export * from './useNotificationsApi';
export * from './useWebNotifications';
export * from './useTitleCounter';
@@ -0,0 +1,60 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback, useEffect, useState } from 'react';
/** @public */
export function useTitleCounter() {
const [title, setTitle] = useState(document.title);
const [count, setCount] = useState(0);
const getPrefix = (value: number) => {
return value === 0 ? '' : `(${value}) `;
};
const cleanTitle = (currentTitle: string) => {
return currentTitle.replace(/^\(\d+\)\s/, '');
};
useEffect(() => {
document.title = title;
}, [title]);
useEffect(() => {
const baseTitle = cleanTitle(title);
setTitle(`${getPrefix(count)}${baseTitle}`);
return () => {
document.title = cleanTitle(title);
};
}, [title, count]);
const titleElement = document.querySelector('title');
if (titleElement) {
new MutationObserver(() => {
setTitle(document.title);
}).observe(titleElement, {
subtree: true,
characterData: true,
childList: true,
});
}
const setNotificationCount = useCallback(
(newCount: number) => setCount(newCount),
[],
);
return { setNotificationCount };
}
@@ -0,0 +1,54 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback, useEffect, useState } from 'react';
/** @public */
export function useWebNotifications() {
const [webNotificationPermission, setWebNotificationPermission] =
useState('default');
const [webNotifications, setWebNotifications] = useState<Notification[]>([]);
useEffect(() => {
if ('Notification' in window && webNotificationPermission === 'default') {
window.Notification.requestPermission().then(permission => {
setWebNotificationPermission(permission);
});
}
}, [webNotificationPermission]);
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
webNotifications.forEach(n => n.close());
setWebNotifications([]);
}
});
const sendWebNotification = useCallback(
(options: { title: string; description: string }) => {
if (webNotificationPermission !== 'granted') {
return null;
}
const notification = new Notification(options.title, {
body: options.description,
});
return notification;
},
[webNotificationPermission],
);
return { sendWebNotification };
}