feat: add support for unread count in document title
Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
@@ -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)
|
||||
```
|
||||
|
||||
Vendored
+31
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -51,6 +51,8 @@
|
||||
"msw": "^1.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
"dist",
|
||||
"config.d.ts"
|
||||
],
|
||||
"configSchema": "config.d.ts"
|
||||
}
|
||||
|
||||
+33
-33
@@ -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 (
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user