mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat: other email notifications for approved/available
also adds UI to configure email notifications to frontend
This commit is contained in:
@@ -770,6 +770,36 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
webhookUrl:
|
webhookUrl:
|
||||||
type: string
|
type: string
|
||||||
|
NotificationEmailSettings:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
|
types:
|
||||||
|
type: number
|
||||||
|
example: 2
|
||||||
|
options:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
emailFrom:
|
||||||
|
type: string
|
||||||
|
example: no-reply@example.com
|
||||||
|
smtpHost:
|
||||||
|
type: string
|
||||||
|
example: 127.0.0.1
|
||||||
|
smtpPort:
|
||||||
|
type: number
|
||||||
|
example: 465
|
||||||
|
secure:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
|
authUser:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
authPass:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
cookieAuth:
|
cookieAuth:
|
||||||
@@ -1225,6 +1255,37 @@ paths:
|
|||||||
nextExecutionTime:
|
nextExecutionTime:
|
||||||
type: string
|
type: string
|
||||||
example: '2020-09-02T05:02:23.000Z'
|
example: '2020-09-02T05:02:23.000Z'
|
||||||
|
/settings/notifications/email:
|
||||||
|
get:
|
||||||
|
summary: Return current email notification settings
|
||||||
|
description: Returns current email notification settings in JSON format
|
||||||
|
tags:
|
||||||
|
- settings
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Returned email settings
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/NotificationEmailSettings'
|
||||||
|
post:
|
||||||
|
summary: Update email notification settings
|
||||||
|
description: Update current email notification settings with provided values
|
||||||
|
tags:
|
||||||
|
- settings
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/NotificationEmailSettings'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'Values were sucessfully updated'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/NotificationEmailSettings'
|
||||||
/settings/notifications/discord:
|
/settings/notifications/discord:
|
||||||
get:
|
get:
|
||||||
summary: Return current discord notification settings
|
summary: Return current discord notification settings
|
||||||
|
@@ -35,9 +35,10 @@ class EmailAgent implements NotificationAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getNewEmail() {
|
private getNewEmail() {
|
||||||
|
const settings = getSettings().notifications.agents.email;
|
||||||
return new Email({
|
return new Email({
|
||||||
message: {
|
message: {
|
||||||
from: 'no-reply@os.sct.dev',
|
from: settings.options.emailFrom,
|
||||||
},
|
},
|
||||||
send: true,
|
send: true,
|
||||||
transport: this.getSmtpTransport(),
|
transport: this.getSmtpTransport(),
|
||||||
@@ -55,9 +56,6 @@ class EmailAgent implements NotificationAgent {
|
|||||||
.filter((user) => user.hasPermission(Permission.MANAGE_REQUESTS))
|
.filter((user) => user.hasPermission(Permission.MANAGE_REQUESTS))
|
||||||
.forEach((user) => {
|
.forEach((user) => {
|
||||||
const email = this.getNewEmail();
|
const email = this.getNewEmail();
|
||||||
logger.debug('Sending email notification', {
|
|
||||||
label: 'Notifications',
|
|
||||||
});
|
|
||||||
|
|
||||||
email.send({
|
email.send({
|
||||||
template: path.join(
|
template: path.join(
|
||||||
@@ -74,6 +72,7 @@ class EmailAgent implements NotificationAgent {
|
|||||||
timestamp: new Date().toTimeString(),
|
timestamp: new Date().toTimeString(),
|
||||||
requestedBy: payload.notifyUser.username,
|
requestedBy: payload.notifyUser.username,
|
||||||
actionUrl: settings.applicationUrl,
|
actionUrl: settings.applicationUrl,
|
||||||
|
requestType: 'New Request',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -87,6 +86,72 @@ class EmailAgent implements NotificationAgent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async sendMediaApprovedEmail(payload: NotificationPayload) {
|
||||||
|
const settings = getSettings().main;
|
||||||
|
try {
|
||||||
|
const email = this.getNewEmail();
|
||||||
|
|
||||||
|
email.send({
|
||||||
|
template: path.join(
|
||||||
|
__dirname,
|
||||||
|
'../../../templates/email/media-request'
|
||||||
|
),
|
||||||
|
message: {
|
||||||
|
to: payload.notifyUser.email,
|
||||||
|
},
|
||||||
|
locals: {
|
||||||
|
body: 'Your request for the following media has been approved:',
|
||||||
|
mediaName: payload.subject,
|
||||||
|
imageUrl: payload.image,
|
||||||
|
timestamp: new Date().toTimeString(),
|
||||||
|
requestedBy: payload.notifyUser.username,
|
||||||
|
actionUrl: settings.applicationUrl,
|
||||||
|
requestType: 'Request Approved',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Mail notification failed to send', {
|
||||||
|
label: 'Notifications',
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendMediaAvailableEmail(payload: NotificationPayload) {
|
||||||
|
const settings = getSettings().main;
|
||||||
|
try {
|
||||||
|
const email = this.getNewEmail();
|
||||||
|
|
||||||
|
email.send({
|
||||||
|
template: path.join(
|
||||||
|
__dirname,
|
||||||
|
'../../../templates/email/media-request'
|
||||||
|
),
|
||||||
|
message: {
|
||||||
|
to: payload.notifyUser.email,
|
||||||
|
},
|
||||||
|
locals: {
|
||||||
|
body: 'Your requsested media is now available!',
|
||||||
|
mediaName: payload.subject,
|
||||||
|
imageUrl: payload.image,
|
||||||
|
timestamp: new Date().toTimeString(),
|
||||||
|
requestedBy: payload.notifyUser.username,
|
||||||
|
actionUrl: settings.applicationUrl,
|
||||||
|
requestType: 'Now Available',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Mail notification failed to send', {
|
||||||
|
label: 'Notifications',
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async send(
|
public async send(
|
||||||
type: Notification,
|
type: Notification,
|
||||||
payload: NotificationPayload
|
payload: NotificationPayload
|
||||||
@@ -97,6 +162,12 @@ class EmailAgent implements NotificationAgent {
|
|||||||
case Notification.MEDIA_PENDING:
|
case Notification.MEDIA_PENDING:
|
||||||
this.sendMediaRequestEmail(payload);
|
this.sendMediaRequestEmail(payload);
|
||||||
break;
|
break;
|
||||||
|
case Notification.MEDIA_APPROVED:
|
||||||
|
this.sendMediaApprovedEmail(payload);
|
||||||
|
break;
|
||||||
|
case Notification.MEDIA_AVAILABLE:
|
||||||
|
this.sendMediaAvailableEmail(payload);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@@ -65,6 +65,7 @@ interface NotificationAgentDiscord extends NotificationAgent {
|
|||||||
|
|
||||||
interface NotificationAgentEmail extends NotificationAgent {
|
interface NotificationAgentEmail extends NotificationAgent {
|
||||||
options: {
|
options: {
|
||||||
|
emailFrom: string;
|
||||||
smtpHost: string;
|
smtpHost: string;
|
||||||
smtpPort: number;
|
smtpPort: number;
|
||||||
secure: boolean;
|
secure: boolean;
|
||||||
@@ -120,6 +121,7 @@ class Settings {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
types: 0,
|
types: 0,
|
||||||
options: {
|
options: {
|
||||||
|
emailFrom: '',
|
||||||
smtpHost: '127.0.0.1',
|
smtpHost: '127.0.0.1',
|
||||||
smtpPort: 465,
|
smtpPort: 465,
|
||||||
secure: false,
|
secure: false,
|
||||||
|
@@ -364,4 +364,19 @@ settingsRoutes.post('/notifications/discord', (req, res) => {
|
|||||||
res.status(200).json(settings.notifications.agents.discord);
|
res.status(200).json(settings.notifications.agents.discord);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
settingsRoutes.get('/notifications/email', (req, res) => {
|
||||||
|
const settings = getSettings();
|
||||||
|
|
||||||
|
res.status(200).json(settings.notifications.agents.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
settingsRoutes.post('/notifications/email', (req, res) => {
|
||||||
|
const settings = getSettings();
|
||||||
|
|
||||||
|
settings.notifications.agents.email = req.body;
|
||||||
|
settings.save();
|
||||||
|
|
||||||
|
res.status(200).json(settings.notifications.agents.email);
|
||||||
|
});
|
||||||
|
|
||||||
export default settingsRoutes;
|
export default settingsRoutes;
|
||||||
|
@@ -1 +1 @@
|
|||||||
= `New Request: ${mediaName} - Overseerr`
|
= `${requestType}: ${mediaName} - Overseerr`
|
||||||
|
228
src/components/Settings/Notifications/NotificationsEmail.tsx
Normal file
228
src/components/Settings/Notifications/NotificationsEmail.tsx
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Field, Form, Formik } from 'formik';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import LoadingSpinner from '../../Common/LoadingSpinner';
|
||||||
|
import Button from '../../Common/Button';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import Axios from 'axios';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
save: 'Save Changes',
|
||||||
|
saving: 'Saving...',
|
||||||
|
});
|
||||||
|
|
||||||
|
const NotificationsEmail: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { data, error, revalidate } = useSWR(
|
||||||
|
'/api/v1/settings/notifications/email'
|
||||||
|
);
|
||||||
|
|
||||||
|
const NotificationsDiscordSchema = Yup.object().shape({
|
||||||
|
emailFrom: Yup.string().required(
|
||||||
|
'You must provide an email sender address'
|
||||||
|
),
|
||||||
|
smtpHost: Yup.string().required('You must provide an SMTP host'),
|
||||||
|
smtpPort: Yup.number().required('You must provide an SMTP port'),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data && !error) {
|
||||||
|
return <LoadingSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
enabled: data.enabled,
|
||||||
|
types: data.types,
|
||||||
|
emailFrom: data.options.emailFrom,
|
||||||
|
smtpHost: data.options.smtpHost,
|
||||||
|
smtpPort: data.options.smtpPort,
|
||||||
|
secure: data.options.secure,
|
||||||
|
authUser: data.options.authUser,
|
||||||
|
authPass: data.options.authPass,
|
||||||
|
}}
|
||||||
|
validationSchema={NotificationsDiscordSchema}
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
try {
|
||||||
|
await Axios.post('/api/v1/settings/notifications/email', {
|
||||||
|
enabled: values.enabled,
|
||||||
|
types: values.types,
|
||||||
|
options: {
|
||||||
|
emailFrom: values.emailFrom,
|
||||||
|
smtpHost: values.smtpHost,
|
||||||
|
smtpPort: values.smtpPort,
|
||||||
|
secure: values.secure,
|
||||||
|
authUser: values.authUser,
|
||||||
|
authPass: values.authPass,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// TODO show error
|
||||||
|
} finally {
|
||||||
|
revalidate();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ errors, touched, isSubmitting }) => {
|
||||||
|
return (
|
||||||
|
<Form>
|
||||||
|
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
||||||
|
<label
|
||||||
|
htmlFor="isDefault"
|
||||||
|
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
Agent Enabled
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<Field
|
||||||
|
type="checkbox"
|
||||||
|
id="enabled"
|
||||||
|
name="enabled"
|
||||||
|
className="form-checkbox rounded-md h-6 w-6 text-indigo-600 transition duration-150 ease-in-out"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
Email Sender Address
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||||
|
<Field
|
||||||
|
id="emailFrom"
|
||||||
|
name="emailFrom"
|
||||||
|
type="text"
|
||||||
|
placeholder="no-reply@example.com"
|
||||||
|
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.emailFrom && touched.emailFrom && (
|
||||||
|
<div className="text-red-500 mt-2">{errors.emailFrom}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
SMTP Host
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||||
|
<Field
|
||||||
|
id="smtpHost"
|
||||||
|
name="smtpHost"
|
||||||
|
type="text"
|
||||||
|
placeholder="localhost"
|
||||||
|
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.smtpHost && touched.smtpHost && (
|
||||||
|
<div className="text-red-500 mt-2">{errors.smtpHost}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
SMTP Port
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||||
|
<Field
|
||||||
|
id="smtpPort"
|
||||||
|
name="smtpPort"
|
||||||
|
type="text"
|
||||||
|
placeholder="465"
|
||||||
|
className="form-input block w-24 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.smtpPort && touched.smtpPort && (
|
||||||
|
<div className="text-red-500 mt-2">{errors.smtpPort}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
||||||
|
<label
|
||||||
|
htmlFor="isDefault"
|
||||||
|
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
Enable SSL
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<Field
|
||||||
|
type="checkbox"
|
||||||
|
id="secure"
|
||||||
|
name="secure"
|
||||||
|
className="form-checkbox rounded-md h-6 w-6 text-indigo-600 transition duration-150 ease-in-out"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
Auth User
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||||
|
<Field
|
||||||
|
id="authUser"
|
||||||
|
name="authUser"
|
||||||
|
type="text"
|
||||||
|
placeholder="localhost"
|
||||||
|
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
Auth Pass
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||||
|
<Field
|
||||||
|
id="authPass"
|
||||||
|
name="authPass"
|
||||||
|
type="password"
|
||||||
|
placeholder="localhost"
|
||||||
|
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 border-t border-gray-700 pt-5">
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||||
|
<Button
|
||||||
|
buttonType="primary"
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting
|
||||||
|
? intl.formatMessage(messages.saving)
|
||||||
|
: intl.formatMessage(messages.save)}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationsEmail;
|
@@ -26,7 +26,7 @@ const settingsRoutes: SettingsRoute[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Notifications',
|
text: 'Notifications',
|
||||||
route: '/settings/notifications',
|
route: '/settings/notifications/email',
|
||||||
regex: /^\/settings\/notifications/,
|
regex: /^\/settings\/notifications/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -10,9 +10,9 @@ interface SettingsRoute {
|
|||||||
|
|
||||||
const settingsRoutes: SettingsRoute[] = [
|
const settingsRoutes: SettingsRoute[] = [
|
||||||
{
|
{
|
||||||
text: 'General',
|
text: 'Email',
|
||||||
route: '/settings/notifications',
|
route: '/settings/notifications/email',
|
||||||
regex: /^\/settings\/notifications$/,
|
regex: /^\/settings\/notifications\/email/,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Discord',
|
text: 'Discord',
|
||||||
|
@@ -2,11 +2,14 @@ import { NextPage } from 'next';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import SettingsLayout from '../../../components/Settings/SettingsLayout';
|
import SettingsLayout from '../../../components/Settings/SettingsLayout';
|
||||||
import SettingsNotifications from '../../../components/Settings/SettingsNotifications';
|
import SettingsNotifications from '../../../components/Settings/SettingsNotifications';
|
||||||
|
import NotificationsEmail from '../../../components/Settings/Notifications/NotificationsEmail';
|
||||||
|
|
||||||
const NotificationsPage: NextPage = () => {
|
const NotificationsPage: NextPage = () => {
|
||||||
return (
|
return (
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
<SettingsNotifications>N/A</SettingsNotifications>
|
<SettingsNotifications>
|
||||||
|
<NotificationsEmail />
|
||||||
|
</SettingsNotifications>
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
Reference in New Issue
Block a user