mirror of
https://github.com/sct/overseerr.git
synced 2025-09-28 21:14:27 +02:00
feat: added the PWA badge indicator for requests pending (#3411)
refactor: removed unnecessary code when sending web push notification fix: moved all notify user logic into webpush refactor: n refactor: remove all unnecessary prettier changes fix: n fix: n fix: n fix: n fix: increment sw version fix: n
This commit is contained in:
21
public/sw.js
21
public/sw.js
@@ -3,7 +3,7 @@
|
|||||||
// previously cached resources to be updated from the network.
|
// previously cached resources to be updated from the network.
|
||||||
// This variable is intentionally declared and unused.
|
// This variable is intentionally declared and unused.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const OFFLINE_VERSION = 3;
|
const OFFLINE_VERSION = 4;
|
||||||
const CACHE_NAME = 'offline';
|
const CACHE_NAME = 'offline';
|
||||||
// Customize this with a different URL if needed.
|
// Customize this with a different URL if needed.
|
||||||
const OFFLINE_URL = '/offline.html';
|
const OFFLINE_URL = '/offline.html';
|
||||||
@@ -107,6 +107,25 @@ self.addEventListener('push', (event) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the badge with the amount of pending requests
|
||||||
|
// Only update the badge if the payload confirms they are the admin
|
||||||
|
if (
|
||||||
|
(payload.notificationType === 'MEDIA_APPROVED' ||
|
||||||
|
payload.notificationType === 'MEDIA_DECLINED') &&
|
||||||
|
payload.isAdmin
|
||||||
|
) {
|
||||||
|
if ('setAppBadge' in navigator) {
|
||||||
|
navigator.setAppBadge(payload.pendingRequestsCount);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.notificationType === 'MEDIA_PENDING') {
|
||||||
|
if ('setAppBadge' in navigator) {
|
||||||
|
navigator.setAppBadge(payload.pendingRequestsCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
event.waitUntil(self.registration.showNotification(payload.subject, options));
|
event.waitUntil(self.registration.showNotification(payload.subject, options));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -19,6 +19,8 @@ export interface NotificationPayload {
|
|||||||
request?: MediaRequest;
|
request?: MediaRequest;
|
||||||
issue?: Issue;
|
issue?: Issue;
|
||||||
comment?: IssueComment;
|
comment?: IssueComment;
|
||||||
|
pendingRequestsCount?: number;
|
||||||
|
isAdmin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class BaseAgent<T extends NotificationAgentConfig> {
|
export abstract class BaseAgent<T extends NotificationAgentConfig> {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { IssueType, IssueTypeName } from '@server/constants/issue';
|
import { IssueType, IssueTypeName } from '@server/constants/issue';
|
||||||
import { MediaType } from '@server/constants/media';
|
import { MediaRequestStatus, MediaType } from '@server/constants/media';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
|
import MediaRequest from '@server/entity/MediaRequest';
|
||||||
import { User } from '@server/entity/User';
|
import { User } from '@server/entity/User';
|
||||||
import { UserPushSubscription } from '@server/entity/UserPushSubscription';
|
import { UserPushSubscription } from '@server/entity/UserPushSubscription';
|
||||||
import type { NotificationAgentConfig } from '@server/lib/settings';
|
import type { NotificationAgentConfig } from '@server/lib/settings';
|
||||||
@@ -19,6 +20,8 @@ interface PushNotificationPayload {
|
|||||||
actionUrl?: string;
|
actionUrl?: string;
|
||||||
actionUrlTitle?: string;
|
actionUrlTitle?: string;
|
||||||
requestId?: number;
|
requestId?: number;
|
||||||
|
pendingRequestsCount?: number;
|
||||||
|
isAdmin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebPushAgent
|
class WebPushAgent
|
||||||
@@ -129,6 +132,8 @@ class WebPushAgent
|
|||||||
requestId: payload.request?.id,
|
requestId: payload.request?.id,
|
||||||
actionUrl,
|
actionUrl,
|
||||||
actionUrlTitle,
|
actionUrlTitle,
|
||||||
|
pendingRequestsCount: payload.pendingRequestsCount,
|
||||||
|
isAdmin: payload.isAdmin,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +157,51 @@ class WebPushAgent
|
|||||||
|
|
||||||
const mainUser = await userRepository.findOne({ where: { id: 1 } });
|
const mainUser = await userRepository.findOne({ where: { id: 1 } });
|
||||||
|
|
||||||
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
|
||||||
|
const pendingRequests = await requestRepository.find({
|
||||||
|
where: { status: MediaRequestStatus.PENDING },
|
||||||
|
});
|
||||||
|
|
||||||
|
const webPushNotification = async (
|
||||||
|
pushSub: UserPushSubscription,
|
||||||
|
notificationPayload: Buffer
|
||||||
|
) => {
|
||||||
|
logger.debug('Sending web push notification', {
|
||||||
|
label: 'Notifications',
|
||||||
|
recipient: pushSub.user.displayName,
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await webpush.sendNotification(
|
||||||
|
{
|
||||||
|
endpoint: pushSub.endpoint,
|
||||||
|
keys: {
|
||||||
|
auth: pushSub.auth,
|
||||||
|
p256dh: pushSub.p256dh,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
notificationPayload
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(
|
||||||
|
'Error sending web push notification; removing subscription',
|
||||||
|
{
|
||||||
|
label: 'Notifications',
|
||||||
|
recipient: pushSub.user.displayName,
|
||||||
|
type: Notification[type],
|
||||||
|
subject: payload.subject,
|
||||||
|
errorMessage: e.message,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Failed to send notification so we need to remove the subscription
|
||||||
|
userPushSubRepository.remove(pushSub);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
payload.notifyUser &&
|
payload.notifyUser &&
|
||||||
// Check if user has webpush notifications enabled and fallback to true if undefined
|
// Check if user has webpush notifications enabled and fallback to true if undefined
|
||||||
@@ -169,7 +219,11 @@ class WebPushAgent
|
|||||||
pushSubs.push(...notifySubs);
|
pushSubs.push(...notifySubs);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.notifyAdmin) {
|
if (
|
||||||
|
payload.notifyAdmin ||
|
||||||
|
type === Notification.MEDIA_APPROVED ||
|
||||||
|
type === Notification.MEDIA_DECLINED
|
||||||
|
) {
|
||||||
const users = await userRepository.find();
|
const users = await userRepository.find();
|
||||||
|
|
||||||
const manageUsers = users.filter(
|
const manageUsers = users.filter(
|
||||||
@@ -192,7 +246,42 @@ class WebPushAgent
|
|||||||
})
|
})
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
pushSubs.push(...allSubs);
|
// We only want to send the custom notification when type is approved or declined
|
||||||
|
// Otherwise, default to the normal notification
|
||||||
|
if (
|
||||||
|
type === Notification.MEDIA_APPROVED ||
|
||||||
|
type === Notification.MEDIA_DECLINED
|
||||||
|
) {
|
||||||
|
if (mainUser && allSubs.length > 0) {
|
||||||
|
webpush.setVapidDetails(
|
||||||
|
`mailto:${mainUser.email}`,
|
||||||
|
settings.vapidPublic,
|
||||||
|
settings.vapidPrivate
|
||||||
|
);
|
||||||
|
|
||||||
|
// Custom payload only for updating the app badge
|
||||||
|
const notificationBadgePayload = Buffer.from(
|
||||||
|
JSON.stringify(
|
||||||
|
this.getNotificationPayload(type, {
|
||||||
|
subject: payload.subject,
|
||||||
|
notifySystem: false,
|
||||||
|
notifyAdmin: true,
|
||||||
|
isAdmin: true,
|
||||||
|
pendingRequestsCount: pendingRequests.length,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
allSubs.map(async (sub) => {
|
||||||
|
webPushNotification(sub, notificationBadgePayload);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pushSubs.push(...allSubs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mainUser && pushSubs.length > 0) {
|
if (mainUser && pushSubs.length > 0) {
|
||||||
@@ -202,6 +291,10 @@ class WebPushAgent
|
|||||||
settings.vapidPrivate
|
settings.vapidPrivate
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (type === Notification.MEDIA_PENDING) {
|
||||||
|
payload = { ...payload, pendingRequestsCount: pendingRequests.length };
|
||||||
|
}
|
||||||
|
|
||||||
const notificationPayload = Buffer.from(
|
const notificationPayload = Buffer.from(
|
||||||
JSON.stringify(this.getNotificationPayload(type, payload)),
|
JSON.stringify(this.getNotificationPayload(type, payload)),
|
||||||
'utf-8'
|
'utf-8'
|
||||||
@@ -209,39 +302,7 @@ class WebPushAgent
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
pushSubs.map(async (sub) => {
|
pushSubs.map(async (sub) => {
|
||||||
logger.debug('Sending web push notification', {
|
webPushNotification(sub, notificationPayload);
|
||||||
label: 'Notifications',
|
|
||||||
recipient: sub.user.displayName,
|
|
||||||
type: Notification[type],
|
|
||||||
subject: payload.subject,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await webpush.sendNotification(
|
|
||||||
{
|
|
||||||
endpoint: sub.endpoint,
|
|
||||||
keys: {
|
|
||||||
auth: sub.auth,
|
|
||||||
p256dh: sub.p256dh,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
notificationPayload
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(
|
|
||||||
'Error sending web push notification; removing subscription',
|
|
||||||
{
|
|
||||||
label: 'Notifications',
|
|
||||||
recipient: sub.user.displayName,
|
|
||||||
type: Notification[type],
|
|
||||||
subject: payload.subject,
|
|
||||||
errorMessage: e.message,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Failed to send notification so we need to remove the subscription
|
|
||||||
userPushSubRepository.remove(sub);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -240,9 +240,11 @@ const MobileMenu = ({
|
|||||||
router.pathname.match(link.activeRegExp)
|
router.pathname.match(link.activeRegExp)
|
||||||
? 'border-indigo-600 from-indigo-700 to-purple-700'
|
? 'border-indigo-600 from-indigo-700 to-purple-700'
|
||||||
: 'border-indigo-500 from-indigo-600 to-purple-600'
|
: 'border-indigo-500 from-indigo-600 to-purple-600'
|
||||||
} !px-1 !py-[1px] leading-none`}
|
} flex h-4 w-4 items-center justify-center !px-[9px] !py-[9px] text-[9px]`}
|
||||||
>
|
>
|
||||||
{pendingRequestsCount}
|
{pendingRequestsCount > 99
|
||||||
|
? '99+'
|
||||||
|
: pendingRequestsCount}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@@ -11,6 +11,7 @@ import { LanguageContext } from '@app/context/LanguageContext';
|
|||||||
import { SettingsProvider } from '@app/context/SettingsContext';
|
import { SettingsProvider } from '@app/context/SettingsContext';
|
||||||
import { UserContext } from '@app/context/UserContext';
|
import { UserContext } from '@app/context/UserContext';
|
||||||
import type { User } from '@app/hooks/useUser';
|
import type { User } from '@app/hooks/useUser';
|
||||||
|
import { Permission, useUser } from '@app/hooks/useUser';
|
||||||
import '@app/styles/globals.css';
|
import '@app/styles/globals.css';
|
||||||
import { polyfillIntl } from '@app/utils/polyfillIntl';
|
import { polyfillIntl } from '@app/utils/polyfillIntl';
|
||||||
import type { PublicSettingsResponse } from '@server/interfaces/api/settingsInterfaces';
|
import type { PublicSettingsResponse } from '@server/interfaces/api/settingsInterfaces';
|
||||||
@@ -127,6 +128,35 @@ const CoreApp: Omit<NextAppComponentType, 'origGetInitialProps'> = ({
|
|||||||
loadLocaleData(currentLocale).then(setMessages);
|
loadLocaleData(currentLocale).then(setMessages);
|
||||||
}, [currentLocale]);
|
}, [currentLocale]);
|
||||||
|
|
||||||
|
const { hasPermission } = useUser();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const requestsCount = async () => {
|
||||||
|
const response = await axios.get('/api/v1/request/count');
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cast navigator to a type that includes setAppBadge and clearAppBadge
|
||||||
|
// to avoid TypeScript errors while ensuring these methods exist before calling them.
|
||||||
|
const newNavigator = navigator as unknown as {
|
||||||
|
setAppBadge?: (count: number) => Promise<void>;
|
||||||
|
clearAppBadge?: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
if ('setAppBadge' in navigator) {
|
||||||
|
if (
|
||||||
|
!router.pathname.match(/(login|setup|resetpassword)/) &&
|
||||||
|
hasPermission(Permission.ADMIN)
|
||||||
|
) {
|
||||||
|
requestsCount().then((data) =>
|
||||||
|
newNavigator?.setAppBadge?.(data.pending)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
newNavigator?.clearAppBadge?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [hasPermission, router.pathname]);
|
||||||
|
|
||||||
if (router.pathname.match(/(login|setup|resetpassword)/)) {
|
if (router.pathname.match(/(login|setup|resetpassword)/)) {
|
||||||
component = <Component {...pageProps} />;
|
component = <Component {...pageProps} />;
|
||||||
} else {
|
} else {
|
||||||
|
Reference in New Issue
Block a user