You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
overseerr/src/components/NotificationTypeSelector/index.tsx

324 lines
11 KiB

import { sortBy } from 'lodash';
import React, { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSettings from '../../hooks/useSettings';
import { Permission, User, useUser } from '../../hooks/useUser';
import NotificationType from './NotificationType';
const messages = defineMessages({
notificationTypes: 'Notification Types',
mediarequested: 'Media Requested',
mediarequestedDescription:
'Send notifications when users submit new media requests which require approval.',
usermediarequestedDescription:
'Get notified when other users submit new media requests which require approval.',
mediaapproved: 'Media Approved',
mediaapprovedDescription:
'Send notifications when media requests are manually approved.',
usermediaapprovedDescription:
'Get notified when your media requests are approved.',
mediaAutoApproved: 'Media Automatically Approved',
mediaAutoApprovedDescription:
'Send notifications when users submit new media requests which are automatically approved.',
usermediaAutoApprovedDescription:
'Get notified when other users submit new media requests which are automatically approved.',
mediaavailable: 'Media Available',
mediaavailableDescription:
'Send notifications when media requests become available.',
usermediaavailableDescription:
'Get notified when your media requests become available.',
mediafailed: 'Media Failed',
mediafailedDescription:
'Send notifications when media requests fail to be added to Radarr or Sonarr.',
usermediafailedDescription:
'Get notified when media requests fail to be added to Radarr or Sonarr.',
mediadeclined: 'Media Declined',
mediadeclinedDescription:
'Send notifications when media requests are declined.',
usermediadeclinedDescription:
'Get notified when your media requests are declined.',
issuecreated: 'Issue Created',
issuecreatedDescription: 'Send notifications when new issues are created.',
issuecomment: 'Issue Comment',
issuecommentDescription:
'Send notifications when issues receive new comments.',
userissuecommentDescription:
'Send notifications when your issue receives new comments.',
issueresolved: 'Issue Resolved',
issueresolvedDescription: 'Send notifications when issues are resolved.',
userissueresolvedDescription:
'Send notifications when your issues are resolved.',
});
export const hasNotificationType = (
types: Notification | Notification[],
value: number
): boolean => {
let total = 0;
// If we are not checking any notifications, bail out and return true
if (types === 0) {
return true;
}
if (Array.isArray(types)) {
// Combine all notification values into one
total = types.reduce((a, v) => a + v, 0);
} else {
total = types;
}
// Test notifications don't need to be enabled
if (!(value & Notification.TEST_NOTIFICATION)) {
value += Notification.TEST_NOTIFICATION;
}
return !!(value & total);
};
export enum Notification {
NONE = 0,
MEDIA_PENDING = 2,
MEDIA_APPROVED = 4,
MEDIA_AVAILABLE = 8,
MEDIA_FAILED = 16,
TEST_NOTIFICATION = 32,
MEDIA_DECLINED = 64,
MEDIA_AUTO_APPROVED = 128,
ISSUE_CREATED = 256,
ISSUE_COMMENT = 512,
ISSUE_RESOLVED = 1024,
}
export const ALL_NOTIFICATIONS = Object.values(Notification)
.filter((v) => !isNaN(Number(v)))
.reduce((a, v) => a + Number(v), 0);
export interface NotificationItem {
id: string;
name: string;
description: string;
value: Notification;
hasNotifyUser?: boolean;
children?: NotificationItem[];
hidden?: boolean;
}
interface NotificationTypeSelectorProps {
user?: User;
enabledTypes?: number;
currentTypes: number;
onUpdate: (newTypes: number) => void;
error?: string;
}
const NotificationTypeSelector: React.FC<NotificationTypeSelectorProps> = ({
user,
enabledTypes = ALL_NOTIFICATIONS,
currentTypes,
onUpdate,
error,
}) => {
const intl = useIntl();
const settings = useSettings();
const { hasPermission } = useUser({ id: user?.id });
const [allowedTypes, setAllowedTypes] = useState(enabledTypes);
const availableTypes = useMemo(() => {
const allRequestsAutoApproved =
user &&
// Has Manage Requests perm, which grants all Auto-Approve perms
(hasPermission(Permission.MANAGE_REQUESTS) ||
// Cannot submit requests of any type
!hasPermission(
[
Permission.REQUEST,
Permission.REQUEST_MOVIE,
Permission.REQUEST_TV,
Permission.REQUEST_4K,
Permission.REQUEST_4K_MOVIE,
Permission.REQUEST_4K_TV,
],
{ type: 'or' }
) ||
// Cannot submit non-4K movie requests OR has Auto-Approve perms for non-4K movies
((!hasPermission([Permission.REQUEST, Permission.REQUEST_MOVIE], {
type: 'or',
}) ||
hasPermission(
[Permission.AUTO_APPROVE, Permission.AUTO_APPROVE_MOVIE],
{ type: 'or' }
)) &&
// Cannot submit non-4K series requests OR has Auto-Approve perms for non-4K series
(!hasPermission([Permission.REQUEST, Permission.REQUEST_TV], {
type: 'or',
}) ||
hasPermission(
[Permission.AUTO_APPROVE, Permission.AUTO_APPROVE_TV],
{ type: 'or' }
)) &&
// Cannot submit 4K movie requests OR has Auto-Approve perms for 4K movies
(!settings.currentSettings.movie4kEnabled ||
!hasPermission(
[Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE],
{ type: 'or' }
) ||
hasPermission(
[Permission.AUTO_APPROVE_4K, Permission.AUTO_APPROVE_4K_MOVIE],
{ type: 'or' }
)) &&
// Cannot submit 4K series requests OR has Auto-Approve perms for 4K series
(!settings.currentSettings.series4kEnabled ||
!hasPermission([Permission.REQUEST_4K, Permission.REQUEST_4K_TV], {
type: 'or',
}) ||
hasPermission(
[Permission.AUTO_APPROVE_4K, Permission.AUTO_APPROVE_4K_TV],
{ type: 'or' }
))));
const types: NotificationItem[] = [
{
id: 'media-requested',
name: intl.formatMessage(messages.mediarequested),
description: intl.formatMessage(
user
? messages.usermediarequestedDescription
: messages.mediarequestedDescription
),
value: Notification.MEDIA_PENDING,
hidden: user && !hasPermission(Permission.MANAGE_REQUESTS),
},
{
id: 'media-auto-approved',
name: intl.formatMessage(messages.mediaAutoApproved),
description: intl.formatMessage(
user
? messages.usermediaAutoApprovedDescription
: messages.mediaAutoApprovedDescription
),
value: Notification.MEDIA_AUTO_APPROVED,
hidden: user && !hasPermission(Permission.MANAGE_REQUESTS),
},
{
id: 'media-approved',
name: intl.formatMessage(messages.mediaapproved),
description: intl.formatMessage(
user
? messages.usermediaapprovedDescription
: messages.mediaapprovedDescription
),
value: Notification.MEDIA_APPROVED,
hasNotifyUser: true,
hidden: allRequestsAutoApproved,
},
{
id: 'media-declined',
name: intl.formatMessage(messages.mediadeclined),
description: intl.formatMessage(
user
? messages.usermediadeclinedDescription
: messages.mediadeclinedDescription
),
value: Notification.MEDIA_DECLINED,
hasNotifyUser: true,
hidden: allRequestsAutoApproved,
},
{
id: 'media-available',
name: intl.formatMessage(messages.mediaavailable),
description: intl.formatMessage(
user
? messages.usermediaavailableDescription
: messages.mediaavailableDescription
),
value: Notification.MEDIA_AVAILABLE,
hasNotifyUser: true,
},
{
id: 'media-failed',
name: intl.formatMessage(messages.mediafailed),
description: intl.formatMessage(
user
? messages.usermediafailedDescription
: messages.mediafailedDescription
),
value: Notification.MEDIA_FAILED,
hidden: user && !hasPermission(Permission.MANAGE_REQUESTS),
},
{
id: 'issue-created',
name: intl.formatMessage(messages.issuecreated),
description: intl.formatMessage(messages.issuecreatedDescription),
value: Notification.ISSUE_CREATED,
hidden: user && !hasPermission(Permission.MANAGE_ISSUES),
},
{
id: 'issue-comment',
name: intl.formatMessage(messages.issuecomment),
description: intl.formatMessage(
user
? messages.userissuecommentDescription
: messages.issuecommentDescription
),
value: Notification.ISSUE_COMMENT,
hasNotifyUser: true,
},
{
id: 'issue-resolved',
name: intl.formatMessage(messages.issueresolved),
description: intl.formatMessage(
user
? messages.userissueresolvedDescription
: messages.issueresolvedDescription
),
value: Notification.ISSUE_RESOLVED,
hasNotifyUser: true,
},
];
const filteredTypes = types.filter(
(type) => !type.hidden && hasNotificationType(type.value, enabledTypes)
);
const newAllowedTypes = filteredTypes.reduce((a, v) => a + v.value, 0);
if (newAllowedTypes !== allowedTypes) {
setAllowedTypes(newAllowedTypes);
}
return user
? sortBy(filteredTypes, 'hasNotifyUser', 'DESC')
: filteredTypes;
}, [user, hasPermission, settings, intl, allowedTypes, enabledTypes]);
if (!availableTypes.length) {
return null;
}
return (
<div role="group" aria-labelledby="group-label" className="form-group">
<div className="form-row">
<span id="group-label" className="group-label">
{intl.formatMessage(messages.notificationTypes)}
{!user && <span className="label-required">*</span>}
</span>
<div className="form-input">
<div className="max-w-lg">
{availableTypes.map((type) => (
<NotificationType
key={`notification-type-${type.id}`}
option={type}
currentTypes={currentTypes}
onUpdate={onUpdate}
/>
))}
</div>
{error && <div className="error">{error}</div>}
</div>
</div>
</div>
);
};
export default NotificationTypeSelector;