feat: allow users to select notification types (#1512)

* feat: allow users to select notification types

* fix(ui): display personal notification types before management types

* fix: update allRequestsAutoApproved check to account for new REQUEST_MOVIE & REQUEST_TV perms

* fix(ui): do not display Discord notif type selector if user not eligible for any types

* refactor(ui): remove unnecessary 'enabled' checkboxes from user notif settings

* fix(ui): correct checkbox behavior

* fix: add missing return type on hasNotificationType

* refactor: remove unused isValid prop in NotificationsWebPush

* fix(ui): use SensitiveInput for users' public PGP keys

* fix(ui): add missing tip/hint for email encryption setting

* refactor(svg): use the new Discord logo

* revert(api): undo breaking change removing discordEnabled from UserSettingsNotificationsResponse

* fix(lang): update notification type descriptions for clarity

* fix(telegram): do not send users notifications of their own auto-approved requests
This commit is contained in:
TheCatLady
2021-06-04 06:31:05 -04:00
committed by GitHub
parent 6a3649f620
commit e60598905b
35 changed files with 1072 additions and 653 deletions

View File

@@ -38,7 +38,7 @@ const NotificationType: React.FC<NotificationTypeProps> = ({
: currentTypes + option.value
);
}}
defaultChecked={
checked={
hasNotificationType(option.value, currentTypes) ||
(!!parent?.value &&
hasNotificationType(parent.value, currentTypes))
@@ -46,10 +46,12 @@ const NotificationType: React.FC<NotificationTypeProps> = ({
/>
</div>
<div className="ml-3 text-sm leading-6">
<label htmlFor={option.id} className="font-medium text-white">
{option.name}
<label htmlFor={option.id} className="block font-medium text-white">
<div className="flex flex-col">
<span>{option.name}</span>
<span className="text-gray-500">{option.description}</span>
</div>
</label>
<p className="text-gray-500">{option.description}</p>
</div>
</div>
{(option.children ?? []).map((child) => (

View File

@@ -1,27 +1,42 @@
import React from 'react';
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:
'Sends a notification when media is requested and requires approval.',
'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:
'Sends a notification when requested media is manually approved.',
'Send notifications when media requests are manually approved.',
usermediaapprovedDescription:
'Get notified when your media requests are approved.',
mediaAutoApproved: 'Media Automatically Approved',
mediaAutoApprovedDescription:
'Sends a notification when requested media is automatically approved.',
'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:
'Sends a notification when requested media becomes available.',
'Send notifications when media requests become available.',
usermediaavailableDescription:
'Get notified when your media requests become available.',
mediafailed: 'Media Failed',
mediafailedDescription:
'Sends a notification when requested media fails to be added to Radarr or Sonarr.',
'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:
'Sends a notification when a media request is declined.',
'Send notifications when media requests are declined.',
usermediadeclinedDescription:
'Get notified when your media requests are declined.',
});
export const hasNotificationType = (
@@ -30,16 +45,23 @@ export const hasNotificationType = (
): 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);
};
@@ -63,69 +85,183 @@ export interface NotificationItem {
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 types: NotificationItem[] = [
{
id: 'media-requested',
name: intl.formatMessage(messages.mediarequested),
description: intl.formatMessage(messages.mediarequestedDescription),
value: Notification.MEDIA_PENDING,
},
{
id: 'media-auto-approved',
name: intl.formatMessage(messages.mediaAutoApproved),
description: intl.formatMessage(messages.mediaAutoApprovedDescription),
value: Notification.MEDIA_AUTO_APPROVED,
},
{
id: 'media-approved',
name: intl.formatMessage(messages.mediaapproved),
description: intl.formatMessage(messages.mediaapprovedDescription),
value: Notification.MEDIA_APPROVED,
},
{
id: 'media-declined',
name: intl.formatMessage(messages.mediadeclined),
description: intl.formatMessage(messages.mediadeclinedDescription),
value: Notification.MEDIA_DECLINED,
},
{
id: 'media-available',
name: intl.formatMessage(messages.mediaavailable),
description: intl.formatMessage(messages.mediaavailableDescription),
value: Notification.MEDIA_AVAILABLE,
},
{
id: 'media-failed',
name: intl.formatMessage(messages.mediafailed),
description: intl.formatMessage(messages.mediafailedDescription),
value: Notification.MEDIA_FAILED,
},
];
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),
},
];
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)}
<span className="label-required">*</span>
{!user && <span className="label-required">*</span>}
</span>
<div className="form-input">
<div className="max-w-lg">
{types.map((type) => (
{availableTypes.map((type) => (
<NotificationType
key={`notification-type-${type.id}`}
option={type}
@@ -134,6 +270,7 @@ const NotificationTypeSelector: React.FC<NotificationTypeSelectorProps> = ({
/>
))}
</div>
{error && <div className="error">{error}</div>}
</div>
</div>
</div>

View File

@@ -23,6 +23,7 @@ const messages = defineMessages({
toastDiscordTestSuccess: 'Discord test notification sent!',
toastDiscordTestFailed: 'Discord test notification failed to send.',
validationUrl: 'You must provide a valid URL',
validationTypes: 'You must select at least one notification type',
});
const NotificationsDiscord: React.FC = () => {
@@ -46,6 +47,13 @@ const NotificationsDiscord: React.FC = () => {
otherwise: Yup.string().nullable(),
})
.url(intl.formatMessage(messages.validationUrl)),
types: Yup.number().when('enabled', {
is: true,
then: Yup.number()
.nullable()
.moreThan(0, intl.formatMessage(messages.validationTypes)),
otherwise: Yup.number().nullable(),
}),
});
if (!data && !error) {
@@ -88,7 +96,15 @@ const NotificationsDiscord: React.FC = () => {
}
}}
>
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
{({
errors,
touched,
isSubmitting,
values,
isValid,
setFieldValue,
setFieldTouched,
}) => {
const testSettings = async () => {
setIsTesting(true);
let toastId: string | undefined;
@@ -211,8 +227,20 @@ const NotificationsDiscord: React.FC = () => {
</div>
</div>
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
currentTypes={values.enabled ? values.types : 0}
onUpdate={(newTypes) => {
setFieldValue('types', newTypes);
setFieldTouched('types');
if (newTypes) {
setFieldValue('enabled', true);
}
}}
error={
errors.types && touched.types
? (errors.types as string)
: undefined
}
/>
<div className="actions">
<div className="flex justify-end">

View File

@@ -6,12 +6,10 @@ import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import globalMessages from '../../../i18n/globalMessages';
import Alert from '../../Common/Alert';
import Badge from '../../Common/Badge';
import Button from '../../Common/Button';
import LoadingSpinner from '../../Common/LoadingSpinner';
import SensitiveInput from '../../Common/SensitiveInput';
import NotificationTypeSelector from '../../NotificationTypeSelector';
const messages = defineMessages({
validationSmtpHostRequired: 'You must provide a valid hostname or IP address',
@@ -37,20 +35,14 @@ const messages = defineMessages({
allowselfsigned: 'Allow Self-Signed Certificates',
senderName: 'Sender Name',
validationEmail: 'You must provide a valid email address',
emailNotificationTypesAlertDescription:
'<strong>Media Requested</strong>, <strong>Media Automatically Approved</strong>, and <strong>Media Failed</strong> email notifications are sent to all users with the <strong>Manage Requests</strong> permission.',
emailNotificationTypesAlertDescriptionPt2:
'<strong>Media Approved</strong>, <strong>Media Declined</strong>, and <strong>Media Available</strong> email notifications are sent to the user who submitted the request.',
pgpPrivateKey: 'PGP Private Key',
pgpPrivateKeyTip:
'Sign encrypted email messages using <OpenPgpLink>OpenPGP</OpenPgpLink>',
validationPgpPrivateKey:
'You must provide a valid PGP private key if a PGP password is entered',
validationPgpPrivateKey: 'You must provide a valid PGP private key',
pgpPassword: 'PGP Password',
pgpPasswordTip:
'Sign encrypted email messages using <OpenPgpLink>OpenPGP</OpenPgpLink>',
validationPgpPassword:
'You must provide a PGP password if a PGP private key is entered',
validationPgpPassword: 'You must provide a PGP password',
});
export function OpenPgpLink(msg: string): JSX.Element {
@@ -130,7 +122,6 @@ const NotificationsEmail: React.FC = () => {
<Formik
initialValues={{
enabled: data.enabled,
types: data.types,
emailFrom: data.options.emailFrom,
smtpHost: data.options.smtpHost,
smtpPort: data.options.smtpPort ?? 587,
@@ -153,7 +144,6 @@ const NotificationsEmail: React.FC = () => {
try {
await axios.post('/api/v1/settings/notifications/email', {
enabled: values.enabled,
types: values.types,
options: {
emailFrom: values.emailFrom,
smtpHost: values.smtpHost,
@@ -184,7 +174,7 @@ const NotificationsEmail: React.FC = () => {
}
}}
>
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
{({ errors, touched, isSubmitting, values, isValid }) => {
const testSettings = async () => {
setIsTesting(true);
let toastId: string | undefined;
@@ -201,7 +191,6 @@ const NotificationsEmail: React.FC = () => {
);
await axios.post('/api/v1/settings/notifications/email/test', {
enabled: true,
types: values.types,
options: {
emailFrom: values.emailFrom,
smtpHost: values.smtpHost,
@@ -238,274 +227,234 @@ const NotificationsEmail: React.FC = () => {
};
return (
<>
<Alert
title={
<>
<p className="mb-2">
{intl.formatMessage(
messages.emailNotificationTypesAlertDescription,
{
strong: function strong(msg) {
return (
<strong className="font-semibold text-indigo-100">
{msg}
</strong>
);
},
}
)}
</p>
<p>
{intl.formatMessage(
messages.emailNotificationTypesAlertDescriptionPt2,
{
strong: function strong(msg) {
return (
<strong className="font-semibold text-indigo-100">
{msg}
</strong>
);
},
}
)}
</p>
</>
}
type="info"
/>
<Form className="section">
<div className="form-row">
<label htmlFor="enabled" className="checkbox-label">
{intl.formatMessage(messages.agentenabled)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<Field type="checkbox" id="enabled" name="enabled" />
<Form className="section">
<div className="form-row">
<label htmlFor="enabled" className="checkbox-label">
{intl.formatMessage(messages.agentenabled)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<Field type="checkbox" id="enabled" name="enabled" />
</div>
</div>
<div className="form-row">
<label htmlFor="senderName" className="text-label">
{intl.formatMessage(messages.senderName)}
</label>
<div className="form-input">
<div className="form-input-field">
<Field id="senderName" name="senderName" type="text" />
</div>
</div>
<div className="form-row">
<label htmlFor="senderName" className="text-label">
{intl.formatMessage(messages.senderName)}
</label>
<div className="form-input">
<div className="form-input-field">
<Field id="senderName" name="senderName" type="text" />
</div>
</div>
</div>
<div className="form-row">
<label htmlFor="emailFrom" className="text-label">
{intl.formatMessage(messages.emailsender)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field
id="emailFrom"
name="emailFrom"
type="text"
inputMode="email"
/>
</div>
{errors.emailFrom && touched.emailFrom && (
<div className="error">{errors.emailFrom}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="smtpHost" className="text-label">
{intl.formatMessage(messages.smtpHost)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field
id="smtpHost"
name="smtpHost"
type="text"
inputMode="url"
/>
</div>
{errors.smtpHost && touched.smtpHost && (
<div className="error">{errors.smtpHost}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="smtpPort" className="text-label">
{intl.formatMessage(messages.smtpPort)}
<span className="label-required">*</span>
</label>
<div className="form-input">
</div>
<div className="form-row">
<label htmlFor="emailFrom" className="text-label">
{intl.formatMessage(messages.emailsender)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field
id="smtpPort"
name="smtpPort"
id="emailFrom"
name="emailFrom"
type="text"
inputMode="numeric"
className="short"
inputMode="email"
/>
{errors.smtpPort && touched.smtpPort && (
<div className="error">{errors.smtpPort}</div>
)}
</div>
{errors.emailFrom && touched.emailFrom && (
<div className="error">{errors.emailFrom}</div>
)}
</div>
<div className="form-row">
<label htmlFor="encryption" className="text-label">
{intl.formatMessage(messages.encryption)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field as="select" id="encryption" name="encryption">
<option value="none">
{intl.formatMessage(messages.encryptionNone)}
</option>
<option value="default">
{intl.formatMessage(messages.encryptionDefault)}
</option>
<option value="opportunistic">
{intl.formatMessage(
messages.encryptionOpportunisticTls
)}
</option>
<option value="implicit">
{intl.formatMessage(messages.encryptionImplicitTls)}
</option>
</Field>
</div>
</div>
</div>
<div className="form-row">
<label htmlFor="allowSelfSigned" className="checkbox-label">
{intl.formatMessage(messages.allowselfsigned)}
</label>
<div className="form-input">
</div>
<div className="form-row">
<label htmlFor="smtpHost" className="text-label">
{intl.formatMessage(messages.smtpHost)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field
type="checkbox"
id="allowSelfSigned"
name="allowSelfSigned"
id="smtpHost"
name="smtpHost"
type="text"
inputMode="url"
/>
</div>
{errors.smtpHost && touched.smtpHost && (
<div className="error">{errors.smtpHost}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="smtpPort" className="text-label">
{intl.formatMessage(messages.smtpPort)}
<span className="label-required">*</span>
</label>
<div className="form-input">
<Field
id="smtpPort"
name="smtpPort"
type="text"
inputMode="numeric"
className="short"
/>
{errors.smtpPort && touched.smtpPort && (
<div className="error">{errors.smtpPort}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="encryption" className="text-label">
{intl.formatMessage(messages.encryption)}
<span className="label-required">*</span>
<span className="label-tip">
{intl.formatMessage(messages.encryptionTip)}
</span>
</label>
<div className="form-input">
<div className="form-input-field">
<Field as="select" id="encryption" name="encryption">
<option value="none">
{intl.formatMessage(messages.encryptionNone)}
</option>
<option value="default">
{intl.formatMessage(messages.encryptionDefault)}
</option>
<option value="opportunistic">
{intl.formatMessage(messages.encryptionOpportunisticTls)}
</option>
<option value="implicit">
{intl.formatMessage(messages.encryptionImplicitTls)}
</option>
</Field>
</div>
</div>
</div>
<div className="form-row">
<label htmlFor="allowSelfSigned" className="checkbox-label">
{intl.formatMessage(messages.allowselfsigned)}
</label>
<div className="form-input">
<Field
type="checkbox"
id="allowSelfSigned"
name="allowSelfSigned"
/>
</div>
</div>
<div className="form-row">
<label htmlFor="authUser" className="text-label">
{intl.formatMessage(messages.authUser)}
</label>
<div className="form-input">
<div className="form-input-field">
<Field id="authUser" name="authUser" type="text" />
</div>
</div>
</div>
<div className="form-row">
<label htmlFor="authPass" className="text-label">
{intl.formatMessage(messages.authPass)}
</label>
<div className="form-input">
<div className="form-input-field">
<SensitiveInput
as="field"
id="authPass"
name="authPass"
autoComplete="one-time-code"
/>
</div>
</div>
<div className="form-row">
<label htmlFor="authUser" className="text-label">
{intl.formatMessage(messages.authUser)}
</label>
<div className="form-input">
<div className="form-input-field">
<Field id="authUser" name="authUser" type="text" />
</div>
</div>
<div className="form-row">
<label htmlFor="pgpPrivateKey" className="text-label">
<span className="mr-2">
{intl.formatMessage(messages.pgpPrivateKey)}
</span>
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.advanced)}
</Badge>
<span className="label-tip">
{intl.formatMessage(messages.pgpPrivateKeyTip, {
OpenPgpLink: OpenPgpLink,
})}
</span>
</label>
<div className="form-input">
<div className="form-input-field">
<SensitiveInput
as="field"
id="pgpPrivateKey"
name="pgpPrivateKey"
type="textarea"
rows="10"
className="font-mono text-xs"
/>
</div>
{errors.pgpPrivateKey && touched.pgpPrivateKey && (
<div className="error">{errors.pgpPrivateKey}</div>
)}
</div>
<div className="form-row">
<label htmlFor="authPass" className="text-label">
{intl.formatMessage(messages.authPass)}
</label>
<div className="form-input">
<div className="form-input-field">
<SensitiveInput
as="field"
id="authPass"
name="authPass"
autoComplete="one-time-code"
/>
</div>
</div>
<div className="form-row">
<label htmlFor="pgpPassword" className="text-label">
<span className="mr-2">
{intl.formatMessage(messages.pgpPassword)}
</span>
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.advanced)}
</Badge>
<span className="label-tip">
{intl.formatMessage(messages.pgpPasswordTip, {
OpenPgpLink: OpenPgpLink,
})}
</span>
</label>
<div className="form-input">
<div className="form-input-field">
<SensitiveInput
as="field"
id="pgpPassword"
name="pgpPassword"
autoComplete="one-time-code"
/>
</div>
{errors.pgpPassword && touched.pgpPassword && (
<div className="error">{errors.pgpPassword}</div>
)}
</div>
<div className="form-row">
<label htmlFor="pgpPrivateKey" className="text-label">
<span className="mr-2">
{intl.formatMessage(messages.pgpPrivateKey)}
</span>
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.advanced)}
</Badge>
<span className="label-tip">
{intl.formatMessage(messages.pgpPrivateKeyTip, {
OpenPgpLink: OpenPgpLink,
})}
</span>
</label>
<div className="form-input">
<div className="form-input-field">
<SensitiveInput
as="field"
id="pgpPrivateKey"
name="pgpPrivateKey"
type="textarea"
rows="10"
className="font-mono text-xs"
/>
</div>
{errors.pgpPrivateKey && touched.pgpPrivateKey && (
<div className="error">{errors.pgpPrivateKey}</div>
)}
</div>
</div>
<div className="actions">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="warning"
disabled={isSubmitting || !isValid || isTesting}
onClick={(e) => {
e.preventDefault();
testSettings();
}}
>
{isTesting
? intl.formatMessage(globalMessages.testing)
: intl.formatMessage(globalMessages.test)}
</Button>
</span>
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid || isTesting}
>
{isSubmitting
? intl.formatMessage(globalMessages.saving)
: intl.formatMessage(globalMessages.save)}
</Button>
</span>
</div>
<div className="form-row">
<label htmlFor="pgpPassword" className="text-label">
<span className="mr-2">
{intl.formatMessage(messages.pgpPassword)}
</span>
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.advanced)}
</Badge>
<span className="label-tip">
{intl.formatMessage(messages.pgpPasswordTip, {
OpenPgpLink: OpenPgpLink,
})}
</span>
</label>
<div className="form-input">
<div className="form-input-field">
<SensitiveInput
as="field"
id="pgpPassword"
name="pgpPassword"
autoComplete="one-time-code"
/>
</div>
{errors.pgpPassword && touched.pgpPassword && (
<div className="error">{errors.pgpPassword}</div>
)}
</div>
</div>
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
/>
<div className="actions">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="warning"
disabled={isSubmitting || !isValid || isTesting}
onClick={(e) => {
e.preventDefault();
testSettings();
}}
>
{isTesting
? intl.formatMessage(globalMessages.testing)
: intl.formatMessage(globalMessages.test)}
</Button>
</span>
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid || isTesting}
>
{isSubmitting
? intl.formatMessage(globalMessages.saving)
: intl.formatMessage(globalMessages.save)}
</Button>
</span>
</div>
</div>
</Form>
</>
</div>
</Form>
);
}}
</Formik>

View File

@@ -23,6 +23,7 @@ const messages = defineMessages({
toastLunaSeaTestSending: 'Sending LunaSea test notification…',
toastLunaSeaTestSuccess: 'LunaSea test notification sent!',
toastLunaSeaTestFailed: 'LunaSea test notification failed to send.',
validationTypes: 'You must select at least one notification type',
});
const NotificationsLunaSea: React.FC = () => {
@@ -43,6 +44,13 @@ const NotificationsLunaSea: React.FC = () => {
otherwise: Yup.string().nullable(),
})
.url(intl.formatMessage(messages.validationWebhookUrl)),
types: Yup.number().when('enabled', {
is: true,
then: Yup.number()
.nullable()
.moreThan(0, intl.formatMessage(messages.validationTypes)),
otherwise: Yup.number().nullable(),
}),
});
if (!data && !error) {
@@ -82,7 +90,15 @@ const NotificationsLunaSea: React.FC = () => {
}
}}
>
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
{({
errors,
touched,
isSubmitting,
values,
isValid,
setFieldValue,
setFieldTouched,
}) => {
const testSettings = async () => {
setIsTesting(true);
let toastId: string | undefined;
@@ -190,8 +206,20 @@ const NotificationsLunaSea: React.FC = () => {
</div>
</div>
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
currentTypes={values.enabled ? values.types : 0}
onUpdate={(newTypes) => {
setFieldValue('types', newTypes);
setFieldTouched('types');
if (newTypes) {
setFieldValue('enabled', true);
}
}}
error={
errors.types && touched.types
? (errors.types as string)
: undefined
}
/>
<div className="actions">
<div className="flex justify-end">

View File

@@ -23,6 +23,7 @@ const messages = defineMessages({
toastPushbulletTestSending: 'Sending Pushbullet test notification…',
toastPushbulletTestSuccess: 'Pushbullet test notification sent!',
toastPushbulletTestFailed: 'Pushbullet test notification failed to send.',
validationTypes: 'You must select at least one notification type',
});
const NotificationsPushbullet: React.FC = () => {
@@ -41,6 +42,13 @@ const NotificationsPushbullet: React.FC = () => {
.required(intl.formatMessage(messages.validationAccessTokenRequired)),
otherwise: Yup.string().nullable(),
}),
types: Yup.number().when('enabled', {
is: true,
then: Yup.number()
.nullable()
.moreThan(0, intl.formatMessage(messages.validationTypes)),
otherwise: Yup.number().nullable(),
}),
});
if (!data && !error) {
@@ -78,7 +86,15 @@ const NotificationsPushbullet: React.FC = () => {
}
}}
>
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
{({
errors,
touched,
isSubmitting,
values,
isValid,
setFieldValue,
setFieldTouched,
}) => {
const testSettings = async () => {
setIsTesting(true);
let toastId: string | undefined;
@@ -170,8 +186,20 @@ const NotificationsPushbullet: React.FC = () => {
</div>
</div>
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
currentTypes={values.enabled ? values.types : 0}
onUpdate={(newTypes) => {
setFieldValue('types', newTypes);
setFieldTouched('types');
if (newTypes) {
setFieldValue('enabled', true);
}
}}
error={
errors.types && touched.types
? (errors.types as string)
: undefined
}
/>
<div className="actions">
<div className="flex justify-end">

View File

@@ -19,12 +19,13 @@ const messages = defineMessages({
userTokenTip:
'Your 30-character <UsersGroupsLink>user or group identifier</UsersGroupsLink>',
validationAccessTokenRequired: 'You must provide a valid application token',
validationUserTokenRequired: 'You must provide a valid user key',
validationUserTokenRequired: 'You must provide a valid user or group key',
pushoversettingssaved: 'Pushover notification settings saved successfully!',
pushoversettingsfailed: 'Pushover notification settings failed to save.',
toastPushoverTestSending: 'Sending Pushover test notification…',
toastPushoverTestSuccess: 'Pushover test notification sent!',
toastPushoverTestFailed: 'Pushover test notification failed to send.',
validationTypes: 'You must select at least one notification type',
});
const NotificationsPushover: React.FC = () => {
@@ -60,6 +61,13 @@ const NotificationsPushover: React.FC = () => {
/^[a-z\d]{30}$/i,
intl.formatMessage(messages.validationUserTokenRequired)
),
types: Yup.number().when('enabled', {
is: true,
then: Yup.number()
.nullable()
.moreThan(0, intl.formatMessage(messages.validationTypes)),
otherwise: Yup.number().nullable(),
}),
});
if (!data && !error) {
@@ -99,7 +107,15 @@ const NotificationsPushover: React.FC = () => {
}
}}
>
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
{({
errors,
touched,
isSubmitting,
values,
isValid,
setFieldValue,
setFieldTouched,
}) => {
const testSettings = async () => {
setIsTesting(true);
let toastId: string | undefined;
@@ -216,8 +232,20 @@ const NotificationsPushover: React.FC = () => {
</div>
</div>
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
currentTypes={values.enabled ? values.types : 0}
onUpdate={(newTypes) => {
setFieldValue('types', newTypes);
setFieldTouched('types');
if (newTypes) {
setFieldValue('enabled', true);
}
}}
error={
errors.types && touched.types
? (errors.types as string)
: undefined
}
/>
<div className="actions">
<div className="flex justify-end">

View File

@@ -21,6 +21,7 @@ const messages = defineMessages({
toastSlackTestSuccess: 'Slack test notification sent!',
toastSlackTestFailed: 'Slack test notification failed to send.',
validationWebhookUrl: 'You must provide a valid URL',
validationTypes: 'You must select at least one notification type',
});
const NotificationsSlack: React.FC = () => {
@@ -41,6 +42,13 @@ const NotificationsSlack: React.FC = () => {
otherwise: Yup.string().nullable(),
})
.url(intl.formatMessage(messages.validationWebhookUrl)),
types: Yup.number().when('enabled', {
is: true,
then: Yup.number()
.nullable()
.moreThan(0, intl.formatMessage(messages.validationTypes)),
otherwise: Yup.number().nullable(),
}),
});
if (!data && !error) {
@@ -78,7 +86,15 @@ const NotificationsSlack: React.FC = () => {
}
}}
>
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
{({
errors,
touched,
isSubmitting,
values,
isValid,
setFieldValue,
setFieldTouched,
}) => {
const testSettings = async () => {
setIsTesting(true);
let toastId: string | undefined;
@@ -168,8 +184,20 @@ const NotificationsSlack: React.FC = () => {
</div>
</div>
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
currentTypes={values.enabled ? values.types : 0}
onUpdate={(newTypes) => {
setFieldValue('types', newTypes);
setFieldTouched('types');
if (newTypes) {
setFieldValue('enabled', true);
}
}}
error={
errors.types && touched.types
? (errors.types as string)
: undefined
}
/>
<div className="actions">
<div className="flex justify-end">

View File

@@ -105,7 +105,15 @@ const NotificationsTelegram: React.FC = () => {
}
}}
>
{({ errors, touched, isSubmitting, values, isValid, setFieldValue }) => {
{({
errors,
touched,
isSubmitting,
values,
isValid,
setFieldValue,
setFieldTouched,
}) => {
const testSettings = async () => {
setIsTesting(true);
let toastId: string | undefined;
@@ -232,6 +240,24 @@ const NotificationsTelegram: React.FC = () => {
<label htmlFor="chatId" className="text-label">
{intl.formatMessage(messages.chatId)}
<span className="label-required">*</span>
<span className="label-tip">
{intl.formatMessage(messages.chatIdTip, {
GetIdBotLink: function GetIdBotLink(msg) {
return (
<a
href="https://telegram.me/get_id_bot"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
code: function code(msg) {
return <code>{msg}</code>;
},
})}
</span>
</label>
<div className="form-input">
<div className="form-input-field">
@@ -254,8 +280,20 @@ const NotificationsTelegram: React.FC = () => {
</div>
</div>
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
currentTypes={values.enabled ? values.types : 0}
onUpdate={(newTypes) => {
setFieldValue('types', newTypes);
setFieldTouched('types');
if (newTypes) {
setFieldValue('enabled', true);
}
}}
error={
errors.types && touched.types
? (errors.types as string)
: undefined
}
/>
<div className="actions">
<div className="flex justify-end">

View File

@@ -8,7 +8,6 @@ import globalMessages from '../../../../i18n/globalMessages';
import Alert from '../../../Common/Alert';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import NotificationTypeSelector from '../../../NotificationTypeSelector';
const messages = defineMessages({
agentenabled: 'Enable Agent',
@@ -49,13 +48,11 @@ const NotificationsWebPush: React.FC = () => {
<Formik
initialValues={{
enabled: data.enabled,
types: data.types,
}}
onSubmit={async (values) => {
try {
await axios.post('/api/v1/settings/notifications/webpush', {
enabled: values.enabled,
types: values.types,
options: {},
});
mutate('/api/v1/settings/public');
@@ -73,7 +70,7 @@ const NotificationsWebPush: React.FC = () => {
}
}}
>
{({ isSubmitting, values, isValid, setFieldValue }) => {
{({ isSubmitting }) => {
const testSettings = async () => {
setIsTesting(true);
let toastId: string | undefined;
@@ -90,7 +87,6 @@ const NotificationsWebPush: React.FC = () => {
);
await axios.post('/api/v1/settings/notifications/webpush/test', {
enabled: true,
types: values.types,
options: {},
});
@@ -125,16 +121,12 @@ const NotificationsWebPush: React.FC = () => {
<Field type="checkbox" id="enabled" name="enabled" />
</div>
</div>
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
/>
<div className="actions">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">
<Button
buttonType="warning"
disabled={isSubmitting || !isValid || isTesting}
disabled={isSubmitting || isTesting}
onClick={(e) => {
e.preventDefault();
testSettings();
@@ -149,7 +141,7 @@ const NotificationsWebPush: React.FC = () => {
<Button
buttonType="primary"
type="submit"
disabled={isSubmitting || !isValid || isTesting}
disabled={isSubmitting || isTesting}
>
{isSubmitting
? intl.formatMessage(globalMessages.saving)

View File

@@ -55,6 +55,7 @@ const messages = defineMessages({
customJson: 'JSON Payload',
templatevariablehelp: 'Template Variable Help',
validationWebhookUrl: 'You must provide a valid URL',
validationTypes: 'You must select at least one notification type',
});
const NotificationsWebhook: React.FC = () => {
@@ -99,6 +100,13 @@ const NotificationsWebhook: React.FC = () => {
}
}
),
types: Yup.number().when('enabled', {
is: true,
then: Yup.number()
.nullable()
.moreThan(0, intl.formatMessage(messages.validationTypes)),
otherwise: Yup.number().nullable(),
}),
});
if (!data && !error) {
@@ -293,8 +301,20 @@ const NotificationsWebhook: React.FC = () => {
</div>
</div>
<NotificationTypeSelector
currentTypes={values.types}
onUpdate={(newTypes) => setFieldValue('types', newTypes)}
currentTypes={values.enabled ? values.types : 0}
onUpdate={(newTypes) => {
setFieldValue('types', newTypes);
setFieldTouched('types');
if (newTypes) {
setFieldValue('enabled', true);
}
}}
error={
errors.types && touched.types
? (errors.types as string)
: undefined
}
/>
<div className="actions">
<div className="flex justify-end">

View File

@@ -11,12 +11,11 @@ import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import { ALL_NOTIFICATIONS } from '../../../NotificationTypeSelector';
import NotificationTypeSelector from '../../../NotificationTypeSelector';
const messages = defineMessages({
discordsettingssaved: 'Discord notification settings saved successfully!',
discordsettingsfailed: 'Discord notification settings failed to save.',
enableDiscord: 'Enable Mentions',
discordId: 'User ID',
discordIdTip:
'The <FindDiscordIdLink>ID number</FindDiscordIdLink> for your user account',
@@ -34,8 +33,8 @@ const UserNotificationsDiscord: React.FC = () => {
const UserNotificationsDiscordSchema = Yup.object().shape({
discordId: Yup.string()
.when('enableDiscord', {
is: true,
.when('types', {
is: (value: unknown) => !!value,
then: Yup.string()
.nullable()
.required(intl.formatMessage(messages.validationDiscordId)),
@@ -51,8 +50,10 @@ const UserNotificationsDiscord: React.FC = () => {
return (
<Formik
initialValues={{
enableDiscord: !!data?.notificationTypes.discord,
discordId: data?.discordId,
types:
(data?.discordEnabledTypes ?? 0) &
(data?.notificationTypes.discord ?? 0),
}}
validationSchema={UserNotificationsDiscordSchema}
enableReinitialize
@@ -64,7 +65,7 @@ const UserNotificationsDiscord: React.FC = () => {
telegramChatId: data?.telegramChatId,
telegramSendSilently: data?.telegramSendSilently,
notificationTypes: {
discord: values.enableDiscord ? ALL_NOTIFICATIONS : 0,
discord: values.types,
},
});
addToast(intl.formatMessage(messages.discordsettingssaved), {
@@ -81,27 +82,23 @@ const UserNotificationsDiscord: React.FC = () => {
}
}}
>
{({ errors, touched, isSubmitting, isValid }) => {
{({
errors,
touched,
isSubmitting,
isValid,
values,
setFieldValue,
setFieldTouched,
}) => {
return (
<Form className="section">
{data?.discordEnabled && (
<div className="form-row">
<label htmlFor="enableDiscord" className="checkbox-label">
{intl.formatMessage(messages.enableDiscord)}
</label>
<div className="form-input">
<Field
type="checkbox"
id="enableDiscord"
name="enableDiscord"
/>
</div>
</div>
)}
<div className="form-row">
<label htmlFor="discordId" className="text-label">
<span>{intl.formatMessage(messages.discordId)}</span>
<span className="label-required">*</span>
{intl.formatMessage(messages.discordId)}
{!!data?.discordEnabledTypes && (
<span className="label-required">*</span>
)}
<span className="label-tip">
{intl.formatMessage(messages.discordIdTip, {
FindDiscordIdLink: function FindDiscordIdLink(msg) {
@@ -127,6 +124,20 @@ const UserNotificationsDiscord: React.FC = () => {
)}
</div>
</div>
<NotificationTypeSelector
user={user}
enabledTypes={data?.discordEnabledTypes ?? 0}
currentTypes={values.types}
onUpdate={(newTypes) => {
setFieldValue('types', newTypes);
setFieldTouched('types');
}}
error={
errors.types && touched.types
? (errors.types as string)
: undefined
}
/>
<div className="actions">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">

View File

@@ -1,5 +1,5 @@
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
@@ -12,13 +12,15 @@ import globalMessages from '../../../../i18n/globalMessages';
import Badge from '../../../Common/Badge';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import { ALL_NOTIFICATIONS } from '../../../NotificationTypeSelector';
import SensitiveInput from '../../../Common/SensitiveInput';
import NotificationTypeSelector, {
ALL_NOTIFICATIONS,
} from '../../../NotificationTypeSelector';
import { OpenPgpLink } from '../../../Settings/Notifications/NotificationsEmail';
const messages = defineMessages({
emailsettingssaved: 'Email notification settings saved successfully!',
emailsettingsfailed: 'Email notification settings failed to save.',
enableEmail: 'Enable Notifications',
pgpPublicKey: 'PGP Public Key',
pgpPublicKeyTip:
'Encrypt email messages using <OpenPgpLink>OpenPGP</OpenPgpLink>',
@@ -50,8 +52,8 @@ const UserEmailSettings: React.FC = () => {
return (
<Formik
initialValues={{
enableEmail: !!(data?.notificationTypes.email ?? true),
pgpKey: data?.pgpKey,
types: data?.notificationTypes.email ?? ALL_NOTIFICATIONS,
}}
validationSchema={UserNotificationsEmailSchema}
enableReinitialize
@@ -63,7 +65,7 @@ const UserEmailSettings: React.FC = () => {
telegramChatId: data?.telegramChatId,
telegramSendSilently: data?.telegramSendSilently,
notificationTypes: {
email: values.enableEmail ? ALL_NOTIFICATIONS : 0,
email: values.types,
},
});
addToast(intl.formatMessage(messages.emailsettingssaved), {
@@ -80,17 +82,17 @@ const UserEmailSettings: React.FC = () => {
}
}}
>
{({ errors, touched, isSubmitting, isValid }) => {
{({
errors,
touched,
isSubmitting,
isValid,
values,
setFieldValue,
setFieldTouched,
}) => {
return (
<Form className="section">
<div className="form-row">
<label htmlFor="enableEmail" className="checkbox-label">
{intl.formatMessage(messages.enableEmail)}
</label>
<div className="form-input">
<Field type="checkbox" id="enableEmail" name="enableEmail" />
</div>
</div>
<div className="form-row">
<label htmlFor="pgpKey" className="text-label">
<span className="mr-2">
@@ -107,8 +109,9 @@ const UserEmailSettings: React.FC = () => {
</label>
<div className="form-input">
<div className="form-input-field">
<Field
as="textarea"
<SensitiveInput
as="field"
type="textarea"
id="pgpKey"
name="pgpKey"
rows="10"
@@ -120,6 +123,19 @@ const UserEmailSettings: React.FC = () => {
)}
</div>
</div>
<NotificationTypeSelector
user={user}
currentTypes={values.types}
onUpdate={(newTypes) => {
setFieldValue('types', newTypes);
setFieldTouched('types');
}}
error={
errors.types && touched.types
? (errors.types as string)
: undefined
}
/>
<div className="actions">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">

View File

@@ -11,12 +11,11 @@ import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import { ALL_NOTIFICATIONS } from '../../../NotificationTypeSelector';
import NotificationTypeSelector from '../../../NotificationTypeSelector';
const messages = defineMessages({
telegramsettingssaved: 'Telegram notification settings saved successfully!',
telegramsettingsfailed: 'Telegram notification settings failed to save.',
enableTelegram: 'Enable Notifications',
telegramChatId: 'Chat ID',
telegramChatIdTipLong:
'<TelegramBotLink>Start a chat</TelegramBotLink>, add <GetIdBotLink>@get_id_bot</GetIdBotLink>, and issue the <code>/my_id</code> command',
@@ -36,8 +35,8 @@ const UserTelegramSettings: React.FC = () => {
const UserNotificationsTelegramSchema = Yup.object().shape({
telegramChatId: Yup.string()
.when('enableTelegram', {
is: true,
.when('types', {
is: (value: unknown) => !!value,
then: Yup.string()
.nullable()
.required(intl.formatMessage(messages.validationTelegramChatId)),
@@ -56,9 +55,9 @@ const UserTelegramSettings: React.FC = () => {
return (
<Formik
initialValues={{
enableTelegram: !!data?.notificationTypes.telegram,
telegramChatId: data?.telegramChatId,
telegramSendSilently: data?.telegramSendSilently,
types: data?.notificationTypes.telegram ?? 0,
}}
validationSchema={UserNotificationsTelegramSchema}
enableReinitialize
@@ -70,7 +69,7 @@ const UserTelegramSettings: React.FC = () => {
telegramChatId: values.telegramChatId,
telegramSendSilently: values.telegramSendSilently,
notificationTypes: {
telegram: values.enableTelegram ? ALL_NOTIFICATIONS : 0,
telegram: values.types,
},
});
addToast(intl.formatMessage(messages.telegramsettingssaved), {
@@ -87,21 +86,17 @@ const UserTelegramSettings: React.FC = () => {
}
}}
>
{({ errors, touched, isSubmitting, isValid }) => {
{({
errors,
touched,
isSubmitting,
isValid,
values,
setFieldValue,
setFieldTouched,
}) => {
return (
<Form className="section">
<div className="form-row">
<label htmlFor="enableTelegram" className="checkbox-label">
{intl.formatMessage(messages.enableTelegram)}
</label>
<div className="form-input">
<Field
type="checkbox"
id="enableTelegram"
name="enableTelegram"
/>
</div>
</div>
<div className="form-row">
<label htmlFor="telegramChatId" className="text-label">
{intl.formatMessage(messages.telegramChatId)}
@@ -166,6 +161,19 @@ const UserTelegramSettings: React.FC = () => {
/>
</div>
</div>
<NotificationTypeSelector
user={user}
currentTypes={values.types}
onUpdate={(newTypes) => {
setFieldValue('types', newTypes);
setFieldTouched('types');
}}
error={
errors.types && touched.types
? (errors.types as string)
: undefined
}
/>
<div className="actions">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">

View File

@@ -1,5 +1,5 @@
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import { Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
@@ -10,12 +10,13 @@ import { useUser } from '../../../../hooks/useUser';
import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import { ALL_NOTIFICATIONS } from '../../../NotificationTypeSelector';
import NotificationTypeSelector, {
ALL_NOTIFICATIONS,
} from '../../../NotificationTypeSelector';
const messages = defineMessages({
webpushsettingssaved: 'Web push notification settings saved successfully!',
webpushsettingsfailed: 'Web push notification settings failed to save.',
enableWebPush: 'Enable Notifications',
});
const UserWebPushSettings: React.FC = () => {
@@ -34,18 +35,18 @@ const UserWebPushSettings: React.FC = () => {
return (
<Formik
initialValues={{
enableWebPush: !!(data?.notificationTypes.webpush ?? true),
pgpKey: data?.pgpKey,
types: data?.notificationTypes.webpush ?? ALL_NOTIFICATIONS,
}}
enableReinitialize
onSubmit={async (values) => {
try {
await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, {
pgpKey: data?.pgpKey,
discordId: data?.discordId,
telegramChatId: data?.telegramChatId,
telegramSendSilently: data?.telegramSendSilently,
notificationTypes: {
webpush: values.enableWebPush ? ALL_NOTIFICATIONS : 0,
webpush: values.types,
},
});
mutate('/api/v1/settings/public');
@@ -63,21 +64,30 @@ const UserWebPushSettings: React.FC = () => {
}
}}
>
{({ isSubmitting, isValid }) => {
{({
errors,
touched,
isSubmitting,
isValid,
values,
setFieldValue,
setFieldTouched,
}) => {
return (
<Form className="section">
<div className="form-row">
<label htmlFor="enableEmail" className="checkbox-label">
{intl.formatMessage(messages.enableWebPush)}
</label>
<div className="form-input">
<Field
type="checkbox"
id="enableWebPush"
name="enableWebPush"
/>
</div>
</div>
<NotificationTypeSelector
user={user}
currentTypes={values.types}
onUpdate={(newTypes) => {
setFieldValue('types', newTypes);
setFieldTouched('types');
}}
error={
errors.types && touched.types
? (errors.types as string)
: undefined
}
/>
<div className="actions">
<div className="flex justify-end">
<span className="inline-flex ml-3 rounded-md shadow-sm">

View File

@@ -42,6 +42,18 @@ const UserNotificationSettings: React.FC = ({ children }) => {
regex: /\/settings\/notifications\/email/,
hidden: !data?.emailEnabled,
},
{
text: intl.formatMessage(messages.webpush),
content: (
<span className="flex items-center">
<CloudIcon className="h-4 mr-2" />
{intl.formatMessage(messages.webpush)}
</span>
),
route: '/settings/notifications/webpush',
regex: /\/settings\/notifications\/webpush/,
hidden: !data?.webPushEnabled,
},
{
text: 'Discord',
content: (
@@ -65,18 +77,6 @@ const UserNotificationSettings: React.FC = ({ children }) => {
regex: /\/settings\/notifications\/telegram/,
hidden: !data?.telegramEnabled || !data?.telegramBotUsername,
},
{
text: intl.formatMessage(messages.webpush),
content: (
<span className="flex items-center">
<CloudIcon className="h-4 mr-2" />
{intl.formatMessage(messages.webpush)}
</span>
),
route: '/settings/notifications/webpush',
regex: /\/settings\/notifications\/webpush/,
hidden: !data?.webPushEnabled,
},
];
settingsRoutes.forEach((settingsRoute) => {

View File

@@ -65,6 +65,8 @@ const UserSettings: React.FC = ({ children }) => {
text: intl.formatMessage(messages.menuNotifications),
route: data?.emailEnabled
? '/settings/notifications/email'
: data?.webPushEnabled
? '/settings/notifications/webpush'
: '/settings/notifications/discord',
regex: /\/settings\/notifications/,
},